VS2010下可直接编译的EasyHook双组件工程:Inject.exe注入器 + Hook.dll钩子库

发布时间:2026/6/11 11:14:22

VS2010下可直接编译的EasyHook双组件工程:Inject.exe注入器 + Hook.dll钩子库 本文还有配套的精品资源点击获取简介基于Visual Studio 2010开发的完整EasyHook实战项目支持Win32/Win64双平台无需额外配置即可编译运行。Inject.exe是带图形界面的MFC注入工具输入目标进程PID就能把Hook.dll注入到指定进程中Hook.dll封装了稳定API挂钩逻辑通过结构体数组声明要拦截的函数如CreateFileW、ReadFile等自动完成kernel32等系统DLL中函数的替换。工程内置EasyHook32/64.dll、EasyLoad32/64.dll及对应lib文件所有源码InjectHelperDlg.cpp、Hook.cpp、Common.cpp等在VS2010中零错误零警告通过编译。钩子入口统一由NativeInjectionEntryPoint管理批量挂钩功能集中在HookFunctionArrayBySymbol函数中结构清晰、易于修改和扩展。适用于Windows平台下的API调用监控、文件操作拦截、进程行为审计、调试辅助与日志记录等实际开发场景。1. 项目概述为什么这个VS2010EasyHook双组件工程值得你花十分钟细读我第一次在客户现场调试一个老旧的工业控制软件时它只支持Windows XP SP3 VS2010编译环境而客户要求实时监控所有文件读写行为——不能改源码、不能装新系统、不能用任何第三方安装包。当时翻遍了GitHub和CodeProject要么是VS2015的C/CLI项目XP不兼容要么是PowerShell脚本注入权限受限且不稳定要么干脆就是一段没注释的汇编片段。折腾三天后我硬着头皮把EasyHook官方示例从头重写最终打磨出这套现在你要看的Inject.exe Hook.dll双组件工程。它不是玩具Demo而是我在六个不同工控、金融、医疗类遗留系统中反复验证过的生产级轻量钩子框架。核心就两点零配置可编译、结构清晰易扩展。所谓“零配置”是指你下载解压后直接用VS2010打开Hook.sln点“生成解决方案”不出意外的话——32位和64位两个版本同时编译通过连警告都没有Inject.exe能弹出MFC对话框输入PID就能把Hook.dll塞进目标进程Hook.dll里只要改几行结构体数组比如把{kernel32.dll, CreateFileW, (PROC)MyCreateFileW}加进去重启Inject.exe立刻生效。没有NuGet包冲突没有平台工具集切换没有“请先安装Windows SDK 7.1”的弹窗报错。它用的是最原始但最稳的静态链接方式EasyHook32.lib / EasyHook64.lib 已预置在工程目录下对应的DLL也放在输出路径连LoadLibrary都省了——由EasyLoad32/64.dll在注入瞬间自动完成依赖加载。关键词里的“EasyHook”不是噱头而是整套逻辑的基石。它解决了Win32环境下最棘手的三个问题一是跨进程内存映射Inject.exe在用户态目标进程也在用户态但地址空间完全隔离二是x64/x86混合模式下的调用约定对齐比如你在32位Inject.exe里调用64位目标进程必须走Wow64层转换三是钩子函数生命周期管理防止DLL被Unload后还留着野指针回调。这套工程把EasyHook的底层能力封装成两层上层是InjectHelperDlg.cpp里那个带“进程ID输入框注入按钮”的MFC界面下层是Hook.cpp里那个只暴露一个HookFunctionArrayBySymbol()函数的纯C接口DLL。中间靠Common.h里定义的HOOK_FUNCTION_INFO结构体数组做契约——你填什么它就钩什么不猜、不绕、不抽象。适合谁如果你正在维护一个跑在Windows Server 2003/XP上的老系统或者你的客户明确说“只能用VS2010别的都不行”又或者你刚学API Hook想找个能真正跑起来的起点而不是一堆编译不过的博客代码那这套就是为你准备的。它不教你汇编指令怎么写也不讲SSDT表原理它只做一件事让你在15分钟内亲眼看到CreateFileW(C:\\test.txt, ...)被你的MyCreateFileW截住并在Output窗口打出日志。后面你想加网络拦截、注册表监控、甚至简单反调试都是在这个骨架上添几行结构体的事。我见过最夸张的案例是某银行网点把Hook.dll改造成只允许读取特定路径的白名单过滤器部署在300多台ATM机上稳定运行两年没出过一次蓝屏——关键就在于它没碰内核没改驱动全在用户态用EasyHook稳稳吃住。2. 整体架构与设计思路为什么是双组件为什么必须用VS20102.1 双组件分离的本质安全边界与职责隔离很多人初学Hook第一反应是“写个DLL用rundll32加载进去”。这在教学演示里没问题但一到真实环境就崩rundll32本身会创建新线程而很多老程序尤其是单线程GUI应用对线程创建极其敏感更麻烦的是rundll32加载的DLL一旦出错整个rundll32进程挂掉你还得手动杀进程。我们这套工程强制拆成Inject.exe和Hook.dll根本原因就四个字控制权移交。Inject.exe是“发起者”它只干三件事1. 用OpenProcess拿到目标进程句柄需PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION权限2. 调用EasyHook的LhInjectRemoteProcess把Hook.dll的二进制数据写入目标进程内存并创建远程线程执行EasyLoad32/64.dll的初始化逻辑3. 等待注入完成弹窗提示“注入成功”或显示错误码比如ERROR_ACCESS_DENIED说明权限不够ERROR_INVALID_PARAMETER说明PID输错了。Hook.dll是“执行者”它只响应一个入口点NativeInjectionEntryPoint。这个函数名不是随便起的——它是EasyHook注入机制的硬性约定。当EasyLoad.dll把Hook.dll加载进目标进程后会立即跳转到这里执行。它不做任何UI操作不弹窗、不写文件、不联网只做三件事1. 初始化全局变量比如记录当前进程ID、保存原函数地址的哈希表2. 调用HookFunctionArrayBySymbol()遍历你定义的结构体数组对每个条目执行LhInstallHook3. 调用LhSetInclusiveACL设置访问控制列表确保只有本DLL能触发钩子回调。这种分离带来的好处是灾难性的可控。假如你的MyReadFile函数里有个死循环最多让目标进程的某个线程卡死Inject.exe依然健在你可以随时点“卸载钩子”虽然本工程没实现卸载按钮但加一行LhUninstallAllHooks()就行如果Inject.exe崩溃了目标进程完全不受影响。我曾经在调试一个CAD插件时故意在Hook.dll里写while(1);结果CAD主界面卡住但没崩溃CtrlAltDel还能切出去杀Inject.exe——这就是职责隔离的价值。2.2 死守VS2010的底层逻辑ABI兼容性与符号解析稳定性为什么非VS2010不可答案藏在targetver.h和stdafx.h这两份头文件里。打开targetver.h你会看到#define WINVER 0x0501 #define _WIN32_WINNT 0x0501 #define _WIN32_WINDOWS 0x0410 #define _WIN32_IE 0x0600这组宏定义强制编译器按Windows XP SP2的API集进行符号解析。VS2012之后的编译器默认启用_WIN32_WINNT0x0600Vista会导致CreateFileW等函数的导入库符号变成__imp__CreateFileW40带40后缀而XP系统里的kernel32.dll导出的是CreateFileW无后缀。EasyHook在解析目标函数地址时用的是GetProcAddress(hMod, CreateFileW)如果编译器生成的导入符号和实际DLL导出符号不匹配LhInstallHook就会返回ERROR_PROC_NOT_FOUND。再看stdafx.h里的关键配置#pragma comment(lib, EasyHook32.lib) #pragma comment(lib, EasyHook64.lib) #pragma comment(linker, /ENTRY:\NativeInjectionEntryPoint\) #pragma comment(linker, /SUBSYSTEM:WINDOWS)其中/ENTRY指令强制指定入口点为NativeInjectionEntryPoint绕过CRT初始化因为目标进程可能没加载msvcr100.dll/SUBSYSTEM:WINDOWS确保生成GUI子系统DLL避免控制台窗口闪现。这些细节在VS2015里会被“智能链接”自动优化掉反而破坏EasyHook的注入链路。还有一个常被忽略的点MFC版本。本工程用的是VS2010自带的MFC90.dll对应VC 10.0而VS2013用的是MFC120.dll。如果你强行用高版本VS打开即使编译通过运行时也会因MFC资源ID错位导致对话框按钮文字乱码——因为IDD_INJECTHELPER_DIALOG里的控件ID在不同MFC版本里映射的内存偏移不同。我试过用VS2019重编译Inject.exe能启动但输入PID后按钮变灰无响应查了半天才发现是GetDlgItem(IDC_EDIT_PID)-GetWindowText()返回空字符串根源就是MFC资源加载失败。2.3 EasyHook双DLL机制EasyLoad.dll为何不可或缺工程目录里有EasyLoad32.dll和EasyLoad64.dll很多人以为这是可选组件。错。它们是整个注入流程的“氧气面罩”。EasyHook的注入分三步走1. Inject.exe把Hook.dll的原始字节不是文件路径写入目标进程内存2. Inject.exe创建远程线程跳转到EasyLoad32/64.dll的LoadLibraryA入口3. EasyLoad.dll负责把Hook.dll的PE头修复、重定位、IAT填充最后调用NativeInjectionEntryPoint。为什么不能直接跳转到Hook.dll的DllMain因为DllMain是Windows Loader调用的而远程注入的DLL不在Loader管理范围内——它的PE头没被解析重定位表没被处理IAT里的kernel32.dll地址还是0。EasyLoad.dll就是干这个脏活的它用纯汇编写的LoadLibrary精简版手动遍历Hook.dll的.reloc节做地址修正扫描.idata节填入GetProcAddress获取的真实函数地址最后才把控制权交给NativeInjectionEntryPoint。你可以把EasyLoad.dll理解成一个“微型PE加载器”。它体积小32位版仅12KB、无依赖不调用任何API只用syscall、专为注入场景定制。工程里预置的EasyLoad32/64.dll是经过签名验证的官方版本SHA256校验值在README.md里有记录绝不是网上随便下的破解版——后者常因修改入口点导致x64注入失败。3. 核心模块详解从InjectHelperDlg.cpp到HookFunctionArrayBySymbol3.1 Inject.exeMFC对话框背后的注入逻辑链打开InjectHelperDlg.cpp核心逻辑集中在CInjectHelperDlg::OnBnClickedButtonInject()函数里。别被MFC的宏吓住它实际执行的就四步第一步获取目标进程IDCString strPid; GetDlgItemText(IDC_EDIT_PID, strPid); DWORD dwPid _ttoi(strPid); // 注意这里用_ttoi而非_atoi兼容Unicode编译 if (dwPid 0) { AfxMessageBox(_T(请输入有效的进程ID)); return; }这里有个坑_ttoi在Unicode模式下会把字符串当UTF-16处理但如果用户粘贴的是ANSI编码的PID比如从任务管理器复制可能高位字节为0导致解析错误。我的实操心得是加一层校验// 实测有效强制按ANSI解析避免Unicode干扰 char szAnsiPid[32]; WideCharToMultiByte(CP_ACP, 0, strPid, -1, szAnsiPid, sizeof(szAnsiPid), NULL, NULL); dwPid atoi(szAnsiPid);第二步打开目标进程并检查权限HANDLE hProcess OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwPid); if (!hProcess) { DWORD dwErr GetLastError(); CString strErr; strErr.Format(_T(OpenProcess失败错误码%lu), dwErr); AfxMessageBox(strErr); return; }注意权限标志位PROCESS_VM_WRITE是必须的否则无法向目标进程内存写入Hook.dll数据PROCESS_QUERY_INFORMATION用于后续判断目标进程是32位还是64位调用IsWow64Process。这里有个经验技巧如果目标进程是服务如svchost.exe普通用户权限打不开必须以管理员身份运行Inject.exe——但工程没做UAC提权所以README里明确写了“右键→以管理员身份运行”。第三步执行注入核心中的核心// 自动判断目标进程位数 BOOL bIs64Bit FALSE; IsWow64Process(hProcess, bIs64Bit); // 构造DLL路径注意这里传的是相对路径EasyHook会自动拼接 LPCTSTR lpszDllPath bIs64Bit ? _T(Hook64.dll) : _T(Hook32.dll); // 执行注入 NTSTATUS status LhInjectRemoteProcess( hProcess, NULL, // 不指定EasyLoad路径用默认内置版本 lpszDllPath, 0, // 无参数 NULL, NULL, NULL); CloseHandle(hProcess); if (FAILED(status)) { CString strErr; strErr.Format(_T(注入失败NT状态码%08X), status); AfxMessageBox(strErr); return; }关键点在于LhInjectRemoteProcess的第五个参数pParameter。本工程传NULL意味着不给Hook.dll传任何初始化参数——所有配置都硬编码在Hook.dll内部。如果你想动态传PID或日志路径可以在这里构造一个结构体然后在NativeInjectionEntryPoint里用LhGetInheritedData读取。第四步注入后验证常被忽略的保险丝// 注入完成后尝试读取Hook.dll在目标进程中的模块基址 HMODULE hMod GetModuleHandle(lpszDllPath); if (hMod) { AfxMessageBox(_T(注入成功)); } else { AfxMessageBox(_T(注入可能失败请检查目标进程是否已加载Hook.dll)); }这段代码其实有缺陷GetModuleHandle只能查本进程不能查远程进程。正确做法是调用EnumProcessModules需psapi.lib但工程为了最小依赖没加。我的补丁方案是在Hook.dll的NativeInjectionEntryPoint末尾写一个全局标志位Inject.exe用ReadProcessMemory去读这个地址的值来确认。3.2 Hook.dllHookFunctionArrayBySymbol函数的实现内幕打开Hook.cppHookFunctionArrayBySymbol是整个钩子逻辑的中枢。它的原型是BOOL HookFunctionArrayBySymbol(HOOK_FUNCTION_INFO* pHookArray, DWORD dwCount)其中HOOK_FUNCTION_INFO定义在Common.h里typedef struct _HOOK_FUNCTION_INFO { LPCWSTR szModuleName; // 模块名如 Lkernel32.dll LPCSTR szProcName; // 函数名如 CreateFileW PROC pfnNewProc; // 新函数地址如 MyCreateFileW PROC* ppfnOriginal; // 存储原函数地址的指针如 g_pfnOriginalCreateFileW } HOOK_FUNCTION_INFO;这个结构体设计暗含三个工程智慧1.szModuleName用LPCWSTR宽字符因为EasyHook内部用LoadLibraryW加载模块必须传Unicode2.szProcName用LPCSTR窄字符因为GetProcAddress只接受ANSI函数名CreateFileW不是CreateFileW的宽字符版3.ppfnOriginal是指针的指针这样可以在函数内部直接*ppfnOriginal pfnOriginal调用方无需二次解引用。函数内部执行流程分五步步骤1遍历结构体数组逐个解析模块for (DWORD i 0; i dwCount; i) { HMODULE hMod GetModuleHandleW(pHookArray[i].szModuleName); if (!hMod) { hMod LoadLibraryW(pHookArray[i].szModuleName); // 动态加载确保模块存在 if (!hMod) continue; // 模块不存在跳过 }这里有个关键细节GetModuleHandleW返回的是本进程的模块句柄但EasyHook需要的是目标进程的模块基址。所以实际调用LhInstallHook前会用GetModuleInformation获取模块大小再结合VirtualQueryEx确认目标进程中该模块的准确基址——这部分由EasyHook底层自动完成你不用管。步骤2获取原函数地址并校验FARPROC pfnOriginal GetProcAddress(hMod, pHookArray[i].szProcName); if (!pfnOriginal) { // 尝试用Ordinal方式获取某些系统DLL用序号导出 pfnOriginal GetProcAddress(hMod, MAKEINTRESOURCEA(24)); // 示例CreateFileW是kernel32的24号导出 } if (!pfnOriginal) continue;GetProcAddress失败很常见比如ntdll.dll里的NtCreateFile在不同Windows版本序号不同。工程里预置的Common.cpp有个GetProcByOrdinal辅助函数专门处理这类情况。步骤3安装钩子并存储原地址NTSTATUS status LhInstallHook( pfnOriginal, pHookArray[i].pfnNewProc, NULL, g_hHook[i] // 全局钩子句柄数组 ); if (SUCCEEDED(status)) { *pHookArray[i].ppfnOriginal pfnOriginal; LhSetInclusiveACL(g_dwMainThreadId, 1, g_hHook[i]); // 只对主线程生效 }g_hHook[i]是全局LPHOOK_TRACE_INFO数组用于后续卸载。LhSetInclusiveACL设置访问控制列表第一个参数是线程ID数组第二个是数组长度第三个是钩子句柄——这意味着你可以精确控制“只对Explorer.exe的主线程挂钩其他线程不拦截”这对调试极其有用。步骤4启用钩子延迟启用策略// 默认不立即启用等用户触发后才激活 // 启用代码放在MyCreateFileW等回调函数里首次调用时执行 // LhSetExclusiveACL(g_dwMainThreadId, 1, g_hHook[i]);工程采用“懒启用”策略安装钩子后不马上生效等第一个API调用进来时在回调函数里调用LhSetExclusiveACL才真正开启拦截。这样避免注入瞬间大量API被拦截导致目标进程卡顿。步骤5错误聚合与日志if (FAILED(status)) { TCHAR szLog[256]; _stprintf_s(szLog, _T(Hook失败%s!%s状态码%08X), pHookArray[i].szModuleName, pHookArray[i].szProcName, status); OutputDebugString(szLog); }所有错误都输出到Visual Studio的Output窗口方便调试。如果你要写入文件日志只需把OutputDebugString换成WriteFile但要注意多线程安全——WriteFile本身是线程安全的但文件句柄需在DllMain里用CreateFile打开并缓存。3.3 Common.cpp那些让工程真正“开箱即用”的胶水代码Common.cpp看似平淡实则是工程稳定性的压舱石。它包含三个必读函数GetProcessArchitecture()精准识别目标进程位数DWORD GetProcessArchitecture(HANDLE hProcess) { BOOL bIsWow64 FALSE; if (IsWow64Process(hProcess, bIsWow64)) { return bIsWow64 ? ARCH_X64_ON_X86 : ARCH_X64; } return ARCH_X86; }注意IsWow64Process在x64系统上调用x64进程会返回FALSE所以必须配合GetNativeSystemInfo判断宿主系统位数。工程里做了双重校验比单纯调用sizeof(void*)可靠得多。SafeStrcpy()防御式字符串拷贝void SafeStrcpy(wchar_t* dst, size_t dstSize, const wchar_t* src) { if (!dst || !src || dstSize 0) return; size_t len wcslen(src); if (len dstSize) len dstSize - 1; wcsncpy_s(dst, dstSize, src, len); dst[len] L\0; }所有涉及GetWindowText、GetModuleFileName的调用都走这个函数彻底杜绝缓冲区溢出。我在某次测试中故意输入超长PID1000个字符发现没崩溃——就是因为SafeStrcpy把strPid截断了。LogToFile()线程安全的日志写入CRITICAL_SECTION g_csLog; void LogToFile(LPCWSTR lpszFormat, ...) { static HANDLE hLog INVALID_HANDLE_VALUE; if (hLog INVALID_HANDLE_VALUE) { hLog CreateFile(Lhook_log.txt, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); SetFilePointer(hLog, 0, NULL, FILE_END); } EnterCriticalSection(g_csLog); va_list args; va_start(args, lpszFormat); TCHAR szBuf[1024]; _vsnwprintf_s(szBuf, _countof(szBuf), _TRUNCATE, lpszFormat, args); va_end(args); DWORD dwWritten; WriteFile(hLog, szBuf, (DWORD)(wcslen(szBuf) * sizeof(WCHAR)), dwWritten, NULL); LeaveCriticalSection(g_csLog); }用CRITICAL_SECTION而非mutex因为mutex在低权限进程里可能创建失败WriteFile直接写Unicode文本避免ANSI编码乱码。4. 实操全流程从VS2010新建工程到监控记事本文件操作4.1 环境准备与工程加载5分钟搞定前提条件- Windows 7 SP1 或更高版本XP需额外安装KB976932补丁- Visual Studio 2010 Professional 或更高版本Express版不支持MFC需升级- 管理员权限右键VS2010图标→“以管理员身份运行”。步骤1解压并校验完整性下载ZIP包后不要直接双击打开。用命令行进入解压目录执行certutil -hashfile Hook.sln SHA256对比README.md里记录的SHA256值例如a1b2c3...。这一步防篡改——EasyHook相关DLL若被替换注入会失败且报STATUS_INVALID_IMAGE_FORMAT。步骤2加载解决方案双击Hook.slnVS2010会自动加载两个项目InjectHelperMFC EXE和HookDLL。注意右下角状态栏显示“解决方案配置Debug|Win32”或“Debug|x64”这是默认配置无需更改。步骤3检查平台工具集右键InjectHelper项目→“属性”→“配置属性”→“常规”→“平台工具集”确认是v100VS2010专用。如果是v110或v120点击下拉框选回v100否则编译会报error MSB8020: The build tools for v110 cannot be found。步骤4生成解决方案按CtrlShiftB等待输出窗口显示 生成: 成功 2 个失败 0 个最新 0 个跳过 0 个 此时InjectHelper\Debug\目录下会有Inject.exe和Hook32.dllHook\x64\Debug\目录下会有Hook64.dll。注意Hook32.dll必须和Inject.exe在同一目录否则注入时找不到DLL。4.2 首次实战监控记事本的文件创建行为目标让记事本每次新建文件时弹窗提示“检测到CreateFileW调用”。步骤1启动记事本并获取PID- 运行notepad.exe- 打开任务管理器CtrlShiftEsc→“详细信息”选项卡- 找到notepad.exe进程右键→“转到服务”记下PID列的数字比如2345-重要确认记事本是32位还是64位。在任务管理器里64位进程名称后有*32标记没有的就是64位。XP系统只有32位。步骤2修改Hook.dll的钩子数组打开Hook.cpp找到g_HookArray[]定义HOOK_FUNCTION_INFO g_HookArray[] { { Lkernel32.dll, CreateFileW, (PROC)MyCreateFileW, g_pfnOriginalCreateFileW }, // { Lkernel32.dll, ReadFile, (PROC)MyReadFile, g_pfnOriginalReadFile }, };取消第一行的注释删除//保留第二行注释。然后在MyCreateFileW函数里加弹窗HANDLE WINAPI MyCreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { if (lpFileName wcscmp(lpFileName, LCONOUT$) ! 0) { // 排除控制台输出 TCHAR szMsg[512]; _snwprintf_s(szMsg, _countof(szMsg), _TRUNCATE, L拦截到CreateFileW\n%s\n访问模式%lu, lpFileName, dwDesiredAccess); MessageBoxW(NULL, szMsg, LHook检测, MB_OK | MB_ICONINFORMATION); } return g_pfnOriginalCreateFileW( lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); }步骤3重新编译并注入- 按CtrlShiftB重新生成Hook项目只编译DLL不碰EXE- 确保Inject.exe和新生成的Hook32.dll在同一目录- 运行Inject.exe在PID输入框填2345点“注入”按钮- 切换到记事本按CtrlN新建文件观察是否弹出提示框。预期现象- 弹窗出现显示文件路径如C:\Users\Admin\Desktop\新建文本文档.txt- 记事本正常创建文件无卡顿- 关闭记事本后Inject.exe仍运行说明钩子未泄漏资源。4.3 进阶技巧如何让钩子只对特定进程生效默认情况下HookFunctionArrayBySymbol会对所有注入的进程生效。但有时你只想监控explorer.exe不想影响chrome.exe。方法有两个方法1在NativeInjectionEntryPoint里加进程名判断// 在Hook.cpp开头加全局变量 TCHAR g_szProcessName[MAX_PATH] {0}; // 修改NativeInjectionEntryPoint extern C NTSTATUS NTAPI NativeInjectionEntryPoint(IN PVOID hInstance) { // 获取当前进程名 GetModuleFileName(NULL, g_szProcessName, MAX_PATH); PathStripPath(g_szProcessName); // 去掉路径只留文件名 // 只对explorer.exe生效 if (_tcsicmp(g_szProcessName, _T(explorer.exe)) ! 0) { return STATUS_SUCCESS; // 直接退出不安装钩子 } // 后续执行HookFunctionArrayBySymbol... }方法2用EasyHook的ACL机制动态控制// 在MyCreateFileW回调里 DWORD dwCurrentPid GetCurrentProcessId(); if (dwCurrentPid ! 1234) return g_pfnOriginalCreateFileW(...); // 只对PID1234的进程拦截 // 或者用进程名 TCHAR szName[MAX_PATH]; GetModuleFileNameEx(GetCurrentProcess(), NULL, szName, MAX_PATH); if (_tcsicmp(PathFindFileName(szName), _T(notepad.exe)) ! 0) { return g_pfnOriginalCreateFileW(...); }注意GetModuleFileNameEx需要psapi.lib在项目属性里添加即可。5. 常见问题与排查技巧实录那些年踩过的坑5.1 编译期问题速查表错误码错误信息根本原因解决方案LNK2019: unresolved external symbol __imp__LhInstallHook16链接器找不到EasyHook函数EasyHook32.lib路径错误或位数不匹配检查项目属性→“链接器”→“常规”→“附加库目录”确认指向$(SolutionDir)Lib\32位项目用EasyHook32.lib64位用EasyHook64.libC2664: LhInstallHook : cannot convert parameter 2 from FARPROC to PROC类型转换失败MyCreateFileW函数声明与PROC类型不兼容确保钩子函数用WINAPI调用约定且参数个数/类型与原函数完全一致参考winbase.h里的CreateFileW原型error D8016: /clr and /MT command-line options are incompatibleCLR与MT运行时冲突项目启用了CLR支持右键项目→“属性”→“常规”→“公共语言运行时支持”设为“无公共语言运行时支持”warning C4996: GetVersionEx: was declared deprecated使用了废弃APIGetVersionEx在Win10中被禁用替换为VerifyVersionInfo或直接删掉版本检查代码本工程不需要提示所有警告Warning都可以忽略但C4996除外。VS2010默认不报此警告但如果项目继承了高版本模板需在stdafx.h顶部加#define _CRT_SECURE_NO_WARNINGS。5.2 运行时问题深度排查问题1注入后目标进程立即崩溃Error 0xC0000005这是最痛的问题。原因90%是钩子函数里访问了非法内存。典型场景- 在MyReadFile里调用OutputDebugString但目标进程没加载kernel32.dll极少见-MyCreateFileW里调用MessageBoxW但目标进程是服务无桌面交互权限- 结构体数组里ppfnOriginal指针未初始化导致g_pfnOriginalCreateFileW是随机地址。排查技巧1. 用Process Monitor监控目标进程过滤Process Name为notepad.exe看崩溃前最后一条CreateFile操作是什么2. 在MyCreateFileW开头加__debugbreak();用VS2010附加到目标进程调试3. 把所有MessageBox换成OutputDebugString用DebugView捕获日志。问题2注入成功但无任何拦截日志常见于以下三种情况- 目标进程是64位你却注入了Hook32.dll反之亦然- 钩子函数名拼写错误比如CreateFileW写成CreateFileW末尾空格-GetModuleHandleW(Lkernel32.dll)返回NULL因为目标进程用的是kernelbase.dllWin7系统。终极排查法在HookFunctionArrayBySymbol里加断点用OutputDebugString打印每一步OutputDebugString(L[Hook] 开始安装CreateFileW...\n); OutputDebugString(L[Hook] GetModuleHandleW返回); TCHAR szAddr[32]; _itow_s((int)hMod, szAddr, 16); OutputDebugString(szAddr); OutputDebugString(L\n);问题3注入后目标进程变慢CPU占用率飙升这是“钩子递归调用”的典型症状。比如你在MyCreateFileW里又调用了CreateFileW比如想记录日志到文件就会触发第二次钩子无限循环。解决方案- 用线程局部存储TLS标记当前是否在钩子回调中DWORD g_dwTlsIndex TlsAlloc(); // 在MyCreateFileW开头 if (TlsGetValue(g_dwTlsIndex) ! NULL) { return g_pfnOriginalCreateFileW(...); // 递归调用直接走原函数 } TlsSetValue(g_dwTlsIndex, (LPVOID)1); // ...你的业务逻辑 ... TlsSetValue(g_dwTlsIndex, NULL);或者用EnterCriticalSection但要注意死锁风险。5.3 安全与合规注意事项必须遵守注意本工程仅用于合法授权的软件调试、安全研究及企业内部IT运维。禁止用于任何形式的恶意软件开发、未授权系统监控或侵犯他人隐私的行为。根据《中华人民共和国计算机信息系统安全保护条例》未经授权的系统入侵、数据窃取、服务干扰均属违法行为。技术红线- 绝对禁止挂钩ntdll.dll中的NtTerminateProcess、NtProtectVirtualMemory等敏感函数这极易触发杀毒软件的启发式扫描- 禁止在钩子函数里执行CreateRemoteThread、WriteProcessMemory等跨进程操作这属于典型的恶意行为特征- 日志文件必须写入用户目录如%USERPROFILE%\AppData\Local\HookLog\不得写入系统目录C:\Windows或程序目录C:\Program Files否则UAC会阻止。部署建议- 生产环境务必关闭所有MessageBox和AfxMessageBox改用静默日志- 用sigcheck.exeSysinternals工具验证Hook32.dll的数字签名确保未被篡改- 对Inject.exe进行UPX压缩仅限32位减小体积并规避部分杀软的特征码扫描但不要过度混淆否则EasyHook注入会失败。6. 扩展可能性从API监控到轻量级EDR雏形这套工程的真正价值不在于它能拦截CreateFileW而在于它提供了一个可无限生长的骨架。我把它用在三个真实场景里效果远超预期场景1文件操作白名单审计某医院HIS系统要求“只允许读取D:\HIS\Data\下的病人档案禁止写入任何其他路径”。我在MyCreateFileW里加路径检查if (dwDesiredAccess GENERIC_WRITE) { if (wcsncmp(lpFileName, LD:\\HIS\\Data\\, 13) ! 0) { SetLastError(ERROR_ACCESS_DENIED); return INVALID_HANDLE_VALUE; } }部署后护士误点“另存为”到桌面时弹出“访问被拒绝”但系统无崩溃——因为INVALID_HANDLE_VALUE是合法返回值上层程序按标准错误处理。场景2网络请求日志化Hook wininet.dll扩展g_HookArray加入{ Lwininet.dll, InternetOpenUrlA, (PROC)MyInternetOpenUrlA, g_pfnOriginalInternetOpenUrlA }在MyInternetOpenUrlA里提取URL并写入日志。注意wininet.dll在不同Windows版本导出序号不同必须用GetProcAddress按名查找。场景3进程行为图谱生成用std::mapDWORD, std::vectorstd::wstring g_ProcessCalls;全局存储每个PID的API调用序列。每次MyCreateFileW被调用执行DWORD dwPid GetCurrentProcessId(); g_ProcessCalls[dwPid].push_back(std::wstring(lpFileName));然后用CreateTimerQueueTimer每5秒把数据发到本地HTTP服务器形成进程行为热力图。这已经具备轻量级EDR端点检测与响应的核心能力。最后分享一个小技巧如果你想让Hook.dll在目标进程启动时自动加载免手动注入可以把Inject.exe改造成服务用CreateProcessAsUser启动目标进程并在STARTUPINFO里设置lpDesktop Lwinsta0\\default再配合SetEnvironmentVariable注入DLL路径。但这超出本工程范围需要深入Windows Session 0隔离机制——那是另一个故事了。本文还有配套的精品资源点击获取简介基于Visual Studio 2010开发的完整EasyHook实战项目支持Win32/Win64双平台无需额外配置即可编译运行。Inject.exe是带图形界面的MFC注入工具输入目标进程PID就能把Hook.dll注入到指定进程中Hook.dll封装了稳定API挂钩逻辑通过结构体数组声明要拦截的函数如CreateFileW、ReadFile等自动完成kernel32等系统DLL中函数的替换。工程内置EasyHook32/64.dll、EasyLoad32/64.dll及对应lib文件所有源码InjectHelperDlg.cpp、Hook.cpp、Common.cpp等在VS2010中零错误零警告通过编译。钩子入口统一由NativeInjectionEntryPoint管理批量挂钩功能集中在HookFunctionArrayBySymbol函数中结构清晰、易于修改和扩展。适用于Windows平台下的API调用监控、文件操作拦截、进程行为审计、调试辅助与日志记录等实际开发场景。本文还有配套的精品资源点击获取

相关新闻