17、内核钩子技术:SSDT Hook、Inline Hook、IDT Hook、钩子检测与绕过

内核钩子,说白了就是「劫持」系统原本的执行流程。你想想看,Windows 内核里那么多函数调用,如果我们能截住某个关键调用,先让它执行我们的代码,再决定要不要放行——这不就是驱动开发的「核心魔法」吗?

我个人习惯把钩子技术分成三类:SSDT Hook、Inline Hook、IDT Hook。每种都有它的脾气,也有它的坑。今天我们就一个一个掰开揉碎了讲。

17.1 SSDT Hook:系统服务描述符表钩子

SSDT 是什么?它是 Windows 内核里的一张「函数跳转表」。用户态程序调用 NtOpenProcessNtCreateFile 这些系统服务时,最终都会通过 SSDT 找到内核里的真正实现。

SSDT Hook 的原理很简单:把表里某个函数的地址,改成我们自己的函数地址。这样每次调用进来,先走我们的代码。

核心思路:找到 SSDT 基址 → 定位目标函数索引 → 替换函数指针 → 恢复时写回原值

// 伪代码示例:SSDT Hook NtOpenProcess
typedef NTSTATUS (*NtOpenProcess_t)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PCLIENT_ID);
NtOpenProcess_t OriginalNtOpenProcess = NULL;

NTSTATUS MyNtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess,
                         POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId) {
    // 检查调用者PID,如果是受保护进程则拒绝
    if (ClientId && ClientId->UniqueProcess == ProtectedPid) {
        return STATUS_ACCESS_DENIED;
    }
    return OriginalNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}

// 安装钩子
VOID HookSSDT() {
    ULONG index = 0x7A; // NtOpenProcess 在 SSDT 中的索引
    KeServiceDescriptorTable->ServiceTable[index] = (ULONG_PTR)MyNtOpenProcess;
}

避坑指南:我曾经在 Win10 上直接写 SSDT 表,结果蓝屏了。后来才发现,Windows 8 以后 SSDT 是只读内存,必须先调用 MmMapIoSpace 或修改 CR0 寄存器关掉写保护。嗯,这个坑我踩过两次才记住。

17.2 Inline Hook:函数体内修改指令

SSDT Hook 虽然好用,但容易被检测。为什么?因为安全软件会监控 SSDT 表有没有被改。那怎么办?我们直接改函数体里的指令——这就是 Inline Hook。

Inline Hook 的思路是:在目标函数开头写入一条 JMP 指令,跳转到我们的函数。执行完我们的逻辑后,再跳回原函数继续执行。

// Inline Hook 核心步骤
// 1. 保存原函数前5个字节
// 2. 写入 JMP 指令(E9 + 偏移)
// 3. 钩子函数执行完毕后,恢复原指令并调用原函数

BYTE originalBytes[5]; // 保存原指令
BYTE jmpCode[5] = {0xE9, 0x00, 0x00, 0x00, 0x00}; // JMP 指令模板

VOID InstallInlineHook(PVOID targetFunc, PVOID hookFunc) {
    // 计算跳转偏移
    ULONG_PTR offset = (ULONG_PTR)hookFunc - (ULONG_PTR)targetFunc - 5;
    *(ULONG*)(jmpCode + 1) = offset;

    // 保存原指令
    RtlCopyMemory(originalBytes, targetFunc, 5);

    // 写入 JMP 指令(注意:需要关中断、改内存属性)
    _disable();
    WriteProtectedMemory(targetFunc, jmpCode, 5);
    _enable();
}

注意:Inline Hook 对多核 CPU 有风险。你在一个核心上改指令,另一个核心可能正在执行这个函数。我建议先挂起所有其他核心,或者用 InterlockedExchange 原子操作。否则你会看到「随机蓝屏」这种最恶心的 bug。

17.3 IDT Hook:中断描述符表钩子

IDT 是处理硬件中断和异常的表。比如键盘中断、缺页异常,都走 IDT。IDT Hook 就是替换某个中断的处理函数。

