Windows pool grooming techniques

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