1. Overview

  • Unpatched Binary: ndis_unpatched.sys
  • Patched Binary: ndis_patched.sys
  • Overall Similarity: 0.9908
  • Diff Statistics:
  • Matched Functions: 3124
  • Changed Functions: 5
  • Identical Functions: 3119
  • Unmatched (Unpatched): 0
  • Unmatched (Patched): 0

Verdict: The only behavioral change in the NDIS control-device IRP dispatch routine (ndisDispatchRequest @ 0x1C001D550) is gated behind a Windows feature-staging flag (Feature_3473086778__private_IsEnabledDeviceUsage @ 0x1C003F56C). When that flag is disabled, the patched build behaves identically to the unpatched build (the per-open context is freed during IRP_MJ_CLEANUP). When the flag is enabled, the free is deferred from IRP_MJ_CLEANUP to IRP_MJ_CLOSE. Because the original free-at-cleanup path is retained in full and the change is a staged, config-gated rollout, this diff does not constitute a delivered security fix. The remaining four changed functions are Windows feature-staging/reporting library helpers. No reachable use-after-free is demonstrable from the code.


2. Nature of the Change

  • Severity: None (informational)
  • Change Class: Feature-staged object-lifetime adjustment (defense-in-depth, not delivered by default)
  • Affected Function: ndisDispatchRequest (NDIS control-device IRP dispatch handler) @ 0x1C001D550

What actually changed: ndisDispatchRequest is the dispatch routine for the NDIS control device. On IRP_MJ_CREATE (major function 0) it allocates a 0x38-byte NonPagedPoolNx per-open context with pool tag 'NDoC' (ExAllocatePoolWithTag) and stores it in the file object at offset +0x18 (FsContext), incrementing a global open counter.

In the unpatched build, IRP_MJ_CLEANUP (major function 0x12) tears down the context (unlinks it from the miniport binding list under a spinlock, drops the miniport reference, and releases the compartment sub-object at context+0x30 via ndisIfDereferenceCompartmentForUser), then unconditionally frees the context with ExFreePoolWithTag. IRP_MJ_CLOSE (major function 2) does nothing with the context.

The patched build inserts a call to the feature-staging gate Feature_3473086778__private_IsEnabledDeviceUsage in both paths: - In IRP_MJ_CLEANUP, after the same teardown, if the gate returns nonzero (feature enabled) the free is skipped and the IRP is completed; if it returns zero (feature disabled) the context is freed exactly as before. - In IRP_MJ_CLOSE, if the gate returns nonzero the context is loaded from FsContext, nulled, and freed here (the deferred free); if it returns zero, nothing is done (unpatched behavior).

Net effect: with the feature disabled the two builds are behaviorally identical (free at cleanup). With the feature enabled the free moves to the later IRP_MJ_CLOSE teardown point. Deferring a per-open context free from IRP_MJ_CLEANUP to IRP_MJ_CLOSE is a recognized object-lifetime hardening pattern, but here it is gated and the original path is preserved, so it is a staged rollout rather than an unconditionally delivered fix.

About the gate: Feature_3473086778__private_IsEnabledDeviceUsage is standard Windows feature-staging (velocity) infrastructure. It reads Feature_3473086778__private_featureState, tests bit 0x10 (state-determined), and either returns state & 1 or falls back to Feature_3473086778__private_IsEnabledFallback with default value 3. It is a compile/configuration feature switch. It is not a runtime check of in-flight asynchronous operations.

Reachability note: Within ndisDispatchRequest, IRP_MJ_DEVICE_CONTROL (major function 0xE) dispatches to ndisHandlePnPRequest (@ 0x1C012BDB8 in the unpatched build) in both builds; this path is unchanged. The cleanup path removes the context from its shared lists under a spinlock and drops the miniport/compartment references before freeing it. No path in this function was found that caches the raw context pointer beyond the lifetime of the IRP, so a reachable use-after-free tied to this free is not demonstrable from the disassembly.


