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

ScyllaHide

宏定义


这些宏是ScyllaHide中批量、标准化实现函数挂钩(Hook) 的核心代码,目的是简化重复的Hook/解钩代码编写,统一错误处理和资源管理逻辑。

  1. STR(x)

    1
    #define STR(x) #x

    这是一个字符串化宏,它会把输入的参数x直接转换成C风格的字符串常量。
    例如:STR(NtOpenProcess)会被预处理器替换为"NtOpenProcess"
    在后面的钩子宏里,用来把函数名转为字符串,传递给Detours函数。

  2. HOOK(name)

    1
    2
    3
    4
    #define HOOK(name) { \
    hdd->d##name = (t_##name)DetourCreateRemote(hProcess, "" STR(name) "", (void*)_##name, Hooked##name, true, &hdd->name##BackupSize); \
    if (hdd->d##name == nullptr) { return false; } \
    }

    创建一个标准的Detours钩子,并处理错误。
    hdd->d##name##是宏的连接符,它会把d和传入的name拼接成一个变量名,比如nameNtOpenProcess,就变成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终止流程。

  1. HOOK_NATIVE(name)

    1
    2
    3
    4
    #define HOOK_NATIVE(name) { \
    hdd->d##name = (t_##name)DetourCreateRemoteNative(hProcess, "" STR(name) "", (void*)_##name, Hooked##name, true, &hdd->name##BackupSize); \
    if (hdd->d##name == nullptr) { return false; } \
    }

    HOOK几乎一样,只是它调用的是DetourCreateRemoteNative
    DetourCreateRemoteNative是专门为Native API(即ntdll.dll导出的原生系统调用)设计的钩子函数,处理这类函数时兼容性和稳定性更好。

  2. HOOK_NATIVE_NOTRAMP(name)

    1
    #define HOOK_NATIVE_NOTRAMP(name) DetourCreateRemoteNative(hProcess, "" STR(name) "", (void*)_##name, Hooked##name, false, &hdd->name##BackupSize)

    创建一个 不带Trampoline(跳板)Native API钩子。

  3. FREE_HOOK(name)

    1
    #define FREE_HOOK(name) FreeMemory(hProcess, (void*)hdd->d##name); hdd->d##name = 0

    释放钩子占用的内存,并重置备份地址。

  4. RESTORE_JMP(name)

    1
    #define RESTORE_JMP(name) RestoreJumper(hProcess, (void*)_##name, (void*)hdd->d##name, hdd->name##BackupSize)

    恢复被Hook函数的原始指令,让它回到未被Hook的状态。


