资讯动态

Windows DLL注入Hook send函数实现TCP负载二进制捕获

发布时间:2026/10/29 15:45:46 来源:尧图企业网站定制
简介本资源是一份面向Windows底层开发与网络安全分析初学者的API钩子实践项目聚焦于拦截ws2_32.dll中的send()函数并实时捕获发送数据写入OB文件适用于网络监控工具开发、协议行为分析及Winsock调用审计等场景。压缩包共23个文件包含核心源码2个cpp、1个h、编译产物1个dll、2个obj、1个lib、1个exp、1个def、调试支持文件2个pdb、1个idb、1个ilk、1个pch及VC6工程配置dsw/dsp/opt/plg/ncb等完整复现了用户态API Hook的典型构建流程包体仅898KB轻量易部署。已有1264人学习下载读者可直接获取可编译的VC6工程、带符号调试信息的DLL、关键拦截逻辑实现含IAT Hook或Detours风格钩子雏形以及清晰的ReadMe与说明文档快速理解Winsock函数劫持原理、数据截获时机与安全边界控制要点。1. 这不是钩子教学而是一份能立刻验证网络层数据捕获的实战包用 DLL 注入精准劫持 send() 调用把原始 TCP 负载原样落盘为 .ob 文件非日志、非内存快照、是带时间戳长度头的二进制流你刚写完一个客户端但服务端收不到数据Wireshark 显示握手成功却无 payload或者你在调试一个黑盒 SDK它调了 send 却不告诉你发了啥——这时候翻文档、加 printf、甚至重编译都来不及。这个项目就是为这种「网络层失语症」准备的止痛针它不改目标进程一行源码不依赖任何调试器只靠 Windows 底层 DLL 导出表劫持机制在 send() 函数入口处「卡点截流」把每次调用的 buf len 原封不动序列化进一个 .ob 文件ob output binary每条记录含 8 字节时间戳GetTickCount64、4 字节长度、N 字节原始数据。它不是抓包工具不解析协议也不是日志框架不格式化字符串它就是一根裸露的网线分接头把 send 的血肉直接导出来。适合逆向分析、协议逆向、安全审计、SDK 行为取证尤其当你面对的是无符号、无 pdb、不开源的闭源进程时——这包代码跑起来30 秒内你就能看到它到底往 socket 里塞了什么。2. 架构拆解从 DLL 注入到二进制落盘的四层链路这个项目表面看只是“拦截 send”但实际是四层紧耦合的工程实现注入层负责打进目标进程、Hook 层负责接管函数指针、转发层确保业务不崩、落盘层保证数据可追溯。漏掉任何一层要么挂不上去要么程序崩溃要么数据错乱。我把它拆成四个模块每个模块都对应一个源文件和一个关键决策点。2.1 注入层CreateRemoteThread LoadLibraryW 的最小可行路径注入不是目的是手段。这里没用反射式注入或 APC 注入这些炫技方案而是最稳的CreateRemoteThreadLoadLibraryW组合。原因很实在兼容性优先。某高校实验室在测试 Win10 21H2 到 Win11 22H2 全系系统时发现反射式注入在某些启用了 HVCI基于虚拟化的安全的设备上会触发 EDR 拦截而标准 LoadLibraryW 注入只要签名正常99% 的环境都放行。核心代码在injector.cpp中// injector.cpp HANDLE hProcess OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); LPVOID pLibRemote VirtualAllocEx(hProcess, NULL, sizeof(szDllPath), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); WriteProcessMemory(hProcess, pLibRemote, szDllPath, sizeof(szDllPath), NULL); // 关键必须用 LoadLibraryW不能用 A 版本否则 Unicode 路径失败 FARPROC pLoadLib GetProcAddress(GetModuleHandle(Lkernel32.dll), LoadLibraryW); HANDLE hThread CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLib, pLibRemote, 0, NULL); WaitForSingleObject(hThread, INFINITE); VirtualFreeEx(hProcess, pLibRemote, 0, MEM_RELEASE); CloseHandle(hThread); CloseHandle(hProcess);提示szDllPath必须是宽字符路径LC:\\path\\ws2_hook.dll且路径中不能含中文或空格不是不能是避免初学者因编码问题卡住。我一般会先用GetFullPathNameW标准化路径再传入。2.2 Hook 层IAT 替换而非 inline hook规避 ASLR 和 hotpatch 冲突为什么不用 Detours 或 MinHook因为这个项目要解决的是「稳定压倒一切」的场景。IAT导入地址表Hook 是最轻量、最兼容的方案它只修改目标进程的 PE 文件中 ws2_32.dll 的导入项把原本指向send真实地址的指针替换成我们 DLL 中的my_send函数地址。它不 patch 任何内存页不触发 DEP/CFG不干扰 JIT 编译也不怕函数被 hotpatchWindows 更新后 send 地址变IAT 项仍有效。关键逻辑在ws2_hook.dll的 DllMain 中// ws2_hook.cpp typedef int (WINAPI *pfn_send)(SOCKET s, const char* buf, int len, int flags); pfn_send real_send nullptr; BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { if (ul_reason_for_call DLL_PROCESS_ATTACH) { HMODULE hWs2 GetModuleHandle(Lws2_32.dll); if (hWs2) { real_send (pfn_send)GetProcAddress(hWs2, send); if (real_send) { // 修改 IAT找到当前模块即本 dll导入的 ws2_32.dll 的 send 条目 PIMAGE_DOS_HEADER dosHeader (PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS ntHeaders (PIMAGE_NT_HEADERS)((BYTE*)hModule dosHeader-e_lfanew); PIMAGE_IMPORT_DESCRIPTOR importDesc (PIMAGE_IMPORT_DESCRIPTOR)( (BYTE*)hModule ntHeaders-OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress ); while (importDesc-Name) { LPCSTR dllName (LPCSTR)((BYTE*)hModule importDesc-Name); if (lstrcmpiA(dllName, ws2_32.dll) 0 importDesc-FirstThunk) { PIMAGE_THUNK_DATA thunk (PIMAGE_THUNK_DATA)((BYTE*)hModule importDesc-FirstThunk); while (thunk-u1.Function) { FARPROC* funcAddr (FARPROC*)thunk-u1.Function; if (*funcAddr (FARPROC)real_send) { // 找到 send 的 IAT 条目替换成我们的钩子函数 DWORD oldProtect; VirtualProtect(funcAddr, sizeof(FARPROC), PAGE_READWRITE, oldProtect); *funcAddr (FARPROC)my_send; VirtualProtect(funcAddr, sizeof(FARPROC), oldProtect, oldProtect); break; } thunk; } } importDesc; } } } } return TRUE; }这段代码干了三件事1定位本 DLL 的 IAT 表2遍历所有导入 DLL找到ws2_32.dll3在它的 FirstThunkIAT中逐项比对找到指向真实send的那个地址然后原子替换。注意VirtualProtect是必须的——IAT 默认是只读页不改保护就写会触发 access violation。2.3 转发层零拷贝转发 errno 透传确保业务逻辑完全无感Hook 后最危险的一步就是调用原函数。很多初学者在这里翻车直接return real_send(...)结果发现目标程序偶尔崩溃或 recv 收不到回包。原因在于errnoWSAGetLastError的透传。Windows Sockets 的错误码是线程局部存储TLSreal_send执行后会设置自己的WSAGetLastError但如果你在my_send里不显式保存并还原上层调用者拿到的就是错的错误码可能误判连接断开。my_send的完整实现如下// ws2_hook.cpp int WINAPI my_send(SOCKET s, const char* buf, int len, int flags) { // 1. 记录原始参数用于落盘 LARGE_INTEGER tick; GetTickCount64(tick); // 注意Win7 才支持若需兼容 Win7 以下用 QueryPerformanceCounter DWORD lastError WSAGetLastError(); // 保存调用前的错误码极少数情况需要 // 2. 调用真实 send int ret real_send(s, buf, len, flags); // 3. 获取调用后的错误码并还原调用前的关键 DWORD newError WSAGetLastError(); WSASetLastError(lastError); // 4. 仅当发送成功ret 0才落盘避免记录失败的垃圾数据 if (ret 0 buf ! nullptr) { HANDLE hFile CreateFileW( Loutput.ob, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); if (hFile ! INVALID_HANDLE_VALUE) { SetFilePointer(hFile, 0, NULL, FILE_END); // 追加写 // 写入8字节 tick 4字节 len ret 字节 buf DWORD written; WriteFile(hFile, tick, sizeof(tick), written, NULL); WriteFile(hFile, len, sizeof(len), written, NULL); WriteFile(hFile, buf, ret, written, NULL); CloseHandle(hFile); } } return ret; // 返回真实结果上层完全无感知 }注意三点WSASetLastError(lastError)必须在real_send之后、return之前执行这是让上层逻辑不崩溃的「后悔药」落盘只在ret 0时进行避免把send失败如WSAEWOULDBLOCK也记下来污染数据CreateFileW用OPEN_ALWAYS而非CREATE_ALWAYS防止并发注入时覆盖已有文件。2.4 落盘层.ob 文件格式定义与跨进程写入安全.ob不是随便起的后缀它代表OutputBinary —— 一种极简二进制容器。每条记录严格按以下结构排列字段长度说明tick8 字节GetTickCount64()返回值单位毫秒可用于时序分析len4 字节实际发送字节数ret值小端序datalen字节原始buf内容未做任何编码或转义这个格式设计有明确意图无分隔符避免buf中恰好含\n或\0导致解析错位定长头8412 字节固定头方便用fseek随机跳转小端序Windows 原生序省去字节序转换开销跨进程写入的安全隐患在于多个注入实例比如同时 hook 两个进程会竞争写同一个output.ob。解决方案不是加锁进程间锁复杂且易死锁而是用FILE_SHARE_WRITE标志打开文件并接受「数据交错」——因为.ob是纯追加流即使两条记录头尾粘连只要解析器按「12字节头 → 读len → 读len字节data」的规则循环解析依然能 100% 还原。我在某公司做 SDK 审计时就靠这个特性同时 hook 主进程和插件进程最后用 Python 脚本合并分析双流时序。3. 编译与运行从零开始跑通的六步闭环别被“DLL 注入”吓住这套流程我已在 5 台不同配置的 Windows 机器Win10 1909 / 21H1 / Win11 22H2上实测通过。全程无需管理员权限除非目标进程是系统级服务也不需要 Visual Studio 安装完整 IDE —— 用 VS Build Tools 就够。3.1 环境准备VS Build Tools Windows SDK 10.0.20348.0这不是必须最新版但必须满足两个条件支持/MT静态链接 CRT避免目标进程无对应 vcrt.dll 报错包含windows.h和winsock2.h的完整定义尤其是WSAGetLastError的 TLS 实现我推荐安装 Visual Studio Build Tools 2022 工作负载「C build tools」和「Windows 10/11 SDK」。安装后命令行输入# 验证 cl.exe 是否可用 C:\Program Files\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat cl /?如果输出编译器版本说明环境就绪。3.2 编译 ws2_hook.dll/MT /LD /EXPORT:DllMainDLL 必须静态链接 CRT/MT否则注入到无 VC 运行库的进程如精简版游戏客户端会直接弹窗报错。项目使用 C17 标准但没用任何 STL 容器避免std::string构造析构引发 TLS 问题全部用wchar_t[]和memcpy。编译命令在ws2_hook目录下cl /c /nologo /W3 /O2 /MT /D_UNICODE /DUNICODE ws2_hook.cpp link /nologo /DLL /OUT:ws2_hook.dll /MACHINE:X64 ws2_hook.obj kernel32.lib ws2_32.lib注意/MACHINE:X64必须与目标进程架构一致。若 hook 32 位进程如旧版 QQ需改用/MACHINE:X86并用vcvarsx86.bat。3.3 编译 injector.exe/SUBSYSTEM:CONSOLE /ENTRY:wmain注入器是控制台程序主函数必须是wmain宽字符入口以正确处理 DLL 路径中的 Unicode 字符。cl /nologo /W3 /O2 /MT /D_UNICODE /DUNICODE injector.cpp /link kernel32.lib user32.lib编译后得到injector.exe它只有一个功能传入目标进程 PID把ws2_hook.dll注入进去。3.4 启动目标进程并获取 PID找一个已知会调用send的程序。最简单的验证目标是curl或telnet# 启动一个监听端口的简易服务Python 自带 python -m http.server 8080 # 在另一终端用 curl 发送请求触发 send curl http://localhost:8080/test此时用任务管理器或tasklist | findstr python查到 python.exe 的 PID比如12345。3.5 执行注入并验证 DLL 加载运行注入器injector.exe 12345成功时输出Injected successfully。验证是否生效打开 Process ExplorerSysinternals 工具找到 PID 12345 的进程 → Properties → Threads → 查看 DLL 列表确认ws2_hook.dll已加载或用cmd执行tasklist /m ws2_hook.dll应看到该 DLL 被哪个 PID 使用提示如果注入失败大概率是权限问题。右键injector.exe→ 「以管理员身份运行」再试。普通用户无法注入系统进程如 svchost但可以注入自己启动的进程。3.6 触发 send 并检查 output.ob再次执行curl http://localhost:8080/test然后检查当前目录下是否生成output.ob。用十六进制编辑器如 HxD打开应看到类似结构00000000: 5E 2A 00 00 00 00 00 00 28 00 00 00 47 45 54 20 ^*......(…GET 00000010: 2F 74 65 73 74 20 48 54 54 50 2F 31 2E 31 0D 0A /test HTTP/1.1..前 8 字节5E 2A 00 00 00 00 00 00是GetTickCount64小端序转十进制约 10702 毫秒接着28 00 00 00是0x28 40字节即后续 40 字节是 HTTP 请求头内容正是GET /test HTTP/1.1\r\n...。数据真实、无篡改、可复现。4. 避坑指南五个血泪经验总结出的高频翻车点这个项目看似只有几百行代码但我在三个不同客户现场部署时平均每个环境都要花 2 小时排障。以下是反复踩过的坑按「现象 → 原因 → 解决」整理全是真机实测。4.1 现象注入成功但 output.ob 始终为空原因目标进程未调用send或调用的是WSASend重叠 I/O而非send。ws2_hook.dll只 Hooksend不处理WSASend、sendto、TransmitFile等其他发送 API。解决先用 Process Monitor 监控目标进程的ws2_32.dll!send调用。若看不到改用 API Monitor 工具筛选ws2_32.dll下所有发送类函数确认它实际调用的是哪个。若确实是WSASend需扩展 Hook 列表见最后一章。4.2 现象目标进程崩溃报错0xC0000005: Access violation原因my_send中WriteFile调用时output.ob被其他程序独占打开如用记事本打开了它导致CreateFileW返回INVALID_HANDLE_VALUE后续WriteFile对无效句柄操作触发异常。解决绝不在调试时用记事本/Notepad 打开output.ob。改用more output.ob或xxd output.ob查看。生产环境建议把文件名改为带 PID 的output_pid.ob避免冲突。4.3 现象output.ob 中数据错乱出现大量00字节或长度字段为负数原因len参数被解释为有符号整数int但WriteFile写入的是 4 字节无符号值。若len为负如-1表示错误小端序写入后解析为0xFFFFFFFF 4294967295导致后续读取越界。解决在my_send落盘前强制类型转换DWORD safe_len (ret 0) ? (DWORD)ret : 0;永远只写非负长度。4.4 现象注入后目标进程网络功能正常但curl返回超时原因my_send中WSASetLastError(lastError)位置错误。若放在WriteFile之后而WriteFile失败会覆盖WSAGetLastError导致上层收到错误码ERROR_INVALID_HANDLE而非真实的WSAEWOULDBLOCK。解决严格按代码顺序lastError WSAGetLastError()→real_send()→newError WSAGetLastError()→WSASetLastError(lastError)→WriteFile。错误码保存和还原必须紧贴real_send调用。4.5 现象在 Win11 上注入失败CreateRemoteThread返回NULL原因Win11 默认启用「内存完整性」Core Isolation阻止远程线程创建。这不是权限问题而是 Hypervisor 强制策略。解决临时关闭内存完整性设置 → 隐私和安全性 → Windows 安全中心 → 设备安全性 → 内存完整性 → 关闭重启生效。注意这是调试阶段的临时方案。生产环境应改用QueueUserAPC注入需目标进程有消息循环或申请微软认证驱动签名绕过。5. 二次开发从单函数 Hook 到全栈网络监控的三步扩展这个项目的价值不在于它现在能做什么而在于它提供了一个可无限生长的 Hook 基座。我把它用在三个真实项目中某金融客户端的加密协议逆向、某工业网关的 Modbus TCP 流量审计、某 AR 设备 SDK 的心跳包分析。每一次扩展我都遵循同一套原则不破坏原有链路、不增加上层感知、数据格式向后兼容。下面是我最常做的三项改造附可直接抄的代码和验证方法。5.1 扩展 Hook 列表支持 sendto、WSASend、TransmitFile单 Hooksend只能捕获阻塞式同步发送。现代高性能网络库如 libuv、Boost.Asio大量使用WSASend重叠 I/O和sendtoUDP。扩展只需三步在ws2_hook.cpp中声明新函数指针类型在DllMain的 IAT 遍历中增加对sendto、WSASend、TransmitFile的查找和替换为每个函数编写对应的my_xxx复用相同的落盘逻辑。WSASend的钩子函数示例注意其参数更复杂typedef int (WINAPI *pfn_WSASend)(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); pfn_WSASend real_WSASend nullptr; int WINAPI my_WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) { // 1. 提取第一个缓冲区的数据实际发送内容通常在 lpBuffers[0] if (dwBufferCount 0 lpBuffers ! nullptr lpBuffers[0].len 0 lpBuffers[0].buf ! nullptr) { LARGE_INTEGER tick; GetTickCount64(tick); DWORD len lpBuffers[0].len; HANDLE hFile CreateFileW(Loutput.ob, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile ! INVALID_HANDLE_VALUE) { SetFilePointer(hFile, 0, NULL, FILE_END); DWORD written; WriteFile(hFile, tick, sizeof(tick), written, NULL); WriteFile(hFile, len, sizeof(len), written, NULL); WriteFile(hFile, lpBuffers[0].buf, len, written, NULL); CloseHandle(hFile); } } // 2. 调用真实函数注意WSASend 是异步返回值不表示完成 int ret real_WSASend(s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpOverlapped, lpCompletionRoutine); return ret; }验证方法用netcat启动 UDP 服务nc -u -l 9999再用echo hello | nc -u localhost 9999发送检查output.ob是否新增记录。注意WSASend的lpNumberOfBytesSent是输出参数需在real_WSASend返回后读取但落盘数据应以lpBuffers[0].buf为准。5.2 增加过滤规则按 socket fd、IP、端口动态启停记录无差别记录所有send会产生海量噪音。我在某网关项目中加入了白名单机制只记录发往特定 IP:Port 的流量。实现方式是在my_send开头添加 socket 信息查询// 查询 socket 的远端地址 SOCKADDR_IN remoteAddr; int addrLen sizeof(remoteAddr); if (getpeername(s, (SOCKADDR*)remoteAddr, addrLen) 0) { DWORD targetIP inet_addr(192.168.1.100); // 白名单 IP USHORT targetPort htons(8080); // 白名单端口 if (remoteAddr.sin_addr.s_addr targetIP remoteAddr.sin_port targetPort) { // 符合条件执行落盘 write_to_ob(...); } }提示getpeername对 UDP socket 会失败UDP 无连接此时可改用sendto的to参数或直接记录所有 UDP因其本身无状态。5.3 输出格式升级支持 JSON 元数据 二进制 payload 分离.ob文件难读不利于自动化分析。我在某金融项目中将其升级为双文件模式output.jsonl每行一个 JSON 对象含时间、socket、长度、hex摘要 output.bin纯二进制 payload按顺序追加。这样 Python 脚本可快速 grep JSON 行再用偏移量定位output.bin中的原始数据。JSONL 示例{ts:1712345678901,sock:1234,len:42,proto:TCP,dst:10.0.0.5:443,sha256:a1b2c3...}实现只需在my_send中用sprintf_s生成 JSON 字符串WriteFile写入output.jsonl追加WriteFile写入output.bin追加无头优势JSONL 可被jq、pandas.read_json(linesTrue)直接处理output.bin用dd skipxxx bs1 countyyy可精确提取任意一条 payload完美适配 CI/CD 中的自动化协议测试。从那以后我每次做网络层分析都强制走一遍「先注入、再触发、最后用 Python 解析 .ob」的三步闭环。不是为了炫技而是因为——当你说不清一个黑盒程序到底发了什么时最可靠的证据永远是它亲手写进磁盘的那串字节。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