EasyHook实战:C++ DLL注入与API Hook完整Demo解析

发布时间:2026/9/9 23:55:57
EasyHook实战:C++ DLL注入与API Hook完整Demo解析 简介面向C及Windows平台开发者的EasyHook函数钩子示例工程基于VS2010编译环境构建提供从DLL注入到API挂钩的完整稳定实现方案适用于文件访问监控、API调用追踪、程序行为分析等系统编程场景。包内合计三十六个文件包含Hook挂钩库与MFC注入程序两个核心模块源码涵盖头文件、实现文件、界面资源、工程配置等类型同时附带了编译好的动态库与静态导入库压缩包大小为278KB目录结构明确便于按模块学习与二次开发。目前已有两千零八十八位学习者浏览对想快速集成钩子功能、绕开EasyHook繁杂底层调用的开发者参考价值突出。实现上Hook库把挂钩流程封装为配置数组只需填写模块名、API函数名和新函数指针即可完成一条挂钩规则演示代码覆盖了CreateFileW、CreateFileA、ReadFile三个常用接口注入端则以MFC对话框接收目标进程ID并自动执行库注入免去手工编写注入逻辑。整体代码风格规范、注释到位是学习远程注入、API拦截与函数挂钩的实用范例也可直接移植为通用钩子模块。 先说结论如果你要在Windows平台上做API钩子又不想被授权费坑、不想自己面对一堆指令级裁缝的活EasyHook是目前最省心的选择之一。这篇博文整理的是一个我实际在用的完整Demo基于VS2010 C把一个“注入器 钩子DLL”的工程从头到尾讲清楚。工程可以直接拿来改适合刚接触函数钩子、想在老项目里快速加监控逻辑、或者被逆向需求逼着研究这个方向的朋友。其实早期我在这块也绕了不少弯路。真正开始用EasyHook是因为当时要给一个遗留系统加文件访问监控试了手写INLINE HOOK结果在x64下被指令重定位折磨了一周后来切到EasyHook两小时就通了。市面上讲EasyHook的资料不少但大多数都是托管版本C#的例子非托管C的完整Demo反而少。这篇就专门补这个缺口。1. 为什么偏偏是EasyHook选型背后的逻辑1.1 自己写钩子 vs 现成库函数钩子的本质是把目标函数开头的几条指令改写成一条跳转指令跳到我们自己的函数里执行。听起来简单但落地全是细节x86下短跳转占5字节长跳转占6字节x64下常用的是14字节的绝对跳转被覆盖掉的原指令必须先搬到一个“垫片”里执行原指令可能是相对寻址搬走之后地址就崩了必须做重定位如果正在有线程执行到一半你改了指令下一秒就是访问违规自己的函数执行完还得干净利落地回到原函数继续跑栈和寄存器一个都不能乱。这些细节自己处理就是著名的inline hook翻车现场。稍微有点规模的项目我都不建议自己造轮子。市面上成熟的库微软Detours是事实标准但3.0 Express版虽然免费商业授权要付费而且老版本在新系统上经常出兼容问题。EasyHook刚好补了这个空档——它开源、免费LGPL、C/C接口齐全在Windows 7到Windows 10上我都实际跑过稳定性没得挑。1.2 EasyHook的核心优势EasyHook和普通“CreateRemoteThread LoadLibrary”注入最大的区别是它把注入动作做成了驱动级操作。早期版本依赖内核驱动EasyHook32.sys / EasyHook64.sys来完成进程内的代码注入和指令改写所以抗反调试能力、注入成功率都明显比纯用户态方案高。对我们做工具链的人来说最直观的感受就一个字——稳。它内部把指令改写的活全封装好了你只需要关心三件事往哪个进程注入Hook哪个APIHook到之后干什么。另外EasyHook支持按线程做ACL过滤比如只想Hook主线程、排除某个线程都控制得明明白白。这个后来排查问题时帮了我大忙。Demo里我选了32位工程因为VS2010时代32位进程还是绝对主流64位操作逻辑完全一样选对DLL和编译选项就行。2. 工程搭建VS2010下的完整配置2.1 环境与补丁先交代环境。Demo是在VS2010 SP1上建的平台工具集默认v100Windows SDK用的7.0版本。建议先把SP1和KB983509补丁装上不然VS2010在Win10上经常有各种莫名崩溃和代码高亮卡死的小毛病和当前工程不一定直接相关但影响体验。如果你手头实在没有VS2010拿VS2013/2015/2019打开这个工程也基本无痛只需要把平台工具集改成v120/v140/v142Windows SDK选8.1或10即可。核心代码没有任何版本强绑定。需要准备的软件包EasyHook 2.7源码包包含inc、lib、release DLL我本地用的是2.7.7097VS2010 SP1一个方便看调试输出的工具比如DebugViewEasyHook包解压后目录里会有EasyHook32.dll、EasyHook64.dll、EasyHook32.lib、EasyHook64.lib以及一堆头文件。我们只需要把inc目录里的EasyHook.h系列头文件和对应lib引用到工程里。2.2 工程结构设计整个解决方案我拆成两个工程职责分开后面维护起来非常清爽工程AEasyHookDemo_InjectorWin32控制台程序x86作用是找到目标进程PID调用EasyHook的注入API把钩子DLL打进去并等一下用户命令来卸载。它要引用的库是EasyHook32.lib。工程BEasyHookDemo_DllWin32 DLLx86这是实际的业务工程里面做真正的Hook安装和日志逻辑。它也要引EasyHook32.lib。这两个工程的输出目录我建议统一指向同一个目录比如$(SolutionDir)bin\$(PlatformName)\$(Configuration)\这样跑Demo时DLL和EXE不会找不到彼此。这个细节初学的人很容易踩坑——明明编译成功了一运行提示“找不到DLL”。注意事项VS2010在编译DLL时默认可能不带导出定义。如果发现注入后在目标进程里加载DLL失败优先检查是不是DLL没有导出我们约定的入口函数。后面我会单独说导出方式。3. Demo核心代码解析一次完整注入与Hook3.1 注入器的实现Injector侧注入器的核心逻辑分三步找PID、注入、等待卸载。第一步用ToolHelp32遍历进程找到目标进程的PID。这个属于Windows基础API直接上代码#include windows.h #include tlhelp32.h #include wchar.h #include iostream #include EasyHook.h #pragma comment(lib, EasyHook32.lib) DWORD FindProcessId(const wchar_t* processName) { DWORD pid 0; HANDLE snapshot CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot INVALID_HANDLE_VALUE) return 0; PROCESSENTRY32W pe { sizeof(PROCESSENTRY32W) }; if (Process32FirstW(snapshot, pe)) { do { if (_wcsicmp(pe.szExeFile, processName) 0) { pid pe.th32ProcessID; break; } } while (Process32NextW(snapshot, pe)); } CloseHandle(snapshot); return pid; }第二步拿到PID后调用RhInjectLibrary。这里有一个关键点DLL路径一定要用绝对路径。很多第一次跑的人直接传相对路径结果目标进程的工作目录和注入器完全不同注入当然失败。int wmain(int argc, wchar_t* argv[]) { if (argc 2) { wprintf(LUsage: EasyHookDemo_Injector ProcessName\n); return 1; } DWORD pid FindProcessId(argv[1]); if (pid 0) { wprintf(LProcess not found.\n); return 1; } wchar_t dllPath[MAX_PATH] { 0 }; GetFullPathNameW(LEasyHookDemo_Dll.dll, MAX_PATH, dllPath, NULL); // 第5个参数是被注入的DLL第4个参数传NULL表示原生DLL注入 NTSTATUS nt RhInjectLibrary( pid, 0, EASYHOOK_INJECT_DEFAULT, NULL, dllPath, NULL, 0 ); if (nt 0) { wprintf(LInject OK, pid %u\n, pid); } else { wprintf(LInject failed, NTSTATUS 0x%08X\n, nt); return 1; } // 等用户按回车再卸载 wprintf(LPress Enter to unhook and exit...\n); getchar(); RhUnhookLibrary(pid, dllPath); wprintf(LUnhooked.\n); return 0; }RhInjectLibrary的第一个参数是目标进程PID第二参数传0表示让EasyHook在目标进程里自己创建远程线程不需要我们指定线程ID。第三个参数EASYHOOK_INJECT_DEFAULT是默认注入方式具体走驱动还是用户态EasyHook内部会根据权限自动决策。最后一个参数0是透传数据大小我们用不到。3.2 钩子DLL入口函数与Hook安装钩子DLL真正的入口不是DllMain而是EasyHook约定的ExternalEntryPoint。这个函数由注入线程在目标进程里调用只有这个函数能拿到注入侧传过来的上下文。我们导出的方式用.def文件最稳在工程里加一个EasyHookDemo_Dll.defEXPORTS ExternalEntryPoint然后在cpp里实现这个导出函数#include windows.h #include strsafe.h #include EasyHook.h #pragma comment(lib, EasyHook32.lib) // 钩子句柄保存用于后续卸载 static TRACED_HOOK_HANDLE g_hHookCreateFileW NULL; extern C __declspec(dllexport) int __stdcall ExternalEntryPoint( void* InParam, DWORD InParamSize, void* InContext) { // 安装CreateFileW钩子 NTSTATUS nt LhInstallHook( CreateFileW, MyCreateFileW, NULL, g_hHookCreateFileW); if (nt 0) { // 安装失败返回负值给注入器 return (int)nt; } // 让钩子作用于目标进程的所有线程 LhSetExclusiveACL(0, 0, g_hHookCreateFileW); return 0; }这一步就是整个Demo的心脏。LhInstallHook的参数含义如下CreateFileW我们要Hook的API地址MyCreateFileW我们的替代函数地址第三个参数是回调事务型监控时用我们直接传NULL第四个参数是输出句柄供卸载/查询用。安装完成后我还调了LhSetExclusiveACL(0, 0, ...)。这里第一次接触的人容易发懵——传0,0是“排除零个进程、零个线程”的意思也就是不排除任何东西等于所有线程都生效。反过来想设置成“只有当前线程”时需要用LhSetInclusiveACL它表达“只包含谁”。这个语义我经常搞混写了注释防止以后忘。3.3 被Hook函数的完整写法MyCreateFileW是真正干活的函数。它必须和原始CreateFileW完全一致的签名和调用约定否则栈就崩了。函数内部先记录日志然后调用被保存的原始函数完成真实的文件打开动作typedef HANDLE (WINAPI *PFN_CreateFileW)( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); HANDLE WINAPI MyCreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { // 用OutputDebugString输出避免在Hook里再调用文件API导致递归 wchar_t msg[512] { 0 }; StringCchPrintfW(msg, 512, L[EasyHookDemo] CreateFileW: %s\n, lpFileName); OutputDebugStringW(msg); // 获得原始函数地址然后调用 PFN_CreateFileW pOriginal (PFN_CreateFileW)LhGetHookProc(g_hHookCreateFileW); if (pOriginal NULL) return INVALID_HANDLE_VALUE; return pOriginal(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); }特别注意递归问题。刚开始试这个Demo时我犯过一个低级错误直接在Hook函数里用fopen写日志文件。而fopen内部走的同样是CreateFileW那条链路于是命中自己的Hook又去写日志又命中Hook……无限递归直接把目标进程栈干爆。调试了一下午才定位。日志输出优先选OutputDebugStringW或者提前把日志文件句柄打开不走CreateFileW这个API。LhGetHookProc的作用是从钩子句柄里拿到“原始函数指针”。EasyHook在改写完指令后会把原始函数的前几条指令完整搬移到一个私有区域并把那条私有区域的地址作为原始函数返回给我们。我们自己写inline hook时最难搞的trampoline部分在这就是一个API调用。3.4 DllMain该做什么DllMain里原则上什么都不用做。EasyHook的注入流程不依赖我们在DllMain里干额外的事保持最简反而更安全BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE; }如果你的Hook逻辑需要初始化一些全局状态可以在ExternalEntryPoint里做。DllMain在Loader Lock限制下容易出幺蛾子这在所有DLL编程里都是常识。4. 稳定性的关键踩坑实录与排查技巧4.1 编译/运行时最常踩的四个坑坑一X86/X64架构不匹配注入器的编译目标、钩子DLL的编译目标、目标进程的架构三者必须一致。32位注入器 64位目标进程或者反过来错误码清一色是0xC000007BSTATUS_INVALID_IMAGE_FORMAT。排查第一步就是统一架构。坑二DLL路径不对上面代码里我用了GetFullPathNameW把相对路径转成绝对路径。经验是永远不要在注入相关代码里写裸的相对路径。目标进程的工作目录和你想象中完全不是一回事尤其用计划任务、服务方式启动的目标进程。坑三Hook函数签名不一致Windows API分WINAPI即__stdcall和__cdecl两种调用约定参数个数和返回值都得和原函数一模一样。一个最典型的故障就是Hook MessageBoxA时把参数写成了const char*结果原函数传的是const wchar_t*栈上取出来的内容全是乱码甚至直接访问违规。建议Hook任何API前先去头文件里把函数原型完整复制过来再改成自己的函数名。坑四EasyHook32.dll没有被正确加载EasyHook落地执行时不仅目标进程需要能加载你的钩子DLL还需要能加载EasyHook32.dll这个运行时库。我把三个文件都放到同一个输出目录里就再没遇到过“加载DLL失败”的报错。4.2 常见错误码速查NTSTATUS / 现象含义处理方式0xC000007B映像格式错误架构不匹配统一x86/x640xC0000005访问违规Hook函数原型写错核对API签名和调用约定注入函数返回负数LhInstallHook失败检查是否重复安装同一条Hook目标进程无日志输出DLL路径错误 / DllMain异常用Process Explorer确认DLL是否被加载用DebugView看调试输出目标进程直接被终止Hook函数递归 / 栈损坏检查日志IO是否重入了原API排查工具我固定用三件套Process Explorer看DLL有没有被目标进程加载、DebugView看OutputDebugString输出、VS的Attach to Process给目标进程上调试器直接在Hook函数里下断点。5. 从Demo到生产扩展方向5.1 改成按线程控制钩子LhSetExclusiveACL(0, 0, ...)是“所有线程都生效”。如果想只在特定线程生效比如只想Hook某个工作线程可以用LhSetInclusiveACL// 只对当前线程生效 LhSetInclusiveACL(1, 1, g_hHookCreateFileW);第二个参数是线程ID列表这里可以传GetCurrentThreadId()。这种细粒度控制在分析某些多线程程序时可以避免误伤。5.2 在Hook函数里做更复杂的事OutputDebugStringW只适合轻量验证。生产场景下你大概率需要把日志写到文件或者上报到服务端。这时有两个选择一是提前打开一个日志文件句柄在Hook函数里直接写文件不走系统API二是用共享内存或命名管道把数据交给外部进程处理。两个方案我都用过简单数据量直接用文件句柄方案最稳数据量大、需要格式化聚合时再上管道。5.3 换成新版Visual StudioVS2010是历史产物如果你只是想在VS2019/2022上用同样的思路操作很简单用Visual Studio Installer把“使用C的桌面开发”装上加载工程后右键重定目标平台工具集选v142或v143EasyHook的lib换一下即可。注意新版VS默认编译C17EasyHook老代码理论上能直接过唯一要改的是个别宏定义位置。EasyHook这套“驱动注用户DLL 指令改写”的架构即使放到现在也并不过时不少商业软件的理念和它一模一样。Demo本身不大但里面藏着的工程经验——绝对路径、ACL语义、递归防护、架构匹配——是我实际跑了很多遍才摸透的也是真正的稳定关键。先按原样跑通再改业务你会少走一半弯路。本文还有配套的精品资源点击获取