第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内核里的一张「函数跳转表」。当用户态程序调用NtOpenProcessNtCreateFile这类系统服务时,CPU会切换到内核态,然后通过SSDT找到真正的内核函数地址。

你想想看,如果我们能修改这张表,把NtOpenProcess的地址换成我们自己的函数,那每次有进程打开操作,都会先经过我们的代码。这就是SSDT Hook的核心思想。

核心要点: SSDT Hook的本质是替换内核函数指针,实现对所有系统服务调用的监控或篡改。

39.2 SSDT Hook的实战步骤

我记得第一次写SSDT Hook时,踩了不少坑。这里我把标准流程拆解一下:

  1. 获取SSDT基址:通过KeServiceDescriptorTable导出符号获取,或者从ntoskrnl.exe中动态查找。
  2. 找到目标函数索引:比如NtOpenProcess在SSDT中的索引号,不同Windows版本不一样。
  3. 修改内存保护属性:SSDT所在内存默认是只读的,需要用MDLCR0寄存器技巧改成可写。
  4. 替换函数指针:把原地址保存下来,换成我们的Hook函数地址。
  5. 实现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;
}
⚠️ 重要警告: 这段代码为了演示做了大量简化。实际项目中,你必须处理MDL锁定、IRQL级别、SMP同步等问题。我曾经因为忘记提升IRQL,导致蓝屏了一整天。

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
}
💡 避坑指南: 我曾经在IDT Hook里直接调用了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 + 双机调试)是必须的。我当年没有调试器硬写内核代码,蓝屏次数多到数不清。别学我,该上工具就上工具。

内核级Hook知识体系 用户态(Ring3) 系统服务调用(Syscall/Sysenter) 内核态(Ring0) SSDT Hook 拦截系统服务调用 如:NtOpenProcess NtCreateFile等 IDT Hook 拦截硬件中断 如:键盘中断IRQ 1 时钟中断IRQ 0等 内核级监控与防护
公众号:蓝海资料掘金营,微信deep3321