1. Overview

  • Unpatched Binary ID: ndis_unpatched.sys
  • Patched Binary ID: ndis_patched.sys
  • Overall Similarity Score: 0.9908
  • Diff Statistics: 5 changed functions, 10 removed functions (all Windows feature-staging config-query helpers), 0 added functions. Every other function that appears to differ does so only by relocated absolute addresses; no other function changed logic.
  • Verdict: The delta between the two builds is a single coherent Windows feature-staging (WIL) change. The DeviceUsage staging feature (feature id 3473086778) is retired: its enable-check gate and the associated staging-config machinery are removed, and the behavior that the feature previously gated is hardcoded on. Inside the \Device\Ndis dispatch routine (ndisDispatchRequest, 0x1C001D550) this hard-enables the "free the per-handle context during IRP_MJ_CLOSE" path and drops the "free during IRP_MJ_CLEANUP" path. The move from CLEANUP to CLOSE is the standard object-lifecycle placement, but the change is delivered as a feature-flag retirement, the CLEANUP-free behavior was only ever the feature-disabled fallback, and no reachable use-after-free is demonstrable from the binaries. There is no demonstrated security-relevant fix here.

2. Change Summary

  • Severity: None (informational). No demonstrable reachable impact.
  • Change Class: Windows feature-staging (WIL) retirement / object-lifecycle placement change.
  • Affected Function: ndisDispatchRequest @ 0x1C001D550 (IRP dispatch routine for the \Device\Ndis control device).

What changed

The per-handle context (FsContext) is a 0x38-byte NonPagedPoolNx allocation with pool tag 'NDoC' (0x636F444E), allocated in the IRP_MJ_CREATE path and stored in FileObject->FsContext.

In the unpatched build, whether that allocation is freed during IRP_MJ_CLEANUP or during IRP_MJ_CLOSE is selected at runtime by a call to Feature_3473086778__private_IsEnabledDeviceUsage (@ 0x1C003F56C). That function is a standard WIL feature-staging gate: it reads the cached feature state, and when the state bit is not yet cached it falls back to Feature_3473086778__private_IsEnabledFallback. Its two return values drive two mirror-image code paths:

  • Feature enabled (returns non-zero): IRP_MJ_CLEANUP skips the free and only completes the IRP; IRP_MJ_CLOSE performs the free. (This is the deferred-to-CLOSE behavior.)
  • Feature disabled (returns zero): IRP_MJ_CLEANUP performs the free; IRP_MJ_CLOSE skips it.

In the patched build the feature is retired. The gate call is gone from both paths, along with the entire staging-config query machinery (10 functions removed). IRP_MJ_CLEANUP now only performs list/spinlock/miniport-reference teardown and completes the IRP; IRP_MJ_CLOSE unconditionally frees FsContext (when non-NULL). This is exactly the feature-enabled behavior made permanent.

Why this is not a demonstrated security fix

  • The selection is a WIL feature-staging flag, not an application-state or attacker-influenced condition. Whether the "free during CLEANUP" fallback is ever taken at runtime depends on the feature's staged configuration, which is not determinable from the binaries.
  • The deferred-to-CLOSE behavior already exists in the unpatched build (it is the feature-enabled branch). The patch delivers a staged feature, it does not add a check that was absent.
  • No reachable use-after-free is demonstrable. Freeing a file-object context during IRP_MJ_CLEANUP is only unsafe if some still-outstanding reference to it survives past CLEANUP; nothing in these two functions establishes such an outstanding reference, and the DEVICE_CONTROL path here does not create one that is shown to reference FsContext after CLEANUP.

Attack Surface

\Device\Ndis is reachable via its symbolic link. The dispatch routine handles IRP_MJ_CREATE (major 0), IRP_MJ_CLOSE (2), IRP_MJ_DEVICE_CONTROL (0x0E), IRP_MJ_INTERNAL_DEVICE_CONTROL (0x0F, completed with no work), IRP_MJ_CLEANUP (0x12), and IRP_MJ_SYSTEM_CONTROL (0x17). The DEVICE_CONTROL path calls ndisHandlePnPRequest (@ 0x1C012CDB8), the NDIS PnP-request handler. The specific behavior of any request that would keep a live reference to FsContext across CLEANUP is not established by this diff.

