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:

  1. \Registry\Machine\System\CurrentControlSet\Services\BFE\Parameters — handle in the local Handle slot.
  2. \Registry\Machine\System\CurrentControlSet\Services\BFE\Parameters\Policy\Options — handle in the local KeyHandle slot (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) is 0 when called during init and nonzero on the IoctlKfdResetState reset path.

bp 0x1400A435A     ; unpatched — second WfpRegOpenKey
bp 0x1400A4361     ; patched   — second WfpRegOpenKey

After return, the second key handle (when the key exists) is in the KeyHandle slot. In both builds this slot is not passed to ZwClose at function exit.

bp 0x14006E5D0     ; WfpRegNotifyCreate

Step through to 0x14006E6CF (success sets gFwAuditModeNotificationHandle) versus the failure path at 0x14006E6E6/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 @ 0x1400A425Cnot security-relevant

  • Adds the Feature_Firewall_BugFixes_25D_AuditMode_regKey_exists feature gate around the audit-mode second-key handling.
  • Feature enabled: skips EnableAuditMode query and change-notification registration when the Policy\Options key does not exist, and checks the WfpRegNotifyCreate NTSTATUS.
  • 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_IsEnabledDeviceUsageNoInline
  • Feature_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 with DesiredAccess = 0x20019 (KEY_READ) and sets its third out-parameter to 1 only when ZwOpenKey returns STATUS_SUCCESS; on STATUS_OBJECT_NAME_NOT_FOUND (0xC0000034) it returns success with that flag left 0. 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.