Windows NDIS中间层包过滤驱动开发实战指南

发布时间:2026/10/9 8:06:36
Windows NDIS中间层包过滤驱动开发实战指南
简介这是一份基于Windows平台的包过滤型防火墙源码实现面向网络安全初学者与C系统编程学习者适用于理解网络层数据包拦截、规则匹配与内核驱动交互等核心机制。资源共34个文件包含11个头文件h定义接口与结构体、8个C源文件cpp实现应用层逻辑与驱动通信、2个系统驱动文件.sys与.h支撑底层包过滤辅以资源文件ico/bmp/rc和工程配置dsw/dsp/clw完整呈现从用户界面到内核驱动的分层架构。压缩包仅89KB轻量易读适合逐模块分析学习。已有759人下载学习读者可获得一套结构清晰、注释充分的实战级防火墙工程涵盖规则管理对话框、驱动加载控制、Socket工具封装及典型过滤策略实现是深入理解Windows防火墙原理与Win32驱动开发的优质入门范例。1. 为什么你打开firewall_source.rar后连DrvFltIp.sys都编译不过——这不是一个“下载即用”的 Windows 防火墙源码包而是一份需要逆向理解、手动补全、逐层验证的包过滤驱动开发骨架你搜到这个压缩包时大概率正卡在三个现实问题上想搞懂 Windows 防火墙底层怎么拦截 TCP SYN 包却找不到真实入口函数想给内网设备加个轻量级白名单过滤器但 Win32 API 调用总被绕过或者正在做等保测评整改被要求“提供防火墙核心模块源码说明”结果翻遍微软文档只看到一堆Netsh AdvFirewall命令。这个.rar文件里没有README.md没有build.bat没有VS2022.sln只有DrvFltIp.sys、Filter.c、ipfltr.h和几处硬编码的#ifdef DEBUG——它不是成品是某位工程师当年在 Windows Driver Kit (WDK) 7600 / 8000 环境下写的包过滤驱动原型目标明确在 NDIS 中间层NDIS IM截获 IPv4 数据包按 ACL 规则做丢弃/放行决策不依赖wfplabel.sys或netio.sys的高级策略栈。它能跑在 Windows 7 SP1 到 Windows 10 1809内核版本 10.0.17763但绝不能直接加载到 Win11 22H2。适合三类人正在啃 WDK 文档却卡在NdisFRegisterFilterDriver返回NDIS_STATUS_FAILURE的驱动新手需要在无管理员权限场景下实现进程级出向控制的嵌入式网关开发者或负责国产化替代中“自主可控网络层过滤模块”技术论证的架构师。别指望它带图形界面或 JSON 规则引擎——它的“黑白名单”就是一块连续的ACL_ENTRY[]数组靠RtlCompareMemory逐字节比对源 IP、目的端口。2. 从DrvFltIp.sys入手还原这个包过滤驱动的真实架构与编译链路这个.sys文件不是最终产物而是 WDK 构建系统生成的中间目标。要让它真正可调试、可修改、可部署必须先重建其构建上下文。很多人解压后双击build.bat报错cl.exe not found就放弃其实根本问题在于它默认绑定的是WDK 7600对应 Windows 7 RTM的旧构建环境而现代 VS 已移除对build.exe的支持。我们得用 WDK 10 的msbuild方式重写构建逻辑。2.1 解析Filter.c的核心数据流不是 Hook是 NDIS 中间层注册Filter.c的主干逻辑非常干净但极易被误读为“类似 WFP 的回调注册”。实际它是典型的 NDIS 5.x 中间层驱动NDIS IM写法// Filter.c 关键片段已去调试宏 NDIS_STATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { NDIS_FILTER_DRIVER_CHARACTERISTICS FiltChar; NDIS_STATUS Status; NdisZeroMemory(FiltChar, sizeof(NDIS_FILTER_DRIVER_CHARACTERISTICS)); FiltChar.Header.Type NDIS_OBJECT_TYPE_FILTER_DRIVER_CHARACTERISTICS; FiltChar.Header.Size sizeof(NDIS_FILTER_DRIVER_CHARACTERISTICS); FiltChar.Header.Revision NDIS_FILTER_DRIVER_CHARACTERISTICS_REVISION_1; FiltChar.MajorNdisVersion 0x06; // 注意不是 0x05这是关键兼容性标识 FiltChar.MinorNdisVersion 0x30; FiltChar.MajorDriverVersion 1; FiltChar.MinorDriverVersion 0; FiltChar.InitializeHandler FilterInitialize; FiltChar.PauseHandler FilterPause; FiltChar.RestartHandler FilterRestart; FiltChar.SetOptionsHandler FilterSetOptions; FiltChar.UnloadHandler FilterUnload; Status NdisFRegisterFilterDriver( DriverObject, RegistryPath, FiltChar, g_FilterDriverHandle ); return Status; }提示MajorNdisVersion 0x06表明它面向 NDIS 6.0Windows Vista 及以后但MinorNdisVersion 0x30即 48对应的是 NDIS 6.30Windows 8.1。这意味着它无法在 Windows 10 20H1NDIS 6.80之后的系统上通过NdisFRegisterFilterDriver注册——因为微软在 20H1 移除了对NDIS_FILTER_DRIVER_CHARACTERISTICS_REVISION_1的支持。这是第一个必须打补丁的地方。2.2 重建构建环境用 WDK 10 Visual Studio 2019 编译DrvFltIp.sysWDK 7600 的build.exe已淘汰我们必须将原始.inf和.rc文件迁移到 MSBuild 模式。以下是实测可用的DrvFltIp.vcxproj最小配置需手动创建!-- DrvFltIp.vcxproj -- Project DefaultTargetsBuild ToolsVersion15.0 xmlnshttp://schemas.microsoft.com/developer/msbuild/2003 Import Project$(VCTargetsPath)\Microsoft.Cpp.Default.props / PropertyGroup ConfigurationTypeDriver/ConfigurationType PlatformToolsetWindowsKernelModeDriver10.0/PlatformToolset TargetFrameworkVersionv10.0/TargetFrameworkVersion /PropertyGroup Import Project$(VCTargetsPath)\Microsoft.Cpp.props / ItemGroup ClCompile IncludeFilter.c / ClCompile IncludeDriver.c / /ItemGroup ItemGroup Inf IncludeDrvFltIp.inf / /ItemGroup PropertyGroup TargetNameDrvFltIp/TargetName TargetExt.sys/TargetExt /PropertyGroup Import Project$(VCTargetsPath)\Microsoft.Cpp.targets / ImportGroup LabelExtensionTargets Import Project$(MSBuildThisFileDirectory)$(MSBuildThisFileName).targets / /ImportGroup /Project关键参数说明PlatformToolsetWindowsKernelModeDriver10.0强制使用 WDK 10 的工具链避免调用已废弃的build.exeTargetFrameworkVersionv10.0对应 Windows 10 SDK非 .NET Framework确保ntddk.h头文件路径正确ConfigurationTypeDriver触发 MSBuild 的驱动专用编译规则自动链接ndis.lib、ntoskrnl.lib必须删除原sources文件WDK 7600 遗留否则 MSBuild 会冲突报错。编译前务必执行# 在 VS2019 x64 本机工具命令提示符中运行 set DISTDIRC:\WinDDK\7600.16385.1 # 仅用于头文件引用不执行 build.exe set INCLUDE%DISTDIR%\inc\api;%DISTDIR%\inc\ddk;%INCLUDE% set LIB%DISTDIR%\lib\wxp\i386;%LIB%注意DISTDIR仅提供ntddk.h、ndis.h等头文件不参与编译过程。真正的编译由 WDK 10 的cl.exe完成DISTDIR只是让预处理器能找到旧版结构体定义如NDIS_FILTER_DRIVER_CHARACTERISTICS。3.ipfltr.h里的 ACL 规则引擎如何把“IP端口协议”条件编译成高效匹配数组这个驱动最实用的部分不是驱动框架而是它内置的轻量级规则引擎。ipfltr.h定义了ACL_ENTRY结构但原始代码里没有规则加载逻辑——所有规则都硬编码在g_AclTable[]全局数组里。要让它真正可用必须补全规则解析和运行时更新能力。3.1ACL_ENTRY结构的内存布局与匹配逻辑// ipfltr.h typedef struct _ACL_ENTRY { ULONG SrcIp; // 网络字节序如 0xC0A80101 (192.168.1.1) ULONG SrcMask; // 如 0xFFFFFFFF 表示精确匹配 ULONG DstIp; // 同上 ULONG DstMask; // 同上 USHORT SrcPort; // 主机字节序注意大小端转换 USHORT DstPort; // 主机字节序 UCHAR Protocol; // IPPROTO_TCP6, IPPROTO_UDP17 UCHAR Action; // 0DROP, 1ALLOW UCHAR Flags; // 保留位当前未用 UCHAR Pad; // 对齐填充 } ACL_ENTRY, *PACL_ENTRY;关键细节SrcPort/DstPort是主机字节序小端而 IP 地址是网络字节序大端——这是最容易翻车的点。Filter.c中匹配时用ntohs()转换端口但很多移植者直接比较导致规则失效Flags字段预留但原始代码未实现TCP_FLAG_SYN等状态匹配仅支持五元组静态过滤g_AclTable是固定长度数组原始代码设为 64 条扩容需改#define MAX_ACL_ENTRIES 64并重新编译。3.2 实现运行时规则热更新用IOCTL注册用户态通信通道原始代码没有用户态交互我们补一个最小可行的 IOCTL 接口// Driver.c 新增 #define IOCTL_SET_ACL_RULES \ CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) NTSTATUS FilterDeviceIoControl( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PIO_STACK_LOCATION IrpSp IoGetCurrentIrpStackLocation(Irp); NTSTATUS Status STATUS_SUCCESS; switch (IrpSp-Parameters.DeviceIoControl.IoControlCode) { case IOCTL_SET_ACL_RULES: { PACL_ENTRY pNewRules (PACL_ENTRY)Irp-AssociatedIrp.SystemBuffer; ULONG Count IrpSp-Parameters.DeviceIoControl.InputBufferLength / sizeof(ACL_ENTRY); if (Count MAX_ACL_ENTRIES) { Status STATUS_INVALID_PARAMETER; break; } // 原子拷贝需自旋锁保护 KeAcquireSpinLock(g_AclLock, OldIrql); RtlCopyMemory(g_AclTable, pNewRules, Count * sizeof(ACL_ENTRY)); g_AclCount Count; KeReleaseSpinLock(g_AclLock, OldIrql); break; } default: Status STATUS_INVALID_DEVICE_REQUEST; } Irp-IoStatus.Status Status; Irp-IoStatus.Information 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return Status; }用户态调用示例C// load_rules.cpp #include windows.h #include iostream #pragma pack(push, 1) struct AclRule { unsigned long src_ip 0xC0A80100; // 192.168.1.0 unsigned long src_mask 0xFFFFFF00; unsigned long dst_ip 0; unsigned long dst_mask 0; unsigned short src_port 0; unsigned short dst_port 80; unsigned char protocol 6; // TCP unsigned char action 1; // ALLOW unsigned char flags 0; unsigned char pad 0; }; #pragma pack(pop) int main() { HANDLE hDev CreateFileW(L\\\\.\\DrvFltIp, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); if (hDev INVALID_HANDLE_VALUE) { std::cerr Open failed\n; return 1; } AclRule rule; DWORD bytes; DeviceIoControl(hDev, 0x800, rule, sizeof(rule), nullptr, 0, bytes, nullptr); CloseHandle(hDev); return 0; }玄学经验DeviceIoControl的IoControlCode必须与驱动中CTL_CODE完全一致且METHOD_BUFFERED模式下SystemBuffer地址由内核分配并拷贝用户态传入的指针不能是栈变量地址易触发STATUS_INVALID_ADDRESS。上面示例用全局AclRule rule是安全的。4. 避坑指南编译、加载、调试阶段的 5 个血泪错误这个驱动骨架看似简单但在真实环境中踩坑率极高。以下是我在线上环境反复验证过的 5 个高频问题每一条都附带现象、根因和可立即执行的修复方案。4.1 现象NdisFRegisterFilterDriver返回NDIS_STATUS_FAILURE事件查看器无日志原因WDK 10 默认启用Kernel-Mode Code Signing内核模式代码签名未签名的.sys文件在 Windows 10 1607 默认拒绝加载。解决开发机临时关闭驱动签名强制仅限测试bcdedit /set testsigning on shutdown /r /t 0生产环境必须用 EV 证书签名或申请 Microsoft WHQL 认证。不要用signtool自签Windows 10 1903 会静默拒绝。4.2 现象驱动加载成功但FilterSendNetBufferLists完全不被调用原因NDIS_FILTER_DRIVER_CHARACTERISTICS中MajorNdisVersion设为0x06但目标系统是 Windows 10 21H2NDIS 6.80而该版本已废弃REVISION_1。解决修改Filter.c升级到NDIS_FILTER_DRIVER_CHARACTERISTICS_REVISION_2FiltChar.Header.Revision NDIS_FILTER_DRIVER_CHARACTERISTICS_REVISION_2; FiltChar.MajorNdisVersion 0x06; FiltChar.MinorNdisVersion 0x80; // 显式声明支持 NDIS 6.804.3 现象规则匹配失效SrcPort总是 0原因Filter.c中解析 TCP 头时TcpHeader-SourcePort直接赋值给ACL_ENTRY.SrcPort但TcpHeader-SourcePort是网络字节序大端而ACL_ENTRY.SrcPort要求主机字节序小端。解决在FilterSendNetBufferLists的包解析循环中插入转换entry.SrcPort ntohs(TcpHeader-SourcePort); // 关键 entry.DstPort ntohs(TcpHeader-DestinationPort);4.4 现象IOCTL_SET_ACL_RULES调用后蓝屏STOP 0x0000001E原因g_AclTable是全局数组多核 CPU 并发访问时未加锁RtlCopyMemory导致内存撕裂。解决在Driver.c全局区声明自旋锁KSPIN_LOCK g_AclLock; ULONG g_AclCount 0;在DriverEntry中初始化KeInitializeSpinLock(g_AclLock);所有对g_AclTable的读写必须包裹KeAcquireSpinLock/KeReleaseSpinLock。4.5 现象驱动卸载后netsh interface show interface显示适配器状态异常原因FilterUnload中未调用NdisFDeregisterFilterDriver(g_FilterDriverHandle)导致 NDIS 栈残留引用。解决补全FilterUnloadVOID FilterUnload(IN PDRIVER_OBJECT DriverObject) { if (g_FilterDriverHandle) { NdisFDeregisterFilterDriver(g_FilterDriverHandle); g_FilterDriverHandle NULL; } }5. 验证包过滤效果用 Raw Socket 发包 Wireshark 抓包定位真实拦截点光编译通过没用必须验证数据包是否真被拦截。这里不用ping或curl它们走 Winsock可能被上层 WFP 拦截而是用 Raw Socket 构造原始 IP 包直击 NDIS 层。5.1 构造测试包绕过 Winsock用WSASocket(AF_INET, SOCK_RAW, IPPROTO_IP, ...)发包// test_raw_send.cpp #include winsock2.h #include ws2tcpip.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsa; WSAStartup(MAKEWORD(2,2), wsa); SOCKET sock WSASocket(AF_INET, SOCK_RAW, IPPROTO_IP, nullptr, 0, WSA_FLAG_OVERLAPPED); if (sock INVALID_SOCKET) return 1; // 设置 IP_HDRINCL 允许自定义 IP 头 BOOL opt TRUE; setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)opt, sizeof(opt)); sockaddr_in dest; dest.sin_family AF_INET; dest.sin_addr.s_addr inet_addr(192.168.1.100); // 目标 IP dest.sin_port 0; // 构造 IP 头 TCP SYN简化版仅示意 BYTE packet[60] {0}; // [0-19] IP header (20 bytes) packet[0] 0x45; // Version IHL packet[1] 0x00; // TOS *(USHORT*)(packet2) htons(60); // Total Length *(USHORT*)(packet4) htons(0x1234); // ID packet[8] 0x40; // TTL packet[9] 0x06; // Protocol TCP *(USHORT*)(packet12) inet_addr(192.168.1.50); // Src IP *(USHORT*)(packet16) inet_addr(192.168.1.100); // Dst IP // [20-59] TCP header (40 bytes) *(USHORT*)(packet20) htons(12345); // Src Port *(USHORT*)(packet22) htons(80); // Dst Port packet[32] 0x02; // SYN flag sendto(sock, (char*)packet, 60, 0, (sockaddr*)dest, sizeof(dest)); closesocket(sock); WSACleanup(); return 0; }5.2 抓包验证Wireshark 过滤ip.src 192.168.1.50 tcp.port 12345对比两台机器发送机192.168.1.50Wireshark 应看到TCP SYN包发出说明 Raw Socket 成功接收机192.168.1.100Wireshark不应看到任何来自 192.168.1.50:12345 的包证明驱动在 NDIS 层丢弃驱动本机192.168.1.50用DbgView捕获KdPrint日志确认FilterSendNetBufferLists被调用且Action DROP。后悔药技巧如果接收机抓到包说明规则未生效。立刻检查g_AclTable内存用 WinDbg 加载驱动符号后执行dds poi(DrvFltIp!g_AclTable) L4确认SrcIp/SrcMask/DstIp/DstMask/Protocol/Action值是否与测试包匹配。常见错误是SrcMask设为0x00000000全零掩码导致匹配失败。5.3 性能压测单核 CPU 下 10K PPS 的实测吞吐瓶颈在哪我用iperf3 -u -b 100M发 UDP 流同时用PerfMon监控Processor(_Total)\% DPC Time场景DPC Time丢包率瓶颈定位无规则空 ACL 表8%0%NDIS 中间层转发开销64 条规则线性扫描32%0.1%RtlCompareMemory循环耗时改用 CIDR 前缀树索引11%0%规则匹配算法优化结论原始代码的线性扫描在规则数 32 时 DPC 时间飙升。真正落地必须替换为 Radix Tree前缀树微软在ndisprot示例中有现成IPAddrToPrefix实现可直接复用。我当年在电力调度网闸项目里第一次把它跑通是在一台 Windows 7 SP1 的工控机上用串口线连着示波器看KeBugCheckEx是否触发——不是为了炫技是因为现场不允许蓝屏。后来发现最可靠的验证方式不是日志而是拔掉网线再插上看FilterRestart是否被调用最省事的调试不是 WinDbg而是把KdPrint替换成OutputDebugString用 DbgView 实时捕获。这些细节不会写在任何文档里但能让你少熬三天夜。希望帮到你。本文还有配套的精品资源点击获取