3. Pseudocode Diff

The following pseudocode reflects the logic in ndisDispatchRequest (0x1C001D550). The condition is the WIL feature gate, not a general runtime flag.

Unpatched Code:

NTSTATUS ndisDispatchRequest(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp);

    if (ioStack->MajorFunction == IRP_MJ_CLEANUP) {
        // ... miniport-reference / list / spinlock teardown ...
        if (Feature_3473086778__private_IsEnabledDeviceUsage() == FALSE) {
            // feature-disabled fallback: free here
            ExFreePoolWithTag(FsContext, 0);
            ioStack->FileObject->FsContext = NULL;
        }
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
    }
    else if (ioStack->MajorFunction == IRP_MJ_CLOSE) {
        if (Feature_3473086778__private_IsEnabledDeviceUsage() == TRUE) {
            PVOID p = ioStack->FileObject->FsContext;
            ioStack->FileObject->FsContext = NULL;
            if (p) ExFreePoolWithTag(p, 0);
        }
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
    }
    // ...
}

Patched Code:

NTSTATUS ndisDispatchRequest(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp);

    if (ioStack->MajorFunction == IRP_MJ_CLEANUP) {
        // ... miniport-reference / list / spinlock teardown only; no free ...
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
    }
    else if (ioStack->MajorFunction == IRP_MJ_CLOSE) {
        // WIL gate removed: always free here
        PVOID p = ioStack->FileObject->FsContext;
        ioStack->FileObject->FsContext = NULL;
        if (p) ExFreePoolWithTag(p, 0);
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
    }
    // ...
}

4. Assembly Analysis

Real disassembly from the two builds. In the unpatched build the CLEANUP and CLOSE paths share the free site at 0x1C001D75C/0x1C001D75E; both are guarded by the WIL feature gate.

Unpatched IRP_MJ_CLEANUP Path (major 0x12)

0x1C001D744  call    Feature_3473086778__private_IsEnabledDeviceUsage
0x1C001D749  test    eax, eax
0x1C001D74B  jnz     loc_1C001D827            ; feature ENABLED -> skip free, complete
0x1C001D751  mov     rax, [r15+30h]           ; FileObject
0x1C001D755  mov     rcx, rdi                 ; FsContext
0x1C001D758  mov     [rax+18h], rbx           ; FileObject->FsContext = NULL
0x1C001D75C  xor     edx, edx                 ; Tag = 0
0x1C001D75E  call    cs:__imp_ExFreePoolWithTag  ; free (feature-disabled fallback only)
0x1C001D76A  lock dec cs:dword_1C00E70DC       ; decrement global counter
0x1C001D771  jmp     loc_1C001D827

Unpatched IRP_MJ_CLOSE Path (major 2)

0x1C001D78C  call    Feature_3473086778__private_IsEnabledDeviceUsage
0x1C001D791  test    eax, eax
0x1C001D793  jz      loc_1C001D827            ; feature DISABLED -> skip free, complete
0x1C001D799  mov     rax, [r15+30h]           ; FileObject
0x1C001D79D  mov     rcx, [rax+18h]           ; FsContext
0x1C001D7A1  mov     [rax+18h], rbx           ; FileObject->FsContext = NULL
0x1C001D7A5  test    rcx, rcx
0x1C001D7A8  jz      short loc_1C001D76A      ; NULL -> just decrement
0x1C001D7AA  jmp     short loc_1C001D75C      ; -> shared free at 0x1C001D75C/75E

Patched IRP_MJ_CLOSE Path (major 2) — unconditional free, no feature gate

0x1C001D760  mov     rax, [rdi+30h]           ; FileObject
0x1C001D764  mov     rcx, [rax+18h]           ; FsContext
0x1C001D768  mov     [rax+18h], rbx           ; FileObject->FsContext = NULL
0x1C001D76C  test    rcx, rcx
0x1C001D76F  jz      short loc_1C001D77F
0x1C001D771  xor     edx, edx                 ; Tag = 0
0x1C001D773  call    cs:__imp_ExFreePoolWithTag
0x1C001D77F  lock dec cs:dword_1C00E609C
0x1C001D786  jmp     short loc_1C001D804

