hackweb
appunti di hacking e tecnologia
IT EN
menù principale
menù utente
non sei loggato

Tcache poisoning: trasformare un use-after-free in scrittura arbitraria

2 aprile 2026 · 3 min di lettura · #binary-exploitation #heap #glibc

Dal glibc 2.26 (2017) ogni thread ha una tcache: un insieme di liste concatenate LIFO, una per classe di dimensione, che servono le malloc/free piccole senza toccare gli arena lock. È velocissima. È anche la struttura con meno controlli di integrità di tutto l'allocatore, ed è per questo che è il primo bersaglio di ogni sfida di heap.

Come è fatta la freelist

Quando liberi un chunk piccolo, glibc lo mette in testa alla lista della sua dimensione. Il puntatore al prossimo chunk libero viene scritto nei primi 8 byte dell'area dati del chunk stesso — cioè nella memoria che l'applicazione considerava sua un istante prima:

typedef struct tcache_entry {
    struct tcache_entry *next;   // primi 8 byte del chunk liberato
    struct tcache_perthread_struct *key;  // anti double-free
} tcache_entry;

Una malloc della stessa dimensione stacca la testa e restituisce quel chunk; il nuovo capolista diventa ciò che next indicava. Nessun controllo che next punti a un chunk vero, a una dimensione coerente, o dentro l'heap. Qui sta tutto.

La primitiva: falsificare next

Se dopo una free riesci ancora a scrivere nel chunk (un use-after-free), sovrascrivi next con l'indirizzo dove vuoi che malloc ti consegni memoria. Due allocazioni dopo, malloc te lo restituisce.

a = malloc(0x20);
free(a);                 // testa della tcache[0x20] -> a, a->next = NULL

// use-after-free: a è liberato ma abbiamo ancora il puntatore
*(void **)a = target;    // a->next = target  (l'indirizzo che vogliamo)

malloc(0x20);            // stacca a; ora la testa è 'target'
void *p = malloc(0x20);  // restituisce 'target': scrittura/lettura arbitraria

A questo punto p punta dove hai scelto. Scrivi su p e stai scrivendo sull'indirizzo arbitrario. I bersagli tipici: la voce GOT di una funzione (dirottala su system), __free_hook o __malloc_hook nelle glibc più vecchie, il return address di uno stack frame se hai leakato lo stack, o __exit_funcs.

Il freno del 2020: safe-linking

glibc 2.32 ha introdotto il safe-linking. Il puntatore next non è più memorizzato in chiaro, ma mascherato con l'indirizzo della sua stessa locazione:

#define PROTECT_PTR(pos, ptr) \
    ((tcache_entry *)((((size_t)(pos)) >> 12) ^ ((size_t)(ptr))))
#define REVEAL_PTR(ptr)  PROTECT_PTR(&(ptr), ptr)

In pratica: next_memorizzato = (indirizzo_dello_slot >> 12) XOR next_reale. Se provi a scrivere un indirizzo grezzo come prima, quando glibc lo smaschera ottiene spazzatura e di solito crasha. Aggiunge anche un controllo di allineamento: il puntatore rivelato deve essere allineato a 16 byte.

Perché non ferma quasi nessuno

Il maskaggio usa una chiave che non è segreta: è indirizzo_dello_slot >> 12, cioè il numero di pagina dell'heap. Se hai un leak dell'heap — e in una catena di exploit di solito ce l'hai, spesso gratis proprio dalla tcache liberata — ricostruisci la chiave e cifri tu il puntatore falso:

# heap_leak = un indirizzo qualsiasi dentro l'heap
def protect(pos_addr, ptr):
    return (pos_addr >> 12) ^ ptr

# vogliamo che a->next riveli 'target'; a sta a heap_addr
forged = protect(heap_addr, target)
uaf_write(a, forged)     # ora la tcache è avvelenata come prima

Il vincolo di allineamento è più reale: target deve finire su un confine di 16 byte. Spesso significa scegliere un bersaglio allineato o aggiustare l'offset di poco. Ma non è una difesa, è un dosso.

Il doppio-free e la chiave anti-abuso

glibc controlla i double-free sulla tcache con il campo key: alla free scrive un valore che identifica la tcache di quel thread, e alla free successiva se lo ritrova uguale sospetta un double-free e aborta. Il bypass è banale una volta che hai una UAF: sovrascrivi key con qualunque cosa diversa prima di liberare di nuovo, e il controllo non scatta.

La morale

La tcache è un caso di scuola di come ottimizzare rimuovendo controlli sposti il costo sulla sicurezza. Il safe-linking ha alzato l'asticella — adesso ti serve un leak dell'heap dove prima non serviva — ma non ha chiuso la classe di bug. La difesa vera resta a monte: non liberare due volte, azzerare i puntatori dopo la free, e compilare con hardening dell'allocatore (GLIBC_TUNABLES=glibc.malloc.check=3, o allocatori con quarantena come quello di hardened_malloc).


« torna alla home

ultimi post
 
il tuo indirizzo IP:
216.73.216.108
visitatore #0
MOTD:
Ogni astrazione perde da qualche parte.
Qui guardiamo dove.
argomenti