Skip to content

House of Water

Heap üzerindeki tcache_perthread_struct'ın üstünde sahte bir chunk forge edip onu geri al; böylece — safe-linking ile korunmayan — tüm tcache count'ları/entry'leri üzerinde doğrudan kontrol elde et ve arbitrary allocation'a leakless bir yol aç.

Mechanism

Invariant: tcache metadata'sı heap'te yaşar ve safe-linking ile korunmaz

glibc, per-thread tcache defterini libc'de değil heap'te bulunan bir tcache_perthread_struct'ta saklar (arena'daki ilk allocation olan bir 0x290 chunk):

typedef struct tcache_perthread_struct {
    uint16_t      counts[TCACHE_MAX_BINS];   // 64 bin counts
    tcache_entry *entries[TCACHE_MAX_BINS];  // 64 head pointers
} tcache_perthread_struct;

İki özellik bunu House of Water'ın (udp / Blue Water, PotluckCTF 2023 "Tamagoyaki") kalbi yapar:

  1. Safe-linking struct'ı kapsamaz. Safe-linking, freelist'teki bir chunk'ın içindeki fd link'ini XOR-obfuscate eder (e->next = ptr ^ (chunk_addr >> 12)), ama tcache_perthread_struct içindeki entries[] pointer'ları raw (çıplak) saklanır. Bu struct'ı kontrol eden, her tcache bin head'ini düz pointer'larla kontrol eder — onları forge etmek için heap leak gerekmez.

  2. counts/entries sınırı bir chunk header gibi görünecek hale getirilebilir. Seçilen bin'lere chunk free etmek belirli counts[] değerleri yazar; ör. bir 0x3e0 ve bir 0x3f0 chunk free etmek counts'un sonunda 0x0001_0001 bırakır, bu sırada bitişik entries[] pointer'ları fd/bk sağlar. Doğru offset'te birlikte okunduğunda bu, tcache struct'ı ile çakışan, PREV_INUSE set'li ~0x10000 boyutunda bir sahte unsorted-bin chunk header'ı sentezler.

Attacker bu sahte chunk'ı, bir bin pointer'ının partial (LSB / 2-byte) overwrite'ı aracılığıyla unsorted bin'e link'ler — yalnızca düşük bitler değişir, dolayısıyla sadece birkaç ASLR biti tahmin edilmeli (challenge bunu deterministik yapmak için ~8 bit leak eder). Eşleşen boyuttaki bir sonraki malloc sahte chunk'ı, yani tüm tcache_perthread_struct döndürür. Oradan attacker tcache bin'lerini stdout/libc'ye işaret ettirmek için raw entries[] yazar, leak edip pivot eder (genellikle House of Apple 2 / FSOP'a). Tam da safe-linking'i atlattığı için glibc 2.32–2.39+ üzerinde etkilidir.

Walkthrough

Bu, mimari-biçimli (architecture-shaped) bir tekniktir (tam chunk size'ları target'ın tcache_perthread_struct layout'una bağlıdır). Aşağıdaki fazlar corgi.rip ve Axura writeup'larını takip eder.

Phase 1 — forge the fake header over tcache metadata
  * malloc/free chunks of sizes (e.g. 0x3e0, 0x3f0, ...) so the written
    counts[] + entries[] bytes read as: size=0x10000 | PREV_INUSE, sane fd/bk.

Phase 2 — get a chunk into the unsorted bin overlapping the struct
  * Fill the relevant tcache bin (7 frees) so a further free spills to the
    unsorted bin, placing a real unsorted chunk adjacent to the fake header.

Phase 3 — LSB / 2-byte partial overwrite
  * Overwrite only the low 2 bytes of an unsorted bin fd/bk so it links the
    fake (tcache-struct) chunk. ~4 bits of bruteforce per write; the program's
    "tiny leak" (8 bits of ASLR) makes this reliable.

Phase 4 — reclaim and own tcache
  * malloc() of the fake chunk's size returns the tcache_perthread_struct.
    Now write raw entries[] (no safe-linking!) to point a bin at &stdout / libc.

Phase 5 — leak + RCE
  * Allocate over stdout, corrupt _IO_FILE buffering fields to leak libc,
    then file-stream-oriented programming (House of Apple 2) -> control flow.

Footgun'lar

  • Layout hassasiyeti. Sahte header counts[] değerlerinden sentezlenir; 0x0001_0001'i (ve aklı başında bir fd/bk'yı) üretmek için free ettiğin tam size'lar senin glibc'in için TCACHE_MAX_BINS'e ve struct offset'lerine bağlıdır. gdb'de gerçek tcache_perthread_struct'a karşı doğrula.
  • Partial overwrite ASLR bitleri. 1-byte overwrite deterministiktir ama fazla kaba; 2-byte overwrite ~4 bit tahmin gerektirir. Teknik, bruteforce'u önlemek için küçük (≈8-bit) bir leak ile eşleşir; dolayısıyla "leakless" sıfır bilgiyi değil, libc/heap pointer leak'inin olmamasını ifade eder.
  • Entry'ler raw, chunk-içi fd değil. entries[]'a yazdığın değerleri safe-link etme — onlar doğrudan pointer olarak tüketilir. Obfuscation yalnızca bir chunk free edildiğinde onun kendi in-band fd'sine uygulanır.
tcache struct overlap in gdb
pwndbg> p *(struct tcache_perthread_struct*)(heap_base+0x10)
  counts  = {..., [0x3e]=1, [0x3f]=1, ...}   # 0x0001_0001 at the boundary
  entries = {..., 0x...560, 0x...570, ...}   # raw, unprotected head ptrs
pwndbg> x/4gx heap_base+<fake_hdr_off>
  0x...: 0x0000000000010001  0x0000... (fd)  # reads as size|PREV_INUSE

Detection

  • En ilk arena chunk'ına (tcache_perthread_struct bölgesine) link'lenen bir unsorted-bin fd/bk'sı anormaldir.
  • Heap chunk'ları yerine libc yapılarına (_IO_2_1_stdout_) işaret eden tcache entries[] head'leri, struct'ın post-exploitation kontrolüne işaret eder.

Mitigation

  • Yapısal düzeltme tcache_perthread_struct metadata'sını korumak olurdu (ör. entries[] üzerinde guard/encoding); upstream glibc onu hâlâ raw saklar, dolayısıyla tek başına safe-linking bunu durdurmaz.
  • FSOP hardening'i (_IO_FILE vtable doğrulaması, _IO_obstack/wide_data kontrolleri) Phase 5 pivot'unun maliyetini yükseltir.
  • Out-of-line / guarded allocator metadata'sı, struct üstünde sahte bir header forge etmeyi etkisizleştirir.

References