ndkping.sys: a null systembuffer deref you can blue-screen on demand
on this page
part of the glaurung windows driver findings catalog. method narrative: reading all of notepad.exe with an llm.
summary
| driver | NDKPing.sys — NetworkDirect / NDK diagnostic test harness, version 10.0.26100.1150 (Windows 11 24H2) |
| class | CWE-476 (NULL pointer dereference) |
| bug | ioctl dispatcher dereferences Irp->AssociatedIrp.SystemBuffer without a null check; METHOD_BUFFERED + zero in/out length leaves it NULL |
| reach | administrator only (device SDDL grants SYSTEM and Administrators only) |
| primitive | kernel NULL-page read fault → bugcheck 0x3B → forced reboot |
| cvss 3.1 | AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H = 4.4 (medium) |
| proof | live BSOD on a Windows 11 24H2 VM, faulting RIP matches the predicted case-body offset |
| disclosure | reported 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.