news 2026/10/15 14:21:44

Windows下拦截ws2_32.dll的send函数实现数据落盘与协议分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows下拦截ws2_32.dll的send函数实现数据落盘与协议分析

简介:这份资源聚焦Windows网络编程中的API拦截技术,面向具备一定C++与Winsock基础、希望深入理解系统级钩子机制的中高级开发者。其核心目标是通过拦截ws2_32.dll中的send()函数,将套接字发送的数据实时写入OB文件,可用于网络监控、安全审计或性能分析等场景的学习与实验。压缩包共23个文件,约898KB,包含cpp与h源码、dll与lib编译产物、def导出定义、dsp与dsw工程文件,以及pdb、obj、ilk等调试中间文件,完整保留了VC6工程结构与构建痕迹,便于直接编译调试或二次改造。资源已有1262人学习下载,读者可从中掌握IAT替换或用户模式钩子的实现思路,理解send()函数原型与调用流程,并借鉴数据捕获、文件落盘及原始逻辑恢复的代码组织方式,同时留意安全性、性能开销与合规性等实践要点。

1. 从一次抓包翻车说起:ws2_32.dll 拦截 send 到底能干什么

有次排查一个老系统的通信问题,抓包工具显示数据发出去了,但服务端就是收不到完整内容。后来用 API 拦截的方式在send调用点直接落盘,才发现是上层拼包时长度字段算错了——数据在进入协议栈之前就已经缺了一截。这件事让我意识到,网络抓包工具看到的是“线上跑的车”,而拦截ws2_32.dll的导出函数看到的是“装车前后的货”,两者定位问题的层次完全不同。

ws2_32.dll是 Windows 上 Winsock 2 的用户态实现库,几乎所有走 TCP/UDP 的程序最终都会调用它导出的send、recv、WSASend、WSARecv等函数。所谓“拦截 send 函数并把发送内容写到 ob 文件里”,本质是在目标进程内对ws2_32.dll的send入口做一次跳转改写,让程序调用send时先进入我们自己的处理函数,把缓冲区内容按自定义格式(这里用.ob后缀)追加落盘,再决定是否放行到原始send。它解决的是“应用层实际交出了什么数据”这个黑匣子问题,适合做协议逆向、私有格式分析、通信行为审计的从业者,也适合想理解 API Hook 落地细节的开发者。

2. 拦截原理与选型:IAT Hook、Inline Hook 该用哪个

2.1 为什么优先考虑 IAT Hook

拦截ws2_32.dll的send,常见做法有两类:IAT Hook 和 Inline Hook。IAT Hook 改的是目标模块导入表里send那一项的地址,把它指向自己的函数;Inline Hook 改的是send函数体开头的机器码,插入跳转指令。两者差别很实际。

IAT Hook 的优点是稳定、可逆、不碰代码段,目标函数升级或重定位时影响小;缺点是只能拦截通过导入表调用的场景,如果程序是运行时GetProcAddress动态取地址再调用,IAT 里根本没有这一项,就拦不到。Inline Hook 覆盖面更广,动态调用也能拦,但需要处理指令长度、热补丁、多线程竞争,稍不注意就把进程搞崩,属于血泪经验高发区。

我一般会先判断目标程序怎么用send:用调试器看调用点是不是call dword ptr [<IAT 项>]。如果是,IAT Hook 足够;如果发现是寄存器间接调用,再上 Inline Hook。本文以 IAT Hook 为主线,因为它可复现性最好,新手照着做不容易翻车。

2.2 拦截点的选择:send 还是 WSASend

很多程序并不直接调send,而是走WSASend,尤其是用了重叠 I/O 或完成端口的服务端。只拦send会漏数据。稳妥做法是同时挂send和WSASend,WSASend的缓冲区是WSABUF数组,需要按dwBufferCount逐个遍历,不能只取第一个。

另外要注意send的返回值语义:返回SOCKET_ERROR时缓冲区内容未必全部发出,但“应用层试图发送什么”这个信息仍然有价值,所以落盘应该在调用原始send之前做,而不是之后。这一点决定了后面代码里写文件的时机。

2.3 落盘格式为什么用 .ob 而不是直接写文本

.ob在这里是一个自定义二进制容器后缀,不是某种标准格式。用它的原因是发送内容可能是二进制协议,直接当文本写会遇到\0截断、编码错乱。常见做法是每条记录写成“长度 + 时间戳 + socket 句柄 + 原始字节”的定长头加变长体,这样后续用脚本解析不会丢边界。下面给出一个最小可用的记录结构。

