第39章 Windows API Hook(四):内核级Hook(SSDT Hook、IDT Hook)
前面几章我们聊的都是用户态的Hook,说白了就是在Ring3层做手脚。但真正的逆向高手都知道,用户态的东西再花哨,内核态一个回调就能把你打回原形。这一章,我们直接杀入内核,聊聊SSDT Hook和IDT Hook。
我个人习惯把内核Hook比作「房子的地基」——你改墙纸(用户态Hook)房东不管,但你要是动承重墙(内核Hook),那整个系统都得听你的。当然,风险也成正比。
39.1 什么是SSDT?为什么它这么香?
SSDT全称System Service Dispatch Table,中文叫系统服务分发表。说白了,它就是Windows内核里的一张「函数跳转表」。当用户态程序调用NtOpenProcess、NtCreateFile这类系统服务时,CPU会切换到内核态,然后通过SSDT找到真正的内核函数地址。
你想想看,如果我们能修改这张表,把NtOpenProcess的地址换成我们自己的函数,那每次有进程打开操作,都会先经过我们的代码。这就是SSDT Hook的核心思想。
39.2 SSDT Hook的实战步骤
我记得第一次写SSDT Hook时,踩了不少坑。这里我把标准流程拆解一下:
- 获取SSDT基址:通过
KeServiceDescriptorTable导出符号获取,或者从ntoskrnl.exe中动态查找。 - 找到目标函数索引:比如
NtOpenProcess在SSDT中的索引号,不同Windows版本不一样。 - 修改内存保护属性:SSDT所在内存默认是只读的,需要用
MDL或CR0寄存器技巧改成可写。 - 替换函数指针:把原地址保存下来,换成我们的Hook函数地址。
- 实现Hook函数:在Hook函数里做你想做的事,最后调用原函数或直接返回。
来看一段核心代码,这是我实际项目里用过的简化版:
// 获取SSDT基址
extern "C" PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;
// 保存原函数地址
PVOID g_OriginalNtOpenProcess = NULL;
// Hook函数
NTSTATUS HookNtOpenProcess(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
) {
// 这里可以加你的逻辑,比如过滤特定进程
DbgPrint("NtOpenProcess called! PID: %d\n", ClientId->UniqueProcess);
// 调用原函数
return ((NTOPENPROCESS)g_OriginalNtOpenProcess)(
ProcessHandle, DesiredAccess, ObjectAttributes, ClientId
);
}
// 安装Hook
VOID InstallSSDTHook() {
ULONG index = 0x7A; // NtOpenProcess在Win10 x64上的索引
g_OriginalNtOpenProcess = KeServiceDescriptorTable->ServiceTable[index];
// 修改内存保护(这里省略MDL操作,实际必须做)
KeServiceDescriptorTable->ServiceTable[index] = HookNtOpenProcess;
}
39.3 IDT Hook:更底层的拦截
如果说SSDT Hook是拦截系统服务,那IDT Hook就是拦截硬件中断。IDT全称Interrupt Descriptor Table,中断描述符表。每个CPU核心都有一张IDT,记录了中断号对应的处理函数地址。
为什么要Hook IDT?举个例子,你想监控所有的键盘输入,就可以Hook键盘中断(IRQ 1)。每次按键,都会触发中断,你的Hook函数就能拿到扫描码。
我个人觉得IDT Hook比SSDT Hook更「硬核」,因为它直接跟硬件打交道。但风险也更大——搞不好整个系统输入就瘫痪了。
39.4 IDT Hook的实现要点
IDT Hook的步骤和SSDT类似,但有几个特殊之处:
- 获取IDT基址:用
SIDT指令读取IDTR寄存器,得到IDT的基址和大小。 - 中断门结构:IDT中的每个条目是8字节(x86)或16字节(x64),包含段选择子、偏移量、属性等。
- 中断处理函数:必须用汇编编写,因为需要处理CPU自动压栈的上下文。
- 链式调用:Hook完成后,记得调用原中断处理函数,否则设备可能不工作。
来看一个IDT Hook的汇编片段,这是我当年调试键盘过滤驱动时写的:
; 键盘中断Hook(IRQ 1)
_asm {
; 保存上下文
pushad
pushfd
; 读取扫描码
in al, 0x60
; 这里可以调用C函数处理
push eax
call HandleKeyboardInput
add esp, 4
; 恢复上下文
popfd
popad
; 跳转到原中断处理函数
jmp OriginalKeyboardHandler
}
DbgPrint,结果死锁了。原因是DbgPrint内部可能触发中断,造成递归调用。记住:中断处理函数里尽量别做复杂操作,尤其是不能调用可能引起中断的函数。
39.5 两种Hook的对比与选择
很多新手会问:SSDT Hook和IDT Hook到底选哪个?我的建议是:
| 对比维度 | SSDT Hook | IDT Hook |
|---|---|---|
| 拦截粒度 | 系统服务调用 | 硬件中断 |
| 实现难度 | 中等 | 较高 |
| 稳定性风险 | 中 | 高 |
| 典型用途 | 进程/文件/注册表监控 | 键盘/鼠标/硬件监控 |
| Windows版本兼容性 | 差(索引经常变) | 较好(中断号相对固定) |
嗯,这里要注意:SSDT Hook在Windows 10/11上越来越难搞,因为PatchGuard会检测SSDT是否被修改。而IDT Hook虽然也受PatchGuard保护,但检测力度相对弱一些。
39.6 内核Hook的防御与检测
作为安全研究员,我们不能只想着怎么Hook,还得知道怎么防。我总结了几种检测内核Hook的方法:
- 校验SSDT完整性:从磁盘加载一份干净的ntoskrnl.exe,对比内存中的SSDT表。
- 扫描IDT异常:检查中断处理函数是否在已知内核模块范围内。
- 使用KDP(Kernel Data Protection):Windows 10引入的新机制,可以保护关键数据结构不被修改。
- 回调监控:注册
PsSetLoadImageNotifyRoutine,监控是否有驱动加载了可疑的Hook代码。
说实话,内核攻防就是一场猫鼠游戏。你Hook我,我检测你,你再绕过检测……这个循环永远不会停。
39.7 本章小结
SSDT Hook和IDT Hook是内核级逆向的必修课。前者拦截系统服务,后者拦截硬件中断。两者都能实现用户态Hook做不到的深度监控,但代价是稳定性和兼容性风险。
我个人建议:如果你只是做安全研究或CTF,SSDT Hook就够用了。但如果你要做真正的内核级Rootkit或反Rootkit工具,IDT Hook也得掌握。
最后提醒一句:内核调试器(WinDbg + 双机调试)是必须的。我当年没有调试器硬写内核代码,蓝屏次数多到数不清。别学我,该上工具就上工具。