Hook分为应用层(Ring3)Hook和内核层(Ring0)Hook,应用层Hook适用于x86和x64,而内核层Hook一般仅在x86平台适用,因为从Windows Vista的64版本开始引入的Patch Guard技术极大地限制了Windows x64内核挂钩的使用。
Ring3 Hook
IAT Hook
核心思路就是修改IAT中的函数地址
IAT(Import Address Table)Hook是一种针对Windows操作系统的API Hooking 技术,用于修改应用程序对动态链接库(DLL)中导入函数的调用。IAT是一个数据结构,其中包含了应用程序在运行时使用的导入函数的地址。
IAT Hook的原理是**通过修改IAT中的函数指针,将原本要调用的函数指向另一个自定义的函数。**这样,在应用程序执行时,当调用被钩子的函数时,实际上会执行自定义的函数。通过IAT Hook,我们可以拦截和修改应用程序的函数调用,以实现一些自定义的行为,比如记录日志、修改函数参数或返回值等。
IAT Hook的步骤通常包括以下几个步骤:
- 获取目标函数的地址:通过遍历模块的导入表,找到目标函数在DLL中的地址。
- 保存原始函数地址:将目标函数的地址保存下来,以便后续恢复。
- 修改IAT表项:将目标函数在IAT中对应的函数指针修改为自定义函数的地址。
- 实现自定义函数:编写自定义的函数,该函数会在被钩子函数被调用时执行。
- 调用原始函数:在自定义函数中,可以选择是否调用原始的被钩子函数。
该技术常用于实现一些系统级的功能,例如API监控、函数跟踪、代码注入等,接下来将具体分析IAT Hook
的实现原理,并编写一个DLL
注入文件,实现IAT Hook
替换MessageBox
弹窗的功能。
分析导入表
在PE加载过程中,双桥结构断裂之后,IAT中存放的就是函数的真实地址,我们修改这个地址也就是实现了Hook。
IAT结构
struct _IMAGE_THUNK_DATA32{
union {
DWORD ForwarderString; //指向一个转向者字符串的RVA
DWORD Function ; //被输入的函数的内存地址
DWORD Ordinal ; //被输入的API的序数值
DWORD AddressOfData ; //高位为0则指向IMAGE_IMPORT_BY_NAME 结构体二
}u1;
}IMAGE_THUNK_DATA32;
原来的调用过程:
call [aaaaa] => IAT[aaaaaa]::bbbbb => xxx.dll::bbbbb
ret后
next code of call [aaaaa] <= next ret of xxx.dll::bbbbb
实现原理和流程
原理
在进行Windows编程的时候,我们会经常使用Windows的API函数。而我们的API函数一般都是写在dll里,导入表里写着加载的dll和函数,代表该模块调用了哪些外部API,模块被加载到内存后, PE加载器会修改该表,地址改成外部API重定位后的真实地址, 我们只要直接把里面的地址改成我们新函数的地址, 就可以完成对相应API的Hook。
流程
1、在Dll里构造Hook函数(也就是我们自己的函数)
2、获取Target函数地址,并找到Target函数所在的IAT的地址
3、保存原始的IAT地址和IAT地址所存储的内容
4、修改IAT地址中的数据(前提:修改内存属性为可写)
5、恢复IAT
核心代码如下
Inline Hook
[原创]万字长文!inlinehook看这一篇足够了!-软件逆向-看雪-安全社区|安全招聘|kanxue.com
Windows下x86和x64平台的Inline Hook介绍 - PeacoorZomboss - 博客园 (cnblogs.com)
内联Hook相比于IAT Hook,显得更简单粗暴,它直接修改内存中任意函数的代码,将其劫持至Hook API。同时,它比IAT Hook的适用范围更广,因为只要是内存中有的函数它都能Hook,而后者只能Hook IAT表里存在的函数(有些程序会动态加载函数)。
核心就是跳板,跳转,本质是一种汇编代码修改技术
原理
Inline Hook的目标是系统函数,如下,左图是Hook之前的状态,procexp.exe进程调用ZwQuerySystemInformation()函数时,ZwQuerySystemInformation()的代码是正常的代码。右图是Hook后的状态,注意红框中的代码,ZwQuerySystemInformation()函数开头5个字节已被修改,变成了jmp 0x10001120,也就是我们恶意代码的地址,之后便可以开始我们的自定义操作。0x1000116A我们先进行unhook操作(脱钩),目的是将ZwQuerySystemInformation()的代码恢复。大家可能有疑惑,为什么刚修改完又要恢复回来,原因很简单,Hook的目的是当调用某个函数时,我们能劫持进程的执行流。现在我们已经劫持了进程的执行流,便可以恢复ZwQuerySystemInformation()的代码,以便我们的恶意代码可以正常调用ZwQuerySystemInformation()。执行完恶意代码后,再次挂钩,监控该函数。
代码实现
首先获取原API的地址,并保存在pfnOrg中,然后修改内存段属性为RWX,备份原有代码(以便后续代码恢复),实时计算JMP的相对偏移,最后修改API前5字节的代码,恢复内存属性。
HotFix Hook
从上节对Code Hook方法的讲解中,我们会发现Code Hook存在一个效率的问题,因为每次Code Hook都要进行“挂钩+脱钩”的操作,也就是要对API的前5字节修改两次,这样,当我们要进行全局Hook的时候,系统运行效率会受影响。而且,当一个线程尝试运行某段代码时,若另一个线程正在对该段代码进行“写”操作,这时就会程序冲突,最终引发一些错误。
有没有办法避免这种隐患呢?答案是有的,可以使用HotFix Hook(“热补丁”)方法。
原理
以上累出的API起始代码有如下两个明显的相似点:
[1]API代码以“MOV EDI,EDI”指令开始。
[2]API代码上方有5个NOP指令。
MOV EDI,EDI用于将EDI的值再次复制给EDI,这没有什么实际意义。也就是说,API起始代码的MOV指令(2个字节)与其上方的5个NOP指令(5个字节)合起来共7个字节的指令没有任何意义。所以我们就可以通过修改这7个字节来实现Hook操作。这种方法因为可以在进程处于运行状态时临时更改进程内存中的库文件,所以微软也常用这种方法来打“热补丁”。
直接修改汇编代码即可
将前7个字节改成:
JMP 10001000(恶意代码地址)
JMP SHORT 0x7C802366
代码实现
难点在与计算偏移地址
由于HotFix Hook需要修改7个字节的代码,所以并不是所有API都适用这种方法,若不适用,请使用5字节代码修改技术。
SetWindowsHook
原理
首先先来了解下常规的Windows消息流:
[1]发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS message queue]。
[2]OS判断哪个应用程序中发生了事件,然后从[OS message queue]取出消息,添加到相应应用程序的[application message queue]中。
[3]应用程序(如记事本)监视自身的[application message queue],发现新添加的WM_KEYDOWN消息后,调用相应的事件处理程序处理。
所以,我们只需在[OS message queue]和[application message queue]之间安装钩子即可窃取键盘消息,并实现恶意操作。
那么我们该如何安装这个消息钩子呢?很简单,Windows提供了一个官方函数SetWindowsHookEx()用于设置消息Hook,编程时只要调用该API就能简单地实现Hook。
消息Hook常被窃密木马用来监听用户的键盘输入,程序里只需写入如下代码就能对键盘消息进行Hook:
SetWindowsHookEx(
WH_KEYBOARD, //键盘消息
KeyboardProc, //钩子函数(处理键盘输入的函数)
hInstance, //钩子函数所在DLL的Handle
0 //该参数用于设定要Hook的线程ID,为0时表示监视所有线程
)
核心代码
该API在简单高效的同时也有一个弊端,就是它只能监视较少的消息,如:击键消息、鼠标移动消息、窗口消息。
DebugActiveProcess
该Hook方法的原理跟调试器的工作机制相似,核心思想都是让进程发生异常,然后自己捕获到该异常,对处于被调试状态下的级才能进行恶意操作。
下图是常规进程的异常事件处理,当进程未被其他进程调试时,其默认异常事件处理者是OS,一旦进程发生异常,OS将捕获到该异常,并进行相应的事件处理。
若进程被另一个进程调试了(如OllyDbg),异常事件的处理工作将移交给调试者,比如进程发生了除0错误,OllyDbg将接收到这个异常事件并对进行相应处理。
PS:调试器无处理或不关心的调试事件最终由OS处理。
所以,调试Hook的核心思路就是将API的第一个字节修改为0xCC(INT 3),当API被调用时,由于触发了异常,控制权就被转交给调试器。
核心代码
Ring0 Hook
SSDT Hook
SSDT Hook属于内核层Hook,也是最底层的Hook。由于用户层的API最后实质也是调用内核API(Kernel32->Ntdll->Ntoskrnl),所以该Hook方法最为强大。
原理
内核通过SSDT(System Service Descriptor Table)调用各种内核函数,SSDT就是一个函数表,只要得到一个索引值,就能根据这个索引值在该表中得到想要的函数地址。
下图0x80563520处就是ntoskrnl对应的服务描述符表结构SSDT。那么第一个32位的0x804e58a0则是SSDT Base,即SSDT的首地址。
通过对这些地址反汇编,就能得到相应的函数,下图中0x80591bfb是SSDT表中的第一个函数NtAcceptConnectPort的地址。
我们接下来试着寻找NtQuerySystemInformation的地址,首先反汇编ZwQuerySystemInformation,得知它要寻找SSDT中索引号为0xAD的地址。
从上面我们可以知道,NtQuerySystemInformation的索引号为0xAD,那么我们就可以算出NtQuerySystemInformation的地址:
0x80591bfb + 0xAD = 0x8056ff1
流程
Hook的原理相同,只不过Hook的对象不一样罢了。Hook步骤还是那5步:
1.修改内存属性为RWX。
2.拼接汇编码jmp [HookFunc]。
3.保存原代码头5个字节。
4.将头5个字节替换为2的汇编码。
5.恢复前5个字节。
6.恢复内存属性。