字段类型长度说明
magicuint324固定 0x4F424731,用于校验
timestampuint648毫秒级时间戳
socketuint648SOCKET 句柄值
directionuint810 表示 send,1 表示 recv
dataLenuint324后续原始数据长度
databytes变长原始发送/接收字节

3. 动手实现:从 DLL 工程到 send 落盘的完整步骤

3.1 工程骨架与导出函数

先建一个 Win32 DLL 工程,导出两个函数供注入器调用:InstallHook和UninstallHook。核心思路是在InstallHook里定位ws2_32.dll的导入表项并替换地址。下面是最小实现,省略了错误分支的日志细节,实际工程里建议每一步都判断返回值。

// hook.cpp : 导出安装/卸载接口 #include <windows.h> #include <cstdio> typedef int (WSAAPI *send_t)(SOCKET, const char*, int, int); static send_t g_originalSend = nullptr; static PVOID* g_iatSlot = nullptr; static FILE* g_obFile = nullptr; // 写一条 .ob 记录:定长头 + 原始数据 static void WriteObRecord(SOCKET s, const char* buf, int len, unsigned char dir) { if (!g_obFile || len <= 0) return; unsigned int magic = 0x4F424731; unsigned long long ts = GetTickCount64(); unsigned long long sk = (unsigned long long)s; unsigned int dlen = (unsigned int)len; fwrite(&magic, 4, 1, g_obFile); fwrite(&ts, 8, 1, g_obFile); fwrite(&sk, 8, 1, g_obFile); fwrite(&dir, 1, 1, g_obFile); fwrite(&dlen, 4, 1, g_obFile); fwrite(buf, 1, dlen, g_obFile); fflush(g_obFile); // 崩溃时尽量少丢数据 } // 替换后的 send:先落盘,再调用原始函数 static int WSAAPI HookedSend(SOCKET s, const char* buf, int len, int flags) { WriteObRecord(s, buf, len, 0); return g_originalSend(s, buf, len, flags); }

逻辑说明:WriteObRecord按 3.3 节的表结构顺序写入,fflush是为了在目标进程异常退出时尽量保留已捕获数据,代价是性能下降,高频发送场景可以改成缓冲批量刷。HookedSend先落盘再转发,保证即使原始send失败,我们也能看到应用层试图发什么。参数s是 socket 句柄,buf是发送缓冲区,len是长度,flags原样透传。

3.2 定位并改写 IAT 项

安装 Hook 的关键是找到当前模块导入表里send的槽位。用GetModuleHandle拿自身基址,解析 PE 头,遍历导入描述符,匹配ws2_32.dll后在其FirstThunk数组里按序号或名字找到send。

