1. Overview

  • Unpatched Binary: mqac_unpatched.sys
  • Patched Binary: mqac_patched.sys
  • Overall Similarity Score: 0.6206 (62.06%)
  • Diff Statistics:
  • Matched Functions: 517
  • Changed Functions: 413
  • Identical Functions: 104
  • Unmatched Functions: 0 (in both directions)

Verdict: No security-relevant change was found. The two behaviors originally suspected as vulnerabilities are both artifacts of compiler inlining: the patched build inlines two small helper routines (CSMAllocator::ReleaseFreeHeaps and CTransaction::RestorePacket) into their callers, which changes the shape of the decompilation but not the semantics. Every validation, cleanup call, and error path present after the patch is already present, byte-for-byte in effect, before the patch. The remaining differences across the driver are toolchain churn: WIL feature-staging code, renamed WPP trace macros, alternate memset intrinsic variants, Control Flow Guard dispatch thunks, ProbeForRead inlining, and NonPagedPoolNx allocation modernization. None of these alters an attacker-reachable security decision.


2. Vulnerability Summary

Finding 1: Suspected Use-After-Free via incomplete cleanup — NOT A VULNERABILITY

  • Status: No security-relevant change (compiler inlining)
  • Affected Function: ACDeviceControl (IOCTL dispatcher) — unpatched @ 0x14000B100, patched @ 0x1C000C880, case 0x1965108F

Analysis: In the unpatched dispatcher, IOCTL 0x1965108F calls CSMAllocator::ReleaseFreeHeaps(*(DeviceExtension + 0x140)) (a single call). That helper, CSMAllocator::ReleaseFreeHeaps @ 0x1400117E0, is itself three lines: it calls CPoolAllocator::PurgeHeaps on this+0x10, this+0x50, and this+0x90. The patched dispatcher performs those same three CPoolAllocator::PurgeHeaps calls at +0x10, +0x50, +0x90 directly inside the case, because the compiler inlined ReleaseFreeHeaps. The single unpatched call therefore does exactly what the three patched calls do. No list entry or sub-context is left dangling in either build, so there is no use-after-free. CPoolAllocator::PurgeHeaps is an allocator-maintenance routine that releases already-free heap views; it is not object teardown that could leave live references stale.

Finding 2: Suspected null-pointer dereference in dispatch handler — NOT A VULNERABILITY

  • Status: No security-relevant change (compiler inlining)
  • Affected Function: ACPutRestoredPacket — unpatched @ 0x140008AD4, patched @ 0x1C0009F50

Analysis: ACPutRestoredPacket obtains a packet buffer via CQEntry::Buffer @ 0x14000263C, which returns NULL when the entry's handle field (*(this+0x10)) is 0xFFFFFFFF. Both builds immediately fold that NULL into v12 = (buffer + 4) & -(buffer != 0) and branch if (v12 == 0) return 0xC000009A; before any dereference of v12, so a NULL buffer is handled identically in both builds. On the transactional tail, the unpatched build calls CTransaction::RestorePacket @ 0x140021A64; the patched build inlines that function's body into ACPutRestoredPacket. The "added" NULL check (if (result) ... else return error) and the "added" sign check (if (*(result+0xC) >= 0)) are already present inside the unpatched CTransaction::RestorePacket (see the assembly in section 4). Nothing was added; a helper was inlined.


3. Pseudocode Diff

Finding 1: IOCTL case 0x1965108F (dispatcher)

// === UNPATCHED ACDeviceControl @ 0x14000B100, case 0x1965108F ===
case 0x1965108Fu:
    CSMAllocator::ReleaseFreeHeaps(*((CSMAllocator **)a1->DeviceExtension + 40));  // +0x140
    v13 = 0;
    goto LABEL_239;

// === UNPATCHED CSMAllocator::ReleaseFreeHeaps @ 0x1400117E0 (the callee) ===
void CSMAllocator::ReleaseFreeHeaps(CSMAllocator *this)
{
    CPoolAllocator::PurgeHeaps((__int64)this + 16);    // +0x10
    CPoolAllocator::PurgeHeaps((__int64)this + 80);    // +0x50
    CPoolAllocator::PurgeHeaps((__int64)this + 144);   // +0x90
}

