☰
C++实现send函数拦截:IAT Hook与DLL注入详解
2026/10/2 19:53:56 网站建设 项目流程

简介:围绕Windows平台网络通信底层实现,分享一套针对套接字发送接口的拦截改造方案。核心目标是截获应用程序通过动态链接库发出的网络数据,将其写入指定输出文件,同时保证原有发送流程不被破坏。这一思路适用于网络监控、接口调试、安全审计和教学演示等场景,要求阅读者熟悉C/C++以及进程地址空间、模块加载与函数调用机制,可作为中高级开发者的进阶实践项目。资源以RAR压缩包提供,共收录23个文件,整体仅898KB。压缩包内既包括C++源程序与头文件、工程配置文件和模块定义文件,也包括编译链接后生成的动态库、中间目标文件与调试符号文件,几乎完整保留了Windows环境下项目的构建痕迹。说明文本则对实现原理和使用流程进行了必要补充,即便首次接触钩子技术也能按图索骥。目前该资源已有1260人学习下载,具有一定参考价值。实际动手时,通过源码可以体会如何记录发送缓冲区中的数据、如何获取原始接口地址,又如何在自定义逻辑结束后回接原函数,避免影响正常通信。后续还能将同一思路迁移到接收接口拦截、日志持久化或流量统计等方向,是一份可运行的范例工程。

1. send 函数拦截不是玄学:先搞清“进程自己发送了什么”这个问题

排查一个客户端程序偷偷上传了什么、确认某段业务代码是否真的把数据 send 给了服务端,单靠 Wireshark 抓包往往不够——解密流量、虚拟网卡、回环地址都会让抓包结果失真。ws2_32.dll 里的 send 函数是应用层进程发起网络发送的必经关口,在这个函数上做拦截,把每次 send 的缓冲区内容原样写到文件里,就能得到“这个进程到底向下层交了什么”的精确证据。这个方案不依赖网络环境,不碰网卡驱动,注入成本低,适合做协议逆向、恶意代码行为分析、以及业务数据完整性验证的辅助手段。下面直接给一套能编译、能注入、能落盘的实现。

2. 从 ws2_32.dll 到 send:三种 hook 方案怎么选,我把拦截点定在导入表

2.1 IAT hook、inline hook 与驱动过滤:各自的适用边界

拦截 send 常见有三条路:改导入表(IAT hook)、inline hook、内核态过滤。IAT hook 的做法是找到目标进程里 ws2_32.dll 的导入地址表,把 send 函数指针替换成自己的函数。它不修改 ws2_32.dll 的代码段,只改进程内存里的函数指针,所以稳定、低风险,但只能拦“这个进程通过导入表调用的 send”——如果程序用GetProcAddress动态取地址再调用,IAT 里没有记录,hook 就会失效。

inline hook 直接改写 send 函数的入口字节,无论对方怎么取函数地址都能拦到。代价是改写的是 w32_32.dll 映射到进程里的共享代码页,杀软对这种行为极其敏感,而且自己写跳转指令很容易在 x64 下因为指令长度、重定位问题踩坑。驱动过滤(WFP/LSP)最彻底,可以拦整个系统的流量,但那是另一个量级的工程,调试要上内核工具,不适合快速取证场景。

方案拦截点稳定性杀软敏感度适用场景
IAT hook进程导入表高低分析普通 exe 的网络行为
inline hooksend 函数入口中高对抗动态取地址的情况
WFP 驱动内核网络栈高中高系统级流量采集

2.2 为什么要拦 "send" 而不是 "WSASend":先看目标程序的调用习惯

标题锁定 send 函数,但实际工程里很多程序用的是WSASend。这两个函数都在 ws2_32.dll 里,send 是基础版本,WSASend 是扩展版本,支持异步 IO 和分散/聚集缓冲。老代码、用socket()+send()的简单模型,走 send;用完成端口(IOCP)或 Overlapped IO 的服务器框架,清一色走 WSASend。如果只 hook send,对 IOCP 类程序什么都拦不到。

我一般会先注入一个只记录函数名的探针,打印当前进程加载 ws2_32.dll 后实际调用的是哪个入口。更快的办法是看程序引用的符号:用 IDA 或 dumpbin 看导入表,如果只有 WSASend 没有 send,直接改钩子目标。下面的实现里我会把 send 和 WSASend 两套钩子都写好,运行前用宏切换。

