前言
虽然在 Windows Vista 以后的 64 位操作系统中,PatchGuard(内核补丁保护机制)对 SSDT(System Service Dispatch Table,系统服务分派表)实施了强力保护,直接 Hook SSDT 的方式几乎不可行;但是在许多旧系统中,32 位的 SSDT Hook 仍然是合法可行的手段。
此外,即便不能直接用于现代系统,SSDT Hook 的实现机制是理解系统调用流程、内核函数转发、内核保护绕过的基础知识。对于从事安全研究、驱动开发、逆向工程的人来说,它是不可绕过的一块内容。
Windows系统调用
Windows 系统调用是用户态程序访问操作系统核心功能(如打开文件、启动进程、分配内存等)的唯一合法通道。具体流程如下:
- 用户态程序调用
kernel32.dll
/user32.dll
等 API; - 这些 API 内部再调用
ntdll.dll
提供的Nt*
或Zw*
系统调用接口; ntdll.dll
使用syscall
(64 位)或int 0x2e
(32 位)将调用转入内核态;- 内核通过调用一系列的内部函数找到
SSDT
中对应的函数指针,执行实际内核函数。
SSDT 就是这张“索引表”,其每个项都是 nt!Nt*
函数的地址。
系统调用号
上一节中提到ntdll.dll
使用 syscall
或 int 0x2e
进入内核态时,其中一个关键参数就是**“系统调用号”(System Call Index)**,它用来在 SSDT 或 Shadow SSDT 中查找目标函数的入口地址。
由于系统调用号在不同版本的 Windows 中的值不一定相同。这意味着不推荐硬编码系统调用号(除非针对特定版本)。
从ZwCreateFile
的反汇编代码中可以看出,通过B8[mov eax] 55 00 00 00[0x55h]
指令将系统调用号赋值给eax寄存器,因此可以通过读取系统调用接口的反汇编指令获取对应的调用号。
// 获取ntdll.dll导出表
PIMAGE_EXPORT_DIRECTORY export_table = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)dos_header + nt_header->OptionalHeader.DataDirectory[0].VirtualAddress);
PULONG func_name_addr = (PULONG)((PUCHAR)dos_header + export_table->AddressOfNames);
PUSHORT func_ordinal_addr = (PUSHORT)((PUCHAR)dos_header + export_table->AddressOfNameOrdinals);
PULONG func_addr = (PULONG)((PUCHAR)dos_header + export_table->AddressOfFunctions);for (ULONG i = 0; i < export_table->NumberOfNames; i++) {PCHAR export_func_name = (PCHAR)((PUCHAR)dos_header + func_name_addr[i]);// 遍历导出函数表 找到对应的函数if (0 == _strnicmp(func_name, export_func_name, strlen(func_name))) {// 获取导出函数地址PULONG entry_point = (PULONG)((PUCHAR)dos_header + func_addr[func_ordinal_addr[i]]);// 匹配特征值 获取索引
#ifdef _WIN64// move r10, rcx// move raxif (entry_point[0] == 0xB8D18B4C) {return entry_point[1];}
#else// move raxif (*(PUCHAR)entry_point == 0xB8) {return *(ULONG*)((PUCHAR)entry_point + 1);}
#endifbreak;}
}
符号表
在Windows操作系统中,存在两张符号表SSDT和Shadow SSDT。其中 SSDT位于内核模块 ntoskrnl.exe
中,映射的是 ntdll.dll
中以 Nt
和 Zw
开头的系统调用函数(如 NtOpenProcess
、ZwCreateFile
);而Shadow SSDT则位于内核图形子系统模块 win32k.sys
中,专门用于处理的是 GUI 子系统调用(如 NtUserCreateWindowEx
、NtGdiBitBlt
)。
#pragma pack(1)
typedef struct _SERVICE_DESCIPTOR_TABLE {PLONG ServiceTableBase; // SSDT base addressPVOID ServiceCounterTableBase; //
#ifdef _WIN64ULONGLONG NumberOfService; // Number of SSDT services
#elseULONG NumberOfService; // Number of SSDT services
#endifPVOID ParamTableBase; // Base address of the system service parameter table
} SSDTEntry, *PSSDTEntry;
#pragma pack()
不管是SSDT,还是Shadow SSDT,其数据结构都是完全一致的。其中所有的导出函数地址都按照系统调用号的顺序存储在ServiceTableBase
指向的ULONG
数组中,在32位系统中这里存储着函数的绝对地址,而在64位系统中,这里存储的则是相对于SSDT基址的“偏移”。
修改只读内存
在获取了SSDT基址和系统调用号之后,需要做的就是将自身的Hook函数替换SSDT表中原本的系统调用,而由于在Windows 内核中, SSDT 表被标记为只读(Read-Only),尝试直接修改将触发保护异常。因此,在实现 SSDT Hook 前,需要一定的手段规避写保护机制。
- [修改
CR0
](x64驱动 关闭&开启 写时保护_wpoffx64-CSDN博客)
// 关闭写保护
KIRQL WPOFFx64()
{// 提高中断请求优先级到 DISPATCH_LEVEL 级别KIRQL irql = KeRaiseIrqlToDpcLevel();// 获取 cr0 寄存器中的值(cr0 控制 cpu 的操作模式)UINT64 cr0 = __readcr0();// 修改写保护cr0 &= 0xfffffffffffeffff;__writecr0(cr0);// 禁用中断_disable();return irql;
}// 开启写保护
void WPONx64(KIRQL irql)
{// 获取 cr0 中的值UINT64 cr0 = __readcr0();// 恢复写保护cr0 |= 0x10000;__writecr0(cr0);// 恢复中断_enable();// 恢复 IRQL 到原始值KeLowerIrql(irql);
}KIRQL kIrql = WPOFFx64();
// 此处修改内存数据
WPONx64(kIrql);
MDL
映射
bool UpdateNonPagedMemory(void* target, void* data, unsigned long length) {PMDL mdl = NULL;__try {if (target == nullptr || data == nullptr) return false;if (length == 0) return true;// 为指定地址申请MDLmdl = IoAllocateMdl(target, length, FALSE, FALSE, NULL);if (mdl == NULL) {return false;}// 判断IRQL级别,大于APC直接返回if (KeGetCurrentIrql() > APC_LEVEL) {return false;}// 更新MDL以描述基础物理页MmBuildMdlForNonPagedPool(mdl); // 将MDL物理页映射到一个虚拟页面PVOID mapping_addr = MmMapLockedPagesSpecifyCache(mdl, KernelMode, MmNonCached, NULL, FALSE, NormalPagePriority); //if (!mapping_addr) {return false;}// 更新内存RtlCopyMemory(mapping_addr, data, length);MmUnmapLockedPages((PVOID)mapping_addr, mdl);return true;} __finally {if (mdl) IoFreeMdl(mdl);}
}