Exploit DevelopmentAdvanced

Pool Grooming for Kernel Exploitation

Shaping the Windows kernel pool allocator to set up reliable adjacent-object overwrites.

spray(0x200, PIPE_ATTRIBUTE) free_alternating() allocate(target_object) [+] adjacent overwrite · reliable
// kernel internals
Pool layout
▸ On this page 6 sections

Background

Kernel pool exploits often require a heap layout you can predict. The Windows segment heap (since 21H1) and the older lookaside/list-based allocator before it both respond to spraying and grooming - deliberately allocating and freeing objects to force the kernel to place your target object adjacent to a controlled allocation.

This is a research note on the core concepts. All vulnerability context is from patched, publicly disclosed CVEs and CTF challenges.

The kernel pool allocators

Windows uses different allocators depending on the pool type and allocation size:

  • NonPagedPoolNx - executable protections stripped; the primary target for data-only exploits
  • PagedPool - pageable; used for many object types
  • Lookaside lists - per-CPU caches for fixed-size allocations; predictable but size-class bound
  • Segment heap (Win10 21H1+) - randomized VSEGs, metadata-hardened; harder to groom than the legacy allocator
windbg · inspect pool chunk
!pool <address>
dt nt!_POOL_HEADER <address>
! Note

The segment heap adds pool header encoding, guard pages, and randomized segment placement. Techniques that relied on deterministic chunk adjacency in the legacy allocator require rethinking for modern builds.

Basic grooming strategy

The classic approach, still valid for fixed-size lookaside allocations:

  1. Spray - fill the lookaside list and relevant pool pages with controlled objects of the same size as your target
  2. Punch holes - free every other object, creating a predictable pattern of free chunks
  3. Allocate - trigger the vulnerable allocation; it lands in one of your holes, adjacent to a controlled object
c · illustrative spray (user mode)
// Spray 0x200 pipe attributes - each is a fixed-size pool allocation
for (int i = 0; i < 0x200; i++) {
    spray_handles[i] = create_pipe_attribute(spray_buf, SPRAY_SIZE);
}
// Free alternating to create holes
for (int i = 0; i < 0x200; i += 2) {
    CloseHandle(spray_handles[i]);
}
// Now trigger the vulnerable allocation - should land adjacent to a held object
trigger_vuln();
! Research note

On a live engagement, kernel exploits are almost never the right tool - the noise, crash risk, and BSOD-on-failure make them unsuitable except in very specific circumstances. This is CTF and research context.

Useful spray primitives

The choice of spray object matters - it needs to be user-controllable, match the target size class, and be releasable individually. Common primitives:

Primitive Pool type Notes
NtCreateEvent NonPagedPoolNx Fixed-size, individually closeable
Named pipe attributes PagedPool Controllable payload size
IoCompletionReserve NonPagedPoolNx Small, fast
NtAllocateReserveObject NonPagedPoolNx Designed for spraying
✓ Tip

Always verify your spray lands in the right pool type and size class with WinDbg before building the exploit around it. A mismatch in size class means the allocations go to a different lookaside list and your grooming assumptions break.

Verifying layout in WinDbg

After spraying, inspect the pool to confirm adjacency:

windbg
# Find spray objects in pool
!poolfind <tag> 0    ; 0 = NonPagedPool

# Inspect chunk layout around a known address
dt nt!_POOL_HEADER <spray_addr - 0x10>
db <spray_addr - 0x20> L0x60

Confirm the pattern: your spray object, a free chunk, another spray object. When the vulnerable allocation fills the free chunk, it lands exactly where you expect.

Takeaway

Pool grooming is fundamentally a reliability problem - a kernel bug that reliably corrupts an adjacent controlled object is far more exploitable than one that hits unpredictable targets. Modern mitigations (segment heap randomization, pool header encoding, guard pages) push the research toward more creative primitives, but the underlying principle remains: control the heap layout before you trigger the bug.

Tested on
Windows 10 21H2 (lab) · Hyper-V guest
Tools
WinDbg · kd (kernel debugger)
Status
patched builds · research only

References