TitanHide与ScyllaHide的Hook区别
TitanHide和ScyllaHide的核心Hook区别在于Hook层级、实现原理和作用范围,前者是内核层(R0)的SSDT表修改,后者是用户层(R3)的指令流替换。
ScyllaHide
宏定义

这些宏是ScyllaHide中批量、标准化实现函数挂钩(Hook) 的核心代码,目的是简化重复的Hook/解钩代码编写,统一错误处理和资源管理逻辑。
STR(x)
1
这是一个字符串化宏,它会把输入的参数
x直接转换成C风格的字符串常量。
例如:STR(NtOpenProcess)会被预处理器替换为"NtOpenProcess"
在后面的钩子宏里,用来把函数名转为字符串,传递给Detours函数。HOOK(name)
1
2
3
4创建一个标准的
Detours钩子,并处理错误。hdd->d##name:##是宏的连接符,它会把d和传入的name拼接成一个变量名,比如name是NtOpenProcess,就变成hdd->dNtOpenProcess,用来保存原始函数的备份地址。t_##name:把t_和name拼接成函数指针类型,比如t_NtOpenProcess,用来做类型转换。
DetourCreateRemote:这是ScyllaHide封装的Detours函数,在远程进程中创建钩子。"" STR(name) "":用STR把函数名转成字符串,比如NtOpenProcess变成"NtOpenProcess",作为被Hook函数的名称。(void*)_##name:_##name指向原始的系统函数地址,比如_NtOpenProcess。Hooked##name:拼接成我们的钩子函数名,比如HookedNtOpenProcess,当目标函数被调用时会先执行这个函数。true:表示创建Trampoline(跳板),这是Detours用来保存原始指令、保证函数能正常执行的机制。&hdd->name##BackupSize:用来保存被Hook函数开头被覆盖的指令长度,后续恢复时需要用到。if (hdd->d##name == nullptr) { return false; }:如果钩子创建失败,直接返回false终止流程。
HOOK_NATIVE(name)
1
2
3
4和
HOOK几乎一样,只是它调用的是DetourCreateRemoteNative。DetourCreateRemoteNative是专门为Native API(即ntdll.dll导出的原生系统调用)设计的钩子函数,处理这类函数时兼容性和稳定性更好。HOOK_NATIVE_NOTRAMP(name)
1
创建一个 不带
Trampoline(跳板)的Native API钩子。FREE_HOOK(name)
1
释放钩子占用的内存,并重置备份地址。
RESTORE_JMP(name)
1
恢复被
Hook函数的原始指令,让它回到未被Hook的状态。

