Pool Spray / Heap Feng Shui
Technique for controlling kernel pool layout to place attacker-controlled objects adjacent to vulnerable allocations, enabling reliable exploitation of pool overflows and use-after-free conditions.
Description
Pool spray is the kernel equivalent of heap spray in user-mode exploitation. The Windows kernel pool allocator services allocation requests from large memory regions divided into pages, which are further subdivided into buckets by size class. When a driver makes a pool allocation, the allocator selects a free slot from the appropriate bucket. Under normal conditions, the layout of objects within a pool page is unpredictable from the attacker's perspective, meaning that a pool overflow or use-after-free would corrupt random adjacent memory, most likely causing a blue screen rather than a useful primitive.
Pool spray (also called pool feng shui or heap grooming) overcomes this unpredictability by making a large number of kernel allocations of a carefully chosen size, saturating the target pool bucket with attacker-controlled objects. Once the pool pages for that bucket are densely packed with spray objects, the attacker selectively frees specific objects to create predictable gaps. When the vulnerable allocation is triggered, it lands in one of the prepared gaps, adjacent to spray objects whose content the attacker controls. After triggering the vulnerability (overflow or UAF), the adjacent controlled object is corrupted, giving the attacker a useful primitive such as an information leak or a controlled write.
The effectiveness of pool spray depends critically on matching the spray object to the target allocation in terms of size class, pool type, and pool tag. Modern Windows versions with the Segment Heap allocator introduce additional isolation that makes spray more difficult but not impossible, requiring larger spray volumes and sometimes tag-matched spray objects.
Technique Details
- Identify the target allocation size -- determine the exact size (including pool header overhead) of the vulnerable buffer by reverse-engineering the driver or observing pool allocations in a debugger
- Select a spray object of matching size that has exploitable fields (function pointers, data pointers, or length fields) and can be allocated from user mode via a syscall or API
- Defragment the pool by allocating many small objects to fill partially-used pool pages, forcing the allocator to carve fresh contiguous pages
- Allocate thousands of spray objects to fill pool pages in the target bucket, creating a dense slab-like pattern where every slot is occupied by a spray object
- Poke holes by freeing specific spray objects (e.g., every other object, or objects at known indices) to create predictable gaps where the vulnerable allocation will land
- Trigger the vulnerable allocation -- the driver allocates the vulnerable buffer, which lands in one of the prepared gaps, adjacent to surviving spray objects on both sides
- Trigger the vulnerability -- an overflow writes into the adjacent spray object, or a freed object's memory is reclaimed by a spray allocation
- Read back or trigger the corrupted spray object to gain an information leak, relative write, or other exploitation primitive
Spray Object Selection Criteria
Choosing the right spray object is critical for reliable exploitation. The following criteria must all be satisfied:
- Size class matching: The spray object must fall into the same pool bucket as the target allocation. The Windows pool allocator groups allocations by size class (e.g., the 0x200 bucket services allocations from 0x181 to 0x200 bytes). If the spray object and the target land in different buckets, they will never be adjacent.
- Pool type matching: The spray object must come from the same pool type (NonPagedPoolNx, PagedPool, or SessionPool). These are serviced from entirely separate memory regions.
- Controllable content: The attacker must control enough of the spray object's content to plant useful values -- fake pointers for redirecting reads/writes, fake length fields for out-of-bounds access, or marker patterns for identifying which spray object was hit.
- Stable lifecycle: The spray object must remain allocated for the duration of the attack. Objects that are garbage collected, freed by a timer, or released by unrelated kernel code paths can collapse the layout mid-exploitation.
- Creation throughput: The attacker needs thousands of objects quickly. Objects requiring expensive operations (device I/O, security checks) per allocation may be too slow for reliable grooming.
Common Spray Objects
| Object | API to Create | Pool | Approx Size | Useful Fields |
|---|---|---|---|---|
DATA_QUEUE_ENTRY |
NtWriteFile on pipe |
NonPagedPoolNx | header + data | DataSize, inline data |
_WNF_STATE_DATA |
NtUpdateWnfStateData |
PagedPool | variable | DataSize, ChangeStamp |
_PIPE_ATTRIBUTE |
NtFsControlFile |
PagedPool | header + name + value | ValueLength, value pointer |
_FILE_OBJECT |
NtCreateFile |
NonPagedPoolNx | ~0x100 | Various pointers |
_TOKEN |
NtDuplicateToken |
PagedPool | ~0x480 | Privileges, SessionId |
| IO Completion entries | NtSetIoCompletion |
NonPagedPoolNx | fixed ~0x58 | Data fields |
| EA buffers | NtCreateFile with EA |
varies | controlled | Fully controlled content |
Pool Topology and Segment Heap
Paged vs NonPaged vs Session
- NonPagedPoolNx: Most driver allocations, pipe data queue entries, network buffers, IRP-related structures. The primary target for driver vulnerability exploitation.
- PagedPool: Registry data, WNF state data, pipe attributes, some file system metadata. Used when the driver calls
ExAllocatePoolWithTagwithPagedPool. - Session Pool: Win32k objects such as bitmaps and palettes (legacy technique). Largely deprecated as an attack surface on modern Windows.
Segment Heap Changes (Windows 10 19H1+)
Starting with Windows 10 19H1, the Segment Heap backend fundamentally changes pool allocation behavior:
- Pool tag isolation: The segment heap separates allocations by pool tag. Two different pool tags may never share a pool page, defeating traditional cross-object spray techniques.
- Variable Size (VS) subsegments: Allocations above the LFH threshold go to VS subsegments with randomized placement.
- LFH randomization: The Low Fragmentation Heap randomizes allocation order within a bucket, so sequential allocations do not produce a sequential layout.
- Tag-matched sprays: Modern exploits target spray objects that share the same pool tag as the vulnerable allocation, bypassing pool isolation entirely.
UAF Reclaim via Spray
Pool spray is also the standard technique for exploiting use-after-free vulnerabilities:
- After the vulnerable object is freed, spray objects of the same size to reclaim the freed memory
- The spray object's content overlaps with the freed object's structure, allowing control over fields the kernel still references through a dangling pointer
- Timing is critical -- the spray must occur after the free but before the dangling pointer is dereferenced
- Both size class and pool type must match the freed allocation
Practical Considerations
- Segment heap randomization requires larger spray volumes (10,000+ objects) compared to pre-19H1 systems
- LFH randomization means exact adjacency is probabilistic, not guaranteed -- exploits must tolerate imprecision
- Pool cookies detect header corruption -- corruption must target object data fields, not the pool header itself
- Different pool types cannot be used interchangeably -- match the target's pool type exactly
- Special Pool and Driver Verifier change pool layout entirely and must be disabled for the actual exploit
- Use WinDbg
!pool,!poolused, and!pooltagcommands during development to verify spray object placement - Pool allocation breakpoints (
baonExAllocatePoolWithTag) filtered by tag and size help confirm bucket targeting
Mitigations and Limitations
- Segment Heap randomization (19H1+): Increases unpredictability of allocation placement within buckets
- Pool header cookies: Detect corruption of pool chunk metadata; overflows that damage the header cause immediate BSOD
- Pool tag isolation: Prevents different object types from sharing pool pages on segment heap
- Type Isolation (future): Microsoft has discussed separating different kernel object types into distinct pool regions, which would defeat cross-object spray entirely
- Kernel Address Space Layout Randomization (KASLR): Does not directly affect pool spray but raises the bar for post-spray exploitation steps that require kernel addresses
Related CVEs
| CVE | Driver | Spray Target |
|---|---|---|
| CVE-2024-30085 | cldflt.sys |
Pipe attribute spray for pool overflow |
| CVE-2024-38193 | afd.sys |
Pool UAF reclaim via spray |
| CVE-2023-28252 | clfs.sys |
WNF spray for controlled layout |
| CVE-2023-36036 | cldflt.sys |
Pool overflow exploitation |
| CVE-2023-29336 | win32kfull.sys |
UAF reclaim with spray |
See Also
- Named Pipe Objects -- pipe-based spray primitives (
DATA_QUEUE_ENTRY) - Pipe Attributes -- pipe attribute spray and R/W primitives
- WNF State Data -- WNF-based spray for PagedPool targets
- I/O Ring -- post-spray R/W primitive commonly chained with pool spray