ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SuperDriver:基于WFP的Windows内核态防火墙驱动开发实战

SuperDriver:基于WFP的Windows内核态防火墙驱动开发实战 简介面向Windows驱动开发学习者的WFP网络驱动防火墙源码工程包适合需要掌握Windows Filtering Platform驱动架构、或希望快速理解防火墙过滤与拦截机制的开发者参考。该资源以驱动源码为核心包含WFP网络驱动防火墙的源码实现可对照官方文档与WDK环境学习驱动注册、过滤引擎、分层层级、隧道策略等关键环节也可作为二次开发的起点。压缩包大小为766KB资源页面未提供详细文件清单但从性质看属于轻量级源码工程解压后可查阅核心C/C源码与工程配置。目前已有283人浏览学习适合中高级Windows驱动开发者、信息安全方向学生以及想深入网络栈的嵌入式/系统工程师。通过阅读这套源码读者可以了解WFP防火墙驱动的项目组织与过滤逻辑掌握驱动与用户态通信的常见写法并借鉴其代码结构搭建自己的驱动原型对于准备驱动相关岗位面试或需要快速搭建WFP实验环境的研究者具有较强的参考价值。1. 标题拆开看SuperDriver WFP 到底是一套什么样的内核态防火墙SuperDriver 这个标题把三个词焊在了一起——防火墙、windowsdriver、wfp。放在 Windows 平台上它指向的是一类基于 WFPWindows Filtering PlatformWindows 过滤平台驱动的内核态防火墙方案用内核 Callout 驱动接管网络包的过滤决策而不是靠应用层托盘程序。很多人问“防火墙关闭有影响吗”——如果关闭的是内核态过滤引擎影响会立刻传导到所有安全软件上。这套方案要解决的是进程被杀规则就失效、端口被占滤网就被绕过的应用层防火墙通病。适合做 EDR、内网准入、终端安全网关的工程师。2. 吃透 WFP 过滤层和 ALE 授权机制流量在哪一层被拦是方案成败的关键2.1 为什么淘汰 TDI/NDIS 选 WFP三种内核过滤方案的取舍先回答一个绕不开的问题做 Windows 内核防火墙为什么不是 TDI也不是 NDIS而是 WFP。早年做防火墙过滤驱动第一反应是用 TDITransport Driver Interface钩 TCP/UDP 的 IRP。TDI 在 XP/2003 时代能拿到连接四元组但微软在 Vista 之后就把它标记为遗留接口Win10/11 里每修一次协议栈就崩一批 TDI 驱动维护成本极高。NDIS 这边过滤驱动NDIS LWF能拦截以太网帧但它工作在链路层拿不到 socket 归属的进程 ID、用户 SID 这些关键身份信息。你想做“按进程放行”的规则NDIS 层得自己反查协议栈等于在底层干上层的活。WFP 正好卡在两层之间。它由过滤引擎Filter Engine统一管理从 IP 层到传输层再到 ALEApplication Layer Enforcement应用层强制授权层都有标准的过滤点而且在 ALE 层直接附带进程 ID、用户 SID、应用名称这些元数据。对防火墙业务来说黑白名单、按进程控制、按用户控制这些全是过滤引擎的“原配”信息不需要自己拼。性能上WFP 的过滤路径是微软在协议栈里预留的公共路径比 HOOK 各种内部函数的方式稳得多。我一般跟同事说只要不碰 MAC 帧层面的处理一律优先考虑 WFP别自己造轮子。2.2 过滤层全景一个 TCP 连接从建连到收发要过几道闸WFP 的过滤层很多但大多数 Callout 驱动用到的只有其中几个。把最关键的四组列出来。层类别关键过滤点能拿到什么IP 包层FWPS_LAYER_INBOUND_IPPACKET_V4/V6、OUTBOUND_IPPACKET_V4/V6源 IP、目的 IP、协议拿不到进程 ID传输层INBOUND_TRANSPORT_V4、OUTBOUND_TRANSPORT_V4五元组拿不到进程 IDALE 授权层RESOURCE_ASSIGNMENT、AUTH_LISTEN、AUTH_CONNECT、AUTH_RECV_ACCEPT五元组 进程 ID 用户 SID 应用名称流层STREAM_V4、STREAM_PACKET_V4TCP 流数据能做内容审计一个典型的出站 TCP 连接从进程调 connect 开始依次经过 OUTBOUND_IPPACKET_V4、OUTBOUND_TRANSPORT_V4再进入 ALE 的 AUTH_CONNECT 授权点。到这一步过滤引擎已经把发起连接的进程 ID、应用路径、用户 SID 全挂在元数据里了。入站侧则反过来SYN 包先过 IP 包层和传输层最后在 ALE 的 AUTH_RECV_ACCEPT 层做授权检查。注意一个容易翻车的点很多人以为“接受连接”的过滤点就是 AUTH_RECV_ACCEPT 一个其实创建 listening socket 的 bind/listen 在更早的 RESOURCE_ASSIGNMENT 和 AUTH_LISTEN 层就已经决定了一次监听是否合法。想彻底拦住一个入站服务最好在 AUTH_LISTEN 层就卡掉它而不是等包进来再 drop。做终端防火墙的尤其要记住应用层能看见的“端口占用”在内核过滤层面对应的是好几个不同的授权点漏掉一个规则就是漏洞。2.3 BFE 与 Callout Driver 的分工不下发规则到内核过滤就是空中楼阁WFP 不是“驱动里写个回调就完事”的结构。它有一个用户态的服务叫 Base Filtering EngineBFE负责管理所有过滤条件、过滤器和 Callout 的注册信息。内核里真正的过滤引擎在每次收发包时会拿当前的 filter 条件去匹配匹配到才调用对应 Callout。也就是说Callout 驱动只是执行体规则本体由 BFE 统一管理和下发。理解这一点对排查问题很有用。FwpmFilterAdd0 是内核态注册过滤器的接口但最终会把条件登记到 BFE 管理的过滤集合里。FwpsFilterEngineOpen0 则打开过滤引擎的会话句柄后续 Callout 注册、Filter 下发都跟这个句柄绑定。如果你的驱动注册了 Callout 却没有做任何 FwpmFilterAdd0那你的回调函数永远不会被调用——因为没有任何过滤条件“引用”它。很多初学者的驱动加载成功却毫无反应根子就在这里。另外BFE 服务bfe被停掉或崩溃所有依赖 WFP 的规则都会失效Windows 自带的防火墙、第三方安全软件会集体报警这就是后面要细说的 0x800706d9 的源头。3. 用 VS2022 WDK 跑通第一个 Callout Driver最小工程与 INF 签名陷阱3.1 工程与 SDK 匹配选对 WDK 版本少踩一半兼容坑写过 Windows 驱动的人都知道驱动开发最恶心的不是写代码而是装环境。我用的是 VS2022 对应版本的 WDKWindows Driver Kit装好后在 VS 里新建项目时选 “Empty WDF KMDF Driver” 模板。这里有个常见坑WDK 和系统 SDK 版本必须匹配不然头文件里的结构体定义对不上Fwps 函数链接报一堆 unresolved external。我的做法是直接看 WDK 安装目录下的 sdk 目录名比如 2004、22H2、24H2选择与你目标 Windows 版本一致的那一套。内核工程还有一个容易忽略的配置C/C 的 Target Platform 要选 “Windows 10” 而不是 “Windows 7”很多 WFP API 在旧平台目标下会被宏排除掉。32 位和 64 位也要拆开建配置虽然现在基本只出 x64但测试机如果是 Arm64还得单独编译。调这些配置的时候顺便把 “Driver Signing” 里的 “Test Sign” 勾上后文会讲为什么。3.2 DriverEntry 到 FwpsCalloutRegister最小 Callout 注册骨架下面这段是注册一个 WFP Callout 的最小代码骨架。它做的只有三件事打开过滤引擎句柄、注册回调、把过滤条件挂到 ALE_AUTH_CONNECT_V4 层。实际工程里你可能要挂多个层但先把这一步跑通后面加逻辑才有基础。#include ntddk.h #include fwpsk.h #define SUPERDRIVER_CALLOUT_GUID \ { 0x2d3f4a10, 0x9b1c, 0x4e7a, \ { 0x8f, 0xa1, 0x5c, 0x3d, 0x2e, 0x9b, 0x74, 0x18 } } HANDLE g_engineHandle NULL; UINT32 g_calloutId 0; NTSTATUS SuperDriverClassify( _In_ const FWPS_INCOMING_VALUES0* inFixedValues, _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, _In_ const FWPS_FILTER* filter, _In_ UINT64 flowContext, _Out_ FWPS_CLASSIFY_OUT* classifyOut) { // 第一个版本全部放行只证明回调被引擎调到了 classifyOut-actionType FWP_ACTION_PERMIT; classifyOut-rights ~FWPS_RIGHT_ACTION_UPDATE; return STATUS_SUCCESS; } NTSTATUS SuperDriverNotify( _In_ FWPS_CALLOUT_NOTIFY_TYPE notifyType, _In_ const GUID* filterKey, _Inout_ FWPS_FILTER* filter) { UNREFERENCED_PARAMETER(filterKey); UNREFERENCED_PARAMETER(filter); if (notifyType FWPS_CALLOUT_NOTIFY_ADD_FILTER) { DbgPrint([SuperDriver] filter added\n); } return STATUS_SUCCESS; } NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { NTSTATUS status; FWPS_CALLOUT callout { 0 }; FWPM_FILTER filter { 0 }; FWPM_FILTER_CONDITION cond[1] { 0 }; UNREFERENCED_PARAMETER(RegistryPath); DriverObject-DriverUnload NULL; // 1. 打开过滤引擎句柄供后续注册使用 status FwpsFilterEngineOpen0(NULL, NULL, NULL, g_engineHandle); if (!NT_SUCCESS(status)) { DbgPrint([SuperDriver] FwpsFilterEngineOpen failed: 0x%x\n, status); return status; } // 2. 注册 Callout 回调拿到 calloutId callout.flags 0; callout.classifyFn SuperDriverClassify; callout.notifyFn SuperDriverNotify; callout.flowDeleteFn NULL; status FwpsCalloutRegister0(g_engineHandle, callout, g_calloutId); if (!NT_SUCCESS(status)) { DbgPrint([SuperDriver] FwpsCalloutRegister failed: 0x%x\n, status); FwpsFilterEngineClose0(g_engineHandle, NULL); return status; } // 3. 用 FWPM_FILTER 把这个 Callout 挂到 ALE_AUTH_CONNECT_V4 层 filter.layerKey FWPM_LAYER_ALE_AUTH_CONNECT_V4; filter.displayData.name LSuperDriver Connect Filter; filter.action.type FWP_ACTION_CALLOUT_UNKNOWN; filter.action.calloutKey SUPERDRIVER_CALLOUT_GUID; filter.filterCondition cond; filter.numFilterConditions 0; // 先匹配所有出站连接 status FwpmFilterAdd0(g_engineHandle, filter, NULL, NULL); return status; }这里说明几个关键参数。FwpsFilterEngineOpen0 的第四个参数返回句柄第一个调用传入 NULL 表示打开默认引擎如果你的驱动还要做 IPsec 相关的功能这里可以传一个设备名来绑定特定会话但防火墙场景通常用默认引擎。FWPS_CALLOUT 里的 classifyFn 和 notifyFn 是必须实现的flowDeleteFn 只有在注册流层STREAM 层或 FLOW_ESTABLISHED时才需要ALE 层的连接删除由引擎自己管。FwpmFilterAdd0 这一步经常被漏掉。就算 Callout 注册成功没有 filter 去引用它引擎就不会调你的回调。filter.action.type 我写的是 FWP_ACTION_CALLOUT_UNKNOWN这是让引擎在匹配时为这个 Callout 未知决策而调用 classifyFn如果写成 CALLOUT_TERMINATING 或 CALLOUT_INSPECTION含义不一样新手先一律用 UNKNOWN。3.3 INF 文件三段式和测试签名让 64 位 Win11 认账的配置内核驱动跑起来之前还有两座山签名和安装。开发阶段我一般不用 INF直接 sc create 加载 SYS这样迭代最快。但如果你是给测试团队打包或者走设备安装流程INF 必须写对。一个最小可用的 WFP 驱动 INF 长这样[Version] Signature $WINDOWS NT$ Class NetService ClassGuid {4D36E974-E325-11CE-BFC1-08002BE10318} Provider %ProviderName% DriverVer 06/26/2024,1.0.0.0 [DestinationDirs] DefaultDestDir 12 [DefaultInstall.NTamd64] CopyFiles DriverFiles [DriverFiles] superdriver.sys netbinary_superdriver,, [netbinary_superdriver] CopyFiles DriverFilesINF 里最容易出错的是 Class 和 ClassGuid。WFP 防火墙驱动一般归类到 NetService 类ClassGuid 就是 {4D36E974-E325-11CE-BFC1-08002BE10318}。如果你按 KernelDriver/System 类去写设备管理器里会出现一个不明设备加载顺序也和网络服务不对齐。签名这件事则绕不开64 位 Win11 默认强制校验内核驱动签名没签名的 SYS 加载时直接报 error 577。开发机开测试签名即可bcdedit /set testsigning on重启后用 sc create 加载sc create superdriver type kernel binPath C:\drivers\superdriver.sys sc start superdriver注意等号后面必须有一个空格sc 命令对格式很挑剔。加载成功后在设备管理器或 sc query superdriver 里能看到 RUNNING 状态再用 DbgView 抓 DbgPrint能看到 notifyFn 打印的 filter added说明引擎已经认账了。4. 把黑白名单写进内核classifyFn0 实现一条可热更新的过滤规则4.1 五元组从哪拿在 classifyFn0 里读取 inFixedValues 的关键索引上一章的 classifyFn 只是放行现在开始干实事。以出站 TCP 连接为例在 ALE_AUTH_CONNECT_V4 层的回调里五元组不是从 IP 头里手工解析的引擎已经把字段拆好放在 inFixedValues 数组里。问题是数组下标是编译期常量不同层的字段枚举不同所以写代码时必须绑定一个明确的 layerId。NTSTATUS SuperDriverClassify( _In_ const FWPS_INCOMING_VALUES0* inFixedValues, _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, _In_ const FWPS_FILTER* filter, _In_ UINT64 flowContext, _Out_ FWPS_CLASSIFY_OUT* classifyOut) { UINT32 remoteAddr 0; UINT16 remotePort 0; UINT8 protocol 0; BOOLEAN deny FALSE; UNREFERENCED_PARAMETER(layerData); UNREFERENCED_PARAMETER(classifyContext); UNREFERENCED_PARAMETER(flowContext); // 只处理 IPv4 出站连接层其他层一律不干预 if (inFixedValues-layerId ! FWPS_LAYER_ALE_AUTH_CONNECT_V4) { classifyOut-actionType FWP_ACTION_PERMIT; return STATUS_SUCCESS; } // ALE 层拿到的远程地址是网络字节序先转主机序 remoteAddr inFixedValues-incomingValue[ FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS ].value.uint32; remotePort inFixedValues-incomingValue[ FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_PORT ].value.uint16; protocol inFixedValues-incomingValue[ FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_PROTOCOL ].value.uint8; // 查黑名单表命中则阻断 deny SuperDriverIsBlocked(remoteAddr, remotePort, protocol); if (deny) { classifyOut-actionType FWP_ACTION_BLOCK; } else { classifyOut-actionType FWP_ACTION_PERMIT; // 保留引擎继续评估其他过滤器的权利 classifyOut-rights | FWPS_RIGHT_ACTION_UPDATE; } return STATUS_SUCCESS; }这里有两个容易被忽略的细节。第一个是字节序ALE 层给出的 IP 地址是网络字节序的大端整数直接和主机序的规则表比较会把规则全打歪。我习惯在规则入表时统一转主机序查表时也对 remoteAddr 调用 ntohl 一次这样规则文件里写成可读的 10.0.0.0/8 或 1.2.3.4 时不会出幺蛾子。第二个是 actionType 的修改权限classifyOut 默认只允许你在一定范围内改动作如果你把过滤器的 action 类型配成了 TERMINATING这里想改成 PERMIT 会被引擎忽略。所以第 3 章我推荐用 CALLOUT_UNKNOWN把决策权完全交给 classifyFn。4.2 规则表结构与并发控制一个自旋锁还是十个自旋锁黑白名单规则在内核里怎么存直接决定并发和性能。我见过有人把三千条规则全部倒进一个双向链表classifyFn 里线性扫描CPU 直接烧到 30%。也见过为了“性能”给每条规则配一个自旋锁把简单事情做成死锁现场。规则量小几百条走链表没问题量大就必须换结构。typedef struct _SUPERDRIVER_RULE { LIST_ENTRY ListEntry; UINT32 RemoteAddrStart; // 主机字节序 UINT32 RemoteAddrEnd; UINT16 RemotePort; // 0 表示不限制端口 UINT8 Protocol; // 0任意, 6TCP, 17UDP BOOLEAN Deny; // TRUE黑名单, FALSE白名单 } SUPERDRIVER_RULE; static LIST_ENTRY g_ruleListHead; static KSPIN_LOCK g_ruleLock; void SuperDriverInitRules(void) { InitializeListHead(g_ruleListHead); KeInitializeSpinLock(g_ruleLock); } BOOLEAN SuperDriverIsBlocked( _In_ UINT32 RemoteAddr, _In_ UINT16 RemotePort, _In_ UINT8 Protocol) { KIRQL oldIrql; PLIST_ENTRY entry; PSUPERDRIVER_RULE rule; BOOLEAN found FALSE; KeAcquireSpinLock(g_ruleLock, oldIrql); for (entry g_ruleListHead.Flink; entry ! g_ruleListHead; entry entry-Flink) { rule CONTAINING_RECORD(entry, SUPERDRIVER_RULE, ListEntry); // 端口和协议先匹配再匹配网段 if (rule-Protocol rule-Protocol ! Protocol) continue; if (rule-RemotePort rule-RemotePort ! RemotePort) continue; if (RemoteAddr rule-RemoteAddrStart RemoteAddr rule-RemoteAddrEnd) { found TRUE; break; } } KeReleaseSpinLock(g_ruleLock, oldIrql); // 黑名单优先命中黑名单返回 TRUE白名单模式相反 return (found rule-Deny); }这段代码把锁的范围压到最小查表全程持锁但持锁时间内只做内存比较不做任何从分页内存拿数据的操作所以在这里用自旋锁是安全的。黑白名单的语义我只写了黑名单优先的逻辑先看是否命中规则命中且 Deny 为 TRUE 才拦。真做白名单管控时通常会在规则表里加一个 DefaultAction 字段表示“未命中任何规则时默认放行还是默认拦截”而不是简单反着写。顺带一提规则表的内存一律用 NonPagedPoolNx 分配链表节点也是因为 ALE 层回调可能运行在 DISPATCH_LEVEL分页内存在那时是炸弹。4.3 用户态下发 IOCTL不重启驱动热更新黑白名单驱动里的规则表不能靠改代码编译来维护需要用户态程序通过 DeviceIoControl 随时增删。先在驱动里建一个设备对象然后挂 IOCTL 分发例程。下面给出最简的内核处理骨架NTSTATUS SuperDriverDispatchDeviceControl( _In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp) { PIO_STACK_LOCATION irpSp IoGetCurrentIrpStackLocation(Irp); ULONG ioControlCode irpSp-Parameters.DeviceIoControl.IoControlCode; PSUPERDRIVER_RULE rule; KIRQL oldIrql; NTSTATUS status STATUS_SUCCESS; UNREFERENCED_PARAMETER(DeviceObject); switch (ioControlCode) { case IOCTL_SUPERDRIVER_ADD_RULE: // SystemBuffer 是用户态传进来的规则结构 rule (PSUPERDRIVER_RULE)Irp-AssociatedIrp.SystemBuffer; if (irpSp-Parameters.DeviceIoControl.InputBufferLength sizeof(SUPERDRIVER_RULE)) { status STATUS_INVALID_BUFFER_SIZE; break; } // 拷一份到非分页池防止用户态缓冲区被释放 rule SuperDriverAllocRule(rule); if (!rule) { status STATUS_INSUFFICIENT_RESOURCES; break; } KeAcquireSpinLock(g_ruleLock, oldIrql); InsertTailList(g_ruleListHead, rule-ListEntry); KeReleaseSpinLock(g_ruleLock, oldIrql); break; default: status STATUS_INVALID_DEVICE_REQUEST; break; } Irp-IoStatus.Status status; Irp-IoStatus.Information 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }这里我给每个 IOCTL 定义了一个独立的控制码IOCTL_SUPERDRIVER_ADD_RULE用 CTL_CODE 宏在头文件里生成建议顺手定义删规则和清空规则的码形成闭环。用户态下发规则时DeviceIoControl 的 lpInBuffer 直接传规则结构体内核在 SystemBuffer 里拿到同一份拷贝。注意 InputBufferLength 一定要校验否则用户态传一个短缓冲区过来驱动直接崩溃。热更新的意义在于防火墙规则在运营中肯定要随时调整不可能每改一条 IP 就重启驱动。规则表的锁只保护链表本身增删操作都是 O(1) 的 InsertTailList / RemoveEntryList以 BFE 下发的 filter 数量性能完全够。如果以后规则上十万级再考虑把链表换成树或哈希并引入多读者单写者锁那是后话。5. 防火墙驱动避坑指南5 条让新手抓狂的 Windows 内核过滤问题5.1 win11 防火墙错误代码 0x800706d9BFE 和 mpssvc 没起来配置下发必失败现象在 Win11 上做防火墙策略导入、或在代码里调用 WFP API 想配置规则返回 0x800706d9。很多人第一反应是权限不够跑去提权结果照样报错。原因这个错误码经常指向 Base Filtering EngineBFE服务或 Windows Defender Firewall 服务mpssvc没有运行。WFP 的过滤引擎、Callout 注册、规则下发全部依赖 BFEBFE 挂了内核对象根本无法创建。mpssvc 则管理着系统自带的防火墙策略它没起来策略导入也会失败。解决去服务管理器手动启动这两个服务并把启动类型改成自动。BFE 的依赖是 RPC 服务如果 BFE 起不来先看 RPC、DCOM Server Process Launcher 是否正常。命令行一条龙sc config bfe start auto sc start bfe sc config mpssvc start auto sc start mpssvc我做过一次血泪经验测试虚拟机为了省资源把一堆服务全禁了结果自己写的驱动加载时 FwpsFilterEngineOpen0 直接返回 0x800706d9当时还以为是代码问题查了半天才发现是服务没起。遇到这个错先查服务再查代码顺序别反。5.2 注册了 Callout 却永远不触发层与方向配对搞反现象驱动加载成功notifyFn 也打印了 filter added但收发包时 classifyFn 里的 DbgPrint 一次都没出现。原因过滤层和流量方向不匹配。这是 WFP 开发里最常见的玄学问题——不是回调没注册而是你把它挂在了根本不会经过的层。比如出站 TCP connect 走 ALE_AUTH_CONNECT_V4入站 SYN 到达走 ALE_AUTH_RECV_ACCEPT_V4bind/listen 分别走 RESOURCE_ASSIGNMENT 和 AUTH_LISTEN。把入站拦截逻辑挂到 AUTH_CONNECT 上等价于对着一辆只出站的车查入站车牌。解决先明确你的规则作用在哪个方向。判断标准是连接箭头本地发起出站用 AUTH_CONNECT远端发起入站用 AUTH_RECV_ACCEPT单纯监听端口用 AUTH_LISTEN。调试时在 notifyFn 里拿到 filterKey再用 netsh wfp show filters 确认过滤器确实挂在你以为的层上。如果层没问题检查 filter 的 weight——权重太低时高优先级的系统过滤条件会先做出 PERMIT 决策你的回调可能根本没机会执行。5.3 classifyFn0 里敢动分页缓冲区蓝屏给你看好戏现象驱动加载后跑一会儿就蓝屏bugcheck 常见 0xAIRQL_NOT_LESS_OR_EQUAL或 0xD1DRIVER_IRQL_NOT_LESS_OR_EQUAL。崩的地方正好在 classifyFn 里访问一个指针时。原因WFP 的回调并不总在 PASSIVE_LEVEL 上。IP 包层、传输层的回调可能运行在 DISPATCH_LEVELALE 层某些回调也在较高 IRQL 上。在这种 IRQL 下访问分页内存、调用 KeWaitForSingleObject 或 IoAllocateMdl 等都是非法操作一次页面错误直接放倒系统没有任何后悔药。解决规则节点用 NonPagedPoolNx 分配classifyFn 里不要调用任何能阻塞的函数自旋锁持锁期间绝不去访问分页数据结构。判断当前 IRQL 可以用 KeGetCurrentIrql在调试版本里写一个分支PASSIVE_LEVEL 才做复杂查表其他情况保守返回 PERMIT。更稳妥的做法是把复杂逻辑挪到 notifyFn 或单独的工作线程里classifyFn 只做最轻量的判断。这块没有捷径只能靠 Driver Verifier 追。5.4 测试机就是装不上 SYS签名、Class、拷贝路径三个坑叠加现象sc create 成功sc start 报“拒绝访问”或者 error 577。用 devcon 安装 INF 时提示“无法验证数字签名”。换了台机器又报“服务名无效”。原因三条问题通常同时出现。第一是 64 位 Windows 默认强制内核签名SYS 没有测试签名或者系统没开测试模式直接 577。第二是 INF 的 Class 写错成 System设备管理器不认。第三是 CopyFiles 段指向的源目录不存在INF 安装把文件拷贝失败了。解决开发机先 bcdedit /set testsigning on 并重启VS 工程属性里勾选 Test Sign。INF 的 Class 和 ClassGuid 用 NetService路径段和实际输出的 SYS 位置保持一致。经验之谈不要在“干净的”Windows 上调试第一个驱动要么先开测试签名要么直接把签名流程做掉。签名工具用 WDK 自带的 signtool配合测试证书签完再看 sc start 的结果。5.5 卸载时死锁重启Callout 注销顺序与 notifyFn 重入现象驱动运行正常但 sc stop 时服务一直停在“停止中”等几分钟系统自动重启。原因最常见的是注销顺序反了——先关了过滤引擎句柄再注销 Callout导致引擎里仍残留引用。或者 notifyFn 在处理 FWPS_CALLOUT_NOTIFY_DELETE 时又去调用了可能阻塞的 API和正在等待的卸载线程互相卡死。解决严格按“先注销回调、再关引擎”的顺序sc stop superdriver驱动内部对应这段逻辑// 正确顺序先注销 callout阻塞等待引用清零 FwpsCalloutUnregisterById0(g_engineHandle, g_calloutId); // 再关闭过滤引擎句柄 FwpsFilterEngineClose0(g_engineHandle, NULL);notifyFn 里遇到 FWPS_CALLOUT_NOTIFY_DELETE只做标记和轻量清理不要把整个规则表销毁、flush IOCTL 等重活塞进去。规则表销毁放到 DriverUnload 例程里做和回调之间用引用计数隔开。这处倒没什么黑匣子纯粹是生命周期管理问题但一旦踩中测试机重启到心累。6. 用命令链验证规则是否真的生效netsh wfp 过滤表快照与 ETL 抓包定位6.1 从加载到自检测试签名、服务状态一次看齐驱动加载后我习惯用一条命令链确认环境全绿再开始业务测试bcdedit /set testsigning on sc create superdriver type kernel binPath C:\drivers\superdriver.sys sc start superdriver sc query superdriver netsh wfp show filters这里 netsh wfp show filters 是 Windows 8 之后自带的 WFP 诊断命令能看到当前引擎里所有已注册的过滤器和 Callout 状态。如果驱动注册成功过滤表里应该能查到 SUPERDRIVER 相关的 filter。用这个命令做环境校验的好处是它直接读 BFE 管理的数据不经过你驱动里的任何打印逻辑能确认真实生效路径。6.2 抓一条被拦流量的 ETL黑名单到底卡在哪一层查表逻辑写好之后光看代码无法确认流量是否真的被拦尤其当系统里还有别的安全软件在抢权重时。可以用 netsh wfp 的跟踪能力抓一个连接尝试的全过程netsh wfp capture start /trace /fileC:\temp\wfp_test.etl :: 让测试机访问一个黑名单里的 IP netsh wfp capture stop产生的 ETL 文件用事件查看器打开或者用 tracerpt 解析后重点看 ALE_AUTH_CONNECT_V4 层的决策事件里有没有 BLOCK 动作以及 action 来自哪个 Callout。这一步能直接把“规则没生效”和“规则生效但被更高权重覆盖”区分开。如果看到引擎根本没调用你的 Callout回头查 filter 的 weight 和 layerKey如果调了但最终动作还是放行查你有没在 classifyOut 里正确设置 FWPS_RIGHT_ACTION_UPDATE。我自己第一次做这个项目时两次翻车都在最基础的地方一次是层配对搞反另一次是忘了把规则表锁在合适的 IRQL 下。后来养成一个习惯每次改代码都先看一眼 netsh wfp show filters 的输出确认过滤器挂载位置、权重、方向都没错再进入业务逻辑调试。这套验证方法现在也留给团队里的新人省下的排查时间远比写代码的时间多。希望帮到你。本文还有配套的精品资源点击获取
返回列表