Chapter 19

Heap Internals

NT Heap vs Segment Heap, the metadata structures that exploitation targets, heap debugging flags used for anti-debug detection, and heap spray as an ASLR bypass technique

Scenario

A browser exploit triggers a use-after-free vulnerability. The attacker controls what data goes into the freed slot. By allocating controlled data before triggering the free, they can craft a fake vtable pointer that gets called by the browser engine. This is heap exploitation — and it depends on understanding exactly how the heap manager lays out allocations and its free lists. You're analyzing the exploit; this chapter gives you the mental model.

Windows Heap Types

Windows provides two heap implementations, selectable per-heap:

Heap TypeUsed ByWhen Introduced
NT Heap (Legacy) Older applications, system processes, explicitly created heaps Windows NT — the original heap manager
Segment Heap Modern Windows processes (default since Win10 19H1 for most processes) Windows 10 19H1 (2019)

A process can have multiple heaps. The default heap is accessible via GetProcessHeap(). Applications can create additional heaps with HeapCreate(). The C runtime (malloc/free) uses the default process heap internally.

NT Heap Structure

The NT Heap is organized around several caching layers to improve allocation performance:


  NT Heap Allocation Pipeline
  ─────────────────────────────────────────────────────────────────────
  HeapAlloc(size) request
       │
       ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  Frontend: Low Fragmentation Heap (LFH)                        │
  │  For allocations ≤ 16 KB in common sizes                       │
  │  Bins: 1-8 bytes, 9-16, 17-24, ... up to 16,368 bytes         │
  │  Fast, thread-local caches — no lock needed                    │
  └──────────────────────┬──────────────────────────────────────────┘
                         │ (miss) — allocation not in LFH
                         ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  Backend: Segment (free list) allocator                        │
  │  Free lists for each size class up to 16 KB                    │
  │  Large allocations (> 16 KB): VirtualAlloc directly            │
  └─────────────────────────────────────────────────────────────────┘

  Each allocated chunk layout (before/around data):
  ┌────────────────────────────────────────────────────────────────┐
  │  HEAP_ENTRY header (8 bytes on x64)                            │
  │  Size: encoded chunk size (encoded with random cookie)         │
  │  Flags: free/busy/last-entry                                   │
  │  SmallTagIndex: corruption detection                           │
  ├────────────────────────────────────────────────────────────────┤
  │  User data (the bytes returned to the caller)                  │
  └────────────────────────────────────────────────────────────────┘

  

Segment Heap

The Segment Heap (introduced Windows 10 19H1) has better security properties than NT Heap — it uses a radically different architecture that makes classic heap exploitation significantly harder:

Edge (Chromium), modern Office, and most current Windows processes use Segment Heap. The NT Heap is still used by older applications and some system components.

Heap API

// Standard Win32 heap operations

// Default process heap
HANDLE hHeap = GetProcessHeap();

// Allocate 256 bytes, zero-initialized
PVOID p = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, 256);

// Resize an allocation
p = HeapReAlloc(hHeap, HEAP_ZERO_MEMORY, p, 512);

// Free
HeapFree(hHeap, 0, p);

// Create a private heap (isolated from process heap)
HANDLE hPrivate = HeapCreate(0, 0, 0);
// Destroy private heap and all its allocations at once
HeapDestroy(hPrivate);

// Enumerate all heaps in the current process
HANDLE heaps[32];
DWORD count = GetProcessHeaps(32, heaps);
for (DWORD i = 0; i < count; i++)
    printf("Heap %lu: %p\n", i, heaps[i]);

Heap Anti-Debug Flags

When a process is created under a debugger, Windows sets additional heap debug flags. These flags cause the heap manager to add extra sentinel bytes around allocations to detect corruption. The flags are stored in the heap header and in the PEB, and they're detectable by malware:

Flag SourceNormal ValueUnder DebuggerHow to Check
NtGlobalFlag (PEB+0x70) 0x0000 0x0070 (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS) Read PEB.NtGlobalFlag; if (value & 0x70) → debugger
Heap header Flags 0x00000002 (HEAP_GROWABLE) 0x40000060 (extra debug flags set) Read heap header Flags field at [GetProcessHeap() + 0x14]
Heap header ForceFlags 0x00000000 0x40000060 Read ForceFlags at [GetProcessHeap() + 0x18]
// Anti-debug heap flag check (x64)
PPEB pPeb = (PPEB)__readgsqword(0x60);