Patched IRP_MJ_CLEANUP Path (major 0x12) — teardown then complete, no free

0x1C001D72F  mov     rcx, [rdi+30h]           ; sub-object at FsContext+0x30
0x1C001D733  test    rcx, rcx
0x1C001D736  jz      loc_1C001D804            ; none -> complete
0x1C001D73C  call    ?ndisIfDereferenceCompartmentForUser@@YAJPEAX@Z
0x1C001D741  mov     [rdi+30h], rbx           ; FsContext+0x30 = NULL
0x1C001D745  jmp     loc_1C001D804            ; complete, no FsContext free

5. Trigger / Reachability Notes

  • \Device\Ndis is opened via CreateFile on its symbolic link (IRP_MJ_CREATE, major 0), which allocates the FsContext (0x38 bytes, tag 'NDoC', NonPagedPoolNx) and stores it in FileObject->FsContext.
  • Closing the last handle sends IRP_MJ_CLEANUP (major 0x12); the final dereference sends IRP_MJ_CLOSE (major 2).
  • Whether the unpatched build frees FsContext during CLEANUP or during CLOSE is decided solely by the WIL feature gate Feature_3473086778__private_IsEnabledDeviceUsage. This is a staged-feature configuration value, not an attacker-controlled condition, and its runtime default is not determinable from the binary.
  • No path in this routine is shown to leave a live reference to FsContext outstanding past IRP_MJ_CLEANUP, so a use-after-free arising from the CLEANUP-free fallback is not demonstrable here. The DEVICE_CONTROL handler on this device is ndisHandlePnPRequest (@ 0x1C012CDB8).

6. Impact Assessment

  • No demonstrable reachable primitive. The change relocates a pool free from IRP_MJ_CLEANUP to IRP_MJ_CLOSE and does so by retiring a WIL feature. The deferred-to-CLOSE behavior already existed in the unpatched build as the feature-enabled branch.
  • Moving a file-object-context free from CLEANUP to CLOSE is the conventional object-lifecycle placement and is defensively preferable, but no outstanding reference surviving CLEANUP is established in these binaries, so no use-after-free is proven and no exploit primitive is supported.
  • No information leak, no bounds/length/allocation change, and no attacker-controlled condition is involved.

7. Analyst Playbook

For an analyst inspecting the unpatched binary:

Breakpoints

bp ndis!ndisDispatchRequest

Why: main dispatch for \Device\Ndis. At entry rdx = IRP, r15 = [rdx+0xB8] (current IO_STACK_LOCATION), and r8d = byte [r15] = MajorFunction. Watch r8d to distinguish CREATE(0)/CLOSE(2)/DEVICE_CONTROL(0x0E)/CLEANUP(0x12)/SYSTEM_CONTROL(0x17).

bp ndis+0x3f56c

Why: Feature_3473086778__private_IsEnabledDeviceUsage, the WIL feature gate. eax at return selects which path frees FsContext: non-zero defers the free to CLOSE, zero frees during CLEANUP.

bp ndis+0x1d75e

Why: the shared ExFreePoolWithTag site used by both the CLEANUP-fallback and the CLOSE path in the unpatched build. rcx holds the FsContext pointer.

What to Inspect

  • At ndis!ndisDispatchRequest entry: r8d (MajorFunction). 0x00 = CREATE, 0x02 = CLOSE, 0x0E = DEVICE_CONTROL, 0x12 = CLEANUP, 0x17 = SYSTEM_CONTROL.
  • At 0x1C001D75E: rcx holds the FsContext pointer being freed. !pool rcx should show a 0x38-byte allocation with tag 'NDoC' (0x636F444E).

Key Instructions/Offsets

  • 0x1C001D744: call Feature_3473086778__private_IsEnabledDeviceUsage in the CLEANUP path (removed in the patched build).
  • 0x1C001D78C: the same gate call in the CLOSE path (removed in the patched build).
  • 0x1C001D75E: the shared ExFreePoolWithTag call (unpatched).
  • 0x1C001D780: call ndisHandlePnPRequest — the DEVICE_CONTROL handler.
  • 0x1C001D818: mov [rax+18h], rdi — stores FsContext into FileObject->FsContext during CREATE.

