探索黑客技术攻防,实战研究与安全创新

导航菜单

黑客技术之HOOK编程

HOOC知识前奏

在DOS时代进行编程,那时操作系统提供的编程接口不称为API函数,而称为中断服务向量。也就是说,当时的操作系统提供的编程接口只有中断,要进行写文件要调用系统中断,要进行读文件也要调用系统中断(当然,也不调用操作系统的中断直接调用更底层的中断)……中断服务向量类似于Windows下的API函数,中断服务向量在操作系统的某个地址保存着,它是以数组形式保存着的,我们也称其为中断向量表。DOS时代的HOOK技术也就是修改中断向量表中的中断地址。比如,要捕获写操作,那么就修改中断向量表中关于写文件的地址,将写文件的中断地址保存好,然后替换为我们的地址,这样当程序调用写文件中断时,我们的函数就被执行了:当程序执行完可以继续调用原来的中断地址,从而完成写文件的操作。

在Windows下HOOK技术的方法比较多,使用比较灵活,常见的应用层的HOOK方法有Inline Hook、IAT HOOK、windows钩子…HOOK技术涉及到了DLL相关的知识。因为HOOK其他进程的时候需要访问其他进程的地址空间,使用DLL是必然的。HOOK技术也涉及到了注入的知识,想要把完成HOOK功能的DLL加载到目标进程空间中就要使用注入的知识了。让我们来学习常用的HOOK技术吧。

内联钩子—Inline Hook

Inline Hook的原理

API函数都保存在操作系统提供的DLL文件中,当在程序中使用某个API函数时,在运行程序后,程序会隐式地特API所在的DLL加载入进程中。这样,程序就会像调用自己的函数一样调用API,大体过程如图5-1所示。从图5-l中可以看出,在进程中当EXE模块调用CreateFile()函数的时候,会去调用kerne132.dil模块中的CreateFile()函数,因为真正的CreateFile()函数的实现在keme132.dll模块中。

CreateFile0是API函数.API函数也是由人编写的代码再编译而成的,也有其对应的二进制代码。既然是代码,那么就可以被修改。通过一种“野蛮”的方法来直接修改API函数在内存中的映像,从而对API函数进行HOOK。使用的方法是,直接使用汇编指令的jmp指令将其代码执行流程改变,进而执行我们的代码,这样就使原来的函数的流程改变了。

执行完我们的流程以后,可以选择性地执行原来的函数,也可以不继续执行原来的函数。

假设要对某进程的kernel32.dll的CreateFile0函教进行HOOK,首先需要在指定进程中的内存中找到CreateFile()函数的地址,然后修改CreateFile()函数的首地址的代码为jmpMyProc的指令。这样,当指定的进程调用CreateFile0函数时,就会首先跳转到我们的函数当中去执行流程,这样就完成了我们的HOOK了。看一下它的流程图,如图5-2所示。

由于这种方法是在程序流程中直接进行嵌入jmp指令来改变流程的,所以就把它叫做Inline Hook。

Inline HOOK的实现

了解了大体的HOOK流程后,现在来学习他的具体实现。

我们的C程序被编译连接后为一个二进制文件,在二进制文件中,对于代码部分来说,都是CPU可以用来执行的机器码,机器码和汇编指令又是一一对应,前面讲过了,inlinehook是在程序中嵌入jmp汇编指令然后跳转到流程处继续执行的,jmp指令的用法是jmp目的地址,jmp在汇编语言中是一个无条件的跳转指令,jmp后面跟随的参数是跳转的目的地址,用OD随便打开一个程序,并且修改它的某条指令为JMP指令,跳转的目的为一个任意地址,如图5-3和4所示。

对比可以看出,jmp指令占用了5个字节,原来从00401600到00401605处的机器码为558B EC 6A FF,当修改为jmp123456后,现在的机器码为F9 73 40F4 11.可以告诉大家,JMP对应的机器码是E9,后面的73 40 F4 11是一个偏移量,这个偏移量是多少呢?,这个偏移量是11f44073,请回忆一下前面提到过的字节顺序的问题,偏移量计算公式如下:

JMP后的偏移量=目标地址-原地址-5

这是一个非常重要的公式,当然了对于我们的使用只要记住就可以了,这里的5是JMP的指令长度,也就是JMPXXXXXXX这个指令的机器码长度为5个字符,验证一下这个公式,目标地址是123456,原地址为00461600.用123456-00401600-5=11f44073,用计算器进行计算,如图5-5所示。

上面地址都是用十六进制进行计算的,大家计算的时候要注意一点,以免计算错误,通过上面的例子可以看出来,修改的时候只需要修改5个字节就可以了,下面来梳理一下inlinehook的流程吧,流程如下。

(1)构造跳转指令

(2)在内存中找到欲hook函数地址,并且保存欲hook位置处的前5个字节

(3)将构造的跳转指令写入需hook的位置处

(4)当被hook位置被执行时会跳转到我们的流程执行

(5)如果要执行原来的流程,那么取消HOOK,也就是还原被修改的字节,

(6)执行原来的流程

(7)继续Hook住原来的位置

这就是Inline Hook的大概的流程

由于InlineHOOK的实现代码比较简单,关键就是一个HOOK和一个取消取消HOOK的过程,因此可用C++封装一个Inline HOOK的类,在今后InlineHook编程中,可以始终使用这个封装的类。

一般情况下封装类都有两个文件,一个是类的头文件,还有一个是类的实现文件,在Windows下,类名都是以C开头的,我们封装的InlineHook类,因为类名师CILHOOK,为了保持一致性,类的头文件和实现文件分别是IlHook.h文件和ILHook.cpp文件,先来看一下iLHOOK.h文件中的类定义部分:

在C++中,类的定义是使用关键字CLASS,再类中定义有成员函数和成员变量,通常情况下把成员函数放在上面,把成员变量放在下面,因为对于拿到头文件的人来说,他们首先关注的是类实现了那些功能,因此应该让他第一眼就能看到实现了的成员函数,当然这不是必须的。

回到类定义,在类中除了构造函数和析构函数以外,还定义了3个成员函数,分别是Hook()、unhook()和Rehook()函数,他们的功能分别是用来进行HOOK操作,取消HOOK操作和重新进行HOOK操作的,对于3个成员函数来说,这里只是一个定义,实现部分在ILHOOK.CPP中。

除了上面的3个成员函数外,还定义了3个成员变量,分别是m_pfnorig、boldbytrs[5]和bnewbytes[5],这3个函数的作用已经在定义中给出了注释,想必大家应该能明白,这里就不具体说了,接着看ILHook.cpp文件中的实现代码吧

在构造函数中主要是完成对成员变量的初始化工作,在构析函数中主要是取消HOOK,构造函数在C++对象被创建时自动执行,同样析构函数是在C++对象被销毁时自动执行。

该函数是Inlinehook类的重要函数,在hook()成员函数中,我们完成了3项工作,首先是获得了被HOOK函数的函数地址,接下来是保存了函数的前5个字符,最后是用构造好的跳转指令来修改被HOOK函数的前5个字节的内容。

除了上面的函数外,还有两个函数,分别是取消挂钩和重新挂钩两个函数,这两个函数非常简单,就是完成修改内存属性、复制字节的工作、代码如下:

上面两个成员函数就不进行介绍了,只要大家看懂了Hook()函数的实现,这两个函数的功能就肯定理解了。

整个Inline Hook的封装已经完成了,在后面的代码中,可以很容易地实现对函数的HOOK功能了。

HOOK MessageBoxA

本小节将完成一个HOOK本进程MessageBoxAO的程序,这个程序的目的是测试我们的类是否封装成功.以便完成今后的程序。在VC6下创建一个控制台程序,添加好封装过的库,然后键入下面的代码:

在主函数中,调用了两次MessageBox()函数,两次弹出的文本内容是一样的。但是第二次调用MessageBox()函数时,对MessageBox0做了HOOK,HOOK的函数是我们自己写的MyMessageBoxA()函数。在MyMessageBoxA()函数中首先恢复了对MessageBox()函数的HOOK,然后连续调用了两次MessageBox()函数。那么,这个测试程序应该弹出3次MessageBox0对话框。大家将其编译连接一下,并运行,结果和我们想的完全一样,弹出了3次MessageBox()对话框。

这里介绍了关于本进程的Inline Hook的例子,接下来要介绍的是其他进程Inline Hook的例子。由于每个进程的地址空间是隔离的,那么对于其他进程的Jnline Hook是需要用到DLL文件的。下面学一些如何使用DLL文件来完成对其他进程的Inline Hook的工作。

HOOK CreateProcessW

在这个例子中,我们先写一个DLL,然后通过DLL来HOOK CrealeProcessW()函数。在Windows下,大部分的应用程序都是由Explorer.exe进程来创建的。我们用“Process Fxplorer”这个工具来查看一下,如图5·6所示。

从图5-6中可以看出,大部分的应用程序都是由Explorer.exe这个进程创建的,那么只要把Explorer,exe进程的CreateProcessWO函数HOOK住,就可以针对要完成的工作做很多事情了,比如,可以记录哪个应用程序被启动了,也可以对应用程序进程进行拦截了。

我们的例子就是通过HOOKCREATEPROCESSW()函数来显示一下被创建的进程的进程名称,还是使用前面给出的ILHOOK类来进行HOOK工作,代码如下:

代码不是很长,Hook功能是由前面封装过的类来完成的,只要去使用封装好的类进行HOOK,并定义一个HOOK函数就可以了。将这段代码编译连接,然后用第3章中编写的DLL注入工具将这个DLL文件注入到ExplOfer.exe中,如图5-7所示。

将这个DLL注入到Explorer.exe进程后,运行一下IE浏览器,会弹出一个对话框,如图5-8所示。

单击“确定”按钮后.IE浏览器就被打开了。再打开记事本、画图、计算器等程序,都成功地显示出了其进程名及进程的路径。

把这个程序修改一下,让它可以拦截进程的创建,这样来达到对创建应用程序的管控。修改的方法很简单,在弹出对话框以后,对话框上有两个按钮,分别选择一下相应的按钮就可以了。修改后的代码如下:

编译链接一下这个程序,提示连接错误,原因是刚才编译链接的DLL文件正在被使用,所以无法对其修改,用DLL注入工具将刚才的DLL进行卸载,然后再次编译链接,这次就通过了,把这个DLL文件注入到EXPLORER.EXE进程中,然后启动IE浏览器,如图5-9所示。

单击是按钮,那么IE浏览器被创建,如果单击否按钮,那么会提示您启动的程序被拦截,并且IE浏览器没有被打开,单击否按钮看一下效果,如图5-10所示

提示框出现了,单击确定按钮以后,IE浏览器没有被打开,在对记事本、计算器、画图程序等进行测试,测试的结果都和IE浏览器的结果一样的,那么说明对应用程序创建的拦截功能已经成功了。

7字节InlineHook

做InlineHook的时候是通过构造一个JMP指令来修改目标函数入口的,在构造JMP指令时候唯一比较不好理解的可能是计算JMP指令后面偏移量,这是由于CPU机器码要求的,既然是修改目标函数入口的指令,可以对修改几条指令,从而达到不计算JMP指令的跳转偏移量。

完成的指令为两条,一条是把目标地址保存入寄存器EAX中,然后直接跳转到寄存器EAX中保存的地址处,用代码如下

用OD随便打开一个程序,然后修改其入口代码为上述代码,然后提取其机器码,如图5-11所示。

