通过两字节修改实现的Windows内核函数Hook框架
写在前面
代码仓库:c2bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6d9k6i4y4W2N6p5#2A6L8X3g2y4K9h3&6V1i4K6u0r3N6r3W2F1P5f1E0W2M7X3&6W2L8p5S2G2L8$3E0Q4c8f1k6Q4b7V1y4Q4z5p5y4Q4c8e0k6Q4z5o6c8Q4z5f1k6Q4c8e0S2Q4b7U0m8Q4b7e0u0Q4c8e0g2Q4b7e0c8Q4b7e0N6Q4c8e0g2Q4b7f1g2Q4b7U0k6Q4c8e0k6Q4z5f1c8Q4b7e0g2K6N6r3q4J5i4@1f1K6i4K6R3H3i4K6R3J5
介绍
Inline Hook是r0/r3通用的hook方法,其原理是修改目标函数的前n个字节,常用的 Inline Hook 的替换指令为jmp qword ptr ds:[xxx]; hook_func_addr
和mov rax,hook_func; push rax; ret
,前者二进制码长度为 14 字节,后者二进制码长度为 12 字节。这么多字节的 Hook 带来了一些问题:有没有其它函数跳转到这些字节的中间呢?
同时,在实现 Hook 后,我们还需要一种手段来修复 Hook,以实现对原始函数的调用,但是内核和应用程序不一样,应用程序的内存空间是独立的,Hook 了某个应用程序中的函数,那么只会影响到该应用程序,那么先 Unhook 再 Hook 回去也来得及;内核空间是共享的,需要保证高并发下 Hook 也稳定。因此,最好的方法是将被替换的原函数代码拷贝一份,每次函数被调用时,先执行这份拷贝,再跳回原执行流。由于在新的内存空间中执行函数,会导致相对寻址指令出错,因此这也是头疼的一点。

使用 IDA 脚本,以14字节为限度,来查看内核模块中哪些函数因为上面的两个原因而不能 Hook。

如果仅使用2字节来实现 Inline Hook 的话,由于上面两个原因而不能被 Hook 的函数会减少很多。

可以看到,相对地址导致不能 Hook 的函数数量从8064减少到了610;由于外部函数引用导致的不能 Hook 的函数数量从705减少到了1;由于函数不足14字节长度导致734个函数无法被 Hook,也变成了由于函数不足2字节长度导致59个函数无法被 Hook。可以看到缩短 Inline Hook 的长度确实能有效增加可 Hook 函数的数量。
当然,除了减少 Hook 字节数以外,还有其它方法来解决上述的问题,比如@smallzhong_驱动挂钩所有内核导出函数来进行驱动逻辑分析一文中提到的将相对寻址修改为绝对地址、将代码拷贝到同模块空白地址处等。本文介绍的 基于异常的两字节 Hook 框架 是对同一事物另一思考,在此之前也有不少利用调试异常、缺页异常做 Hook 的实现,在此只是分享一下自己的实现、抛砖引玉,希望对大家有帮助,如果文章中有实质性的错误,欢迎大家指正~!
一、使用异常处理程序分发Hook的可行性分析
1.1 理论支持
Hook函数不是对单一函数进行Hook,如果要为每一个Hook都准备一个中断向量号,一是可hook的函数总数受限,二是不够经济。因此,最好是使用一个中断向量号来分发Hook,这样的话就需要解决“哪个函数导致的Hook?”这一问题。
我们知道,CPU响应异常或中断后,硬件会压栈SS\RSP\RFLAGS\CS\RIP
到栈中,RIP
是极佳的选择。但是我不想在中断上下文做太多事情,因此我选择在异常触发后复用当前栈来存储RIP
的值。下面是 Intel 白皮书中对如何使用当前栈来存储RIP值的介绍。

由上图的内容可以看到,在 IA-32 模式下,当中断或者异常发生时,如果特权级没有改变,那么并不会切换被使用的栈。


由上图中的内容可以看到,在 IA-32e 模式下,中断或者异常发生时,其栈切换工作流程和 legacy 模式没有本质区别,只是如果发生了特权级切换,那么新的SS选择子会被赋值为NULL。同时,旧SS和旧RSP会被无条件压栈。


由上图中的内容可以看到,在 IA-32e 模式下,出现了 IST 机制,可选修改 legacy 栈切换机制。如果,64位 IDT 门描述符的 3-bit IST 属性的值设置为 001 - 111
中的任意值,那么在中断或者异常触发时,将会将其处理程序使用的栈指针修改为 TSS 中对应的 IST 指针。
但是如果,3-bit IST 属性的值设置为 000
,那么就会使用上述modified legacy stack-switching
机制。

