
简介面向Windows 2000/XP平台驱动开发者、安全爱好者和毕业设计选题学生这份压缩包聚焦系统防火墙底层实现围绕过滤驱动与IP包拦截技术展开重点解决早期Windows环境下网络数据包过滤与访问控制需求。资源共5个文件核心为两个zip源码工程分别提供驱动过滤程序与防火墙功能钩子的参考实现可对照学习驱动框架和过滤逻辑CHM帮助文档对使用方法和驱动原理进行说明htm页面与txt文件则补充了readme或备注信息方便快速上手。整套资源仅122KB体积精悍但结构完整适合在调试器中配合源码逐行理解。目前已有73人浏览学习虽然规模不大但针对性强适合希望以最小成本对比学习早期Windows防火墙驱动编写思路的读者。通过源码配合帮助文档可以梳理出从驱动加载、包截获到用户态通信的完整流程理解关键回调与过滤机制为后续在XP环境下开发或维护防火墙驱动提供清晰参考。1. 为什么还要开发 Windows 2000/XP 下的防火墙老系统没有死透。医院里的生化分析仪、工厂里的 CNC 控制台、银行网点的柜面终端大量设备仍跑在 Windows 2000 或 XP 上。这些机器既不能随便升级又要暴露在车间网络或办公网里操作系统自带的防火墙要么没有2000 默认不带要么默认关闭且策略粗糙XP SP2 之前的版本。更麻烦的是这类老平台上跑的业务软件经常用裸 Socket 或 NetBIOS 通信随便上一个现代防火墙很可能把合法流量一起掐断。自己做一套轻量、可控、能按进程放行的防火墙反而是这类场景里最稳妥的解法。Windows Vista 引入 WFPWindows Filtering Platform之后过滤网络流量成了标准活。但在 2000/XP 时代没有统一的过滤接口开发者只能在用户态的 Winsock SPI、内核态的 TDI 过滤驱动和 NDIS 中间层驱动这三条路之间选。标题里的这个压缩包做的就是这件事在 2000/XP 环境下实现可用的个人防火墙。下文按三条技术路线展开先把选型逻辑讲清楚再给出能跑起来的最小代码骨架最后落在部署、测试和蓝屏排错上。2. 防火墙开发的三条路线SPI、TDI 与 NDIS2.1 三条路线的本质区别在 WFP 出现之前Windows 的网络栈从下往上大致是网卡驱动 → NDIS 中间层 → TCP/IP 协议驱动tcpip.sys→ TDI 层 → Winsock。每一层都能做拦截但拦截的位置决定了你能看到什么、能改什么。路线拦截位置能看到的流量开发复杂度系统兼容性Winsock SPI应用层应用与协议栈之间的调用低2000/XP 均支持无需驱动TDI 过滤驱动内核态TDI 层传输层端点连接、数据报中2000/XP 均支持NDIS 中间层驱动内核态NDIS 层网卡收发的所有帧高各版本差异大需处理即插即用SPIService Provider Interface服务提供者接口是 Winsock 2 体系里的扩展点。它工作在应用层防火墙实际上是一个替换掉默认 Winsock 服务提供者的 DLL。进程发起connect()、send()、recv()等调用时先经过你的 DLL于是你可以在应用真正发出网络请求之前做检查。这样做的好处是能看到操作进程的 PID、路径和完整指令序列坏处是只对走 Winsock 的流量有效如果程序用 Raw Socket 或者直接操作 TDISPI 就管不到。TDITransport Driver Interface传输驱动接口是 tcpip.sys 向上层提供的通信接口。TDI 过滤驱动把自己挂到设备对象上所有到达 TCP/IP 驱动的 IRP 都会先经过你的过滤例程。在这里能看到连接的源地址、目的地址、端口和协议类型TCP/UDP但看不到具体的数据内容——除非你额外挂钩数据包传输的 IRP解析缓冲区的偏移。NDIS 中间层驱动则往下走了一步它位于协议驱动之下、网卡驱动之上。所有进出网卡的包都会经过这里能看到完整的 IP 头、TCP/UDP 头以及应用数据。拦截能力最强但工程量也最大。开发 NDIS 中间层需要同时处理数据包发送路径和接收路径还要应对网卡的即插即用和电源管理事件。个人防火墙或小型商业防火墙产品里真正的流量统计和内容过滤都放在这一层而规则匹配和连接跟踪往往还是由驱动与应用层协同完成。2.2 选型判断我的建议顺序如果从零开始做我的建议是先评估需求再选层。只做应用级访问控制比如禁止某个进程联网SPI 就够了。需要按 IP、端口做连接拦截且不依赖应用层走 TDI 过滤驱动。需要做数据包内容过滤、流量整形或者虚拟网卡直接考虑 NDIS 中间层。这三个方案不是互斥关系很多实际产品是 SPI 做应用识别TDI 或 NDIS 做数据包拦截两层配合。单从能跑起来、代码量适中、兼容性覆盖 2000 和 XP这个目标看TDI 过滤驱动是性价比最高的路线。它不需要像 SPI 那样处理 Windows 的 SPI 排序和卸载残留问题也不需要像 NDIS 那样维护复杂的驱动对象模型。下面各用一章分别展开 SPI 和 TDI 的具体实现NDIS 限于篇幅只做关键路径说明。3. 用户态方案用 Winsock SPI 实现应用级过滤3.1 SPI 的工作机制Winsock SPI 本质上是一套与 Winsock API 一一对应的服务提供者接口。标准 Winsock 调用流程是应用调用socket()→ ws2_32.dll → 底层服务提供者的WSPStartup()分派函数。你编写一个自定义服务提供者 DLL修改注册表让 ws2_32 优先加载你的 DLL。你的 DLL 在WSPStartup里保存原始服务提供者的函数表然后返回自己的函数表给 ws2_32。防火墙的逻辑就藏在这个函数表里。你需要替换的函数表项是lpWSPConnect、lpWSPSend、lpWSPRecv、lpWSPAccept和lpWSPCloseSocket这几个。当进程执行connect()时ws2_32 调用你的WSPConnect你在其中拿到目标地址和当前进程信息查规则库后再决定是调用原始的WSPConnect还是返回WSAECONNREFUSED。3.2 最小可用的 SPI 挂钩骨架实现完整 SPI 提供程序需要处理 30 多个函数这里给出核心挂钩的代码骨架可用于快速验证思路。// spiframe.cpp —— SPI 防火墙核心挂钩 #include winsock2.h #include ws2spi.h #include windows.h // 保存原始服务提供者的分派表 static WSPUPCALLTABLE g_upCallTable; static LPWSAPROTOCOL_INFOW g_protocolInfo; // 重写后的协议信息 static int g_totalProtocols; // 原始的函数指针 static LPWSPCONNECT g_origWSPConnect NULL; static LPWSPSEND g_origWSPSend NULL; static LPWSPRECV g_origWSPRecv NULL; static LPWSPACCEPT g_origWSPAccept NULL; // 规则检查返回 TRUE 放行FALSE 拒绝 static BOOL CheckRule(DWORD pid, const struct sockaddr* addr, int addrlen, int op) { // 按 pid - 地址/端口 - 操作(connect/send/accept) 匹配规则表 // 这里省略规则表查询细节实际可读注册表或配置文件 return TRUE; } // 挂钩 WSPConnect int WSPAPI HookWSPConnect( SOCKET s, const struct sockaddr* name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPINT lpErrno) { DWORD pid GetCurrentProcessId(); if (!CheckRule(pid, name, namelen, 1)) { // 1 表示 connect 操作 *lpErrno WSAECONNREFUSED; return SOCKET_ERROR; } return g_origWSPConnect(s, name, namelen, lpCallerData, lpCalleeData, lpErrno); } // 入口WSPStartup 被 ws2_32 首次调用 int WSPAPI WSPStartup( WORD wVersionRequested, LPWSPDATA lpWSPData, LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE upcallTable, LPWSPUPCALLTABLE lpUpCallTable) { // 1. 加载原始 SPI DLL // 2. 获取原始 WSPStartup // 3. 调用原始 WSPStartup 取得原始分派表 // 4. 用 HookXxx 覆盖分派表对应项 // 5. 返回本 DLL 的分派表 return 0; }WSPStartup是整个 SPI 提供程序的入口。第 1 到第 3 步是从注册表或固定路径加载底层服务提供者通常是 mswsock.dll拿到它的WSPStartup并调用。第 4 步是关键把返回的WSPUPCALLTABLE里的函数指针逐一替换为HookWSPConnect等挂钩函数。upcallTable是 SPI 提供给上层 ws2_32 的回调表用于事件通知一般原样透传。3.3 注册与卸载写好 DLL 后需要把它注册为 Winsock 服务提供者。这一步通过WSCInstallProvider完成命令行可用微软提供的installspi.exe或自行编写注册程序。# 安装自定义 SPI 提供程序DLL 为 spifw.dll installspi.exe -p SPI Firewall Provider -d C:\fw\spifw.dll -t 1-t 1表示协议类型为 TCP。如果要同时处理 UDP需要注册第二份提供程序。注册完成后用netsh winsock show catalogXP 系统或winsock.dll检查顺序确认自定义提供程序排在 mswsock 之前。如果没有 installspi.exe可以直接编写一个调用WSCInstallProvider的小程序函数原型在ws2spi.h中定义参数与命令行一致。SPI 方案最大的坑在卸载。WSCDeinstallProvider删除注册表项后已经加载的应用程序可能还持有旧 DLL 句柄。针对这个问题的常见做法是在注册表里保留一个标记位DLL 加载时发现标记位已清除就直接透传而不做过滤而不是强制卸载 DLL。4. 内核态方案TDI 过滤驱动的实现与部署4.1 为什么要做 TDI 过滤SPI 捕获所有 Winsock 调用但具备以下局限。第一无法捕获非 Winsock 的流量比如 Raw Socket第二进程是通过注入方式挂钩的如果一个进程动态加载 ws2_32 之后绕开 SPI直接调用底层函数过滤会漏第三SPI 本身是用户态 DLL可以被恶意代码 unload 或 patch。TDI 过滤驱动工作在内核态拦截发生在协议栈内部用户态进程绕不过去安全性和完整性都要高一个层次。TDI 过滤驱动的实现思路是创建一个设备对象把它附加attach到 tcpip.sys 创建的设备对象上。所有发往 TCP/IP 驱动的 IRP 会先经过我们的设备。我们需要创建一个与 tcpip 设备同类型的设备对象并绑定到 tcpip 设备的\Device\Tcp或\Device\Udp上。通常通过 IoRegisterDeviceInterface 和 IoAttachDevice 组合完成。4.2 TDI 过滤驱动的骨架代码// tdifw.c —— TDI 过滤驱动核心 #include ntddk.h #include tdikrnl.h static PDEVICE_OBJECT g_tcpDevice NULL; // tcpip.sys 的 Tcp 设备 static PDEVICE_OBJECT g_filterDevice NULL; // 过滤器设备 static PDEVICE_OBJECT g_attachedDevice NULL; // 附加的目标设备 // 完成例程IRP 完成时恢复原 IRP 栈位置 static NTSTATUS TdiCompletion( PDEVICE_OBJECT device, PIRP irp, PVOID context) { IoSetNextIrpStackLocation(irp); return STATUS_SUCCESS; } // 处理 TDI 请求的主要分发函数 static NTSTATUS TdiDispatch( PDEVICE_OBJECT device, PIRP irp) { PIO_STACK_LOCATION irpSp IoGetCurrentIrpStackLocation(irp); NTSTATUS status; if (g_attachedDevice NULL) { return STATUS_NOT_SUPPORTED; } // 备份当前栈位置把 IRP 传给目标设备 IoCopyCurrentIrpStackLocationToNext(irp); IoSetCompletionRoutine(irp, TdiCompletion, NULL, TRUE, TRUE, TRUE); status IoCallDriver(g_attachedDevice, irp); return status; } // DriverEntry 入口 NTSTATUS DriverEntry( PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) { NTSTATUS status; UNICODE_STRING deviceName, tcpName; // 创建设备对象 RtlInitUnicodeString(deviceName, L\\Device\\TdiFilter); status IoCreateDevice(driverObject, 0, deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, g_filterDevice); if (!NT_SUCCESS(status)) return status; // 找到 tcpip.sys 创建的 Tcp 设备 RtlInitUnicodeString(tcpName, L\\Device\\Tcp); status IoGetDeviceObjectPointer(tcpName, FILE_ALL_ACCESS, g_tcpDevice, g_attachedDevice); if (!NT_SUCCESS(status)) { return status; } // 绑定到 tcpip 设备 status IoAttachDevice(g_filterDevice, tcpName, g_attachedDevice); if (!NT_SUCCESS(status)) { return status; } // 设置分发例程 driverObject-MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] TdiDispatch; driverObject-DriverUnload TdiUnload; return STATUS_SUCCESS; }这段代码里IoAttachDevice是最关键的一步。它让当前驱动挂在 tcpip.sys 之上IRP 先经过TdiDispatch再通过IoCallDriver传递给底层。IoCopyCurrentIrpStackLocationToNext负责把当前 IRP 栈位置复制到下一层IoSetCompletionRoutine注册完成例程便于在 IRP 返回时查看处理结果。所有挂接驱动的常见错误都在于忘记复制栈位置或忘记设置完成例程导致 IRP 在返回阶段访问无效内存。实际做连接过滤时还需要解析 TDI 请求的具体参数。这部分通常在IRP_MJ_INTERNAL_DEVICE_CONTROL里通过IOCTL_TDI_QUERY_INFORMATION、TDI_ACCEPT等控制码区分是建立连接、发送数据还是接收数据。控制码定义在tdikrnl.h中参数结构由PTDI_REQUEST_KERNEL_ACCEPT等结构体承载里面有完整的目标地址、端口和协议类型。4.3 编译与 INF 配置TDI 过滤驱动可以运行在未签名驱动环境但 XP 默认允许加载未签名内核驱动2000 默认也不强制。实际部署时需要准备一个 INF 文件声明驱动版本和硬件 ID。以下是最小的 INF 骨架[Version] Signature $WINDOWS NT$ Class System Provider %ProviderName% DriverVer 01/01/2005,1.0.0.0 [Manufacturer] %ProviderName%Devices [Devices] %DeviceDesc% TdiFilterDevice, Root\TdiFilter [DestinationDirs] TdiFilterDevice.CopyFiles 12 [TdiFilterDevice] CopyFiles TdiFilterDevice.CopyFiles [TdiFilterDevice.NT] CopyFiles TdiFilterDevice.CopyFiles [SourceDisksFiles] tdifw.sys 1 [Strings] ProviderName TdiFirewall DeviceDesc TDI Firewall Filter DriverINF 文件决定了系统如何安装驱动。CopyFiles段的12表示目标目录是%SystemRoot%\system32\drivers。在 Windows 2000 上INF 中不应出现[TdiFilterDevice.NT]之外的附加节名因为 2000 的 INF 解析器更严格不认识NT之外的后缀。4.4 启动驱动的命令编译生成tdifw.sys后用命令行完成创建和启动# 复制驱动到系统驱动目录 copy /Y tdifw.sys %SystemRoot%\system32\drivers\ # 创建内核服务 sc create tdifw type kernel binPath \SystemRoot\system32\drivers\tdifw.sys # 启动驱动 sc start tdifw # 停止并删除 sc stop tdifw sc delete tdifwsc create的type kernel必须指定否则系统会按用户态服务处理驱动加载会失败。binPath使用\SystemRoot\...格式而不是绝对盘符这样可以保证系统分区不在 C 盘时也能正确加载。注意sc命令中等号后必须有一个空格这是 Windows 命令解析器的硬性要求。驱动加载后可以通过sc query tdifw查看服务状态确认是否运行。5. 测试、兼容性与排错在 2000 和 XP 上都稳5.1 最小验证流程驱动写完后建议先在两台虚拟机里做最小验证一台 XP SP2一台 2000 SP4。测试顺序固定为加载驱动 → 停用 TCP/IP 转发 → 用 telnetXP或自编测试程序尝试连接 → 停用驱动 → 验证连接恢复。设计防火墙规则时要在规则的导入导出中提前预留一个开关允许所有流量和拒绝所有流量。先用拒绝模式测试连接是否被正确阻断再用允许模式验证 IRP 通路无性能损耗。测试阶段重点关注两类场景。第一类是 TCP 握手建立后立即断开这通常发生在 ACK 未按预期转发时第二类是 FTP 等主动协议服务器回连端口是动态的如果规则引擎不做协议状态跟踪会丢回连包。常见做法是在规则引擎里维护一张连接表以源IP源端口目标IP目标端口为键记录连接来自哪个进程 PID 以及匹配的规则 ID。5.2 用 Driver Verifier 定位蓝屏TDI 驱动一旦出错通常表现为蓝屏而且往往出现在卸载阶段或高并发连接场景。95% 的蓝屏原因是驱动在卸载时没有正确释放设备对象引用计数或者 IRP 完成例程里访问了已释放的上下文结构。这时用verifier.exe做内核内存压力验证能在几分钟内暴露问题。# 启用对 tdifw.sys 的验证需要管理员权限 verifier /standard /driver tdifw.sys # 重启后用事件查看器检查 verifier 输出/standard包含内存池追踪、IRQL 检查、内存一致性检查等一组标准验证项。打开验证后驱动运行速度明显变慢属正常现象这是验证器在逐一检查每次内核内存访问。出现蓝屏时用 windbg 打开C:\Windows\Minidump\下的转储文件执行!analyze -v重点看BUGCHECK_CODE和失败发生的驱动模块。5.3 一个值得收藏的技巧粘性绑定的正确姿势TDI 过滤驱动最容易出现的问题是设备附加顺序。很多驱动在DriverEntry里只绑定了\Device\Tcp没绑定\Device\Udp导致 UDP 流量完全不经过过滤器。另一个问题是 tcpip.sys 在系统启动过程中可能重新创建设备对象和服务绑定导致过滤器附加在旧的设备对象上PID 在系统重启后失联驱动看上去加载了但实际不工作。我的做法是在TdiDispatch分发例程里做一次懒绑定当检测到g_attachedDevice NULL时不直接返回错误而是再次调用IoGetDeviceObjectPointer查找当前\Device\Tcp和\Device\Udp设备重新IoAttachDevice。这种粘性重绑定机制可以覆盖系统重枚举和热插拔场景。内核模式下判断当前是否有新的绑定目标还可以监听IoRegisterPlugPlayNotification的事件但这在 2000 系统上支持有限懒绑定反而是最兼容的做法。最后分享一个快速排查技巧加载驱动后把注册表HKLM\SYSTEM\CurrentControlSet\Services\tdifw\ImagePath的路径检查一遍确认与sc create时写入的路径完全一致然后打开 CMD 执行netstat -ano对比过滤前后连接状态这样能快速定位过滤是发生在连接建立阶段还是数据发送阶段。本文还有配套的精品资源点击获取