Congruent5-11中可以看出MOVEAX12345678对应的机器码为B8 78 56 34 12 也就是说B8是MOV指令的机器码,在看一下IMPeax,其对应的机器码为FFE0,将其定义为一个字节数组为”

这样定义以后,只要把目标函数的地址保存在从第一个到第四个字节的位置就可以了,通过这种方式,就不用在计算JMP要跳转的位置对应的偏移位置而来,这也是一种进行INLINEHOOK的方法,不过同样都是修改目标函数的入口。

InlineHOOK的注意事项

在写HOOK函数的时候一定要注意函数的调用约定,函数的调用约定决定了函数调用后负责平衡栈的一个约定,如果在调用函数后栈不恢复到调用前的样子的话,那么程序后续的部分一定会报错,也许程序短可能会不报错,但是千万不要有这样的侥幸心理。

下面用HOOK本进程的例子做一个简单的修改,来演示一下调用。

HOOK的是MESSAGEBOXA()函数,该函数有4个参数,我们定义的函数也一定要有4个参数,MESSAGEBOXA()函数的调用约定是_stdcall,那么定义函数时也要使用_stdcall,定义时使用的是WINAPI,这是一个宏,该宏的定义如下:在MSDN中看一下messagebox()的函数定义,该函数的定义如下:

在MSDN中,并没有看到对MESSAGEBOX()函数有关调用约定方面的修饰,在WINYSERB中看一下关于MESSAGEBOXA()函数的定义,该定义如下:

在WINUSER.H这个头文件中可以看到,在定义中使用了WINAPI这个函数调用约定的修饰,现在来修改一下代码,修改的代码如下:

从代码中可以看到,这里把WINAPI函数调用约定的宏注释掉了,将程序进行编译链接并运行,运行后,看到了MESSAGEBOX()的对话框,但是最后却出现了报错,如图5-12和图13所示。

在出现图5-12后,单击忽略按钮,会弹出如图5-13所示的错误提示,从图5-13中可以看到一个提示FILEI386\CHKESP.C,看到这个提示以后首先要知道,这个提示会告诉我们是DEBUG版本在检查栈平衡时报的错误,虽然这个代码是系统的代码,不是自己写的代码,但是在系统检查栈时报错,多半是由于代码破坏了栈的平衡,因此要检查我们的代码。

出现这个错误的时候,起始我们是知道错误的原因的,是因为我们吧Winapi这个函数调用约定的修饰去掉了,因此,的确是要检查我们的代码,但是应该从哪里开始呢?修改了调用约定以后,栈会不平衡,使用_STDCALL是在被调用函数内进行平栈,而VC默认的调用约定是_cdcel,而此种调用约定是由调用方进行平栈,那么,我们就要手动进行平栈了MESSAGEBOXA()函数有4个参数,每个参数占用4个字节,那么我们自己在函数中进行平栈,只要在返回的时候调用RET0X10就可以了,修改的代码如下:

编译链接,并且运行,仍然提示错误,看来要进行更进一步的调试了,在MSGHOOK.UNHOOK()位置处按F9键设置断点,如图5-14所示。

按F5键执行代码,运行到断点处,单击工具滥觞的反汇编按钮.

单击了反汇编按钮以后,将代码窗口往卜移动,到函数的定义处。

通过反汇编看到有几条修改栈的操作,分别是push ebp、sub esp,40h、push ebx、push esi、push edi这5条代码。根据这5条代码来修改我们的代码以保证栈的平衡。按F7键停止调试状态的程序,并修改代码,修改后的代码如下:将代码编译链接并且运行,这次运行正常了。

以上演示了一次如何手动平衡栈的过程,是不是非常的麻烦?起始对汇编熟悉的话就不麻烦了。不过个人感觉即使不麻烦,还是要按照原来函数的函数定义HOOK函数,以避免不必要的麻烦,在学习的过程中,为了深入的学习和掌握知识,手动平衡栈是可以的,但是在实际编程的过程中,仍然使用手动进行栈的平衡,那就成了钻牛角尖了