Windows pool grooming techniques
TL;DR: Pool grooming arranges the kernel non-paged pool so that a specific object is allocated adjacent to your vulnerable buffer, or so that a freed slot is immediately reclaimed by an attacker-controlled allocation. Techniques: (1) spray same-size objects via named pipes / reserve objects / WNF state, (2) defragment by free-and-reallocate cycles, (3) “low-fragmentation heap”-style controlled pages. Modern Windows hardened with isolation between pool buckets (Segment Heap, low-fragmentation pool). Companion to hevd-pool-overflow-walkthrough and heap-exploitation-windows.
What we’re controlling
The non-paged pool is segmented into size classes. Allocations of the same size class come from the same backing store. Within a backing store, ordering depends on:
- Lookaside-list state (per-CPU).
- Backing store free-list ordering.
- LFH (Low-Fragmentation Heap) randomisation.
To groom: drive enough allocations of the target size that order becomes predictable enough to land your bug allocation next to your spray.
Allocators you can drive from userland
| Allocator | Size control | Tag | Notes |
|---|---|---|---|
Named pipes (NtCreateNamedPipeFile + attributes) | by attribute buffer length | NpFa etc. | most reliable on modern Windows |
Reserve objects (NtAllocateReserveObject) | fixed by reserve type | Iocp etc. | smaller surface |
WNF state data (NtCreateWnfStateName) | fixed sizes | Wnf* | works pre-hardening |
Section objects (NtCreateSection) | metadata fixed | MmSe | indirect |
| Token objects | indirect via process creation | Toke | very useful target itself |
| Pipe attributes | length-controlled | NpFa | best primitive on modern builds |
| Event with name | by name length | Even | small range |
For modern Windows: named pipe attributes are the workhorse. The kernel allocates 0x?? + len for each attribute; precise size control.
Pipe attribute primitive
1
2
3
4
5
6
7
8
HANDLE hRead, hWrite;
CreatePipe(&hRead, &hWrite, NULL, 0);
// Set attribute (length L → allocation of L+small_overhead)
BYTE attrBuf[L];
RtlSecureZeroMemory(attrBuf, sizeof(attrBuf));
// NtFsControlFile with FSCTL_PIPE_SET_ATTRIBUTE_LIST internally; wrap as helper
SetAttribute(hRead, attrBuf, sizeof(attrBuf));
The attribute buffer lives in non-paged pool, of attacker-chosen size, with attacker-controlled contents. The reverse — read attributes — returns the buffer contents.
This combination is what makes pipes useful:
- Allocation of specific size (matches your target).
- Contents you control (overwrite into adjacent corruption).
- Lifetime you control (close pipe → free attribute).
- Read-back to detect successful overwrite from corrupted neighbour.
Step 1 — pick the size class
Your bug allocates N bytes; you need adjacent objects of the same pool block size. Lookup the segment-heap class for N:
| Block class | Range (bytes) |
|---|---|
| 0x40 | small |
| 0x80 | medium-small |
| 0x100 | medium |
| 0x200 | larger |
| … | varies |
Modern Segment Heap has many specific buckets. Confirm with kd> !poolfind <tag> <size>.
Step 2 — spray to fill
1
2
3
4
5
#define SPRAY 0x2000
HANDLE pipes[SPRAY];
for (int i = 0; i < SPRAY; ++i) {
pipes[i] = create_pipe_with_attr(targetSize);
}
After spray, pool contains many adjacent pipe-attribute allocations.
Step 3 — punch holes
Free every other (or every Nth) → creates holes for your bug allocation to slot into.
1
for (int i = 0; i < SPRAY; i += 2) close_pipe(pipes[i]);
Now half the slots are free. Your bug allocation (e.g., HEVD’s 0x1F8) is more likely to land in a freed slot adjacent to a still-existing pipe attribute.
Step 4 — trigger the bug
Send the IOCTL → kernel allocates 0x1F8 → likely lands in one of the holes. Overflow writes into the adjacent (still-existing) pipe attribute.
Step 5 — verify
Read back the surviving pipes’ attributes. The one whose contents changed is the victim. From there:
- If you overwrote its size field → out-of-bounds read primitive via read attribute.
- If you overwrote a length-prefix → controlled-length write primitive on next set.
Iterate to arbitrary read/write.
Modern hardening
| Mitigation | Effect | Bypass approach |
|---|---|---|
| Segment Heap (Win10+) | random offsets within bucket | larger spray; statistical |
| Pool Inline Cookies | header has random cookie | overflow into content, not header |
| Pool Quarantine | freed chunks delayed | longer wait between free and reclaim |
| Low-Fragmentation Pool | per-class randomisation | brute over many runs |
| Pool tag enforcement | typed pool | use objects of correct tag |
| KASLR per-pool | randomised per-cpu pools | leak base of target pool |
Pool isolation (post-2020 builds)
Microsoft introduced Pool Isolation — allocations from different drivers/components go to separate pools. This makes cross-driver grooming harder; you can’t necessarily spray with pipe attributes and expect adjacency with a third-party driver’s allocations.
Bypass: find a primitive that allocates in the same pool as the target driver. Often requires bug research.
Real-world groom examples
- CVE-2020-1054 (Win32k): pool overflow groomed with palette objects.
- HEVD on Win10 21H1: pipe attributes groom reliably.
- Bochspwn-style bugs: discovered via fuzzing; groom is bug-specific.
Debugging grooming
In WinDbg:
1
2
3
4
kd> !pool <addr> ; show pool block info
kd> !for_each_thread .echo @#Thread
kd> !poolfind <tag> ; find all allocations with tag
kd> !verifier 3 <module> ; enable driver verifier on target
Driver Verifier slows execution but catches pool corruption immediately.
Heuristics that improve reliability
- Warm up the pool — allocate-and-free for many cycles before the real groom.
- Time the allocation — bug IOCTL fires immediately after holes opened.
- Multiple runs — exploit success rate is often 70-90%; loop the exploit.
- Defragmentation — if pool is already fragmented, groom less reliable; allocate a large block first to push out junk.
When grooming fails
If your spray + free + trigger doesn’t land adjacency:
- Increase spray size.
- Use a different allocator.
- Add a settle delay between free and trigger (some kernel housekeeping happens).
- Check that you’re freeing into the right size class.
References
- Tarjei Mandt — “Kernel Pool Exploitation on Windows 7” (historic, foundational)
- Connor McGarr — modern pool grooming
- k0shl — Windows kernel research
- Microsoft — Segment Heap docs
- See also: hevd-pool-overflow-walkthrough, hevd-uaf-walkthrough, heap-exploitation-windows, arbitrary-read-write-primitives