HOOK相关原理与例子

2019/04/10 10:10
阅读数 23

消息HOOK

原理:

1. 用户输入消息,消息被放到系统消息队列。

2. 程序发生了某些需要获取输入的事件,就从系统消息队列拿出消息放到程序消息队列中。

3. 应用程序检测到有新的消息进入到程序消息队列中后,调用相应的事件去处理该消息。

所以在系统消息队列与程序消息队列的中间安装hook,即可获取消息队列中的信息。

安装:

SetWindowsHookEx(键盘消息(WH_xxx),Hook函数(处理键盘输入的函数),句柄(hook函数所在的DLL的句柄),线程ID(要hook的线程ID,0为所有线程))

API在简单高效的同时也有一个弊端,就是它只能监视较少的消息,如:击键消息、鼠标移动消息、窗口消息。

 

SEH(调试)HOOK

原理:与调试器工作方式类似,让进程发生异常,然后自己捕获到异常,对于除于被调试状态下的级进行操作。

1. 正常情况下,进程未被其他进程调试时,当进程发生异常事件,系统将捕获该事件,并进行事件处理。

2. 当进程被其他进程调试时,处理该进程的异常事件的工作则交给了调试进程。(调试进程未处理或不关心的调试事件由系统处理)

3. 调试HOOK的核心思路就是将API的第一个字节修改为0xCC(INT 3,留给调试工具的中断,调试工具运行完后,会将下一条指令手动替换回原先的代码),当API被调用时,由于触发了异常,控制权就被转交给调试器(调试进程)。

利用调试技术来HOOK API函数的相关步骤如下

· 1对想要钩取的进程进行附加操作,使之成为被调试者。

· 2将要钩取的API的起始地址的第一个字节修改为0xcc(或者使用硬件断点)

· 3当调用目标API的时候,控制权就转移到调试器进程。

· 4执行需要的操作。

· 5脱钩,将API 函数的第一个字节恢复。

· 6运行相应的API

 

注入HOOK

原理:Hook的核心思想就是修改API的代码,使用DLL注入技术,我们将Hook的代码写入一个DLL(或直接一个shellcode),将此DLL注入到目标进程中,此时因为DLL在目标进程的内存中,所以就有权限直接修改目标进程内存中的代码了。

shellcode:填充数据,利用软件漏洞而执行的代码,可在暂存器eip溢出后,塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。

API HOOK:又分为IAT HOOK和inline HOOK,都是改变函数以达到跳转到自己的HOOK API的目的。但又有差别。

一、

1.IAT HOOK:通过修改IAT(导入表)的函数地址,实现对API进行HOOK。在把原函数替换成目标函数,并在目标函数执行完后,必须要调用回原函数(HOOK前应保存原函数地址),这样才能保证功能的完整性。

· 1.计算出导入表的位置

· 2.在导入表中找到原函数的位置(即将被执行的函数),保存该函数的地址

· 3.将原函数地址改为目标函数,运行完目标函数后,调用回原函数。

注:该操作会将程序中所有调用被hook的函数变为调用hook后的函数,可加判断,如果为自己调用操作,则进行hook函数的处理,如果是系统调用,直接将数据调用给原函数去处理

 

2.inline HOOK:直接修改内存中任意函数的代码,将其劫持至Hook API。同时,它比IAT Hook的适用范围更广,因为只要是内存中有的函数它都能Hook,而后者只能Hook IAT表里存在的函数(有些程序会动态加载函数)。

inline HOOK的目标是系统函数,直接修改函数的前5字节,改为jmp目标函数地址,执行完后进行unhook(脱钩)操作,以便将原函数恢复。Hook的目的是当调用某个函数时,我们能劫持进程的执行流。现在我们已经劫持了进程的执行流,便可以恢复原函数代码,以便我们的恶意代码可以正常调用。

· 1.获取原API地址,保存起来(方便后续还原)

