简介:lsp注入(原理及其实现代码)是一套关于LSP技术的C++源码资源,面向希望深入理解Windows COM组件与底层钩子机制的开发者,尤其适合研究文件操作拦截、进程注入及安全检测的人群。资料从LSP组件创建、注册表配置到运行时注入与请求拦截,系统梳理了原理流程,并给出可编译的工程实现。包体为7z压缩,共38个文件,约150KB;其中以15个cpp源码、7个头文件为主,辅以sln/vcxproj工程文件、makefile、def与dll,另有bat脚本可用于LSP的注册与卸载,txt说明便于快速上手。压缩包小但结构完整,适合直接阅读或二次修改。目前已被1567人浏览学习,是LSP底层机制入门中少见的带完整代码的参考资料。通过阅读源码与脚本,读者可以掌握COM接口实现、系统注册表操作、注入API调用等关键技能,同时了解其安全风险与防御思路,为后续开发调试工具或安全检测模块打下基础。
1. LSP注入到底是什么:一张必须接住的下层服务链
做网络流量分析的同学,迟早会碰到一个绕不开的名词:LSP注入。它和你熟悉的 Hook ws2_32.dll 思路完全不同——LSP 是 Winsock 2 SPI 体系里一个正经的“层”,系统在创建套接字时就会把它加载成协议链中的一环,不用猜进程内部的函数地址,也不依赖注入时机,只要网络活动经由 Winsock,你的代码就天然被执行。它的价值很直接:在应用层和真正的 TCP/IP 协议栈之间卡一个位置,做流量记录、内容过滤、上行下行审计。适合 Windows 平台上做安全审计、网络分析、协议实验的开发者;想做生产级流量过滤的人会优先用 WFP,但理解 LSP 仍然是搞懂 Windows 网络栈的分水岭。
2. 原理先行:Winsock 2 SPI 的分层模型与 LSP 挂载位置
2.1 从 WSAStartup 到底层:数据包究竟经过了几道门
一个 Winsock 应用调用socket()、send()、recv()时,代码表面上是打进 ws2_32.dll 的导出函数,但 ws2_32 只是分发层。真正干活的是 Winsock 2 的服务提供程序接口,也就是 SPI(Service Provider Interface)层。SPI 层里有两类参与者:一类是传输服务提供者(Transport Provider),比如 TCP/IP 栈自带的 msafd.dll,它负责把数据交给内核协议栈;另一类是分层服务提供者(Layered Service Provider),也就是本文的主角 LSP,它不直接面对硬件,而是挂在传输提供者上面,专门截取上层应用发起的 SPI 调用。
系统把所有提供者按顺序编成一条目录链,称为 Winsock Catalog。应用每次创建套接字,ws2_32 会顺着这条链依次调用各层提供者的入口,直到最底层的传输提供者。LSP 的挂载位置决定了它的性质:只要链不断,所有经 Winsock 的流量都必须穿过它,这是它和 API Hook 最本质的差别。API Hook 拦截的是 ws2_32 的导出层,LSP 拦截的是导出层之下的 SPI 派遣层,位置更深,也更容易做透明的双向修改。
2.2 入口契约:WSPStartup 与 WSPProc 函数表
每个 LSP 的 DLL 只需导出一个核心函数:WSPStartup。系统加载 LSP 时调用它,传给它一个LPWSAPROTOCOL_INFOW描述当前协议条目,以及一张上行回调表,同时要求它返回一张WSPPROC_TABLE函数表。这张表就是你的全部工作台面,里面罗列了 30 多个 SPI 函数指针,从WSPSend、WSPRecv、WSPConnect到WSPAccept、WSPCloseSocket都在其中。
关键规则是:LSP 不是一个独立的终点,它必须把未处理的服务委托给链上下一环。所以WSPStartup内部的常规做法是先定位下层 provider,调用下层的WSPStartup拿到它的函数表,再把自己要钩的函数替换进当前表,其余原样透传。只要有一环断了,比如你没有正确加载下层 provider,套接字创建就会直接失败,表现成各种各样的玄学报错。
2.3 为什么选 LSP 而不是直接 Hook ws2_32.dll
每个做流量拦截的人都会先试 inline Hook,把send、recv的跳板指令改掉,简单直接。但它的短板很明显:同一个进程里多个模块都可能对 ws2_32 动手,跳板互相覆盖;目标程序自身的安全校验也会检测函数头部是否被改动;而且 Hook 时机不好控制,程序可能在你注入前就已经完成了初始化。LSP 的绕行方式更取巧——它根本不改任何进程内的代码,只是在系统 Catalog 里多注册一个合法节点,由 ws2_32 在分发时主动调进来。
两者的取舍我在实际项目里是这样看的:
| 对比项 | LSP | inline Hook ws2_32 |
|---|---|---|
| 注入时机 | 进程调用 Winsock 时由系统加载 | 需要外部线程注入 |
| 隐蔽性 | 在系统目录链中可见,属于正规机制 | 修改内存代码,容易被校验发现 |
| 拦截范围 | 所有使用 Winsock 的进程 | 仅被注入的进程 |
| 实现成本 | 中,需处理 SPI 函数表 | 低,但要处理反 Hook |
| 系统兼容风险 | 安装后影响全局,卸载需干净 | 影响局部,崩溃面小 |
单纯做单进程实验,Hook 够用;做全局流量审计或协议改造,LSP 的架构更干净。这篇文章后面的代码骨架就按 LSP 路径展开。
3. 最小可跑骨架:WSPStartup 链式绑定与拦截/透传代码
3.1 导出声明:DLL 里只有一个入口
LSP 的 DLL 不需要导出几十个函数,只要一个WSPStartup能被 GetProcAddress 找到即可。注意 C++ 环境下要做 extern "C" 修饰,否则符号被改名后系统找不到入口。下面是最小的导出声明与全局状态:
// lsp_dll.h #pragma once #include <winsock2.h> #include <ws2spi.h> #include <ws2tcpip.h> // 保存真正下层 provider 的入口表与协议描述 static WSPPROC_TABLE g_nextProcTable; static WSAPROTOCOL_INFOW g_nextProtocolInfo; static WSPUPCALLTABLE g_upcallTable;g_nextProcTable是你给上层交付的透传目标,所有没打算拦截的 SPI 函数都从这里抄进自己的表;g_upcallTable是系统给 LSP 的上行回调表,一般原样透传给下层即可。这两份状态在进程生命周期内保持有效,DLL 被加载时会绑定到具体的 Winsock 目录链条目。
WSPStartup 的签名是系统写死的,参数顺序不能错,少了任何一个字段都会导致编译期不报错、运行期内存错乱。声明与实现如下:
// lsp_dll.cpp extern "C" __declspec(dllexport) int WSPAPI WSPStartup( WORD wVersionRequested, LPWSPDATA lpWSPData, LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE upcallTable, LPWSPPROC_TABLE lpProcTable) { // 绑定上表,后续透传要用 g_upcallTable = upcallTable; // 1. 找到链上的下层 provider // 2. 加载它的 DLL // 3. 把请求转发给它,拿它的 proc table // 4. 组装自己的表 }wVersionRequested是上层要求的 Winsock 版本,通常是 2.2;lpWSPData由系统分配,你的 DLL 要回填服务描述;lpProtocolInfo描述当前这个 catalog 条目是哪个协议、哪条链;lpProcTable是系统等你的输出的,填好它才算注册成功。这里别做任何耗时操作,系统在进程首次使用 Winsock 的路径上等你,拖太久会拖慢所有应用的网络初始化。
3.2 链式绑定:从系统 Catalog 里找到你的下一环
LSP 开发里最容易翻车的一步就是“向下找”。你不能写死下层 DLL 的文件路径,因为系统使用者的 TCP/IP 栈实现可能不同;正规做法是用WSCEnumProtocols枚举当前 Catalog,找到一个和你相邻的、位于你之后的 BASE_PROTOCOL 条目,再用WSCGetProviderPath取出它的 DLL 路径。
#include <winsock2.h> #include <ws2spi.h> int find_next_provider( LPWSAPROTOCOL_INFOW lpProtocolInfo, LPWSAPROTOCOL_INFOW lpNextInfo, INT* lpErrno) { WSAPROTOCOL_INFOW protocols[32] = {0}; DWORD dwSize = sizeof(protocols); INT n = WSCEnumProtocols(NULL, protocols, &dwSize, lpErrno); if (n == SOCKET_ERROR) return SOCKET_ERROR; // 记录当前自己的 catalog entry id DWORD myId = lpProtocolInfo->dwCatalogEntryId; // 枚举结果按 catalog 顺序排列,找自己后面的第一个 BASE_PROTOCOL for (INT i = 0; i < n; i++) { if (protocols[i].dwCatalogEntryId == myId) { // 从 i+1 开始找第一个非分层协议 for (INT j = i + 1; j < n; j++) { if (protocols[j].ProtocolChain.ChainLen == BASE_PROTOCOL) { *lpNextInfo = protocols[j]; return 0; } } break; } } return SOCKET_ERROR; }这段逻辑的核心是让你的 LSP 与具体协议实现解耦。WSCEnumProtocols返回的数组顺序就是 Catalog 里的加载顺序,DLL 所在位置决定了“下一个”是谁。找 BASE_PROTOCOL 而不是 LAYERED_PROTOCOL 是为了落到真正的传输层,否则你可能又指回另一个 LSP,造成循环加载。拿到 ProtocolInfo 后继续做三层操作:WSCGetProviderPath 取路径、LoadLibrary 加载、GetProcAddress 拿到下层 WSPStartup。
wchar_t dllPath[MAX_PATH] = {0}; DWORD pathLen = MAX_PATH; INT err = 0; WSCGetProviderPath(&nextInfo.ProviderId, dllPath, &pathLen, &err); HMODULE hNext = LoadLibraryW(dllPath); if (!hNext) return SOCKET_ERROR; LPWSPSTARTUP pfnNextStartup = (LPWSPSTARTUP)GetProcAddress(hNext, "WSPStartup"); if (!pfnNextStartup) return SOCKET_ERROR; // 关键动作:把请求转发给下层,拿它的函数表 int ret = pfnNextStartup( wVersionRequested, lpWSPData, &nextInfo, // 注意:传下层的 protocol info upcallTable, &g_nextProcTable // 下层回填自己的函数表 );这个“转交”动作是 LSP 的生命线。很多人图省事,先直接返回成功,再自己填一张表——结果程序能编译能运行,但任何套接字操作都不通。原因很简单:你没把链条交到真正的 TCP/IP 提供者手里,上层拿到的是一张没有底座的空头支票。转交时把ws2_32传给自己的lpWSPData原样传给下层即可,它会帮系统建立完整的 SPI 会话。
3.3 拦截与透传:钩住 WSPSend / WSPRecv 的核心代码
拿到下层的g_nextProcTable后,组装你向上返回的lpProcTable。核心策略是:要拦截的成员替换成你自己的实现,其余成员原样从上表抄下来。下面给一个 WSPSend 与 WSPRecv 的简化实现骨架:
int WSPAPI HookedWSPSend( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno) { // 这里做你想要的过滤/记录操作 // 例如将发送内容写到日志文件或控制台输出 // 必须交给下层处理,否则数据永远到不了对端 return g_nextProcTable.lpWSPSend( s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno); } int WSPAPI HookedWSPRecv( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno) { int ret = g_nextProcTable.lpWSPRecv( s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno); // recv 返回后,可以检查 lpBuffers 中的数据 // 注意:返回值和 lpNumberOfBytesRecvd 指的是收到真实数据 // 如果你改了内容,务必同步这些值 return ret; }代码里出现的lpErrno是 SPI 层的错误传递通道。它不同于普通函数的 errno——下层返回错误码后,LSP 必须原样传递这个指针给下层,不能自己创建一个局部变量替代,否则错误码会丢失,上层拿到的是 0,完全感知不到失败。同时注意LPWSABUF是应用传入的缓冲区数组,直接改它的内容就是改应用即将收到的数据,这是做内容过滤的基础。
接着在WSPStartup里把这两钩子装进表:
// 组装返回的表:钩住 send/recv,其余透传 WSPPROC_TABLE procTable = {0}; // send / recv 走我们的实现 procTable.lpWSPSend = HookedWSPSend; procTable.lpWSPRecv = HookedWSPRecv; // 其余函数直接抄下层 procTable.lpWSPAccept = g_nextProcTable.lpWSPAccept; procTable.lpWSPConnect = g_nextProcTable.lpWSPConnect; procTable.lpWSPCloseSocket = g_nextProcTable.lpWSPCloseSocket; procTable.lpWSPShutdown = g_nextProcTable.lpWSPShutdown; procTable.lpWSPIoctl = g_nextProcTable.lpWSPIoctl; procTable.lpWSPBind = g_nextProcTable.lpWSPBind; procTable.lpWSPListen = g_nextProcTable.lpWSPListen; // ... 其余成员按同样规则复制 *lpProcTable = procTable;这里的组装逻辑用加减法就能理解:你向上交付的表,一半是对方给你的真引用,一半是你自己的实现。不要试图把整个WSPPROC_TABLE都换成自己的——里面每个成员的类型、调用约定、返回值语义都不一样,全部重写的工作量等同于重写一个协议栈。只钩你关心的点,是本末不倒置的稳妥路线。
3.4 编译注意:三个最常见的构建环境问题
第一,必须包含<ws2spi.h>和<ws2tcpip.h>,且先包含<winsock2.h>,不能和<windows.h>混用后引发宏冲突。第二,工程类型要选 DLL,并确保WSPStartup在导出表中可见,检查生成的 .lib 文件或使用dumpbin /exports验证。第三,32 位和 64 位 DLL 要分别编译,稍后第 5 章会讲为什么。
dumpbin /exports my_lsp.dll # 输出中应看到: # ordinal hint RVA name # 1 0 00001000 WSPStartup看不到WSPStartup就回去检查extern "C"。另外因为WSPStartup是__cdecl(用WSPAPI宏加上类似 stdcall 的调用约束),导出表的名称就是WSPStartup而不是带下划线前缀的_WSPStartup;如果导出了带前缀的版本,通常是工程属性里调用约定设置不对。
4. 让系统认识你的 LSP:WSCInstallProvider 与 Winsock Catalog 读写
4.1 Winsock Catalog:系统是怎么记录分层服务的
Winsock Catalog 的实体保存在注册表的HKLM\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\Protocol_Catalog9节点下。每一个 provider 是一个子键,里面有 ProviderId、DllPath、ProtocolChain 等字段。用户态来看,最直观的查看方式是命令:
netsh winsock show catalog这条命令会列出所有 transport provider 与 layered service provider,并显示它们各自的 Catalog Entry ID、协议链长度和 DLL 路径。调试 LSP 时我会先跑一次它,确认当前系统活着哪些链,安装自己的条目前后各跑一次做对比,立刻能看出有没有写进 Catalog、排序在什么位置。
这里有个容易忽略的细节:64 位 Windows 上同时存在两套 Catalog,一套属于 64 位进程视角,另一套在注册表Wow6432Node路径下属于 32 位进程。想让 32 位和 64 位应用都被拦截,必须分别注册两份不同位数的 DLL。很多人只装了一个位数的 LSP,测试某类程序好用,换一个位数就失效,原因就在这里。
4.2 用 WSCInstallProvider 完成注册
安装 LSP 的官方 API 是WSCInstallProvider。它接收一个 GUID 作为 provider 的唯一标识、DLL 的绝对路径、一条或多条协议描述数组。下面的代码演示了一个最小注册过程:
#include <winsock2.h> #include <ws2spi.h> #include <objbase.h> // CoCreateGuid INT install_lsp(const wchar_t* dllPath) { INT err = 0; GUID providerGuid; CoCreateGuid(&providerGuid); // 生成一次性 GUID,注意保存备用 WSAPROTOCOL_INFOW protoInfo = {0}; // 说明这是一条分层协议,不是基础协议 protoInfo.ProtocolChain.ChainLen = LAYERED_PROTOCOL; // 值为 1 // 隐藏该协议条目,避免在协议列表中暴露 protoInfo.dwProviderFlags = PFL_HIDDEN; protoInfo.dwServiceFlags1 = XP1_IFS_HANDLES; protoInfo.iAddressFamily = AF_INET; protoInfo.iSocketType = SOCK_STREAM; protoInfo.iProtocol = IPPROTO_TCP; // 分类说明,给上层程序展示用 wcscpy(protoInfo.szProtocol, L"Demo LSP"); INT ret = WSCInstallProvider( &providerGuid, dllPath, &protoInfo, 1, // 只注册一条条目 &err); if (ret == SOCKET_ERROR) { // 处理 err return -1; } return 0; }参数里有两处新手最容易填错。ChainLen = LAYERED_PROTOCOL表示这是一层“加挂”的滤波层,填成BASE_PROTOCOL会要求系统把它当独立协议栈初始化,你根本没有实现底层协议,必然失败。dwServiceFlags1的XP1_IFS_HANDLES是性能开关:设置了 LSP 可以走系统快速路径,减少每次 IO 的调度开销;不设也能工作但性能差一截,且部分新版本系统会把它当作必填项校验。注册完成后最好到netsh winsock show catalog里验证条目确实在链上且显示Layered。
4.3 卸载与顺序管理:后悔药要提前备好
卸载 LSP 用WSCDeinstallProvider,只需要传入安装时用的 GUID。这个 GUID 一定要存好,忘了就只能扫注册表按 DLL 路径反查:
INT universal_err = 0; INT ret = WSCDeinstallProvider(&providerGuid, &universal_err); if (ret != SOCKET_ERROR) { // 成功,链上已经移除该层 }移除后,原先依赖该 LSP 的进程需要重新调用 Winsock 才会重新获取 Catalog,已经建立连接的进程不受影响。如果装完发现系统网络异常,又找不到自己的 GUID,最简单的兜底办法是执行:
netsh winsock reset这条命令会把 Catalog 恢复成系统默认状态,所有第三方 LSP 都被清掉,代价是系统里其他软件注册的 LSP 也一起没了。所以调试期内我推荐先在自己实验机装,别直接在主力开发机上折腾。顺序调整用WSCWriteProviderOrder可以重新排列 provider 顺序,但多数场景用不到,知道有这个东西即可。
5. 避坑:LSP 开发里最常见的五个翻车现场
5.1 安装了 LSP 之后浏览器全部打不开网页
现象:Chrome、Edge 一律白屏,报ERR_EMPTY_RESPONSE;命令行 ping 正常,但任何走 Winsock 的应用都连不上网。
原因:这是最典型的“断链”事故。检查代码会发现WSPStartup里没有把WSPConnect透传给下层——你返回给上层的函数表里lpWSPConnect是空的,或者只填了 send/recv 却忘了 connect。TCP 建连过程首先调 connect,connect 断档后整个会话建立不了,web 请求当然发不出去。
解决:先把表中的lpWSPConnect、lpWSPBind、lpWSPListen全部透传,再逐步加拦截逻辑。改完后重新编译安装,执行netsh winsock reset再试。如果 reset 前系统已经黑屏级别断网,reset 是唯一的后悔药。
5.2 32 位程序正常,64 位程序完全不受影响
现象:自己用 64 位编译的测试程序能抓到数据,但客户反馈说某个老软件没被过滤;跑了一遍发现那个老软件是 32 位进程。
原因:64 位 Windows 上 Winsock Catalog 是双轨的,系统默认netsh winsock show catalog只看当前 shell 的位数视角。你只安装了 64 位 DLL,32 位进程初始化 Winsock 时走的是Wow6432Node下那一套 Catalog,根本不会加载你的代码。
解决:安装逻辑里同时调用WSCInstallProvider和WSCInstallProvider32(或者先装 64 位再用 32 位工具再跑一次相同的注册逻辑),分别指向两个位数的 DLL。测试时也分两次跑:用 64 位测试程序确认,再用 SysWOW64 下的 32 位 netsh 或 32 位测试程序重新验证一遍。
5.3 程序显示发送成功,但 Wireshark 看不到包
现象:目标程序调用 send 返回了正确的字节数,日志也记录了“发送成功”,但 Wireshark 里死活找不到这个连接的出站包。
原因:HookWSPSend里做了拦截,却没有把它转交给下层的lpWSPSend。返回值已经伪造给了上层,真实数据永远停在 DLL 里,被你的代码“黑匣子”式吞掉了。这个现象是 LSP 特有的,API Hook 一般不出现——因为 Hook 失败时错误会往上抛,LSP 这条链会让错误悄悄消失。
解决:在 send 钩子末尾加一行日志,确认g_nextProcTable.lpWSPSend是否被调用、返回值是多少;再在所有 return 路径上检查是否漏了透传分支。我的习惯是每个钩子函数开头先无条件调下层,拿到结果后再决定要不要改,而不是把下层调用放在最后。
5.4 安装时报错 10106 / WSAEINVAL,套接字创建直接失败
现象:安装阶段一切正常,但目标应用一执行WSASocket就返回WSAEINVAL,配合系统日志能看到 Winsock Catalog 校验失败。
原因:WSAPROTOCOL_INFOW里ProviderId或ProtocolChain.ChainLen填错。最常见的错误是把ChainLen填成自定义数值或在同一层里写了多条条目,系统无法判断链的合法性。另一个常见来源是安装了多个 LSP,链序被搞乱。
解决:先删除自己注册的 provider,重新查看 Catalog 默认状态;再检查代码里ChainLen是否按分层协议正确填写。如果链上还残留别人的 LSP,用WSCWriteProviderOrder或者官方工具理清顺序,保证自己的层在正确的上下级之间。
5.5 系统大版本更新或杀毒软件扫描后 LSP 消失
现象:Windows 更新后,netsh winsock show catalog里找不到自己的 LSP,或者杀毒软件报告“检测到 Winsock Provider 修改”并自动清理。
原因:LSP 产品没有数字签名的话,在 Windows 10 及以后很容易被 Windows Defender 当作异常修改移除;系统更新重构 Catalog 时也可能丢弃未签名条目。这是平台对 LSP 机制收紧的直接体现。
解决:发布前给 DLL 做代码签名证书;安装后写一个自检脚本定期查询 Catalog 中是否存在自己的条目,掉了就自动重装。而且从长远考虑,如果业务还处于选型阶段,尽量把新功能做在 WFP(Windows Filtering Platform)上,LSP 更适合学习理解和做轻量实验,生产级拦截的兼容性风险逐年上升。
6. 收尾验证:用最小测试程序确认钩子真在链上
写完代码、装好 DLL,第一件事不是冲进功能测试,而是验证“钩子到底上链了没有”。我会先准备一个 20 行不到的 Winsock 客户端,它只做一件事:连接回环地址并发送一个特征字符串。字符串内容固定,方便待会在日志里搜索。
// test_client.cpp #include <winsock2.h> #include <ws2tcpip.h> #pragma comment(lib, "ws2_32.lib") int main() { WSADATA wsa; WSAStartup(MAKEWORD(2,2), &wsa); SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(8888); inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); connect(s, (sockaddr*)&addr, sizeof(addr)); const char* payload = "LSP_HOOK_CHECK_001"; send(s, payload, (int)strlen(payload), 0); closesocket(s); WSACleanup(); return 0; }我一般在 LSP 的HookWSPSend里加一行fprintf(stderr, "[lsp] hook send: %s\n", lpBuffers[0].buf),然后跑这个客户端。运行后能在调试输出或日志文件里看到LSP_HOOK_CHECK_001就说明拦截点已经生效,另一头再用netsh winsock show catalog确认 DLL 路径与链顺序。如果客户端能跑通但日志里没有标志串,优先检查是不是位数没配对——这是我在现场踩过最多次的坑。
从那以后我每次改完 LSP 代码,发布前都强制走一遍四个动作:装 DLL、跑回环客户端、查日志、看 Catalog,四步全绿才敢交给测试机,并且卸载后必执行一次netsh winsock reset恢复现场。这套流程帮我挡掉了至少五次“把自己主力机搞断网”的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取