在逆向工程领域,我们之前探讨过用户模式下最简单的Hook原理及MinHook框架。然而,当战场转移到系统内核(Ring 0)时,核心原理虽然不变,但在实现细节上必须进行重大调整。
本文仅从安全研究与防御识别的角度进行技术探讨。
TL;DR:本文将详细解析内核模式下两种基础但经典的Hook技术:Inline Hook与SSDT Hook。
一、基础 Inline Hook 原理与实现
其核心步骤与用户态一致:
- 定位函数地址:找到目标函数在内核中的内存地址。
- 备份原指令:保存函数起始处的若干条完整汇编指令(
n条)。
- 植入跳转:用一段跳转到我们自定义
hook_func的“跳板代码”覆盖原函数起始处的指令。
- 构建蹦床:在保存原指令的“蹦床”区域末尾,添加跳回原函数后续地址的指令,确保原始逻辑能被正确调用。
那么,在内核中我们尝试沿用此思路(注意:本次实验在关闭内核隔离并开启调试器的测试模式下进行,暂时规避了PatchGuard。在现代生产环境的Windows系统上直接操作是不可行的)。
内核中的函数寻址
在内核编程中,可以使用 MmGetSystemRoutineAddress 来获取已导出内核函数的地址。

基础驱动代码如下:
#include<ntifs.h>
#include<windef.h>
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Driver Stopping -> %wZ\n", &DriverObject->DriverName);
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrint("Driver Running -> %wZ\n", &DriverObject->DriverName);
DriverObject->DriverUnload = DriverUnload;
NTSTATUS status = STATUS_SUCCESS;
UNICODE_STRING funcName = { 0 };
RtlInitUnicodeString(&funcName, L"NtOpenFile");
PVOID funcPtr = MmGetSystemRoutineAddress(&funcName);
DbgPrint("[test driver] NtOpenFile at: 0x%p\n", funcPtr);
return status;
}
运行后,可以获取到 NtOpenFile 函数的地址。


关键点:加载两份驱动得到的地址是相同的。这意味着,按照此思路实施的Hook能够拦截所有内核模块(其他驱动)对 NtOpenFile 的调用。
实施 Inline Hook
逆向分析 NtOpenFile 的起始代码,确定需要备份和覆盖17个字节(因为用于跳转的mov rax, addr; jmp rax指令共12字节,覆盖长度必须大于此值)。

详细实现代码请见文末附录。
其中,与用户态Hook一个显著的不同是,需要操作 CR0 寄存器来临时关闭写保护(WP)。
VOID DisableWP()
{
__writecr0(__readcr0() & (~0x10000));
_disable();
}
VOID EnableWP()
{
__writecr0(__readcr0() | 0x10000);
_enable();
}
修改 CR0 寄存器 WP 位的目的是临时关闭CPU对只读内存页的写保护机制。只有这样,内核代码页(如 NtOpenFile 所在的 .text 段)才能被直接修改以植入Hook。


“一旦进入保护模式,便不再存在用于开启或关闭该保护机制的控制位...然而,通过执行以下操作,仍可禁用页级保护:
- 清除控制寄存器 CR0 中的 WP 标志。
- 为每一个页目录项和页表项设置读/写(R/W)标志及用户/管理(U/S)标志。此操作将使每一个页面均变为可写的用户页面,从而在实质上禁用了页级保护功能。”
详细内容可参阅《英特尔® 64 位和 IA-32 架构开发人员手册合订本》第三卷第6章。
Hook效果演示
成功安装Inline Hook后,对 NtOpenFile 的调用会被重定向。
安装Hook后的函数地址:

执行流程进入我们的Hook函数:

Hook函数打印的系统文件访问记录:

二、SSDT Hook 技术详解
SSDT(System Service Descriptor Table,系统服务描述符表)存储了操作系统底层服务的入口点,其中包含许多未导出的函数(无法通过 MmGetSystemRoutineAddress 找到)。
定位 SSDT
这里需要用到 MSR(Model-Specific Register,型号特定寄存器)。MSR是CPU提供的一组特殊寄存器,用于控制或查询处理器的特定功能。其在Intel手册中的定义位于第一卷(3-4 Vol. 1):

“特定型号寄存器(MSRs)——处理器提供多种特定型号寄存器,用于控制和报告处理器的性能。几乎所有的 MSR 均负责处理系统相关功能,且应用程序无法直接访问。此规则的一个例外是时间戳计数器...”
通过读取特定的MSR,我们可以获取 SYSCALL 指令进入内核后的入口点地址。
unsigned __int64 syscall_entry = __readmsr(0xC0000082);
示例驱动代码:
#include<ntifs.h>
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Driver Unloaded\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrint("Driver Loaded\n");
DriverObject->DriverUnload = DriverUnload;
DWORD64 dmsr = __readmsr(0xC0000082);
DbgPrint("KiSystemCall64 at %p\n", dmsr);
return STATUS_SUCCESS;
}
运行结果:

