简介:本资源是一套完整支持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 字节 JMP,x64 用 14 字节 MOV+JMP),并完整处理函数对齐、内存页保护、指令边界校验等底层细节。适合做安全工具开发、逆向辅助模块、游戏外挂底层框架、或任何需要在目标进程中「无感植入」逻辑的场景。如果你正卡在「为什么我的 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/cpp:C++ 封装类,提供 RAII 自动管理(构造安装、析构卸载)、支持 lambda 回调、支持多实例共存(每个 hook 实例独立保存原指令);arch_detector.h:架构感知核心,通过IsWow64Process+GetNativeSystemInfo组合判断当前进程真实位数(关键!不能只看编译目标),并决定后续指令生成策略。
提示:项目不使用
#ifdef _WIN64做编译期分支,而是运行时动态决策——这意味着你编译一个 x64 DLL,加载进 WoW64 进程(即 32 位进程跑在 x64 系统上)时,它仍能正确 hook 32 位目标函数,而不是报“架构不匹配”。
2.2 跨架构跳转指令生成:x86 vs x64 的机器码级差异
InlineHook 的本质,是在目标函数入口处覆写为跳转指令(JMP/CALL),跳转到你的代理函数。但 x86 和 x64 的跳转能力天差地别:
- x86(32 位):
E9 rel32指令,5 字节,rel32 是相对于下一条指令的 32 位有符号偏移,最大跳转范围 ±2GB,足够覆盖绝大多数情况; - x64(64 位):
E9 rel32依然存在,但rel32 无法覆盖全 64 位地址空间(比如你的代理函数在0x7fff12345678,而目标函数在0x1000,差值远超 32 位)。因此必须用两步走:先MOV RAX, imm64(10 字节)把目标地址装入寄存器,再JMP RAX(2 字节),共 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的计算必须减去 5(x86 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::array<BYTE, 16>),避免堆分配开销。
2.4 C++ 封装类设计:RAII + 多实例 + 原函数调用支持
C++ 封装不是简单包一层 C 函数,而是解决实际工程问题:
- RAII 自动管理:构造时安装,析构时卸载,避免忘记
uninstall导致内存泄漏或残留 hook; - 多实例隔离:每个
InlineHook对象持有自己的original_bytes和jmp_size,互不影响; call_original()支持:提供call_original<T>(args...)模板方法,自动拼接原指令 + 参数,绕过 hook 执行原始逻辑(比如 hookMessageBoxW后,想在特定条件下仍弹窗)。
核心类结构如下:
// InlineHook.h class InlineHook { private: void* m_target = nullptr; void* m_hook = nullptr; std::array<BYTE, 16> m_original_bytes; // 最大 14 字节 + 2 字节 padding size_t m_jmp_size = 0; bool m_installed = false; public: template<typename 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 SDK(10.0.19041.0 或更新)和标准 C/C++ 工具链。验证环境如下(你可用更低版本,但需确保IsWow64Process可用):
| 组件 | 版本要求 | 说明 |
|---|---|---|
| Windows SDK | ≥ 10.0.17763.0 | 提供IsWow64Process、FlushInstructionCache |
| Visual Studio | 2019 或 2022 | 支持/arch:AVX2非必需,但推荐启用/Zi便于调试 |
| 目标平台 | Win10 1903+ / Win11 | WoW64 支持必须开启(默认开启) |
注意:项目不使用 C++17 的
std::byte或std::span,兼容 C++14;C 部分完全符合 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是独立可执行文件,分别启动后会自测 hookGetTickCount64(x64)或GetTickCount(x86),打印 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 2022:
cd inlinehook-master\build build_x64.batbuild_x64.bat内容如下(已预置):
@echo off cl /nologo /c /O2 /D_WIN32_WINNT=0x0A00 /I"..\src" ..\src\hook_core.c ..\src\arch_detector.c cl /nologo /c /O2 /D_WIN32_WINNT=0x0A00 /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_WINNT=0x0A00 /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行,加/MT:link /nologo /MT /OUT:test_x64.exe *.obj ..\test\test_x64.obj kernel32.lib user32.lib - VS 中:「属性」→ 「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,请转向KeStackAttachProcess+MmCopyVirtualMemory方案(本项目不覆盖)。
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 flag:
static volatile LONG g_in_hook = 0; extern "C" DWORD WINAPI MyHookFunc() { if (InterlockedCompareExchange(&g_in_hook, 1, 0) != 0) { // 已在 hook 中,直接调用原函数防递归 return call_original<DWORD>(); } __try { // 你的 hook 逻辑 DWORD ret = call_original<DWORD>(); return ret; } __finally { InterlockedExchange(&g_in_hook, 0); } }5. 二次开发实战:如何扩展支持fastcall函数与热更新 hook
5.1 支持fastcall调用约定:修改generate_jmp_bytes与跳板逻辑
fastcall将前两个 DWORD 参数放ECX/EDX(x86)或前四个 QWORD 放RCX/RDX/R8/R9(x64),其余仍在栈上。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 逻辑热更新。项目原设计需uninstall+install,会有短暂空白期。可改造为「原子指针替换」:
- 在
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 是离硬件最近的代码层,一个字节的偏差,就是整个进程的雪崩起点。希望帮到你。
本文还有配套的精品资源,点击获取