// === PATCHED ACDeviceControl @ 0x1C000C880, case 0x1965108F (ReleaseFreeHeaps inlined) ===
case 0x1965108Fu:
    v24 = *((_QWORD *)a1->DeviceExtension + 40);       // +0x140
    CPoolAllocator::PurgeHeaps(v24 + 16);              // +0x10
    CPoolAllocator::PurgeHeaps(v24 + 80);              // +0x50
    CPoolAllocator::PurgeHeaps(v24 + 144);             // +0x90
    v12 = 0;
    goto LABEL_247;

Both builds purge the same three offsets (+0x10, +0x50, +0x90) of the context at DeviceExtension+0x140. Semantically identical.

Finding 2: ACPutRestoredPacket — buffer NULL guard and transactional tail

// === Buffer NULL guard — IDENTICAL in both builds ===
// UNPATCHED ACPutRestoredPacket @ 0x140008AD4:
v11 = CQEntry::Buffer(v10);                             // returns NULL if *(v10+0x10)==0xFFFFFFFF
v12 = ((unsigned __int64)v11 + 4) & -(__int64)(v11 != nullptr);
if ( v12 == 0 )
    return 3221225626LL;                                // 0xC000009A, before any deref

// PATCHED ACPutRestoredPacket @ 0x1C0009F50 (CQEntry::Buffer inlined):
if ( *((_DWORD *)v10 + 4) == -1 ) AccessibleBuffer = 0;
else AccessibleBuffer = CMMFAllocator::GetAccessibleBuffer(*((_QWORD *)v10 + 3), *((unsigned int *)v10 + 4));
v12 = (AccessibleBuffer + 4) & -(__int64)(AccessibleBuffer != 0);
if ( v12 == 0 )
    return 3221225626LL;                                // 0xC000009A, same guard

// === Transactional tail ===
// UNPATCHED: calls the helper (which contains the null + sign checks):
if ( operator!=<BOID>(...) && (v21 = CTransaction::Find(a1, (BOID *)(v12 + 16))) != nullptr )
    return CTransaction::RestorePacket(v21, v5, v10, v15);     // helper @ 0x140021A64
else
    return CQueue::RestorePacket(v5, v10, v15);

// UNPATCHED CTransaction::RestorePacket @ 0x140021A64 (the callee) already contains both checks:
v8 = CQEntry::Buffer(a3);
v9 = ((unsigned __int64)v8 + 4) & -(__int64)(v8 != nullptr);
if ( v9 == 0 ) return 3221225626LL;                    // NULL check
if ( *(char *)(v9 + 12) >= 0 ) { CQueue::RestorePacket(...); return CTransaction::ProcessReceivedPacket(...); }  // sign check
else { CTransaction::SendPacket(...); return 0; }

// PATCHED ACPutRestoredPacket @ 0x1C0009F50 (CTransaction::RestorePacket inlined):
if ( RtlCompareMemory((v12 + 16), &unk, 0x10) == 16 ) return CQueue::RestorePacket(v5, v10, v15);
v24 = CTransaction::Find(a1, (BOID *)(v12 + 16));
if ( v24 == nullptr ) return CQueue::RestorePacket(v5, v10, v15);
// re-fetch buffer, then the SAME null + sign checks that were inside the helper:
if ( (v26 & -(v27 != 0)) != 0 ) {
    if ( *(char *)((v26 & ...) + 0xC) >= 0 ) { CQueue::RestorePacket(...); return CTransaction::ProcessReceivedPacket(v24, v10, nullptr); }
    else CTransaction::SendPacket(v24, v5, v10);
} else return -1073741670;                              // 0xC000009A

4. Assembly Analysis

Finding 1: dispatcher case 0x1965108F and the inlined helper

; --- UNPATCHED ACDeviceControl @ 0x14000B100, case 0x1965108F ---
000000014000B6A5  mov     rcx, [r15+40h]
000000014000B6A9  mov     rcx, [rcx+140h]          ; DeviceExtension+0x140
000000014000B6B0  call    ?ReleaseFreeHeaps@CSMAllocator@@QEAAXXZ   ; single call
000000014000B6B5  xor     ebx, ebx                 ; STATUS_SUCCESS

