Glibc tcache double-free key¶
glibc 2.29+, free edilmiş her tcache chunk'ına per-thread tcache address'ine set edilen bir
keyfield'ı ekler, böylece aynı chunk'ı tcache'e iki kez free etmek yakalanır.
Mechanism¶
glibc 2.29'dan önce tcache'in hiç double-free check'i yoktu: bir chunk'ı iki kez free etmek onu basitçe per-thread tcache bin'ine iki kez linkliyordu ki bu tcache-poisoning'in kanonik kurulumudur. Commit bcdaad21 (DJ Delorie, 2018-11-20, glibc 2.29'da geldi), free-chunk metadata'sına ikinci bir field ekledi:
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
Tcache'e free'de tcache_put key'i yazar; allocation'da tcache_get onu temizler:
e->key = tcache; /* tcache_put: mark "this chunk is in MY tcache" */
e->key = NULL; /* tcache_get: clear on hand-out */
_int_free'de, bir chunk'ı tcache'e push etmeden önce key incelenir:
if (__glibc_unlikely (e->key == tcache && tcache))
{
/* possible double free -- walk the bin to confirm */
...
malloc_printerr ("free(): double free detected in tcache 2");
}
Invariant enforced
Şu anda bu thread'in tcache'inde duran bir chunk'ın key == tcache'i vardır. Onun ikinci bir free()'si o marker'ı görür, entries[tc_idx] üzerinde doğrulayıcı bir walk tetikler ve "free(): double free detected in tcache 2" ile abort eder. Böylece naif bir tcache double-free, sessiz bir exploit primitive'inden bir crash'e dönüşür.
key == tcache testi hızlı bir filter'dır (tesadüfen match edebilir), bu yüzden abort etmeden önce bir list traversal gerçek bir duplicate'i doğrular. glibc 2.34+'te key, per-thread tcache struct pointer'ı yerine process genelinde tek bir static uintptr_t tcache_key değeriyle değiştirildi; bu değer initialization sırasında getrandom() ile bir kez randomize edilir, bu da basit key forgery'ye karşı hardening sağlar.
Walkthrough¶
Check'i doğrudan tetikle:
#include <stdlib.h>
int main(void) {
void *a = malloc(0x20);
free(a);
free(a); /* second free of the same tcache chunk */
return 0;
}
Expected output (glibc >= 2.29)
glibc < 2.29'da aynı program burada abort etmez; chunk basitçe iki kez linklenir.Bypassable with a write primitive
Check yalnızca e->key'i inceler. Free edilmiş chunk'ın key'ini tcache address'i (veya NULL) dışında herhangi bir değere overwrite eden bir UAF veya heap-overflow write ile key == tcache testi başarısız olur ve chunk tekrar free edilebilir — klasik glibc 2.29 double-free bypass'ı. Mitigation, kazara/naif double free'leri durdurur; free edilmiş chunk'ın içeriğini zaten kontrol eden bir attacker'ı değil.
Detection¶
Bir crash log'unda "free(): double free detected in tcache 2" ile gelen beklenmedik bir abort, ya gerçek bir bug'ı ya da key'i temizlemeyen bir exploitation girişimini gösterir. Bunları erken yüzeye çıkarmak için fuzzing sırasında ASan/MALLOC_CHECK_/glibc'in kendi mesajları altında çalıştır.
Mitigation¶
glibc-safe-linking (mangle edilmiş fd) ve daha yeni glibc'deki (2.34+) process-geneli tcache_key randomization ile birleştir; nihayetinde altta yatan use-after-free / overflow'u düzelt, çünkü key üzerine controlled bir write tek başına bu check'i yener.