3. Pseudocode Diff

Control-flow of the two builds. Comments describe the actual gate behavior.

// === UNPATCHED ndisDispatchRequest ===

// IRP_MJ_CLEANUP (major == 0x12)
void HandleCleanup(IRP* irp) {
    FileObject* file = irp->Tail.Overlay.FileObject;
    void* P = file->FsContext;              // file+0x18

    // Teardown: unlink from miniport binding list under spinlock,
    // drop miniport reference, release sub-object at P+0x30.

    file->FsContext = NULL;
    ExFreePoolWithTag(P, 0);                // free at cleanup
    global_open_count--;                    // dword_1C00E609C
    IofCompleteRequest(irp, 2);
}

// IRP_MJ_CLOSE (major == 2)
void HandleClose(IRP* irp) {
    // Does nothing with the context
    IofCompleteRequest(irp, 2);
}
// === PATCHED ndisDispatchRequest ===

// IRP_MJ_CLEANUP (major == 0x12)
void HandleCleanup(IRP* irp) {
    // Same teardown ...

    if (Feature_3473086778__private_IsEnabledDeviceUsage()) {
        // Feature ON: skip free, defer to IRP_MJ_CLOSE
        IofCompleteRequest(irp, 2);
    } else {
        // Feature OFF: free now, identical to unpatched
        FileObject* file = irp->Tail.Overlay.FileObject;
        void* P = file->FsContext;
        file->FsContext = NULL;
        ExFreePoolWithTag(P, 0);
        global_open_count--;                // dword_1C00E70DC
        IofCompleteRequest(irp, 2);
    }
}

// IRP_MJ_CLOSE (major == 2)
void HandleClose(IRP* irp) {
    if (Feature_3473086778__private_IsEnabledDeviceUsage()) {
        // Feature ON: free the deferred context here
        FileObject* file = irp->Tail.Overlay.FileObject;
        void* P = file->FsContext;
        file->FsContext = NULL;
        if (P) { ExFreePoolWithTag(P, 0); global_open_count--; }
    }
    // Feature OFF: nothing, identical to unpatched
    IofCompleteRequest(irp, 2);
}

4. Assembly Analysis

UNPATCHED ndisDispatchRequest — IRP_MJ_CLEANUP free path

00000001C001D61B  mov     rax, [r15+30h]              ; file object
00000001C001D61F  mov     rdi, [rax+18h]              ; rdi = P (FsContext)
00000001C001D623  mov     rbp, [rdi+20h]              ; P->0x20 (miniport binding)
00000001C001D627  cmp     [rdi+18h], ebx              ; P->0x18 == 0 ?
00000001C001D62A  jnz     loc_1C005749E               ; teardown (unlink, deref miniport)
00000001C001D630  mov     rcx, [rdi+30h]              ; P->0x30 sub-object
00000001C001D634  test    rcx, rcx
00000001C001D637  jnz     loc_1C00575E1              ; ndisIfDereferenceCompartmentForUser
00000001C001D63D  mov     rax, [r15+30h]
00000001C001D641  xor     edx, edx                    ; Tag = 0
00000001C001D643  mov     rcx, rdi                    ; P
00000001C001D646  mov     [rax+18h], rbx              ; FsContext = NULL
00000001C001D64A  call    cs:__imp_ExFreePoolWithTag  ; free at cleanup (unconditional)
00000001C001D656  lock dec cs:dword_1C00E609C         ; open count--
00000001C001D65D  jmp     loc_1C001D5BB               ; complete IRP

UNPATCHED ndisDispatchRequest — IRP_MJ_CLOSE path

00000001C001D5A1  sub     r8d, 2
00000001C001D5A5  jz      short loc_1C001D5BB          ; major==2: complete, no context handling

PATCHED ndisDispatchRequest — IRP_MJ_CLEANUP path (feature gate added)