; --- UNPATCHED CSMAllocator::ReleaseFreeHeaps @ 0x1400117E0 (the callee) ---
00000001400117E9  add     rcx, 10h
00000001400117ED  call    ?PurgeHeaps@CPoolAllocator@@...           ; +0x10
00000001400117F2  lea     rcx, [rbx+50h]
00000001400117F6  call    ?PurgeHeaps@CPoolAllocator@@...           ; +0x50
00000001400117FB  lea     rcx, [rbx+90h]
0000000140011802  call    ?PurgeHeaps@CPoolAllocator@@...           ; +0x90

; --- PATCHED ACDeviceControl @ 0x1C000C880, case 0x1965108F (ReleaseFreeHeaps inlined) ---
00000001C000CDC1  mov     rax, [rsi+40h]
00000001C000CDC5  mov     rbx, [rax+140h]          ; DeviceExtension+0x140
00000001C000CDCC  lea     rcx, [rbx+10h]
00000001C000CDD0  call    ?PurgeHeaps@CPoolAllocator@@...           ; +0x10
00000001C000CDD5  lea     rcx, [rbx+50h]
00000001C000CDD9  call    ?PurgeHeaps@CPoolAllocator@@...           ; +0x50
00000001C000CDDE  lea     rcx, [rbx+90h]
00000001C000CDE5  call    ?PurgeHeaps@CPoolAllocator@@...           ; +0x90
00000001C000CDEA  xor     ebx, ebx                 ; STATUS_SUCCESS

The unpatched single ReleaseFreeHeaps call and the patched three PurgeHeaps calls are the same three-offset purge; the only difference is inlining.

Finding 2: the NULL + sign checks already present in the unpatched helper

; --- UNPATCHED CTransaction::RestorePacket @ 0x140021A64 ---
0000000140021A87  call    ?Buffer@CQEntry@@...      ; get packet buffer
0000000140021A8C  lea     r10, [rax+4]
0000000140021A90  neg     rax
0000000140021A93  sbb     rax, rax
0000000140021A96  and     rax, r10                 ; (buf+4) & -(buf!=0)
0000000140021A99  jnz     short loc_140021AA2      ; NULL check already here
0000000140021A9B  mov     eax, 0C000009Ah          ; error return on NULL
0000000140021AA0  jmp     short loc_140021AD7
0000000140021AA2  mov     al, [rax+0Ch]
0000000140021AA5  test    al, al
0000000140021AA7  jns     short loc_140021ABB      ; sign check already here

Address 0x140021A99 is the NULL check and 0x140021AA50x140021AA7 is the sign check that the original report attributed to the patch. Both exist in the unpatched build; the patch merely inlines this routine into ACPutRestoredPacket.


5. Trigger Conditions

No reachable vulnerable condition exists in either build for the two behaviors examined.

  • Finding 1: Sending IOCTL 0x1965108F to \\.\MQAC purges the three free-heap lists at DeviceExtension+0x140 (+0x10, +0x50, +0x90) in both the unpatched and patched drivers. No dangling reference is produced, so no follow-up IOCTL can reach freed memory as a result of this call.
  • Finding 2: Reaching ACPutRestoredPacket with an entry whose handle field is 0xFFFFFFFF causes CQEntry::Buffer to return NULL, which both builds detect via if (v12 == 0) return 0xC000009A; before dereferencing. No NULL dereference occurs in either build.

6. Exploit Primitive & Development Notes

No exploit primitive is available from the behaviors examined. Neither the free-heap purge (Finding 1) nor the buffer/transaction handling (Finding 2) yields a use-after-free, out-of-bounds access, or NULL dereference in either build, because the cleanup and validation are semantically identical across the two builds. No arbitrary read/write, information leak, or privilege-escalation primitive was demonstrated or is supported by the code.


7. Debugger PoC Playbook

No proof-of-concept is applicable, because no vulnerability was confirmed. For anyone independently re-verifying the two behaviors:

  • Finding 1: Break on mqac_unpatched.sys!CSMAllocator::ReleaseFreeHeaps (0x1400117E0) and on the patched dispatcher case (0x1C000CDCC). In both cases the first argument is DeviceExtension+0x140, and CPoolAllocator::PurgeHeaps is invoked on +0x10, +0x50, +0x90. Confirm the free-heap lists at those offsets are drained equivalently.
  • Finding 2: Break on mqac_unpatched.sys!CTransaction::RestorePacket (0x140021A64). Confirm the NULL branch at 0x140021A99 and the sign branch at 0x140021AA5 execute before any dereference of the buffer, matching the inlined logic in the patched ACPutRestoredPacket (0x1C0009F50).

