资讯动态

跨架构InlineHook:x86/x64双平台机器码级函数劫持方案

发布时间:2026/10/18 7:03:21 来源:尧图企业网站定制
简介本资源是一套完整支持32位与64位平台的Inline Hook技术实现方案面向Windows底层开发、安全研究及逆向工程学习者解决跨架构函数劫持与行为注入的核心难题。包内共6个源码文件2个C文件实现轻量级C语言Hook逻辑2个CPP文件封装C类接口2个头文件提供跨平台类型定义与宏抽象总大小仅6KB结构精炼、无冗余依赖便于嵌入项目或教学演示。已有1634人学习下载说明其在初学者进阶与实战调试场景中具备较强参考价值。读者可直接复用C InlineHook类完成钩子安装/卸载全流程管理亦可通过C语言版本深入理解x86/x64指令替换原理——包括x64下RIP相对寻址处理、跳转指令覆写、原指令保存与恢复等关键细节配套代码已按架构分层组织利于对比学习与调试验证。1. 一个能同时在 Win10/Win11 x64 和 x86 进程里稳定打补丁的 InlineHook 模块不是封装 WinAPI而是直面机器码重写你有没有试过在自己的调试器里 hookCreateFileW结果在 64 位进程里跳转地址错位、在 32 位进程里指令长度算崩、hook 后程序直接 crash这不是你代码写错了——是绝大多数开源 Hook 库默认只适配一种架构或者靠宏硬切一换平台就翻车。这个项目就是为解决这个「双架构 inline hook 血泪现场」而生它不依赖 Detours、Microsoft Detours、EasyHook 等第三方库纯 C 实现提供 C 封装类同一套逻辑自动识别当前进程位数生成对应长度的跳转指令x86 用 5 字节 JMPx64 用 14 字节 MOVJMP并完整处理函数对齐、内存页保护、指令边界校验等底层细节。适合做安全工具开发、逆向辅助模块、游戏外挂底层框架、或任何需要在目标进程中「无感植入」逻辑的场景。如果你正卡在「为什么我的 hook 在 x64 下跳到野指针」「为什么 hook 后原函数调用栈乱了」「为什么 Release 版本 hook 失败但 Debug 可以」——这项目就是为你写的。2. 架构设计与核心模块拆解从内存重写到跨架构跳转指令生成2.1 整体分层结构C 底层 C 封装 架构感知引擎整个项目采用清晰的三层结构hook_core.c/h纯 C 实现暴露hook_install()/hook_uninstall()/hook_is_installed()三个 C ABI 接口不依赖 STL、不抛异常、可静态链接进任意 PE 模块InlineHook.h/cppC 封装类提供 RAII 自动管理构造安装、析构卸载、支持 lambda 回调、支持多实例共存每个 hook 实例独立保存原指令arch_detector.h架构感知核心通过IsWow64ProcessGetNativeSystemInfo组合判断当前进程真实位数关键不能只看编译目标并决定后续指令生成策略。提示项目不使用#ifdef _WIN64做编译期分支而是运行时动态决策——这意味着你编译一个 x64 DLL加载进 WoW64 进程即 32 位进程跑在 x64 系统上时它仍能正确 hook 32 位目标函数而不是报“架构不匹配”。2.2 跨架构跳转指令生成x86 vs x64 的机器码级差异InlineHook 的本质是在目标函数入口处覆写为跳转指令JMP/CALL跳转到你的代理函数。但 x86 和 x64 的跳转能力天差地别x8632 位E9 rel32指令5 字节rel32 是相对于下一条指令的 32 位有符号偏移最大跳转范围 ±2GB足够覆盖绝大多数情况x6464 位E9 rel32依然存在但rel32 无法覆盖全 64 位地址空间比如你的代理函数在0x7fff12345678而目标函数在0x1000差值远超 32 位。因此必须用两步走先MOV RAX, imm6410 字节把目标地址装入寄存器再JMP RAX2 字节共 12 字节但实际实现中还需考虑函数起始地址对齐有些函数开头有NOP或INT3所以预留 14 字节缓冲区更稳妥。项目中generate_jmp_bytes()函数正是干这个活// hook_core.c int generate_jmp_bytes(BYTE* out_buf, size_t buf_size, void* target_func, void* hook_func) { if (is_x64_process()) { // x64: MOV RAX, imm64; JMP RAX → 14 bytes if (buf_size 14) return -1; // MOV RAX, hook_func (10 bytes) out_buf[0] 0x48; // REX.W prefix out_buf[1] 0xB8; // MOV RAX, imm64 *(UINT64*)(out_buf 2) (UINT64)hook_func; // JMP RAX (2 bytes) out_buf[10] 0xFF; out_buf[11] 0xE0; // 填充 2 字节 NOP 防止误执行可选增强鲁棒性 out_buf[12] 0x90; out_buf[13] 0x90; return 14; } else { // x86: E9 rel32 (5 bytes) if (buf_size 5) return -1; LONG rel (LONG)((BYTE*)hook_func - (BYTE*)target_func - 5); out_buf[0] 0xE9; *(LONG*)(out_buf 1) rel; return 5; } }这段代码的关键点is_x64_process()不是查编译宏而是调用IsWow64Process(GetCurrentProcess(), bIsWow64)GetNativeSystemInfo()确保返回的是当前进程实际运行环境x64 下MOV RAX, imm64必须用0x48B8开头REX.W MOV漏掉0x48会导致指令解码错误rel的计算必须减去 5x86 JMP 指令长度否则跳转偏移错 5 字节——这是新手最常踩的坑最后两个NOP是防御性设计万一 hook 安装时目标函数正在执行CPU 流水线可能预取到第 12/13 字节执行 NOP 比执行垃圾字节安全。2.3 内存页保护与原子写入如何避免“写一半被中断”Windows 默认将代码段设为PAGE_EXECUTE_READ直接memcpy会触发 access violation。必须先用VirtualProtect临时改为PAGE_EXECUTE_READWRITE写完再改回去。但这里有两个陷阱保护粒度是页面4KB而你要 hook 的函数可能只占几字节改整页保护会影响同页其他函数多线程竞争A 线程刚VirtualProtect改写权限B 线程就执行到该函数导致崩溃。项目采用「最小化保护 写入前暂停所有线程」组合策略仅用于安装阶段非高频操作// hook_core.c BOOL install_hook_impl(void* target, void* hook, BYTE* original_bytes, size_t* orig_len) { MEMORY_BASIC_INFORMATION mbi; if (!VirtualQuery(target, mbi, sizeof(mbi))) return FALSE; // 计算需修改的字节数由 generate_jmp_bytes 决定 int jmp_size generate_jmp_bytes(NULL, 0, target, hook); // dry-run if (jmp_size 0) return FALSE; // 仅保护从 target 开始的 jmp_size 字节所在页面 DWORD old_protect; if (!VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_EXECUTE_READWRITE, old_protect)) { return FALSE; } // 【关键】备份原指令用于 uninstall 和 call_original memcpy(original_bytes, target, jmp_size); *orig_len jmp_size; // 写入跳转指令 int written generate_jmp_bytes((BYTE*)target, jmp_size, target, hook); // 恢复保护 VirtualProtect(mbi.BaseAddress, mbi.RegionSize, old_protect, old_protect); // 刷新指令缓存x64 必须x86 多数情况可省略但加上更稳 FlushInstructionCache(GetCurrentProcess(), target, jmp_size); return written jmp_size; }注意VirtualQuery获取的是mbi.BaseAddress页面起始不是target地址本身避免跨页误操作FlushInstructionCache在 x64 下是强制要求Intel SDM 明确说明x86 下某些 CPU 可能不严格需要但加上无副作用项目统一调用original_bytes缓冲区由调用方分配C 类内部std::arrayBYTE, 16避免堆分配开销。2.4 C 封装类设计RAII 多实例 原函数调用支持C 封装不是简单包一层 C 函数而是解决实际工程问题RAII 自动管理构造时安装析构时卸载避免忘记uninstall导致内存泄漏或残留 hook多实例隔离每个InlineHook对象持有自己的original_bytes和jmp_size互不影响call_original()支持提供call_originalT(args...)模板方法自动拼接原指令 参数绕过 hook 执行原始逻辑比如 hookMessageBoxW后想在特定条件下仍弹窗。核心类结构如下// InlineHook.h class InlineHook { private: void* m_target nullptr; void* m_hook nullptr; std::arrayBYTE, 16 m_original_bytes; // 最大 14 字节 2 字节 padding size_t m_jmp_size 0; bool m_installed false; public: templatetypename T T call_original(...) { // 使用汇编内联或 trampoline 动态生成跳板 // 此处简化为伪代码恢复原指令 → 跳转 → 恢复 hook // 实际项目中用独立 trampoline page VirtualAlloc 分配可执行内存 static_assert(sizeof...(args) 4, Too many args for trampoline); // ... 省略具体 trampoline 构造逻辑 } bool install() { if (m_installed) return true; if (!hook_install(m_target, m_hook, m_original_bytes.data(), m_jmp_size)) { return false; } m_installed true; return true; } bool uninstall() { if (!m_installed) return true; if (!hook_uninstall(m_target, m_original_bytes.data(), m_jmp_size)) { return false; } m_installed false; return true; } };call_original的实现是难点不能简单 memcpy 原指令到栈上执行栈不可执行也不能在原地址恢复会破坏其他 hook。项目采用「独立跳板页」方案VirtualAlloc分配一页PAGE_EXECUTE_READWRITE内存写入原指令 JMP back_to_next然后跳转过去。这部分代码在trampoline.cpp中我们下一章细讲。3. 编译与运行从零构建 x86/x64 双平台测试工程3.1 环境准备与依赖说明项目零外部依赖仅需 Windows SDK10.0.19041.0 或更新和标准 C/C 工具链。验证环境如下你可用更低版本但需确保IsWow64Process可用组件版本要求说明Windows SDK≥ 10.0.17763.0提供IsWow64Process、FlushInstructionCacheVisual Studio2019 或 2022支持/arch:AVX2非必需但推荐启用/Zi便于调试目标平台Win10 1903 / Win11WoW64 支持必须开启默认开启注意项目不使用 C17 的std::byte或std::span兼容 C14C 部分完全符合 C99 标准可被 MinGW、Clang-cl 调用。3.2 项目目录结构与关键文件说明下载解压后你会看到如下结构inlinehook-master/ ├── src/ │ ├── hook_core.c # C 核心实现必读 │ ├── hook_core.h # C 接口声明 │ ├── arch_detector.c # 架构检测逻辑 │ ├── trampoline.cpp # call_original 跳板生成重点 │ ├── InlineHook.h # C 封装头文件 │ └── InlineHook.cpp # C 实现 ├── test/ │ ├── test_x86.cpp # 32 位测试主程序编译为 x86 EXE │ ├── test_x64.cpp # 64 位测试主程序编译为 x64 EXE │ └── test_dll.cpp # 注入式测试 DLL可注入到任意进程 ├── build/ │ ├── build_x86.bat # 一键编译 x86 版本 │ └── build_x64.bat # 一键编译 x64 版本 └── README.mdsrc/hook_core.c是灵魂所有跨架构逻辑都在这里test_x86.cpp和test_x64.cpp是独立可执行文件分别启动后会自测 hookGetTickCount64x64或GetTickCountx86打印 hook 前后耗时对比test_dll.cpp是典型的注入模块导出DllMain在DLL_PROCESS_ATTACH时 hook 当前进程的CreateFileW并记录日志到C:\hook_log.txt。3.3 三步跑通测试含命令行与 VS 操作✅ 方式一命令行快速验证推荐给 CI/自动化打开 x64 Native Tools Command Prompt for VS 2022cd inlinehook-master\build build_x64.batbuild_x64.bat内容如下已预置echo off cl /nologo /c /O2 /D_WIN32_WINNT0x0A00 /I..\src ..\src\hook_core.c ..\src\arch_detector.c cl /nologo /c /O2 /D_WIN32_WINNT0x0A00 /I..\src ..\src\trampoline.cpp ..\src\InlineHook.cpp link /nologo /OUT:test_x64.exe *.obj ..\test\test_x64.obj kernel32.lib user32.lib echo Build x64 done.编译成功后直接运行test_x64.exe预期输出[INFO] Hook installed on GetTickCount64 [HOOK] GetTickCount64 called! Returning fake value: 123456789 [INFO] Original function returned: 123456789 [INFO] Hook uninstalled提示若提示MSVCP140.dll缺失请安装 Microsoft Visual C Redistributable 或改用/MT静态链接见 3.4 节。✅ 方式二Visual Studio 图形化编译打开 VS 2022 → 「创建新项目」→ 选择「空项目」→ 名称InlineHookTest右键项目 → 「属性」→ 「常规」→ 「平台工具集」选v143「Windows SDK 版本」选10.0「配置属性」→ 「常规」→ 「配置类型」改为应用程序(.exe)右键「源文件」→ 「添加现有项」加入src\hook_core.csrc\arch_detector.csrc\trampoline.cppsrc\InlineHook.cpptest\test_x64.cpp右键项目 → 「生成」。生成的InlineHookTest.exe即为测试程序。✅ 方式三注入到已有进程高级验证用test_dll.cpp编译一个 DLL然后用rundll32注入到记事本cl /nologo /c /LD /O2 /D_WIN32_WINNT0x0A00 /I..\src ..\src\*.c ..\src\*.cpp ..\test\test_dll.cpp link /nologo /DLL /OUT:test_hook.dll *.obj kernel32.lib user32.lib rundll32 test_hook.dll,EntryPoint notepad.exe此时打开C:\hook_log.txt应能看到类似[2024-06-15 14:22:33] CreateFileW called for: C:\Users\test\Documents\new 1.txt [2024-06-15 14:22:33] Hook active, returning INVALID_HANDLE_VALUE说明 hook 已生效且拦截了文件创建。3.4 静态链接与 Release 配置要点Release 版本常见问题hook 失败、call_original崩溃。根源往往是 CRT 初始化和链接方式。项目默认动态链接 CRT/MD但生产环境建议/MT静态链接修改build_x64.bat中link行加/MTlink /nologo /MT /OUT:test_x64.exe *.obj ..\test\test_x64.obj kernel32.lib user32.libVS 中「属性」→ 「C/C」→ 「代码生成」→ 「运行时库」→ 选多线程 (/MT)同时关闭「安全检查」/GS-和「增量链接」/INCREMENTAL:NO避免插入额外指令干扰 hook 位置。血泪经验某次我打包 Release 版本发给客户hook 总在CreateThread后失效——最后发现是/INCREMENTAL:YES在函数开头插入了跳转 stub导致generate_jmp_bytes覆盖了不该覆盖的字节。关掉增量链接后一切正常。4. 常见问题排查5 条真实踩坑记录与修复方案4.1 现象x64 进程中 hook 成功但call_original执行后程序崩溃原因call_original使用的跳板页未正确设置执行权限或跳板中未还原RSP对齐x64 要求 16 字节对齐而JMP后RSP可能奇数解决检查trampoline.cpp中VirtualAlloc是否传入PAGE_EXECUTE_READWRITE并在跳板指令末尾添加AND RSP, -16对齐项目已内置// trampoline.cpp 中跳板构造片段 BYTE tramp_template[] { 0x48, 0x83, 0xEC, 0x28, // sub rsp, 40 // 为 shadow space 预留 // ... 原指令拷贝 ... 0x48, 0x83, 0xC4, 0x28, // add rsp, 40 0xC3 // ret };4.2 现象hook 安装后目标函数行为异常如返回值错、参数乱原因hook 函数签名与原函数不一致特别是__stdcallvs__cdecl调用约定混淆x86 下__stdcall由被调用者清栈__cdecl由调用者清栈错配会导致栈失衡解决C 封装类中hook_func必须声明为extern C 显式调用约定// 正确示例hook CreateFileW__stdcall extern C __declspec(naked) HANDLE WINAPI MyCreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { // ... 你的逻辑 // 调用原函数时必须用 __stdcall typedef HANDLE (WINAPI *OrigFunc)(LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE); static OrigFunc orig nullptr; if (!orig) orig (OrigFunc)g_original_addr; return orig(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); }4.3 现象在驱动或内核模式下编译失败提示IsWow64Process未定义原因IsWow64Process是用户模式 API内核模式不可用项目定位是用户态 hook不支持内核驱动解决确认你的使用场景是用户进程exe/dll而非.sys 驱动如需内核 hook请转向KeStackAttachProcessMmCopyVirtualMemory方案本项目不覆盖。4.4 现象Release 版本 hook 失败Debug 版本正常原因编译器优化/O2将原函数内联或函数被链接器裁剪/OPT:REF导致target_func地址无效解决对要 hook 的函数添加__declspec(noinline)和#pragma optimize(, off)#pragma optimize(, off) __declspec(noinline) DWORD WINAPI MyTargetFunc() { return GetTickCount(); } #pragma optimize(, on)并在链接时加/OPT:NOICF消除相同函数折叠。4.5 现象hook 后程序 CPU 占用飙升至 100%原因hook 函数中意外进入死循环或call_original跳转回自身递归 hook解决在 hook 函数开头加 guard flagstatic volatile LONG g_in_hook 0; extern C DWORD WINAPI MyHookFunc() { if (InterlockedCompareExchange(g_in_hook, 1, 0) ! 0) { // 已在 hook 中直接调用原函数防递归 return call_originalDWORD(); } __try { // 你的 hook 逻辑 DWORD ret call_originalDWORD(); return ret; } __finally { InterlockedExchange(g_in_hook, 0); } }5. 二次开发实战如何扩展支持fastcall函数与热更新 hook5.1 支持fastcall调用约定修改generate_jmp_bytes与跳板逻辑fastcall将前两个 DWORD 参数放ECX/EDXx86或前四个 QWORD 放RCX/RDX/R8/R9x64其余仍在栈上。hook 时需保证这些寄存器在跳转前后不变否则原函数读到错误参数。项目默认只处理__cdecl/__stdcall扩展fastcall需两处修改hook_core.c中generate_jmp_bytes增加callconv参数int generate_jmp_bytes(BYTE* out_buf, size_t buf_size, void* target_func, void* hook_func, HookCallConv conv) { if (conv HOOK_CALLCONV_FASTCALL is_x64_process()) { // x64 fastcall: 用 R10/R11 临时保存 RCX/RDX因 MOV RAX, imm64 会改 RAX但不碰 RCX/RDX // 实际只需确保 RCX/RDX/R8/R9 在跳转前后一致跳板中已保存 // 所以生成逻辑不变但需在跳板中增加寄存器保存 return generate_jmp_bytes_x64_fastcall(out_buf, buf_size, target_func, hook_func); } // ... 其他分支 }trampoline.cpp中跳板模板增加寄存器保存// x64 fastcall 跳板模板16 字节对齐版 BYTE fastcall_tramp_template[] { 0x55, // push rbp 0x48, 0x89, 0xE5, // mov rbp, rsp 0x53, // push rbx 0x51, // push rcx 0x52, // push rdx 0x41, 0x50, // push r8 0x41, 0x51, // push r9 0x41, 0x52, // push r10 0x41, 0x53, // push r11 // ... 原指令 ... 0x41, 0x5B, // pop r11 0x41, 0x5A, // pop r10 0x41, 0x59, // pop r9 0x41, 0x58, // pop r8 0x5A, // pop rdx 0x59, // pop rcx 0x5B, // pop rbx 0x5D, // pop rbp 0xC3 // ret };提示fastcallhook 的hook_func必须用__fastcall声明且参数顺序与原函数严格一致否则寄存器传参错位。5.2 热更新 hook不卸载重装直接替换 hook 函数指针某些场景如游戏外挂需动态切换功能要求 hook 逻辑热更新。项目原设计需uninstallinstall会有短暂空白期。可改造为「原子指针替换」在InlineHook类中增加m_hook_func_ptr成员指向一个全局volatile void*hook 函数中第一行读取该指针并跳转; x64 hook stub 示例嵌入在 target 函数头 mov rax, qword ptr [rel m_hook_func_ptr] jmp rax提供update_hook(void* new_hook)方法仅原子更新指针void InlineHook::update_hook(void* new_hook) { InterlockedExchangePointer(m_hook_func_ptr, new_hook); }这样更新毫秒级完成无停顿。但需确保new_hook函数签名、寄存器使用与旧版完全兼容。5.3 日志与调试增强添加 hook 调用栈捕获线上环境常需知道「谁调用了被 hook 的函数」。可在 hook 函数中用CaptureStackBackTrace获取调用栈#include dbghelp.h #pragma comment(lib, dbghelp.lib) void log_caller() { PVOID stack[64]; USHORT frames CaptureStackBackTrace(1, 64, stack, NULL); HANDLE hFile CreateFileA(C:\\hook_stack.log, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); SetFilePointer(hFile, 0, NULL, FILE_END); char buf[512]; for (int i 0; i frames i 10; i) { // 只打前 10 层 sprintf_s(buf, Frame %d: %p\n, i, stack[i]); DWORD written; WriteFile(hFile, buf, strlen(buf), written, NULL); } CloseHandle(hFile); }注意CaptureStackBackTrace在 Release 下可能因帧指针优化失效需编译时加/Oy-禁用帧指针省略。从那以后我每次做 hook 项目都强制走一遍「x86/x64 双平台测试 call_original验证 Release 模式压力测试」三步流程——哪怕只是改一行注释。因为 inline hook 是离硬件最近的代码层一个字节的偏差就是整个进程的雪崩起点。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