2.3 导入表 hook 的最小原理:PE 头里的线索

Windows 可执行文件加载时,加载器会读取 PE 头里的导入表(Import Table)。导入表里的每个描述符对应一个被导入的 DLL,描述符里的 FirstThunk 指向一张函数指针数组——这张数组在进程内存里叫 IAT。程序真正调用 ws2_32.dll!send 时,是通过这个 IAT 项做间接跳转的。所以把 IAT 里那一项从“send 的地址”改成“我们函数的地址”,进程下次调用 send 就会先进钩子函数。

这个原理不难,但要注意两个坑:第一,x64 下 IAT 项长度是 8 字节,按 4 字节写会踩坏相邻项;第二,导入表里同一个 DLL 可以有多个描述符项,ws2_32.dll 可能出现不止一次(比如 exe 和某个静态库各自导入一份)。在代码里遍历时要处理完所有描述符,不能找到第一个匹配就收工。

3. 用 C++ 写一个 send 拦截 Dll:最小工程、注入流程与 x64 注意事项

3.1 工程骨架:DLL 入口只做两件事,别在里面渲染 UI

先给一个能编译成 DLL 的最小工程。这个工程只做三件事:拿到原始 send 函数地址、替换导入表、把数据写入文件。DLL 入口函数里不做任何 UI 操作、不弹窗,因为注入到目标进程后,DLL 运行在别人的进程上下文,弹窗会干扰目标程序,也容易被安全软件盯上。初始化临界区用于写文件时的线程保护即可。

// send_hook_dll.cpp #include <winsock2.h> #include <windows.h> #include <stdio.h> #pragma comment(lib, "ws2_32.lib") // 保存原始函数地址,64位下指针是8字节 int (WSAAPI* RealSend)(SOCKET s, const char* buf, int len, int flags) = nullptr; // 输出文件目录,保持一致便于清点 static const char* OUTPUT_DIR = "C:\\send_ob_dump"; // 临界区保护文件写入,防止多线程交错 CRITICAL_SECTION g_csWrite; // 把 buf 内容写成一个 ob 文件 void DumpToObFile(const char* buf, int len, SOCKET s) { SYSTEMTIME st; GetLocalTime(&st); char path[MAX_PATH]; wsprintfA(path, "%s\\%04d%02d%02d_%02d%02d%02d_%d.ob", OUTPUT_DIR, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, (int)s); HANDLE hFile = CreateFileA(path, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) return; DWORD written = 0; WriteFile(hFile, buf, len, &written, nullptr); CloseHandle(hFile); } // 钩子函数:先落盘,再调用真实send int WSAAPI HookedSend(SOCKET s, const char* buf, int len, int flags) { EnterCriticalSection(&g_csWrite); DumpToObFile(buf, len, s); LeaveCriticalSection(&g_csWrite); return RealSend(s, buf, len, flags); }

代码说明:RealSend存的是原始 send 地址,HookedSend是替换后的入口。DumpToObFile用文件名里的时间戳和 socket 句柄区分每一次 send 调用,socket 句柄在同一个进程内是唯一标识,可以快速看出是哪个连接发的数据。写文件用的是CreateFileA+WriteFile,没有用 C 运行库的fopen,因为 DLL 注入到别的进程后,CRT 版本不一致容易出问题,直接走 Windows API 最稳。

参数说明:SOCKET s是发送套接字,能帮你对应到连接;buf是要发送的缓冲区;len是发送长度,写文件时按这个长度写,避免把缓冲区里未初始化的尾部也 dump 出来。注意HookedSend里必须先写文件再调用真实 send,因为真实 send 是同步阻塞的,等它返回后缓冲区可能被上层代码重用。

3.2 拦截函数里把 buf 原样落盘:写 ob 文件的时机与线程锁

上面的DumpToObFile每次调用都创建新文件,这在 send 频率低的取证场景没问题,但如果目标程序每秒发送几百个小包,会产生大量小文件,文件句柄频繁开关开销也大。更稳妥的写法是 DLL 加载时就打开一个全局文件句柄,后续每次 send 直接追加写入,文件按大小做旋转。这个优化留到下一章讲,这里先看导入表替换的核心逻辑。

