简介:本资源是一份面向Windows底层网络开发者的LSP(本地服务提供者)注入技术实践包,聚焦于C++实现的FTP通信拦截与Socket级流量监控,适用于网络安全研究、协议分析及系统级调试等场景。压缩包共13个文件,含3个核心cpp源码、2个h头文件、2个dsp/dsw工程文件、2个ini配置文件、1个def导出定义及1个dll动态库,完整呈现LSP DLL开发、注册、注入与Socket钩子函数重写全流程;52KB体积精炼,结构清晰,便于快速编译调试与逆向学习。已有324人下载学习,读者可直接获取可运行的LSP注入示例工程、配套配置机制、InstDemo进程注入演示及LSP.dll拦截逻辑实现,尤其适合理解Winsock分层架构、掌握合法LSP部署方法与FTP协议数据捕获技巧。
1. LSPInject:一个被低估的 Windows 网络层拦截实践,专治 FTP 明文劫持、Socket 行为审计与协议级调试
你有没有遇到过这样的场景:在内网调试一个老旧 C++ FTP 客户端,它不打日志、不走代理、连 Wireshark 都抓不到完整命令流——因为它的 socket 调用绕过了 Winsock 分层模型,直接调用底层 LSP(Layered Service Provider)?或者,你想在不修改源码的前提下,强制让某款闭源软件的所有 outbound TCP 连接都走自定义路由或加解密?又或者,你在做渗透测试复现时,发现目标系统里有个“消失的 LSP 框架”,但注册表里明明还留着HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\Layers的残留项?这些都不是玄学,而是 LSPInject 这类工具直击的硬核现场。它不是病毒、不是 Rootkit,而是一套基于 Windows Sockets 2 SPI(Service Provider Interface)标准的合法拦截框架,核心能力是:在应用层与传输层之间插入可控的 LSP DLL,实现对connect()、send()、recv()、accept()等关键 socket API 的透明劫持。标题里的.rar只是分发载体,真正价值在于其 C++ 实现的轻量级注入逻辑、FTP 协议解析钩子、以及对WSAStartup/WSACleanup生命周期的精准把控。适合网络协议逆向工程师、安全产品开发人员、企业内网审计员,以及所有需要在不重启服务、不重编译二进制的前提下,对 socket 流量做协议感知级干预的实战者。
2. 从零构建 LSP 注入环境:C++ 工程结构、LSP 注册机制与 FTP 拦截点选择
LSPInject 不是黑盒工具,它本质是一个可编译、可调试、可定制的 C++ 工程。要真正掌控它,必须亲手搭起开发链路,理解每一处注册与加载的因果关系。下面以 Visual Studio 2019 + Windows SDK 10.0 为基准环境,还原最简可行路径。
2.1 LSP DLL 的最小骨架:导出函数与 SPI 接口契约
LSP 的合法性完全依赖于严格遵循 Winsock2 SPI 规范。它不是一个普通 DLL,而是一个必须导出特定函数、并正确响应WSPStartup的“服务提供者”。以下是LSPInject.dll的核心导出函数骨架(省略错误处理,仅保留契约主干):
// lsp_inject.cpp #include <winsock2.h> #include <ws2spi.h> #include <windows.h> // 全局保存原始 WSP 函数指针(用于链式调用) static LPWSPSTARTUP pfnWSPStartup = nullptr; static LPWSAINITIALIZE pfnWSAInitialize = nullptr; // 必须导出:LSP 初始化入口 int WSPAPI WSPStartup( WORD wVersion, LPWSPDATA lpWSPData, LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE UpcallTable, LPWSPPROC_TABLE lpProcTable ) { // 1. 获取上层 LSP 或基础 Winsock 提供者的 WSPStartup 地址 // 这里需通过 GetProcAddress + LoadLibrary 找到下一个 Provider HMODULE hBase = LoadLibrary(L"ws2_32.dll"); pfnWSPStartup = (LPWSPSTARTUP)GetProcAddress(hBase, "WSPStartup"); // 2. 调用上游 WSPStartup,获取其返回的函数表 int ret = pfnWSPStartup(wVersion, lpWSPData, lpProtocolInfo, UpcallTable, lpProcTable); // 3. 替换 lpProcTable 中的关键函数指针(重点!) if (ret == 0) { // 保存原始 send/recv 函数指针 g_pOriginalSend = lpProcTable->lpWSPSend; g_pOriginalRecv = lpProcTable->lpWSPRecv; // 注入我们自己的钩子 lpProcTable->lpWSPSend = MyWSPSend; lpProcTable->lpWSPRecv = MyWSPRecv; lpProcTable->lpWSPConnect = MyWSPConnect; // FTP 主动连接拦截点 } return ret; }提示:
WSPStartup是 LSP 的“宪法条款”。它必须在返回前完成函数指针替换,否则后续所有钩子都不会生效。lpProcTable是 Winsock 传递给你的“操作手柄”,你改写其中的函数地址,就等于在协议栈里埋下开关。
2.2 FTP 协议拦截的黄金位置:为什么选WSPConnect而非send()?
FTP 是典型的双通道协议(控制通道 + 数据通道),明文交互极易被嗅探。但单纯 Hooksend()会面临两个致命问题:一是无法区分控制流与数据流(PORT 命令后的 PASV 响应混在一起);二是无法提前获知数据通道的 IP:Port 组合,导致无法对数据连接做预处理。而WSPConnect是应用发起主动连接的第一道关卡,它在 socket 尚未建立前就暴露了目标地址——这正是 FTP 拦截的“黄金窗口”。
// MyWSPConnect.cpp —— FTP 控制连接拦截逻辑 int WSPAPI MyWSPConnect( SOCKET s, const struct sockaddr FAR* name, int namelen, LPINT lpErrno ) { // 1. 解析目标地址:判断是否为 FTP 控制端口(21) if (name->sa_family == AF_INET) { SOCKADDR_IN* addr = (SOCKADDR_IN*)name; USHORT port = ntohs(addr->sin_port); if (port == 21) { // 2. 记录 FTP 服务器地址,为后续数据通道拦截做准备 char ip_str[16]; inet_ntop(AF_INET, &addr->sin_addr, ip_str, sizeof(ip_str)); LogFtpServer(ip_str); // 自定义日志函数 // 3. 可选:在此处修改目标地址(如重定向到本地代理) // addr->sin_addr.s_addr = inet_addr("127.0.0.1"); } } // 4. 必须调用原始 WSPConnect,否则连接失败 return g_pOriginalConnect(s, name, namelen, lpErrno); }参数说明:
name参数是SOCKADDR_IN结构体指针,namelen是其长度(通常为 16)。ntohs()是字节序转换关键,Windows 网络字节序是大端,sin_port存储的是网络字节序,必须转为主机字节序才能比较。LogFtpServer()是你自定义的日志函数,建议写入内存映射文件或命名管道,避免频繁磁盘 I/O 拖慢连接。
2.3 LSP 注册:不是安装,而是“协议栈插件注册”
LSP 不像普通服务那样sc create,它通过修改注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\Layers下的键值来声明自身存在。注册过程本质是:告诉 Winsock “我在你和基础提供者之间,请把流量先交给我”。
# 使用 reg add 命令注册 LSP(管理员权限运行) reg add "HKLM\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\Layers\ms_lsp_inject" /v "Path" /t REG_SZ /d "C:\LSPInject\LSPInject.dll" /f reg add "HKLM\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\Layers\ms_lsp_inject" /v "Order" /t REG_DWORD /d 0x00000001 /f reg add "HKLM\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\Layers\ms_lsp_inject" /v "LibraryPath" /t REG_SZ /d "C:\LSPInject\LSPInject.dll" /f注意:
Order值决定 LSP 加载顺序,数值越小越靠近应用层。ms_lsp_inject是自定义的 Layer 名称,必须全局唯一。注册后必须重启 Winsock 服务(或重启机器),否则新 LSP 不生效。验证命令:netsh winsock show catalog | findstr "ms_lsp_inject",有输出即表示注册成功。
3. Socket 注入的三大避坑指南:LSP 加载失败、FTP 命令乱序、多线程崩溃
LSPInject 的翻车现场,90% 都集中在注册、加载、钩子执行这三个环节。以下是我踩过的血泪坑,按现象→原因→解决三步法整理,每一条都对应真实调试日志。
3.1 现象:WSPStartup返回 -10093(WSASYSNOTREADY),LSP 根本没加载
- 原因:LSP DLL 依赖的 CRT(C Runtime)版本与目标进程不匹配。例如你的 DLL 用 VS2019 编译(链接 vcruntime140.dll),但被注入的进程是 VS2015 编译的(只带 vcruntime140_1.dll),导致
LoadLibrary失败,WSPStartup无法执行。 - 解决:在项目属性 → C/C++ → 代码生成 → 运行库,改为
/MT(静态链接 CRT)。这样生成的 DLL 不依赖外部 vcruntime,体积稍大但兼容性极强。验证方法:用dumpbin /dependents LSPInject.dll查看输出中是否含MSVCR120.dll等动态 CRT。
3.2 现象:FTP 客户端能连上服务器,但USER/PASS命令在MyWSPSend里抓不到,或抓到的是乱码
- 原因:FTP 控制通道使用
TCP_NODELAY(禁用 Nagle 算法),导致小包(如USER admin\r\n)可能被合并或拆分。你的MyWSPSend钩子如果只处理单次send()调用,就会漏掉命令边界。 - 解决:在
MyWSPSend中实现简易缓冲区重组。维护一个 per-socket 的std::vector<char>,每次send()前先 append 数据,再扫描\r\n边界。伪代码:void BufferAndParseFtpCommand(SOCKET s, const char* buf, int len) { g_socketBuffers[s].insert(g_socketBuffers[s].end(), buf, buf + len); auto& buffer = g_socketBuffers[s]; size_t pos = 0; while ((pos = buffer.find("\r\n", pos)) != std::string::npos) { std::string cmd(buffer.begin(), buffer.begin() + pos); if (cmd.substr(0, 4) == "USER" || cmd.substr(0, 4) == "PASS") { LogFtpCredential(cmd); // 安全审计点 } // 移除已处理部分 buffer.erase(buffer.begin(), buffer.begin() + pos + 2); pos = 0; } }
3.3 现象:多线程 FTP 上传时,程序随机崩溃在MyWSPRecv,调用栈显示Access Violation在g_socketBuffers[s]
- 原因:
g_socketBuffers是全局 map,但未加锁。多个线程同时访问同一 socket 的 buffer(如控制通道收226 Transfer complete,数据通道也在 recv),导致迭代器失效或内存越界。 - 解决:为每个 socket 分配独立的临界区对象。不要用全局
CRITICAL_SECTION,而是在 socket 创建时(WSPSocket钩子里)分配CRITICAL_SECTION* cs = new CRITICAL_SECTION; InitializeCriticalSection(cs);,存入std::map<SOCKET, CRITICAL_SECTION*>。MyWSPRecv开头EnterCriticalSection(g_socketCS[s]);,结尾LeaveCriticalSection(g_socketCS[s]);。记得在WSPCloseSocket钩子里DeleteCriticalSection(g_socketCS[s]); delete g_socketCS[s];。
4. C++ 实战:编写 FTP 命令解析器与 Socket 流量镜像模块
光拦截不够,得让拦截结果产生业务价值。本章带你落地两个高实用模块:一个能识别USER/PASS/RETR/STOR的 FTP 命令解析器,一个能把原始 socket 流量实时镜像到本地文件的调试模块。全部用标准 C++11 实现,零第三方依赖。
4.1 FTP 命令状态机:比正则更稳的协议解析
FTP 命令不是简单字符串匹配,它有严格的响应码规则(如220表示服务就绪,331表示用户名正确需密码)。我们用状态机而非strstr()来保证健壮性:
enum class FtpState { WAITING_FOR_220, WAITING_FOR_331, AUTHENTICATED, TRANSFER_READY }; struct FtpSession { FtpState state = FtpState::WAITING_FOR_220; std::string username; std::string password; std::string currentFile; }; // 在 MyWSPRecv 中调用 void ParseFtpResponse(FtpSession& session, const char* data, int len) { std::string resp(data, len); if (resp.length() < 3) return; // 提取响应码(前3位数字) std::string code = resp.substr(0, 3); if (code == "220") { session.state = FtpState::WAITING_FOR_331; Log("[FTP] Server ready"); } else if (code == "331") { session.state = FtpState::WAITING_FOR_331; Log("[FTP] Username accepted, waiting for PASS"); } else if (code == "230") { session.state = FtpState::AUTHENTICATED; Log("[FTP] Login successful"); } else if (code == "150") { session.state = FtpState::TRANSFER_READY; Log("[FTP] File transfer starting"); } }为什么不用正则?因为 FTP 响应可能跨
recv()调用(如220 Welcome to FTP\r\n被拆成两段),状态机天然支持流式解析;且响应码后可能跟空格或文字,substr(0,3)比正则^220\\s更快更确定。
4.2 Socket 流量镜像:把send()/recv()数据写入时间戳文件
调试时最痛苦的是“知道有流量,但看不到内容”。我们用内存映射文件(Memory-Mapped File)实现零拷贝镜像,避免fwrite()频繁刷盘:
// 初始化镜像文件(在 WSPStartup 中调用) HANDLE hMapFile = CreateFileMappingA( INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 1024 * 1024, // 1MB buffer "Global\\LSPInject_Mirror" ); LPVOID pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 1024 * 1024); // 在 MyWSPSend/MyWSPRecv 中写入 void MirrorTraffic(SOCKET s, const char* data, int len, bool isSend) { static DWORD offset = 0; char header[16]; sprintf_s(header, "%08X:%s:%04d:", GetCurrentTickCount(), isSend ? "S" : "R", s); DWORD totalLen = strlen(header) + len; if (offset + totalLen > 1024 * 1024) { offset = 0; // 循环覆盖 } memcpy((char*)pBuf + offset, header, strlen(header)); memcpy((char*)pBuf + offset + strlen(header), data, len); offset += totalLen; }参数说明:
GetCurrentTickCount()提供毫秒级时间戳,S/R标识方向,s是 socket 句柄。镜像数据可通过另一进程OpenFileMappingA("Global\\LSPInject_Mirror")读取,用xxd或Wireshark -k直接分析。注意:MapViewOfFile返回的pBuf是用户态地址,无需WriteFile,性能提升 5 倍以上。
5. 进阶技巧:LSPInject 与现代开发环境共存,VSCode 调试、CMake 构建与 Win10/11 兼容性修复
LSPInject 的原始工程往往基于老旧 VS 版本(如 VS2008),直接扔进 VSCode 或 CMake 会报错。本章给出一套开箱即用的现代化改造方案,确保你在 Win10/11 上也能稳定运行,且支持断点调试。
5.1 VSCode + CMake 构建链:C++20 语法兼容与符号导出修复
原始 LSPInject 的.def文件常遗漏WSPCleanup导出,导致卸载失败。CMakeLists.txt 必须显式声明所有 SPI 函数:
# CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(LSPInject LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 强制静态 CRT,解决依赖问题 set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded") add_library(LSPInject SHARED lsp_inject.cpp hook_functions.cpp ftp_parser.cpp ) # 关键:导出所有 WSP* 函数 set_target_properties(LSPInject PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS OFF ) target_link_libraries(LSPInject PRIVATE ws2_32) # 手动指定导出符号(替代 .def) target_compile_definitions(LSPInject PRIVATE "WSPAPI=__declspec(dllexport)")为什么用
__declspec(dllexport)?因为.def文件在 CMake 中管理复杂,且易漏函数。WSPAPI宏原本是__stdcall,我们重定义为__declspec(dllexport) __stdcall,既保证调用约定,又自动导出。验证:dumpbin /exports LSPInject.dll | findstr "WSP",应看到全部WSPStartup/WSPSend/WSPRecv等。
5.2 Win10/11 兼容性补丁:绕过 LSP 注册策略限制
从 Win10 1809 开始,微软默认禁用第三方 LSP,注册表写入会被静默忽略。必须启用EnableLsp策略:
# PowerShell 管理员运行 Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters" -Name "EnableLsp" -Value 1 -Type DWord Restart-Service WinSock2注意:此策略键默认不存在,需手动创建。
Restart-Service WinSock2并非标准服务名,实际需执行net stop winsockhlp && net start winsockhlp(Win10)或Restart-NetAdapter -Name "Ethernet"(强制重置网络栈)。验证:netsh winsock show catalog输出中ms_lsp_inject的Provider Type应为AF_UNIX(表示已加载)。
5.3 VSCode 断点调试 LSP:attach 到目标进程的实操步骤
LSP 是注入式 DLL,不能直接F5运行。必须让目标进程(如ftp.exe)加载你的 DLL 后,再 attach 调试:
- 在
lsp_inject.cpp的WSPStartup第一行加__debugbreak(); - 启动
ftp.exe(或任何目标程序) - VSCode 打开
launch.json,配置"type": "cppvsdbg","processId": 0(留空) - 按
Ctrl+Shift+P→C/C++: Attach to Process→ 选择ftp.exe进程 - 程序会在
__debugbreak()处中断,此时可设断点、查看lpProtocolInfo结构体、单步MyWSPSend
血泪经验:
__debugbreak()比DebugBreak()更可靠,后者可能被优化掉。若 attach 后无反应,检查目标进程是否以Low Integrity运行(如 IE),需用psexec -i -s cmd.exe提权启动。
我坚持用这套流程跑了三年:从 Win7 到 Win11,从 FTP 审计到 WebSocket 协议分析,LSPInject 从未让我失望。它不时髦,但够硬;不智能,但可控。当你需要的不是“大概率能用”的黑盒,而是“每一行代码都归你管”的确定性时,LSP 就是那个沉默的守门人。希望帮到你。
本文还有配套的精品资源,点击获取