Skip to content

House of Husk

glibc'nin printf custom-specifier dispatch table'larını (__printf_function_table / __printf_arginfo_table) hijack et; böylece seçtiğin bir format karakteriyle yapılan sonraki herhangi bir printf-ailesi çağrısı bir attacker function pointer'a sıçrar.

Mechanism

Suistimal edilen invariant

glibc, programların custom printf conversion karakterleri için handler register etmesine register_printf_function / __register_printf_specifier ile izin verir. Bu registry iki libc global'inde yaşar: __printf_function_table (output handler'ları) ve __printf_arginfo_table (arg-parse eden handler'lar). vfprintf/__vfprintf_internal fonksiyonunun başında tek bir check dispatch path'ini belirler:

if (__glibc_unlikely (__printf_function_table != NULL
                      || __printf_modifier_table != NULL
                      || __printf_va_arg_table != NULL))
    goto do_positional;          /* slow path -> printf_positional() */

Not: glibc'nin tam check'i ayrıca __printf_modifier_table ve __printf_va_arg_table'ı da kapsar, ama exploit yalnızca iki table'ı kullanır: __printf_function_table'ı slow path'i tetiklemek için ve __printf_arginfo_table'ı handler dispatch'ini hijack etmek için. Diğer ikisi burada yalnızca defensive bir OR-check olarak görünür.

Kritik nokta: iki table'dan birindeki non-NULL bir değer, "bir handler register edilmiş" gibi muamele görür ve bunun gerçek bir handler table'a işaret edip etmediğine dair hiçbir validation yoktur. Slow path'te bir conversion karakteri c parse edilirken table karakterin kendisiyle index'lenir ve üzerinden çağrı yapılır. Yani bir attacker __printf_arginfo_table'a fake bir table base yazarsa (ve slow path'i zorlamak için __printf_function_table'a non-NULL koyarsa) ve c-inci slot'a bir pointer yerleştirirse, sonraki printf("%c"...) attacker kodunu çağırır. Attack anında, target'ın zaten yaptığı bir tane dışında ekstra printf gerekmez.

Walkthrough

glibc < 2.28'e "kolayca" geniş ölçüde uygulanır ve aynı yüzeyle modern sürümlere de (writeup'lar 2.35, 2.37 ve 2.41'i doğruluyor — Xprintf_buffer refactor'ı table'ları kaldırmadı). Prerequisite'ler: bir libc leak ve libc data'ya bir heap-pointer'ı yazabilen bir arbitrary-write primitive.

  1. Delivery write'ı seç. House of Husk klasik olarak bir large-bin-attack (veya unsorted-bin-attack) ile eşleşir. İkisi de seçtiğin bir libc global'ine bir heap address yazmanı sağlar — tam da gereken şey, çünkü table base'i kontrol ettiğin bir heap chunk olacak.

  2. __printf_function_table'ı non-NULL yap. Buradaki herhangi bir non-zero değer do_positional slow path'ini zorlar. Seçilen specifier onun üzerinden de handle edilmediği sürece junk'a işaret etmesi sorun değil; spec parse sırasında önce arginfo path tetiklenir.

  3. __printf_arginfo_table'ı fake table'ına yönlendir. Bin attack'i kullanarak bu global'i içeriğini kontrol ettiğin bir heap address ile overwrite et. Table pointer-array[spec] şeklindedir: c karakterinin entry'si c index'inde yaşar.

  4. Function pointer'ı yerleştir. Heap chunk'ında, target pointer'ı (one-gadget, ORW stub, ROP pivot veya bir backdoor) seçtiğin specifier'ın slot'una koy. Yaygın bir konvansiyon, chunk metadata'sını hesaba katarak c karakterinin offset'ini forge ettiğin chunk'tan (c - 2) * 8 olacak şekilde yerleştirir — normal output'u ezmemek için %X gibi nadiren kullanılan bir conversion seç.

  5. Trigger. Target'ta seçtiğin specifier'ı emit eden (veya emit etmesini etkileyebileceğin) sonraki herhangi bir printf/fprintf/snprintf, printf_positional__parse_one_specmb → senin __printf_arginfo_table[c] üzerinden geçer ve control senin pointer'ına aktarılır.

Yaklaşık slow-path akışı
printf("%X", ...)
  -> __vfprintf_internal
     [ __printf_function_table != NULL ] -> goto do_positional
       -> printf_positional()
          -> __parse_one_specmb('%X')
             -> __printf_arginfo_table[ 'X' ](...)   // attacker pointer

Footgun'lar

  • İki table da önemli: __printf_function_table'ı NULL bırakmak fast path'in alınması ve arginfo table'ının asla danışılmaması demektir.
  • Programın meşru olarak kullanmadığı bir specifier seç, yoksa normal formatlama senin table'ından geçer ve output'u bozar / crash eder.
  • Slot index'i ham karakter değeridir; (c-2)*8/metadata hesabındaki bir off-by-one genellikle wild bir çağrının nedenidir.

Detection

  • register_printf_function hiç çağrılmamış bir process'te non-NULL bir __printf_function_table / __printf_arginfo_table oldukça anormaldir.
  • İki global'den birinin register edilmiş bir handler table yerine heap'e işaret etmesi attack'i işaret eder.

Mitigation

  • Table'lar meşru glibc state'idir, dolayısıyla "fix" edilmezler; savunmacılar write primitive'inin bloklanmasına güvenir — örn. glibc 2.30+ large-bin check'leri ve 2.28 unsorted-bin bck->fd check'i delivery adımındaki çıtayı yükseltir.
  • RELRO bunları korumaz (bunlar libc .bss'idir, target'ın GOT'u değil).
  • Dispatch edilen çağrı üzerinde CFI / pointer-signing son sıçramayı nötralize eder.

References