Named Pipe Objects
Exploitation of kernel named pipe data structures for pool spray, information leaks, and controlled read/write primitives in NonPagedPoolNx.
Description
Named pipes in Windows are a fundamental inter-process communication (IPC) mechanism implemented by the Named Pipe File System driver (npfs.sys). When data is written to a named pipe, the kernel allocates a DATA_QUEUE_ENTRY structure in pool memory with the written data stored inline immediately after the structure header. Because the attacker controls both the size and content of these allocations, and because they can be rapidly created and destroyed, named pipe data queue entries have become one of the most widely used pool spray primitives in Windows kernel exploitation.
Beyond spray, corrupted DATA_QUEUE_ENTRY structures provide powerful read and write primitives. If an adjacent pool overflow or UAF modifies the DataSize field of a queue entry, a subsequent ReadFile call on the pipe causes the kernel to copy more data than was originally written, leaking adjacent pool memory contents back to user mode. This relative read primitive is commonly used for KASLR bypass and kernel pointer disclosure. The same corruption can be leveraged for relative writes by extending the perceived buffer boundary.
The technique was popularized by Yarden Shafir's research on pipe-based exploitation primitives and has been used in numerous real-world exploit chains since Windows 10 1809. Named pipes work on all modern Windows versions and are one of the few spray primitives effective in both NonPagedPoolNx and (in some configurations) PagedPool.
DATA_QUEUE_ENTRY Structure
The DATA_QUEUE_ENTRY is the kernel structure allocated by npfs.sys when data is written to a pipe. Its layout is approximately:
DATA_QUEUE_ENTRY
+0x000 QueueEntry // LIST_ENTRY (Flink/Blink)
+0x010 Irp // Associated IRP, or NULL for buffered data
+0x018 SecurityContext // PSECURITY_CLIENT_CONTEXT
+0x020 EntryType // Read/write/flush indicator
+0x024 QuotaInEntry // Pool quota charged for this entry
+0x028 DataSize // Size of the inline data (ULONG)
+0x030 Data[] // Inline data -- fully attacker-controlled
- Total allocation size is
sizeof(DATA_QUEUE_ENTRY) + data_length. On x64, the header is typically 0x30-0x38 bytes (alignment-dependent), giving precise control over which pool bucket the allocation lands in. - Writing 0x1C8 bytes of data produces an allocation of approximately 0x200 bytes, targeting the 0x200 pool bucket.
- The
Data[]field contains exactly what the user-mode process wrote to the pipe.
Technique Details: Pool Spray
- Create pipe instances -- call
CreateNamedPipewithPIPE_TYPE_MESSAGEto create N pipe instances (e.g., 10,000), each followed byCreateFileto open the client end - Write data of target size -- call
WriteFileon each pipe client with a buffer sized to produce allocations in the desired pool bucket; each write creates oneDATA_QUEUE_ENTRY - Fill target bucket -- the spray saturates the target size class with controlled allocations, creating a dense layout
- Poke holes -- selectively read from specific pipes with
ReadFileto free theirDATA_QUEUE_ENTRYobjects, creating gaps at known positions - Trigger vulnerable allocation -- the driver's vulnerable allocation fills one of the prepared gaps, landing adjacent to surviving pipe spray objects
- Detect hit -- after triggering the vulnerability, read back from each pipe to identify which entry was corrupted (modified marker bytes indicate adjacency to the vulnerable object)
Technique Details: Relative Read (Information Leak)
After a pool overflow or UAF corrupts a DATA_QUEUE_ENTRY:
- Corrupt
DataSizefield -- the overflow modifies theDataSizefield of an adjacent entry to a value larger than the actual data (e.g., change 0x100 to 0x1000) - Read from pipe -- call
ReadFileon the corresponding pipe handle - Kernel overread -- the kernel reads
DataSizebytes starting fromData[], extending past the original allocation boundary into adjacent pool memory - Extract leaked data -- the excess bytes come from adjacent pool allocations and may contain:
- Pool chunk headers and metadata
- Kernel pointers (for KASLR bypass)
_OBJECT_HEADERfields (for determining adjacent object types)- Adjacent object content (for locating specific kernel structures)
This provides a reliable information leak without crashing the system, as the kernel performs a normal memcpy from the extended data region.
Technique Details: Relative Write
The same corruption pattern enables writing beyond allocation boundaries:
- Corrupt
DataSizefield -- extend the perceived buffer size of aDATA_QUEUE_ENTRY - Write to pipe -- if the pipe allows a second write (or if the entry is reused),
WriteFilesends attacker-controlled data that the kernel copies beyond the allocation boundary - Targeted overwrite -- the excess bytes overwrite specific fields in neighboring kernel objects:
- Another
DATA_QUEUE_ENTRY.DataSize(for chaining relative reads) - A
_TOKEN.Privilegesfield (for direct privilege escalation) - An
_IORING_OBJECTbuffer table entry (for I/O Ring exploitation) - An
_OBJECT_HEADERto change object type or security descriptor
Practical Considerations
- Message mode required: The pipe must be created with
PIPE_TYPE_MESSAGEto ensure eachWriteFilecreates a discreteDATA_QUEUE_ENTRY. Byte-mode pipes may append to an existing buffer instead. - Speed: Creating 10,000 pipes and writing data takes under a second, making named pipe spray one of the fastest available options.
- Multiple entries per pipe: Each pipe can hold multiple
DATA_QUEUE_ENTRYobjects (one per write without a corresponding read), but most exploits use one write per pipe for cleaner hole management. - Quota fields: The
QuotaInEntryandQuotaChargedfields are checked during cleanup. Corrupting quota fields causes a BSOD during pipe handle closure, so exploits must avoid modifying them or restore them before cleanup. - Segment heap isolation: On Windows 11 with segment heap,
DATA_QUEUE_ENTRYallocations are taggedNpDqand may be isolated from other pool tag allocations, limiting cross-object spray. Tag-matched spray (using objects with the same pool tag) can bypass this. - Dual pipe sets: When using named pipes for both spray and read/write in the same exploit, create two sets of pipes: one for controlling layout (spray) and a separate set for data exfiltration (relative read operations).
- Pool type:
DATA_QUEUE_ENTRYallocations are typically in NonPagedPoolNx for buffered writes. Match this against the target driver's allocation pool type.
Mitigations and Limitations
- Segment Heap pool tag isolation (Windows 11): Pipe data queue entries tagged
NpDqare isolated from allocations with different pool tags, preventing cross-object spray unless the target allocation shares the same tag. - Pool header cookies: Overflows that damage pool chunk headers are detected and cause immediate BSOD. Exploitation must target object data fields, not the pool header.
- KASLR: Leaked pointers from a relative read must be decoded relative to the randomized kernel base. The relative read technique itself is often used to defeat KASLR.
- LFH randomization: Within a bucket, allocation order is randomized. Larger spray volumes compensate for this unpredictability.
- No mitigation against in-band data corruption: Once a pool overflow reaches a
DATA_QUEUE_ENTRY, there is no integrity check on theDataSizeorData[]fields before they are used byReadFile.
Related CVEs
| CVE | Driver | Role of Named Pipes |
|---|---|---|
| CVE-2024-30085 | cldflt.sys |
Pool feng shui for adjacent allocation control |
| CVE-2024-38193 | afd.sys |
Pipe spray for UAF reclaim |
| CVE-2023-28252 | clfs.sys |
Named pipe info leak for KASLR bypass |
| CVE-2023-36036 | cldflt.sys |
Pool spray to groom overflow target |
| CVE-2021-31956 | ntfs.sys |
Pool spray + relative read for KASLR bypass |
See Also
- Pipe Attributes -- pipe attribute-based spray and R/W primitives (PagedPool)
- Pool Spray / Heap Feng Shui -- general pool spray theory and techniques
- I/O Ring -- commonly chained after named pipe info leak for full kernel R/W
- WNF State Data -- alternative spray primitive for PagedPool targets