游戏逆向中常用的Hook技术

张开发
2026/4/17 5:55:23 15 分钟阅读

分享文章

游戏逆向中常用的Hook技术
一、内联HookInlineHook1inline hook 是什么当我们想要拦截现有运行中的进程内某个现有的汇编函数体最常用的办法就是inline hook。它可以在权限允许内通过修改程序运行中的内存代码段汇编以达到拦截任何函数的目的包括系统api(只限非内核态的函数体,要hook内核函数需要进内核态)以及程序内部现有的任何函数体。比如想拦截系统APICreateFileW的调用修改原调用参数并继续执行CreateFileW原函数逻辑获得返回值或者直接拦截返回NULL失败或者拦截程序本身代码汇编的函数体用inline hook都可以做到。具体步骤如下备份原始指令在目标函数入口处保存前几个字节通常是 5 到 12 字节。写入跳转指令将目标函数的开头替换为一条跳转指令通常是JMP。执行自己的逻辑程序运行到目标函数时会直接跳到你写的“钩子函数”里。跳回原函数如果你还想让原程序继续运行就在执行完你的逻辑后先执行备份的原始指令再跳回目标函数的后续位置。2示例代码解析x86示例代码如下需要注意的是编译器如果发现 OriginalHelloWorldFunction 内容很短且在同一个文件里它在编译 main 函数时将不会去执行 CALL 指令而是直接把那句 printf 的内容复制到了 main 里面。所以需要在函数定义前加上 __declspec(noinline)通知编译器不要内联这个函数。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061#include windows.h#include stdio.h// --- 被 HOOK 的目标函数 ---__declspec(noinline)voidOriginalHelloWorldFunction() {printf([执行] 原始的 Hello World 函数\n);}// --- 我们自定义的钩子函数 (Hook 函数) ---voidMyCustomHelloWorldFunction(){printf([拦截] 成功进入了我们的钩子函数\n);}// --- 核心执行 Inline Hook 的逻辑 ---voidSetupInlineHookJmp32(){// 1. 定义跳转机器码结构 (E9 4字节偏移量)// 机器码格式E9 XX XX XX XXBYTEJumpInstruction[5] { 0xE9, 0, 0, 0, 0 };// 2. 计算跳转偏移量 (公式目标地址 - 原地址 - 指令长度)// 注意跳转是相对于当前指令下一条地址开始计算的DWORDRelativeOffset (DWORD)MyCustomHelloWorldFunction - (DWORD)OriginalHelloWorldFunction - 5;// 将计算好的 4 字节偏移填充到机器码中*(DWORD*)(JumpInstruction 1) RelativeOffset;// 3. 修改目标内存属性为“可读写执行”否则修改代码会引发崩溃 (Access Violation)DWORDOldMemoryProtection;VirtualProtect(OriginalHelloWorldFunction, 5, PAGE_EXECUTE_READWRITE, OldMemoryProtection);// 4. 正式写入机器码 (覆盖原函数开头的 5 个字节)memcpy(OriginalHelloWorldFunction, JumpInstruction, 5);// 5. 恢复内存原始保护属性 (养成良好的安全习惯)VirtualProtect(OriginalHelloWorldFunction, 5, OldMemoryProtection, OldMemoryProtection);printf([系统] Inline Hook 已部署完毕。\n);}intmain(){printf( 32位 Inline Hook 测试开始 \n\n);// 第一步测试 Hook 之前的函数行为printf(1. 尝试直接调用函数此时未 Hook\n);// 注意这里如果先调用可能会被编译器内联测试建议直接开始 HookOriginalHelloWorldFunction();// 第二步部署 HookSetupInlineHookJmp32();// 第三步再次调用原函数名观察输出printf(\n2. 再次尝试调用原函数\n);OriginalHelloWorldFunction();printf(\n 测试结束 \n);getchar();// 暂停程序查看结果return0;}x64示例代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859#include windows.h#include stdio.h// 告诉编译器不要内联这些函数__declspec(noinline)voidOriginalHelloWorldFunction(){printf([执行] 原始的 x64 Hello World 函数\n);}voidMyCustomHelloWorldFunction(){printf([拦截] 成功进入了 x64 钩子函数\n);}voidSetupInlineHookX64(){// 1. 定义 12 字节的绝对跳转指令// 48 B8 [8字节地址] FF E0 (jmp rax)BYTEJumpInstruction[12] {0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0,// mov rax, address0xFF, 0xE0// jmp rax};// 2. 将 64 位绝对地址写入机器码void* TargetAddress MyCustomHelloWorldFunction;memcpy(JumpInstruction[2], TargetAddress, 8);// 3. 修改内存属性注意这次需要 12 字节空间DWORDOldMemoryProtection;if(!VirtualProtect((LPVOID)OriginalHelloWorldFunction, 12, PAGE_EXECUTE_READWRITE, OldMemoryProtection)) {printf([错误] 内存权限修改失败\n);return;}// 4. 写入 Hookmemcpy((LPVOID)OriginalHelloWorldFunction, JumpInstruction, 12);// 5. 还原属性VirtualProtect((LPVOID)OriginalHelloWorldFunction, 12, OldMemoryProtection, OldMemoryProtection);printf([系统] x64 绝对跳转 Hook 已部署完毕。\n);}intmain(){printf( 64位 Inline Hook 测试开始 \n\n);printf(1. 尝试直接调用函数\n);OriginalHelloWorldFunction();SetupInlineHookX64();printf(\n2. 再次尝试调用原函数\n);OriginalHelloWorldFunction();printf(\n 测试结束 \n);system(pause);return0;}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 FFCPU 同样能带你跳回去。在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

更多文章