static bool PatchIAT(HMODULE mod, const char* dllName, const char* funcName, PVOID newFunc, PVOID* oldFunc) { auto dos = (PIMAGE_DOS_HEADER)mod; auto nt = (PIMAGE_NT_HEADERS)((BYTE*)mod + dos->e_lfanew); auto imp = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)mod + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); for (; imp->Name; ++imp) { const char* name = (const char*)((BYTE*)mod + imp->Name); if (_stricmp(name, dllName) != 0) continue; auto thunk = (PIMAGE_THUNK_DATA)((BYTE*)mod + imp->FirstThunk); auto orig = (PIMAGE_THUNK_DATA)((BYTE*)mod + imp->OriginalFirstThunk); for (; thunk->u1.Function; ++thunk, ++orig) { if (orig->u1.Ordinal & IMAGE_ORDINAL_FLAG) continue; // 跳过序号导入 auto ibn = (PIMAGE_IMPORT_BY_NAME)((BYTE*)mod + orig->u1.AddressOfData); if (strcmp((const char*)ibn->Name, funcName) != 0) continue; DWORD oldProtect; VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect); *oldFunc = (PVOID)thunk->u1.Function; thunk->u1.Function = (ULONG_PTR)newFunc; VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect); return true; } } return false; }

逻辑说明:OriginalFirstThunk保存名字信息,FirstThunk保存实际地址,两者同步遍历。改写前必须VirtualProtect把该页改成可写,否则会触发访问违例。oldFunc回传原始地址,供HookedSend转发使用。参数mod传GetModuleHandle(nullptr)即可,dllName传"ws2_32.dll",funcName传"send"。

3.3 安装、卸载与文件打开

把上面两块拼起来,InstallHook负责打开.ob文件并打补丁,UninstallHook负责还原并关闭文件。还原时把保存的原始地址写回 IAT 槽位即可。

extern "C" __declspec(dllexport) BOOL InstallHook() { if (!g_obFile) { fopen_s(&g_obFile, "capture.ob", "ab"); // 追加模式,避免覆盖历史 if (!g_obFile) return FALSE; } HMODULE self = GetModuleHandle(nullptr); return PatchIAT(self, "ws2_32.dll", "send", (PVOID)HookedSend, (PVOID*)&g_originalSend); } extern "C" __declspec(dllexport) void UninstallHook() { if (g_iatSlot && g_originalSend) { DWORD oldProtect; VirtualProtect(g_iatSlot, sizeof(PVOID), PAGE_READWRITE, &oldProtect); *g_iatSlot = (PVOID)g_originalSend; VirtualProtect(g_iatSlot, sizeof(PVOID), oldProtect, &oldProtect); } if (g_obFile) { fclose(g_obFile); g_obFile = nullptr; } }

逻辑说明:fopen_s用"ab"追加模式,多次运行不会互相覆盖,方便对比。g_iatSlot需要在PatchIAT成功时记录槽位地址,上面示例为简洁未展开,实际工程里应在PatchIAT内把&thunk->u1.Function存到全局。卸载顺序是先还原 IAT 再关文件,避免还原瞬间还有线程进入HookedSend写已关闭的文件句柄。

3.4 注入与验证

DLL 编译好后需要注入目标进程。常见做法是用CreateRemoteThread配合LoadLibrary,或者用调试器附加后手动LoadLibrary。注入后调用InstallHook,然后触发目标程序发数据,观察capture.ob是否增长。

验证时不要只看文件大小,建议写个解析脚本按 3.3 节的表结构读回来,确认 magic、长度、数据能对上。下面是一个 Python 解析片段。

import struct with open("capture.ob", "rb") as f: while True: head = f.read(25) # 4+8+8+1+4 if len(head) < 25: break magic, ts, sk, direction, dlen = struct.unpack("<IQQBI", head) assert magic == 0x4F424731, "magic 不匹配,文件可能被截断" data = f.read(dlen) print(f"ts={ts} sock={sk} dir={direction} len={dlen} data={data[:32]!r}")

逻辑说明:struct格式串<IQQBI对应小端下的 uint32、uint64、uint64、uint8、uint32,与写入顺序严格一致。assert用来在解析错位时立刻暴露,而不是继续读出乱码。参数dlen来自文件头,必须校验它不超过剩余文件长度,否则恶意或损坏文件会导致内存问题。

4. 避坑与排查:拦截 send 最常见的五类翻车

4.1 注入后进程直接崩溃

现象:注入 DLL 并调用InstallHook后目标进程秒退。原因通常是 IAT 页没有改可写就写入,或者PatchIAT遍历时把序号导入项当成名字项解析,读到非法地址。解决:改写前一律VirtualProtect,遍历时先判断IMAGE_ORDINAL_FLAG,对序号导入直接跳过或单独处理。

4.2 文件里只有头没有数据

现象:capture.ob有记录头,但data为空或长度对不上。原因多半是send的len为 0,或者程序实际走的是WSASend,而只挂了send。解决:在WriteObRecord里对len <= 0直接返回,同时补挂WSASend,按WSABUF数组逐个写。

4.3 高频发送时目标程序卡顿

现象:拦截后程序吞吐明显下降。原因是每条记录都fflush,磁盘 I/O 成了瓶颈。解决:改成内存缓冲,累计到一定字节数或定时刷盘;对延迟敏感的场景可以只记录头部和长度,不记录完整数据。

4.4 多线程下记录交错

现象:解析时发现两条记录的数据粘在一起。原因是多个线程同时进入HookedSend,fwrite序列不是原子的。解决:在WriteObRecord里加临界区或互斥量,保证“头 + 数据”作为一个整体写入;或者每个线程写独立文件再合并。

4.5 卸载后程序行为异常

现象:调用UninstallHook后程序发送失败。原因是还原 IAT 时写回了错误的原始地址,或者文件已关闭但仍有线程在HookedSend里写。解决:PatchIAT时务必保存原始地址到全局,还原用保存值;卸载前先确保没有新线程进入,必要时用引用计数等待在途调用结束。

5. 进阶技巧:把 .ob 变成可检索的协议分析底稿

抓到.ob只是第一步,真正省时间的是把它变成能按 socket、按时间、按方向过滤的底稿。我一般会再写一个转换脚本,把二进制记录转成 JSON Lines,每行一条,方便用jq或日志工具检索。下面这个脚本在解析基础上加了方向过滤和十六进制预览。

import struct, json, sys def iter_records(path): with open(path, "rb") as f: while True: head = f.read(25) if len(head) < 25: break magic, ts, sk, direction, dlen = struct.unpack("<IQQBI", head) if magic != 0x4F424731: raise ValueError("记录头损坏,停止解析") data = f.read(dlen) if len(data) < dlen: raise ValueError("数据被截断") yield {"ts": ts, "sock": sk, "dir": direction, "len": dlen, "hex": data.hex(), "raw": data} if __name__ == "__main__": src, dst = sys.argv[1], sys.argv[2] with open(dst, "w", encoding="utf-8") as out: for r in iter_records(src): out.write(json.dumps(r, ensure_ascii=False) + "\n")

逻辑说明:iter_records用生成器逐条读,避免一次性把大文件读进内存。hex字段保留完整字节,raw在 JSON 里会被转义,实际用的时候可以只留hex。参数src是.ob路径,dst是输出的 JSONL 路径。转完之后用jq 'select(.dir==0) | .hex'就能只看发送方向,排查“到底发了什么”比翻二进制快得多。

还有一个容易被忽略的点:send的缓冲区在调用返回后可能被上层复用或释放,所以落盘必须在调用原始send之前完成,不能先转发再异步写。我早期图省事在另一个线程里写,结果抓到的数据偶尔被覆盖,排查了半天才定位到生命周期问题。从那以后我每次做 API 拦截,都强制把“拷贝数据”放在转发之前,哪怕多一次内存拷贝也不省这个步骤。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 14:21:18

CAM-chem MUSICA模型:城市大气污染跨尺度模拟实战

1. 项目背景与核心价值CAM-chem MUSICA&#xff08;Model for Urban Scale Interactions of Chemistry and Aerosols&#xff09;是NCAR开发的大气化学-气候耦合模型系统中的关键组件。这个工具让我在分析城市尺度大气污染物传输时&#xff0c;第一次真正实现了从全球环流到局地…

作者头像 李华
网站建设 2026/10/15 14:20:11

岳云鹏能扛起牛栏山“香精酒”标签下的救赎大旗吗?

“岳云鹏扛得起牛栏山的救赎大旗吗&#xff1f;”这个问法&#xff0c;看着像娱乐八卦&#xff0c;实际上是在问一个很务实的商业问题&#xff1a;一个品牌在被消费者贴上负面标签之后&#xff0c;还能不能靠代言人扳回来&#xff1f;我做白酒营销这行有些年头了&#xff0c;亲…

作者头像 李华
网站建设 2026/10/15 14:20:00

通达OA升级实操:版本识别、备份与部署避坑指南

简介&#xff1a;这份通达OA 2017&#xff08;版本号10.16.20180831&#xff09;破解补丁包&#xff0c;由作者实测验证后分享&#xff0c;适合需要临时体验OA完整功能的开发人员、测试人员或运维学习者。补丁包内共40个文件&#xff0c;以31个PHP脚本为主&#xff0c;配合6个e…

作者头像 李华
网站建设 2026/10/15 14:19:30

金融级Agent闭环:支付系统中的确定性决策架构

1. 这不是概念炒作&#xff0c;而是支付系统里真实跑起来的Agent闭环“金融支付、工程架构、伦理监督&#xff0c;Agent闭环三要素这么快就齐了&#xff01;”——看到这个标题&#xff0c;我第一反应不是兴奋&#xff0c;而是放下手里的咖啡杯&#xff0c;打开生产环境监控面板…

作者头像 李华
网站建设 2026/10/15 14:18:06

LPCLINK2+LPC4370调试链路三要素:固件/Host/Flash算法协同解析

简介&#xff1a;本资源是面向嵌入式开发工程师与高校电子类专业学生的LPC4370微控制器配套开发程序包&#xff0c;聚焦于其核心优势——80MHz采样率的12位6路独立ADC功能&#xff0c;性能达常规IC或通用ADC的200倍&#xff0c;适用于高速数据采集、实时信号处理等进阶应用场景…

作者头像 李华
网站建设 2026/10/15 14:17:43

Diffblue Cover社区版:AI自动生成JUnit单测,老项目覆盖率速升

简介&#xff1a;面向IntelliJ IDEA 2022.2用户的自动化单元测试生成工具&#xff0c;主要服务Java开发者与重视代码质量的技术团队。Diffblue Cover Community Edition通过分析代码结构与执行路径&#xff0c;自动产出符合JUnit或TestNG规范的测试类与方法&#xff0c;解决手动…

作者头像 李华