10、SSDT与系统服务:SSDT结构、系统服务号、Hook SSDT、Shadow SSDT、Inline Hook
好,咱们今天聊点硬核的——SSDT。这东西,说白了就是Windows内核给用户态程序开的一扇门。你想想看,一个普通的exe怎么调用到内核里?比如CreateFile,最终会走到NtCreateFile,而这个NtCreateFile的地址,就存在SSDT里。
我刚开始做驱动开发那会儿,对SSDT的理解也就停留在“一个表”的层面。直到有一次,我想拦截某个系统调用,结果蓝屏了一整天……嗯,从那以后,我才真正开始研究它的结构。
10.1 SSDT的结构
SSDT全称是System Service Dispatch Table。它的核心就是一个函数指针数组。每个系统服务号对应一个函数地址。Windows内核通过这个表,把用户态的请求分发到对应的内核函数。
它的结构大致是这样的:
typedef struct _SERVICE_DESCRIPTOR_TABLE {
PVOID ServiceTableBase; // SSDT基地址
PVOID ServiceCounterTable; // 服务计数器(调试用)
ULONG NumberOfServices; // 服务数量
PVOID ParamTableBase; // 参数表基地址
} SERVICE_DESCRIPTOR_TABLE, *PSERVICE_DESCRIPTOR_TABLE;
这里要注意,ServiceTableBase指向的就是那个函数指针数组。每个指针占4字节(32位系统)或8字节(64位系统)。NumberOfServices告诉你这个表里有多少个服务。
关键点:Windows有两个SSDT表。一个是KeServiceDescriptorTable,另一个是KeServiceDescriptorTableShadow。前者管内核服务,后者还管Win32k.sys的GUI服务。
10.2 系统服务号
每个系统调用都有一个唯一的服务号。比如NtCreateFile的服务号是0x55(在Windows 10 x64上可能不同)。这个服务号就是SSDT数组的索引。
用户态调用系统服务时,会把服务号放在eax寄存器里,然后执行syscall指令。内核的KiSystemServiceHandler会根据eax里的值,去SSDT里找对应的函数地址。
我曾在项目中遇到过一个问题:不同Windows版本的服务号不一样。比如Windows 7和Windows 10,同一个NtCreateFile的服务号可能差了好几个。所以写Hook代码时,千万别写死服务号,得动态获取。
小技巧:可以用ZwQuerySystemInformation来枚举系统服务,或者直接解析ntoskrnl.exe的导出表,找到NtCreateFile的地址,然后反推服务号。
10.3 Hook SSDT
Hook SSDT,说白了就是修改SSDT表里的函数指针,让它指向你自己的函数。这样当用户态调用某个系统服务时,就会先执行你的代码。
基本步骤是这样的:
- 获取KeServiceDescriptorTable的地址
- 找到你要Hook的服务号对应的表项
- 把原来的函数地址保存下来
- 把表项改成你的Hook函数地址
- 在你的Hook函数里,处理完逻辑后,调用原函数
代码示例:
// 保存原函数地址
typedef NTSTATUS (*NTCREATEFILE)(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength
);
NTCREATEFILE OriginalNtCreateFile = NULL;
// Hook函数
NTSTATUS HookNtCreateFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength
) {
// 在这里做你的拦截逻辑
DbgPrint("File opened: %wZ\n", ObjectAttributes->ObjectName);
// 调用原函数
return OriginalNtCreateFile(
FileHandle, DesiredAccess, ObjectAttributes,
IoStatusBlock, AllocationSize, FileAttributes,
ShareAccess, CreateDisposition, CreateOptions,
EaBuffer, EaLength
);
}
// 安装Hook
VOID InstallHook() {
ULONG serviceIndex = 0x55; // NtCreateFile的服务号
OriginalNtCreateFile = (NTCREATEFILE)
KeServiceDescriptorTable->ServiceTableBase[serviceIndex];
// 注意:需要先关闭写保护
_disable();
KeServiceDescriptorTable->ServiceTableBase[serviceIndex] = HookNtCreateFile;
_enable();
}
警告:修改SSDT需要关闭CR0寄存器的写保护位。在64位系统上,还需要禁用PatchGuard。否则系统会蓝屏给你看。我曾经在Windows 10上试过,没关PatchGuard,结果系统直接崩溃了。
10.4 Shadow SSDT
Shadow SSDT是KeServiceDescriptorTableShadow。它比普通的SSDT多了一个表,专门处理Win32k.sys的GUI服务。比如NtUserCreateWindowEx、NtGdiBitBlt这些。
为什么要单独搞一个Shadow SSDT?因为GUI服务涉及到窗口管理、绘图这些,跟普通的内核服务不一样。它们需要访问Win32k.sys里的数据结构。
Hook Shadow SSDT的步骤跟普通SSDT差不多,但有个坑:Shadow SSDT的地址不是导出的。你得自己找。我记得有一种方法是通过解析KTHREAD结构里的ServiceTable字段来定位。
// 获取Shadow SSDT的地址
PVOID GetShadowSSDT() {
// 通过当前线程的KTHREAD结构获取
PKTHREAD currentThread = KeGetCurrentThread();
PSERVICE_DESCRIPTOR_TABLE shadowTable =
(PSERVICE_DESCRIPTOR_TABLE)currentThread->ServiceTable;
// 注意:ServiceTable[1]才是Shadow SSDT
return shadowTable[1].ServiceTableBase;
}
提示:在64位系统上,Shadow SSDT的Hook更复杂。因为PatchGuard会检查SSDT的完整性。我建议用Inline Hook代替直接修改表项。
10.5 Inline Hook
Inline Hook,说白了就是直接修改目标函数的代码。在函数开头写入一个jmp指令,跳转到你的Hook函数。这样就不需要动SSDT表了。
它的好处是:
- 不需要修改SSDT,避免被PatchGuard检测
- 可以Hook任意内核函数,不限于系统服务
- 更灵活,可以只Hook函数的某一部分
但缺点也很明显:
- 需要处理指令对齐,否则会破坏函数逻辑
- 多核环境下需要同步
- 如果函数被多次Inline Hook,容易出问题
代码示例:
// Inline Hook的jmp指令
// x64: jmp [rip+offset]
BYTE jmpCode[] = {
0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // jmp [rip+0]
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // 目标地址
};
VOID InstallInlineHook(PVOID targetFunc, PVOID hookFunc) {
// 保存原函数的前几个字节
memcpy(originalBytes, targetFunc, sizeof(jmpCode));
// 写入jmp指令
// 注意:需要先关闭中断和写保护
_disable();
memcpy(targetFunc, jmpCode, sizeof(jmpCode));
_enable();
}
VOID UninstallInlineHook(PVOID targetFunc) {
// 恢复原函数
_disable();
memcpy(targetFunc, originalBytes, sizeof(jmpCode));
_enable();
}
注意:Inline Hook时,一定要确保你覆盖的指令是完整的。比如一个指令是5字节,你只覆盖了3字节,那函数执行到一半就崩了。我曾经因为这个原因,调试了整整两天。
10.6 知识体系图
下面这张图展示了SSDT相关的核心概念和它们之间的关系:
10.7 避坑指南
做SSDT Hook,有几个坑是绕不开的:
- PatchGuard:64位系统上,PatchGuard会定期检查SSDT的完整性。一旦发现被修改,直接蓝屏。我建议用Inline Hook或者直接绕过PatchGuard(但风险很大)。
- 多核同步:修改SSDT时,如果另一个核心正在调用同一个服务,可能会出问题。最好在修改前暂停所有核心。
- 版本兼容性:不同Windows版本的服务号不一样。写代码时一定要动态获取,别写死。
- 函数原型:Hook函数的参数和返回值必须跟原函数完全一致。否则栈会乱掉。
个人经验:我建议初学者先从Inline Hook入手。它比直接修改SSDT更安全,也更容易调试。等熟悉了内核调用的流程,再尝试SSDT Hook。
好了,SSDT这块的内容就讲到这里。记住,Hook不是目的,理解系统服务的调用流程才是关键。下次遇到需要拦截系统调用的需求,你就能游刃有余了。