顺带提一下,如果我们需要在中断/异常触发时,由CPU帮我们关中断,可以选择将IDT中的门描述符设置为正确的类型。如果时中断门,那么CPU会帮自动清除IF位,以关中断;反之,陷阱门则不会。
因此,想要使用当前栈来存储函数的标识信息,只需要做一件事:将中断门/陷阱门描述符中的 3-bit IST 设置为0即可。
1.2 中断和异常
根据"intel 白皮书 Vol. 3A CHAPTER 6 INTERRUPT AND EXCEPTION HANDLING "中描述,中断的来源是:来自外部的硬件中断、Int n软件模拟的中断;异常的来源是:来自CPU在程序运行过程中的错误检查、Int n软件模拟的异常、机器检查的异常。
可以注意到,中断和异常都可以由 Int n 指令进行模拟,而且由 Int n 触发的中断不会受到 EFLAGS 寄存器中 IF 位的影响。

同时,由于异常是由 CPU 产生的,因此往往是在进程上下文同步执行的,因此异常在任何情况都应该被第一时间处理,否则 CPU 就会出现错误。
配合 Linux 中断子系统进行理解,硬件中断是异步中断,通常上半部在中断上下文进行处理(即没有固定进程上下文的进程上下文),而下半部可以在使用软中断、微任务在中断上下文进行处理,也可以使用中断线程化、工作队列在进程上下文进行处理。而异常是同步异常,在进程上下文进行处理。稍微了解一些Linux 中断子系统的实现,可以发现,对于异常特别简单的硬中断(CPU 核间中断等),其处理程序往往短、平、快,而稍微耗时、不太紧要的操作就可以延迟到软中断(允许中断的中断上下文)和进程上下文进行操作,本文实现的 基于异常的两字节 Hook 框架 参考这样的设计,在中断上下文仅仅将控制流转向 Hook 分发函数的 wrapper ,在进程上下文完成真正的 Hook 分发。
因此可以得出这样的结论,这也是本文实现 Hook 框架的核心指导:
1 2 | 1. 中断分为可屏蔽和不可屏蔽中断,由 EFLAGS 的 IF 位决定是否响应可屏蔽中断, Int n 模拟的中断不可屏蔽。
2. 异常必须立刻同步执行异常处理程序,但是此时CPU并不自动屏蔽中断。
|
1.3 可行性总结
1 2 3 | 1. 由于异常发生后会立即执行异常处理程序,因此完全可以胜任 改变执行流 的重任。
2. 64 - bit 模式下,通过设置中断 / 陷阱门描述符的 3 - bit IST 为 0 ,在同特权级下,将不会进行栈空间的切换。因此,可以存储被 Hook 函数的信息到原执行流使用的栈中。
3. IDT 中通常都会存在空余的表项未被使用。
|
因此,使用 IDT hook + Inline Hook的方式,可以减小 Inline Hook 对被 Hook 函数的修改影响,可以仅使用2字节实现对控制流的劫持。
二、 Windows IRQL 对异常的影响
在微软官方的内核编程手册中,往往有着对该函数应该在什么样的 IRQL 来调用的限制;在 Dispatch_level 不能使用分页内存,因为 在该 IRQL 级别下无法执行缺页中断等等。我们知道,缺页中断也是异常的一种,既然如此,是否基于异常的控制流改变方法,不适用于 Dispatch_level 及以上级别运行的函数的 Hook 呢?
如果如上文所述,那么基于异常的 Hook 将会处处受限,所有适用于 <= Dispatch_level
的函数和> Dispatch_level
的函数都不能 Hook,因为你不知道这个函数是在什么 IRQL 执行的。所幸,该机制是 Windows 独有的,并非 CPU 所实现的。
仔细回想在 Dispatch_level 访问分页内存导致的蓝屏,总是在缺页异常处理函数中来触发的KeBugCheckEx
。不难想到,在Dispatch_level 其实还是进入了异常处理程序,只不过 Windows 实现的缺页异常处理程序内部会检查当前的 IRQL 罢了。那我们实现的异常处理程序不去实现这个机制就好了,但是为了避免 Windows 出问题,所以需要尽可能快一点退出中断上下文。


不论是 windbg 接管,还是实验的TestHook
被 Hook、成功自定义 Hook 函数都可以说明,IRQL 对异常没啥影响,主要看代码实现。
三、总结
本文主要是介绍了 12aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6d9k6i4y4W2N6p5#2A6L8X3g2y4K9h3&6V1i4K6u0r3N6r3W2F1P5f1E0W2M7X3&6W2L8p5S2G2L8$3D9`. 中 基于异常的两字节 Hook 框架 的理论支持和在实现过程中笔者的思考过程,文章有很多不成熟的地方,还希望各位多多指正。
参考文章
https://bbs.kanxue.com/thread-286641.htm
481K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6k6h3y4H3N6h3I4K6k6g2)9J5k6h3y4G2L8g2)9J5c8X3q4J5j5$3S2A6N6X3g2K6i4K6u0r3y4K6j5#2z5o6y4Q4x3X3g2Z5N6r3#2D9
627K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Z5k6h3q4H3k6s2g2E0M7q4)9J5k6h3y4F1i4K6u0r3j5i4u0@1K9h3y4D9k6g2)9J5c8U0b7#2x3e0b7@1x3K6x3`.
《Combined Volume Set of Intel® 64 and IA-32 Architectures Software Developer’s Manuals.pdf》