Heap Exploitation Fundamentals
Understand glibc malloc structure, Use-After-Free (UAF), double free vulnerabilities, and tcache poisoning attacks.
// prerequisite reading
Glibc Malloc Architecture
The glibc heap allocator (ptmalloc) manages dynamically allocated memory requested via malloc(), realloc(), and free().
Malloc Chunk Layout
In memory, every allocated or free chunk consists of metadata fields preceding user data:
+-----------------------------------+-----------------------------------+
| Prev Size (if prev chunk is free) | Size of Chunk | A | M | P Flags |
+-----------------------------------+-----------------------------------+
| User Data ... |
| |
+-----------------------------------------------------------------------+
- Chunk Size: Size aligned to 8 or 16 bytes.
- Flags:
P(PREV_INUSE=0x1): Indicates if the previous chunk is allocated.M(IS_MMAPPED=0x2): Allocated viammap.A(NON_MAIN_ARENA=0x4): Allocated from non-main thread arena.
Free Bins in Glibc
When a chunk is freed, it is placed into a linked list bin for quick reuse:
- tcache (Thread Local Cache - glibc >= 2.26): Singly linked list storing freed chunks up to 1032 bytes per thread (up to 7 chunks per bin size). Fast allocations without lock contention.
- Fastbins: Singly linked list for small chunks (under 80 bytes in 32-bit, under 160 bytes in 64-bit).
- Unsorted Bin: Doubly linked list holding recently freed chunks before sorting.
- Small / Large Bins: Doubly linked lists for sorted chunks.
1. Use-After-Free (UAF)
A Use-After-Free vulnerability occurs when a program continues to use a pointer after the memory referenced by that pointer has been passed to free().
struct Note {
void (*print_fn)();
char content[32];
};
struct Note *n1 = malloc(sizeof(struct Note));
free(n1); // Pointer n1 is freed but NOT set to NULL!
// Allocate a raw string buffer of the exact same size
char *p2 = malloc(sizeof(struct Note));
// p2 receives the EXACT same memory address previously assigned to n1!
// Attacker controls p2 content -> overwrites n1->print_fn function pointer!
read(0, p2, sizeof(struct Note));
// Executing stale pointer n1 calls attacker-controlled function pointer!
n1->print_fn(); // Control flow hijacked!
2. Double Free
A double free occurs when free() is called twice on the same memory address without an intervening allocation.
In older glibc versions lacking duplicate checks, freeing chunk $A$ twice puts $A$ into the free list twice:
$$\text{tcache bin} \longrightarrow A \longrightarrow A \longrightarrow A$$
malloc()returns $A$.- Second
malloc()returns $A$ again! - Now two independent code paths hold active pointers to identical memory, allowing arbitrary write primitives.
3. Tcache Poisoning
In glibc $\ge 2.26$, tcache entries use a singly-linked list where the first bytes of a freed chunk’s payload store the fd (forward pointer) to the next free chunk.
If an attacker has a UAF or heap buffer overflow:
- Free a chunk $A$ so it enters the tcache bin.
- Overwrite chunk $A$’s
fdpointer to target addressTARGET_ADDR(e.g.,__free_hook, GOT entry, or stack address). - First
malloc()returns chunk $A$. Tcache head updates toTARGET_ADDR. - Second
malloc()returnsTARGET_ADDR! - Writing to the second
malloc()payload overwritesTARGET_ADDRdirectly!
# Pwntools snippet for Tcache Poisoning
# Overwrite fd pointer of freed tcache chunk with target address (__free_hook)
edit_note(note_idx, p64(free_hook_addr))
# First malloc consumes original chunk
alloc_note(size, b"dummy")
# Second malloc returns __free_hook address!
alloc_note(size, p64(one_gadget_addr))
# Trigger free() -> executes system("/bin/sh") or one_gadget!
delete_note(0)