Winsock LSP注入与WSPStartup协议链挂接实践

发布时间:2026/11/4 16:38:07

Winsock LSP注入与WSPStartup协议链挂接实践 简介围绕 Windows 语言服务提供者LSP注入的原理与代码实现展开面向已具备 C/COM 基础、希望理解系统底层钩子机制与文件处理拦截流程的开发者与安全研究人员。内容从 LSP 的 COM 组件本质讲起串联组件创建、注册表注册、运行时注入以及请求拦截与转发等关键环节并给出 IDispatch、IUnknown 与 ILanguageService 等接口的实现思路。压缩包共 38 个文件约 150KB以 15 个 cpp 源文件和 7 个 h 头文件为主体配合 vcxproj/sln 工程配置、bat 注册脚本、dll 与 exe 等编译产物及 def 导出定义形成可对照阅读的工程样例。已有 1583 人学习。读者可据此梳理 LSP 注入从接口实现、注册卸载到注入代码的完整链路理解其在安全检测、数据恢复、调试工具等场景中的用法与边界同时注意底层操作可能带来的稳定性与合规风险适合作为原理学习与代码剖析的参考。1. 一个进程启动就自动多出个 DLLLSP 注入改的到底是什么有次排查一台机器的怪现象随便开个程序用进程查看工具一瞧连记事本里都挂着一个来路不明的模块。不是驱动也不是常见的启动项劫持而是 LSP 注入——把自己的 DLL 注册成 Winsock 分层服务提供者从此凡是用套接字的进程都会被ws2_32.dll自动拉起来。这套机制的正式名字是 Winsock 2 SPIlsp和lsp注入说的就是它协议链怎么排、WSPStartup怎么拿到下层函数表、三十多个WSPxxx怎么逐个转发、装完怎么排到协议目录最前面。适合两类人已经会写 DLL、想搞清楚 Windows 网络过滤层真实调用链的开发者以及接手了前人在服务器上装过一层东西没卸干净、现在网络时好时坏这种遗留问题的同学。下面按原理、核心代码、安装卸载、排查、二次开发的顺序往下拆。2. 原理拆解Winsock 2 SPI 协议链是怎么把 DLL 塞进每个进程的2.1 从 WSAStartup 到 WSPStartup一次 socket 调用到底走了几层很多人以为ws2_32.dll就是网络实现其实它更像一个调度器。真正的收发动作由服务提供者Service Provider完成ws2_32.dll只负责在中间查表、分发。这个表就是协议目录落盘在HKLM\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\Protocol_Catalog9下面。进程第一次调用WSAStartup时ws2_32.dll会做几件事读取协议目录把所有条目读成WSAPROTOCOL_INFOW数组对每个条目检查它的ProtocolChain如果ChainLen 1说明这是个分层链需要把链上每一层的 DLL 都加载进来加载方式是LoadLibrary加GetProcAddress(WSPStartup)然后按链的顺序从下往上调用各自的WSPStartup把下层的函数表一层层传上去。于是调用链变成这样应用代码 socket()/connect()/send() ↓ ws2_32.dll 查当前进程的 provider 表找到匹配的链 ↓ 你的 LSP 的 WSPConnect / WSPSend ... ← 代码跑在目标进程里 ↓ g_NextProcTable.lpWSPConnect下层 ↓ 基础提供者TCP/IP最终进内核注意一个容易搞混的点WSPStartup只会被调用一次在进程初始化阶段之后每次 socket 操作走的都是函数表里的指针。这意味着挂接逻辑必须放在WSPStartup里完成等业务代码跑起来再想改函数表就晚了。还有一层认知要摆正这个 DLL 是加载进目标进程的地址空间的它的权限就是宿主进程的权限。普通用户跑的程序LSP 代码也只有普通用户权限想让管理员进程也加载得由管理员身份的进程去触发WSAStartup。所以别指望它天然拥有系统级能力。2.2 协议链与协议目录WSAPROTOCOL_INFOW 里哪几个字段是命门一条目录记录就是一个WSAPROTOCOL_INFOW结构字段不少但真正决定行为的就那么几个字段作用改错的后果dwCatalogEntryId条目在目录里的唯一编号链里靠它引用编号对不上链断WSAStartup直接失败ProviderId提供者 GUID安装时由你生成和 DLL 里写的不一致卸载时找不到ProtocolChain.ChainLen链长1 表示基础协议≥2 表示分层写成 1 就退化成普通条目永远不会调你的WSPStartupProtocolChain.ChainEntries[]从上层到基础的编号序列顺序反了会把自己递归调死dwServiceFlags1支持的能力位如XP1_CONNECTIONLESS和基础协议不一致会导致某些程序建套接字失败iAddressFamily/iSocketType/iProtocol匹配维度只写AF_INET就拦不到 IPv6 流量szProtocol显示名在netsh winsock show catalog里能看到只是个名字但排查时全靠它认人链的结构是最容易翻车的地方。假设你把自己的条目编为 1000分层基础 TCP 条目是 1001那么一个两层的链长这样ChainLen 2 ChainEntries[0] 1000 ; 最上层也就是你的 LSP ChainEntries[1] 1001 ; 基础协议干活的那一层再往下套一层就变成[1000, 1002, 1001]转发时每层把自己的头摘掉再传给下一层。这个摘头动作在WSPStartup里完成后面代码会写。匹配规则也要清楚应用调socket(af, type, protocol)时ws2_32从目录按顺序找第一个iAddressFamily、iSocketType、iProtocol都匹配的条目。所以想让自己的链被选中光装进去不够还必须排到目录最前面。这一步是很多人第一次做 LSP 时漏掉的装完了、日志一条没有以为代码写错其实是顺序没调。2.3 注入时机DLL 被加载进进程的真实边界说注入其实有点误导它没有远程线程、没有CreateRemoteThread、没有 APC 排队纯粹是 DLL 被正常加载。加载时机就在目标进程第一次WSAStartup的时候由ws2_32.dll主动LoadLibrary。所以它的覆盖面取决于谁调了 WSAStartup而不是谁在运行。有几类流量它天然拦不到这点在评估方案时必须先想清楚走内核态网络栈的服务。某些 Web 服务用的是内核态的 HTTP 栈压根不经过用户态 WinsockLSP 代码一行都不会执行。自己实现协议栈、直接和设备驱动打交道的程序。进程内部把ws2_32.dll静态链接或用非标准方式加载的极端情况少见但遇到过。只做文件映射、不走套接字的本地通信。反过来覆盖面又很广浏览器、下载工具、IM、游戏客户端只要用了 Winsock全都会被加载。这也是为什么这东西装错了后果特别严重——不是某个程序坏是所有联网程序一起坏。我在第 5 章会专门写这块的排查。另外还有个执行顺序的坑如果宿主进程加载你的 DLL 时你的DllMain里又调了WSAStartup或者创建套接字就会触发再次加载形成递归。常见做法是在DllMain里只做最保守的初始化保存模块句柄、初始化临界区任何和网络相关的动作都推到WSPStartup里去做。3. 核心实现WSPStartup 拿到下层函数表再逐个挂接 WSPxxx3.1 工程骨架与位数配置一个 DLL 要出两份先看目录一个能跑的最小工程大概长这样lsp-audit/ ├── src/ │ ├── dllmain.c // 入口只存模块句柄 │ ├── wspstartup.c // 核心拿下层表、填自己的表 │ ├── hooks.c // 挂接的 WSPConnect / WSPSend / WSPRecv │ ├── forward.c // 其余 WSPxxx 的批量转发 │ ├── install.c // 安装、卸载、顺序调整 │ └── log.c // 落盘日志 ├── include/ │ └── lsp.h ├── lsp-audit.def // 导出 WSPStartup └── lsp-audit.vcxproj.def文件必须导出WSPStartup名字不能带修饰LIBRARY lsp-audit EXPORTS WSPStartup位数是第一个硬性要求。64 位系统上32 位进程和 64 位进程读的是两套独立的协议目录ws2_32也是两份。你只装 32 位版本那 64 位程序全都不受影响反之只装 64 位32 位程序全不受影响。所以工程里至少要出Win32和x64两个目标安装的时候两个都要装。这点在第 5 章还会展开属于最高频的装了没效果。链接依赖只有ws2_32.lib头文件是winsock2.h和ws2spi.h注意包含顺序winsock2.h必须在windows.h之前否则会撞上一堆重定义。3.2 WSPStartup四步拿到下层函数表这是整个项目的心脏。逻辑可以拆成四步确定下一层是谁、把下一层的 DLL 加载起来、构造一个摘掉自己的协议信息传下去、复制下层函数表并替换挂接点。// wspstartup.c #include winsock2.h #include ws2spi.h #include lsp.h WSPPROC_TABLE g_NextProcTable; // 下层的原始函数表转发时全靠它 WSPUPCALLTABLE g_UpCallTable; // 上层回调表拿下层路径要用 static DWORD g_NextProviderId; // 下一层的目录编号 int WSPAPI WSPStartup( WORD wVersionRequested, LPWSPDATA lpWSPData, LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE UpcallTable, LPWSPPROC_TABLE lpProcTable) { if (lpProtocolInfo NULL || lpProcTable NULL) return WSAEPROVIDERFAILEDINIT; g_UpCallTable UpcallTable; // 步骤 1链形如 [自己, 下层, ..., 基础协议]取 ChainEntries[1] if (lpProtocolInfo-ProtocolChain.ChainLen 2) return WSAEPROVIDERFAILEDINIT; // 安装时链写错了 g_NextProviderId lpProtocolInfo-ProtocolChain.ChainEntries[1]; // 步骤 2通过回调表问出下层 DLL 的完整路径 WCHAR szPath[MAX_PATH] {0}; int nPathLen MAX_PATH, nErr 0; if (UpcallTable.lpWPUGetProviderPath(g_NextProviderId, szPath, nPathLen, nErr) SOCKET_ERROR) return WSAEPROVIDERFAILEDINIT; HMODULE hNext LoadLibraryW(szPath); if (hNext NULL) return WSAEPROVIDERFAILEDINIT; LPWSPSTARTUP pfnNextStartup (LPWSPSTARTUP)GetProcAddress(hNext, WSPStartup); if (pfnNextStartup NULL) return WSAEPROVIDERFAILEDINIT; // 步骤 3把本层从链头摘掉再交给下层初始化 WSAPROTOCOL_INFOW info *lpProtocolInfo; info.ProtocolChain.ChainLen--; for (int i 0; i info.ProtocolChain.ChainLen; i) info.ProtocolChain.ChainEntries[i] lpProtocolInfo-ProtocolChain.ChainEntries[i 1]; int nRet pfnNextStartup(wVersionRequested, lpWSPData, info, UpcallTable, g_NextProcTable); if (nRet ! 0) return nRet; // 下层都起不来别硬撑原样上报 // 步骤 4先整体复制再替换要挂接的那几个 *lpProcTable g_NextProcTable; lpProcTable-lpWSPConnect WSPConnect; lpProcTable-lpWSPSend WSPSend; lpProcTable-lpWSPRecv WSPRecv; lpProcTable-lpWSPCloseSocket WSPCloseSocket; LoggerInit(); // 日志在这里初始化别放到 DllMain return 0; }几个参数值得单独说。wVersionRequested是宿主进程要的 Winsock 版本原样往下传就行不要自己改成 2.2有些老程序对版本很敏感。lpWSPData是出参由下层填写实际协商的版本和描述我们不动。lpProtocolInfo里的ChainEntries[0]一定是自己[1]才是下一层——如果安装时把顺序写反这里取到的会是自己于是LoadLibrary加载的还是自己WSPStartup无限递归进程直接栈溢出。这个错误我在第 5 章会给出识别特征。步骤 4 里的顺序是有讲究的必须先整体复制再改。如果写成g_NextProcTable.lpWSPConnect WSPConnect那转发函数里再调g_NextProcTable.lpWSPConnect就是调自己一次connect直接死循环。血泪经验别问怎么知道的。还有一点LoggerInit()放在这里而不是DllMain是因为DllMain里拿锁、开文件都有加载器锁死锁的风险。WSPStartup阶段加载器锁已经释放安全得多。3.3 三十多个 WSPxxx 怎么转发用宏批量生成WSPPROC_TABLE里有三十多个函数指针真正需要动手改的可能就四五个剩下的全是原样透传。手写三十个函数既枯燥又容易抄错参数用宏批量生成是常见做法// forward.c —— 参数列表整体当宏参数传避免展开时逗号打架 #define FORWARD(fn, parms, argv) \ int WSPAPI fn parms { \ return g_NextProcTable.lp##fn argv; \ } FORWARD(WSPAccept, (SOCKET s, struct sockaddr *addr, LPINT addrlen, LPCONDITIONPROC lpfnCondition, DWORD_PTR dwCallbackData, LPINT lpErrno), (s, addr, addrlen, lpfnCondition, dwCallbackData, lpErrno)) FORWARD(WSPBind, (SOCKET s, const struct sockaddr *name, int namelen, LPINT lpErrno), (s, name, namelen, lpErrno)) FORWARD(WSPListen, (SOCKET s, int backlog, LPINT lpErrno), (s, backlog, lpErrno)) FORWARD(WSPGetPeerName, (SOCKET s, struct sockaddr *name, LPINT namelen, LPINT lpErrno), (s, name, namelen, lpErrno))宏展开后lp##fn拼出的是lpWSPAccept、lpWSPBind正好对上WSPPROC_TABLE的成员名所以不用逐个核对。参数列表整体加括号当宏参数传是为了防止参数里的逗号把宏拆散——这是 C 预处理器的老规矩。这种写法有个代价编译错误信息会变得很难读宏展开后的行号指向定义处而不是调用处。调试期我一般先手写两三个确认对得上剩下的再批量套宏。另外不同 SDK 版本里WSPPROC_TABLE的成员数量会有出入转发宏只覆盖你实际用到的那些其余靠*lpProcTable g_NextProcTable整体复制兜底比一个个补要稳妥。如果嫌宏太黑也可以写个脚本从ws2spi.h里把函数原型抽出来生成.c文件一次性生成、之后手改。项目一旦超过三层链生成比手写省心。3.4 挂接点选择每个 WSPxxx 能拿到什么不是所有函数都值得挂。挑挂接点要看你需要什么信息挂接点能拿到的信息典型用途WSPConnect目标地址、端口调用前后都能插手连接审计、按地址决定放行还是拒绝WSPSend/WSPSendTo发送缓冲区内容、长度协议特征识别、请求记录WSPRecv/WSPRecvFrom接收缓冲区、实际字节数响应记录、状态码提取WSPCloseSocket套接字句柄可关联会话会话时长、流量统计WSPIoctl控制码如FIONBIO、FIONREAD非阻塞状态同步举个最常用的WSPConnect写法只记录不阻断// hooks.c int WSPAPI WSPConnect(SOCKET s, const struct sockaddr *name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, LPINT lpErrno) { // 只处理 IPv4 的连接请求其它原样放行 if (name ! NULL namelen (int)sizeof(SOCKADDR_IN) name-sa_family AF_INET) { const SOCKADDR_IN *p (const SOCKADDR_IN *)name; LogConnect(s, ntohs(p-sin_port), p-sin_addr.S_un.S_addr); } return g_NextProcTable.lpWSPConnect(s, name, namelen, lpCallerData, lpCalleeData, lpSQOS, lpGQOS, lpErrno); }参数里的name是目标地址namelen是它的长度判断长度是为了防止某些程序传一个截断的结构体导致越界读——这种崩溃在宿主进程里发生堆栈一看全是别人的代码很难定位。s是套接字句柄可以拿它当 key 关联后续的 send/recv。想阻断连接就在调用下层之前返回SOCKET_ERROR并把*lpErrno设成WSAECONNREFUSED应用侧看到的就是连接被拒绝表现和真实拒绝一样不会暴露中间层的存在。但要注意只拦WSPConnect拦不住 UDP 的WSPSendTo也拦不住WSPAccept进来的入站连接按需补挂接点。WSPSend里能直接读到业务数据这是做协议识别的入口。但这里有个性能红线别在WSPSend里做同步磁盘写。一次大文件下载会有几十万次调用每次都fflush的话宿主进程会被拖到几乎不可用。稳妥做法是写进环形缓冲区由独立线程批量落盘。4. 安装与卸载把协议链写进 Protocol_Catalog9 并排到最前4.1 安装三步装条目、找编号、调顺序安装比写代码更容易出错因为出错往往没有明显报错只是没生效。完整流程是三步用WSCInstallProvider把 DLL 和一个分层条目 基础条目的数组注册进协议目录重新枚举目录按自己的 GUID 找到系统分配的新编号用WSCWriteProviderOrder把这个编号排到最前面让socket()匹配时优先选中它。// install.c int InstallLSP(void) { WCHAR szDll[MAX_PATH] {0}; GetModuleFileNameW(g_hSelf, szDll, MAX_PATH); // 1. 从目录里找一个 TCP/IPv4 基础协议当模板ChainLen 1 WSAPROTOCOL_INFOW base {0}; if (!FindBaseTcpProvider(AF_INET, SOCK_STREAM, IPPROTO_TCP, base)) return -1; // 2. 组装两条记录分层条目 基础条目 WSAPROTOCOL_INFOW arr[2] {0}; arr[0] base; // 字段先抄基础协议 arr[0].ProviderId g_LspGuid; // 换成自己的 GUID arr[0].ProtocolChain.ChainLen 1; // 安装时先写 1系统会回填成 2 wcscpy_s(arr[0].szProtocol, WSAPROTOCOL_LEN 1, LAuditLayer over TCP/IPv4); arr[1] base; // 链尾保持原样 int nErr 0; if (WSCInstallProvider(g_LspGuid, szDll, arr, 2, nErr) SOCKET_ERROR) return nErr; // 10013 权限不足 / 10014 参数错 // 3. 找到刚装进去的条目编号 DWORD dwNewId FindCatalogIdByGuid(g_LspGuid); if (dwNewId 0) return -1; // 4. 枚举全部条目把新编号排到最前其余保持原序 DWORD dwBytes 0; WSCEnumProtocols(NULL, NULL, dwBytes, nErr); // 首次调用返回所需字节数 DWORD n dwBytes / sizeof(WSAPROTOCOL_INFOW); WSAPROTOCOL_INFOW *pAll (WSAPROTOCOL_INFOW *)malloc(dwBytes); if (WSCEnumProtocols(NULL, pAll, dwBytes, nErr) SOCKET_ERROR) { free(pAll); return nErr; } DWORD *pOrder (DWORD *)malloc(sizeof(DWORD) * n); pOrder[0] dwNewId; DWORD k 1; for (DWORD i 0; i n; i) if (pAll[i].dwCatalogEntryId ! dwNewId) pOrder[k] pAll[i].dwCatalogEntryId; int nRet (WSCWriteProviderOrder(pOrder, k) SOCKET_ERROR) ? nErr : 0; free(pOrder); free(pAll); return nRet; }arr[0].ProtocolChain.ChainLen 1这行看着别扭其实是故意的安装时系统会根据数组里的两条记录重新构造链把ChainLen改成 2 并把两条编号分别填进ChainEntries[0]和[1]。你手工填编号反而是白填因为新编号要装完才分配。WSCEnumProtocols的两次调用模式第一次传 NULL 拿字节数、第二次传缓冲区是 Winsock 里的通用套路注意dwBytes是字节数不是条目数除一下sizeof才对。WSCWriteProviderOrder需要管理员权限且只修改调用进程位数的目录。64 位进程调它改的是 64 位目录32 位程序要单独跑一次 32 位的安装器。这一点在部署脚本里必须体现不能只跑一次了事。4.2 装完怎么确认真的生效装完不验证等于没装。三个层次的确认手段建议都过一遍第一个看目录。在管理员命令行里跑netsh winsock show catalog输出里能找到你写的szProtocol名字重点看它是不是排在前面以及Chain那一列是不是2 1000 1001这种形式。如果名字在但顺序靠后说明第 4 步没成功。第二个看加载。随便启动一个联网程序用进程查看工具能列模块的那种看它加载的 DLL 列表里有没有你的模块。这一层能确认WSPStartup被调到了。第三个看日志。日志里应该能看到宿主进程名和真实的连接记录。如果前两层都过了但日志为空问题多半出在挂接点没替换成功——回去检查*lpProcTable g_NextProcTable之后那几行赋值是不是被别的代码覆盖了。卸载是反过来的顺序先WSCDeinstallProvider(g_LspGuid, nErr)把条目删掉再把目录顺序恢复。顺序不能反先删条目再调顺序的话顺序数组里还留着已经不存在的编号WSCWriteProviderOrder会返回错误目录就停在一个半残状态。// 卸载先删条目再刷顺序 int UninstallLSP(void) { int nErr 0; if (WSCDeinstallProvider(g_LspGuid, nErr) SOCKET_ERROR) return nErr; ReorderRemoveGuid(g_LspGuid); // 重新枚举并写回不含本层的顺序 return 0; }5. 避坑与排查装错一次整台机器上不了网LSP 的坑有个共同特点错误发生在别人进程里堆栈、日志、崩溃报告全都不指向你的代码。下面几条是我实际踩过、也见别人踩过的。5.1 只装了 32 位一半程序没反应另一半正常现象装完之后用 64 位浏览器访问网页完全正常日志一条没有换 32 位的老客户端立刻就有记录。或者反过来。原因64 位系统的协议目录是两套ws2_32.dll也是两份分别给 32 位和 64 位进程用。你的安装器如果是 32 位的WSCInstallProvider改的只能是 32 位目录。解决编译两个目标平台安装器也出两份。部署时先跑 64 位版本再跑 32 位版本或者在 64 位安装器里显式调用 32 位的安装逻辑。验证方式是分别用 32 位和 64 位的netsh看目录——32 位版本在SysWOW64目录下。5.2 装了但完全没生效条目在就是不加载现象netsh winsock show catalog里能看到自己的条目名字、链长都对但任何程序都不加载日志空白。原因九成是顺序问题。协议目录是有序表socket()按顺序匹配第一个符合iAddressFamily/iSocketType/iProtocol的条目。你的条目排在基础协议后面永远轮不到。解决确认WSCWriteProviderOrder真的成功了返回值要检查。另一个隐蔽原因是排序数组里少了条目——枚举拿到的总数和实际写入的数量不一致系统会拒绝整个顺序报错但不生效。写入前先断言k n。5.3 装完直接断网所有程序都连不上现象安装脚本跑完浏览器打不开即时通讯软件全掉线甚至 ping 通但应用层全部超时。原因这是最严重的一种。通常是WSPStartup里return了非零值——比如LoadLibraryW失败、GetProcAddress拿不到WSPStartup、或者下层初始化返回错误。链上任何一层起不来整个WSAStartup就失败宿主进程彻底失去网络能力。解决第一件事是恢复网络用管理员权限跑netsh winsock reset然后重启。这条命令会重建协议目录把非标准条目清掉是必备的后悔药。恢复之后再回头查szPath是不是取到了正确的 DLL 路径64 位进程拿到 32 位路径就会LoadLibrary失败、DLL 依赖的运行时库在目标机器上有没有。写代码时的自保做法是任何一步失败都先尝试退化到透传而不是直接返回错误。理想情况下如果拿不到下层表宁可让调用失败也不要让整个进程起不来。但现实是链式结构不允许这幺做——WSPStartup失败就是失败。所以更可靠的做法是在WSPStartup里把所有可能失败的点都记日志出问题时能立刻定位到是哪一步。5.4 无限递归进程一联网就崩现象目标进程启动后立刻崩溃事件日志里是栈溢出或者进程直接消失没有任何提示。原因协议链的顺序写反了ChainEntries[0]填了基础协议的编号ChainEntries[1]填了自己的。于是WSPStartup里取ChainEntries[1]拿到的是自己LoadLibrary加载的还是同一个 DLLGetProcAddress拿到的还是自己的WSPStartup一路递归到底。解决安装前打印一遍准备写入的数组人工核对arr[0].ProviderId g_LspGuid。另一个判断方法是看加载次数——正常情况一个进程只加载一次你的 DLL工具里看到同一个模块反复出现就是这个问题。5.5 装上了但机器变卡性能黑洞现象装上之后功能都对但整机明显变慢尤其是下载、视频这类高吞吐场景。原因在WSPSend/WSPRecv里做了同步 IO。这两个函数一秒能被调用上万次每次写文件、每次OutputDebugString代价都会被放大几万倍。OutputDebugString尤其隐蔽——没有调试器监听时它会走异常路径比有监听时慢得多。解决日志改成内存环形缓冲加独立线程批量落盘OutputDebugString只在调试构建里开。另外转发函数里不要加锁非要用共享状态就上无锁队列或者线程局部存储。6. 二次开发把它改成只拦指定进程的审计层原样跑通只是第一步真正落地时最常改的方向是别拦所有程序只盯我关心的那几个。改起来比想象中简单因为WSPStartup是在宿主进程里执行的问一句我是谁就行。// 在 WSPStartup 步骤 4 之前插入非目标进程直接透传 static const WCHAR *g_TargetExe[] { Ltarget-app.exe, Lhelper.exe }; static BOOL IsTargetProcess(void) { WCHAR szExe[MAX_PATH] {0}; GetModuleFileNameW(NULL, szExe, MAX_PATH); // NULL 宿主进程自身 const WCHAR *pName wcsrchr(szExe, L\\); pName (pName ! NULL) ? pName 1 : szExe; for (int i 0; i _countof(g_TargetExe); i) if (_wcsicmp(pName, g_TargetExe[i]) 0) return TRUE; return FALSE; }调用点放在拿到g_NextProcTable之后、替换函数表之前*lpProcTable g_NextProcTable; if (!IsTargetProcess()) return 0; // 不认识的进程一行代码都别挂这样 DLL 依然会被加载进所有进程加载是ws2_32决定的拦不住但只有目标进程会真正进入你的挂接逻辑其余进程的函数表和原生完全一致零开销。这是个很实用的取舍用一点点加载开销换全局的兼容性。再往下走就该考虑还要不要继续用 LSP 了。它有几个结构性限制只能在用户态、本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/4 16:38:07

