作者:daiwen 

名字起得好,Inline hook,乍一听,似乎很高深。此处的Inline,我以为,意指将汇编代码直接写入内核API的内存区域。Inline Hook不像用户态Hook或SSDT hook(用C语言就足够),它需要在程序中嵌入汇编代码(Inline Assembly)以操作堆栈和执行内核API对应的部分汇编指令。当然,这些都须以驱动的形式进行。

所谓API Hook,就是用自己写的函数去替代系统API的“职位”,此后,自己写的函数便掌握了以前由被hook的API所“经手”的一切“事宜”。 Windows系统分用户态和内核态,API也就有了用户级和内核级两大类。想要比较底层、彻底地做点事情,当然要hook内核API了(不过hook用户态API也有诸多用途)。

Hook内核API比较常见的是SSDT hook,一句话——Windows把需要调用的内核API地址全都存在了一个表中(System Service Descriptor Table),要想hook一个内核API,比较简单的办法就是把该内核API在表(SSDT)中保存的地址修改为自己撰写的函数地址。这个道理类似于把"Windows"先生的"内核API电话簿"给篡改了,当老先生想要打电话给"被hook的api先生"时,他找到的"电话号码"其实已被我们篡改,拨通电话,我们的"函数小子"开始应答,信息过滤自此开始。

非常不幸,ICESword等检测工具可以轻松判断SSDT是否被篡改,并且会以适当的方式通知用户(比如检索结果的字体变红)。

Inline Hook要比SSDT Hook来得更彻底一点。如果说SSDT Hook只是把某位"内核API先生"绑架,然后用我们的“自己人”来接管其工作,而ICESword却可以从其他联系途径找到被绑架的"内核API先生"并“报警”,那么——Inline Hook可以说是给"内核API先生"动了手术,让他成为"我们阵营的一分子"。Inline Hook通过硬编码的方式向内核API的内存空间(通常是开始的一段字节,且一般在第一个call之前,这么做是为了防止堆栈混乱)写入跳转语句,这样,该API只要被调用,程序就会跳转到我们的函数中来,我们在自己写的函数里需要完成3个任务:
1)重新调整当前堆栈。程序流程在刚刚跳转的时候,内核API并没有执行完,而我们的函数需要根据其结果来进行信息过滤,所以我们需要保证内核API能在顺利执行完毕后返回到我们的函数中来,这就要求对当前堆栈做一个调整。
2)执行遗失的指令。我们向内核API地址空间些如跳转指令(jmp xxxxxxxx)时,势必要覆盖原先的一些汇编指令,所以我们一定要保证这些被覆盖的指令能够顺利执行(否则,你的及其就要BSOD了,呵呵,Blue Screen Of Death)。关于这部分指令的执行,一般是将其放在我们的函数中,让我们的函数“帮助”内核API执行完被覆盖的指令,然后再跳回内核API中被覆盖内后后的地址继续执行剩余内容。跳回去的时候,一定要算好是跳回到什么地址,是内核API起始地址后的第几个字节。
一个朋友曾提出把内核API的被覆盖内容还原,然后执行之——这种方法,我没有试验,但我认为应该不会很稳定,因为内核里常有线程切换,如果你把内核API还原,万一自己函数的线程被挂起,而又有线程要调用给API,这就会出现“Hook 遗漏”。
3)信息过滤。这个就不用多说了,内核API顺利执行并返回到我们的函数中,我们自然要根据其结果做一些信息过滤,这部分内容因被hook的API以及Hook目的的不同而不同。

Inline hook的工作流程:
1)验证内核API的版本(特征码匹配)。
2)撰写自己的函数,要完成以上三项任务。
2)获取自己函数的地址,覆盖内核API内存,供跳转。

Inline Hook的缺点:
1) 不够通用。各个windows版本中,内核API的开始一段不尽相同,要想通吃,就要多写几个版本或者做一个特征码搜索(因为有的内核API在各个版本中非常相似,只是在“特征码”之前或之后加一点东西)。
2) 已被一些检测工具列入检测范围,如果直接从内核API第一个字节开始覆盖,那么很容易被检测,如果把覆盖范围往后推,并加以变形,也许能抵挡一气。具体情况,我才疏学浅,尚未试验。

上文权且当作是以下两文的读书笔记:
1) kernel inline hook 绕过vice检测——xfocus上的文章
2) 实现kernel-mode inline function hook的简单方法(http://www.phpfav.com/?p=35)——5eCur!ty上的文章

大家可以去参看原文,文章1中的代码可以用文章2的方法优化一下。