· 2.修改内存属性为RWX(即read(编号4)write(2)execute执行(1)

· 3.备份原代码(同1

· 4.实时计算JMP的相对偏移

· 5.最后修改API5字节的代码(跳转到目标函数地址)

· 6.恢复内存属性。

注:

自编inline hook:从hook的位置跳到自己的函数中执行想要的动作,但需要将原指令再执行一遍,再跳转回到被hook的位置+指令长度的位置。以达成栈平衡的目的。

如果hook的位置为跳转指令,则需要将该指令所跳转的目标地址给计算并保存起来(普通跳转指令(非FF15 FF25 push时),跳转的为偏移量,一旦hook了偏移量就不再适用了),并在自己的函数中跳转到目标地址。

mhook库:编写一个参数 返回类型与被HOOK函数一样的新函数,hook后,代替原函数被调用,注意该hook只能hook一整个函数,并且会将所以调用原函数的行为用于调用新函数,因此应该加个判断条件,判断是系统进行调用还是我们要hook的程序进行调用。

二、

HotFix HOOK

原理:Code Hook存在一个效率的问题,因为每次Code Hook都要进行“挂钩+脱钩”的操作(对API的前5字节修改两次),当要进行全局Hook的时候,系统运行效率会受影响。而且,当一个线程尝试运行某段代码时,若另一个线程正在对该段代码进行“写”操作,会程序冲突,最终引发一些错误。

API的起始代码上都有这样的特点,5个NOP(空)指令,1个“MOV EDI,EDI”(占2字节),这7字节的指令实际没有任何意义,因此可以通过修改这7字节来实现HOOK操作,这种方法可以使得进程处于运行状态时临时更改进程内存中的库文件,因此被称为打“热补丁”。

在上述5字节代码修改技术中,unhook(脱钩)是为了调用原函数,但使用HotFix HOOK API时,在API代码被修改的状态下仍然能够正常的调用原API(从[原API起始地址+2]开始,仍能正常调用原API,且 执行动作一致)。

· 1.将内存属性修改为RWX

· 2.计算HOOK函数与被HOOK函数之间的地址偏移

· 3.将JMP [得到的结果]写入原函数-5的位置(即5NOP

· 4.再将JMP-7写到原函数的位置(MOV EDI,EDI)

· 5.恢复内存属性。

由于HotFix Hook需要修改7个字节的代码,所以并不是所有API都适用这种方法,若不适用,请使用5字节代码修改技术。

 

SSDT HOOK

原理:SSDT Hook属于内核层Hook,也是最底层的Hook。由于用户层的API最后实质也是调用内核API(Kernel32->Ntdll->Ntoskrnl),所以该Hook方法最为强大。

SSDT(System Service Descriptor Table):系统服务描述符表

定义类型变量:

DD(Define Dword):双字类型,一个双字数据占4字节。DW:字类型,占2字节。DB:字节类型,占1字节

内核通过SSDT调用各种内核函数,SSDT就是一个函数表,只要得到一个索引值,就能根据这个索引值在该表中得到想要的函数地址。

SSDT所在地址后面的第一个32位数据即为SSDT的基地址,跳到基地址后,第一个位32位数据即为SSDT表中第一个函数的地址,对该地址反汇编后,就能得到该函数相关的信息(包括该函数的索引号)。

例:要找AABBCC函数的地址,先对该函数进行反汇编u nt!ZwAABBCC,得到它的索引号为0x12;那么它的地址为:基地址+0x12 对其反汇编后,即可得到该函数的详细信息。

· 1.修改内存属性为RWX(即read(编号4)write(2)execute执行(1)

· 2.实时计算JMP的相对偏移

· 3.备份原代码头5字节(同1

· 4.将头5字节替换成2.的汇编码

· 5.运行完后还原头5字节

· 6.恢复内存属性。

 

 

 

 

 

 

例子及博主自身理解:

API HOOK——IAT HOOK实例

技巧:

通过模块句柄,得到PE头:(PBYTE)PE=(PBYTE)进程句柄

通过PE头,获得dos头:PIMAGE_DOS_HEADER  dos=(PIMAGE_DOS_HEADER)PE

通过PE头和dos头,获得NT PIMAGE_NT_HEADERS NT=(PIMAGE_NT_HEADERS)(PE+ dos->e_lfanew)

通过NT头的结构成员的数组成员,获得导入表信息:

IMAGE_DATA_DIRECTORY 数据表=NT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]

导入表的尺寸=数据表.size 起始地址(基址)=数据表.VirtualAddress

 获取函数的地址(DWORD),GetProcAddress(GetModuleHandleA(DllName),ProcName

通过PE头+导入表基址定位到导入表:PIMAGE_IMPORT_DESCRIPTOR 导入表 = (PIMAGE_IMPORT_DESCRIPTOR)(PE + 导入表基址);

通过对比DLL名 循环查找每个导入表,找到DLL所在的结构体

 strcmp((char*)DLL名,(char*)(PE+导入表-name))找到目标DLL所在结构体; PIMAGE_THUNK_DATA 结构体= (PIMAGE_THUNK_DATA)(PE + 导入表->FirstThunk);

通过对比函数地址,循环查找每个结构体的u1.Function成员,找到目标函数结构体->u1.Function == (DWORD)要被替换的函数地址

!!切记,应在32位下的Release版本生成dll,否则替换新的函数地址处会出错

找到目标函数位置后,替换成新的函数的地址,pthunk->u1.Function = NewFuncAddress;

然后将位置(这位置保存的是地址)保存起来,方便事后还原。(保存至全局变量)

DWORD g_dwIatAddr = (DWORD)&pthunk->u1.Function;(<-保存一个指针指向的变量所保存的地址)

事后还原:DWORD * pdwAddr = (DWORD*)g_dwIatAddr;//把旧地址所在的位置传递给一个指针

*pdwAddr = oldFuncAddress; //将旧地址放入该位置 达到还原

 

注意,每次更改导入表的函数地址时,都应该调用VirtualProtect来修改保护属性

VirtualProtect((LPVOID)&pthunk->u1.Function, 4, PAGE_EXECUTE_READWRITE, &oldprotect);

 

 

通过REG注入DLL (即通过注册表HOOK)

平时用程序来进行DLL注入时,需要被注入程序在运行起来之后才能进行注入,在某些初始化阶段就想通过HOOK来控制程序时,达不到效果。此时,我们可以通过注册表表进行注入,被注入的程序在运行前就已经被HOOK。注:被注入的进程与DLL必须同为32/64位。

REG注入原理:利用在Windows 系统中,当REG以下键值中存在有DLL文件路径时,会跟随EXE文件的启动加载这个DLL文件路径中的DLL文件。当如果遇到有多个DLL文件时,需要用逗号或者空格隔开多个DLL文件的路径。

1.通过WIN+R运行regedit打开注册表

2.分别打开:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows

把路径下的AppInit_DLLs改为将要注入的DLL路径;

3.并将LoadAppInit_DLLs注册表项的值修改为1:(64位改2个 32位改1个)

注:注入表注入的DLL由mHOOK生成,因为注入表HOOK会给所有进程HOOK

所以要加入限制条件:获取当前进程名,当进程名=目标进程名时,往下执行HOOK的操作

 

自写inline hook

 

1.自定义一个全局变量 让hook只运行一次

2.通过进程IDGetProcessId(HANDLE(-1))得到当前进程ID 和程序名(自定义),获得目标进程的模块入口地址(即HOOK进程的基地址)(要HOOK的地方属于哪个模块,就获取哪个模块的句柄)

2/1.直接GetModuleHandle("进程名");获得模块入口地址或者通过下面4个步骤

2-1.根据进程ID获得进程快照,该快照包含所有包含该进程ID的模块 CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid); 

2-2.通过该快照,获得与该进程相关的第一个模块的信息 Module32First(2-1返回的句柄, &存放模块信息的结构)

2-3.通过对比,找到目标模块srcmp(存放模块信息的结构.szModule, 进程名(即1的程序名)) == 0) ,如果不匹配,Module32Nex(2-1返回的句柄, &存放模块信息的结构)查看下一个。(进程名可通过任务管理器-详细信息查看)

2-4.找到目标模块,返回模块句柄存放模块信息的结构.hModule

 

3.得到要替换的原指令的绝对偏移位置RVA=VA-IB(要被替换的第一条指令—模块入口地址) (可通过MDebug查看,如要HOOK的为自定义函数,则该模块名称为进程名.exe,如果要HOOK的为系统函数,则模块名为该系统函数所在的DLL)(找到合适的HOOK位置是逆向的关键)

4.得到当前要hook的指令所在位置。原指令的绝对偏移地址(3得到的)+基地址(不定,每次加载时都容易改变,所以要通过2来求得)

5.保存等下要返回的地址 返回地址= 当前要hook的指令所在位置+hook指令的长度(5字节或以上,替换掉了多长就加多长)

6.保存原始指令内容UCHAR szOldInstruct[指令长度]={指令内容},指令内容为从左到右从上到下保存。 如不还原,则可不保存

7.计算跳转位置。DWORD 跳转位置 = (DWORD)自己编写函数(汇编编写)- 要hook的指令的起始位置- 指令所占长度;这样跳转后即能略过原本指令,执行自定义指令(远跳转指令e9占5字节,近跳转EB占2字节,跳转的位置为偏移量)

偏移量=目标地址 - 指令地址 - 指令长度

8.编写跳转指令UCHAR 跳转指令[5] = { 0xe9, 0, 0, 0, 0 };e9为跳转指令jmp),如果指令长于5字节,则在地址后填充nop空指令(0x90)

也可定死为5字节,因为跳转指令由e9+4字节地址组成,而返回地址自动跳过了该指令的位置,所以只改前面5字节数据亦可

memcpy(跳转指令 + 1(e9占1位), &(3得到的), sizeof(3得到的));

9.修改要hook的指令所在位置的保护属性 VirtualProtect((LPVOID)(4得到的),, 5, PAGE_EXECUTE_READWRITE, &保存旧属性的变量);

10.将要hook的指令替换成要跳转的指令 memcpy((LPVOID)(4得到的), (8得到的), sizeof((8得到的)));

11.还原指令所在位置的保护属性 此时inline hook完成

 

注:跳转指令所指的函数必须为汇编指令编写

 

 

通过上面例子生成DLL后,通过注入HOOK实例

在生成完DLL后(具体参照前面例子),如有.lib文件 需拉到同一目录并加载

 

_T在项目定义了UNICODE时,等价于L,即(_T"123")同理(L"123"); 否则就是多字节。

在数组中 TCHAR如果定义了宽字节UNICODE TCHAR就为wchar_t 否则为char

DLL所在地址可用UnICODE,方便处理汉字这种双字节字符。

 

1.搜索指定类名或窗口名(窗口标题)findwindow(类名,窗口名);,得到窗口句柄。

2.GetWindowThreadProcessId(窗口句柄,&保存ID的变量);得到该进程的ID。

3.OpenProcess(PROCESS_ALL_ACCESS,FALSE,进程ID);得到该进程的句柄

4.VirtualAllocEx(进程句柄,内存地址,内存大小,分配方式(MEM_COMMIT),权限(PAGE_READWRITE));在该进程中申请一个内存空间(用于打开DLL)

5.将DLL路径写入对方进程中

WriteProcessMemory (进程句柄(3),内存位置(4),DLL所在地址(绝对路径),路径所占空间,&写入字节数)

注意,此时的路径含有汉字,用的为宽字符声明:L"c\\xxx",所以可用wcslen( )求得长度,长度*2+2(结束符) 即得到宽字符路径所占空间

6.创建远程线程,让目标进程调用LoadLibrary来打开注入的DLL

CreateRemoteThread(进程句柄,安全属性(NULL),线程大小(0,即默认),目标线程调用的函数名(函数名即函数起始位置),

传递给函数的参数(此处为4申请的空间,该空间存放着DLL的路径),线程创建标志(NULL),线程ID的指针(不需要保存则为NULL) )

这里传递的函数名为LoadLibrary 用来加载DLL

7.检测句柄的信号状态WaitForSingleObject (线程句柄(6),等待时间(-1无限));当参1现场为有信号状态,或者到了参2时间,该函数返回。否则挂起

8.释放目标进程的空间VirtualFreeEx(进程句柄(3),空间首地址(4),空间大小(与申请时一致),释放类型(MEM_DECOMMIT));

注意:可直接跳到步骤3开始执行 进程ID从任务管理器-详细信息-目标进程的PID中获得

 

 

 

小技巧:如果想实现嵌套hook 应该保存被hook的函数指令,然后在hook的函数里面 用函数再次实现该指令,如果为jmp指令,则需获得跳转指令后接的4字节偏移地址 :因为跳转指令后接的为偏移量,而hook位置的地址为该指令所在的地址,并非偏移量

具体操作如下:1.memcpy(g_szOldInstruct, (char*)(HOOK的位置), 5); //将hook位置的指令保存下来(指令多长就复制多长 如果是jmp的话一般都是5字节E9 或2字节EB)

2.lea eax, dword ptr[g_szOldInstruct]; //取出这行指令的值(为E9 XXXX XXXX)

3 add eax, 1; mov eax, dword ptr[eax]; //+1为跳过e9,然后取4字节则为取出跳转的偏移地址

得到了偏移地址 就可以求得被hook的指令所要实现的跳转位置 跳转的目标地址=偏移地址+跳转指令长度+跳转指令所在的位置(即被hook前 这行指令所在的位置)

 

调试DLL是否执行成功,可在生成DLL的函数的代码段添加__asm int 3;来进行调试

在下断点后,如果运行起来一直会回到断点处,说明系统在该程序运行时会一直调用断点处的函数,此时应在hook函数前加判断条件,如果是自己操作的HOOK下来,如果是系统调用的,就调用原函数

 

 

原文出处:https://www.cnblogs.com/aaaguai/p/12628329.html

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部