
简介EasyHook 是 Windows 平台下基于动态注入的钩子库压缩包提供了一套完整可编译的 VS2010 C Demo包含 Hook.dll 动态库与 Inject.exe 注入程序。Hook.dll 将下钩子流程封装为函数表数组填写模块名、函数名与新函数指针即可批量挂钩适合中高级 C 开发者用于 API 监控、调试辅助或逆向分析。Inject.exe 基于 MFC输入进程 ID 即可完成 DLL 注入。包内共 36 个文件以 h/cpp 源码为主附带 rc 资源、vcxproj/sln 工程配置、dll/lib 依赖及 EasyHook 库便于在 VS2010 中直接编译运行整体约 278KB已有 2000 余人学习。Demo 代码风格清晰带 TRACE 输出并演示 CreateFileW/ReadFile 双版本 API 挂钩可作后续钩子项目基础框架。1. EasyHook 是什么一个让 Hook 从黑匣子变成透明管道的完整 Demo某公司一台老服务器上的 VC 程序已经十年没动过业务方想统计它到底在什么时候调用了多少次 MessageBox既不能改源码也不能重编译。某开发者在网上翻遍资料后最终落到 EasyHook 这套 C Demo 上。跑通之后才发现Hook 并没有想象中那么玄学注入、安装、回调、清理四个环节的代码全部完整可编译剩下的就是重复劳动。这套 Demo 的核心价值是把四个环节用一个 VS2010 直接打开就能编译的工程完整呈现。宿主程序负责注入HookDll 负责拦截目标 API回调数据通过进程间通信回传到宿主端最后还能干净卸载。适合两类人一类是给老项目加 API 监控能力、又不想自己写汇编跳板的人一类是想把 EasyHook 钩子机制从原理到实战完整过一遍再照着做二次开发的 C 工程师。2. 技术选型与工程结构为什么用 EasyHook 而不是 Detours2.1 选型对比Detours、EasyHook 与手写 Inline Hook在 Windows 上做函数钩子摆在面前的路有三条。手写 Inline Hook 是一条“看似可控、实则全是坑”的路你要手工修改目标函数头部的字节把它改成一条跳转指令同时在别的内存区域保存被覆盖的原始指令还要处理多线程同时进入被 Hook 区域的竞争问题。一个字节算错目标进程直接崩溃而且这种崩溃极难复现调试器一挂上就“一切正常”。Detours 比手写高明不少它把指令修补和 trampoline 生成都封装好了。但真实接入老工程时你会发现两个麻烦一是它的发布版对老编译环境的支持不算友好二是授权成本需要评估。对一个内部工具来说为一个统计需求引入商业授权流程上就过不去。EasyHook 的优势在于它把最复杂的部分——注入器、x86/x64 双通道、回调分发、线程 ACL 过滤——全部封装成了 DLL 和一组 API你的代码只需要关心“钩子装上之后干什么”。方案注入支持x64 支持回调复杂度老工程适配手写 Inline Hook自己实现自己实现高中Detours需配合 SDK需配合 SDK中中EasyHook自带注入器内置低高选 EasyHook 还有一个很实际的理由它能让你在 VS2010 里用同一份 C 代码分别编译出 x86 和 x64 两个版本的 DLL位数差异被库本身屏蔽掉了。对一台跑着老系统的服务器来说能少编译一次就少一次风险。2.2 工程结构宿主 EXE 与注入 DLL 的分工打开这个 Demo 的 sln你会看到解决方案里有两个主要工程外加一个共享头文件目录整体结构是这样的EasyHook-Demo/ ├── EasyHook-Demo.sln ├── Host/ # 宿主程序负责注入与日志展示 │ ├── Host.cpp │ ├── Host.vcxproj │ └── HostDlg.cpp # 简单的注入控制界面 ├── HookDll/ # 被注入目标进程的拦截库 │ ├── HookDll.cpp │ ├── HookDll.def │ └── HookDll.vcxproj ├── shared/ │ └── HookProtocol.h # 宿主与注入 DLL 共用的数据结构 └── bin/ ├── EasyHook32.dll ├── EasyHook64.dll └── run_demo.batHost 工程是整个 Demo 的“前台”。它负责枚举当前系统的进程列表让你选一个目标进程然后调用 EasyHook 的注入 API把 HookDll.dll 送进目标进程。HookDll 工程则是真正干活的“后台”它被加载到目标进程后会在自己的初始化线程里完成LhInstallHook安装操作并注册回调函数。shared 目录下的 HookProtocol.h 放的是两头都要用的结构体定义比如回调日志的数据格式保证编译 Host 和 HookDll 时用的是同一份协议。bin 目录容易被忽略但它其实是跑通 Demo 的关键。EasyHook 运行时包含两个 DLL一个 32 位一个 64 位注入目标进程时需要按目标进程的位数选择对应版本。如果你只拷贝了一个 EasyHook32.dll 到 bin 里而目标是 64 位进程注入就会静默失败这是这个 Demo 第一个值得记住的坑。2.3 一次完整的 Hook 生命周期在动手读代码之前先用一张“路线图”把整个流程串起来。一次成功的 Hook 生命周期分五个阶段第一阶段宿主程序拿到目标进程 PID调用RhInjectLibrary参数里带上 HookDll 的路径、宿主自身的路径和一个透传缓冲区。第二阶段目标进程加载 HookDll系统触发DLL_PROCESS_ATTACHDLL 里启动一个独立初始化线程不在 DllMain 里做重活。第三阶段初始化线程拿到目标 API 的函数地址调用LhInstallHook挂上回调再调用LhSetInclusiveACL设置线程白名单。第四阶段目标进程里有人调用被 Hook 的 API流程就会先进入你写的回调函数回调数据通过命名管道或共享内存送回宿主。第五阶段宿主或 DLL 触发卸载流程依次调用LhUninstallHook、LhWaitForPendingRemoval最后释放 DLL。后面所有代码拆解本质上都是对这张路线图的具体化。你把每一段代码和这五个阶段对应上整个工程就不再是黑匣子了。3. 编译与首次运行在 VS2010 环境里把 Demo 完整跑起来3.1 环境配置平台工具集、字符集与运行库在 VS2010 里打开 sln 之后别急着点生成先检查三个配置项否则你会在编译阶段浪费大量时间。第一项是平台工具集。确认工程使用的是 v100 平台工具集而不是高版本 Visual Studio 默认的 v143。如果工程被高版本 IDE 自动升级过编译时会报 MSB8025 之类的工具集错误解决办法是在工程属性的“常规 → 平台工具集”里改回 v100。第二项是字符集。EasyHook 的 API 参数大多以LPCWSTR为主建议把字符集设置成“使用 Unicode 字符集”避免代码里的宽字符串和窄字符串混用导致一堆 C2664 编译错误。第三项是运行库。HookDll 和 Host 都建议使用“多线程调试 DLL (/MDd)”或“多线程 DLL (/MD)”模式不要让注入 DLL 静态链接一份独立的 CRT否则把它注入目标进程后可能和对方的 CRT 冲突。这三项配置里最容易翻车的是运行库。你平时写 EXE 时用 /MT 和 /MD 都能跑但注入 DLL 是跑到别人进程里去的DLL 里如果自带一份静态 CRT全局状态就和宿主进程不一致轻则日志写不进去重则加载直接失败。这个项目场景下我一般会统一改成 /MD省心。3.2 编译顺序与运行步骤编译顺序上先编译 HookDll 再编译 Host。这个顺序不是强制的但先编出 DLL 会让你心里有底Host 注入时依赖的 HookDll 路径已经存在避免运行时才发现 DLL 缺失。x86 和 x64 各编译一次会得到四个主要产物Host_x86.exe、Host_x64.exe、HookDll_x86.dll、HookDll_x64.dll。编译完成后把产物和 EasyHook 运行库统一放到 bin 目录。可以写一个批处理搞定echo off set BINbin copy /Y Host\Release_x86\Host.exe %BIN%\Host_x86.exe copy /Y Host\Release_x64\Host.exe %BIN%\Host_x64.exe copy /Y HookDll\Release_x86\HookDll.dll %BIN%\HookDll_x86.dll copy /Y HookDll\Release_x64\HookDll.dll %BIN%\HookDll_x64.dll copy /Y %BIN%\EasyHook32.dll %BIN%\EasyHook32.dll copy /Y %BIN%\EasyHook64.dll %BIN%\EasyHook64.dll echo done批处理里有个细节要注意copy命令不会自动创建目标目录所以 bin 目录要提前建好。另外EasyHook32.dll和EasyHook64.dll必须和 EXE 在同一目录因为 EasyHook 注入器运行时是按模块路径加载这两个库的。运行步骤也不复杂。用管理员权限打开命令行启动 bin 目录下的 Host 程序再启动一个“会调用 MessageBox 的测试程序”比如一个用来做验证的小工具在 Host 界面里输入目标进程的 PID点击注入按钮最后在目标程序里触发一次 MessageBox 调用观察 Host 端的日志窗口。3.3 验证钩子是否生效三种验证方式注入之后怎么确认钩子真的生效了有三种常用的验证手段。第一种是看日志。回调函数里写入日志文件目标进程每触发一次被 Hook 的 API文件里就会追加一行包含调用线程 ID 和参数内容。如果日志行数和触发次数对得上说明链路是通的。第二种是看宿主端界面。如果 Demo 用了命名管道做 IPC回调会把计数消息发给宿主宿主界面上会有一个不断增长的数字这是最直观的验证。第三种是看行为变化。Hook 回调里如果直接篡改了 API 的返回值比如让 MessageBox 直接返回 IDOK 而不弹窗目标程序的行为会立刻改变这是“钩子真的拦到函数”的最有力证据。第三种方式也是最容易让新人产生误解的地方。Hook 回调里修改行为不代表你的 Hook 写得有问题它恰恰是在证明拦截成功。验证阶段建议先用这种方式确认链路再用日志方式做数据统计。4. 核心模块拆解从 InstallHook 到 UninstallHook 的完整链路4.1 注入入口RhInjectLibrary 的参数与调用时机Host 端最核心的一段代码是对RhInjectLibrary的调用。几乎所有注入逻辑都由这个函数封装好了// Host.cpp: 注入目标进程 ULONG targetPid 4172; // 从进程列表里选出来的目标 PID BYTE passThru[256] { 0 }; // 透传缓冲区随 DLL 一起进目标进程 NTSTATUS status RhInjectLibrary( targetPid, // 目标进程 ID Lbin\\HookDll_x86.dll, // 32 位注入 DLL 路径 Lbin\\HookDll_x64.dll, // 64 位注入 DLL 路径 Lbin\\Host_x86.exe, // 宿主程序自身路径 LEasyHook Host, // 宿主窗口标题用于 IPC 标识 passThru, // 透传缓冲指针 sizeof(passThru) // 透传缓冲字节数 ); if (!NT_SUCCESS(status)) { // 把 status 用 FormatMessage 转成可读错误文本 // 常见的失败原因权限不足、位数不匹配、DLL 依赖缺失 }参数逐一说一下。targetPid决定 HookDll 被注入到哪个进程这个值通常通过 Toolhelp 快照 API 遍历进程列表拿到而不是手写。两个 DLL 路径是 EasyHook 特有的设计注入器会先判断目标进程的位数再决定加载哪个版本的 HookDll如果你只编译了 x86 版本那注入到 64 位进程时就会失败。InHostProcessPath指向宿主程序本身EasyHook 需要用这个路径找到宿主来建立 IPC。最后一个透传缓冲区很关键它是 HookDll 加载后拿到的第一份数据可以放配置开关、宿主 PID、回调标志之类的东西相当于宿主和 DLL 之间的“握手包”。调用时机上常见做法是 Host 启动后先弹出进程列表用户选中目标后立刻注入。我一般会在注入前强制做一次位数检查把目标进程的位数和 HookDll 两个版本的产物存在性都验证一遍再调用RhInjectLibrary这样能省掉大量“注入失败但不知道错在哪”的排查时间。4.2 DLL 内安装钩子LhInstallHook 与线程 ACLHookDll 被加载进目标进程后DllMain会被调用。这里有个 EasyHook 开发中必须遵守的规矩不要在DllMain里直接安装钩子。DllMain持有系统加载锁在里面做复杂调用极易造成死锁正确的姿势是启动一个独立的初始化线程让线程去完成安装// HookDll.cpp DWORD WINAPI InstallThread(LPVOID context) { HMODULE hUser32 LoadLibraryW(Luser32.dll); LPVOID pMessageBoxW (LPVOID)GetProcAddress(hUser32, MessageBoxW); if (!pMessageBoxW) { return 1; } TRACED_HOOK_HANDLE hHook { 0 }; NTSTATUS status LhInstallHook( pMessageBoxW, // 要拦截的目标函数地址 (LPVOID)HookMessageBoxW, // 自己的回调函数地址 NULL, // 回调句柄Demo 里用不到就传 NULL hHook // 安装后拿到的钩子句柄 ); if (!NT_SUCCESS(status)) { return 2; } DWORD tid GetCurrentThreadId(); status LhSetInclusiveACL(1, (HANDLE*)tid, hHook); if (!NT_SUCCESS(status)) { return 3; } return 0; } BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) { if (reason DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(hDll); HANDLE hThread CreateThread(NULL, 0, InstallThread, NULL, 0, NULL); CloseHandle(hThread); } return TRUE; }LhInstallHook是 EasyHook 最核心的安装接口。第一个参数是目标函数的真实入口地址通过GetProcAddress拿到第二个参数是你的回调函数地址最后一个参数是输出型参数安装成功后里面就是钩子句柄之后卸载要靠它。安装成功后还必须调用LhSetInclusiveACL来设置线程白名单这个 ACL 的意思是只有列表里的线程才会被拦截其他线程直接放行。这一步极其重要如果不设 ACL 或者设成空列表目标进程里所有线程都可能在任意时刻触碰被 Hook 的函数回调并发问题会让你调试到怀疑人生。回调函数本身也很有讲究。由于被 Hook 的是 MessageBoxW回调必须使用和它兼容的签名调用约定也要保持 WINAPI也就是__stdcallint WINAPI HookMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) { // 记录一次调用哪个线程弹窗标题是什么 WriteLog(MessageBoxW intercepted, caption%s, lpCaption); // 直接返回 IDOK目标进程会认为用户点了确定 // 注意这里不能再调用原 MessageBoxW入口已被改写调用会递归回来 return IDOK; }这里有一个新手必踩的坑回调里直接调用::MessageBoxW以为能弹出一个正常的框结果立刻栈溢出。因为 user32.dll 里的 MessageBoxW 入口已经被 EasyHook 改写成跳转到你的回调了你再调它等于又进了回调形成无限递归。Demo 的处理方式是回调里不调用原函数只记录日志后篡改返回值这样既能验证拦截成功又规避了 trampoline 的复杂性。4.3 数据回传与安全卸载IPC 通道与 LhUninstallHook回调函数在目标进程里执行记录的数据怎么传回宿主进程常见做法是命名管道。HookDll 通过透传缓冲区拿到宿主 PID 和管道名然后用CreateFile连接宿主端已经创建好的管道宿主端用CreateNamedPipe监听收到一条就写一条日志// Host.cpp 侧创建命名管道监听 HANDLE hPipe CreateNamedPipeW( L\\\\.\\pipe\\EasyHookDemo, PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 0, 0, 30000, NULL ); while (ConnectNamedPipe(hPipe, NULL)) { char buf[512]; DWORD read 0; if (ReadFile(hPipe, buf, sizeof(buf), read, NULL)) { AppendLog(buf, read); } DisconnectNamedPipe(hPipe); }这段代码是宿主端的监听循环。PIPE_TYPE_MESSAGE表示按消息模式传输每次ReadFile读到的就是完整的一条日志不会出现半包错位。ConnectNamedPipe会阻塞等待 DLL 侧连接DLL 侧每发一条数据就连接一次、写一次、断开一次这样宿主端就能稳定地逐条接收。卸载流程同样有顺序讲究。很多人卸载钩子时只调用LhUninstallHook就立刻FreeLibrary结果目标进程在下一次调用原 API 时直接崩溃// 卸载钩子顺序不能乱 LhUninstallHook(hHook); // 第一步摘除钩子 LhWaitForPendingRemoval(); // 第二步等待正在执行的回调全部退出 CloseHandle(hPipe); // 第三步关闭 IPC 通道 FreeLibrary(hDll); // 第四步最后卸载 DLLLhUninstallHook只是把函数入口的跳转指令改回来但此刻可能还有别的线程正停留在回调函数里。如果紧接着卸载 DLL那些线程返回时就会跳到一块已卸载的内存地址上崩溃就成了必然。LhWaitForPendingRemoval干的事就是等回调队列排空所以它必须在FreeLibrary之前调用。我把这个顺序写进代码注释里因为每次调试崩溃时第一反应基本都会怀疑到它头上。5. 常见问题与避坑跑通 Demo 后才会遇到的四个高频故障5.1 注入超时DLL 加载阶段直接卡死现象RhInjectLibrary返回超时类错误目标进程没有被注入或者 Host 端长时间无响应。原因注入 DLL 在目标进程内加载时卡住了。最常见的两个诱因是 DLL 的依赖项缺失和 CRT 运行库不匹配。如果 HookDll 隐式链接了一个目标进程里不存在的 DLL系统加载会一直等下去如果 HookDll 用 /MT 编译而目标进程用 /MD也可能在 CRT 初始化阶段出问题。解决先用 dumpbin 的/dependents参数检查 HookDll 依赖了哪些 DLL确认里面没有第三方运行库再把 HookDll 的工程配置改成 /MD 重新编译。如果问题依旧换一个目标进程试试排除目标进程自身持有加载锁的可能。5.2 LNK2019外部符号链接失败现象编译 Host 或 HookDll 时报unresolved external symbol指向LhInstallHook、RhInjectLibrary这类 EasyHook API。原因EasyHook 的头文件里声明了这些函数但链接阶段没有把对应的 .lib 文件加进输入列表。还有一个隐蔽场景附加库目录指向了错误的平台目录比如用 x86 工程去链接 x64 的 lib符号就对不上。解决打开工程属性在“链接器 → 输入 → 附加依赖项”里加上 EasyHook.lib确认“附加库目录”指向和当前编译平台一致的那份 lib。这里我习惯在切换平台配置后重新检查一次因为 VS2010 的库目录经常会带着旧平台的残留路径。5.3 钩子装不上64 位目标进程静默失败现象注入函数返回成功但目标进程调用被 Hook 的 API 时日志里没有任何记录。原因这个故障九成是位数不匹配。Host 是 32 位、目标是 64 位注入器会尝试加载 HookDll_x64.dll但你没有编译 x64 版本或者编译出来了却放在错误路径下。EasyHook 在这种情况下不会立刻报错而是在目标进程里静默失败。另一个高频原因是目标进程以管理员身份运行而 Host 没有管理员权限注入被系统拒绝。解决打开进程管理器确认目标进程位数检查 bin 目录下 HookDll_x86.dll 和 HookDll_x64.dll 是否都存在最后用管理员权限重新启动 Host 和测试程序。我把这个检查项叫做“位数匹配三步确认”每次跑 Demo 前花十秒钟扫一眼能省掉大量消耗在玄学调试上的时间。5.4 卸载后崩溃回调还没结束就释放了 DLL现象钩子卸载完毕宿主正常退出但目标进程在几秒后突然崩溃崩溃栈里指向 HookDll 的内存区域。原因LhUninstallHook摘除了钩子但没有等待回调队列排空。目标进程中可能有某个线程正在执行回调函数或者正准备跳进回调而你的代码已经执行了FreeLibrary那块内存被回收后变成悬空指针线程一碰就炸。解决严格遵循“先LhUninstallHook再LhWaitForPendingRemoval最后FreeLibrary”的顺序。后两个调用之间不要插入任何耗时操作。这个教训我在一次给老系统做热更新时栽过跟头从那以后卸载代码永远是三行固定注释摆在那里一行都不敢动。6. 二次开发把 Demo 改造成一个可配置的通用 Hook 工具6.1 从硬编码到配置把 PID 和 Hook 目标抽出来Demo 里目标进程 PID 是写死的MessageBoxW 也是写死的。要把它变成能反复使用的工具第一件事就是做一个配置项结构体struct HookConfig { DWORD targetPid; // 要注入的目标进程 LPCWSTR moduleName; // 被拦截函数所在模块如 Luser32.dll LPCWSTR functionName; // 被拦截函数名如 LMessageBoxW int callbackMode; // 0只记录1改返回值2调用原函数 };然后从配置文件里读这个结构体Host 启动时加载配置注入时把配置通过透传缓冲区传给 HookDll。这样以后要换一个 Hook 目标只需要改配置文件不需要重新编译任何东西。6.2 调用原函数从“篡改行为”到“保留行为”Demo 里回调直接返回 IDOK这是为了安全验证。真实场景里你往往需要既拦截又保留原行为。这时候就不能直接调::MessageBoxW了正确做法是在安装钩子前保存原函数地址并通过 EasyHook 提供的 trampoline 机制拿到可安全调用的原始实现。这块机制值得单独花时间读 EasyHook 的文档和源码搞清楚它怎么复制被覆盖的头部指令你就真正理解了 Inline Hook 的底层原理。改造完成后要验证两件事一是回调触发前后目标进程行为一致二是连续触发上千次不崩溃、不泄漏。6.3 改造后的验证习惯每次改完配置和回调逻辑我都强制走一遍固定流程确认 Host、HookDll、目标进程三者位数一致用管理员权限启动小规模触发目标 API查看日志落盘和回调计数最后做一次卸载测试观察目标进程是否还活着。这套流程看起来笨但在 Hook 这种“一旦崩溃很难复现”的开发场景下它是最可靠的后悔药。那次被卸载顺序坑过之后我每次拿到一个 Hook 工程都会先做一遍“位数匹配三步确认”和“卸载四步顺序检查”确认无误再谈效果。这个习惯帮我躲过了后面好几次大坑希望帮到你。本文还有配套的精品资源点击获取