5.2 内联钩子——Inline Hook
5.2.1 Inline Hook的原理
API函数都保存在操作系统提供的DLL文件中,当在程序中使用某个API函数时,在运行程序后,程序会隐式地将API所在的DLL加载入进程中。这样,程序就会像调用自己的函数一样调用API,大体过程如图5-1所示。
从图5-1中可以看出,在进程中当EXE模块调用CreateFile()函数的时候,会去调用kernel32. dll模块中的CreateFile()函数,因为真正的CreateFile()函数的实现在kernel32. dll模块中。
CreateFile()是API函数,API函数也是由人编写的代码再编译而成的,也有其对应的二进制代码。既然是代码,那么就可以被修改。通过一种“野蛮”的方法来直接修改API函数在内存中的映像,从而对API函数进行HOOK。使用的方法是,直接使用汇编指令的jmp指令将其代码执行流程改变,进而执行我们的代码,这样就使原来的函数的流程改变了。执行完我们的流程以后,可以选择性地执行原来的函数,也可以不继续执行原来的函数。
假设要对某进程的kernel32. dll的CreateFile()函数进行HOOK,首先需要在指定进程中的内存中找到CreateFile()函数的地址,然后修改CreateFile()函数的首地址的代码为jmpMyProc的指令。这样,当指定的进程调用CreateFile()函数时,就会首先跳转到我们的函数当中去执行流程,这样就完成了我们的HOOK了。看一下它的流程图,如图5-2所示。
图5-1 调用API函数的大体过程
图5-2 Inline Hook的流程
由于这种方法是在程序流程中直接进行嵌入jmp指令来改变流程的,所以就把它叫做Inline Hook.
5.2.2 Inline Hook的实现
了解了大体的HOOK流程后,现在来学习它的具体实现。
我们的C程序被编译连接后为一个二进制文件,在二进制文件中,对于代码部分来说,都是CPU可以用来执行的机器码,机器码和汇编指令又是一一对应的。前面讲过了,Inline Hook是在程序中嵌入jmp汇编指令然后跳转到流程处继续执行的,jmp指令的用法是jmp目的地址。jmp在汇编语言中是一个无条件的跳转指令,jmp后面跟随的参数是跳转的目的地址。用OD随便打开一个程序,并且修改它的某条指令为jmp指令。跳转的目的为一个任意地址,如图5-3和图5-4所示。
图5-3 准备修改00401600地址处的代码为jmp指令
图5-4 修改后的代码内容
从图5-3和图5-4的对比可以看出,jmp指令占用了5个字节。原来从00401600到00401605处的机器码为55 8BEC6AFF,当修改为jmp 12345678后,现在的机器码为E9 73 40 F4 11。可以告诉大家,jmp对应的机器码是E9(针对长转移来说的),后面的73 40 F4 11是一个偏移量,这个偏移量是多少呢?这个偏移量是11F44073,请回忆一下前面提到过的字节顺序的问题。偏移量的计算公式如下:
JMP后的偏移量=目标地址-原地址-5
这是一个非常重要的公式,当然对于我们的使用只要记住就可以了,这里的5是JMP的指令长度,也就是说JMP XXXXXXXX这个指令的机器码长度为5个字节。验证一下这个公式,目标地址是12345678,原地址为00401600,用12345678-00401600-5 = 11F44073,用计算器进行计算,如图5-5所示。
上面地址都是用十六进制进行计算的,大家计算时要注意这一点,以免计算错误。通过上面的例子可以看出来,修改时只需要修改5个字节就可以了。下面来梳理一下Inline Hook的流程吧。流程如下。
(1)构造跳转指令。
(2)在内存中找到欲HOOK函数地址,并保存欲HOOK位置处的前5个字节。
(4)当被HOOK位置被执行时会转到我们的流程执行。
(5)如果要执行原来的流程,那么取消HOOK,也就是还原被修改的字节。
(6)执行原来的流程。
(7)继续HOOK住原来的位置。
图5-5 偏移量计算
这就是Inline Hook的大概的流程。
由于Inline Hook的实现代码比较简单,关键就是一个HOOK和一个取消HOOK的过程,因此可用C++封装一个Inline Hook的类,在今后Inline Hook编程中,可以始终使用这个封装好的类。
一般情况下封装类都有两个文件,一个是类的头文件;另一个是类的实现文件。在Windows下,类名(Class Name)都是以“C”开头的,我们封装的是Inline Hook类,因此类名是CILHook。为了保持一致性,类的头文件和实现文件分别是ILHook.h文件和 ILHook.cpp文件。先来看一下ILHook.h文件中的类定义部分。
在C++中,类的定义使用关键字“Class”。在类中定义有成员函数和成员变量,通常情况下把成员函数放在上面,把成员变量放在下面。因为对于拿到头文件的人来说,他首先关注的是类实现了哪些功能,因此应该让他第一眼就能看到实现了的成员函数,当然这不是必须的。
回到类定义,在类中除了构造函数和析构函数以外,还定义了3个成员函数,分别是 Hook()、UnHook()和ReHook()函数。它们的功能分别是用来进行HOOK操作、取消HOOK操作和重新进行HOOK操作的。对于3个成员函数来说,这里只是一个定义,实现部分在 ILHook.cpp中。
除了上面的3个成员函数外,还定义了3个成员变量,分别是m_pfnOrig、bO1dBytes[5]和bNewBytes[5]。这3个函数的作用已经在定义中给出了注释,想必大家应该能明白,这里就不具体说了。接着看ILHook.cpp文件中的实现代码吧。
在构造函数中主要是完成对成员变量的初始化工作,在析构函数中主要是取消HOOK。构造函数在C++对象被创建时自动执行,同样析构函数是在C++对象被销毁时自动执行。
该函数是InlineHook类的重要函数,在Hook()成员函数中,我们完成了3项工作,首先是获得了被HOOK函数的函数地址;接下来是保存了函数的前5个字节;最后是用构造好的跳转指令来修改被HOOK函数的前5个字节的内容。
除了上面的函数外,还有两个函数,分别是取消挂钩和重新挂钩两个函数。这两个函数非常简单,就是完成修改内存属性、复制字节的工作。代码如下:
上面两个成员函数就不进行介绍了,只要大家看懂了Hook()函数的实现,这两个函数的功能就肯定理解了。
整个Inline Hook的封装已经完成了,在后面的代码中,可以很容易地实现对函数的HOOK功能了。
5.2.3 HOOK MessageBoxA
本小节将完成一个HOOK本进程MessageBoxA()的程序,这个程序的目的是测试我们的类是否封装成功,以便完成今后的程序。在VC6下创建一个控制台程序,添加好封装过的库,然后键入下面的代码:
在主函数中,调用了两次MessageBox()函数,两次弹出的文本内容是一样的。但是第二次调用MessageBox()函数时,对MessageBox()做了HOOK, HOOK的函数是我们自己写的 MyMessageBoxA()函数。在MyMessageBoxA()函数中首先恢复了对MessageBox()函数的HOOK,然后连续调用了两次MessageBox()函数。那么,这个测试程序应该弹出3次 MessageBox()对话框。大家将其编译连接一下,并运行,结果和我们想的完全一样,弹出了3次MessageBox()对话框。
这里介绍了关于本进程的Inline Hook的例子,接下来要介绍的是其他进程Inline Hook的例子。由于每个进程的地址空间是隔离的,那么对于其他进程的Inline Hook是需要用到 DLL文件的。下面学一些如何使用DLL文件来完成对其他进程的Inline Hook的工作。
5.2.4 HOOK CreateProcessW
在这个例子中,我们先写一个DLL,然后通过DLL来HOOK CreateProcessW()函数。在 Windows下,大部分的应用程序都是由Explorer.exe进程来创建的。我们用“Process Explorer”这个工具来查看一下,如图5-6所示。
从图5-6中可以看出,大部分的应用程序都是由Explorer. exe这个进程创建的,那么只要把Explorer. exe进程的CreateProcessW()函数HOOK住,就可以针对要完成的工作做很多事情了,比如,可以记录哪个应用程序被启动了,也可以对应用程序进程进行拦截了。
图5-6 “Process Explorer”查看应用程序的父进程
我们的例子就是通过HOOK CreateProcessW()函数来显示一下被创建的进程的进程名。还是使用前面给出的ILHook类来进行HOOK工作。代码如下:
代码不是很长,Hook功能是由前面封装过的类来完成的,只要去使用封装好的类进行HOOK,并定义一个HOOK函数就可以了。将这段代码编译连接,然后用第3章中编写的 DLL注入工具将这个DLL文件注入到Explorer.exe中,如图5-7所示。
图5-7 用DLL注入工具注入HOOK DLL
将这个DLL注入到Explorer.exe进程后,运行一下IE浏览器,会弹出一个对话框,如图5-8所示。
图5-8 对话框标题栏上显示了被创建的进程名
单击“确定”按钮后,IE浏览器就被打开了。再打开记事本、画图、计算器等程序,都成功地显示出了其进程名及进程的路径。
把这个程序修改一下,让它可以拦截进程的创建,这样来达到对创建应用程序的管控。修改的方法很简单,在弹出对话框以后,对话框上有两个按钮,分别选择一下相应的按钮就可以了。修改后的代码如下:
编译连接一下这个程序,提示连接错误。原因是刚才编译连接的DLL文件正在被使用,所以无法对其修改。用DLL注入工具将刚才的DLL进行卸载,然后再次编译连接,这次就通过了。把这个DLL文件注入到Explorer.exe进程中,然后启动IE浏览器,如图5-9所示。
单击“是”按钮,那么IE浏览器被创建。如果单击“否”按钮,那么会提示“您启动的程序被拦截”,并且IE浏览器没有被打开。单击“否”按钮看一下效果,如图5-10所示。
图5-9 是否创建进程的提示框图5-10进程创建被拦截的提示
图5-10 进程创建被拦截的提示
提示框出现了,单击“确定”按钮以后,IE浏览器没有被打开。再对记事本、计算器、画图等程序进行测试。测试的结果都和IE浏览器的结果是一样的,那么说明对应用程序创建的拦截功能已经成功了。
5.2.5 7字节Inline Hook
做Inline Hook的时候是通过构造一个jmp指令来修改目标函数入口的。在构造jmp指令时唯一比较不好理解的可能是计算jmp指令后面的偏移量,这是由于CPU机器码要求的。既然是修改目标函数入口指令,可以多修改几条指令,从而达到不计算jmp指令的跳转偏移量。
完成的指令为两条,一条是把目标地址保存入寄存器eax中,然后直接跳转到寄存器eax中保存的地址处。代码如下:
用OD随便打开一个程序,然后修改其入口代码为上述代码,然后提取其机器码,如图 5-11所示。
图5-11 修改入口代码
从图5-11中可以看出mov eax,12345678对应的机器码为B8 78 56 34 12,也就是说B8是mov指令的机器码。再看一下jmp eax,其对应的机器码为FF E0。将其定义为一个字节数组为:
Byte bJmpCode[]={‘\xb8’, ‘\0’, ‘\0’, ‘\’, ‘\0’, ‘\xFF’, ‘\xE0’};
这样定义以后,只要把目标函数的地址保存在从第一个到第四个字节的位置就可以了 (下标是从0开始的)。通过这种方法,就不用再计算jmp要跳转的位置对应的偏移地址了。这也是一种进行Inline Hook的方法,不过同样都是修改目标函数的入口。
5.2.6 Inline Hook的注意事项
在写Hook函数时一定要注意函数的调用约定,函数的调用约定决定了函数调用后负责平衡栈的一个约定,如果在调用函数后栈不恢复到调用前的样子的话,那么程序后续的部分一定会报错。也许程序短可能会不报错,但是千万不要有这样侥幸的心里。
下面用Hook本进程的例子做一个简单的修改,来演示一下调用。
Hook的是MessageBoxA()函数,该函数有4个参数,我们定义的函数也一定要是4个参数。MessageBoxA()函数的调用约定是stdcall,那么定义函数时也要使用stdcall。定义时使用的是WINAPI,这是一个宏,该宏的定义如下:
在MSDN中看一下MessageBox()的函数定义,该函数的定义如下:
在MSDN中,并没有看到对MessageBox()函数有关于调用约定方面的修饰。在WinUser.h中看一下关于MessageBoxA()函数的定义,该定义如下:
在WinUser.h这个头文件中可以看到,在定义中使用了WINAPI这个函数调用约定的修饰。现在来修改一下代码,修改的代码如下:
从代码中可以看到,这里把WINAPI函数调用约定的宏注释掉了。将程序进行编译连接,并运行。运行后,看到了MessageBox()的对话框,但是最后却出现了报错,如图5-12和图 5-13所示。
图5-12 错误对话框
图5-13 错误对话框
在出现图5-12后,单击“忽略”按钮,会弹出如图5-13所示的错误提示。从图5-13中可以看到一个提示“File:i386\chkesp.c”。看到这个提示以后首先要知道,这个提示告诉我们是Debug版本在检查栈平衡时报的错误。虽然这个代码是系统的代码,不是自己写的代码,但是在系统检查栈时报错,多半是由于代码破坏了栈的平衡。因此,要检查我们的代码。
出现这个错误时,其实我们是知道错误原因的,是因为我们把WINAPI这个函数调用约定的修饰去掉了。因此,的确是要检查我们的代码。但是应该从哪里开始着手呢?修改了调用约定以后,栈会不平衡。使用—stdcall是在被调用函数内进行平栈,而VC默认的调用约定是——cdcel,而此种调用约定是由调用方进行平栈。那么,我们就要手动进行平栈了。
MessageBoxA()函数有4个参数,每个参数占用4个字节,那么我们自己在函数中进行平栈,只要在返回时调用ret 0×10就可以了。修改的代码如下:
编译连接,并运行,仍然提示有错误。看来,要进行更进一步的调试了。在 MsgHook.UnHook()位置处按F9键设置断点,如图5-14所示。
按F5键执行代码,运行到断点处。单击工具栏上的反汇编按钮,如图5-15所示。
图5-14 在MsgHook.UnHook()位置处设置断点
图5-15 反汇编按钮
单击了反汇编按钮以后,将代码窗口往上移动,到函数的定义处,如图5-16所示。
通过反汇编看到有几条修改栈的操作,分别是push ebp、sub esp,40h、push ebx、push esi、push edi这5条代码。根据这5条代码来修改我们的代码以保证栈的平衡。按F7键停止调试状态的程序,并修改代码,修改后的代码如下:
图5-16 函数定义处的代码
将该代码编译连接并运行,这次运行正常了。
以上演示了一次如何手动平衡栈的过程,是不是很麻烦?其实对汇编熟悉的话就不麻烦了。不过个人觉得即使不麻烦,还是要按照原函数的函数定义来定义HOOK函数,以避免不必要的麻烦。在学习的过程中,为了深入地学习和掌握知识,手动平衡栈是可以的。但是在实际编程的过程中,仍然使用手动进行栈的平衡,那就成了钻牛角尖了。
本节介绍了Inline HOOK的原理,并通过两个例子学习了Inline Hook的用法。一个例子是对本进程的HOOK,另一个例子是对其他进程的HOOK。在对其他进程的HOOK中,演示了如何HOOK CreateProcessW()函数,并且从中学习了如何拦截应用程序进程被创建的过程,又强调了对函数栈平衡的重要性。再接下来的HOOK学习中,我们将要学习的是IATHOOK。