浅谈Windows下的inline hook

  • 内容
  • 评论
  • 相关

inline hook在我个人看来是Windows API编程里面一个比较重要的东西,尽管按照某些方面来说这个是初级的,不过多多少少还是要点内容才能学上手的。所以这篇文章的目的也只是简单的谈一谈其实现原理等东西。这里所说到的原本也是准备在CFC上学期的分享会上说的,然而由于一些奇奇怪怪的原因我那天并没有说,所以在这里补上,等下一次有机会再说了。至于其他的嘛,我只能按照我学的深度来说了。:)

所谓的inline hook是Windows系统中“挂钩”方式的一种,大家知道Windows系统是消息驱动的工作模式,在代码段的实现部分则是一个个函数之前的调用关系。inline hook就是对函数的一种挂钩方式,通过这种挂钩,使得在调用被挂钩函数之前执行自己所期望的操作,比如跳转到自己的函数,继续执行原函数功能,或者返回特定内容,等等。通俗的来说,比如A和B两个函数存在调用关系(A调用B),而inline hook就是在两个“恩爱的”函数A和B的调用关系之间加一个“小三”C函数,这个“小三”可以在A调用B的时候随意的执行自己想要的事情,比如不执行B,或者伪装成B返回假的结果,等等,而B函数本身对此却无可奈何。

所以在此之前呢我先从C语言的一小段代码讲起。在大家写C语言程序的时候一般会声明函数,或者调用。例如:

如上述内容所示,这段C语言代码包含了一个main函数和一个add函数。main函数中调用了自定义的函数add和库函数printf。你通过调用add函数完成1和2的加法,然后调用printf打印这段输出。

上面这段代码在正常执行的情况下,应该的输出是

然而某一天,写这段代码的程序猿突然脑子抽了一下,他想让add返回错误的值,但又不想修改add函数本身,于是他构造了一个fake_add函数,想让他作为那个破坏main和add之间的小三函数。

但是,函数写好了,程序猿又该怎么实现这个目的呢?

然后这里我们来跳一下,看下代码在内存里面是个什么样子。

程序员们应该知道的是C语言这种编译性语言是直接生成成二进制指令的。实际上,上面这段代码在内存里面给系统所看到的就是一大段二进制内容。如果你知道一点点汇编的话,你可能会知道实际上在内存里面的代码在“反编译”之后都是类似于下面这个样子的。你可能通过网上的文档,或者其他的途径,知道在Windows的二进制中,0x8B表示汇编的MOV指令,0xe9表示汇编的jmp指令,等等。

QQ截图20160215015523

一堆汇编指令真要看起来的话会比较头疼。那么我说这个的目的是想说明,其实你写的代码、函数,到最后都会是这种二进制的形式。某一个字节代表什么指令,后面跟的代表什么含义。就是这个样子。包括函数本身的调用也只是一条指令(汇编的call指令,二进制表示为0xE8)。在计算机组成原理课上说过,指令是一条一条按照顺序执行的,也就是说上面这些汇编指令都是按照顺序一条一条执行的,当我执行到call的时候,表示我要调用某个函数,执行到jmp的时候表示跳转到哪一条指令。

那么回到之前的问题,既然指令按照顺序执行,那么我人为的改变他的执行过程,修改内存里面指令的内容,使得他在调用目标函数之前先调用我自己的函数,这样不就可以达到挂钩的目的了吗?

这也就是inline hook的由来。因为这个钩子是通过修改函数执行指令来达到挂钩的。

那么,程序猿又应该如何去修改这段指令,使得add函数调用之前先调用fake函数呢?这里要提到两个函数。VirtualProtect和WriteProcessMemory.

这里我要补充一点点关于PE(可执行程序)本身的事情。在Windows将程序载入内存的时候,会在内存中分配多个“页”(对于不理解PE内容的,将其认为是一块内存区域就可以了),将程序的内容载入进去,并且按照数据类型分配“页”的属性,比如,数据段不能执行,所以这段内存属性是“可读可写”,代码段应该只能被执行,因此这段属性为“可执行,只读”。

了解了上面这点东西以后也就知道,如果我们要修改二进制代码内容,需要有权限修改这段区域的内容才可以,因此调用VirtualProtect修改add函数附近的内存属性,使得其可以被修改。

如上述代码片段所示,调用VirtualProtect函数修改add函数附近的5个字节码为可读可写(第三个参数64表示修改该内存段保护为可读可写),并将该段内存原来的保护存储到dwOldProtect中。在完成do hook部分(也就是修改代码执行逻辑)的内容后,将内存保护修改回原来的值。

既然do hook部分是要修改函数的执行逻辑,让执行add函数的时候执行到fake_add函数,我们要想办法执行一条指令,使得执行跳转到fake_add函数去。在汇编语言中使用jmp进行跳转,而jmp指令对应的二进制是0xe9,那么我们构造下面这种代码

并将这段代码写入add函数的开始部分,就可以达到执行add前跳转执行fake_add的目的了。涉及到内存写入,这才有了WriteProcessMemory函数

在执行过程中,当Hook写入成功时,会弹出一个消息框,提示Write Hook Success.如下图

hook成功

然后我们输出下新的结果呢?

QQ截图20160215023616

看到了吗?现在add函数返回的结果成了4,证明fake_add函数挂钩成功。

所以经过上述简单的讲解以后,我们可以知道进行inline hook的基本操作是:

  1. 获取到原函数和挂钩函数的地址
  2. 修改原函数所在内存区段的保护
  3. 计算并写入跳转代码
  4. 修改回内存区段保护
  5. enjoy and check !

对于获取原函数地址,可以通过函数名(例如 LPVOID originFunction = MessageBoxA),或者如果该函数是存在在一个DLL并且已声明公开,则可以通过GetProcAddress函数来获取函数的地址。

比如要获取kernel32.dll里面的WriteProcessMemory函数:

其中GetModuleHandle函数是获取已加载到内存中的,DLL的基地址句柄。如果该DLL未被加载到内存中,你可以使用LoadLibrary函数将其加载,并且函数会返回DLL的基地址句柄。

至此,程序内的inline hook简单讲解完毕。

附:文章中的测试源代码(VS2013编译通过并正常运行):

By CrazyChen @ 2016/2/15

评论

1条评论
  1. Gravatar 头像

    Hackerl

    jmp指令占5字节,执行该指令后,IP + 5,然后在加上jmp指令的位移量
    所以 JMP的地址(fakeAdd) – 代码地址(originAdd) – 5(字节) = 机器码跳转地址(E9 位移量)