-
-
[原创]游戏逆向中常用的Hook技术
-
发表于: 6小时前 140
-
依旧是复习过去学过的知识。
本文的Hook技术对应内容请看侧边目录。
一、内联Hook(InlineHook)
(1)inline hook 是什么
当我们想要拦截现有运行中的进程内某个现有的汇编函数体,最常用的办法就是 inline hook。
它可以在权限允许内,通过修改程序运行中的内存代码段汇编,以达到拦截任何函数的目的,包括系统api(只限非内核态的函数体,要hook内核函数需要进内核态),以及程序内部现有的任何函数体。
比如想拦截系统APICreateFileW的调用,修改原调用参数并继续执行CreateFileW原函数逻辑,获得返回值,或者直接拦截返回NULL失败,或者拦截程序本身代码汇编的函数体,用 inline hook都可以做到。
具体步骤如下:备份原始指令:在目标函数入口处,保存前几个字节(通常是 5 到 12 字节)。写入跳转指令:将目标函数的开头替换为一条跳转指令(通常是 JMP)。执行自己的逻辑:程序运行到目标函数时,会直接跳到你写的“钩子函数”里。跳回原函数:如果你还想让原程序继续运行,就在执行完你的逻辑后,先执行备份的原始指令,再跳回目标函数的后续位置。
(2)示例代码解析
x86示例代码如下,需要注意的是编译器如果发现 OriginalHelloWorldFunction 内容很短,且在同一个文件里,它在编译 main 函数时,将不会去执行 CALL 指令,而是直接把那句 printf 的内容复制到了 main 里面。所以需要在函数定义前加上 __declspec(noinline),通知编译器不要内联这个函数。
#include <windows.h>
#include <stdio.h>
// --- 被 HOOK 的目标函数 ---
__declspec(noinline) void OriginalHelloWorldFunction() {
printf("[执行] 原始的 Hello World 函数\n");
}
// --- 我们自定义的钩子函数 (Hook 函数) ---
void MyCustomHelloWorldFunction()
{
printf("[拦截] 成功进入了我们的钩子函数!\n");
}
// --- 核心:执行 Inline Hook 的逻辑 ---
void SetupInlineHookJmp32()
{
// 1. 定义跳转机器码结构 (E9 + 4字节偏移量)
// 机器码格式:E9 XX XX XX XX
BYTE JumpInstruction[5] = { 0xE9, 0, 0, 0, 0 };
// 2. 计算跳转偏移量 (公式:目标地址 - 原地址 - 指令长度)
// 注意:跳转是相对于当前指令下一条地址开始计算的
DWORD RelativeOffset = (DWORD)MyCustomHelloWorldFunction - (DWORD)OriginalHelloWorldFunction - 5;
// 将计算好的 4 字节偏移填充到机器码中
*(DWORD*)(JumpInstruction + 1) = RelativeOffset;
// 3. 修改目标内存属性为“可读写执行”,否则修改代码会引发崩溃 (Access Violation)
DWORD OldMemoryProtection;
VirtualProtect(OriginalHelloWorldFunction, 5, PAGE_EXECUTE_READWRITE, &OldMemoryProtection);
// 4. 正式写入机器码 (覆盖原函数开头的 5 个字节)
memcpy(OriginalHelloWorldFunction, JumpInstruction, 5);
// 5. 恢复内存原始保护属性 (养成良好的安全习惯)
VirtualProtect(OriginalHelloWorldFunction, 5, OldMemoryProtection, &OldMemoryProtection);
printf("[系统] Inline Hook 已部署完毕。\n");
}
int main()
{
printf("====== 32位 Inline Hook 测试开始 ======\n\n");
// 第一步:测试 Hook 之前的函数行为
printf("1. 尝试直接调用函数(此时未 Hook):\n");
// 注意:这里如果先调用,可能会被编译器内联,测试建议直接开始 Hook
OriginalHelloWorldFunction();
// 第二步:部署 Hook
SetupInlineHookJmp32();
// 第三步:再次调用原函数名,观察输出
printf("\n2. 再次尝试调用原函数:\n");
OriginalHelloWorldFunction();
printf("\n====== 测试结束 ======\n");
getchar(); // 暂停程序查看结果
return 0;
}
x64示例代码:
#include <windows.h>
#include <stdio.h>
// 告诉编译器不要内联这些函数
__declspec(noinline) void OriginalHelloWorldFunction()
{
printf("[执行] 原始的 x64 Hello World 函数\n");
}
void MyCustomHelloWorldFunction()
{
printf("[拦截] 成功进入了 x64 钩子函数!\n");
}
void SetupInlineHookX64()
{
// 1. 定义 12 字节的绝对跳转指令
// 48 B8 [8字节地址] FF E0 (jmp rax)
BYTE JumpInstruction[12] = {
0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, // mov rax, <address>
0xFF, 0xE0 // jmp rax
};
// 2. 将 64 位绝对地址写入机器码
void* TargetAddress = &MyCustomHelloWorldFunction;
memcpy(&JumpInstruction[2], &TargetAddress, 8);
// 3. 修改内存属性(注意:这次需要 12 字节空间)
DWORD OldMemoryProtection;
if (!VirtualProtect((LPVOID)OriginalHelloWorldFunction, 12, PAGE_EXECUTE_READWRITE, &OldMemoryProtection)) {
printf("[错误] 内存权限修改失败!\n");
return;
}
// 4. 写入 Hook
memcpy((LPVOID)OriginalHelloWorldFunction, JumpInstruction, 12);
// 5. 还原属性
VirtualProtect((LPVOID)OriginalHelloWorldFunction, 12, OldMemoryProtection, &OldMemoryProtection);
printf("[系统] x64 绝对跳转 Hook 已部署完毕。\n");
}
int main()
{
printf("====== 64位 Inline Hook 测试开始 ======\n\n");
printf("1. 尝试直接调用函数:\n");
OriginalHelloWorldFunction();
SetupInlineHookX64();
printf("\n2. 再次尝试调用原函数:\n");
OriginalHelloWorldFunction();
printf("\n====== 测试结束 ======\n");
system("pause");
return 0;
}
(3)原理解析
Inline Hook 是通过直接修改目标函数在内存中的机器指令来实现的。
在x86系统中。
常用 JMP(操作码 0xE9)去完成这个跳转操作,而JMP指令有两个特点:1.E9 指令后面需要跟一个 4 字节的偏移地址。2.总计 5 字节。记住这两个之后,就到了跳转地址的计算技术(重点),这是很多初学者容易卡住的地方。JMP 指令里的地址不是目标的绝对内存地址,而是相对偏移量。
计算公式:相对偏移 = 目标函数地址 - 原函数地址 - 跳转指令本身长度(5字节)。之所以这么算,是因为 CPU 执行到 JMP 时,指令寄存器(EIP/RIP)已经指向了 JMP 的下一条指令地址。所以你得把这 5 个字节抠掉,剩下的才是要跨越的距离。
假设:
目标地址 (TargetAddress):你想跳去的地方(你的钩子函数 MyCustomFunction),地址是 0x00401050。
源地址 (SourceAddress):你准备动手修改的地方(原函数 OriginalFunction),地址是 0x00401000。
套用公式:我们要计算的是填在 0xE9 后面的那 4 个字节到底是多少。相对偏移 = 0x00401050 - 0x00401000 - 5。先算地址差:0x00401050 - 0x00401000 = 0x50 (十进制的 80)。再减去指令长度:0x50 - 5 = 0x4B (十进制的 75)所以,相对偏移量就是 0x0000004B。
CPU 在执行指令时,EIP(指令指针)永远指向“下一条即将执行的指令”:CPU 读到了 0x00401000 处的 E9。在它还没开始“跳”之前,它的 EIP 已经自动增加,指向了 JMP 指令结束后的那个位置,即 0x00401005。此时 CPU 执行跳转,它会拿 当前的 EIP (0x00401005) + 你的偏移量 (0x4B)。计算结果:0x00401005 + 0x4B = 0x00401050。
如果是“往回跳”怎么办:如果你的目标地址比源地址小(比如从 0x401050 跳回 0x401000),公式依然成立。相对偏移 = 0x00401000 - 0x00401050 - 5 = -0x50 - 5 = -0x55。在 32 位计算机中,负数用补码表示。-0x55 转换成 4 字节十六进制就是 0xFFFFFFAB。你写入 E9 AB FF FF FF,CPU 同样能带你跳回去。
在x 64 位系统中。
由于内存空间太大,4 字节的偏移量(最大 ±2GB)可能跳不过去。所以常用 12 字节 的绝对跳转:
mov rax, 0x1122334455667788 ; 48 B8 ... (把 8 字节绝对地址存入寄存器) jmp rax ; FF E0
也就是把跳转地址存放到寄存器中,然后通过jmp寄存器的方式跳过去。
涉及的关键系统函数是VirtualProtect,代码段在内存里通常是“只读”的(PAGE_EXECUTE_READ)。想要修改人家的机器码,必须先用这个函数把权限改成“可读可写可执行”(PAGE_EXECUTE_READWRITE),改完后再换回去。
在 32 位(或 64 位的 E9 跳转)中,指令里存放的是“距离”。而我们在 64 位中常用的 12 字节 Hook,利用了寄存器作为中转站,直接把目标的绝对地址(Absolute Address)写进了指令里。在这种模式下,你只需要通过 &MyCustomFunction 获取钩子函数的 64 位完整地址,然后用 memcpy 直接把它塞进机器码的第 2 到第 9 个字节位置即可。没有加减法,只有搬运。当然,如果选择5 字节相对跳转,那么必须满足目标函数和原函数的距离必须在 ± 2GB 之内,也就是依旧要用到那个公式。
二、IAT Hook
熟悉PE结构的应该知道.IAT 是导入表。
对于不熟悉PE结构的人:
IAT (Import Address Table),即导入地址表。
你写了一个程序,调用了 MessageBoxA。但你的程序本身并不知道 MessageBoxA 在内存的哪个角落,因为 user32.dll 每次加载的地址可能都不一样。Windows 的做法:在你的程序(PE文件)里留一张“通讯录”。加载时:当程序启动时,Windows 加载器会找到 MessageBoxA 的真实地址,并把它填进这张表里。运行时:你的程序每次想弹窗,都会去查这张表,然后跳到表里记录的地址。
其IAT表结构如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; 指向INT表 4个字节一组.是RVA指向名字跟序号 } DUMMYUNIONNAME; DWORD TimeDateStamp;
DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; 在文件中跟INT表一样.这是IAT } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
我们知道PE有两种状态.第一种.在文件中的状态. 所以才有 VA 转 FOA等等的互相转换.
在文件状态. IAT表(firstThunk)跟 INT表一样.都是指向一个很大的表.这个表里面是4个字节进行存储.存储的是Rva. 这些RVA分别指向 导入序号以及以0结尾的字符串.
如果在内存状态.则INT表跟上面说的文件状态一样指向 导入序号.以及导入的函数名字.
而IAT此时不同了.IAT此时就是保存着INT指向的导入函数的地址了.

三、VTable Hook
四、SSDT Hook
五、EPT Hook
参考视频: c0cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4k6A6k6r3g2G2i4K6u0r3b7W2j5I4h3U0S2r3M7r3g2p5c8i4g2W2i4K6u0r3i4K6y4r3M7$3S2S2M7X3g2Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1c8U0L8%4m8&6i4K6g2X3N6$3g2T1i4K6t1$3j5h3#2H3i4K6y4n7N6X3c8Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1b7&6x3r3u0U0y4e0W2S2y4X3q4X3y4U0V1#2x3o6y4U0k6h3b7J5k6U0l9$3z5o6j5&6k6X3g2V1x3X3j5I4j5b7`.`.