00000001C001D732  mov     rcx, [rdi+30h]              ; P->0x30 sub-object
00000001C001D736  test    rcx, rcx
00000001C001D739  jz      short loc_1C001D744
00000001C001D73B  call    ?ndisIfDereferenceCompartmentForUser@@YAJPEAX@Z
00000001C001D740  mov     [rdi+30h], rbx
00000001C001D744  call    Feature_3473086778__private_IsEnabledDeviceUsage  ; feature-staging gate
00000001C001D749  test    eax, eax
00000001C001D74B  jnz     loc_1C001D827               ; feature ON: skip free, complete IRP
00000001C001D751  mov     rax, [r15+30h]
00000001C001D755  mov     rcx, rdi                    ; P
00000001C001D758  mov     [rax+18h], rbx              ; FsContext = NULL
00000001C001D75C  xor     edx, edx                    ; Tag = 0
00000001C001D75E  call    cs:__imp_ExFreePoolWithTag  ; feature OFF: free (== unpatched)
00000001C001D76A  lock dec cs:dword_1C00E70DC
00000001C001D771  jmp     loc_1C001D827

PATCHED ndisDispatchRequest — IRP_MJ_CLOSE path (feature gate added)

00000001C001D78C  call    Feature_3473086778__private_IsEnabledDeviceUsage
00000001C001D791  test    eax, eax
00000001C001D793  jz      loc_1C001D827               ; feature OFF: nothing (== unpatched)
00000001C001D799  mov     rax, [r15+30h]              ; file object
00000001C001D79D  mov     rcx, [rax+18h]              ; P from FsContext
00000001C001D7A1  mov     [rax+18h], rbx              ; FsContext = NULL
00000001C001D7A5  test    rcx, rcx
00000001C001D7A8  jz      short loc_1C001D76A         ; skip if already NULL
00000001C001D7AA  jmp     short loc_1C001D75C         ; free deferred context

The gate: Feature_3473086778__private_IsEnabledDeviceUsage @ 0x1C003F56C

00000001C003F576  mov     eax, cs:Feature_3473086778__private_featureState
00000001C003F580  test    al, 10h                     ; state determined?
00000001C003F582  jz      short loc_1C003F589
00000001C003F584  and     eax, 1                      ; return cached state bit
00000001C003F587  jmp     short loc_1C003F598
00000001C003F589  mov     edx, 3                      ; default enabled-state
00000001C003F593  call    Feature_3473086778__private_IsEnabledFallback

5. Trigger Conditions

No reachable use-after-free is demonstrable from the changed code, so there is no security trigger to describe. The observable difference between the builds is limited to when the per-open 'NDoC' context is freed (during IRP_MJ_CLEANUP versus IRP_MJ_CLOSE), and only when the Feature_3473086778 DeviceUsage feature is enabled. With the feature disabled the two builds are indistinguishable at runtime.


6. Exploit Primitive & Development Notes

No exploit primitive is supported by the binaries. The per-open context free within ndisDispatchRequest is preceded, in both builds, by teardown that unlinks the context from its shared miniport binding list under a spinlock and drops the miniport and compartment references. No path was found in which the raw context pointer is retained past the IRP that frees it. The feature-staged move of the free to IRP_MJ_CLOSE is an object-lifetime hardening (defense-in-depth), not evidence of a demonstrable freed-memory access. Claims of pool spray, arbitrary kernel read/write, information disclosure, or privilege escalation are not substantiated by the code and are omitted.


7. Debugger Notes

To observe the behavioral difference on the two builds, the relevant instructions are:

Unpatched free at cleanup:   ndis+ (ExFreePoolWithTag call) 0x1C001D64A
Patched cleanup gate:        0x1C001D744 (call Feature_3473086778__private_IsEnabledDeviceUsage)
Patched cleanup free:        0x1C001D75E (ExFreePoolWithTag, feature-OFF path)
Patched close gate:          0x1C001D78C (call Feature_3473086778__private_IsEnabledDeviceUsage)
Patched close free:          0x1C001D75E (ExFreePoolWithTag, feature-ON deferred path)

