简介这份 LSP 注入技术学习资料围绕 Windows 语言服务提供者Language Service Provider的底层钩子机制展开面向已具备 C 基础、希望理解 COM 组件注册与进程注入原理的中级开发者。内容从 LSP 概念与 COM 接口规范切入讲解自定义语言服务 DLL 的编写、注册表登记、运行时注入以及请求拦截处理的完整链路并说明安全检测、数据恢复、调试工具与恶意软件滥用等典型应用场景帮助读者建立对底层钩子技术的能力边界与风险认知。压缩包共 38 个文件、约 150KB以 15 个 cpp 源文件和 7 个 h 头文件为主体配合 vcxproj 与 sln 工程文件可直接用 Visual Studio 打开编译另有 bat 注册脚本、def 导出定义及已编译的 dll、exe便于对照源码与成品验证注入效果。目前已有 1582 人学习适合想动手复现 LSP 注入流程、厘清 COM 接口实现细节的读者作为练手工程。1. 一次断网排查lsp 注入 到底在应用进程里做了什么某公司的终端合规客户端上线第二天八台办公机网页打不开ping 网关却正常重装网卡驱动、重置 IP 全都没用。运维把netsh winsock show catalog的输出导出来发现协议目录里多了一条链链尾挂着一个已经被删掉的 DLL 路径。这就是 lsp 注入 的典型现场它跟往进程里写内存的那类注入不是一回事lsp 注入 走的是 Winsock SPI 的分层机制把自己的 DLL 注册进协议目录应用一调WSAStartup系统就按链把这一层拉进进程之后每一次connect、send、recv都要先过它。适合看这篇的人很明确做桌面终端安全客户端、本机流量审计、网络行为管控的开发者以及想弄明白这类客户端底层凭什么能拦住连接的人。下面按一份能跑起来的样例工程拆开讲——链怎么排、WSPStartup怎么接、拦截点放在哪、卸载怎么收尾不把自己玩死。2. 分层链的内部结构协议目录、ChainEntries 与三张函数表2.1 协议目录里那一行到底存了什么协议目录是注册表里的一组二进制记录32 位和 64 位各存一套netsh winsock show catalog打出来的是当前进程位数对应的那一套。每一条记录展开就是一个WSAPROTOCOL_INFOW真正决定行为的是三个字段dwCatalogEntryId是全局唯一编号ProviderId是 GUID指向注册时登记的 DLLProtocolChain描述这条链怎么串。基础协议TCP、UDP 自己的ChainLen是 1ChainEntries[0]就是它自己的编号一旦ChainLen大于 1它就是一条分层链ChainEntries[0]固定为 1表示这是一条以基础协议收尾的链往后按从外到内的顺序排列各层的 catalog id最后一个通常就是 TCP 或 UDP。理解这个数组的方向很重要——它决定了WSPStartup里怎么找“下一层”。// 从 lpProtocolInfo 里把自己在链上的位置和下一层找出来 DWORD FindNextProviderId(const WSAPROTOCOL_INFOW* info) { const WSAPROTOCOLCHAIN* chain info-ProtocolChain; // ChainLen 1 说明拿到的是基础协议下面没东西了 if (chain-ChainLen 1) return 0; // ChainEntries[0] 固定是 1从下标 1 开始才是真正的各层编号 for (int i 1; i chain-ChainLen; i) { if (chain-ChainEntries[i] info-dwCatalogEntryId) { // 找到自己下一格就是更内层自己是最后一层时返回 0 表示到底了 return (i 1 chain-ChainLen) ? chain-ChainEntries[i 1] : 0; } } return 0; // 链里没有自己说明注册信息被改坏了 }这段函数是整个实现的地基参数只有一个lpProtocolInfo返回值是下一层的 catalog id。注意它做了两件事第一用dwCatalogEntryId而不是 GUID 来定位自己因为同一个 GUID 可能被注册成 TCP 和 UDP 两个入口用 GUID 会认错第二找不到自己时返回 0 而不是随便返回个数让上层有机会直接报错退出。我踩过一次坑早期版本用“取数组最后一个元素”来当下一层结果系统里还有别的厂商的分层链排在后面WSPStartup直接把别人的 DLL 当成了 TCP一调就崩。自那以后我只认dwCatalogEntryId精确匹配。2.2 WSPStartup系统什么时候把你的 DLL 拉进进程应用调用WSAStartup时ws2_32.dll去协议目录里找匹配的链然后从链头开始逐层LoadLibrary并调用 DLL 导出的WSPStartup。注意加载顺序是链头先、链底后返回值却是链底先、链头后——因为每一层都要先拿到下层的 30 个函数指针才能填自己的表往上传。整个 LSP 里只有WSPStartup是必须导出的其余 30 个 SPI 函数不需要__declspec(dllexport)它们是通过lpProcTable这个结构体以函数指针形式返回给调用方的。这一点很多人第一次写会搞混在 .def 文件里把 30 个函数全导出了结果系统根本不看反而因为导出表太长拖慢加载。// 按 catalog id 反查 DLL 路径先枚举拿长度再枚举拿内容 static BOOL PathFromCatalogId(DWORD catalogId, WCHAR* outPath, DWORD cch) { INT err 0; DWORD bufLen 0; // 第一次调用只为拿所需缓冲区大小返回 SOCKET_ERROR 是正常的 WSCEnumProtocols(NULL, NULL, bufLen, err); BYTE* buf (BYTE*)HeapAlloc(GetProcessHeap(), 0, bufLen); if (!buf) return FALSE; BOOL found FALSE; if (WSCEnumProtocols(NULL, (LPWSAPROTOCOL_INFOW)buf, bufLen, err) ! SOCKET_ERROR) { for (BYTE* p buf; p buf bufLen; ) { WSAPROTOCOL_INFOW* pi (WSAPROTOCOL_INFOW*)p; if (pi-dwCatalogEntryId catalogId) { INT len (INT)cch; if (WSCGetProviderPath((LPGUID)pi-ProviderId, outPath, len, err) ! SOCKET_ERROR) { // 返回的路径常带 %SystemRoot% 前缀不展开 LoadLibrary 直接失败 ExpandEnvironmentStringsW(outPath, outPath, cch); found TRUE; } break; } p sizeof(WSAPROTOCOL_INFOW) pi-dwProviderReserved; } } HeapFree(GetProcessHeap(), 0, buf); return found; }这里有两个细节值得单说。WSCEnumProtocols的缓冲区不是定长数组每条记录后面可能还跟着dwProviderReserved字节的附加数据所以遍历的时候步长必须算上它不能直接p sizeof(WSAPROTOCOL_INFOW)。另一个是ExpandEnvironmentStringsW少了这一行LoadLibrary拿到的字符串里带着%SystemRoot%在部分系统上直接返回 NULL表现就是“WSPStartup 里 LoadLibrary 失败但路径打印出来看着是对的”这种玄学问题。2.3 三张函数表把整条链串起来真正让这个架构转起来的是三张函数表。WSPStartup的参数里带着UpcallTable这是系统给的一批向上回调处理异步事件时会用到下层的WSPStartup会通过lpProcTable输出它的 30 个函数指针你再把这张表填回去给上层。三者的关系可以对着看表谁提供内容典型用途WSPUPCALLTABLEws2_32传入异步事件、重叠 IO 的向上回调转发WSAAsyncSelect关联的窗口消息NextProcTable下层 provider 输出30 个 SPI 函数指针不拦截的调用原样转发lpProcTable你填返回给上层同样 30 个指针覆盖掉要拦的那几个有了这三张表一个“只观测、不改变数据”的 LSP 其实只要写一个函数就够了——把NextProcTable整体赋值给lpProcTable然后替换想挂钩的成员。这也是我建议所有新项目的起步形态先让链跑通、能加载、能正常上网再一个一个加拦截点别一上来就把 30 个函数全写一遍。3. 最小可跑工程从 WSPStartup 骨架到 WSCInstallProviderAndChains 注册3.1 目录结构32 位和 64 位必须出两套 DLL工程我一般这么摆重点是build下面两个平台分开编译导出的 DLL 名字也分开安装时一次性把两个路径传给注册函数lsp-demo/ src/ lsp_core.c # WSPStartup 主体负责接链和填表 lsp_hooks.c # 拦截点WSPConnect / WSPSendTo / WSPCloseSocket lsp_state.c # 每 socket 的状态表与临界区 lsp_rule.c # 规则判定先做成硬编码后面再抽出配置 lsp_install.c # 安装卸载调用 WSCInstallProviderAndChains lsp_ctx.h build/ Win32/lspdemo32.dll x64/lspdemo64.dll tools/ dump_catalog.bat # 封装 netsh winsock show catalog 并打时间戳分开编译不是洁癖。32 位进程只会去读 32 位那一套协议目录64 位进程读 64 位那一套两套目录互相独立。只注册 64 位结果就是浏览器64 位能拦住、某些老客户端32 位完全不受影响测试同学会以为拦截逻辑有 bug其实是根本没加载你的 DLL。3.2 WSPStartup 骨架先整体拷贝再覆盖下面是lsp_core.c的主体。它的职责就三件找到下一层、把下层拉起来、把表填回去。typedef struct _LSP_CTX { WSPUPCALLTABLE UpcallTable; // 系统给的向上回调 WSPPROC_TABLE NextTable; // 下层的 30 个函数指针 HMODULE hNextDll; // 下层 DLL 句柄卸载时要 FreeLibrary DWORD selfId; } LSP_CTX; static __declspec(thread) LSP_CTX* g_tlsCtx NULL; // 每个线程一份即可 int WSPAPI WSPStartup(WORD wVersionRequested, LPWSPDATA lpWSPData, LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE UpcallTable, LPWSPPROC_TABLE lpProcTable) { // 1) 定位自己在链上的下一层 DWORD nextId FindNextProviderId(lpProtocolInfo); if (nextId 0) return WSAEINVAL; // 2) 找到下层 DLL 并取出它的 WSPStartup WCHAR dllPath[MAX_PATH] {0}; if (!PathFromCatalogId(nextId, dllPath, MAX_PATH)) return WSAEPROVIDERFAILEDINIT; HMODULE hNext LoadLibraryW(dllPath); if (!hNext) return WSAEPROVIDERFAILEDINIT; LPWSPSTARTUP pfnNext (LPWSPSTARTUP)GetProcAddress(hNext, WSPStartup); if (!pfnNext) { FreeLibrary(hNext); return WSAEPROVIDERFAILEDINIT; } // 3) 把入参里的编号换成下一层链结构原样往下传下层自己会定位 WSAPROTOCOL_INFOW nextInfo *lpProtocolInfo; nextInfo.dwCatalogEntryId nextId; WSPPROC_TABLE nextTable {0}; INT ret pfnNext(wVersionRequested, lpWSPData, nextInfo, UpcallTable, nextTable); if (ret ! 0) { FreeLibrary(hNext); return ret; } // 4) 建上下文 LSP_CTX* ctx (LSP_CTX*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(LSP_CTX)); if (!ctx) { FreeLibrary(hNext); return WSAENOBUFS; } ctx-UpcallTable UpcallTable; ctx-NextTable nextTable; ctx-hNextDll hNext; ctx-selfId lpProtocolInfo-dwCatalogEntryId; g_tlsCtx ctx; // 5) 关键一步整表拷贝再只覆盖要拦的那几个 *lpProcTable nextTable; lpProcTable-lpWSPConnect Hook_WSPConnect; lpProcTable-lpWSPSendTo Hook_WSPSendTo; lpProcTable-lpWSPCloseSocket Hook_WSPCloseSocket; lpProcTable-lpWSPCleanup Hook_WSPCleanup; return 0; }参数逐个说明wVersionRequested和lpWSPData只管透传不用改lpProtocolInfo是入参复制后改dwCatalogEntryId就够了不要自作聪明去改ProtocolChain.ChainLen改了以后下层如果按链长做校验会直接拒绝UpcallTable要存下来后面处理异步事件时用得上lpProcTable是输出参数*lpProcTable nextTable;这一行是整个实现里最省事的地方把 30 个转发函数一次性解决。g_tlsCtx用线程局部存储是因为WSPStartup在一个进程里可能被调用多次应用多次WSAStartup或者不同线程各自初始化用全局单例会在多线程下互相踩。真实的商用实现通常在一个进程里维护一张 socket 到状态的映射但上下文本身用 TLS 存最省心。3.3 安装与卸载WSCInstallProviderAndChains 的调用细节注册这一步不需要自己拼注册表WSCInstallProviderAndChains会帮你把 entry 写进目录、把链拼好。前提是有个固定的 GUID安装和卸载必须用同一个否则卸载时找不到自己// 自己生成一次之后写死在代码里别每次安装随机生成 static const GUID kLspGuid { 0x9a3f1c20, 0x7b44, 0x4e21, { 0x8d, 0x35, 0x2c, 0x11, 0x6e, 0x90, 0x4a, 0x77 } }; INT InstallLsp(const WCHAR* dll64, const WCHAR* dll32) { INT err 0; DWORD entryId 0; WSAPROTOCOL_INFOW info {0}; // 描述这一层支持的协议TCP 和 UDP 各登记一次这里以 TCP 为例 info.dwServiceFlags1 XP1_IFS_HANDLES; info.iAddressFamily AF_INET; info.iSocketType SOCK_STREAM; info.iProtocol IPPROTO_TCP; info.iVersion 2; info.ProviderId kLspGuid; wcscpy_s(info.szProtocol, WSAPROTOCOL_LEN, LSampleLsp over TCP); INT ret WSCInstallProviderAndChains( kLspGuid, dll64, // 64 位 DLL 完整路径 dll32, // 32 位 DLL 完整路径传 NULL 表示只装当前位数 LSampleLsp, XP1_IFS_HANDLES, info, 1, entryId, err); return (ret SOCKET_ERROR) ? err : 0; }dll64和dll32必须是绝对路径WSCInstallProviderAndChains不会帮你解析相对路径。entryId是输出拿到后建议落一行日志后面排查“装的是哪一条”时全靠它。dwServiceFlags传XP1_IFS_HANDLES表示这层能透传 IFS 句柄如果你的层不创建新 socket这个标志必须带上否则部分应用尤其是用 IOCP 的会拿不到可用句柄而报错。卸载就一句WSCDeinstallProvider(kLspGuid, err)但顺序不能错先调它把目录恢复再想办法删 DLL 文件。反过来先删文件目录里那条记录还指向不存在的路径全机所有进程WSAStartup都会失败——这就是开头那次断网事故的成因。4. 拦截点实现WSPConnect 的判定姿势与每 socket 状态表4.1 连接拦截地址判定与拒绝的正确姿势WSPConnect是最自然的拦截点因为目标地址就在参数里不用维护状态。写法上要特别注意拒绝之后怎么收场int WSPAPI Hook_WSPConnect(SOCKET s, const struct sockaddr FAR* name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, LPINT lpErrno) { LSP_CTX* ctx g_tlsCtx; if (!ctx) { *lpErrno WSAENOTCONN; return SOCKET_ERROR; } if (name name-sa_family AF_INET namelen (int)sizeof(SOCKADDR_IN)) { const SOCKADDR_IN* v4 (const SOCK p a hrefhttps://download.csdn.net/download/u013761036/9632021 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
企业数字化 ERP 产品动态
相关推荐
STM32实现FX3U兼容PLC:源码架构与工程避坑 简介:面向工业自动化与嵌入式开发者的FX3U V8.8源码升级包,围绕三菱FX3U系列PLC的编程逻辑、指令集应用及STM32协同控制展开,可用于学习“仿三菱”实现思路、教学验证与原型开发,资料仅供学习参考。压缩包共713个文件、约78.89MB&… · 2026/11/5 18:45:19
AI编程助手Skills机制解析:从Claude Code到Codex的插件化实践 1. 从“skills”这个模糊词说起:它到底指什么第一次看到“skills”这个词单独作为项目标题,说实话我是有点懵的。它太宽泛了,宽泛到几乎可以套在任何东西上。但结合热搜词里反复出现的 Claude Code、Codex、plugin、agents 这些词,… · 2026/11/5 20:45:21
模型4i理论:用洞察、激励、互动、迭代搭建用户增长闭环 做增长的人大概都有过这种时刻:投放预算批下来了,活动页上线了,数据看板上的新增曲线也翘了几天,然后呢?一周之后曲线回落,用户留存像漏斗底部的沙子一样往下漏,团队开会时大家各说各话——做渠… · 2026/11/5 20:44:21
EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析 1. 为什么一个PE维护盘值得单独拿出来聊装系统、修引导、救数据、清病毒,这些活儿但凡干过几次的人都知道,真正决定效率的往往不是你手速多快,而是手边那套PE环境靠不靠谱。EasyU(圈内习惯叫优启通)就是在这个场景下被… · 2026/11/5 16:49:35
Rust原来很简单二:所有权系统 Part 2 核心篇 —— 所有权系统
本篇覆盖 第 6 ~ 10 章:所有权、借用与引用、生命周期、字符串与切片、内存模型。 ⚠️ 这是全书最重要的一篇。 把它吃透,Rust 从此“很简单”;跳过它,你会处处碰壁。请慢下来,动手敲每一段代码。 第 6 章 所有权:Rust 的第一性原理
6… · 2026/11/5 16:26:08
基于Hadoop+Spark的民宿智能推荐系统设计与实现 1. 项目概述:大数据时代的民宿智能推荐这个毕业设计项目融合了当前大数据领域最主流的技术栈,构建了一个完整的民宿推荐系统解决方案。作为一名长期从事大数据系统开发的工程师,我认为这个选题非常贴合实际应用场景——随着旅游业的复苏和共享… · 2026/11/5 14:59:25
ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41
UART接口24位ADC搭配RS485,解决远距离温度监控难题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41
UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07