// 遍历导入表,把 WS2_32.dll 的 send 指针替换为 HookedSend bool PatchImportTable() { HMODULE hMod = GetModuleHandleW(nullptr); // 主模块,不含系统DLL auto dos = (PIMAGE_DOS_HEADER)hMod; if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; auto nt = (PIMAGE_NT_HEADERS)((BYTE*)hMod + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) return false; DWORD impRva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; if (!impRva) return false; auto impDesc = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)hMod + impRva); while (impDesc->Name) { const char* dllName = (const char*)((BYTE*)hMod + impDesc->Name); if (_stricmp(dllName, "WS2_32.dll") == 0) { auto thunk = (PIMAGE_THUNK_DATA)((BYTE*)hMod + impDesc->FirstThunk); while (thunk->u1.Function) { if (!(thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)) { auto byName = (PIMAGE_IMPORT_BY_NAME)((BYTE*)hMod + thunk->u1.AddressOfData); if (strcmp(byName->Name, "send") == 0) { RealSend = (decltype(RealSend))thunk->u1.Function; DWORD oldProtect = 0; VirtualProtect(&thunk->u1.Function, sizeof(thunk->u1.Function), PAGE_READWRITE, &oldProtect); thunk->u1.Function = (ULONG_PTR)HookedSend; VirtualProtect(&thunk->u1.Function, sizeof(thunk->u1.Function), oldProtect, &oldProtect); return true; } } thunk++; } } impDesc++; } return false; }

这段代码有两个关键点。第一,VirtualProtect是必须的——IAT 所在的内存页映射为只读,直接写函数指针会触发访问违例崩溃。用VirtualProtect临时改成读写,写完再恢复原保护属性。第二,sizeof(thunk->u1.Function)在 x86 下是 4,x64 下是 8,自动适配位数,不会出现高低位截断。这也是我坚持写(ULONG_PTR)HookedSend而不是(DWORD)强转的原因。

3.3 注入器与验证:从 LoadLibrary 到第一条 send 记录

DLL 写好后需要注入目标进程。常见做法是用 CreateRemoteThread 在目标进程里调用 LoadLibraryA,让系统帮我们把 DLL 加载进去。这个方案特征明显但代码量最小,适合实验环境。

// injector.cpp #include <windows.h> #include <tlhelp32.h> #include <stdio.h> DWORD FindPidByName(const wchar_t* name) { HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32W pe = { sizeof(pe) }; if (Process32FirstW(snap, &pe)) { do { if (wcsstr(pe.szExeFile, name)) { CloseHandle(snap); return pe.th32ProcessID; } } while (Process32NextW(snap, &pe)); } CloseHandle(snap); return 0; } int main(int argc, char* argv[]) { if (argc < 3) { printf("usage: injector.exe <pid> <dll_path>\n"); return 1; } DWORD pid = atoi(argv[1]); HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (!hProc) { printf("OpenProcess failed, error=%u\n", GetLastError()); return 1; } char dllPath[MAX_PATH]; GetFullPathNameA(argv[2], MAX_PATH, dllPath, nullptr); void* remoteBuf = VirtualAllocEx(hProc, nullptr, sizeof(dllPath), MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProc, remoteBuf, dllPath, sizeof(dllPath), nullptr); HMODULE k32 = GetModuleHandleA("kernel32.dll"); FARPROC loadLib = GetProcAddress(k32, "LoadLibraryA"); HANDLE hThread = CreateRemoteThread(hProc, nullptr, 0, (LPTHREAD_START_ROUTINE)loadLib, remoteBuf, 0, nullptr); if (!hThread) { printf("CreateRemoteThread failed, error=%u\n", GetLastError()); return 1; } WaitForSingleObject(hThread, 10000); CloseHandle(hThread); VirtualFreeEx(hProc, remoteBuf, 0, MEM_RELEASE); CloseHandle(hProc); printf("inject complete\n"); return 0; }

注入器流程:OpenProcess 拿到进程句柄,VirtualAllocEx 在目标进程里分配内存,WriteProcessMemory 把 DLL 路径写进去,最后 CreateRemoteThread 启动 LoadLibraryA。注意 LoadLibraryA 的地址必须在目标进程里有效,kernel32.dll 加载地址在多数 Windows 系统上是统一的,但严格做法应该读目标进程的 kernel32 基址再算偏移。这条代码在简单场景够用,遇到异常再排查。