At the cleanup/close free instruction, rcx holds the pointer to the 0x38-byte 'NDoC' context being freed. The major function code is the byte at [r15] at function entry (0 = CREATE, 2 = CLOSE, 0xE = DEVICE_CONTROL, 0x12 = CLEANUP). These addresses only illustrate the timing difference; they do not correspond to any demonstrated fault.

Struct/Offset Notes

  • FileObject+0x18: per-open context pointer (FsContext).
  • Context (0x38 bytes, tag 'NDoC', NonPagedPoolNx):
  • +0x18: state field checked during cleanup.
  • +0x20: miniport binding node.
  • +0x30: compartment sub-object (released via ndisIfDereferenceCompartmentForUser).

8. Changed Functions — Full Triage

  • ndisDispatchRequest @ 0x1C001D550 (No security-relevant change): NDIS control-device IRP dispatch handler. Adds a Feature_3473086778__private_IsEnabledDeviceUsage gate in the IRP_MJ_CLEANUP and IRP_MJ_CLOSE paths that, when enabled, defers the per-open context free from cleanup to close. The original free-at-cleanup path is retained when the feature is disabled. Feature-staged; not delivered by default.
  • wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath @ 0x1C003DFC8 (unpatched) (Non-security): Windows feature-staging state-cache helper, rebuilt as part of adding the Feature_3473086778 DeviceUsage staging.
  • wil_details_FeatureReporting_ReportUsageToService @ 0x1C003DE58 (unpatched) (Non-security): Windows feature-staging usage-reporting helper, rebuilt for the same staging change.
  • Feature_NdisDatapathVerifier__private_ReportDeviceUsage @ 0x1C003DAC4 (unpatched) (Non-security): Feature-staging usage-report thunk for the DatapathVerifier feature; feature-reporting library churn.
  • Feature_ScreenON_NAPS__private_ReportDeviceUsage @ 0x1C003F8EC (unpatched) (Non-security): Feature-staging usage-report thunk for the ScreenON_NAPS feature; feature-reporting library churn.

(The four helper functions relocated between builds; the addresses above are from the unpatched build. In the patched build those exact addresses are occupied by different, unrelated functions.)


9. Added / Removed Functions

  • Removed: None.
  • Added: The patch introduces new Windows feature-staging helper functions that implement and support the Feature_3473086778 DeviceUsage gate, including Feature_3473086778__private_IsEnabledDeviceUsage (0x1C003F56C), Feature_3473086778__private_IsEnabledFallback (0x1C003F5A4), and the wil_details_StagingConfig_* / wil_details_GetCurrentFeatureEnabledState / wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState family. These are feature-staging configuration/state-cache plumbing, not security fixes.

Two additional functions (ndisEtwRegisterGuids, ndisDriverSystemDispatch) differ between builds only in a compile-time WPP trace-GUID symbol; this is tracing metadata churn with no control-flow change.

All security-relevant behavior remains contained in ndisDispatchRequest, whose only change is the feature-staged, config-gated deferral of the per-open context free.


10. Confidence & Caveats

  • Confidence Level: High. The changed logic in ndisDispatchRequest is fully accounted for: a single feature-staging gate that conditionally defers a free, with the original path retained. The gate is standard Windows feature-staging infrastructure, and the other four changed functions are feature-staging/reporting library helpers.
  • Direction: The patched build is not less strict. When the feature is enabled the free is moved to the later IRP_MJ_CLOSE point; when disabled behavior is identical to the unpatched build. This is not a regression.
  • Caveat: Whether the Feature_3473086778 DeviceUsage feature is enabled in any given deployment is a configuration/velocity decision not determinable from the binary. Because the original free-at-cleanup path is preserved and no reachable use-after-free was demonstrable, this diff is classified as a feature-staged object-lifetime change with no delivered security impact.