ndkping.sys: a null systembuffer deref you can blue-screen on demand

part of the glaurung windows driver findings catalog. method narrative: reading all of notepad.exe with an llm.

summary

driverNDKPing.sys — NetworkDirect / NDK diagnostic test harness, version 10.0.26100.1150 (Windows 11 24H2)
classCWE-476 (NULL pointer dereference)
bugioctl dispatcher dereferences Irp->AssociatedIrp.SystemBuffer without a null check; METHOD_BUFFERED + zero in/out length leaves it NULL
reachadministrator only (device SDDL grants SYSTEM and Administrators only)
primitivekernel NULL-page read fault → bugcheck 0x3B → forced reboot
cvss 3.1AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H = 4.4 (medium)
prooflive BSOD on a Windows 11 24H2 VM, faulting RIP matches the predicted case-body offset
disclosurereported to MSRC (case VULN-190756); declined

the bug

NDKPing.sys is a diagnostic driver for the NetworkDirect / NDK stack. it has a single IRP_MJ_DEVICE_CONTROL handler at 0x1c0001280 that dispatches six ioctl codes. the handler loads the request’s system buffer once, up front, and never checks it for null:

mov   r8,  [rdx+0x18]       ; r8 = Irp->AssociatedIrp.SystemBuffer   <-- no null check
mov   r9d, [rcx+0x18]       ; r9d = IoControlCode
sub   r9d, 0x00220404       ; dispatch on the control code...
je    0x1c000133b           ; 0x220404 -> case body
sub   r9d, 4
je    0x1c000132a           ; 0x220408 -> case body
...

every one of the six case bodies starts with the same first instruction:

cmp   byte ptr [r8+0x28], 0    ; r8 = SystemBuffer, dereferenced immediately

the dispatcher never reads InputBufferLength or OutputBufferLength before it jumps. that matters because of the windows I/O manager’s allocation contract for METHOD_BUFFERED: it only allocates the system buffer when the request has a nonzero input or output length. issue the ioctl with both lengths zero and Irp->AssociatedIrp.SystemBuffer stays NULL. the handler loads r8 = NULL, jumps into a case body, and executes cmp byte ptr [NULL+0x28], 0 — a read from address 0x28 in the null page. page fault in ring 0, bugcheck 0x3B SYSTEM_SERVICE_EXCEPTION, reboot.

all six control codes (0x220404, 0x220408, 0x22040c, 0x220410, 0x220414, 0x220418) reach an identical deref; they differ only in which case-body address faults.

who can reach it

the device, \Device\NDKPing, has this SDDL:

D:P(A;;GA;;;SY)(A;;GA;;;BA)

SY is local system; BA is BUILTIN\Administrators. there is no Everyone or World ACE — a non-admin CreateFile on \\.\NDKPing returns ERROR_ACCESS_DENIED. on top of the ACL, the service ships Manual/Stopped, so the device does not even exist until someone with rights starts it. reaching this bug requires administrator privileges twice over.

what it actually buys you

a clean, on-demand blue screen. the whole trigger is ~30 lines:

HANDLE h = CreateFileW(L"\\\\.\\NDKPing", GENERIC_READ | GENERIC_WRITE,
                       0, NULL, OPEN_EXISTING, 0, NULL);
DWORD ret = 0;
DeviceIoControl(h, 0x220404, NULL, 0, NULL, 0, &ret, NULL);   // never returns; box bugchecks

it is ~100% reliable, first try, no race window, no setup beyond starting the service. and it is the whole impact. the faulting instruction is a read, so there is no write primitive and no path to code execution; it never returns to user mode. there is no information disclosure — the null-page read just faults. and there is no privilege escalation, because the thread that triggers it was already administrator; the bugcheck terminates it, it does not elevate it.

how glaurung found it

this one came out of a structural pass rather than an llm reading source. glaurung’s windows ioctl-taint analysis lifts every driver function to its own IR and runs an abstract interpretation that tracks IRP-derived values — Irp, the stack location, SystemBuffer, the type-3 input buffer, and so on — through the dispatcher. it flags any load or store whose base register is tainted SystemBuffer and is not guarded by a preceding null-or-length check that would imply the pointer is non-null.

across a sweep of several hundred system drivers, this produced thousands of candidate sites; NDKPing ranked near the top on signal quality. the tell was a perfect focal ratio — the function had exactly six tainted, unguarded derefs, and all six were the first instruction of a dispatch case body. that is the structural fingerprint of “switch on ioctl code, then immediately trust the buffer,” which is precisely the missing-null-and-length-check pattern. the analysis had to be careful to distinguish [Irp+0x18] (the system buffer) from [stack_loc+0x18] (the ioctl code) after Irp was aliased into another register, which the register-kill-on-overwrite logic handled.

the candidate was then confirmed against capstone disassembly — the six case-body offsets and the shared cmp [r8+0x28] were read off the real bytes, not the lifter’s output.

reproduction

unlike the ndfltr finding, this one has a live splat. a ~30-line PoC, compiled with MSVC, run from an elevated console on a Windows 11 24H2 VM (build 10.0.26100.1150) after Start-Service NDKPing, bugchecks the guest within milliseconds. the captured bugcheck is 0x3B SYSTEM_SERVICE_EXCEPTION with parameters (0xC0000005, 0xFFFFF80468A5133B, ...) — status STATUS_ACCESS_VIOLATION, and a faulting RIP that, against the observed driver load base, lands byte-exactly on the predicted case-body offset 0x133b for ioctl 0x220404. five iterations, identical params each time. only 0x220404 was driven live; the other five codes are statically predicted to fault at their own case-body offsets.

disclosure

reported to the MSRC researcher portal as case VULN-190756. microsoft declined to service it, and that is the textbook-correct outcome. under microsoft’s windows security servicing criteria, the administrator-to-kernel transition is not a defended security boundary: an administrator can already load a driver, write to \Device\PhysicalMemory-class surfaces, or otherwise crash and own the kernel, so a driver that lets an admin bugcheck the box via an ioctl is not crossing a line microsoft commits to hold. there is direct precedent — the usbprint NULL-deref (disclosed days earlier, same pattern) drew the same “logged as a next-version candidate, not serviced” response.

so the finding is real, reproduces a guaranteed blue screen, and is correctly not a security fix. that is worth saying plainly: a confirmed kernel crash with a live bugcheck is not automatically a vulnerability, and knowing which side of the boundary line a bug sits on is as much the job as finding it.

on this page