编译命令(Visual Studio 开发者命令行里执行):

cl /LD send_hook_dll.cpp ws2_32.lib cl injector.cpp

/LD生成 DLL,第二个命令生成注入器 exe。编译出来后先建好输出目录C:\send_ob_dump,然后启动目标程序,运行注入器,正常的话目标进程每次调 send 都会生成一个带时间戳和 socket 编号的 ob 文件。验证方式很简单:随便跑一个会发包的程序,甚至 ping 一下局域网地址(ping 走 ICMP 不经过 ws2_32.dll,所以要跑 TCP/UDP 程序),然后看 dump 目录里是不是有文件。

4. 把发送内容按 ob 文件组织:命名规则、旋转写入与 WSASend 补齐

4.1 ob 文件命名与输出目录:先定契约再写代码

做拦截取证时最怕数据混在一起分不清来源。每次 send 生成独立文件这种方式,文件名里至少要包含三层信息:时间、进程内 socket 句柄、数据序号。时间精确到秒够用,同一秒内多次 send 同一个 socket 的情况需要在文件名里加自增序号,否则覆盖写会把前面的包丢掉。

字段格式示例作用
时间戳20250603153045排序和追溯
socket 句柄220区分连接
自增序号001同一秒内同 socket 的区分
扩展名.ob约定为原始缓冲区内容

我一般把输出目录固定成C:\send_ob_dump,并在 DLL 代码里做成宏。如果日志目录不存在,CreateFileA会静默失败,ob 文件就生成不出来。所以 DLL 加载时用CreateDirectoryA主动建目录,建失败说明权限不够,再去事件日志里查。

4.2 每次 send 都开文件太浪费:句柄常驻与按体积旋转

每秒几十次 send 的场景,每次都 CreateFile/WriteFile/CloseHandle 三连,性能损耗很可观。改成句柄常驻:DLL 加载时打开一个文件,之后所有数据追加写入到这个文件里,每满 10MB 关闭旧文件、打开新文件继续写。这样 ob 文件就是一个顺序的二进制流,配合时间戳前缀可以还原出完整的发送时序。

// 追加写入模式的关键参数 DWORD writeMode = OPEN_ALWAYS; // 文件存在则追加,不存在则创建 DWORD shareMode = FILE_SHARE_READ; // 允许其他进程读,方便一边跑一边查看 DWORD flags = FILE_ATTRIBUTE_NORMAL; // 普通文件,不设加密和压缩 HANDLE g_hLogFile = INVALID_HANDLE_VALUE; DWORD g_currentSize = 0; const DWORD MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB 旋转 void WriteToLog(const char* buf, int len) { if (g_hLogFile == INVALID_HANDLE_VALUE) return; DWORD written = 0; WriteFile(g_hLogFile, buf, len, &written, nullptr); g_currentSize += written; // 超过 10MB 就关闭旧文件重开一个 if (g_currentSize >= MAX_FILE_SIZE) { CloseHandle(g_hLogFile); g_hLogFile = CreateFileA(gLogPath, writeMode, shareMode, nullptr, OPEN_ALWAYS, flags, nullptr); g_currentSize = 0; } }

OPEN_ALWAYS这个参数容易被忽略:它表示文件已存在就打开并保留原有内容、写入位置移动到末尾,不存在就新建。这保证目标程序重启后,DLL 重新注入,ob 文件不会清空重来,而是接着上次的位置继续写。FILE_SHARE_READ让你在程序运行的任意时刻都能用十六进制编辑器打开文件查看,不会被共享违例弹窗挡住。

4.3 只拦 send 会漏掉一半流量:WSASend 与 sendto 的钩子写法

真实程序里还有两个入口必须考虑:WSASend 和 sendto。WSASend 是 IOCP 程序的实际出口,sendto 用于 UDP 发送。它们和 send 的参数结构不一样,钩子函数签名也完全不同,但落盘逻辑可以复用。下面这段是 WSASend 的钩子,核心差异在参数:WSASend 的第二个参数是LPWSABUF,指向一组缓冲区,发送的数据可能分散在多个 buffer 里,要逐个拷贝再统一落盘。

