简介:在Windows网络编程中,拦截应用层网络请求常需面对闭源程序、加密协议等难题。LSP注入(Layered Service Provider,分层服务提供程序)作为一种基于Winsock2 SPI机制的用户态DLL注入技术,能够在不改动目标程序的前提下,通过协议链自动加载到进程空间,从而获取所有Winsock调用的上下文。它本质上是在传输提供程序与基础协议栈之间插入中转层,既能用于流量审计、内容过滤、协议分析,也可作为理解Windows网络栈分层设计的入口。与API Hook和WFP驱动相比,LSP无需内核权限,开发门槛低,但需注意64/32位Catalog隔离及卸载顺序等陷阱。本文围绕LSP注入的原理、最小工程实现、数据记录与避坑验证展开,为流量监控与协议分析实践提供参考。
1. LSP注入是什么:一个能“偷看”所有网络请求的DLL接口
做网络流量监控或协议分析的人多半遇到过这种尴尬:目标程序是闭源的,没法改它的代码,可又想知道它跟服务器之间到底发了什么、收了什么。抓包工具能看到包,但包是加密的或是二进制协议,还原不出应用层语义。LSP注入就是一条老派但依旧有效的路——LSP(Layered Service Provider,分层服务提供程序)是Winsock2 SPI体系里的一个可注入DLL,进程只要调用了socket相关的API,系统就会按协议链自动把这个DLL加载进去,之后你就能在收发路径上插入自己的处理逻辑。
它解决的核心问题是“不改程序、不碰驱动、在用户态拿到所有Winsock调用的上下文”。适合做流量审计、内容过滤、协议学习、内部系统监控这类场景。新手能从这里理解Windows网络栈的分层思路,熟手则能用它补上对SPI机制和协议链的边界认知。本文会从原理、工程实现、安装部署一路讲到四五个经典的坑,最后给出验证和进程级过滤的技巧,供直接照抄。
2. LSP注入的底层原理:Winsock2 SPI与分层服务提供程序
2.1 从API到SPI:ws2_32.dll只是转发层
Windows的Winsock2设计里,程序员平时调用的socket、send、recv这些函数并不直接访问协议驱动。它们都在ws2_32.dll里,真正的协议实现被抽象成一组SPI(Service Provider Interface,服务提供程序接口)。ws2_32.dll的角色更像一个路由壳,它把一次socket调用转交给配置好的“传输提供程序”。这个提供程序链在系统中注册在一个叫Winsock Catalog的目录里,目录项就是一个个服务提供程序的描述。
LSP是插在这一串provider之间的那一环。它本身也是一个DLL,系统通过一个约定好的入口函数WSPStartup把它初始化。只要协议链上挂载了这个LSP,进程里任何一个socket操作(创建、连接、收发、关闭)都会先经过它。严格说LSP不是传统意义的“注入”,它不需要往进程空间里写代码,而是靠Winsock自身的扩展机制被自动加载,所以稳定性和兼容性比一般的远程线程注入高出不少。
在讲实现之前,建议先理解几个关键术语,否则后续代码会看得一头雾水。每个传输提供程序在Catalog里对应一个WSAPROTOCOL_INFO结构,里面记录服务类型、协议ID、链的层数。如果有多个LSP串起来,WSAPROTOCOL_INFO里的ProtocolChain字段会记录整条链的组成顺序。系统通过这个链把一个socket的收发请求从上层一路传到最下面的基础传输提供程序(比如TCP/IP的mswsock.dll),LSP在其中扮演拦截中转的角色。
2.2 协议链与Catalog条目:LSP挂在哪一层
LSP并不自己实现TCP/IP协议栈,它必须依托一个基础传输提供程序才能工作。安装LSP时要做两件事:一是在Catalog里注册一个新的分层provider(Layered Provider),二是把这个provider“夹”进一或多个基础传输提供程序的链里。这条链在Winsock2的术语里叫ProtocolChain,链由若干分层provider和一个基础provider组成。用Winsock Catalog查看工具可以看到形如“分层服务提供程序 → TCP/IP”这样的条目。
整个Catalog本身存在注册表里,系统启动和进程初始化Winsock时会读取它。因此LSP的安装本质上就是把DLL路径和一个WSAPROTOCOL_INFO写进注册表对应的Catalog键值。微软提供的API是WSCInstallProvider和WSCWriteProviderOrder,前者负责注册provider,后者负责调整链的顺序。顺序很重要:如果自己的LSP排在别的LSP后面,先被调用的就是别人,你的LSP可能在它的转发链里被包了一层。
目录结构上还有一个易踩的坑:64位系统和32位程序的Winsock Catalog并不互通,注册表里分成了两个位置。这也是后面避坑章节要重点展开的问题。
2.3 和API Hook、WFP驱动的边界对比
很多人在做流量监控时会纠结技术选型,最容易混淆的三条路线是LSP、API Hook/Inline Hook、WFP驱动。LSP只在Winsock协议的范畴内生效,如果目标程序不走Winsock而是直接和驱动通信,LSP是看不到的。API Hook可以覆盖更底层,但要改写目标进程的指令,容易被游戏保护或安全软件拦截,也容易把自己的DLL搞出递归。WFP(Windows Filtering Platform)则是在内核态的过滤框架,功能最强、性能最好,但需要驱动签名、需要跨版本维护,开发门槛高出一大截。
对比下来,LSP的定位是“用户态、全局生效、不需要提权驱动、能拿到应用层数据”的折中方案。适合在企业内网审计、自研协议栈分析、教学实验这些场景中做透明中间层。注意,现在常说的LSP有时也指Language Server Protocol(语言服务器协议),那是编辑器里给代码补全用的东西,比如Claude Code这类AI编程工具里就有它的身影。本文的LSP是Winsock分层服务提供程序,两者只是首字母缩写碰巧一样,千万别混。
3. 搭一个最小LSP注入工程:工程结构、导出函数与初始化顺序
3.1 环境准备与DLL工程的最小结构
开发LSP不需要特殊的SDK,Visual Studio中的Windows桌面应用程序开发组件就够。工程类型选“动态链接库(DLL)”,项目名称可以叫LspSandbox,然后关掉预编译头,语言标准选C++14以上。核心产物只有一个DLL,但建议把安装器单独做成一个控制台工程,避免调试时反复改代码导致系统Catalog不稳定。
一个LSP DLL至少需要三样东西:DllMain入口、WSPStartup导出函数、以及一组实现了SPI接口的转发函数。WSPStartup是系统调用LSP的起点,相当于LSP的黑匣子入口,所有初始化都在这里完成。此外还需要一个.def文件或导出声明,把WSPStartup按序号导出。很多初学朋友在这里犯迷糊,以为DLL只要导出了WSPStartup就行,其实还需要保证ws2_32.dll能找得到它。
最简单的导出方式是在.def文件中写明导出符号,如下所示:
LIBRARY "LspSandbox" EXPORTS WSPStartup如果不想维护.def文件,也可以在代码里用#pragma comment(linker, "/EXPORT:WSPStartup=WSPStartup")声明导出。两种都行,但.def文件更清晰,我一般倾向于用.def。这个文件编译后不会出现在产物目录里,它是一个编译指令,让链接器把WSPStartup这个符号暴露出去,供系统加载时绑定。
3.2 WSPStartup入口:接收调用表与协议信息
接下来是最核心的入口函数。Winsock2 SPI规定,系统的目录管理器会调用WSPStartup,传入版本号、上层调用函数表(UpCallTable)、协议信息,以及一个用于返回下层调用表的指针。LSP要做的事是“截胡”这个返回过程,把下层表替换成自己的函数表,但保留真正的下层函数指针,之后自己的转发函数再调用真实下层完成最终收发。
#include <winsock2.h> #include <ws2spi.h> #include <windows.h> #pragma comment(lib, "ws2_32.lib") static WSPUPCALLTABLE g_UpCallTable; static LPWSAPROTOCOL_INFO g_ProviderInfo; static WSPPROC_TABLE g_TrueProcTable; static WSPPROC_TABLE g_NextProcTable; static int WINAPI MyWSPRecv( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCbKey) { int ret = g_NextProcTable.lpWSPRecv( s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpCbKey); return ret; } int WSPAPI WSPStartup( WORD wVersion, WORD wHighVersion, LPWSPUPCALLTABLE lpUpCallTable, LPWSPPROTOCOL_INFO lpProtocolInfo, WSPPROC_TABLE lpProcTable, LPWSPPROC_TABLE lpNextProcTable) { // 保存上层回调表与协议信息 g_UpCallTable = *lpUpCallTable; g_ProviderInfo = lpProtocolInfo; // 保存系统给的真实下层函数表 g_TrueProcTable = *lpNextProcTable; g_NextProcTable = *lpNextProcTable; // 替换自己需要的几个函数 g_NextProcTable.lpWSPRecv = MyWSPRecv; // 把替换后的表写回,让上层调用者走我们的逻辑 *lpProcTable = g_NextProcTable; return 0; } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { if (ul_reason_for_call == DLL_PROCESS_ATTACH) { // LSP会被系统加载到每个使用Winsock的进程,这里不做重型初始化 DisableThreadLibraryCalls(hModule); } return TRUE; }这段代码的逻辑很直白:先把系统传进来的真实下层表保存到g_NextProcTable,然后把其中的lpWSPRecv换成自己的MyWSPRecv,最后把替换后的整张表返回给系统。这样进程里后续的recv操作会先进入MyWSPRecv,再在函数体内调用原始的下层实现。
这里有几个参数值得单独说。wVersion和wHighVersion是Winsock的版本协商,LSP一般直接接受,不主动拒绝。lpUpCallTable是上层应用回调表,用于异步完成通知,大多数情况只保存不修改。lpProtocolInfo则是当前这条链对应的WSAPROTOCOL_INFO,如果LSP安装在多条协议链上,不同socket可能走不同条目,建议把感兴趣的协议ID打印出来。最后lpNextProcTable才是真正要保留的“真实下层函数表”。注意结构体内部成员很多,不要整表拷贝后再去修改个别成员,修改完务必保证其他成员的原值不被破坏。
3.3 需要用到的关键数据结构
WSPPROC_TABLE是一张函数指针表,定义了LSP能替换的全部SPI函数。现实中不需要全部替换,只要替换和业务相关的几个,比如lpWSPRecv、lpWSPSend、lpWSPCloseSocket。其余的保持原值即可。以下表格列出最常用的几个成员,方便对照:
| 成员名 | 对应SPI函数 | 说明 |
|---|---|---|
| lpWSPRecv | WSPRecv | 接收数据,主要劫持点 |
| lpWSPSend | WSPSend | 发送数据,主要劫持点 |
| lpWSPConnect | WSPConnect | 拦截连接动作,可做目标地址过滤 |
| lpWSPCloseSocket | WSPCloseSocket | 拦截关闭动作,可清理上下文 |
| lpWSPEventSelect | WSPEventSelect | 事件选择,必要时可跳过 |
这段代码可以编译成一个最小DLL,完成“加载但不干坏事”的验证。真正的数据拦截逻辑还需要在MyWSPRecv里补充,这就是下一章的内容。
4. 实现一个能记录通信数据的LSP:recv/send转发与日志落地
4.1 在WSPRecv里拿到数据并写出日志
当我们把g_NextProcTable.lpWSPRecv替换成自己的函数之后,进程里所有socket的接收数据都会经过MyWSPRecv。此时我们要做的第一件事是先调用真实下层函数,让系统真正去收数据,然后对收到的缓冲区进行检查和记录。注意WSPRecv的缓冲区管理机制:lpBuffers可能一次对应多个WSABUF,每个WSABUF有自己的长度和指针,必须全部遍历。
static void WriteLog(const char* tag, SOCKET s, WSABUF* buffers, DWORD count, DWORD totalLen) { FILE* fp = fopen("C:\\Temp\\LspLog.txt", "a"); if (!fp) return; SYSTEMTIME st; GetLocalTime(&st); fprintf(fp, "[%02d:%02d:%02d.%03d] %s socket=%llu len=%lu\n", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, tag, (unsigned long long)s, (unsigned long)totalLen); for (DWORD i = 0; i < count; ++i) { if (buffers[i].buf && buffers[i].len > 0) { // 只记录前256字节,避免日志膨胀 DWORD n = buffers[i].len > 256 ? 256 : buffers[i].len; for (DWORD j = 0; j < n; ++j) { fprintf(fp, "%02X ", (unsigned char)buffers[i].buf[j]); if (j % 16 == 15) fprintf(fp, "\n"); } fprintf(fp, "\n"); } } fclose(fp); } static int WINAPI MyWSPRecv( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCbKey) { // 先调用真实下层函数,完成数据接收 int ret = g_NextProcTable.lpWSPRecv( s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpCbKey); // 接收成功且有数据时,记录日志 if (ret == 0 && lpNumberOfBytesRecvd && *lpNumberOfBytesRecvd > 0) WriteLog("RECV", s, lpBuffers, dwBufferCount, *lpNumberOfBytesRecvd); return ret; }关于这段代码的调用时机需要特别注意:必须先把参数完整转交给真下层函数,再读取缓冲区内容。因为有些协议栈实现会在调用后填充缓冲区,我们的日志代码读到的才是实际收下来的数据。如果先读缓冲区再调下层,可能拿到的是上一次的数据或未初始化的内存。对于重叠IO(Overlapped IO)的情况,WSPRecv可能返回WSA_IO_PENDING,数据要等完成例程或事件通知后才有效,此时lpNumberOfBytesRecvd指向的值没有实际意义,直接记录会导致日志里出现大量长度为0的噪声。处理办法是遇到WSA_IO_PENDING就不记录,或者在完成回调里补一条记录。
4.2 实现对发送数据的拦截:WSPSend
接收是下半场,发送是上半场。如果要分析客户端上传了什么、或者做内容过滤,必须在WSPSend里动手。逻辑和接收一样:先调真实下层发送函数,再把要发的数据写进日志。发送日志放在调用之后有一个好处,就是只要下层函数成功返回,说明数据已经交给协议栈,记录下来的内容是真实发出的数据。
static int WINAPI MyWSPSend( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCbKey) { int ret = g_NextProcTable.lpWSPSend( s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, lpFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpCbKey); if (ret == 0 && lpNumberOfBytesSent && *lpNumberOfBytesSent > 0) WriteLog("SEND", s, lpBuffers, dwBufferCount, *lpNumberOfBytesSent); return ret; }和WSPRecv唯一的差别是文件标签从“RECV”换成了“SEND”,函数签名里的完成通知成员保持一致。调完真实下层后,如果返回值是SOCKET_ERROR且错误码是WSA_IO_PENDING,同样不能记录,因为数据还没真正发出。这种做法在非阻塞模式下比较常见,也是数据记录容易漏掉的一环。想要处理阻塞和非阻塞两种模式,可以把记录动作移到WSPEventSelect的FD_WRITE/FD_READ通知里,但工程复杂度会上升不少,实验阶段用同步判断就够了。
4.3 编译完成后如何把LSP装进系统目录
写完了DLL只是第一步,得把这个LSP挂载到Winsock Catalog里才能真正生效。挂载动作可以用一个独立的安装器完成,核心是调用WSCInstallProvider和WSCWriteProviderOrder。这两个API在ws2spi.h里声明,需要链接ws2_32.lib。安装过程本质上是在注册表里创建一个新的分层provider条目,然后把它插到TCP/IP等基础provider的前面。
#include <winsock2.h> #include <ws2spi.h> #include <stdio.h> #pragma comment(lib, "ws2_32.lib") int main() { // 先找到TCP/IP基础provider的目录ID DWORD dwSize = 0; WSAPROTOCOL_INFO protocols[32]; int count = WSCEnumProtocols(NULL, protocols, &dwSize, &error); if (count == SOCKET_ERROR) { printf("enum failed\n"); return -1; } // 挑一个AF_INET流式套接字的基础provider,比如TCP/IP int baseCatalog = -1; for (int i = 0; i < count; ++i) { if (protocols[i].iAddressFamily == AF_INET && protocols[i].iSocketType == SOCK_STREAM && protocols[i].iProtocol == IPPROTO_TCP) { baseCatalog = protocols[i].dwCatalogEntryId; break; } } if (baseCatalog == -1) { printf("no tcp provider\n"); return -1; } // 构造分层provider的描述,链结构指向基础提供程序 WSAPROTOCOL_INFO info = { 0 }; info.dwServiceFlags1 = XP1_IFS_HANDLES | XP1_MESSAGE_ORIENTED | XP1_SEQUENCED_PACKET; info.iAddressFamily = AF_INET; info.iSocketType = SOCK_STREAM; info.iProtocol = IPPROTO_TCP; info.dwCatalogEntryId = 0; // 安装时由系统分配 info.ProtocolChain.ChainLen = 2; info.ProtocolChain.ChainEntries[0] = 0; // 系统安装后回填本provider的id info.ProtocolChain.ChainEntries[1] = baseCatalog; // 下一层是基础TCP/IP // 安装DLL提供的LSP int error = 0; if (WSCInstallProvider(&info, L"C:\\Path\\LspSandbox.dll", NULL, &error) == SOCKET_ERROR) { printf("install failed: %d\n", error); return -1; } printf("install ok\n"); return 0; }这段代码的逻辑是把LSP挂到TCP流式协议上,链长是2,表示一层LSP加一层基础provider。WSCInstallProvider的第一个参数需要传入一个WSAPROTOCOL_INFO,其中ProtocolChain描述了我们这条链的组成。安装成功后系统会分配新的目录项ID,并把这个LSP条目注册进Catalog,但不会自动把它放到所有用TCP的应用前面。
要让LSP真正排在前面,还需要调用WSCWriteProviderOrder调整顺序,把刚安装的provider的CatalogEntryId放到TCP/IP条目前面。这一步常常被初学者忽略,导致DLL装好了却不生效——系统里存在这条链,但应用默认选用的还是最原始的TCP/IP provider。顺序调整的代码不复杂,就是枚举所有provider,排序后传入一个目录ID数组。调整完需要重启使用Winsock的进程才生效。另外注意,写安装器时要在64位和32位两种环境下分别做,否则会出现“能装上但有些程序不走链”的诡异现象。
5. LSP注入的4个经典坑:现象、原因与解决
5.1 装上了但进程完全不加载DLL
这是最常见的翻车现场。现象是安装器执行成功,日志文件也没有任何报错,但目标进程里就是看不到DLL被加载。用进程查看器核对,加载模块列表里根本没有你的LspSandbox.dll。原因往往是安装器里只调用了WSCInstallProvider,没有调用WSCWriteProviderOrder。Catalog里确实出现了新条目,但它的位置在所有基础provider后面,进程初始化Winsock时选择了最常用的基础provider,根本不会走到LSP。
解决方法是补上顺序调整逻辑,把LSP的目录ID通过WSCWriteProviderOrder排到最前面。然后在命令行执行netsh winsock show catalog,确认分层provider条目下面确实链接了TCP/IP基础条目。这个命令输出里可以看到Catalog的完整排列,非常直观。调整完记得重启浏览器或要监控的目标程序。
5.2 卸载LSP后机器网络异常
这是最让人心悸的坑。现象是卸载DLL或删除注册表条目后,系统出现浏览器打不开网页、网络连接显示正常但数据传输停滞等大面积异常。原因是Winsock Catalog被写坏了,系统里所有依赖Winsock的应用都在初始化时失败,而Windows自身很多服务也依赖Winsock。LSP的卸载不是简单地删除DLL文件,必须用WSCDeinstallProvider按目录ID移除条目,否则链的指向会悬空。
如果已经发生网络异常,最可靠的处理是运行命令netsh winsock reset,这个命令会把Winsock Catalog重置为系统初始状态,清掉所有第三方协议提供程序。注意它会同时重置所有网络相关组件,执行后需要重启系统。这个命令本身是Windows自带的安全恢复手段,不是本文方案的扩展,用来给LSP开发兜底很合适。经验是改Catalog前先做一个注册表导出备份,出问题时导入回去能省很多事。
5.3 64位系统下32位程序不走LSP
现象比较隐蔽:64位进程能正常记录到日志,但同一个系统里运行一个32位的老程序,它的流量完全看不到。这不能在代码里修,而是Windows的Winsock Catalog本身就有两套。64位进程读的是原生注册表项,32位进程读的是Wow6432Node下的对应项。如果安装器是在64位模式下编译并运行的,那么它只会往64位Catalog里写,32位进程自然不受影响。
解决方法是把安装器做成两套,或者用WOW64重定向机制在安装时分别写入两个Catalog。最简单可靠的做法是编译一版x86的安装器一版x64的安装器,分别运行一次。DLL文件也分别输出到两个路径,因为32位进程无法加载64位DLL,反过来也是一样。至于做LSP的DLL本身,必须编译两套:x86版给32位进程用,x64版给64位进程用,安装时对应写入。这个坑如果不提前防,往往要排查几个小时才能定位到架构差异上。
5.4 日志里同一进程数据重复记录
现象是往浏览器里访问一次页面,日志文件却出现了两条相同的“SEND”记录。初看以为是自己的函数被递归调用了,仔细排查才发现不是代码的问题,而是Winsock Catalog里同一个LSP被挂载到了多条链上。比如你的LSP同时挂进了TCP/IP的IPv4链和IPv6链,而应用创建了一个双栈socket,底层实际走了两条链,接收时两条LSP条目都执行了一次。
另一种可能是安装时重复执行了多次安装器,Catalog里留下了多个相同DLL的条目。解决方法是先枚举当前Catalog里所有同名产品的provider条目,把重复项卸载掉;再检查应用创建的socket类型,针对双栈情况设置IPV6_V6ONLY。日志里加一行socket句柄和进程ID的输出会大幅提升排查效率。以前我还见过一次“每个包记录两次”的翻车,最后定位到是系统里残留了另一个团队安装的同名LSP,两个LSP串在一起,同一个包被两层先后记录,所以写日志时一定要带上provider的目录ID或进程ID,方便日后续排查。
提示:LSP开发中遇到任何“看似正常但行为不对”的情况,先查Catalog,先看日志,先确认架构,再怀疑自己的转发逻辑。这条排查顺序能省掉大半晚上的时间。
6. 进阶验证与进程级定向监控技巧
先给一套能快速验证LSP是否生效的自检方法。在DLL里写一个独立的初始化标志文件,比如WSPStartup被调用时在C:\Temp下创建LspLoaded_<进程ID>.txt,然后启动记事本、浏览器这类一定会用到socket的进程,观察目录里是否出现对应文件。加上时间戳后,还能看出进程启动后多久被注入。如果目标程序迟迟没有创建文件,先用netsh winsock show catalog确认排列顺序,再用Process Explorer或任务管理器查看目标进程的已加载模块列表,确认DLL是否在列表中。
LSP装好后是全进程生效的,但很多场景下我们只关心某个特定程序的行为,其余进程的记录会撑爆日志文件。优化办法是在WSPStartup入口处判断调用者进程名,不是目标进程就直接把原始调用表原样返回,不做替换。Windows下获取当前进程名有多种方式,推荐用GetModuleFileNameEx查询进程句柄,或者用更底层的NtQueryInformationProcess取PEB里的ImagePath,前者写法更标准,后者更快。
#include <psapi.h> #pragma comment(lib, "psapi.lib") static bool IsTargetProcess() { // 当前进程的模块文件名,仅提取名称部分 wchar_t path[MAX_PATH] = { 0 }; DWORD pid = GetCurrentProcessId(); HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); if (!hProc) return false; DWORD size = MAX_PATH; if (QueryFullProcessImageNameW(hProc, 0, path, &size) == 0) { CloseHandle(hProc); return false; } CloseHandle(hProc); // 只要目标程序的exe名,这里以trafficTool.exe为例 wchar_t* baseName = wcsrchr(path, L'\\'); if (!baseName) return false; return _wcsicmp(baseName + 1, L"trafficTool.exe") == 0; } int WSPAPI WSPStartup(...) { // 非目标进程直接返回原表,不劫持 if (!IsTargetProcess()) { *lpProcTable = *lpNextProcTable; return 0; } // 目标进程才做替换逻辑 g_NextProcTable = *lpNextProcTable; g_NextProcTable.lpWSPRecv = MyWSPRecv; *lpProcTable = g_NextProcTable; return 0; }这段代码的思路很清晰:先确认当前进程是否为目标exe,不是就直接把系统给的原始表返回,劫持逻辑完全不加载。这样日志文件只会有目标进程的记录,系统其它进程的属性、崩溃率、性能开销全部归零。wcsrchr用于截取最后一个反斜杠后的文件名,注意如果路径中带有反斜杠序列,C字符串写法需要写成双反斜杠。
最后说一条实战教训:LSP不是一个可以随意在开发机上反复测试的东西,它活在系统全局网络栈里,一次错误的Catalog写入就可能让整机“断网”。我给自己定下的规矩是所有的安装测试都在虚拟机里做,测试前打一个系统还原点,当作后悔药,出问题可以秒回滚。真机上从未直接装过第一版DLL,这习惯帮我躲过至少三次差点搞坏宿主机的险情。希望这篇文章的代码和排查路径能帮你避开这些坑,顺利把第一个能记录数据的LSP跑起来。
本文还有配套的精品资源,点击获取