Heap Overflow & Use-After-Free
Overview
Heap exploitation targets dynamically allocated memory. Unlike stack exploits that overwrite return addresses, heap exploits corrupt allocator metadata or application data stored on the heap. The goal is typically to achieve an arbitrary write primitive — writing a controlled value to a controlled address — which is then leveraged for code execution.
This file focuses on glibc ptmalloc2, the default allocator on Linux. Heap exploitation is version-dependent — techniques that work on one glibc version may fail on another due to added security checks.
ATT&CK Mapping
- Tactic: TA0002 - Execution
- Technique: T1203 - Exploitation for Client Execution
Prerequisites
- A binary with a heap vulnerability (overflow, use-after-free, double-free)
- Understanding of the target glibc version and its heap implementation
- GDB with pwndbg (provides
heap,bins,viscommands for heap inspection) - pwntools for exploit scripting
glibc Heap Basics
Chunk Structure
Every malloc() allocation returns a pointer to user data within a larger
"chunk" structure. The chunk header sits immediately before the user data:
┌──────────────────────────────┐
│ prev_size (8 bytes) │ ← only used if previous chunk is free
├──────────────────────────────┤
│ size (8 bytes) │ ← chunk size + flags (low 3 bits)
├──────────────────────────────┤ ← pointer returned by malloc()
│ user data │
│ ... │
├──────────────────────────────┤
│ next chunk header │
└──────────────────────────────┘
The size field contains the chunk size with three flag bits:
- Bit 0 (P) — PREV_INUSE: previous chunk is allocated (not free)
- Bit 1 (M) — IS_MMAPPED: chunk was allocated with mmap()
- Bit 2 (A) — NON_MAIN_ARENA: chunk belongs to a thread arena
Free Chunk Structure
When a chunk is freed, its user data area is repurposed for freelist pointers:
┌──────────────────────────────┐
│ prev_size │
├──────────────────────────────┤
│ size │
├──────────────────────────────┤
│ fd (forward pointer) │ ← points to next free chunk
├──────────────────────────────┤
│ bk (backward pointer) │ ← points to previous free chunk
├──────────────────────────────┤
│ (remaining user data) │
└──────────────────────────────┘
Bin Types
glibc maintains several "bins" — linked lists of free chunks organized by size:
| Bin Type | Size Range (x86-64) | Structure |
|---|---|---|
| tcache | Up to 0x410 bytes | Per-thread singly linked list (7 entries per size) |
| fastbin | Up to 0x80 bytes | Singly linked list (LIFO) |
| unsorted bin | Any size | Doubly linked list (temporary holding) |
| small bins | Less than 0x400 bytes | Doubly linked list (62 bins, exact size) |
| large bins | 0x400+ bytes | Doubly linked list (sorted by size) |
Freed chunks go to tcache first (if available), then fastbins or unsorted bin.
Inspecting the Heap in GDB
# pwndbg
# https://github.com/pwndbg/pwndbg
# Inside GDB with pwndbg:
# Show all heap chunks
pwndbg > heap
# Show all bins (free lists)
pwndbg > bins
# Show tcache entries
pwndbg > tcachebins
# Show fastbin entries
pwndbg > fastbins
# Visualize heap layout
pwndbg > vis_heap_chunks
Heap Overflow
A heap overflow occurs when writing beyond the bounds of a heap allocation, corrupting the header or data of the adjacent chunk.
Overwriting Application Data
If two allocations are adjacent, overflowing the first can corrupt data in the second:
char *a = malloc(32);
char *b = malloc(32); // b is adjacent to a on the heap
strcpy(a, long_input); // overflows into b's chunk
This can overwrite:
- Function pointers stored in b
- Object metadata (vtable pointers in C++)
- File names, command strings, or other data the program uses
Overwriting Chunk Metadata
Overwriting the size field of the next chunk can cause the allocator to
misinterpret chunk boundaries, leading to overlapping allocations.
Use-After-Free (UAF)
A use-after-free occurs when a program continues to use a pointer after the memory it points to has been freed. If the freed memory is reallocated for a different purpose, the dangling pointer now accesses the new data.
char *ptr = malloc(64);
free(ptr); // ptr is now dangling
char *new_ptr = malloc(64); // may reuse the same memory
// ptr and new_ptr may point to the same address
strcpy(ptr, "controlled"); // corrupts new_ptr's data
Exploitation Pattern
- Allocate object A
- Free object A (goes to tcache or fastbin)
- Allocate object B of the same size (gets the same memory as A)
- Use the dangling pointer to A — it now reads/writes object B's data
- If object B contains function pointers, overwrite them for code execution
Double-Free
Freeing the same chunk twice corrupts the freelist. When the same chunk appears twice in the free list, two subsequent allocations return the same pointer, creating an overlap.
char *a = malloc(64);
free(a);
free(a); // double-free — same chunk in freelist twice
char *b = malloc(64); // gets a's chunk
char *c = malloc(64); // also gets a's chunk — b == c
glibc Double-Free Protections
Modern glibc versions detect double-frees:
- tcache key field (glibc 2.29+): freed tcache chunks store a key value;
free()checks for it before inserting. Bypass: overwrite the key before the secondfree() - fastbin check: detects consecutive frees of the same chunk. Classic bypass: free A, free B, free A (interleave to avoid detection)
Tcache Poisoning
Tcache (thread-local cache, glibc 2.26+) uses a singly linked list. Corrupting
the fd pointer of a free tcache chunk causes malloc() to return an
attacker-controlled address.
Attack Steps
- Free chunk A — it goes into tcache
- Overwrite A's
fdpointer (via overflow, UAF, or double-free) - Set
fdto target address (e.g.,__malloc_hookor a stack address) malloc()returns chunk Amalloc()again — returns the target address- Write to the returned pointer to control the target location
Safe-Linking (glibc 2.32+)
Starting with glibc 2.32, tcache and fastbin fd pointers are obfuscated
using safe-linking. The stored fd is XORed with the chunk's address shifted
right by 12 bits:
stored_fd = real_fd ^ (chunk_addr >> 12)
To poison the fd, you need to know (or leak) the chunk address to compute
the correct obfuscated value.
Common Exploitation Targets
| Target | Glibc Version | Notes |
|---|---|---|
__malloc_hook |
< 2.34 | Called on every malloc(), one-shot to shell |
__free_hook |
< 2.34 | Called on every free(), write system then free("/bin/sh") |
| GOT entries | Partial RELRO | Overwrite to redirect function calls |
| Stack return address | Any | If you can write to the stack |
stdout _IO_FILE |
Any | Leak addresses via _IO_2_1_stdout_ corruption |
__malloc_hook and __free_hook were removed in glibc 2.34. For newer
versions, exploitation typically targets _IO_FILE structures, the stack,
or application-specific function pointers.
Checking glibc Version
# Check glibc version on the system
ldd --version | head -1
# Check which libc a binary uses
ldd ./binary | grep libc
# Get exact version from the library
strings /lib/x86_64-linux-gnu/libc.so.6 | grep "GNU C Library"