// WSASend 钩子:多缓冲区拼接后落盘 int WSAAPI HookedWSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) { // 统计总长度 int totalLen = 0; for (DWORD i = 0; i < dwBufferCount; i++) totalLen += lpBuffers[i].len; // 拼接到一个局部缓冲区 std::string combined; combined.reserve(totalLen); for (DWORD i = 0; i < dwBufferCount; i++) { combined.append(lpBuffers[i].buf, lpBuffers[i].len); } EnterCriticalSection(&g_csWrite); DumpToObFile(combined.data(), totalLen, s); LeaveCriticalSection(&g_csWrite); return RealWSASend(s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpOverlapped, lpCompletionRoutine); }

注意这里的lpBuffers[i].buf是内存指针,异步 WSASend 在函数返回前可能不保证缓冲区内容被读取,所以要在进入钩子时马上拷贝,不能等真实 WSASend 返回后再读。另外DumpToObFile里如果沿用“每次 send 建一个文件”的逻辑,UDP 的每个包就是独立文件,定位方便但文件多;如果走追加合并,就需要在数据前写一个长度头部,解析时按长度切包。

5. send 拦截的 5 条踩坑记录:从安全中心拦截到 hook 静默失效

5.1 注入器一运行就被安全中心拦,进程直接没了

现象:运行 injector.exe 后,目标进程当场退出,或者系统弹出通知说检测到恶意行为,注入器也被删掉。

原因:CreateRemoteThread 注入是安全软件重点监控的行为模式,尤其是往别的进程注入 DLL,行为特征和恶意软件几乎一样。杀软不必知道你注入的是什么,只看“远程线程 + WriteProcessMemory”就足以判定为可疑。

解决:实验环境先给目标目录加白名单,或临时关闭实时防护;正式场景换注入方式,比如用 SetWindowsHookEx 挂消息钩子,利用系统机制加载 DLL,行为特征温和得多。UWP 打包的应用和以更高完整性级别运行的系统进程,普通管理员权限也注入不进去,这是完整性级别问题,和杀软无关。

5.2 拦截了前几次,后面 ob 文件不再增长

现象:刚开始能正常生成 ob 文件,跑了几分钟之后文件停在某个时间点不再更新,目标程序网络行为正常。

原因:常见原因是输出目录被写满了,或者 DLL 持有的文件句柄因为文件被移动、删除而失效。另一个隐蔽原因是目标程序可能在运行中调用了SetDllDirectory改变了 DLL 搜索路径,或者主动调用了LoadLibrary覆盖了旧模块,导致进程里有两份 ws2_32.dll 的映射,加载器后续把 IAT 恢复到了未 hook 的状态。

解决:先在HookedSend里加一个轻量日志,把每次进入钩子的时间、len 写到事件日志或用 OutputDebugString 输出,确认钩子是否还在生效。通常我会用 Procmon 监控 dump 目录的文件操作,看是不是写入方向出了问题。目录满就换路径或加旋转;IAT 被恢复就考虑改用 inline hook 持久驻留。

5.3 send 返回值正常,ob 文件里却是空的

现象:目标程序 send 返回成功,对端也收到了数据,但 ob 文件里没内容,或者只有几字节。

原因:hook 时把 buf 指针和 len 取到了,但写文件时目标程序已经重新分配了这块缓冲区——send 是同步阻塞的没错,但有些框架用独立的发送线程,缓冲区在函数返回后被立即清零。还有更基础的错误:DumpToObFile里我用len作为写入长度,如果 len 本身是 0(比如 send 长度为 0 的心跳包),文件里自然什么都没有。

解决:在进入HookedSend的瞬间把buf内容复制到本地栈或堆上,再执行写文件。不要在调用RealSend之后再碰buf。另外写文件前判断len > 0,零长度发送直接跳过,没必要落盘。

5.4 x64 进程里怎么都 hook 不上,IAT 地址不对

现象:注入成功、DLL 加载成功,但 ob 文件就是不生成,检查PatchImportTable返回值是 false。