Instafilter:Swift轻量滤镜库,5分钟接入Core Image链式处理

简介:本资源是一份基于Swift语言开发的iOS实时图片滤镜应用「Instafilter」完整Xcode工程,面向iOS初学者与Swift进阶开发者,聚焦CoreImage图像处理、AVFoundation视频流捕获及UI实时渲染等核心实践场景。压缩包共12个文件,含3个Sw…

2026/11/4 16:38:07

SpringBoot+Vue超市管理系统毕业设计:从能跑到能交付的完整方案

简介:这份资源是面向计算机专业毕业设计场景的超市管理系统完整项目源码,采用SpringBoot后端与Vue.js前端的前后端分离架构,适合正在准备毕设或需要学习企业级Web开发的学生参考。压缩包共约2000个文件,涵盖1614个js、233个css、6…

2026/11/4 16:38:07

Python爬虫+百度AI+MATLAB:股吧评论情感分析与股票涨跌相关性验证

简介:这份资源面向对量化分析与自然语言处理感兴趣的开发者与研究者,围绕股票评论与股价涨跌之间的关联展开,提供一套从数据采集到结果呈现的完整实践素材。包内共10个文件,以4张png图表、2个xlsx数据表、2个py脚本、1个m脚本和1个…

2026/11/4 19:48:51

