Named Pipe Objects
Named pipes are one of the oldest IPC mechanisms in Windows, dating back to the LAN Manager era. Their exploitation potential, however, is thoroughly modern. When data is written to a named pipe, the kernel allocates a DATA_QUEUE_ENTRY structure in NonPagedPoolNx with the written data stored inline after a small header. The attacker controls both the size of this allocation (through the write length) and its content (through the write data). Allocations persist until the pipe is read or the handle is closed. They can be created and destroyed rapidly, thousands per second. And if a pool overflow or UAF corrupts the DataSize field of a queue entry, reading from the pipe causes the kernel to return more bytes than were originally written, leaking adjacent pool memory back to user mode.
This combination of size control, content control, stable lifetime, and exploitable corruption targets has made DATA_QUEUE_ENTRY the standard spray and relative-read primitive for NonPagedPoolNx targets on all modern Windows versions. The technique was systematized by Yarden Shafir's research on pipe-based exploitation and has since appeared in exploit chains for cldflt.sys, afd.sys, clfs.sys, ntfs.sys, and numerous other drivers. While pipe attributes serve the same role for PagedPool targets, DATA_QUEUE_ENTRY objects are the workhorse for the NonPagedPoolNx allocations that most kernel driver vulnerabilities produce.
The DATA_QUEUE_ENTRY structure
When WriteFile is called on a named pipe created with PIPE_TYPE_MESSAGE, the kernel allocates a DATA_QUEUE_ENTRY and copies the written data inline after the header:
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
The total allocation size is sizeof(DATA_QUEUE_ENTRY) + data_length. On x64, the header is typically 0x30 to 0x38 bytes depending on alignment, which gives precise control over the final allocation size. Writing 0x1C8 bytes of data produces an allocation of approximately 0x200 bytes, landing in the 0x200 pool bucket. Writing 0x68 bytes produces a ~0xA0 allocation. This arithmetic is straightforward and predictable, making DATA_QUEUE_ENTRY one of the most precisely sizable spray objects available.
The Data[] field contains exactly what the user-mode process wrote to the pipe, byte for byte. This is the content control that makes pipe queue entries useful as spray: the attacker fills Data[] with marker bytes (to detect which entry was corrupted), fake pointers (for type confusion scenarios), or fake structure fields (for UAF reclamation).
Pool spray with DATA_QUEUE_ENTRY
The spray sequence follows the same general pattern as other pool spray techniques, adapted for the pipe API.
The attacker creates N pipe instances (typically 5,000 to 10,000) by calling CreateNamedPipe with PIPE_TYPE_MESSAGE for the server end and CreateFile for the client end. Using message mode is critical: it ensures each WriteFile creates a discrete DATA_QUEUE_ENTRY. Byte-mode pipes may append to an existing buffer instead, breaking the one-write-one-allocation relationship.
Next, the attacker writes data of the target size to each pipe client. Each write creates one DATA_QUEUE_ENTRY in NonPagedPoolNx. The data content includes pipe-specific marker bytes so the attacker can later identify which entry was corrupted. The spray saturates the target size class, filling pool pages with controlled allocations.
To create holes for the vulnerable allocation to land in, the attacker selectively reads from specific pipes with ReadFile. Reading consumes and frees the DATA_QUEUE_ENTRY, creating a gap in the pool layout. The typical pattern is to free every other entry, or to free entries at regular intervals, creating a series of gaps surrounded by surviving spray objects on both sides.
After triggering the vulnerable allocation (which fills one of the prepared gaps) and the vulnerability itself (which corrupts an adjacent spray object), the attacker reads back from each pipe. Modified marker bytes indicate which entry was adjacent to the vulnerable object and has been corrupted.
Relative read: the information leak
The most common exploitation of a corrupted DATA_QUEUE_ENTRY is the relative read. If a pool overflow or UAF modifies the DataSize field of an entry to a value larger than the actual data, reading from the corresponding pipe causes the kernel to copy more bytes than were originally written. The kernel performs a normal memcpy from the Data[] field for DataSize bytes. Since DataSize now exceeds the allocation boundary, the copy extends into adjacent pool memory.
The excess bytes come from neighboring pool allocations and may contain pool chunk headers and metadata, kernel pointers that defeat KASLR (the most common use of this technique), _OBJECT_HEADER fields that reveal adjacent object types, and content from adjacent kernel structures that the attacker can use to locate specific kernel objects.
This relative read does not crash the system because the memory is valid (it belongs to adjacent pool allocations). The read extends linearly from the corrupted entry into higher addresses, which is why the spray must place the corruption target immediately before the data to be leaked.
Relative write: extending the corruption
The same DataSize corruption enables a relative write, though this variant is used less frequently. If a pipe allows a second write (or if the entry's data region is reused), calling WriteFile with attacker-controlled data causes the kernel to copy bytes beyond the original allocation boundary. The excess bytes overwrite specific fields in neighboring kernel objects.
Common targets for relative writes include another DATA_QUEUE_ENTRY.DataSize (for chaining multiple relative reads), a _TOKEN.Privileges field (for direct privilege escalation), an _IORING_OBJECT buffer table entry (for setting up I/O Ring exploitation), and _OBJECT_HEADER fields (for changing object types or security descriptors).
Practical considerations
Several details separate a working exploit from a crash.
Message mode is required. Byte-mode pipes may buffer data differently, breaking the one-to-one relationship between writes and allocations. Always create pipes with PIPE_TYPE_MESSAGE.
Speed is not a bottleneck. Creating 10,000 pipes and writing data to each takes well under a second. The API overhead is minimal, and the kernel's pipe implementation is optimized for high throughput.
Multiple entries per pipe are possible (one per write without a corresponding read), but most exploits use one write per pipe. This simplifies hole management: freeing a specific entry means reading from a specific pipe, which is straightforward when each pipe has exactly one entry.
Quota fields must not be corrupted. The QuotaInEntry and QuotaCharged fields are validated during pipe cleanup. Corrupting them causes a BSOD when the pipe handle is closed. If the overflow might touch these fields, the attacker must either avoid closing the affected pipe handle or restore the fields before cleanup.
Segment Heap isolation on Windows 11 separates DATA_QUEUE_ENTRY allocations (pool tag NpDq) from allocations with different pool tags. This means the spray cannot groom allocations across pool tag boundaries. When pool tag isolation applies, the attacker must use tag-matched spray objects or find a vulnerability path where the target allocation shares the NpDq tag.
Dual pipe sets are a practical necessity when using named pipes for both spray and data exfiltration in the same exploit. One set of pipes controls layout (spray and hole-poking). A separate set handles the relative read operations after corruption. Mixing these roles in a single pipe set leads to confusing cleanup ordering and race conditions.
Mitigations and Limitations
Segment Heap pool tag isolation (Windows 11) is the most impactful mitigation. DATA_QUEUE_ENTRY allocations tagged NpDq are isolated from allocations with different pool tags, preventing the cross-object spray that was trivial on pre-19H1 systems. Tag-matched spray or same-tag vulnerability paths bypass this, but the mitigation eliminates many spray combinations.
Pool header cookies detect overflows that damage pool chunk headers, causing immediate BSOD before the corruption can be leveraged. Exploitation must target the DATA_QUEUE_ENTRY's data fields, not the pool header preceding it.
LFH randomization shuffles the order of allocations within a bucket, making exact adjacency probabilistic. Larger spray volumes (10,000+ objects) compensate by ensuring high coverage regardless of allocation order.
Notably, there is no integrity check on the DataSize field or the Data[] content before they are used by ReadFile. The kernel trusts these fields implicitly. This absence of validation is the fundamental weakness that enables the relative read primitive, and it has not been addressed in any current Windows build.
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