
简介面向网络安全研究人员、逆向工程师或系统管理员该压缩包提供一套基于VC开发的API Hook拦截工具可对指定进程发送和接收的网络数据包进行监控、分析与自定义处理适用于调试分析、协议解析、安全测试等场景。包内共22个文件以8个头文件和4个C源文件为核心工程另含2个dsp工程文件、2个dsw工作区文件、2个txt说明文档、1个动态链接库及图标资源等整体仅37KB便于快速查看与编译。工程代码覆盖了进程枚举与目标选择、虚拟内存中API函数入口前8字节跳转修改以及结合VirtualQuery、VirtualProtect、WriteProcessMemory等API的底层Hook过程可对收发的数据包进行检查或改写。配套源码与说明文档完整展示了DLL注入、系统调用表Hook及共享内存等实现思路适合希望深入理解Windows API Hook机制并开展网络监控实验的读者参考。该资源已有876人学习下载。1. 什么是API HOOK拦截指定进程数据包为什么它是调试网络程序的最后手段当你怀疑某个客户端在后台偷偷发数据、或者自己写的网络程序在特定机器上连不上服务器通常第一反应是开Wireshark抓包。可抓包只能看到线路上的字节流一旦程序用了TLS抓包工具拿到的是密文而且你没法直观判断某个包到底是哪个进程、哪次调用发出去的。API HOOK拦截指定进程发送和接收的网络数据包就是绕开这套黑匣子直接住进目标进程内部在它调用send/recv的那一刻把进出缓冲区的内容原样截获下来。这个方案适合Windows客户端开发、协议联调、以及排查“某个进程到底在发什么”这类问题你不用重新编译目标程序只要注入一个DLL进去就行。这份方案的核心动作只有三步把Hook DLL注入到指定进程、钩住Winsock层的收发函数、把缓冲区和连接信息按可读格式记下来。整个过程不依赖任何内核驱动运行在应用层看得见进出该进程的真实Buffer。下面直接拆原理、给实现、列参数再把这几年实操遇到过的五种翻车情况讲透。2. HOOK点选型为什么盯住send/recv而不是直接用抓包库2.1 应用层HOOK与抓包、WFP的分工Wireshark、Npcap这类工具位于内核协议栈旁路通过NDIS或AFD的复制路径拿到线路数据。它的优势是能看到所有进程所有网卡接口的流量劣势是对不上进程的调用上下文你得靠端口反查pid加密流量拿到也只能干瞪眼。Windows的WFPWindows Filtering Platform能在内核层按进程ID做过滤但它要写驱动或服务配置复杂而且它拿到的仍然是线路上的数据对应用层明文同样无能为力。API HOOK的位置完全不同它在目标进程的用户态内存里直接在Winsock的send/recv导出函数入口挂跳转因此天然具备两个抓包工具拿不到的能力。第一能看到应用层缓冲区buf里面真正要发出去的内容包括TLS封装前的明文前提是你把钩子挂在更上层比如SSL_write如果只钩send那看到的就是TLS密文。第二能看到发起调用的线程结合线程栈和当前进程ID可以精确知道“这个数据是哪个线程发的”。这正是“指定进程”这个需求最核心的价值——不是拦全网而是只盯一个目标进程的收发动作。所以选型结论很直接如果你要分析的是目标进程应用层的数据内容、调用行为、协议交互规律API HOOK是最短路径如果你需要全网流量审计、丢包重传分析、或者抓Wi-Fi驱动层面的帧那该用抓包工具还是用抓包工具两者不是替代关系。2.2 四个候选函数send/recv/WSASend/WSARecv怎么选Windows下应用层网络收发的主干调用链是应用逻辑 → 协议库TLS/HTTP等→ ws2_32.dll → AFD.sys → 内核协议栈。对我们做HOOK来说ws2_32.dll这一层是用户态的最后一道关卡钩住它就能覆盖目标进程绝大多数网络行为又不用碰驱动。需要优先考虑的函数列表如下函数场景备注sendTCP/UDP已连接socket的发送参数直接给buf、len逻辑最简单recvTCP/UDP已连接socket的接收注意必须在调用后根据返回值确定有效长度WSASend异步/多缓冲区发送I/O完成端口场景更常用WSARecv异步/多缓冲区接收尊重lpNumberOfBytesRecvdsendto / recvfromUDP未连接场景可选的补充钩子closesocket连接关闭日志可选用于记录socket生命周期最常见的做法是先钩住send和recv这两个基础函数跑通一个最小日志输出当发现目标程序走的是投递型异步I/O比如IOCP再补上WSASend和WSARecv。需要注意WSASend的第二个参数是WSABUF数组一个调用可能同时携带多个缓冲区日志输出必须遍历数组否则会丢数据WSARecv则在调用成功后才通过dwNumberOfBytesRecvd给出真实接收长度很多时候它比入参的len小得多别拿入参len去写日志。除了这4个收发函数connect也可以一起钩住在connect返回成功后调一次getpeername记录对端IP和端口后续所有send/recv日志里的socket都能映射到具体的远程地址。这个准备工作做得好第4章解析连接信息时就会轻松很多。如果你要覆盖UDP广播/组播那种没有建立“连接”的socket那就必须再勾上sendto/recvfrom因为getpeername对这类socket基本不可用。2.3 进程过滤的粒度按PID过滤还是按socket句柄过滤标题里“指定进程”听起来像在HOOK之前就要选好进程但工程上有两种实现路径过滤粒度不一样。第一种是远程线程注入CreateRemoteThread LoadLibraryDLL只被加载到目标进程内部。这种情况下DLL里的Hook函数只会被目标进程触达理论上不需要再做进程ID判断。但我依然建议在日志写入前加一道GetCurrentProcessId() g_targetPid的检查当作双保险——防止注入线程自身的网络调用也被记录。第二种是WH_GETMESSAGE全局钩子方式HookDLL会通过注册表被系统自动加载进所有满足消息条件的进程此时你必须用PID过滤否则全系统进程的收发都会被记录下来日志会瞬间爆炸。socket句柄级过滤则更精细如果目标进程里有多个连接我们只关心其中某几个远端端口就可以在Hook_connect里把感兴趣的SOCKET句柄加到白名单集合只有白名单内的socket才写日志。这样做的好处是日志干净缺点是并发量大时哈希表的查询和锁竞争会引入额外开销。我实际做的时候通常先用PID级过滤保证数据不脏再在日志还原阶段把连接信息打全最后按端口条件筛出需要关注的行而不是在HOOK热路径上做复杂过滤。进程与线程在这里也有区别PID过滤是进程级的粗粒度筛选TID记录是线程级的细粒度追踪——记下发出本次调用的线程ID排查数据竞争时比单纯看进程好用得多。3. 最小可运行方案DLL注入加API HOOK的完整实现3.1 注入器CreateRemoteThread LoadLibrary 的标准姿势既然要“指定进程”最常见的注入方案就是让目标进程自己调一次LoadLibrary把我们的HookDLL加载进去。下面这段注入器代码是Windows平台的老牌做法网上也能搜到同类实现核心是四步打开进程、异地分配内存、写入DLL路径、远程线程执行LoadLibrary。// injector.cpp #include windows.h #include stdio.h int wmain(int argc, wchar_t* argv[]) { if (argc 3) { wprintf(Lusage: injector.exe pid hookdll_path\n); return 0; } DWORD pid (DWORD)_wtoi(argv[1]); const wchar_t* dllPath argv[2]; // 按需申请最小权限不要像网上老代码那样直接 PROCESS_ALL_ACCESS HANDLE hProcess OpenProcess( PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid); if (!hProcess) { wprintf(LOpenProcess failed, error%d\n, GetLastError()); return 1; } size_t len (wcslen(dllPath) 1) * sizeof(wchar_t); LPVOID pRemotePath VirtualAllocEx(hProcess, NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!pRemotePath) { wprintf(LVirtualAllocEx failed\n); CloseHandle(hProcess); return 1; } if (!WriteProcessMemory(hProcess, pRemotePath, dllPath, len, NULL)) { wprintf(LWriteProcessMemory failed\n); VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE); CloseHandle(hProcess); return 1; } HMODULE hKernel32 GetModuleHandleW(Lkernel32.dll); LPTHREAD_START_ROUTINE pLoadLibrary (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, LoadLibraryW); HANDLE hThread CreateRemoteThread(hProcess, NULL, 0, pLoadLibrary, pRemotePath, 0, NULL); if (!hThread) { wprintf(LCreateRemoteThread failed, error%d\n, GetLastError()); } else { // 这就是典型的进程等待wait等远程线程执行完 LoadLibrary WaitForSingleObject(hThread, 10000); DWORD exitCode 0; GetExitCodeThread(hThread, exitCode); wprintf(LLoadLibraryW returned 0x%x\n, exitCode); CloseHandle(hThread); } VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE); CloseHandle(hProcess); return 0; }逻辑说明OpenProcess的权限参数不需要给满PROCESS_ALL_ACCESS在部分安全软件策略下会直接触发拦截给到能创建线程、能读写内存、能查询信息即可。VirtualAllocEx在目标进程地址空间里分配一块区域存放DLL路径字符串长度要按宽字符计算别只算strlen。CreateRemoteThread的线程函数地址取自kernel32导出表因为每个进程都加载kernel32LoadLibraryW首地址在所有进程里基本一致只要ASLR没有跨进程随机化问题实际中取当前进程的地址是可用的。WaitForSingleObject就是“进程等待”在Windows上的具体形态等这个注入线程退出再用GetExitCodeThread把LoadLibraryW的返回值取出来——这个返回值就是DLL的模块基址如果为0说明加载失败。3.2 Hook DLL用Detours挂住send/recv/WSASend/WSARecv注入只是把DLL送进目标进程真正干活的是DLL里的Detour钩子。这里用的是微软Detours库的经典用法先把原函数指针保存下来再用DetourAttach替换入口之后原函数指针指向的是绕过HOOK的“真函数”。下面这段是DLL的核心实现。// hookdll.cpp #include windows.h #include detours.h #include winsock2.h #include ws2tcpip.h #include stdio.h #pragma comment(lib, ws2_32.lib) #pragma comment(lib, detours.lib) static DWORD g_targetPid 0; static FILE* g_logFile nullptr; static CRITICAL_SECTION g_logLock; static int (WINAPI * Real_send)(SOCKET s, const char* buf, int len, int flags) send; static int (WINAPI * Real_recv)(SOCKET s, char* buf, int len, int flags) recv; static int (WINAPI * Real_WSASend)(SOCKET s, LPWSABUF bufs, DWORD cnt, LPDWORD sent, DWORD flags, LPWSAOVERLAPPED ov, LPWSAOVERLAPPED_COMPLETION_ROUTINE cr) WSASend; static int (WINAPI * Real_WSARecv)(SOCKET s, LPWSABUF bufs, DWORD cnt, LPDWORD recvd, LPDWORD flags, LPWSAOVERLAPPED ov, LPWSAOVERLAPPED_COMPLETION_ROUTINE cr) WSARecv; bool IsTargetProcess() { return g_targetPid ! 0 GetCurrentProcessId() g_targetPid; } void WriteLog(const char* dir, SOCKET s, const char* data, int len) { if (!IsTargetProcess() || len 0) return; EnterCriticalSection(g_logLock); SYSTEMTIME st; GetLocalTime(st); fprintf_s(g_logFile, [%02d:%02d:%02d.%03d][%s][sock%lld][len%d] , st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, dir, (long long)s, len); int dump len 32 ? len : 32; // 默认只打前32字节后续按配置扩展 for (int i 0; i dump; i) fprintf_s(g_logFile, %02x , (unsigned char)data[i]); fprintf_s(g_logFile, \n); fflush(g_logFile); LeaveCriticalSection(g_logLock); } int WINAPI Hook_send(SOCKET s, const char* buf, int len, int flags) { WriteLog(SEND, s, buf, len); // send 是调用前记录 return Real_send(s, buf, len, flags); } int WINAPI Hook_recv(SOCKET s, char* buf, int len, int flags) { int ret Real_recv(s, buf, len, flags); if (ret 0) // recv 必须先调用再记录 WriteLog(RECV, s, buf, ret); return ret; } int WINAPI Hook_WSASend(SOCKET s, LPWSABUF bufs, DWORD cnt, LPDWORD sent, DWORD flags, LPWSAOVERLAPPED ov, LPWSAOVERLAPPED_COMPLETION_ROUTINE cr) { for (DWORD i 0; i cnt; i) WriteLog(WSASEND, s, bufs[i].buf, (int)bufs[i].len); return Real_WSASend(s, bufs, cnt, sent, flags, ov, cr); } int WINAPI Hook_WSARecv(SOCKET s, LPWSABUF bufs, DWORD cnt, LPDWORD recvd, DWORD flags, LPWSAOVERLAPPED ov, LPWSAOVERLAPPED_COMPLETION_ROUTINE cr) { int ret Real_WSARecv(s, bufs, cnt, recvd, flags, ov, cr); if (ret 0 recvd) { DWORD total *recvd; // 实际收到的总字节数 for (DWORD i 0; i cnt total 0; i) { DWORD n (bufs[i].len total) ? bufs[i].len : total; WriteLog(WSARECV, s, bufs[i].buf, (int)n); total - n; } } return ret; } void LoadConfig() { // 配置文件里只有一行目标PID // 常见做法是注入器启动前把PID写进 C:\hook_log\hook.ini FILE* cfg nullptr; fopen_s(cfg, C:\\hook_log\\hook.ini, r); if (cfg) { fscanf_s(cfg, %lu, g_targetPid); fclose(cfg); } } void InstallHooks() { InitializeCriticalSection(g_logLock); LoadConfig(); if (!g_targetPid) return; // 强制加载winsock避免DetourAttach时ws2_32还没进进程 LoadLibraryA(ws2_32.dll); fopen_s(g_logFile, C:\\hook_log\\netpacket.log, a); if (!g_logFile) return; DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach((PVOID)Real_send, Hook_send); DetourAttach((PVOID)Real_recv, Hook_recv); DetourAttach((PVOID)Real_WSASend, Hook_WSASend); DetourAttach((PVOID)Real_WSARecv, Hook_WSARecv); if (DetourTransactionCommit() ! NO_ERROR) { fclose(g_logFile); g_logFile nullptr; } } BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) { if (reason DLL_PROCESS_ATTACH) { InstallHooks(); } else if (reason DLL_PROCESS_DETACH) { if (g_logFile) { DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); // 卸载顺序必须和安装顺序相反 DetourDetach((PVOID)Real_WSARecv, Hook_WSARecv); DetourDetach((PVOID)Real_WSASend, Hook_WSASend); DetourDetach((PVOID)Real_recv, Hook_recv); DetourDetach((PVOID)Real_send, Hook_send); DetourTransactionCommit(); fclose(g_logFile); g_logFile nullptr; } DeleteCriticalSection(g_logLock); } return TRUE; }逻辑说明所有原函数指针都初始化为ws2_32导出函数的地址DetourAttach会把这4个指针改写为“跳板函数”地址。之后在Hook_send里调用Real_send执行的是绕过钩子的原始代码路径不会递归。recv方向必须先调用原始函数再用返回值ret作为实际长度写日志因为入参len只是缓冲区容量数据是否真正填充要等调用返回才知道。WSASend的多缓冲遍历要注意完负数——cnt一般不会为0但防御性判断不可省。参数说明DetourTransactionBegin/Commit是一组事务多个Attach之间原子性生效DetourUpdateThread(GetCurrentThread())是让当前线程在事务提交时调整指令指针避免正在执行被HOOK函数前几条指令的线程崩溃。Installer里LoadLibraryA(ws2_32.dll)是为了确保winsock已经映射进进程地址空间——如果目标进程还从没调过任何网络APIws2_32.dll可能尚未加载DetourAttach就会因为找不到模块而失败。3.3 配置与启动PID从哪来、日志写到哪里上面的DLL从C:\hook_log\hook.ini读取目标PID这是最简单直接的传参方式。注入器只需要在启动前把PID写进这个文件或者启动时先用命令行参数生成这个文件不需要在目标进程里做复杂的内存共享。备选方案还有写注册表项、或把PID作为环境变量——但环境变量在CreateRemoteThread的线程里不会天然继承所以不建议用。整个方案的启动流程是这样的以管理员身份打开命令行启动需要监控的目标进程从任务管理器或tasklist拿到PID把HookDLL.dll放到目标机器磁盘上执行injector.exe pid C:\hook_log\HookDLL.dll观察C:\hook_log\netpacket.log。日志按追加模式打开进程不退出就一直写。一个容易忽略的点是日志目录必须先创建否则fopen_s会失败整个Hook静默退出。编译时注意平台位数注入器和HookDLL必须和目标进程保持一致。调试阶段建议先用一个自己写的测试进程比如一个循环发送UDP数据的小程序来验证关键步骤确定日志能正常输出再上真实目标。这样能把“注入失败”和“HOOK不生效”两类问题分开排查而不是混在一起瞎猜。4. 把原始buffer还原成能分析的流量连接信息解析与过滤4.1 从SOCKET句柄还原对端地址HOOK到了send/recv的缓冲区和长度但日志里只有socket整数值SOCKET本质是unsigned int不直观。每次记录时对端IP和端口是多少最常见做法是直接在Hook函数里调一次getpeername把socket转成可读的地址。这个API本身就是Ws2_32导出的不需要额外钩子直接调用真实系统函数即可。void GetPeerInfo(SOCKET s, char out[64]) { sockaddr_in addr; int addrLen sizeof(addr); if (getpeername(s, (sockaddr*)addr, addrLen) 0) { sprintf_s(out, 64, %s:%u, inet_ntoa(addr.sin_addr), // 仅IPv4IPv6请换inet_ntop ntohs(addr.sin_port)); } else { strcpy_s(out, 64, unknown); } }在WriteLog里增加一个字段把GetPeerInfo的输出放进日志行首。这样每条SEND/RECV记录都自带远程地址后续在大量日志里grep某个IP非常方便。需要注意对UDP非连接socketgetpeername在没有调用connect时会失败返回“unknown”这就是为什么第2章里说UDP场景要额外挂sendto/recvfrom那组函数会直接返回目标地址。对TCP的CLOSE_WAIT等状态getpeername一般仍然可用只是拿到的可能是一个已经不存在的对端端口依然有参考价值。4.2 TCP与UDP两套函数族两种日志形态先澄清一个概念标题里说的“网络数据包”在应用层HOOK这个视角下其实看到的是“进程写入socket的字节流”和“socket读出的字节流”不包含IP头、TCP头也不是按网络包边界划分的。TCP是流协议你调用send一次内核可能拆成多个报文发出远端发来的一个报文recv可能分两次读出来。所以日志里的SEND记录表示“这一次调用写入了多少字节”不代表线路上真实的一个IP包。UDP就不一样sendto/recvfrom天然对应数据报文边界一条记录就是一个完整的数据报。如果你要做的分析对报文边界敏感比如TURN、QUIC、DNS那么在实现时要把sendto/recvfrom也挂上。钩子签名类似send/recv只是多了目标地址参数sockaddr*在Hook_sendto里地址信息由参数直接给出不需要再调用getpeername。将TCP的字节流日志和UDP的报文日志混合记录时建议在日志类型字段上区分流TCP和数据报UDP否则后面处理时很容易误用报文边界。日志文件本身建议用“时间戳 方向 socket 对端地址 长度 hex转储”这种一行一记录的布局。hex转储默认截断到前32字节对大多数协议识别已经够用要分析完整协议内容时再通过配置项把它放大到64、128甚至全量。对明显是文本协议的数据可以在hex后面顺带补一行ASCII方便肉眼快速扫。4.3 过滤规则白名单端口、长度阈值与hexdump开关日志增长非常快几秒钟就能刷出几十MB所以过滤规则不是可选项而是必选项。我在实际项目中维护过一组配置项同样放在hook.ini里配置项含义示例target_pid目标进程PID8899port_whitelist只记录这些远端端口逗号分隔443, 8080, 53min_len小于该长度不记录4dump_lenhexdump截断字节数64enable_wsabufWSASend/WSARecv是否启用1过滤逻辑放在WriteLog入口比放在Hook函数里更统一先判断方向、socket、长度全部通过才进临界区写日志。例如端口白名单可以在GetPeerInfo之后取出端口用strchr句或字符串查找判断也可以维护一个std::set编译成Release时查一次。请注意“少记日志”和“丢失关键包”的权衡端口白名单通常只放感兴趣的端口一旦目标程序改成随机高端口连服务器白名单外的流量就被错过了。我建议保留全量漏斗式记录到文件再写一个独立的小脚本按月归档压缩而不是在热路径上做太多过滤。5. API HOOK拦截网络包的五条避坑记录从注入失败到日志死锁5.1 注入成功但日志里一条包都没有Winsock模块时序现象任务管理器里已经能看到HookDLL被加载进目标进程目标进程也在正常收发数据但netpacket.log里一条记录都没有。原因目标进程可能在启动早期就被注入那个时候ws2_32.dll还没有被加载进地址空间。DetourAttach需要替换ws2_32.dll里导出函数的入口地址如果模块不存在DetourAttach会返回失败可是代码里没有检查DetourAttach的返回值事务提交时全部一起提交失败被掩盖了。解决在InstallHooks里先显式调用LoadLibraryA(ws2_32.dll)确保winsock映射进进程再执行DetourAttach。同时要检查DetourTransactionCommit的返回值提交不成功就打印错误字符串到日志文件别让它静默失败。另外如果注入发生在进程初始化极早期还可以用DLL_PROCESS_ATTACH里启动一个延迟初始化线程等2秒后再装钩子。这个方案有一定风险但实测中用CreateThread等待目标进程稳定后再安装比在Loader Lock里硬干要稳。5.2 32位注入器打64位进程直接翻车现象注入器对64位目标进程执行CreateRemoteThread返回NULL错误码是ERROR_INVALID_HANDLE或者ERROR_ACCESS_DENIED偶尔成功注入但LoadLibrary的返回值是0。原因32位进程无法加载64位DLL64位进程的模块句柄空间也和32位不同。如果你的注入器和HookDLL都是32位编译而目标进程是64位那CreateRemoteThread可能创建成功但LoadLibraryW尝试加载32位DLL时直接失败。解决先确认目标进程位数。可用Process Explorer查看镜像架构或者代码里用IsWow64Process检测如果目标进程是64位而当前注入器是32位则必须切换编译配置。我在实际项目里统一维护Win32和x64两套构建注入时根据目标PID判断位数再选对应批处理启动。另一种隐蔽情况是HookDLL编译为64位但注入器是32位OpenProcess同名进程时打开的是另一个同名32位进程这也是常见的“注入错进程”误判。确认打开的句柄和日志里的进程ID完全一致再动手分析。5.3 recv的日志全是乱码记录时机不对现象RECV记录的内容明显是垃圾数据长度有时超过实际网络返回值有时和发送方对不上。原因在调用Real_recv之前就用入参里的buf指针写日志此时缓冲区是空的里面是上一次recv留下的旧内容。常见于把send的“调用前记录”逻辑复制到recv里忘了接收方向的区别。SEND是“你手里的数据确定有效”RECV是“系统还没往这块buf里填数据”两者时机完全相反。解决先把Real_recv调用返回ret 0才用buf做记录长度用ret而不是len。WSARecv同理必须等调用完成后读lpNumberOfBytesRecvd。这个坑属于血泪经验每次新写一个hook函数我都会先在代码注释里写明“调用前可读/调用后可读”避免惯性写错。5.4 目标进程卡死同步日志IO堵住了热路径现象日志文件越来越大目标进程的网络请求变慢界面卡顿严重时整个进程假死结束后日志尾部出现长达几秒的时间间隔。原因日志里每个包都执行fprintf_s fflush而fflush是同步磁盘I/O。当网络吞吐高时HOOK函数在调用链上被磁盘写阻塞相当于每次send/recv都要等一次硬盘写完成。多线程时多路网络线程同时写日志还会争用临界区形成锁竞争。解决把写盘操作移出HOOK热路径。更好的办法是进程通信IPC的共享内存方案Hook函数只负责把记录塞进一块环形缓冲区另一个独立观察进程负责从共享内存读出并落到磁盘。目标进程内部只做内存拷贝IO压力完全转移。这样既能实时观察目标进程的收发数据又不会把它卡死。如果你只想快速验证退一步把fflush从每次写都调用改成定时刷新能缓解但不能根治高并发场景。5.5 进程退出时崩溃Hook卸载顺序反了现象目标进程正常退出时弹异常或者日志最后几条记录丢失文件截断。原因DLL_PROCESS_DETACH里先关了g_logFile而后面DetourDetach还没执行完此时其他线程正好还停在某次WriteLog里往一个已经fclose的FILE*写入触发访问违例。还有一种原因是DetourDetach的恢复顺序和Attach顺序不一致导致原函数指针在恢复期间被写乱。解决严格按“先Detach恢复所有钩子再关日志文件”的顺序。如果担心进程退出瞬间其他线程还在执行被钩函数可以先在WriteLog入口加一个g_shutdown标记位detach前设置标记并等待20~50毫秒让正在执行中的WriteLog先退出临界区再做DetourDetach和fclose。注意如果目标进程被TerminateProcess强杀系统不会给DLL_PROCESS_DETACH机会日志尾部缺失属于可接受现象不用在这上面浪费过多时间。6. 验证收尾不用抓包工具也能确认钩子生效的三个方法第一个验证方法是看日志文件本身。启动目标进程做几次网络操作后打开C:\hook_log\netpacket.log看到结构化的SEND和RECV记录就说明钩子链路已经通了。这里有个细节日志行里的对端地址字段必须和你的实际业务服务地址一致如果地址是unknown或者端口不对往往说明Winsock时序或者UDP连接状态有问题需要回到第5章排查。第二个验证方法是确认DLL确实被加载进目标进程。执行tasklist /m HookDLL.dll输出里能看到目标进程的映像名称和PID说明注入成功。也可以把HookDLL.dll文件路径临时改成一个带版本信息长度的名称日志里能顺手验证版本。这一步配合第一个验证方法能从“DLL在没在”和“钩子有没有生效”两个维度把问题切分干净。第三个验证方法是用netstat交叉核对。执行netstat -ano | findstr pid能找到目标进程当前活动的TCP连接与本地/远端端口。把这两个端口和日志里记录的对端地址比对应该能对上。如果netstat显示有连接但日志里没有对应记录大概率是UDP连接或异步I/O场景——这种高吞吐场景下HOOK的日志丢失多半是线程和缓冲问题而不是钩子没生效。进阶用法是把日志落盘改成共享内存加独立观察进程这也是进程通信IPC在HOOK场景下的常见演进。Hook函数写入预分配共享内存观察进程用消息循环实时刷新目标进程几乎不承担任何磁盘开销。我最初做这个方案时在recv的“调用前记录”这个简单环节上栽过跟头日志全是垃圾排查半天才发现是时机问题。之后养成了一个习惯每个hook函数先写清数据何时有效再动手实现。希望帮到你。本文还有配套的精品资源点击获取