SSE 流式输出:数据块边界、语义边界与状态解析

一、SSE 的基本概念与运行机制SSE(Server-Sent Events,服务器发送事件)是一种基于 HTTP 的服务器向客户端持续推送事件的通信机制。它使用 text/event-stream 作为响应的媒体类型,允许服务端在一个持续打开的 HTTP 响应中依次发送…

2026/11/4 19:48:51

从零开始的C语言生活----------变量,分支与循环

1.类型创建变量,运算中的第一缕分支在C语言中,变量的意思十分容易理解,就是变化的量。之前我们学习了类型,那么,类型是做什么的呢?类型就像是月饼的模具侵删像int,char,double之类的…

2026/11/4 19:48:51

万亿团餐走到数字化拐点-2026订餐领域最受欢迎的小程序-优八群报名

一、市场背景:万亿团餐走到数字化拐点,工具供给却仍卡在“接龙”阶段订餐管理从来不是一个“小需求”。它背后站着的,是一个以万亿计、且仍在持续扩容的团餐市场。艾媒咨询数据显示,中国团餐市场规模已从2016年的0.90万亿元攀升至…

2026/11/4 19:43:51

下载Python与Pycharm教程 (Windows版)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/4 0:03:20

AI辅助技术栈评估:提升研发决策效率60%

1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年…

2026/11/4 0:03:20

从生成Token到直接决策:Jev如何重塑AI Agent工程实践

1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你…

2026/11/4 0:03:20

32G统一内存跑大模型:内存账、TPS实测与端云分流

想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