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
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 Type | Used By | When 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:
- Variable-size segment model: memory is organized into variable-sized segments, not a single contiguous region
- Type-segregated allocations: different object types go to different subsegments, making type confusion attacks harder
- Randomized placement: allocations within a subsegment are randomized, defeating predictable heap layout attacks
- Metadata protection: metadata encoding makes it harder to corrupt heap headers to control free list pointers
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 Source | Normal Value | Under Debugger | How 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:
- Safe unlinking: before unlinking chunk A from the free list, verify that A's forward pointer's backward pointer points back to A, and vice versa. A corrupted pointer breaks this check and triggers a heap corruption exception.
- Cookie encoding: the chunk header stores a size value XORed with a random cookie. Decoding requires the cookie — an attacker who overwrites the size field produces an invalid result that's detected.
- Guard pages between segments: large heap segments have guard pages at their boundaries to detect overflows into adjacent segments.
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).