// Check 1: NtGlobalFlag in PEB
ULONG ntGlobalFlag = *(ULONG*)((BYTE*)pPeb + 0xBC);
if (ntGlobalFlag & 0x70) {
    // debugger detected via heap debug flags
    TerminateProcess(GetCurrentProcess(), 0);
}

// Check 2: Default heap ForceFlags
PVOID hHeap = *(PVOID*)((BYTE*)pPeb + 0x30);  // PEB.ProcessHeap
ULONG heapForceFlags = *(ULONG*)((BYTE*)hHeap + 0x74); // heap+0x74 on x64
if (heapForceFlags != 0) {
    // non-zero ForceFlags = heap debug mode = debugger
    TerminateProcess(GetCurrentProcess(), 0);
}
// Counter: patch NtGlobalFlag to 0 and heap flags to 0 in memory

Heap Spray

Heap spray is an ASLR bypass technique for browser exploits. The idea: fill the heap with a predictable pattern (NOP sled + shellcode) repeated thousands of times, then jump to a guessed address in the sprayed range. Because the heap spans a large portion of the address space and the spray fills it densely, the guess is likely to land in sprayed memory.


  Heap Spray Concept
  ─────────────────────────────────────────────────────────────────────
  1. Allocate 10,000 blocks of 64 KB each  = 640 MB heap memory
  2. Fill each block with:
     [NOP NOP NOP ... NOP | shellcode]
     ^—————————————————^   ^—————————^
          NOP sled           payload
  3. Exploit jumps to any address in range 0x12300000–0x1A000000
     (where the spray likely landed)
  4. Lands somewhere in a NOP sled → slides to shellcode → executes

  Modern mitigations:
  - Segment Heap randomizes allocation placement → spray harder to predict
  - Isolated Heap / MemGC (in modern browsers) isolates object types
  - Address space compression (fewer predictable ranges)

  

Safe Unlinking

Classic heap exploitation relied on corrupting a free chunk's backward/forward list pointers. When the heap manager unlinked a free chunk during allocation, it would write attacker-controlled data to an attacker-controlled address (the "write-what-where" condition). Modern heaps prevent this through:

Q & A

When should you use HeapCreate instead of VirtualAlloc?

Use HeapCreate when you need: (1) Many small allocations: VirtualAlloc has 64 KB granularity — allocating 16 bytes via VirtualAlloc wastes 65,520 bytes. The heap manager suballocates large VirtualAlloc'd regions into small chunks. (2) Isolated memory: a private heap (HeapCreate) can be destroyed with a single HeapDestroy call, freeing all allocations at once — no need to track individual pointers. Useful for parsers or request handlers with clear lifetimes. (3) Thread safety: the default heap is thread-safe with locks. A single-threaded component can use HEAP_NO_SERIALIZE flag for better performance. Use VirtualAlloc directly when: (1) Allocating large (>100 KB) regions where heap overhead is irrelevant. (2) You need specific page protection (VirtualAlloc lets you specify PAGE_* flags directly). (3) Allocating executable memory for JIT compilation or shellcode (though this is flagged by EDRs). The C runtime's malloc/free uses the process heap internally — for general-purpose allocation it's the right choice.

How does the Low Fragmentation Heap (LFH) improve performance?

The LFH solves two problems with the basic free-list heap: (1) Fragmentation: if you allocate and free many different sizes, the free list fills with chunks of various sizes that don't match future requests, wasting memory. LFH buckets allocations into fixed size classes (increments of 8 bytes up to 16 KB). All 64-byte allocations come from the same pool of 64-byte chunks — no fragmentation within a size class. (2) Contention: a single lock on the free list becomes a bottleneck in multi-threaded programs. LFH has per-thread caches (subsegments) that most allocations use without taking any lock. The LFH activates automatically after a size class has been allocated 17 times — the heap manager recognizes it as a hot allocation path and switches to LFH caching. For security, LFH randomizes the allocation order within a subsegment to make heap spray and use-after-free exploitation harder (you can't reliably predict which free slot will be filled by your controlled allocation).