根据当前编译的目标系统(32位/64位),为DetourCreateRemoteNative这个宏选择不同的底层实现函数。
本文仅分析64位系统上的Native API钩子逻辑(即普通函数钩子逻辑DetourCreateRemote

DetourCreateRemote

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
void * DetourCreateRemote(void * hProcess, const char* funcName, void * lpFuncOrig, void * lpFuncDetour, bool createTramp, DWORD * backupSize)
{
//(1)初始化与前置检查
BYTE originalBytes[50] = { 0 };// 存储目标函数开头的原始字节
BYTE tempSpace[1000] = { 0 };// 临时缓冲区,用于构造跳转指令
PBYTE trampoline = 0;// 跳板地址(保存原始函数开头+跳转回原函数)
DWORD protect; // 用于修改内存保护属性

bool success = false;

if (fatalFindSyscallIndexFailure || fatalAlreadyHookedFailure)// 前置失败标记检查:避免重复弹窗报错
return nullptr; // Don't spam user with repeated error message boxes

//(2)读取目标函数的原始字节
if (!ReadProcessMemory(hProcess, lpFuncOrig, originalBytes, sizeof(originalBytes), nullptr))
{
MessageBoxA(nullptr, "DetourCreateRemote->ReadProcessMemory failed.", "ScyllaHide", MB_ICONERROR);
return nullptr;
}

ClearSyscallBreakpoint(funcName, originalBytes);

// Note that this check will give a false negative in the case that a function is hooked *and* has a breakpoint set on it (now cleared).
// We can clear the breakpoint or detect the hook, not both. (If the hook is ours, this is actually a hack because we should be properly unhooking)
//(3)检测目标函数是否已被 Hook
#ifdef _WIN64
const bool isHooked = (originalBytes[0] == 0xFF && originalBytes[1] == 0x25) ||
(originalBytes[0] == 0x90 && originalBytes[1] == 0xFF && originalBytes[2] == 0x25);
#else
const bool isHooked = originalBytes[0] == 0xE9;
#endif
if (isHooked)
{
fatalAlreadyHookedFailure = true;
char errorMessage[256];
_snprintf_s(errorMessage, sizeof(errorMessage), sizeof(errorMessage) - sizeof(char),
"Error: %hs is already hooked!", funcName);
MessageBoxA(nullptr, errorMessage, "ScyllaHide", MB_ICONERROR);
return nullptr;
}

//(4)计算需要 Hook 的指令长度
int detourLen = GetDetourLen(originalBytes, minDetourLen);

//(5)创建跳板(Trampoline)
if (createTramp)
{
*backupSize = detourLen;

trampoline = (PBYTE)VirtualAllocEx(hProcess, 0, detourLen + minDetourLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!trampoline)
return 0;

WriteProcessMemory(hProcess, trampoline, originalBytes, detourLen, 0);

ZeroMemory(tempSpace, sizeof(tempSpace));
WriteJumper(trampoline + detourLen, (PBYTE)lpFuncOrig + detourLen, tempSpace, false);
WriteProcessMemory(hProcess, trampoline + detourLen, tempSpace, minDetourLen, 0);
VirtualProtectEx(hProcess, trampoline, detourLen + minDetourLen, PAGE_EXECUTE_READ, &protect);
}

//(6)写入钩子跳转指令
if (VirtualProtectEx(hProcess, lpFuncOrig, detourLen, PAGE_EXECUTE_READWRITE, &protect))
{
// 1. 清空临时缓冲区,构造跳转到钩子函数的指令
ZeroMemory(tempSpace, sizeof(tempSpace));
WriteJumper((PBYTE)lpFuncOrig, (PBYTE)lpFuncDetour, tempSpace, scl::IsWindows64() && !scl::IsWow64Process(NtCurrentProcess));
// 2. 把跳转指令写入目标函数开头(覆盖原始指令)
WriteProcessMemory(hProcess, lpFuncOrig, tempSpace, minDetourLen, 0);

// 3. 恢复目标函数的内存保护属性
VirtualProtectEx(hProcess, lpFuncOrig, detourLen, protect, &protect);
success = true;
}

//(7)收尾与返回
if (createTramp)
{
if (!success)
{
VirtualFree(trampoline, 0, MEM_RELEASE);
trampoline = 0;
}
return trampoline;
}
else
{
return 0;
}
}

实现了Windows平台下对进程内函数的内联钩子(Inline Hook) 逻辑,
在指定的远程进程(hProcess)中,对目标函数(lpFuncOrig)创建内联钩子,将其执行流程重定向到自定义的钩子函数(lpFuncDetour),同时可选创建跳板(Trampoline)保留原始函数逻辑。

TitanHide

SSDT 定位:SSDTfind()

SSDT(System Service Descriptor Table)是内核中存储系统调用函数地址的表,SSDTfind()的作用是找到内核中SSDT的实际地址(32/64位逻辑不同)。

  1. 32 位系统(x86)

    1
    2
    3
    UNICODE_STRING routineName;
    RtlInitUnicodeString(&routineName, L"KeServiceDescriptorTable");
    SSDT = (SSDTStruct*)MmGetSystemRoutineAddress(&routineName);

    32位系统中,KeServiceDescriptorTable是内核导出的全局变量,直接通过MmGetSystemRoutineAddress(内核版GetProcAddress)获取其地址即可。

  2. 64 位系统(x64)
    64位系统中KeServiceDescriptorTable不能直接导出,需要通过特征码扫描定位:

    1. 获取内核基址:通过Undocumented::GetKernelBase()获取内核镜像(ntoskrnl.exe)基地址和大小;
    2. 找到.text段:遍历PE节表,找到.text节(内核核心代码段);
    3. 扫描KiSystemServiceStart特征码:
      KiSystemServiceStart是内核处理系统调用的入口函数,其开头有固定指令序列(特征码 {0x8B, 0xF8, 0xC1, 0xEF, 0x07, …})。
      遍历.text段,匹配该特征码找到KiSystemServiceStart的位置。
    4. 提取SSDT地址:
      KiSystemServiceStart中会有lea r10,KeServiceDescriptorTable指令(机器码4C 8D 15[相对偏移])。
      解析该指令的相对偏移,计算出KeServiceDescriptorTable的实际地址,即SSDT的地址。

SSDT Hook安装:SSDT::Hook()

SSDT Hook的核心是修改SSDT表中目标系统调用对应的函数地址,但32/64位系统的实现差异极大:

  1. 前置准备
    无论32/64位,都需要先完成:
    调用SSDTfind()获取SSDT表地址。
    通过NTDLL::GetExportSsdtIndex(apiname)获取目标APISSDT表中的索引(比如NtOpenProcess对应的索引)。
    校验索引的合法性(不超过SSDT表的服务数量)。

  2. 32位系统(x86Hook逻辑

    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));

    32SSDT表中直接存储函数的物理地址,因此直接将目标索引对应的条目替换为自定义Hook函数地址即可。

  3. 64位系统(x64Hook逻辑
    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节)中连续的、无意义的空闲内存区域,通常是:
0x90NOP指令):空操作,执行后无任何效果;
0xCCINT3指令):断点指令,未被使用时是空闲的;
这些区域属于内核原生内存,PatchGuard不会检测其内容修改(只要不破坏核心代码)。

代码洞穴的作用(x64 Hook中):
在洞穴中写入跳转指令(JMP),指向我们的自定义Hook函数;
修改SSDT表项,让其指向这个洞穴(而非直接指向Hook函数);
系统调用NtXXX时,流程变为:

1
用户层调用 NtQueryInformationProcess → 查 SSDT 表 → 跳转到代码洞穴 → 执行 JMP 到 Hook 函数 → 执行完后可选跳回原始函数

整个过程中,SSDT表项指向的仍然是内核原生内存(洞穴),PatchGuard不会触发保护,完美绕开检测。

x64 Hook(代码洞穴)的完整实现

  1. 找到代码洞穴(FindCaveAddress函数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static 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;
    }
  2. 在洞穴中写入跳转指令(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;
    }
  3. 构造符合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
  4. 修改SSDT表项为新值

    1
    2
    // 内存拷贝(带保护检查),修改 SSDT 表项
    RtlSuperCopyMemory(&SSDT->pServiceTable[FunctionIndex], &newValue, sizeof(newValue));

卸载 Hook(恢复 SSDT 表项):SSDT::Unhook ()

SSDT表项恢复为原始值,x86/x64差异仅在内存释放:

1
2
3
4
5
6
7
8
9
10
// 恢复原始表项
RtlSuperCopyMemory(&SSDT_Table[hHook->SSDTindex], &hHook->SSDTold, sizeof(hHook->SSDTold));

#ifdef _WIN64
// x64:释放代码洞穴的Hook
Hooklib::Unhook(hHook, true);
#else
// x86:释放Hook结构体内存
RtlFreeMemory(hHook);
#endif