简介这份资源聚焦Windows网络编程中的API拦截技术面向具备一定C与Winsock基础、希望深入理解系统级钩子机制的中高级开发者。其核心目标是通过拦截ws2_32.dll中的send()函数将套接字发送的数据捕获并写入OB文件可用于网络监控、安全审计或性能分析等场景的学习与实验。压缩包共23个文件约898KB包含cpp与h源码、dll与lib库文件、def导出定义、dsp与dsw工程文件以及pdb、obj、ilk等编译调试产物和说明文档构成一套可直接编译调试的完整VC工程。目前已有1262人学习下载。借助源码与工程配置读者可掌握导入地址表替换、钩子注入与数据落盘的关键思路理解拦截点选择、原始函数转发及性能与合法性权衡并对照说明文档完成编译验证与二次改造。1. 从一次抓包需求说起为什么我要拦截 ws2_32.dll 的 send去年排查一个客户端和服务端通信异常的问题日志里只看到“发送失败”但具体发了什么、发到哪一步断的完全是个黑匣子。用现成的抓包工具吧加密流量解不开在应用层加日志吧改动太大还容易漏掉底层调用。后来我干脆换了个思路既然所有 TCP 发送最终都要走ws2_32.dll的send那我直接在这个函数入口做拦截把缓冲区内容原样 dump 到文件里不就能拿到最原始的发送数据了吗这个项目做的就是这件事通过 API Hook 技术拦截ws2_32.dll中的send函数在目标进程调用它时先把待发送的内存数据写入一个.ob文件再放行原始调用。它适合两类人一是需要做协议逆向、流量分析但拿不到明文的开发者二是想学习 Windows 平台 API Hook 原理、想自己动手写一个拦截 Demo 的 Windows 安全方向初学者。整个项目不依赖驱动纯用户态实现编译出来就是一个 DLL注入目标进程即可生效。2. 技术选型与整体架构为什么是 Inline Hook 而不是 IAT Hook2.1 两种 Hook 路线的取舍在 Windows 用户态拦截 API常见做法有两种IAT Hook 和 Inline Hook。IAT Hook 的思路是修改目标模块的导入地址表把send的地址替换成我们自己的函数地址。这种做法实现简单但有个致命缺陷如果目标程序是动态加载ws2_32.dll或者通过GetProcAddress直接拿函数地址调用IAT Hook 就完全失效了。而且现在很多程序会做 IAT 校验改表容易被检测到。Inline Hook 则是直接修改send函数开头的机器码写入一条跳转指令让执行流跳到我们的代理函数。这种方式的优势是不依赖导入表只要函数地址被调用就会命中覆盖面更广。代价是需要处理指令长度、内存保护属性、多线程竞争等问题实现复杂度高一些。这个项目选择了 Inline Hook因为它的目标场景是“不管目标程序怎么调用 send我都要拦到”IAT Hook 的漏网情况太多。2.2 整体模块划分项目结构很清晰核心就三个模块Hook 引擎模块负责定位send函数地址、修改函数头指令、写入跳转、保存原始字节用于恢复。代理函数模块自定义的MySend函数签名与send完全一致负责把数据写入.ob文件然后调用原始send。DLL 入口与注入模块DllMain中在DLL_PROCESS_ATTACH时安装 Hook在DLL_PROCESS_DETACH时卸载 Hook注入方式可以用常见的远程线程注入或SetWindowsHookEx。目录结构大致如下ws2_32_hook/ ├── src/ │ ├── dllmain.cpp // DLL 入口安装/卸载 Hook │ ├── hook_engine.cpp // Inline Hook 核心实现 │ ├── hook_engine.h │ ├── my_send.cpp // 代理 send 函数写文件逻辑 │ └── my_send.h ├── include/ │ └── detours_lite.h // 简化的 Hook 辅助宏 ├── build/ │ └── ws2_32_hook.sln // VS 工程文件 └── README.md2.3 关键数据结构在 Hook 引擎里需要保存原始函数头部的若干字节以便卸载时恢复。常见做法是定义一个结构体typedef struct _HOOK_CONTEXT { LPVOID targetFunc; // 目标函数地址这里是 send BYTE originalBytes[16]; // 保存原始机器码 BYTE patchBytes[16]; // 跳转指令机器码 SIZE_T patchSize; // 实际修改的字节数 BOOL installed; // 是否已安装 } HOOK_CONTEXT;originalBytes的长度取决于跳转指令占多少字节。在 32 位环境下一条push addr; ret组合占 6 字节64 位环境下需要用mov rax, addr; jmp rax占 12 字节。项目里为了通用统一预留 16 字节。3. 核心模块拆解从定位 send 到写入 .ob 文件的完整链路3.1 定位 send 函数地址send函数在ws2_32.dll中导出可以通过GetProcAddress拿到地址。但要注意如果目标进程还没加载ws2_32.dll需要先LoadLibrary。代码片段// 获取 ws2_32.dll 模块句柄如果未加载则主动加载 HMODULE hWs2 GetModuleHandleA(ws2_32.dll); if (hWs2 NULL) { hWs2 LoadLibraryA(ws2_32.dll); } if (hWs2 NULL) { return FALSE; // 加载失败直接返回 } // 获取 send 函数地址 typedef int (WSAAPI *SEND_PROC)(SOCKET s, const char *buf, int len, int flags); SEND_PROC pSend (SEND_PROC)GetProcAddress(hWs2, send); if (pSend NULL) { return FALSE; }这里有个细节send的调用约定是WSAAPI在 32 位下是stdcall64 位下就是默认的fastcall。代理函数的签名必须和它完全一致否则栈会乱掉程序直接崩溃。3.2 安装 Inline Hook拿到send地址后就可以修改函数头了。核心步骤是保存原始字节 → 修改内存保护属性 → 写入跳转指令 → 恢复保护属性。BOOL InstallHook(HOOK_CONTEXT *ctx, LPVOID target, LPVOID proxy) { ctx-targetFunc target; ctx-patchSize 12; // 64 位下使用 mov rax, addr; jmp rax // 1. 保存原始字节 memcpy(ctx-originalBytes, target, ctx-patchSize); // 2. 构造跳转指令mov rax, proxy; jmp rax ctx-patchBytes[0] 0x48; // REX.W ctx-patchBytes[1] 0xB8; // mov rax, imm64 *(ULONGLONG *)(ctx-patchBytes 2) (ULONGLONG)proxy; ctx-patchBytes[10] 0xFF; // jmp rax ctx-patchBytes[11] 0xE0; // 3. 修改内存保护属性为可读写执行 DWORD oldProtect; if (!VirtualProtect(target, ctx-patchSize, PAGE_EXECUTE_READWRITE, oldProtect)) { return FALSE; } // 4. 写入跳转指令 memcpy(target, ctx-patchBytes, ctx-patchSize); // 5. 恢复保护属性 VirtualProtect(target, ctx-patchSize, oldProtect, oldProtect); FlushInstructionCache(GetCurrentProcess(), target, ctx-patchSize); ctx-installed TRUE; return TRUE; }参数说明target是send的地址proxy是我们自己的MySend函数地址。FlushInstructionCache这一步不能省否则 CPU 可能执行到旧的指令缓存导致 Hook 不生效。3.3 代理函数 MySend 的实现代理函数要做两件事把发送数据写入.ob文件然后调用原始send。写文件时要注意线程安全多个线程同时调用send时不能把文件内容写乱。// 全局变量保存原始 send 地址和文件句柄 static SEND_PROC g_originalSend NULL; static HANDLE g_hLogFile INVALID_HANDLE_VALUE; static CRITICAL_SECTION g_cs; // 临界区保证写文件线程安全 int WSAAPI MySend(SOCKET s, const char *buf, int len, int flags) { // 1. 写入 .ob 文件 if (g_hLogFile ! INVALID_HANDLE_VALUE buf ! NULL len 0) { EnterCriticalSection(g_cs); // 写入长度前缀方便后续解析 DWORD written; WriteFile(g_hLogFile, len, sizeof(len), written, NULL); // 写入实际数据 WriteFile(g_hLogFile, buf, len, written, NULL); LeaveCriticalSection(g_cs); } // 2. 调用原始 send保证网络功能正常 if (g_originalSend ! NULL) { return g_originalSend(s, buf, len, flags); } return SOCKET_ERROR; }这里用CRITICAL_SECTION而不是互斥体是因为临界区在单进程内开销更小。写入格式采用“长度 数据”的方式后续可以用脚本按长度切分还原出每一次send调用的完整内容。3.4 DLL 入口与生命周期管理DllMain负责在进程加载时安装 Hook卸载时恢复原始函数。注意DllMain里不能做太复杂的操作尤其是不能调用LoadLibrary去加载其他 DLL否则可能死锁。BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) { switch (reason) { case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(hModule); InitializeCriticalSection(g_cs); // 打开 .ob 文件追加模式 g_hLogFile CreateFileA(send_dump.ob, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); // 安装 Hook InstallSendHook(); break; case DLL_PROCESS_DETACH: // 卸载 Hook恢复原始字节 UninstallSendHook(); if (g_hLogFile ! INVALID_HANDLE_VALUE) { CloseHandle(g_hLogFile); } DeleteCriticalSection(g_cs); break; } return TRUE; }DisableThreadLibraryCalls是为了减少不必要的线程通知提升性能。文件用CREATE_ALWAYS每次注入时重新创建避免旧数据干扰。4. 编译、注入与验证让拦截真正跑起来4.1 编译环境与工程配置项目用 Visual Studio 打开ws2_32_hook.sln即可编译。需要注意几点平台工具集选择与目标进程匹配的版本如果目标进程是 32 位DLL 也必须编译成 32 位。运行库建议选择“多线程调试 (/MTd)”或“多线程 (/MT)”避免目标进程缺少对应运行库导致加载失败。链接器需要额外依赖ws2_32.lib因为代理函数里用到了SOCKET类型。编译成功后得到ws2_32_hook.dll这就是要注入的模块。4.2 注入方式的选择常见的注入方式有三种注入方式优点缺点远程线程注入实现简单通用性强容易被安全软件拦截SetWindowsHookEx对消息循环进程有效只对特定类型进程生效手动映射注入隐蔽性好实现复杂不适用于本 Demo这个项目推荐用远程线程注入做验证。核心代码// 在目标进程中分配内存写入 DLL 路径 SIZE_T pathSize strlen(dllPath) 1; LPVOID remoteMem VirtualAllocEx(hProcess, NULL, pathSize, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProcess, remoteMem, dllPath, pathSize, NULL); // 获取 LoadLibraryA 地址创建远程线程 LPTHREAD_START_ROUTINE loadLib (LPTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandleA(kernel32.dll), LoadLibraryA); HANDLE hThread CreateRemoteThread(hProcess, NULL, 0, loadLib, remoteMem, 0, NULL); WaitForSingleObject(hThread, INFINITE);注入成功后目标进程会加载我们的 DLLDllMain被调用Hook 自动安装。4.3 验证拦截效果验证步骤很直接准备一个简单的 TCP 客户端程序调用send发送一段已知字符串。注入 DLL 到该客户端进程。客户端发送数据后检查当前目录下是否生成了send_dump.ob。用十六进制工具打开.ob文件按“4 字节长度 数据”的格式解析看是否与发送内容一致。如果文件为空先检查send是否真的被调用了可以在MySend里加一个OutputDebugString输出调试信息用 DebugView 查看。5. 避坑与常见问题排查5.1 注入后目标进程直接崩溃现象DLL 注入成功但目标进程立刻退出或报错。原因最常见的是代理函数调用约定不匹配。32 位下send是stdcall如果代理函数声明成cdecl栈平衡会出错。另一个原因是patchSize计算错误覆盖了send函数开头不完整的指令导致执行流跳转后指令解码失败。解决确保代理函数签名带WSAAPI宏用调试器附加到目标进程查看崩溃地址是否在send附近如果是检查写入的跳转指令长度是否覆盖了完整指令。可以用反汇编工具确认send开头几条指令的总长度。5.2 Hook 安装成功但拦截不到数据现象DLL 加载了但.ob文件始终为空。原因目标程序可能没有直接调用ws2_32.dll的send而是通过WSASend或者更底层的NtDeviceIoControlFile发送数据。另外如果目标程序在 DLL 注入前就已经获取了send地址并保存在自己的函数指针里Inline Hook 修改的是send函数体仍然会被命中但 IAT Hook 就会漏掉。解决确认目标程序实际调用的 API。可以用 API Monitor 工具先观察。如果是WSASend需要额外 Hook 这个函数。项目当前只覆盖send这是已知边界。5.3 多线程下 .ob 文件内容错乱现象解析.ob文件时发现长度字段和数据对不上或者数据被截断。原因多个线程同时调用send写文件时没有加锁导致一个线程写入长度后另一个线程插入写入顺序被打乱。解决项目里已经用了CRITICAL_SECTION保护写文件操作。如果仍然错乱检查是否在MySend里提前返回而没有释放锁或者g_hLogFile被意外关闭。5.4 卸载 Hook 后程序行为异常现象DLL 卸载后目标程序调用send失败或崩溃。原因卸载时没有正确恢复原始字节或者恢复时内存保护属性没有改回可执行导致send函数头变成不可执行区域。解决UninstallSendHook里必须先用VirtualProtect把目标地址改为PAGE_EXECUTE_READWRITE写回originalBytes再恢复原来的保护属性最后调用FlushInstructionCache。顺序不能乱。5.5 杀软拦截导致注入失败现象CreateRemoteThread返回失败或者 DLL 刚加载就被卸载。原因远程线程注入和 Inline Hook 都是安全软件的重点监控行为。解决在测试环境中将目标进程和注入器加入白名单。这个项目定位是学习和内部调试不建议在生产环境或未授权环境中使用。6. 二次开发方向从 dump send 到协议解析与自动化这个项目目前只做了最基础的一步把send的数据原样写到文件。但拿到.ob文件只是开始真正的价值在于后续怎么用。我一般会从三个方向做二次开发。第一个方向是扩展拦截范围。现在只 Hook 了send但实际通信中recv、WSASend、WSARecv同样重要。可以照着MySend的模式再写MyRecv、MyWSASend把收发数据分别写到不同的文件里加上时间戳和方向标记。这样就能还原出完整的会话时序。第二个方向是改进数据格式。当前的“长度 数据”格式虽然简单但缺少元信息。可以改成自定义的二进制结构typedef struct _DUMP_HEADER { DWORD magic; // 标识比如 0x4F425344 DWORD threadId; // 调用线程 ID ULONGLONG timestamp; // 时间戳毫秒 int dataLen; // 后续数据长度 } DUMP_HEADER;写入时先写这个头再写数据。解析脚本按magic定位每条记录就能拿到线程和时间信息对分析多线程并发发送特别有用。第三个方向是加过滤和触发条件。不是所有send都值得记录可以加一个简单的规则只记录长度大于某个阈值的数据或者只记录包含特定字节序列的数据。这样能大幅减小 dump 文件体积也方便聚焦关键流量。验证改造是否成功我习惯用一个小型回环测试写一个客户端和一个服务端客户端发送已知序列的数据服务端接收后校验。注入 DLL 后对比.ob文件解析出的数据和服务端收到的数据是否完全一致。如果一致说明拦截和写文件逻辑没有破坏原始通信。有个血泪教训早期版本里我直接在MySend里调用fwrite结果在某些多线程场景下 CRT 锁竞争导致性能急剧下降甚至死锁。后来换成 Windows 原生WriteFile加临界区问题才消失。从那以后我每次写 Hook 代理函数都强制走一遍“最小依赖”检查代理函数里只调用最底层的系统 API绝不引入额外的库调用。希望这个思路能帮到你。本文还有配套的精品资源点击获取