Skip to content

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

  1. Create pipe instances -- call CreateNamedPipe with PIPE_TYPE_MESSAGE to create N pipe instances (e.g., 10,000), each followed by CreateFile to open the client end
  2. Write data of target size -- call WriteFile on each pipe client with a buffer sized to produce allocations in the desired pool bucket; each write creates one DATA_QUEUE_ENTRY
  3. Fill target bucket -- the spray saturates the target size class with controlled allocations, creating a dense layout
  4. Poke holes -- selectively read from specific pipes with ReadFile to free their DATA_QUEUE_ENTRY objects, creating gaps at known positions
  5. Trigger vulnerable allocation -- the driver's vulnerable allocation fills one of the prepared gaps, landing adjacent to surviving pipe spray objects
  6. 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:

  1. Corrupt DataSize field -- the overflow modifies the DataSize field of an adjacent entry to a value larger than the actual data (e.g., change 0x100 to 0x1000)
  2. Read from pipe -- call ReadFile on the corresponding pipe handle
  3. Kernel overread -- the kernel reads DataSize bytes starting from Data[], extending past the original allocation boundary into adjacent pool memory
  4. Extract leaked data -- the excess bytes come from adjacent pool allocations and may contain:
  5. Pool chunk headers and metadata
  6. Kernel pointers (for KASLR bypass)
  7. _OBJECT_HEADER fields (for determining adjacent object types)
  8. 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:

  1. Corrupt DataSize field -- extend the perceived buffer size of a DATA_QUEUE_ENTRY
  2. Write to pipe -- if the pipe allows a second write (or if the entry is reused), WriteFile sends attacker-controlled data that the kernel copies beyond the allocation boundary
  3. Targeted overwrite -- the excess bytes overwrite specific fields in neighboring kernel objects:
  4. Another DATA_QUEUE_ENTRY.DataSize (for chaining relative reads)
  5. A _TOKEN.Privileges field (for direct privilege escalation)
  6. An _IORING_OBJECT buffer table entry (for I/O Ring exploitation)
  7. An _OBJECT_HEADER to change object type or security descriptor

Practical Considerations

  • Message mode required: The pipe must be created with PIPE_TYPE_MESSAGE to ensure each WriteFile creates a discrete DATA_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_ENTRY objects (one per write without a corresponding read), but most exploits use one write per pipe for cleaner hole management.
  • Quota fields: The QuotaInEntry and QuotaCharged fields 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_ENTRY allocations are tagged NpDq and 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_ENTRY allocations 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 NpDq are 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 the DataSize or Data[] fields before they are used by ReadFile.
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