advancedcat/pwn~3 min read

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 via mmap.
    • 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:

  1. 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.
  2. Fastbins: Singly linked list for small chunks (under 80 bytes in 32-bit, under 160 bytes in 64-bit).
  3. Unsorted Bin: Doubly linked list holding recently freed chunks before sorting.
  4. 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$$

  1. malloc() returns $A$.
  2. Second malloc() returns $A$ again!
  3. 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:

  1. Free a chunk $A$ so it enters the tcache bin.
  2. Overwrite chunk $A$’s fd pointer to target address TARGET_ADDR (e.g., __free_hook, GOT entry, or stack address).
  3. First malloc() returns chunk $A$. Tcache head updates to TARGET_ADDR.
  4. Second malloc() returns TARGET_ADDR!
  5. Writing to the second malloc() payload overwrites TARGET_ADDR directly!
# 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)