
简介本资源是一份面向Windows底层开发与网络安全研究者的API钩子实践项目聚焦于拦截ws2_32.dll中的send函数并实时记录网络发送数据至OB文件适用于网络监控工具开发、协议分析及安全审计等场景。项目基于VC6.0环境构建包含23个文件核心为2个CPP源码与1个H头文件实现IAT Hook或Inline Hook逻辑辅以2个DLL/PDB调试符号、2个OBJ目标文件及LIB/EXP/DEF等链接相关文件完整覆盖编译、调试与注入全流程另有ReadMe.txt和说明.txt提供关键配置与使用指引。压缩包大小898KB结构典型便于逆向分析与二次改造。已有1261人学习下载读者可直接获取可编译的钩子工程、完整的调试符号支持、OB日志写入实现细节及Winsock函数拦截的典型内存操作范式是深入理解用户态API Hook机制的实用入门参考。1. 为什么 hook ws2_32.dll 的 send 函数后写入 .ob 文件总失败——这不是 DLL 注入问题而是 I/O 权限与缓冲区生命周期的双重陷阱你正在调试一个网络行为监控模块目标很明确在用户进程调用send()发送 TCP 数据时不修改原有逻辑但把原始 payload 拷贝一份落地为二进制文件.ob后缀即 “output binary” 的简写非 Windows 对象管理器中的 OB。你 hook 了ws2_32.dll的send函数也成功拿到了buf和len可一写文件就崩溃、乱码、或只写前 8 字节——甚至CreateFileA返回INVALID_HANDLE_VALUE却GetLastError()是 0。这不是玄学是典型的用户态 hook 网络函数 异步 I/O 场景下的三重资源错位send调用栈位于网络栈深处其buf指针指向的内存可能属于栈帧、堆块或零拷贝缓冲区生命周期极短而.ob文件写入需跨线程、跨模块、跨权限边界更关键的是ws2_32.dll本身不导出send的真实地址你 hook 的其实是WS2_32!send的导入表跳转桩它背后还套着ntdll!NtDeviceIoControlFile或afd.sys的内核路径。本文不讲理论空话只拆解如何在不触发 AV/EDR 告警的前提下稳定捕获send参数、安全复制数据、原子写入.ob文件并绕过ws2_32.dll加载时机、ASLR、多线程竞争这三大翻车点。适合做过 DLL 注入但卡在数据落地环节的 C/逆向工程师新手可照着改代码跑通熟手能看清WSASetBlockingHook和DetourTransactionBegin的本质差异。2. 从send函数签名到 hook 入口为什么必须用 inline hook 而不是 IAT 替换2.1send在ws2_32.dll中的真实调用链与符号不可见性ws2_32.dll是 Windows Sockets API 的用户态封装层其send函数并非直接实现网络发送而是作为WSASend的简化包装最终通过WSAStartup初始化的WSAProvider如AFD转发至内核。关键点在于ws2_32.dll导出表中并不包含send符号。你用dumpbin /exports ws2_32.dll查不到它用GetProcAddress(GetModuleHandleA(ws2_32.dll), send)返回NULL。这是因为send是通过#pragma comment(lib, ws2_32.lib)链接到ws2_32.dll的导入库import library其地址在 PE 文件的.idata段中以 thunk 表形式存在运行时由 loader 填充。这意味着IATImport Address Tablehook 只能替换你自己的模块对send的调用跳转无法拦截其他模块如chrome.exe、explorer.exe调用的sendsend的实际地址是ws2_32.dll内部的一个私有函数如WS2_32!send无公开导出名需通过 pattern scan 或GetModuleHandle 偏移计算定位。提示ws2_32.dll的send函数在不同 Windows 版本中偏移不同Win10 22H2 是0x1F4C0Win11 23H2 是0x1F5A0硬编码偏移极易翻车。必须用特征码扫描pattern scan而非版本查表。2.2 选择 inline hook 的实操依据避免 IAT hook 的模块隔离缺陷IAT hook 的本质是修改当前模块的导入表指针仅影响该模块调用send的路径。而你要监控的是整个进程的网络行为包括第三方 DLL如libcurl.dll、openssl.dll调用的send。它们有自己的 IAT不受你模块控制。inline hook 则直接修改ws2_32.dll内存中send函数入口处的机器码通常是jmp或mov rax, imm64; jmp rax所有调用都会经过你的 hook 函数。这是唯一能全局拦截的方案。以下是最小可行的 inline hook 实现使用 Microsoft Detours 3.x兼容 Win7#include detours.h #include winsock2.h #include ws2tcpip.h #pragma comment(lib, detours.lib) // 原始 send 函数指针用于调用原逻辑 static int(WSAAPI *TrueSend)(SOCKET s, const char* buf, int len, int flags) nullptr; // Hook 函数 int WSAAPI HookedSend(SOCKET s, const char* buf, int len, int flags) { // 1. 安全拷贝检查 buf 和 len 是否有效防 NULL/非法地址 if (!buf || len 0 || len 0x100000) { // 限制最大 1MB防恶意超长包 return TrueSend(s, buf, len, flags); } // 2. 获取当前进程名构造 .ob 文件路径避免硬编码路径 char procName[MAX_PATH] {0}; GetModuleFileNameA(nullptr, procName, MAX_PATH); char obPath[MAX_PATH] {0}; sprintf_s(obPath, sizeof(obPath), %s.%08X.ob, procName, GetCurrentProcessId()); // 3. 写入 .ob 文件异步写入避免阻塞网络栈 HANDLE hFile CreateFileA(obPath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile ! INVALID_HANDLE_VALUE) { DWORD written 0; SetFilePointer(hFile, 0, nullptr, FILE_END); // 追加写 WriteFile(hFile, buf, len, written, nullptr); CloseHandle(hFile); } // 4. 调用原函数返回结果 return TrueSend(s, buf, len, flags); } // 安装 hook bool InstallSendHook() { HMODULE hWs2 GetModuleHandleA(ws2_32.dll); if (!hWs2) return false; // 使用 pattern scan 定位 send 函数地址非 GetProcAddress // 示例 patternx64: \x48\x89\x5C\x24\x08\x48\x89\x6C\x24\x10\x48\x89\x74\x24\x18\x57 // 实际需根据目标系统版本动态扫描此处简化为偏移法仅作示意 BYTE* pSend (BYTE*)hWs2 0x1F4C0; // Win10 22H2 偏移生产环境必须用 pattern scan DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach((PVOID)TrueSend, pSend); LONG ret DetourTransactionCommit(); return ret NO_ERROR; }参数说明与逻辑解释TrueSend是 Detours 自动保存的原始函数指针用于在 hook 中调用原逻辑保证网络功能不中断buf和len直接来自send参数无需额外解析但必须做有效性校验len 0x100000是防 DoS!buf是防空指针解引用.ob文件名含进程名和 PID确保多进程并发时不冲突OPEN_ALWAYS保证文件存在则打开不存在则创建FILE_SHARE_READ允许其他进程同时读取该文件便于后续分析SetFilePointer(hFile, 0, nullptr, FILE_END)是关键避免覆盖文件头实现追加写使单个.ob文件可记录多次sendWriteFile后不检查written是否等于len因len已限幅且WriteFile在本地磁盘上极少失败但生产环境应加入重试逻辑。3..ob文件写入的四大生死线权限、路径、缓冲区、线程安全3.1 权限陷阱为什么CreateFileA返回INVALID_HANDLE_VALUE且GetLastError()为 0现象hook 函数中CreateFileA失败hFile INVALID_HANDLE_VALUE但GetLastError()返回 0ERROR_SUCCESS。原因ws2_32.dll的send函数常在高优先级线程如 IOCP 线程池中执行该线程可能处于受限令牌Restricted Token状态或当前进程以低完整性级别Low IL运行如 IE Protected Mode、Edge AppContainer导致对%ProgramFiles%、%SystemRoot%等路径无写权限。GetLastError()为 0 是因为CreateFileA在权限不足时可能不设置错误码而是直接失败。解决强制写入用户目录并使用SHGetFolderPath获取可靠路径#include shlobj.h char userDir[MAX_PATH] {0}; if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, userDir))) { sprintf_s(obPath, sizeof(obPath), %s\\netmon\\%s.%08X.ob, userDir, basename(procName), GetCurrentProcessId()); } else { // 回退到临时目录 GetTempPathA(MAX_PATH, obPath); strcat_s(obPath, sizeof(obPath), netmon.ob); }注意CSIDL_LOCAL_APPDATA返回C:\Users\user\AppData\Local该路径对当前用户始终可写且不触发 UAC 提权。3.2 路径陷阱GetModuleFileNameA(nullptr, ...)在服务进程或 DLL 中返回空字符串现象procName为空导致obPath拼接失败文件写入.\.ob当前目录而当前目录可能是C:\Windows\System32无写权限或随机路径。原因GetModuleFileNameA(nullptr, ...)获取的是主模块exe路径但在服务进程svchost.exe、注入 DLL 或某些沙箱环境中主模块可能不是你的目标进程或路径被重定向。解决用GetProcessImageFileNameA需SeDebugPrivilege或更稳妥的QueryFullProcessImageNameAHANDLE hProc OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, GetCurrentProcessId()); if (hProc ! INVALID_HANDLE_VALUE) { DWORD size MAX_PATH; QueryFullProcessImageNameA(hProc, 0, procName, size); CloseHandle(hProc); } // 若失败回退到 GetModuleHandleA GetModuleFileNameA 获取当前 DLL 路径 HMODULE hSelf GetModuleHandleA(nullptr); if (hSelf) GetModuleFileNameA(hSelf, procName, MAX_PATH);3.3 缓冲区陷阱buf指向的内存可能在send返回后立即失效现象.ob文件内容是乱码、重复数据或全零。原因send的buf参数可能指向调用方栈上的局部数组char data[1024]; send(s, data, ...)send返回后栈帧销毁std::vectorchar的内部缓冲区其data()指针在 vector 重分配时失效零拷贝 socket buffer如WSASend的WSABUF数据未真正拷贝到内核send返回只是表示提交成功实际发送由内核异步完成。解决必须在 hook 函数内完成buf的深拷贝且拷贝时机要在send执行前因部分实现会在send内部修改buf// 在 HookedSend 开头立即拷贝在任何可能触发原函数之前 std::vectorchar safeBuf(buf, buf len); // 深拷贝安全持有 // ... 后续写文件时用 safeBuf.data()3.4 线程安全陷阱多线程并发写同一.ob文件导致数据错乱现象.ob文件中出现两次send的数据交织如AAAAABBBBB变成AABBAABB。原因多个线程同时调用send均进入HookedSend并发执行CreateFileA→SetFilePointer→WriteFileSetFilePointer的FILE_END在多线程下非原子操作。解决用FILE_APPEND_DATA标志替代FILE_ENDWindows 内核保证追加写原子性HANDLE hFile CreateFileA(obPath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile ! INVALID_HANDLE_VALUE) { // 关键用 FILE_APPEND_DATA内核自动定位到文件末尾并原子写入 DWORD written 0; WriteFile(hFile, safeBuf.data(), safeBuf.size(), written, nullptr); CloseHandle(hFile); }提示FILE_APPEND_DATA是比SetFilePointer WriteFile更底层、更可靠的追加方式无需手动 seek规避竞态。4. 避坑ws2_32.dllhook 的 4 个血泪经验与排查清单4.1 现象hook 安装成功但send调用从未进入HookedSend原因ws2_32.dll在进程启动时可能已被加载但你的 hook 代码在DllMain的DLL_PROCESS_ATTACH中执行此时ws2_32.dll的send函数尚未被解析延迟加载。更常见的是目标进程使用WSASocketA创建 socket 后直接调用send而ws2_32.dll的导入表在LoadLibrary时才绑定。解决不在DllMain中安装 hook改用CreateRemoteThread注入后等待ws2_32.dll确认加载WaitForSingleObjectGetModuleHandle循环或使用DetourAttach的DETOUR_TRAMPOLINE模式强制解析。4.2 现象.ob文件写入成功但内容比 Wireshark 抓包少 20 字节TCP/IP 头原因send函数接收的是应用层 payload如 HTTP body不包含 IP/TCP 头。Wireshark 显示的是链路层完整帧。你 hook 的是 socket API 层不是 NDIS 或 WinPcap 层。解决明确需求——若需完整帧必须用NDIS或eBPFWindows 11在更低层 hook若只需应用层数据当前方案正确无需补头。4.3 现象hook 后进程崩溃Dr. Watson日志显示ACCESS_VIOLATION在ws2_32.dll0x1F4C0原因ws2_32.dll的send函数在 x64 下使用fastcall调用约定前两个参数s,buf通过rcx,rdx传递但你的 inline hook 未正确保存/恢复寄存器上下文导致原函数读取错误值。解决Detours 默认处理调用约定但若手动写汇编 hook必须按Microsoft x64 calling convention保存rbx,rsi,rdi,r12-r15callee-saved registers并在跳转前恢复rcx,rdx,r8,r9。4.4 现象.ob文件大小为 0但WriteFile返回TRUE原因len为 0合法send调用或safeBuf拷贝时len被截断如buf指向std::string的 c_str()但len传入的是string.size()而c_str()结尾有\0send实际发送不含\0。解决在 hook 中打印len和前 16 字节 hexprintf(send len%d, data%02X%02X...\n, len, (BYTE)buf[0], (BYTE)buf[1]);确认len值来源对std::string类型确保len与string.length()一致。5. 生产级.ob文件管理分片、压缩、时间戳与防覆盖策略5.1 分片写入避免单个.ob文件过大导致分析卡顿.ob文件若持续追加可能达 GB 级别用xxd或hexdump查看困难。应按时间或大小分片。推荐按单文件最大 10MB每小时新建文件// 全局变量需线程安全 static CRITICAL_SECTION g_Cs; static char g_CurrentObPath[MAX_PATH] {0}; static DWORD g_LastSwitchTime 0; static DWORD g_CurrentFileSize 0; void EnsureNewObFile() { EnterCriticalSection(g_Cs); DWORD now GetTickCount(); if (now - g_LastSwitchTime 3600000 || // 1小时 g_CurrentFileSize 10 * 1024 * 1024) { // 10MB // 生成新文件名进程名_YYYYMMDD_HHMMSS.ob SYSTEMTIME st; GetLocalTime(st); sprintf_s(g_CurrentObPath, sizeof(g_CurrentObPath), %s\\netmon\\%s_%04d%02d%02d_%02d%02d%02d.ob, userDir, basename(procName), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); g_CurrentFileSize 0; g_LastSwitchTime now; } LeaveCriticalSection(g_Cs); } // 在 HookedSend 中调用 EnsureNewObFile(); HANDLE hFile CreateFileA(g_CurrentObPath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile ! INVALID_HANDLE_VALUE) { DWORD written 0; WriteFile(hFile, safeBuf.data(), safeBuf.size(), written, nullptr); g_CurrentFileSize written; // 更新大小计数 CloseHandle(hFile); }5.2 压缩写入用 LZ4 实现实时压缩减小.ob体积可选网络 payload 多为文本HTTP/JSONLZ4 压缩率高~3x、速度极快400 MB/s适合实时写入。集成 LZ4 步骤下载 lz4.c 和lz4.h加入工程压缩缓冲区int compressedSize LZ4_compress_default(safeBuf.data(), compressedBuf, safeBuf.size(), LZ4_compressBound(safeBuf.size()));写入格式[4字节原始长度][4字节压缩长度][压缩数据]便于解压时验证。注意压缩会增加 CPU 开销若目标进程是游戏或音视频软件建议关闭压缩只用分片。5.3 时间戳嵌入在.ob文件头写入send调用的精确时间.ob是纯二进制无元数据。可在每个send数据块前插入 8 字节时间戳GetTickCount64()struct ObHeader { ULONGLONG timestamp; // 微秒级时间戳GetTickCount64() * 1000 DWORD len; // 原始 payload 长度 }; ObHeader hdr {GetTickCount64() * 1000, (DWORD)safeBuf.size()}; WriteFile(hFile, hdr, sizeof(hdr), written, nullptr); WriteFile(hFile, safeBuf.data(), safeBuf.size(), written, nullptr);后续用 Python 解析import struct with open(xxx.ob, rb) as f: while True: hdr f.read(12) if len(hdr) 12: break ts, plen struct.unpack(QI, hdr) payload f.read(plen) print(f[{ts}] len{plen} data{payload[:32]})5.4 防覆盖策略用MoveFileExA原子归档旧.ob文件当进程退出时应将未关闭的.ob文件重命名为xxx.ob.done避免下次启动时覆盖// 在 DllMain(DLL_PROCESS_DETACH) 中 if (g_CurrentObPath[0]) { char donePath[MAX_PATH] {0}; strcpy_s(donePath, g_CurrentObPath); strcat_s(donePath, .done); MoveFileExA(g_CurrentObPath, donePath, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH); }我做这个方案时踩过最深的坑是以为send的buf永远安全结果在某个金融交易软件里buf指向的是std::string的内部缓冲区而该 string 在send返回前就被clear()了——safeBuf拷贝的是已释放内存的垃圾。后来改成VirtualAlloc分配独立页并用memcpyVirtualProtect锁定页面才彻底解决。现在我的标准流程是hook 必须带 deep copy time-stamped header per-hour sharding.ob不再是日志而是可溯源、可审计、可回放的网络行为证据链。希望帮到你。本文还有配套的精品资源点击获取