
简介面向VC开发者的API Hook学习与实战资源聚焦通过截获CreateFile和CloseHandle实现对WORD/DOC文件的透明加解密与防拷贝同时探讨chorus与hook的区别适合有一定C/C基础、希望深入Windows系统钩子机制的读者。资源共24个文件压缩包仅56KB以h头文件、cpp源文件为主附带dsp/dsw工程文件、已编译的API_HOOK.dll与演示exe、以及rc资源文件可对照源码理解Hook注入与应用层拦截的完整流程。已有1225人学习。内容不仅覆盖CreateFile/CloseHandle的Hook实现还记录了实际开发中曾因CreateFileW第3个参数未指定写访问权导致Hook失效的排错要点有助于规避同类问题。整体小巧精炼适合作为Windows系统编程、文件安全与防拷贝项目的参考。1. 从“拷走打不开”说起CreateFile 与 CloseHandle 之间发生了什么从同事机器上拷走的 doc 文件双击打开全是乱码回到原电脑一切正常。这种“带不走”的效果不是文件加密软件而是一个加载进 WinWord.exe 的 DLL 在 CreateFileW 返回之后、CloseHandle 关闭之前对落盘内容做了替换。项目就是用 Visual C 6.0 调用 API Hook截获 CreateFile 和 CloseHandle 两个接口对 Word 文档做透明加解密同时实现文件防拷贝。核心难点不在加解密算法而在挂载点选择、句柄权限和 Word 保存临时文件的干扰。源码包里那条记录“以前不能用的原因是 CreateFileW 的第 3 个参数没有指定写访问权”实际是最值得先看的排错线索多数 Hook 失败都是从这里开始的。2. API Hook 选型IAT 替换和内联 Hook 分别解决什么2.1 两种挂载点进程导入表和函数入口API Hook 原理上就两条路线。第一条是改进程的导入地址表IAT把 CreateFileW 在 IAT 里对应的地址替换成自己的函数。进程里大部分对 CreateFileW 的调用都走系统 DLL链接器会把这些符号地址写进模块的导入表Hook 代码只需要拿到模块基址和导入表地址找到对应项改写即可。这种方案实现快、不碰机器码但覆盖不到通过 GetProcAddress 动态取地址、或延迟加载 DLL 的调用路径。Word 自身有很多动态加载模块实际拦截漏点不少。第二条是内联 Hook直接覆盖 kernel32!CreateFileW 入口处几个字节改成跳转到自己的函数。这样所有调用路径都会被拦截不区分静态导入还是动态解析。代价是要处理被覆盖指令的保存、跳板函数trampoline的重定位x86 下函数入口前五个字节通常可以安全改写x64 下要小心指令边界。这个压缩包里 Main.cpp 的写法就是这条路配合 GetModuleHandleW 拿 kernel32 的基址再交给自定义的跳板逻辑。// 简化示意按名字查找导入表并改写地址 PIMAGE_IMPORT_DESCRIPTOR pImp GetImportDescriptor(hMod, KERNEL32.dll); PIMAGE_THUNK_DATA pThunk GetImportByName(pImp, CreateFileW); DWORD dwOldProtect; VirtualProtect(pThunk-u1.Function, 4, PAGE_READWRITE, dwOldProtect); pThunk-u1.Function (DWORD)HookCreateFileW; VirtualProtect(pThunk-u1.Function, 4, dwOldProtect, dwOldProtect);这段是 IAT 替换最核心的写内存动作。VirtualProtect 先把目标地址改成可写写入后再恢复原保护属性否则在只读页上直接赋值会触发访问违例。真实项目里还要解析 PE 的延迟导入表并且处理“同一个函数被多次导入”的情况否则只改了第一个导入项后面 DLL 的调用还是会落到原始函数上。维度IAT 替换内联 Hook拦截范围仅静态导入进程内所有调用路径实现难度低高需处理指令长度与跳板对 GetProcAddress 动态调用无效有效典型依赖直接解析 PE 结构MinHook / Detours 或手写跳板适用场景对某个模块的特定调用做实验对全局 API 行为做控制2.2 为什么要把 CreateFileW 和 CloseHandle 放一起 HookCreateFileW 负责建立文件对象拿到句柄才算真正“打开”CloseHandle 负责销毁句柄是用户态最后一个能安全拿到句柄状态的位置。文档的加解密必须在内容真正落盘之后、句柄销毁之前完成所以这两个函数天然形成一个拦截边界。常见实现是 CreateFileW 返回句柄后立刻处理文件内容CloseHandle 里只做清理和兜底校验。比如记录句柄与路径的映射关系在关闭时删除 Word 留下的临时副本或者校验文件头是否仍是明文防止某些延迟写入操作绕过加密。两个函数分开拦截比在一个函数里反复猜测状态要稳。源码包里 ExeDlg.cpp 的流程就是按这个思路组织的先在对话框里做配置再安装 Hook。2.3 被反复问到的 chorus 和 hook 区别聊天里经常有人把 chorus 和 hook 放一起对比其实是两种不同距离的干预方式。chorus 类方案偏旁路捕获通常借助文件系统过滤驱动在内核态观察文件流不改动目标进程行为hook 是进程内调用点拦截能针对单个进程做细粒度控制和返回值篡改。本项目选 hook 而不是 chorus原因很直接需要针对 WinWord.exe 做透明加解密还要在用户态控制访问路径不需要加载驱动也不需要管理员权限常驻内核。缺点是只管得住被注入的进程这一点在防拷贝里反而成了特性——其他进程没有解密逻辑看到的只能是密文。3. Detours 风格内联 Hook 接管 CreateFileW 与 CloseHandle核心实现与参数陷阱3.1 先定义函数签名和真实函数指针内联 Hook 要保证被替换后栈平衡函数签名必须与原始 API 完全一致。CreateFileW 是 __stdcall 调用约定参数分别是文件路径、访问权限、共享模式、安全属性、创建方式、属性标志、模板句柄写错一个参数后面排查起来非常痛苦。手写跳板或借用 Detours 风格都行前提是先把类型和调用约定对齐。typedef HANDLE(WINAPI *PFN_CreateFileW)( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); typedef BOOL(WINAPI *PFN_CloseHandle)(HANDLE hObject); PFN_CreateFileW RealCreateFileW (PFN_CreateFileW) GetProcAddress(GetModuleHandleW(Lkernel32.dll), CreateFileW); PFN_CloseHandle RealCloseHandle (PFN_CloseHandle) GetProcAddress(GetModuleHandleW(Lkernel32.dll), CloseHandle);GetProcAddress 从 kernel32 模块里取真实函数地址Hook 库或手写跳板都基于这个地址做指令改写。调用约定这里必须是 WINAPI也就是 __stdcall用默认的 __cdecl 会导致栈指针不平衡Word 会在连续保存几个文件后莫名崩溃。VC 6 编译这类 DLL 时建议在工程设置里把 Calling Convention 显式改成 __stdcall避免宏展开不一致。3.2 Hook 函数拿到真实句柄后再判断自定义的 HookCreateFileW 不能一进来就自己处理文件先调用 RealCreateFileW 让系统完成正常打开流程拿到有效句柄后再判断目标进程和目标文件。这样普通文件的打开路径完全不受影响判断逻辑只加在成功分支上。HANDLE WINAPI HookCreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { // 先走原始函数保证非目标文件不受影响 HANDLE hFile RealCreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); if (hFile INVALID_HANDLE_VALUE) return hFile; // 判断进程是不是 WINWORD.EXE再判断文件是不是 Office 文档 if (IsTargetProcess(GetCurrentProcessId()) IsTargetFile(lpFileName)) { EncryptFileOnDisk(lpFileName); } return hFile; }lpFileName 可能是相对路径、带\\?\前缀的完整路径或网络路径。判断目标文件前最好先 GetFullPathNameW 做一次规范化否则同一文件D:\doc\a.doc和D:\DOC\a.doc会走两条逻辑。IsTargetProcess 用 GetModuleBaseName 或从 PEB 取进程名都行设置一个静态缓存避免每个打开操作都重复取进程名否则 Word 打开文件多的目录时会有可感知的延迟。3.3 原资源里记录的坑CreateFileW 访问权限和共享模式压缩包里有一个文本文件专门记录“以前不能用的原因是 CreateFileW 的第 3 个参数没有指定写访问权”。按 Windows API 标准签名数第二个参数 dwDesiredAccess 才是访问权限第三个 dwShareMode 是共享模式。这里实际要表达的问题有两层如果原来打开的句柄没带 GENERIC_WRITE用它写文件一定会产生拒绝访问如果按参数位置严格数到第三个dwShareMode 没指定 FILE_SHARE_WRITEWord 或 Hook 里重新打开文件时也会因为共享冲突失败。两条都需要直接在代码里校验。提示dwShareMode 不指定 FILE_SHARE_WRITE自己重新打开文件做加密时大概率得到 ERROR_SHARING_VIOLATION。参数典型值说明dwDesiredAccessGENERIC_READ | GENERIC_WRITE必须包含写权限否则 WriteFile 失败dwShareModeFILE_SHARE_READ | FILE_SHARE_WRITE不加会引发共享冲突dwCreationDispositionOPEN_EXISTING打开已有文件用这个CREATE_ALWAYS 会清空内容dwFlagsAndAttributesFILE_ATTRIBUTE_NORMAL对普通文档足够不需要 FILE_FLAG_BACKUP_SEMANTICSBOOL EncryptFileOnDisk(LPCWSTR lpFileName) { // 重新打开一个同时具备读写权限的句柄 HANDLE hWrite CreateFileW(lpFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hWrite INVALID_HANDLE_VALUE) return FALSE; DWORD dwSize GetFileSize(hWrite, NULL); if (dwSize 0 || dwSize INVALID_FILE_SIZE) { CloseHandle(hWrite); return FALSE; } BYTE *pBuf (BYTE*)HeapAlloc(GetProcessHeap(), 0, dwSize); DWORD dwRead 0; ReadFile(hWrite, pBuf, dwSize, dwRead, NULL); // 演示用逐字节异或正式场景换成带偏移的流式变换 for (DWORD i 0; i dwRead; i) pBuf[i] ^ 0xA5; SetFilePointer(hWrite, 0, NULL, FILE_BEGIN); DWORD dwWritten 0; WriteFile(hWrite, pBuf, dwRead, dwWritten, NULL); SetEndOfFile(hWrite); HeapFree(GetProcessHeap(), 0, pBuf); CloseHandle(hWrite); return TRUE; }这个函数里GENERIC_READ | GENERIC_WRITE 保证读写都能做FILE_SHARE_READ | FILE_SHARE_WRITE 允许 Word 后续继续以共享方式打开文件两者缺一个都会导致 Word 保存时报“文件被占用”。dwSize 是 DWORD超过 2GB 的文件会溢出实际拦截普通 Word 文档没问题但代码里最好加上 INVALID_FILE_SIZE 判断。HeapAlloc 一次性读整个文件的做法仅适合中小文件VC 6 编译的 Hook DLL 跑在 Word 进程里最影响运行效率的往往就是这种大块内存申请建议改成按 64KB 分块读写。3.4 CloseHandle Hook做句柄映射和临时文件清理CloseHandle 的 Hook 不需要改句柄内容主要做两件事从句柄映射表里找到对应路径决定是否删除临时文件再把映射项移除避免表无限增长。句柄本质是内核对象句柄不能直接问系统要路径必须在 HookCreateFileW 成功返回后自行记录。BOOL WINAPI HookCloseHandle(HANDLE hObject) { std::wstring strPath GetPathFromHandleMap(hObject); if (!strPath.empty()) { if (IsTempWordFile(strPath)) DeleteFileW(strPath.c_str()); // 防止临时副本留在磁盘上 RemoveFromHandleMap(hObject); } return RealCloseHandle(hObject); }映射表用 std::map 或自实现哈希表都行注意 Word 打开大量文件时 CloseHandle 会被频繁调用查找性能直接决定 Hook 对打字、保存的拖累程度。另一个容易忽略的点是 DeleteFileW 如果文件正被别的句柄引用会失败返回 FALSE 不需要马上处理等系统清理。真实句柄的关闭永远要交给 RealCloseHandle不要因为插了逻辑就改变返回值。4. 进程注入、OLE 文件头识别与 Word 临时文件防拷贝落到文件泄漏路径4.1 让 Hook DLL 进 WinWord.exeSetWindowsHookEx 注入Hook 代码写好后要加载进 WinWord.exe。常见做法是 SetWindowsHookExW 挂一个 WH_GETMESSAGE 钩子让系统把 DLL 注入到指定线程也可以直接 CreateRemoteThread 调 LoadLibraryW。压缩包里 Exe 模块显然是先做注入端再把 API_HOOK.dll 送进目标进程。HWND hWnd FindWindowW(LOpusApp, NULL); // Word 主窗口类名 DWORD dwThreadId GetWindowThreadProcessId(hWnd, NULL); HHOOK hHook SetWindowsHookExW(WH_GETMESSAGE, HookProc, g_hInstDll, dwThreadId);HookProc 是一个空的消息钩子过程只负责让 DLL 被加载不处理消息。SetWindowsHookEx 要求注入者与目标进程位数一致VC 6 编出来的 32 位 DLL 不能注入 64 位 Word反过来也一样在 64 位系统上先确认 Office 是 32 位还是 64 位否则会出现“DLL 已加载但 WinWord 里看不到任何效果”的怪问题。4.2 用文件头识别 doc/docx而不是只看扩展名Word 文档分两类老版 doc 是 OLE 复合文档文件头固定为 D0 CF 11 E0docx 是 zip 容器文件头为 PK50 4B 03 04。按扩展名过滤在大多数场景够用但“另存为”、“插入对象”和 WPS 互转都会产生扩展名与内容不一致的临时文件容易误判。BOOL IsOfficeFile(LPCWSTR lpFileName) { HANDLE hFile CreateFileW(lpFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile INVALID_HANDLE_VALUE) return FALSE; BYTE bHead[4] {0}; DWORD dwRead 0; ReadFile(hFile, bHead, 4, dwRead, NULL); CloseHandle(hFile); // doc: D0 CF 11 E0 docx: 50 4B 03 04 return (bHead[0] 0xD0 bHead[1] 0xCF bHead[2] 0x11 bHead[3] 0xE0) || (bHead[0] 0x50 bHead[1] 0x4B bHead[2] 0x03 bHead[3] 0x04); }用文件头做判断有一个额外好处HookCreateFileW 打开文件时先读四个字节空文件和系统非文档文件会直接返回减少误加密。要注意 IsOfficeFile 内部调用 CreateFileW 时建议直接走 RealCreateFileW 或换一个内部私有函数否则会再次进入 HookCreateFileW 形成递归。实际项目里用 RealCreateFileW 最省心。4.3 防拷贝策略表什么文件能开、什么不能开防拷贝不只是“文件拷走打不开”还包括“从共享目录打开”、“U 盘复制”这些路径。Hook 可以根据文件所在卷类型决定策略这一步写在 EncryptFileOnDisk 之前避免对移动盘文件也做透明加解密。文件位置进程处理策略效果本机固定盘 C/D 盘WinWord.exe创建后加密读取时还原正常工作落盘为密文网络共享路径WinWord.exe直接返回拒绝访问防止通过共享目录把文件带走移动硬盘/U 盘WinWord.exe拒绝打开或只读拷到 U 盘也无法使用任意路径其他进程不做任何处理读到的是加密后的密文对应代码用一个辅助函数查卷类型BOOL IsBlockedDrive(LPCWSTR lpFileName) { // 取盘符根路径判断卷类型 WCHAR szRoot[4] { lpFileName[0], L:, L\\, 0 }; UINT uType GetDriveTypeW(szRoot); if (uType DRIVE_REMOVABLE || uType DRIVE_CDROM) return TRUE; // 网络路径以 \\ 开头也可以用 WNetGetConnection 拿实际共享名 if (lpFileName[0] L\\ lpFileName[1] L\\) return TRUE; return FALSE; }网络路径判断要考虑 UNC 路径在低层会被重定向成\\?\UNC\server\share只判断前两个字符不够最好统一先 GetFullPathNameW 再判断。策略表这里只是业务层决策具体项目里按需要调整比如允许读取但禁止写入就能做到“能看不能拷”。4.4 Word 临时文件和快速保存加密容易把文档搞坏Word 保存文档时先创建~$开头的临时文件写完后替换原文件如果勾选了“允许快速保存”还会把修改部分追加到临时结构里。Hook 把临时文件也加密Word 在关闭时读取临时内容合并就会报告“文件已损坏”。这个坑比句柄权限更隐蔽因为错误出现在保存流程最后一步。处理思路有两个一是对临时文件跳过加密在 IsTargetFile 里排除~$开头和扩展名为 .tmp 的文件二是在 CloseHandle 的 Hook 里把临时文件清理掉让 Word 下次启动时直接从主文档恢复。第二种方式配合防拷贝更彻底临时文件往往包含编辑过程中的完整内容比主文档更容易泄漏。注意排除规则要放在文件头识别之前否则临时文件内容识别出来后还是会走加密分支。5. 验证 Hook 是否真的生效日志开关、文件头检查与 DLL 调试验证一套 Hook 最忌讳“直接打开 Word 试试”因为 DLL 没注入、文件类型判断错、加密函数 WriteFile 失败表现可能都是“没效果”。我给这套项目配一个双模式开关g_bLogOnly 只记录不加密先把链路打通再动数据。BOOL g_bLogOnly TRUE; BOOL g_bCryptoEnabled FALSE;g_bLogOnly 为 TRUE 时HookCreateFileW 里只调用 OutputDebugStringW 写出进程名、完整路径、dwDesiredAccess 和文件头四个字节加密函数直接 return。用 DebugView 或 VS 输出窗口观察。看到 Word 的保存路径被记录说明注入和 Hook 生效没有记录先查 DLL 有没有加载进 WinWord.exe。这一步十分钟内就能定位是注入问题还是 Hook 问题比反复改加解密强得多。加载验证用 Process Explorer 看 WinWord.exe 的 DLL 列表出现 API_HOOK.dll 才算注入成功。x64 系统上 32 位 DLL 注入 64 位 Word 不会出现在列表里优先确认 Office 位数。链路通畅后再把 g_bLogOnly 改成 FALSE打开一个测试 doc保存后用十六进制编辑器看文件头正常结果应该是 D0 CF 11 E0 变成其他值而 docx 的 PK 头也不见。调试 DLL 时把 Word 设为调试会话目标VC 6 菜单 Project Settings 的 Debug 选项卡里Executable for debug session 填 WINWORD.EXE 的完整路径Working directory 指向测试文档目录F5 启动后断点会停在 HookCreateFileW。注意 Word 如果是已安装的 32 位版本VC 6 可以直接调试如果是 64 位版本VC 6 的调试器认不了改用 VS 或者用 OutputDebugString 日志代替断点。日志开关保持可切换状态后续排查“某个文件没加密”“某个网络路径没拦”时都不用重新编译改一个全局变量再重启 Word 就能复现效率高很多。本文还有配套的精品资源点击获取