8. Changed Functions — Full Triage

The two functions originally flagged as security-relevant are refactors only. The remaining changes across the driver are toolchain/rebuild churn. Function names below are the demangled symbols.

  • ACDeviceControl (IOCTL dispatcher) — unpatched 0x14000B100, patched 0x1C000C880
  • Change Type: Not security-relevant (inlining).
  • Notes: Case 0x1965108F inlines CSMAllocator::ReleaseFreeHeaps into three CPoolAllocator::PurgeHeaps calls at +0x10/+0x50/+0x90; identical to the unpatched single-call behavior. Other cases show ProbeForRead inlining and buffer-size checks that are byte-equivalent in effect.
  • ACPutRestoredPacket (restored-packet handler) — unpatched 0x140008AD4, patched 0x1C0009F50
  • Change Type: Not security-relevant (inlining).
  • Notes: Inlines CQEntry::Buffer, CQEntry::ReleaseBuffer, and CTransaction::RestorePacket. The NULL check and sign check exist unchanged in the unpatched CTransaction::RestorePacket @ 0x140021A64.
  • ACCreateRemoteProxy0x14000644C
  • Change Type: Not security-relevant (modernization).
  • Notes: ProbeForRead inlined; ExAllocatePoolWithTag replaced with ExAllocatePoolWithTagPriority(NonPagedPoolNx, ...); custom free wrapper replaced with direct ExFreePoolWithTag. No semantic change.
  • ACBeginGetPacket2Remote0x140005328
  • Change Type: Not security-relevant.
  • Notes: GUID comparison helper inlined to RtlCompareMemory; equivalent.
  • ACAssociateQueue0x140004E38
  • Change Type: Not security-relevant (modernization).
  • Notes: ProbeForRead inlined; error-path free modernized to ExFreePoolWithTag; share-access checks preserved.
  • ACCreateQueue0x140005F20
  • Change Type: Not security-relevant (modernization).
  • Notes: Probes inlined; pool allocation modernized to NonPagedPoolNx. Index-guard logic present in both builds.
  • CUserHeader constructor0x1400138D8
  • Change Type: Cosmetic.
  • Notes: Subtraction-based pointer comparison rewritten as direct equality; semantically equivalent.
  • ACSendMessage0x140009478
  • Change Type: Not security-relevant.
  • Notes: Helper inlining and ProbeForRead inlining. The local zero-fill is memset(..., 0x118) in both builds (identical size; the original claim of a 0x1100x118 extension and an added +0x78 zero-init is not present).
  • ACPutRemotePacket0x140008950
  • Change Type: Not security-relevant.
  • Notes: Helper inlining, inlined refcount decrement, and added WPP trace of the refcount value.
  • Driver-wide churn (many functions): WIL feature-staging code (Feature_*__private_IsEnabled*, wil_details_*), renamed WPP trace macros (e.g. WPP_SF_qddWPP_SF_qDd), alternate memset intrinsic variants (__memset_repmovs, __memset_spec_ermsb, etc.), Control Flow Guard dispatch thunks (_guard_dispatch_icall), and ProbeForRead/pool-allocation modernization. These are rebuild artifacts, not security fixes.

9. Unmatched Functions

There were no unmatched (added or removed) functions in this patch by content. The apparent name-only differences (functions present under one symbol in one build and inlined in the other, plus renamed WPP/WIL helper symbols) are consistent with a full toolchain rebuild rather than a structural code change.


10. Confidence & Caveats

  • Confidence Level: High. Both suspected findings were disproven directly against the disassembly and decompilation of both builds: the "missing" cleanup calls and the "missing" NULL/sign checks are present in the unpatched build inside helper routines that the patched build inlines.
  • Basis: Real symbols are present in both builds. Finding 1 rests on CSMAllocator::ReleaseFreeHeaps @ 0x1400117E0 (three PurgeHeaps calls) versus the inlined patched dispatcher case at 0x1C000CDCC. Finding 2 rests on CTransaction::RestorePacket @ 0x140021A64 (NULL check at 0x140021A99, sign check at 0x140021AA5) versus the inlined patched ACPutRestoredPacket @ 0x1C0009F50.
  • Scope: The remaining changed functions were surveyed by content across the relocation; no changed function introduces a new attacker-reachable validation, bounds check, or object-lifetime fix. No omitted security-relevant change was found.