解析 SSDT 结构
在开启内核隔离的情况下,获取到的可能是 KiSystemCall64Shadow,未开启则是 KiSystemCall64。本文实验环境未开启内核隔离。
通过反汇编 KiSystemCall64 并查找其内部的 KiSystemServiceRepeat 例程,可以定位到 KeServiceDescriptorTable(或KeServiceDescriptorTableShadow)的引用。在机器码中,这通常表现为 4C 8D 15(lea r10, [addr])这样的指令模式。

由此可找到SSDT在内存中的地址。其结构定义大致如下:
typedef struct _SYSTEM_SERVICE_TABLE
{
PVOID tableBase;
PVOID serviceCountBase;
ULONG64 numberOfServices;
PVOID unkown;
}SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;
通过手动计算和解析,我们可以找到SSDT的基地址和函数地址表(tableBase)。

- SSDT 地址:
0xfffff805170018c0
- tableBase 地址:
0xfffff805162c79f0
tableBase 是一个指向ULONG数组的指针,每个ULONG值编码了对应系统调用服务函数的偏移量。

服务函数地址计算公式:
函数地址 = tableBase + (tableBase[index] >> 4)
例如,计算第0号函数的地址:
offset = SSDT->tableBase[0] >> 4 = 0x27fe004 >> 4 = 0x27fe000
funcAddr = SSDT->tableBase + offset = 0xfffff805162c79f0 + 0x27fe000 = 0xfffff805165477f0
验证计算结果:

编写程序自动化遍历SSDT:
#include<ntifs.h>
typedef struct _SYSTEM_SERVICE_TABLE
{
PVOID tableBase;
PVOID serviceCountBase;
ULONG64 numberOfServices;
PVOID unkown;
}SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;
LONG64 GetFuncAddr(size_t index, PSYSTEM_SERVICE_TABLE ssdt){
if (index > ssdt->numberOfServices)
return 0;
LONG offset = ((PLONG)(ssdt->tableBase))[index] >> 4;
return (DWORDLONG)(ssdt->tableBase) + offset;
}
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Driver Unloaded\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrint("Driver Loaded\n");
DriverObject->DriverUnload = DriverUnload;
DWORD64 dmsr = __readmsr(0xC0000082);
DbgPrint("KiSystemCall64 at %p\n", dmsr);
PUCHAR tempptr = (PUCHAR)(dmsr);
LONG offset = 0;
PSYSTEM_SERVICE_TABLE table = NULL;
for (size_t i = 0; i < 0x1000; i++)
{
if (*(tempptr + i) == 0x4c && *(tempptr + i + 1) == 0x8d && *(tempptr + i + 2) == 0x15) {
offset = *((PLONG)(tempptr + i + 3));
table = (PSYSTEM_SERVICE_TABLE)(tempptr + i + 7 + offset);
break;
}
}
if (offset == 0) {
DbgPrint("Not found KeServiceDescriptorTable\n");
return STATUS_NOT_FOUND;
}
DbgPrint("KeServiceDescriptorTable at: %p\n tableBase: %p\n", table, table->tableBase);
for (size_t i = 0; i < table->numberOfServices-1; i++)
{
DbgPrint("No.%d func at: %p\n",i, GetFuncAddr(i, table));
}
return STATUS_SUCCESS;
}
运行结果成功列出了SSDT中的函数地址:


如何确定序号对应的函数? 可以通过解析ntdll.dll中syscall stub的汇编代码来获得(例如,mov eax, 0x33 中的0x33就是系统调用号)。
SSDT Hook 尝试与挑战
找到SSDT后,理论上可以通过修改tableBase数组中对应索引的偏移值来实现Hook。
一个未能成功的示例代码(核心逻辑):
// ... 结构体、函数指针等定义 ...
VOID HookSSDT(PSYSTEM_SERVICE_TABLE ssdt)
{
PULONG table = ssdt->tableBase;
ULONG entry = table[NtOpenFileIndex];
ULONG64 base = (ULONG64)table;
ULONG64 original = base + (entry >> 4);
OriginalNtOpenFile = (NTOPENFILE)original;
ULONG param = entry & 0xF;
ULONG64 hookAddr = (ULONG64)HookNtOpenFile;
ULONG newEntry = (ULONG)(((hookAddr - base) << 4) | param);
DisableWP();
table[NtOpenFileIndex] = newEntry;
EnableWP();
}
驱动运行后,虽然显示Hook地址已修改:

但实际调用并未进入我们的函数:

问题根源:tableBase中存储的偏移值是4字节(32位)有符号整数,这限制了其寻址范围。当我们的Hook函数地址与tableBase的距离超过此范围时,计算出的新偏移就会溢出,导致跳转失败。这类似于在Ring3下Inline Hook时,跳转距离超出jmp指令范围的问题。
潜在的解决方案:
- 在SSDT附近部署蹦床:在内核模块中(尽量靠近SSDT)分配一小块内存作为蹦床,SSDT Hook先跳转到这个蹦床,再由蹦床跳转到最终的Hook函数。这是Ring3下MinHook等框架采用的方法。
- 在ntoskrnl模块附近分配内存:尝试在系统内核模块(ntoskrnl.exe)的地址空间附近分配可执行内存作为蹦床。
- 利用未使用空间:在
ntoskrnl.exe或win32k.sys中寻找未使用的代码“空隙”,将蹦床代码写入这些区域(win32k涉及Shadow SSDT,本文不展开)。
当然,对于已导出的函数,直接使用前述的Inline Hook是更简单直接的选择。
附录:Windows 10 内联钩子完整测试代码

#include <ntddk.h>
typedef NTSTATUS(*NTOPENFILE)(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
ULONG ShareAccess,
ULONG OpenOptions
);
NTOPENFILE OriginalNtOpenFile = NULL;
UCHAR OriginalBytes[17];
PVOID Trampoline = NULL;
PVOID TargetFunction = NULL;
VOID DisableWP()
{
ULONG64 cr0 = __readcr0();
cr0 &= 0xfffffffffffeffff;
__writecr0(cr0);
_disable();
}
VOID EnableWP()
{
ULONG64 cr0 = __readcr0();
cr0 |= 0x10000;
__writecr0(cr0);
_enable();
}
NTSTATUS HookNtOpenFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
ULONG ShareAccess,
ULONG OpenOptions
)
{
if (ObjectAttributes && ObjectAttributes->ObjectName)
{
DbgPrint("InlineHook NtOpenFile: %wZ\n",
ObjectAttributes->ObjectName);
}
return OriginalNtOpenFile(
FileHandle,
DesiredAccess,
ObjectAttributes,
IoStatusBlock,
ShareAccess,
OpenOptions
);
}
VOID BuildTrampoline()
{
Trampoline = ExAllocatePool2(
POOL_FLAG_NON_PAGED_EXECUTE,
32,
'HKTN');
RtlCopyMemory(Trampoline, OriginalBytes, 17);
UCHAR* p = (UCHAR*)Trampoline + 17;
p[0] = 0x48;
p[1] = 0xB8;
*(PVOID*)(p + 2) = (PUCHAR)TargetFunction + 17;
p[10] = 0xFF;
p[11] = 0xE0;
OriginalNtOpenFile = (NTOPENFILE)Trampoline;
}
VOID InstallInlineHook()
{
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"NtOpenFile");
TargetFunction = MmGetSystemRoutineAddress(&name);
if (!TargetFunction)
return;
RtlCopyMemory(OriginalBytes, TargetFunction, 17);
BuildTrampoline();
DisableWP();
UCHAR patch[17];
patch[0] = 0x48;
patch[1] = 0xB8;
*(PVOID*)(patch + 2) = HookNtOpenFile;
patch[10] = 0xFF;
patch[11] = 0xE0;
patch[12] = 0x90;
patch[13] = 0x90;
patch[14] = 0x90;
patch[15] = 0x90;
patch[16] = 0x90;
RtlCopyMemory(TargetFunction, patch, 17);
EnableWP();
}
VOID RemoveInlineHook()
{
if (!TargetFunction)
return;
DisableWP();
RtlCopyMemory(TargetFunction, OriginalBytes, 17);
EnableWP();
if (Trampoline)
ExFreePool(Trampoline);
}
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
RemoveInlineHook();
DbgPrint("Inline hook removed\n");
}
NTSTATUS DriverEntry(
PDRIVER_OBJECT DriverObject,
PUNICODE_STRING RegistryPath
)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
InstallInlineHook();
DbgPrint("Inline hook installed\n");
return STATUS_SUCCESS;
}
本文介绍了两种基础的内核Hook技术,希望能为你在云栈社区进行安全技术探讨时提供一些思路。理解攻击原理,是构建有效防御的第一步。