tcache key bypass¶
glibc 2.29+'ın free edilmiş tcache chunk'larına damgaladığı
keyalanını overwrite ederek, double-free guard'ının chunk'ı artık tanımamasını ve ikinci bir free'nin başarılı olmasını sağlamak.
Mechanism¶
Note
glibc 2.29, yalnızca double free'leri yakalamak için tcache overlay'ine ikinci bir alan ekledi:
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;
(Bu typedef glibc 2.34+ formudur; 2.29–2.33'te alan
struct tcache_perthread_struct *key pointer tipindeydi — key o dönemde
tcache struct'ının adresine set edilirdi. 2.34, hem tipi uintptr_t'a hem
de değeri getrandom-randomize bir global'e taşıdı — bkz. Mitigation.)
tcache_put, her free edilen chunk'ı process genelindeki bir tcache_key
ile damgalar (static uintptr_t tcache_key;, 2.34+'ta getrandom ile randomize edilir):
free'de, _int_free tekrar push etmeden önce o damgayı kontrol eder:
tcache_entry *e = (tcache_entry *) chunk2mem (p);
if (__glibc_unlikely (e->key == tcache_key))
{
tcache_entry *tmp;
size_t cnt = 0;
for (tmp = tcache->entries[tc_idx]; tmp;
tmp = REVEAL_PTR (tmp->next), ++cnt)
{
if (cnt >= mp_.tcache_count)
malloc_printerr ("free(): too many chunks detected in tcache");
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
}
}
Guard iki aşamalıdır: ucuz e->key == tcache_key testi ve yalnızca
eşleşirse, p'nin gerçekten mevcut olduğunu doğrulamak için bin'in bir
yürüyüşü. Zayıflık: key free edilen chunk'ın user data'sında yaşar. İlk
free'den sonra oraya bir byte yazabilirsen, e->key != tcache_key olur ve
pahalı doğrulayıcı yürüyüş tamamen atlanır — ikinci free() doğrudan
tcache_put'a ilerler.
Walkthrough¶
Bypass, free edilen chunk'a bir use-after-free write'ı
gerektirir (ya da user data'sının +0x8 offset'ine ulaşan herhangi bir overlap):
long *a = malloc(0x40);
free(a); // tcache_put: a->key = tcache_key
// free(a); // <-- would abort: "free(): double free detected in tcache 2"
a[1] = 0; // UAF: clobber the key field (user offset +8 == e->key)
free(a); // key no longer matches -> guard skipped -> a freed AGAIN
Şimdi tcache bin'i a'yı iki kez içerir (bir tcache dup);
gerisi poisoning'dir:
long *b = malloc(0x40); // pops a
b[0] = (long)(target ^ (((long)b) >> 12)); // safe-linking-aware next overwrite (2.32+)
malloc(0x40); // pops a again
long *x = malloc(0x40); // pops &target
Beklenen çıktı: a[1]=0 olmadan program
free(): double free detected in tcache 2 ile ölür; key temizlendiğinde, her iki
free de başarılı olur ve sonraki malloc'lar overlapping/kontrol edilen
pointer'lar döndürür.
0'a temizlemek neden işe yarar
tcache_key, sıfırdan farklı rastgele bir değerdir (2.34+) ya da bir
tcache-struct pointer'ıdır (2.29–2.33). Canlı tcache_key dışındaki
herhangi bir değer ucuz testi yener. Sıfırlamak kullanışlıdır ama farklı tek
bir byte bile yeterlidir; key'e bir off-by-one ya da kısmi overwrite'ın
yeterli olmasının nedeni budur.
Warning
Kontrol yalnızca hesaplanan size'ı geçerli bir tcache index'ine eşlenen
chunk'lar için tetiklenir. tcache_get'i korumaz — allocation'da key
kontrolü yoktur — dolayısıyla tcache poisoning
key'den bağımsız olarak etkilenmez. Ayrıca guard, iki thread'e bölünmüş bir
double-free'yi yakalayamaz (her thread'in kendi tcache'i vardır).
Detection¶
key alanı değiştirilmiş bir free edilmiş tcache chunk'ı ya da counts'u ile
linked-list uzunluğu uyuşmayan bir bin anormaldir.
free(): too many chunks detected in tcache ve
free(): double free detected in tcache 2 abort'ları glibc'nin kendi runtime
tespitleridir; hardened allocator'lar (Scudo, hardened_malloc) yeniden kullanımı
doğrudan reddeder.
Mitigation¶
- glibc 2.34,
tcache_key'i kriptografik olarak rastgele yaptı (getrandom); 2.29–2.33 key'inin sunduğu ASLR/heap-pointer leak'ini kaldırdı ve bypass etmek için gerçek bir UAF write'ı zorunlu kıldı. - 2.32 safe-linking
next'i mangle eder, dolayısıyla bypass sonrası poisoning mangle edilmiş pointer'ı türetmelidir. - Guard kasıtlı olarak ucuz ve best-effort'tur; kazara double free'leri ve naif exploit'leri durdurur ama free edilen chunk'a herhangi bir write verildiğinde tamamen bypass edilebilir.