WdNisDrv 的网络检查链路连接着三个对象:WFP 中的流、驱动内的 FLOW_CONTEXT,以及 NisSrv.exe 承载的 WdNisSvc 服务收到的事件。只有沿同一个流标识把三者串起来,才有依据解释“哪些数据被检查”与“某次通知为什么没有出现”。
分析限定于文件版本 4.18.2102.3-0 的 x64 实现。该版本的设备访问受服务 SID 和独占打开约束;下述异常调试发生在这些条件被修改之后,因此要将内部缓冲区缺陷与默认配置下的可达性分开。
过滤器注册之后,还需要流上下文
netsh wfp show filters在导出的规则中检索 windefend。ALE 连接建立层负责为选中的连接建立上下文,STREAM 层随后处理 TCP 数据;数据报以及可选的传输层路径则承担不同层级的检查,不应把所有回调都叫作“抓包”。
flowchart TD A["TCP/IP 与 WFP 层"] --> B["ALE:选择连接"] B --> C["关联 FLOW_CONTEXT"] C --> D["STREAM / DATAGRAM 回调"] D --> E["通知队列 → WdNisSvc"]
| 规则组 | 关键条件或行为 |
|---|---|
| ALE_FLOW_ESTABLISHED_V4 / V6 | 出站,TCP 或 UDP,检查型动作 |
| STREAM_V4 / V6 | TCP 流数据,关联 context 后分类 |
| DATAGRAM_DATA_V4 / V6 | 数据报路径,使用相应流状态 |
| OUTBOUND_TRANSPORT_V4 / V6 | 可选传输层路径 |
| INBOUND_TRANSPORT_V4 / V6 | 可选传输层路径 |
STREAM 规则中的 FWP_CALLOUT_FLAG_CONDITIONAL_ON_FLOW 是关键:引擎只向已关联上下文的流调用该 classify 回调。FWPM_FILTER_FLAG_PERMIT_IF_CALLOUT_UNREGISTERED 则规定 callout 未注册时按放行处理。两者分别控制回调条件和缺席行为;没有检查不等于被阻断。FWPS_CALLOUT2、FWPM_FILTER0
用 FlowHandle 贯穿对象生命周期
| 偏移 | 名称 | 类型 | 大小 |
|---|---|---|---|
| 0x00 | RefCount | int | 4 |
| 0x04 | 填充 | 4 | |
| 0x08 | FlowHandle | uint64_t | 8 |
| 0x10 | FilterFlag | int | 4 |
| 0x14 | 填充 | 4 | |
| 0x18 | RemoteSockaddrStorage | SOCKADDR_STORAGE | 0x80 |
| 0x98 | pFilterStream | pointer | 8 |
| 0xa0 | Status | int | 4 |
| 0xa4 | Init | char | 1 |
| 0xa5 | 填充 | 3 | |
| 0xa8 | pWFP_Registration | pointer | 8 |
| 0xb0 | BytesReceived | uint64_t | 8 |
| 0xb8 | BytesSent | uint64_t | 8 |
| 0xc0 | ReceivedPacketsCount | uint64_t | 8 |
| 0xc8 | SentPacketsCount | uint64_t | 8 |
这是逆向恢复的版本相关布局,空洞表示字段间填充,并非公开 ABI。RefCount 约束生命周期,FilterFlag 和 Status 参与处理决策,FlowHandle 则用于把建立、数据和删除事件归到同一条流。
FwpsCalloutRegister2 注册 classify、notify 和 flow-delete 函数指针,但每组 callout 使用哪些回调,应按该组注册参数确认。这里的 ALE classify 提取端点和进程信息并建立上下文;数据层读取上下文;删除路径递减引用并释放关联资源。不要把 WFP 的 notifyFn 与驱动发往服务的“通知消息”混为同一接口。
通知是异步事件,不是逐包镜像
| 序号 | 源地址 | 目的地址 | 协议 | 长度 | 信息 |
|---|---|---|---|---|---|
| 2 | 192.0.2.10:49695 | 192.0.2.20:8080 | TCP | 66 | [SYN] Seq=0 Len=0 |
| 3 | 192.0.2.20:8080 | 192.0.2.10:49695 | TCP | 66 | [SYN, ACK] Seq=0 Ack=1 Len=0 |
| 4 | 192.0.2.10:49695 | 192.0.2.20:8080 | TCP | 54 | [ACK] Seq=1 Ack=1 Len=0 |
| 5 | 192.0.2.10:49695 | 192.0.2.20:8080 | HTTP | 174 | GET / HTTP/1.1 |
[+] FLOW_CLASSIFY:
Flow handle : 46
CalloutId : 290
Layer : STREAM
Local : 192.0.2.10 (49695)
Remote : 192.0.2.20 (8080)
[+] FLOW_DATA:
Flow handle : 46
CalloutId : 290
Layer : STREAM上述同次观测保留了包号、端口、长度及 FlowHandle = 46,地址已替换为文档地址。网络上的 HTTP 请求与服务收到的 STREAM 通知可以相互对应,但单个 TCP 段、一次 WFP 数据指示和一次用户态读取没有固定的一一关系。
服务用 0x22A00E 等待通知。无事件时,IRP 留在 cancel-safe 队列中;有事件时,驱动组织输出并完成请求。这类队列解决挂起请求的取消与同步问题,业务事件的格式仍由驱动决定。Microsoft IRP 队列文档
公共头由两个八字节字段组成:CreationTime 和 NotificationType。连接建立分支携带流、层、callout、协议、端点及进程信息;数据分支携带同一流标识、方向与长度;删除分支携带 FlowHandle。先解公共头,再按类型和实际返回长度解释有效分支,避免把 union 尾部当成协议载荷。
策略、排除项与注入是不同入口
$ Get-MpPreference
EnableNetworkProtection : 0
ExclusionExtension : {N/A: Must be admin to view exclusions}
ExclusionIpAddress : {N/A: Must be admin to view exclusions}
ExclusionPath : {N/A: Must be admin to view exclusions}
ExclusionProcess : {N/A: Must be admin to view exclusions}配置输出中的权限提示不是“排除列表为空”。该版本把 IP 排除项存入 AVL 树,在连接建立时查询目标地址;进程排除状态通过 WdFilter 的进程通知链更新,再查进程条目中的 IsPathExcluded、ProcessFlag 等字段。命中条件应从分支恢复,而不是从单个变量名推断。
| IOCTL | 该版本用途 |
|---|---|
0x226005 |
过滤状态 |
0x226009 |
进程排除路径 |
0x226011 |
流或数据报注入 |
0x226015 |
IP 排除项 |
0x22A00E |
异步连接通知 |
EnableNetworkProtection 的 AuditMode 与 Enabled 分别对应审计和阻断语义。检查 Microsoft-Windows-Windows Defender/Operational 时,1125 表示审计模式事件,1126 表示阻断模式事件;应同时记录策略和版本,事件数量并不等于驱动处理的流数量。Microsoft Network protection
注入观察中,一组记录使用 FlowHandle = 62、CalloutId = 290,线上出现了数据但同一通知路径没有再次报告它;另一组用 FlowHandle = 454、CalloutId = 290 关联到端口 49733 → 8080 的 20 字节 TCP 载荷。两组是独立观察,不能混用流 ID。
FwpsStreamInjectAsync0 明确规定:注入流数据不会再次指示给该 callout,但仍可供更低权重子层的流 callout 检查。因此这支持“同一回调不重复接收”的解释,而不是“所有检测均失效”。函数成功返回也仅代表异步注入已启动,效果还要看完成回调和接收端。Microsoft 注入接口文档
设备访问边界先于 IOCTL 解析
AceCount : 0x1
AceType : ACCESS_ALLOWED_ACE_TYPE
Mask : 0x001f01ff
SID : S-1-5-80-3668810961-2468724468-4084584310-3029221373-430494444
Service : NT SERVICE\WdNisSvc安全描述符包含一个允许 ACE,目标是 NT SERVICE\WdNisSvc。再加上设备的独占属性,普通测试进程并不因为拥有管理员身份就自然获得与服务相同的打开条件。WdmlibIoCreateDeviceSecure 的安全描述符与 Exclusive 参数是两个需要分别核实的输入。Microsoft 设备创建接口
异常调试先修改了这些限制,之后才取得测试句柄。该前提必须随结果保留,否则会把调试可达路径误写成默认攻击面。此外,该实现的 IRP_MJ_CLEANUP 会注销 callout;关闭额外句柄后通知消失,也可能是全局清理副作用。
三种长度错误,三条独立证据链
数量乘法回绕
mov IPAddress_ArrayCount, [SystemBuffer]
mov ExpectedBufferLength, IPAddress_ArrayCount
shl ExpectedBufferLength, 7
add ExpectedBufferLength, 8
cmp InputBufferLength, ExpectedBufferLength入口先要求输入至少八字节,然后按 count × 128 + 8 计算地址数组所需长度。以上是保留符号操作数的指令节选,不是可直接汇编的源文件。
在 64 位无符号运算中,count = 1 << 57 使乘积回绕为零,计算长度退化为八字节;后续按原始 count 遍历,长度校验与迭代上界由此脱节。更稳健的判断是在 N >= 8 之后验证 count <= (N - 8) / 128,避免先形成溢出乘积。
外层头通过,内层仍然越界
| 偏移 | 名称 | 类型 | 大小 |
|---|---|---|---|
| 0x00 | unk1 | 8 | |
| 0x08 | FlowId | 8 | |
| 0x10 | layer_id | 2 | |
| 0x12 | callout_id | 4 | |
| 0x16 | stream_flags | 1 | |
| 0x17 | pad1 | 3 | |
| 0x1a | stream_size | 4 |
入口验证至少存在 0x1E 字节,再加上末尾声明的数据长度。字段在不同反汇编命名中出现 stream_size / data_size,这里统一展示它所在的四字节槽位,并不据命名认定整个分支只处理 TCP 流。
lea rdi, [rdx+1Eh]
add rcx, 8
cmp dword ptr [rdi], 0
jz loc_FFFFF8076E1771D2
mov r8d, [rdi+98h]数据长度为零、输入恰好 0x1E 字节时,第一个 [rdi] 读取就落在输入之后。非零分支再读取 rdi + 0x98,相对于缓冲区是 0xB6,四字节访问结束于 0xBA。
- 步骤 1/1代码0xfffff8026b4971c944 8b 87 98 00 00 00movr8d, dword ptr [rdi+98h]寄存器
- rdi
- 0xffffb083156e179e
对应调试状态的 InputBufferLength = 0x1E、IoControlCode = 0x226011、SystemBuffer = 0xffffb083156e1780,与 rdi = SystemBuffer + 0x1E 一致。它支持越界读取的结论,未建立稳定写入、提权链或默认访问条件下的触发路径。
目标清零挡不住源对象尾部残留
pointer0x00–0x07
删除通知的定义字段仅需公共头 16 字节加 FlowHandle 八字节。返回数据中却出现了上方八字节值,按小端解释为 0xfffff8097a032a90,调试器将它解析到 WdNisDrv+0x2a90 的指令入口。
错误在于临时栈对象仅初始化当前分支,却按更大的 union 容量复制并返回。先清零目标分配也无济于事:整块复制会再次覆盖目标尾部。修正应覆盖源对象初始化、复制长度、IRP 实际返回长度三处;缺少完整 ABI 与返回长度记录时,不给出固定泄漏字节数。
离线验证与复核重点
下面是实际运行的离线边界模型输出;它不加载驱动,不更改设备权限,也不产生网络注入。
$ python wdnis-boundary-model.py
service_sid=S-1-5-80-3668810961-2468724468-4084584310-3029221373-430494444
wrapped_size=8; safe_check=False
header=0x1e; deeper_read=[0xb6,0xba)
division_bound_cases=24582 PASS
zeroed_destination_overwritten_by_source_tail=True模型核对服务 SID 的 SHA-1 / UTF-16LE 计算、24,582 组除法上界、紧凑头长及越界位置,并用 64 字节示例验证“清零目标后整块复制仍带入尾部”。该示例大小不代表真实通知大小。历史驱动的完整二进制摘要、对应 Windows 构建及修复版本尚未逐一匹配,因此这些结果不外推到当前 Defender。
审阅驱动时,按 设备打开条件 → IOCTL 分支 → 流上下文 → 长度转换 → 完成与返回 逐段保留证据。网络回调控制哪些流进入检查,设备 DACL 控制哪些调用者提交请求;两类边界共同决定缺陷的实际影响。