直接改函数前5字节为JMP易崩溃,因JMP rel32要求目标在RIP±2GB内,远地址导致偏移溢出;需+5计算相对位移、检查int32范围、调用VirtualProtect改页保护、FlushInstructionCache刷新缓存,并同步所有线程。

为什么直接改函数前 5 字节为 JMP 很容易崩溃
因为 x86-64 下 JMP rel32 指令占 5 字节(0xE9 + 4 字节有符号偏移),但目标地址必须落在当前指令指针(RIP)±2GB 范围内。如果挂钩目标函数在堆、DLL 远端或 ASLR 启用后随机基址上,计算出的 rel32 偏移很可能溢出,CPU 执行时跳到非法地址,触发 ACCESS_VIOLATION 或静默崩溃。
- Windows 默认启用 DEP 和 CFG,写入代码页(如 .text)需先调用
VirtualProtect改为PAGE_EXECUTE_READWRITE - 多线程环境下,必须在所有线程都未执行该函数时打补丁,否则可能 CPU 正在解码旧指令流,你却已覆写为 JMP,造成指令错位(如只写入了
0xE9和前 2 字节偏移) - 某些编译器(如 MSVC)会在函数开头插入安全检查(如
__security_cookie校验),盲目覆盖前 5 字节会破坏校验逻辑
如何安全计算并写入 JMP rel32 指令
核心是:用 DWORD 类型算偏移,不是简单相减。目标地址减去「JMP 指令下一条指令的地址」(即原函数地址 + 5),再做符号扩展截断。
// 示例:hook_target 是你要跳转到的函数地址,original_func 是原函数入口
uintptr_t from = (uintptr_t)original_func;
uintptr_t to = (uintptr_t)hook_target;
int32_t rel32 = (int32_t)(to - (from + 5)); // 关键:+5!
uint8_t patch[5] = {0xE9, (uint8_t)(rel32 & 0xFF), (uint8_t)((rel32 >> 8) & 0xFF),
(uint8_t)((rel32 >> 16) & 0xFF), (uint8_t)((rel32 >> 24) & 0xFF)};
- 务必检查
rel32是否在INT32_MIN~INT32_MAX范围内,超出则不可用此法,需用JMP rax三指令跳转(mov rax, imm64; jmp rax) - 写入前调用
VirtualProtect(original_func, 5, PAGE_EXECUTE_READWRITE, &old_protect),写完立刻恢复保护 - 调用
FlushInstructionCache(GetCurrentProcess(), original_func, 5),尤其在 ARM64 或某些虚拟化环境必需
怎么避免被编译器优化或热补丁干扰
MSVC 的 `/hotpatch` 编译选项会在函数开头预留 2 字节空隙(0x8BFF —— mov edi,edi),但这不是通用方案;而 `/guard:cf` 会插入 CFG 检查,覆盖前 5 字节大概率触发异常。所以不能依赖编译器“配合”。
- 对目标函数使用
#pragma code_seg(".text")+#pragma optimize("", off)禁用优化,确保函数以标准 prologue 开头(无 inlining、no hotpatch padding、no CFG call guard) - 用
__declspec(naked)写一个 stub 函数作为 hook 目标,自己控制汇编,避免编译器插入额外指令干扰跳转逻辑 - 若 hook 系统 DLL(如 kernel32.dll 中的
WriteFile),需注意 Windows 10 1809+ 启用 CFG 后,间接跳转会被拦截——此时必须用SetThreadInformation关闭当前线程的 CFG 检查,或改用 Microsoft Detours 库
实际部署时最常漏掉的三件事
不是写完 patch 就完事。真实场景里,90% 的失败源于忽略运行时上下文。
- 没调用
SuspendThread暂停所有可能执行该函数的线程,导致 patch 过程中某线程刚好执行到第 3 字节,看到半截 JMP 指令 - 没保存原函数前 5 字节用于构造 trampoline(比如想在 hook 函数里调用原逻辑),结果 patch 后再也无法还原或跳转回去
- 在 DLL 加载时(
DLL_PROCESS_ATTACH)打补丁,但目标函数已被 JIT 编译(如 .NET P/Invoke 调用的 native 函数)或被其他 hook 工具抢先修改,此时读到的已是 patched 指令而非原始机器码
真正稳定的 inline hook 必须把内存保护、线程同步、指令完整性、兼容性兜底全串起来——单靠“改 5 字节”只是最表层动作。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