说实话,IDT Hook 现在用得少了。为什么?因为 Windows 内核已经很少直接暴露 IDT 给驱动开发者。但在某些底层安全产品里,它还是有用武之地。

中断号 用途 钩子场景
0x2E 系统服务中断(旧版) 监控系统调用
0x03 断点中断 调试器检测
0x0E 缺页异常 内存访问监控
// IDT Hook 示例:替换中断处理函数
// 注意:需要知道 IDT 基址,通常通过 SIDT 指令获取
#pragma pack(push, 1)
typedef struct _IDT_ENTRY {
    USHORT OffsetLow;
    USHORT Selector;
    UCHAR  Reserved;
    UCHAR  Flags;
    USHORT OffsetHigh;
} IDT_ENTRY, *PIDT_ENTRY;
#pragma pack(pop)

VOID HookIDT(UCHAR interruptNum, PVOID newHandler) {
    IDT_ENTRY* idt = (IDT_ENTRY*)GetIDTBase();
    ULONG_PTR oldOffset = ((ULONG_PTR)idt[interruptNum].OffsetHigh << 16) | idt[interruptNum].OffsetLow;
    // 替换为新的处理函数地址
    idt[interruptNum].OffsetLow = (USHORT)((ULONG_PTR)newHandler & 0xFFFF);
    idt[interruptNum].OffsetHigh = (USHORT)((ULONG_PTR)newHandler >> 16);
}

个人经验:IDT Hook 最大的问题是兼容性。不同 Windows 版本 IDT 结构可能不一样。我记得在 Win7 x64 上,IDT 条目变成了 16 字节,跟 x86 完全不同。所以如果你要做跨平台,建议用 SSDT 或 Inline Hook 替代。

17.4 钩子检测与绕过

有钩子就有反钩子。安全软件会检测你的钩子,你又要绕过它的检测。这就像猫鼠游戏。

检测方法

  • SSDT 表校验:读取 SSDT 表,对比函数地址是否在 ntoskrnl.exe 范围内。如果不在,说明被钩了。
  • Inline Hook 扫描:检查函数开头 5 个字节是不是 JMPCALL 指令。
  • IDT 完整性检查:对比 IDT 条目是否指向内核模块内的合法地址。

绕过技巧

  • 直接系统调用(Syscall):不经过 SSDT,直接调用 syscall 指令。这样 SSDT Hook 就抓不到你了。
  • 多级跳转:Inline Hook 检测到 JMP 指令?那我在 JMP 之前再加一层跳板,让检测代码看到的是「干净」的函数开头。
  • 硬件断点:用调试寄存器(DR0-DR3)设置断点,而不是修改代码。这样内存校验就查不出来。

核心观点:没有绝对安全的钩子,也没有绝对可靠的检测。你想想看,如果安全软件能检测所有钩子,那 Rootkit 早就绝迹了。反过来,如果钩子能完全隐藏,那杀毒软件也没用了。现实是——双方都在不断进化。

17.5 知识体系总览

下面这张图总结了三种钩子技术的核心流程和检测绕过关系。我建议你把它存下来,写代码时对照着看。

内核钩子技术体系 SSDT Hook Inline Hook IDT Hook 替换SSDT表函数指针 修改系统服务分发表 需处理写保护 修改函数开头JMP指令 保存原字节,执行后恢复 多核需原子操作 替换IDT中断处理函数 兼容性差,版本差异大 x64下结构不同 检测:校验函数地址范围 对比ntoskrnl.exe模块边界 检测:扫描JMP/CALL指令 检查函数开头5字节 检测:IDT条目合法性 检查是否指向内核模块 绕过:直接Syscall调用 绕过:多级跳板隐藏 绕过:硬件断点替代

最后说一句:钩子技术是一把双刃剑。用好了,可以做安全防护、性能监控;用歪了,就是 Rootkit。我个人建议,学习阶段多写代码多调试,别怕蓝屏。蓝屏是最好的老师——每次蓝屏都教会你一个知识点。


公众号:蓝海资料掘金营,微信deep3321