5字节JMP是x86-64下最稳妥的Inline Hook起点,因其指令固定占5字节且能对齐多数函数开头,但需反汇编确认前N字节是否构成完整指令,避免截断导致崩溃。

为什么5字节JMP是x86-64下最稳妥的Inline Hook起点
因为x86-64的jmp rel32指令固定占5字节(1字节opcode + 4字节有符号偏移),能覆盖绝大多数函数开头的指令边界,且不破坏后续指令解码。但注意:若目标函数前5字节本身是多字节指令(比如mov rax, [rip+0x12345678]这种7字节指令),硬塞5字节jmp会截断原指令,导致崩溃——所以必须先反汇编,确认前N字节是否构成完整指令。
实操建议:
- 用
capstone或手写简单反汇编器,从函数地址开始逐条解析,累加指令长度直到≥5字节;常见情况是前1~3条指令加起来刚好5字节(如push rbp+mov rbp, rsp) - 若首指令就超5字节(如AVX指令
vaddps ymm0, ymm1, ymm2),不能硬切,得找下一个对齐点,或改用trampoline方式 - Windows下记得调用
VirtualProtect把代码页设为PAGE_EXECUTE_READWRITE,否则写入会触发访问违例
如何安全地写入5字节JMP指令(含偏移计算)
目标是生成形如\xE9\xXX\xXX\xXX\xXX的机器码,其中\xE9是jmp rel32 opcode,后面4字节是「跳转目标地址」减去「下一条指令地址」的32位有符号差值。
关键点:
立即学习“C++免费学习笔记(深入)”;
- 跳转目标地址 = 你的hook函数地址
- 下一条指令地址 = 原函数地址 + 5(因为刚写入5字节
jmp,CPU执行完它就落到原函数第6字节) - 偏移 =
(int32_t)(hook_func_addr - (original_func_addr + 5))—— 必须强转为int32_t,否则高位会被零扩展,导致跳错 - 写内存前用
FlushInstructionCache(Windows)或__builtin___clear_cache(GCC/Clang)同步指令缓存,否则新指令可能不生效
示例(伪代码):
uint8_t jmp_bytes[5] = { 0xE9 };
int32_t offset = (int32_t)(hook_func - (original_func + 5));
memcpy(jmp_bytes + 1, &offset, 4);
// 写入 original_func 地址处
Hook后如何调用原函数(Trampoline的必要性)
你把原函数开头5字节覆盖成jmp了,那原逻辑就丢了。想在hook里调用原函数?不能直接跳回去——会再次触发hook,死循环。必须构造一段「跳板」(trampoline):把原被覆盖的5字节指令复制出来,末尾再补一条jmp original_func + 5,让它继续执行原函数剩余部分。
注意事项:
- trampoline内存必须可执行,推荐用
VirtualAlloc申请PAGE_EXECUTE_READWRITE页,写完再用VirtualProtect锁为只执行 - 复制的5字节若含相对寻址(如
call rel32、lea rax, [rip+0x123]),其偏移量要重定位:新地址 = 原偏移 + (trampoline_addr - original_func_addr) - Windows x64下若原函数使用了影子栈(shadow stack)或CFG,直接跳转可能触发异常,需确保trampoline也满足相同安全策略
Linux下mprotect与SELinux的隐藏陷阱
Linux用mprotect改写代码页权限时,若进程启用了SELinux(尤其Enforcing模式),可能拒绝PROT_EXEC权限升级,报错Permission denied而非EPERM。
排查和绕过:
- 检查
getenforce输出,临时设为Permissive:sudo setenforce 0(仅调试) - 更健壮的做法:用
mmap分配新页,设PROT_READ|PROT_WRITE|PROT_EXEC,把trampoline和patch指令都放进去,避免修改原代码段 - glibc 2.34+ 默认启用
memfd_create+mmap的JIT内存方案,若用libffi等库,优先走这套路径而非直接mprotect
真正麻烦的从来不是写5个字节,而是确保这5个字节写进去之后,CPU信你,OS不拦你,编译器不优化掉你的指针,还有多线程环境下没人正巧在执行那5字节——这些都得单独处理。


