原因:最常见的错误是用 x86 版 DLL 注入 x64 进程。CreateRemoteThread 能启动 LoadLibrary 不假,但 32 位 DLL 在 64 位进程里无法正常初始化,代码根本不走你的DllMain。其次,如果目标程序是延迟加载 ws2_32.dll(第一次调用 send 时才加载),DLL 注入时导入表里还没有 WS2_32.dll 的描述符,遍历自然找不到。

解决:严格匹配位数——目标 x86 就编译 x86 DLL,目标 x64 就编译 x64 DLL。延迟加载的情况要等模块加载后再 patch,可以在 DLL 的DLL_PROCESS_ATTACH里开一个定时器,延迟 100ms 再跑PatchImportTable,或者 hookLoadLibraryA等待 ws2_32.dll 被加载。

5.5 多线程程序把同一段数据写乱,ob 内容拼接错位

现象:多个线程同时调用 send,落盘内容交错,单条数据完好但整体顺序乱,或者两个线程的数据拼在一起无法还原。

原因:HookedSend里的临界区只保护了写文件操作,但读取buf内容的动作发生在进程内、由各线程自己执行,临界区无法保证两个线程的buf内容在调用序列上的顺序与真实发送一致。更严重的是跨线程写同一个文件句柄时,WriteFile内部有重叠写指针的问题。

解决:每个线程用独立的文件,用TlsGetValue取线程 ID 拼到文件名里;或者恢复“每次 send 一个文件”的独立文件模式,再由外部工具按时间戳排序合并。对取证来说,独立文件模式虽然文件数量多,但内容边界清晰,解析成本低,更推荐。

6. 进阶技巧:把 ob 文件做成按进程分目录,并对拍验证拦截结果

6.1 按目标进程名分目录,排除系统自带的发送噪音

注入器一次只能注入一个进程,但同一台机器上可能有多个进程都被你注入了同一个 DLL。如果所有 ob 文件混写在同一个目录里,很难分清哪份数据属于哪个程序。我习惯在DllMain的DLL_PROCESS_ATTACH里调用GetModuleFileNameA(nullptr, ...)拿到当前进程的 exe 名,去掉路径和扩展名,拼到输出目录后面作为子目录。这样每个进程的数据独立存放,排查“某个连接是不是这个程序发的”时可以直接按目录过滤。

// 按进程名建子目录,防止多个注入进程的数据混在一起 char exePath[MAX_PATH]; GetModuleFileNameA(nullptr, exePath, MAX_PATH); std::string exeName = std::string(strrchr(exePath, '\\') + 1); auto dotPos = exeName.find_last_of('.'); if (dotPos != std::string::npos) exeName = exeName.substr(0, dotPos); char fullDir[MAX_PATH]; wsprintfA(fullDir, "C:\\send_ob_dump\\%s", exeName.c_str()); CreateDirectoryA(fullDir, nullptr);

这段代码的坑在strrchr:如果 exePath 里没有反斜杠(比如工作目录直接在根下),返回 nullptr 会崩溃,所以要先判空再取子串。Windows 的进程名一般都有反斜杠,但防御性写法不能省。

6.2 拿本地 Client/Server 对拍,确认拦到的数据完整性

hook 方案最大的风险是静默失效——你看着 ob 文件在增长,但不一定代表每个包都拦到了。我的验证习惯是搭一个最小回环测试:本机起一个 UDP socket 发已知内容,同时用同样的数据做一份十六进制对照。具体做法是写一个小程序,连续 send 一段带递增序号的内容,然后比对 ob 文件里的数据是否完整覆盖了序号区间。

这个测试有两个层面:第一验证 hook 本身没有漏包,第二验证 ob 文件落盘的数据没有被截断。我踩过的坑是 WriteFile 的written参数可能与len不一致——磁盘满、文件被外部锁定都会导致 short write,而代码里没判断返回值。现在我的写法是每次 WriteFile 后比对written == len,不一致时在文件名里加.trunc后缀,至少让后续分析的人一眼看出这份数据不完整,而不是默默扔掉。

做这类工具几年下来,我的体会是:hook 代码只占三成工作量,剩下七成都在处理“数据没写全、钩子掉了、被安全软件当恶意样本清掉”这些边角状况。第一版能跑通不算完,把上面这些排查项提前做进代码里,才敢把工具丢给同事用。希望这些记录帮你在做自己的 send 拦截时少走几步弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询