Struct/Offset Notes

The FsContext structure (0x38 bytes, tag 'NDoC'), from observed accesses in ndisDispatchRequest: - +0x00 / +0x01 (bytes): access-check results written during CREATE by two ndisCheckAccess calls (against ndisSecurityDescriptor and unk_1C00E6E48). - +0x08 (LIST_ENTRY): list linkage unlinked under the miniport spinlock during CLEANUP. - +0x18 (dword): count/state field (cmp [rdi+18h], ebx, and dec dword [r14+18h] on the miniport block). - +0x1C (dword): compared as a count (mov r8d, [rdi+1Ch]; test; jle). - +0x20 (pointer): _NDIS_MINIPORT_BLOCK pointer; the block's spinlock is at +0x1168. - +0x30 (pointer): sub-object released via ndisIfDereferenceCompartmentForUser during CLEANUP.

8. Changed Functions — Full Triage

All five changed functions are Windows feature-staging (WIL) machinery centered on the DeviceUsage feature retirement.

  • ndisDispatchRequest @ 0x1C001D550 (Sim: 0.93): \Device\Ndis IRP dispatch. The two Feature_3473086778__private_IsEnabledDeviceUsage gate calls are removed; FsContext is now freed unconditionally in IRP_MJ_CLOSE and never in IRP_MJ_CLEANUP. Remaining differences are compiler register re-allocation from that removal. This is the feature-enabled behavior made permanent; not a demonstrated security fix.
  • wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath @ 0x1C003E254 (Sim: 0.8259): WIL feature-state-cache helper. The reporting-kind computation branch is removed and the value is hardcoded to 0x10; the corresponding argument is dropped. Feature-cache plumbing, not security-relevant.
  • wil_details_FeatureReporting_ReportUsageToService @ 0x1C003DFE4 (Sim: 0.9868): WIL usage-telemetry helper. The reporting "kind" is hardcoded to 3 internally instead of taken from an argument. Telemetry plumbing, not security-relevant.
  • Feature_NdisDatapathVerifier__private_ReportDeviceUsage @ 0x1C003DC44 (Sim: 0.9704): WIL usage-reporting caller updated to match the helper's new hardcoded-argument convention. Not security-relevant.
  • Feature_ScreenON_NAPS__private_ReportDeviceUsage @ 0x1C004030C (Sim: 0.9822): the same caller-side change applied to a different feature-reporting object. Not security-relevant.

An independent content-level diff of both builds (matching by function content across relocations, ignoring address-only differences) confirms these are the only functions with logic changes. Every other apparently-differing function differs solely by relocated absolute addresses.

9. Removed / Added Functions

Ten functions present in the unpatched build are absent from the patched build; all are WIL staging-config query machinery removed when the DeviceUsage feature was retired:

  • Feature_3473086778__private_IsEnabledDeviceUsage
  • Feature_3473086778__private_IsEnabledFallback
  • wil_RtlStagingConfig_QueryFeatureState
  • wil_StagingConfig_QueryFeatureState
  • wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState
  • wil_details_GetCurrentFeatureEnabledState
  • wil_details_IsEnabledFallback
  • wil_details_StagingConfigFeature_HasUniqueState
  • wil_details_StagingConfig_Load
  • wil_details_StagingConfig_QueryFeatureState

No functions were added.

10. Confidence & Caveats

  • Confidence: High that this is a WIL feature-staging retirement and not a demonstrated security fix. The gate Feature_3473086778__private_IsEnabledDeviceUsage and the entire staging-config query path are removed, and the retained behavior is exactly the feature-enabled branch that already existed in the unpatched build.
  • Caveat: Relocating a file-object-context free from IRP_MJ_CLEANUP to IRP_MJ_CLOSE is the conventional and defensively preferable object-lifecycle placement. If a real outstanding reference to FsContext survived past CLEANUP, the CLEANUP-free fallback would be a use-after-free. No such surviving reference is established by these binaries, so this remains an unproven lifecycle hardening delivered via feature-flag retirement, not a demonstrated vulnerability fix. Its severity is informational.