netio.sys — feature-gated audit-mode registry robustness bugfix, old path retained
KB5079473
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | netio_unpatched.sys |
| Patched binary | netio_patched.sys |
| Overall similarity | 0.9914 |
| Function with a genuine logic change | 1 (KfdStartModuleEx) |
| New helper functions (patched only) | 2 (WIL feature-staging helpers for the new gate) |
Verdict: The only genuine code change is confined to KfdStartModuleEx (@ 0x1400A425C). The patched build introduces a new Windows feature-staging gate, Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists, that when enabled (a) skips the EnableAuditMode query and the registry change-notification registration when the BFE\Parameters\Policy\Options key does not exist, and (b) checks the NTSTATUS returned by WfpRegNotifyCreate. The pre-existing behavior is retained verbatim in the else branch, so a build with the feature not enabled behaves exactly as the unpatched build. No handle-lifetime change, no memory-safety change, and no attacker-reachable primitive was delivered.
2. Vulnerability Summary
Finding 1 — Claimed registry key handle leak: NOT a delivered fix
| Attribute | Detail |
|---|---|
| Severity | None (no security-relevant change) |
| Class | — |
| Function | KfdStartModuleEx (0x1400A425C) |
| Entry point | Module start; also reached from IoctlKfdResetState (0x140065840) on engine reset |
What the code actually does. KfdStartModuleEx opens two registry keys:
\Registry\Machine\System\CurrentControlSet\Services\BFE\Parameters— handle in the localHandleslot.\Registry\Machine\System\CurrentControlSet\Services\BFE\Parameters\Policy\Options— handle in the localKeyHandleslot (only reached when the audit-mode path is taken and the key exists).
At function exit both builds run identical cleanup: they read only Handle and pass it to ZwClose. The KeyHandle slot is not passed to ZwClose in either build.
Why this is not a fix. The close behavior for the second key is byte-for-byte identical between the two builds. The unpatched cleanup at 0x1400A43A0 closes Handle only; the patched cleanup at 0x1400A43F8 closes Handle only. The patch does not add any ZwClose for the KeyHandle slot and does not introduce any new lifetime tracking for it. Whatever the second key's lifetime semantics are, they are unchanged by the patch, so no handle leak is fixed. The WfpRegNotifyCreate wrapper does not take the KeyHandle value as an argument — it opens and owns its own key handle internally (see Finding 2) — so the second key handle is not transferred to it either.
Finding 2 — Claimed unchecked ZwNotifyChangeKey registration return: no dangling-pointer/UAF risk
| Attribute | Detail |
|---|---|
| Severity | None (no security-relevant change on the default path) |
| Class | — |
| Function | KfdStartModuleEx call to WfpRegNotifyCreate (0x14006E5D0) |
What the code actually does. WfpRegNotifyCreate (@ 0x14006E5D0) is self-contained. On entry it zeroes the global gFwAuditModeNotificationHandle (0x14006E605), opens its own key handle (0x14006E60D), allocates a callback tracking structure, and calls ZwNotifyChangeKey (0x14006E6A7). It sets gFwAuditModeNotificationHandle to the tracking structure only on success (0x14006E6CF). On failure it reports the error, calls WfpRegNotifyDestroy on its own allocation (0x14006E6E6), closes its own key handle (0x14006E6F4), and returns the NTSTATUS.
Why this is not a fix (on the default path). Because the global is set only on success and the failure path fully unwinds its own allocation and handle, ignoring the return value of WfpRegNotifyCreate does not leave a dangling pointer or an orphaned callback — the global stays zero on failure. In the patched build the return value is checked only inside the new feature-enabled branch (0x1400A43BC). The retained default branch at 0x1400A43E8 still ignores the return, exactly as the unpatched build does. There is no NTSTATUS check added on the path that runs when the new feature is not enabled.
3. Pseudocode Diff
Unpatched (audit-mode path, after the second key open)
// second key open — handle in the KeyHandle slot, exists-flag in arg_8
KeyHandle = 0;
if (!WfpRegOpenKey(&KeyHandle,
u"\\Registry\\Machine\\...\\BFE\\Parameters\\Policy\\Options", &arg_8)) {
arg_10 = 0; arg_8 = 0;
WfpRegGetUint32Value(KeyHandle, u"EnableAuditMode", &arg_10, &arg_8);
if (arg_8)
WfpSetAuditMode(arg_10);
WfpRegNotifyCreate(); // return value not used
ebx = edi;
}
// cleanup: close the FIRST key only (Handle); KeyHandle is not closed here
if (Handle) ZwClose(Handle);
Patched (same path, new feature gate added; old behavior retained in else)
KeyHandle = 0;
if (!WfpRegOpenKey(&KeyHandle,
u"\\Registry\\Machine\\...\\BFE\\Parameters\\Policy\\Options", &arg_10)) {
arg_18 = 0; arg_8 = 0;
if (Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists__IsEnabled()) {
// NEW branch
if (!arg_10) {
// key did not exist: skip query + notification registration
ebx = edi;
} else {
WfpRegGetUint32Value(KeyHandle, u"EnableAuditMode", &arg_18, &arg_8);
if (arg_8) WfpSetAuditMode(arg_18);
if (WfpRegNotifyCreate()) // return value CHECKED
goto cleanup; // on failure, skip ebx = edi
ebx = edi;
}
} else {
// RETAINED old path (feature not enabled) — identical to unpatched
WfpRegGetUint32Value(KeyHandle, u"EnableAuditMode", &arg_18, &arg_8);
if (arg_8) WfpSetAuditMode(arg_18);
WfpRegNotifyCreate(); // return value not used
ebx = edi;
}
}
// cleanup: close the FIRST key only (Handle); KeyHandle is not closed here (unchanged)
if (Handle) ZwClose(Handle);
What actually changed:
- A new WIL feature gate, Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists, absent from the unpatched build entirely.
- When the gate is enabled: a guard that skips the EnableAuditMode query and the notification registration if the second key's exists-flag is zero, plus an NTSTATUS check on WfpRegNotifyCreate.
- When the gate is not enabled: the original code path is executed unchanged.
- No change to the closing of the second key handle in either branch.
4. Assembly Analysis
Unpatched — KfdStartModuleEx @ 0x1400A425C (audit-mode path and cleanup)
; ===== audit-mode selection =====
00000001400A432E mov eax, dword ptr cs:WPP_MAIN_CB.Dpc.DeferredContext
00000001400A4334 test al, 10h
00000001400A4336 jz short 0x1400A433D
00000001400A4338 and eax, 1
00000001400A433B jmp short 0x1400A4342
00000001400A433D call Feature_Firewall_AuditMode__private_IsEnabledDeviceUsageNoInline
00000001400A4342 test eax, eax
00000001400A4344 jz short 0x1400A4395 ; audit off -> skip second open
; ===== second key open (BFE\Parameters\Policy\Options) =====
00000001400A4346 and [rbp+KeyHandle], 0
00000001400A434B lea r8, [rbp+arg_8] ; exists-flag out
00000001400A434F lea rdx, aRegistryMachin_7 ; "...BFE\Parameters\Policy\Options"
00000001400A4356 lea rcx, [rbp+KeyHandle]
00000001400A435A call WfpRegOpenKey
00000001400A435F test rax, rax
00000001400A4362 jnz short 0x1400A4397
; ===== EnableAuditMode query =====
00000001400A4364 mov rcx, [rbp+KeyHandle]
00000001400A4368 lea r9, [rbp+arg_8]
00000001400A436C and [rbp+arg_10], eax
00000001400A436F lea r8, [rbp+arg_10]
00000001400A4373 and [rbp+arg_8], eax
00000001400A4376 lea rdx, aEnableauditmod ; "EnableAuditMode"
00000001400A437D call WfpRegGetUint32Value
00000001400A4382 cmp [rbp+arg_8], 0
00000001400A4386 jz short 0x1400A4390
00000001400A4388 mov ecx, [rbp+arg_10]
00000001400A438B call WfpSetAuditMode
; ===== notification registration — return not used =====
00000001400A4390 call WfpRegNotifyCreate
00000001400A4395 mov ebx, edi
; ===== cleanup — closes the FIRST key only =====
00000001400A4397 mov rcx, [rbp+Handle]
00000001400A439B test rcx, rcx
00000001400A439E jz short 0x1400A43AC
00000001400A43A0 call cs:__imp_ZwClose
00000001400A43AC mov eax, ebx
00000001400A43AE add rsp, 40h
00000001400A43B2 pop rdi
00000001400A43B3 pop rbx
00000001400A43B4 pop rbp
00000001400A43B5 retn
Patched — KfdStartModuleEx @ 0x1400A425C (new feature gate + retained path)
; ===== second key open =====
00000001400A434E lea r8, [rbp+arg_10] ; exists-flag out
00000001400A4352 mov [rbp+KeyHandle], rsi
00000001400A4356 lea rdx, aRegistryMachin_7 ; "...BFE\Parameters\Policy\Options"
00000001400A435D lea rcx, [rbp+KeyHandle]
00000001400A4361 call WfpRegOpenKey
00000001400A4366 test rax, rax
00000001400A4369 jnz 0x1400A43EF
; ===== NEW feature gate =====
00000001400A436F mov [rbp+arg_18], esi
00000001400A4372 mov [rbp+arg_8], esi
00000001400A4375 mov eax, cs:Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists__private_featureState
00000001400A437B test al, 10h
00000001400A437D jz short 0x1400A4384
00000001400A437F and eax, 1
00000001400A4382 jmp short 0x1400A4389
00000001400A4384 call Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists__private_IsEnabledDeviceUsageNoInline
00000001400A4389 test eax, eax
00000001400A438B jz short 0x1400A43C3 ; feature OFF -> retained old path
; ===== feature ON: skip when key absent; check notify return =====
00000001400A438D cmp [rbp+arg_10], esi ; exists-flag == 0 ?
00000001400A4390 jz short 0x1400A43ED ; key absent -> skip query+notify
00000001400A4392 mov rcx, [rbp+KeyHandle]
00000001400A4396 lea r9, [rbp+arg_8]
00000001400A439A lea r8, [rbp+arg_18]
00000001400A439E lea rdx, aEnableauditmod ; "EnableAuditMode"
00000001400A43A5 call WfpRegGetUint32Value
00000001400A43AA cmp [rbp+arg_8], esi
00000001400A43AD jz short 0x1400A43B7
00000001400A43AF mov ecx, [rbp+arg_18]
00000001400A43B2 call WfpSetAuditMode
00000001400A43B7 call WfpRegNotifyCreate
00000001400A43BC test rax, rax ; return CHECKED
00000001400A43BF jnz short 0x1400A43EF ; on failure -> cleanup
00000001400A43C1 jmp short 0x1400A43ED
; ===== feature OFF: retained old path (return NOT used) =====
00000001400A43C3 mov rcx, [rbp+KeyHandle]
00000001400A43C7 lea r9, [rbp+arg_8]
00000001400A43CB lea r8, [rbp+arg_18]
00000001400A43CF lea rdx, aEnableauditmod ; "EnableAuditMode"
00000001400A43D6 call WfpRegGetUint32Value
00000001400A43DB cmp [rbp+arg_8], esi
00000001400A43DE jz short 0x1400A43E8
00000001400A43E0 mov ecx, [rbp+arg_18]
00000001400A43E3 call WfpSetAuditMode
00000001400A43E8 call WfpRegNotifyCreate
00000001400A43ED mov ebx, edi
; ===== cleanup — closes the FIRST key only (unchanged vs unpatched) =====
00000001400A43EF mov rcx, [rbp+Handle]
00000001400A43F3 test rcx, rcx
00000001400A43F6 jz short 0x1400A4404
00000001400A43F8 call cs:__imp_ZwClose
00000001400A4404 mov eax, ebx
00000001400A4406 mov rbx, [rsp+40h+arg_0]
00000001400A440B add rsp, 40h
00000001400A440F pop rdi
00000001400A4410 pop rsi
00000001400A4411 pop rbp
00000001400A4412 retn
WfpRegNotifyCreate @ 0x14006E5D0 — self-contained lifetime (both builds)
000000014006E605 and cs:gFwAuditModeNotificationHandle, 0 ; cleared on entry
000000014006E60D call WfpRegOpenKey ; opens its OWN key
000000014006E6A7 call cs:__imp_ZwNotifyChangeKey
000000014006E6B3 test eax, eax
000000014006E6B5 jns short 0x14006E6CF ; success
000000014006E6C5 call WfpReportSysErrorAsNtStatus ; failure: report...
000000014006E6E6 call WfpRegNotifyDestroy ; ...destroy own alloc
000000014006E6F4 call cs:__imp_ZwClose ; ...close own key
000000014006E6CF mov cs:gFwAuditModeNotificationHandle, rbx ; set ONLY on success
The global is populated only on success and the failure path fully unwinds. Ignoring the return in the caller does not create a dangling pointer.
5. Trigger Conditions
There is no attacker-reachable security defect to trigger in either build. For completeness, KfdStartModuleEx runs at module start and is also reached from IoctlKfdResetState (0x140065840) via the direct call at 0x1400658A0 on the engine-reset path. Because the second key handle's close behavior and the notification registration's success/failure handling are identical between builds on the default path, repeatedly invoking that path does not produce any behavioral difference introduced by the patch.
6. Exploit Primitive & Development Notes
No exploit primitive is present. The change does not alter handle lifetime, does not add or remove a bounds/NULL check on attacker-controlled data, and does not touch any memory-safety-relevant path. WfpRegNotifyCreate cleans up its own resources on failure and sets the audit-mode notification global only on success, so the previously ignored return value carries no dangling-pointer or use-after-free risk. The delta is a robustness bugfix gated behind a Windows feature-staging flag, with the original behavior retained for builds where the flag is not enabled.
7. Debugger Notes
Target: netio_unpatched.sys / netio_patched.sys.
Useful breakpoints to observe the actual behavior (not a vulnerability trigger):
bp netio!KfdStartModuleEx
Entry at
0x1400A425C.rcx(arg1) is0when called during init and nonzero on theIoctlKfdResetStatereset path.
bp 0x1400A435A ; unpatched — second WfpRegOpenKey
bp 0x1400A4361 ; patched — second WfpRegOpenKey
After return, the second key handle (when the key exists) is in the
KeyHandleslot. In both builds this slot is not passed toZwCloseat function exit.
bp 0x14006E5D0 ; WfpRegNotifyCreate
Step through to
0x14006E6CF(success setsgFwAuditModeNotificationHandle) versus the failure path at0x14006E6E6/0x14006E6F4, which destroys its own allocation and closes its own key. This confirms the caller ignoring the return is benign.
Key Offsets
| Offset | Instruction | Significance |
|---|---|---|
0x1400A4375 (patched) |
mov eax, cs:Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists__private_featureState |
New feature-staging gate; absent from the unpatched build. |
0x1400A438D (patched) |
cmp [rbp+arg_10], esi |
New guard: skip when the second key does not exist. |
0x1400A43BC (patched) |
test rax, rax after WfpRegNotifyCreate |
Return checked only on the feature-enabled branch. |
0x1400A43E8 (patched) |
call WfpRegNotifyCreate |
Retained default branch: return still ignored (as unpatched). |
0x1400A43A0 / 0x1400A43F8 |
call cs:__imp_ZwClose |
Cleanup closes the first key only in both builds. |
0x14006E6CF |
mov cs:gFwAuditModeNotificationHandle, rbx |
Notification global set only on success. |
8. Changed Functions — Full Triage
An independent function-by-function comparison of both builds (matching by content across relocations, not by reused address) was performed. After filtering pure relocation and disassembler-labeling noise (shifted global/jumptable addresses, per-build WPP trace GUID hashes, and identical memory operands printed with different symbol/struct-field names), exactly one function has a genuine logic change, and two helper functions are newly present.
KfdStartModuleEx @ 0x1400A425C — not security-relevant
- Adds the
Feature_Firewall_BugFixes_25D_AuditMode_regKey_existsfeature gate around the audit-mode second-key handling. - Feature enabled: skips
EnableAuditModequery and change-notification registration when thePolicy\Optionskey does not exist, and checks theWfpRegNotifyCreateNTSTATUS. - Feature not enabled: original behavior retained verbatim.
- Second-key handle close behavior is unchanged (first key only, both branches).
New helper functions (patched only) — not security-relevant
Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists__private_IsEnabledDeviceUsageNoInlineFeature_Firewall_BugFixes_25D_AuditMode_regKey_exists__private_IsEnabledFallback
These are standard WIL feature-staging query helpers backing the new gate. Their presence confirms this is a staged feature rollout.
No other function contains a genuine instruction-level change.
9. Unmatched Functions
| Direction | Count | Details |
|---|---|---|
| Removed (unpatched only) | 0 | — |
| Added (patched only) | 2 | The two WIL feature-staging helpers for Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists. |
No sanitizers or security checks were removed. The added functions are feature-staging query helpers, not standalone mitigations.
10. Confidence & Caveats
Confidence: High
Rationale:
- The second key handle's close behavior is identical between builds: the unpatched cleanup at 0x1400A43A0 and the patched cleanup at 0x1400A43F8 both close only the Handle slot (first key). No ZwClose for the KeyHandle slot exists in either build, so the patch does not fix any handle leak.
- WfpRegNotifyCreate (0x14006E5D0) owns its own key handle and allocation, sets gFwAuditModeNotificationHandle only on success (0x14006E6CF), and unwinds on failure (0x14006E6E6/0x14006E6F4). The ignored return value is therefore benign, and the patched default branch (0x1400A43E8) still ignores it.
- The feature gate Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists is absent from the unpatched build and new in the patched build, with the original path retained in the else branch — the signature of a staged rollout rather than a delivered fix.
- The independent full diff confirms the change is confined to KfdStartModuleEx plus its two feature-staging helpers; all other flagged differences are relocation or symbol-labeling noise.
Notes
WfpRegOpenKey(0x14006E848) opens withDesiredAccess = 0x20019(KEY_READ) and sets its third out-parameter to1only whenZwOpenKeyreturnsSTATUS_SUCCESS; onSTATUS_OBJECT_NAME_NOT_FOUND(0xC0000034) it returns success with that flag left0. The new patched guard keys off exactly this exists-flag.- The wrapper symbols (
WfpRegOpenKey,WfpRegGetUint32Value,WfpRegNotifyCreate,WfpSetAuditMode) are the same functions in both builds; apparent address/ID differences are relocation, not distinct functions.