根据当前编译的目标系统(32位/64位),为DetourCreateRemoteNative这个宏选择不同的底层实现函数。
本文仅分析64位系统上的Native API钩子逻辑(即普通函数钩子逻辑DetourCreateRemote)
DetourCreateRemote
1 | void * DetourCreateRemote(void * hProcess, const char* funcName, void * lpFuncOrig, void * lpFuncDetour, bool createTramp, DWORD * backupSize) |
实现了Windows平台下对进程内函数的内联钩子(Inline Hook) 逻辑,
在指定的远程进程(hProcess)中,对目标函数(lpFuncOrig)创建内联钩子,将其执行流程重定向到自定义的钩子函数(lpFuncDetour),同时可选创建跳板(Trampoline)保留原始函数逻辑。
TitanHide
SSDT 定位:SSDTfind()
SSDT(System Service Descriptor Table)是内核中存储系统调用函数地址的表,SSDTfind()的作用是找到内核中SSDT的实际地址(32/64位逻辑不同)。
32 位系统(x86)
1
2
3UNICODE_STRING routineName;
RtlInitUnicodeString(&routineName, L"KeServiceDescriptorTable");
SSDT = (SSDTStruct*)MmGetSystemRoutineAddress(&routineName);32位系统中,KeServiceDescriptorTable是内核导出的全局变量,直接通过MmGetSystemRoutineAddress(内核版GetProcAddress)获取其地址即可。64 位系统(x64)
64位系统中KeServiceDescriptorTable不能直接导出,需要通过特征码扫描定位:- 获取内核基址:通过
Undocumented::GetKernelBase()获取内核镜像(ntoskrnl.exe)基地址和大小; - 找到
.text段:遍历PE节表,找到.text节(内核核心代码段); - 扫描
KiSystemServiceStart特征码:
KiSystemServiceStart是内核处理系统调用的入口函数,其开头有固定指令序列(特征码 {0x8B, 0xF8, 0xC1, 0xEF, 0x07, …})。
遍历.text段,匹配该特征码找到KiSystemServiceStart的位置。 - 提取
SSDT地址:
KiSystemServiceStart中会有lea r10,KeServiceDescriptorTable指令(机器码4C 8D 15[相对偏移])。
解析该指令的相对偏移,计算出KeServiceDescriptorTable的实际地址,即SSDT的地址。
- 获取内核基址:通过
SSDT Hook安装:SSDT::Hook()
SSDT Hook的核心是修改SSDT表中目标系统调用对应的函数地址,但32/64位系统的实现差异极大:
前置准备
无论32/64位,都需要先完成:
调用SSDTfind()获取SSDT表地址。
通过NTDLL::GetExportSsdtIndex(apiname)获取目标API在SSDT表中的索引(比如NtOpenProcess对应的索引)。
校验索引的合法性(不超过SSDT表的服务数量)。32位系统(x86)Hook逻辑1
2
3
4
5
6
7
8
9// 直接修改 SSDT 表中的函数地址
newValue = (ULONG)newfunc;
// 分配 HOOK 结构体,保存原始值(用于后续卸载)
hHook = (HOOK)RtlAllocateMemory(true, sizeof(HOOKSTRUCT));
hHook->SSDTindex = FunctionIndex;
hHook->SSDTold = oldValue;
hHook->SSDTnew = newValue;
// 写入新地址到 SSDT 表
RtlSuperCopyMemory(&SSDT->pServiceTable[FunctionIndex], &newValue, sizeof(newValue));32位SSDT表中直接存储函数的物理地址,因此直接将目标索引对应的条目替换为自定义Hook函数地址即可。64位系统(x64)Hook逻辑
x64 系统的SSDT地址计算
x64 中 SSDT 表存储的不是直接地址,而是偏移值,1
原始函数地址 = (SSDT表项值 >> 4) + SSDT基地址
例如:
SSDT基地址:SSDTbase = (ULONG_PTR)SSDT->pServiceTable(比如0xFFFFF80000000000);SSDT表项值:SSDT->pServiceTable[readOffset](比如0x12345670);
右移4位:0x12345670 >> 4 = 0x1234567(清除低4位标志位);
实际地址 =0xFFFFF80000000000 + 0x1234567 = 0xFFFFF8001234567;x64 SSDT表项的低4位是 “标志位”(记录函数属性),不是地址的一部分,必须清除才能得到真实偏移。
间接Hook
间接Hook的前提,核心原因是PatchGuard(补丁保护):x86系统:无PatchGuard,直接修改SSDT表项(把函数地址换成Hook函数)不会触发系统保护;x64系统:Windows引入PatchGuard(也叫kGuard),会定期检查内核关键结构(包括SSDT表、内核代码段)的完整性:
如果直接修改SSDT表项指向自定义Hook函数(非内核原生地址),PatchGuard会检测到篡改,直接触发蓝屏(BSOD);
如果直接修改内核代码段(比如内联Hook),同样会被PatchGuard检测到。
因此,x64下的SSDT Hook必须走 “间接路线”——代码洞穴(Code Cave) 中转,让SSDT表项仍然指向内核原生内存区域,绕开PatchGuard检测。
代码洞穴(Code Cave)
代码洞穴是指内核代码段(.text节)中连续的、无意义的空闲内存区域,通常是:
0x90(NOP指令):空操作,执行后无任何效果;
0xCC(INT3指令):断点指令,未被使用时是空闲的;
这些区域属于内核原生内存,PatchGuard不会检测其内容修改(只要不破坏核心代码)。
代码洞穴的作用(x64 Hook中):
在洞穴中写入跳转指令(JMP),指向我们的自定义Hook函数;
修改SSDT表项,让其指向这个洞穴(而非直接指向Hook函数);
系统调用NtXXX时,流程变为:
1 | 用户层调用 NtQueryInformationProcess → 查 SSDT 表 → 跳转到代码洞穴 → 执行 JMP 到 Hook 函数 → 执行完后可选跳回原始函数 |
整个过程中,SSDT表项指向的仍然是内核原生内存(洞穴),PatchGuard不会触发保护,完美绕开检测。
x64 Hook(代码洞穴)的完整实现
找到代码洞穴(
FindCaveAddress函数)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15static PVOID FindCaveAddress(PVOID CodeStart, ULONG CodeSize, ULONG CaveSize)
{
unsigned char* Code = (unsigned char*)CodeStart;
// 遍历内核代码段,找连续的 NOP/INT3 区域
for(unsigned int i = 0, j = 0; i < CodeSize; i++)
{
if(Code[i] == 0x90 || Code[i] == 0xCC) // 匹配 NOP 或 INT3
j++; // 连续计数
else
j = 0; // 中断则重置计数
if(j == CaveSize) // 找到满足大小的连续区域
return (PVOID)((ULONG_PTR)CodeStart + i - CaveSize + 1);
}
return 0;
}在洞穴中写入跳转指令(
Hooklib::Hook)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 伪代码(Hooklib::Hook 核心逻辑)
bool Hooklib::Hook(PVOID CaveAddress, PVOID NewFunc)
{
// 1. 修改洞穴内存保护属性(只读 → 可写可执行)
ULONG oldProtect;
ZwProtectVirtualMemory(NtCurrentProcess(), &CaveAddress, &CaveSize, PAGE_EXECUTE_READWRITE, &oldProtect);
// 2. 构造 x64 绝对跳转指令(JMP NewFunc)
// x64 绝对跳转指令格式:0xFF 0x25 0x00 0x00 0x00 0x00 + 64位地址
unsigned char jmpCode[] = {0xFF, 0x25, 0x00, 0x00, 0x00, 0x00};
RtlCopyMemory(CaveAddress, jmpCode, 6); // 写入跳转前缀
RtlCopyMemory((PBYTE)CaveAddress + 6, &NewFunc, 8); // 写入 Hook 函数地址
// 3. 恢复内存保护属性
ZwProtectVirtualMemory(NtCurrentProcess(), &CaveAddress, &CaveSize, oldProtect, &oldProtect);
return true;
}构造符合
x64 SSDT规则的新表项值1
2
3
4
5
6
7
8
9
10
11
12
13
14// 1. 计算洞穴地址相对于 SSDT 基地址的偏移
newValue = (LONG)((ULONG_PTR)CaveAddress - SSDTbase);
// 示例:
// CaveAddress = 0xFFFFF80011111111(洞穴地址)
// SSDTbase = 0xFFFFF80000000000(SSDT 表基地址)
// 偏移 = 0x1111111
// 2. 左移 4 位:把偏移值放到高 60 位,低 4 位留空
// 3. 保留原始表项的低 4 位标志位(oldValue & 0xF)
newValue = (newValue << 4) | oldValue & 0xF;
// 示例:
// 偏移 0x1111111 <<4 = 0x11111110
// 原始标志位 oldValue &0xF = 0x0(假设)
// 最终 newValue = 0x11111110修改
SSDT表项为新值1
2// 内存拷贝(带保护检查),修改 SSDT 表项
RtlSuperCopyMemory(&SSDT->pServiceTable[FunctionIndex], &newValue, sizeof(newValue));
卸载 Hook(恢复 SSDT 表项):SSDT::Unhook ()
将SSDT表项恢复为原始值,x86/x64差异仅在内存释放:
1 | // 恢复原始表项 |
