Comment un buffer overflow écrase la pile
Section intitulée « Comment un buffer overflow écrase la pile »Un débordement de tampon apparaît quand un programme copie plus d’octets que la zone mémoire de destination ne peut en contenir. Dans du code natif, cette erreur peut dépasser une variable locale, corrompre l’état de la fonction appelante et, dans certaines conditions, modifier l’adresse vers laquelle le processeur reprendra son exécution.
Un formulaire papier prévoit une case pour un numéro court. Si quelqu’un continue d’écrire après la bordure, il finit par remplir les cases voisines, y compris celles réservées aux instructions du responsable. Le problème n’est pas l’écriture elle-même, mais l’absence de limite au bon endroit.
→ Lire l’explication complète de la marmotte
Pile d’appel, cadre de pile et adresse de retour
Section intitulée « Pile d’appel, cadre de pile et adresse de retour »La pile d’exécution est une région mémoire utilisée pour suivre les appels de fonctions. Chaque appel crée généralement un cadre de pile, aussi nommé stack frame. Ce cadre contient les données temporaires nécessaires à une invocation précise : arguments, variables locales, registres sauvegardés et informations de retour.
Sur les architectures courantes, une instruction d’appel place une adresse de retour sur la pile. Cette adresse désigne l’instruction à exécuter une fois la fonction terminée. La fonction appelée réserve ensuite de l’espace pour ses variables locales, souvent en déplaçant le pointeur de pile. Un pointeur de base peut aussi être conservé pour accéder à ces données avec des décalages fixes.
Un cadre de pile conceptuel peut ressembler à ceci :
Cette représentation est pédagogique, pas contractuelle. L’ordre exact dépend de l’architecture, de l’ABI (Application Binary Interface), du compilateur, de ses options et de l’optimisation. Un compilateur peut supprimer le pointeur de base, réordonner des variables, conserver une valeur en registre ou ne pas matérialiser le tampon sur la pile. Tirer des conclusions de sécurité depuis un schéma unique relève de la divination par diagramme.
Le point invariant est ailleurs : une fonction doit préserver un état de contrôle permettant de rendre la main à son appelant. Si une écriture non bornée atteint cet état, le flot d’exécution devient potentiellement contrôlable. Le processeur ne distingue pas une adresse de retour légitime d’une adresse corrompue : il applique ce que la mémoire contient. L’intégrité de cette mémoire est donc une propriété de sécurité, pas un détail d’implémentation.
Cette mécanique appartient aux fondations de l’architecture cyber, mais elle a une conséquence directe pour l’architecture applicative : tout composant natif qui traite des données externes constitue une frontière d’exécution. Une bibliothèque C appelée depuis un proxy, un agent de supervision ou un conteneur ne devient pas moins risquée parce qu’elle porte une étiquette cloud-native. Les slides n’ont jamais arrêté un strcpy.
Débordement de tampon local avec strcpy
Section intitulée « Débordement de tampon local avec strcpy »strcpy copie une chaîne C jusqu’au premier caractère nul, sans recevoir la taille de la destination. Elle ne peut donc pas vérifier si la copie tient dans le tampon. Quand l’entrée est plus longue que la destination, l’écriture continue dans les octets adjacents.
Voici un programme de démonstration volontairement vulnérable. Il sert à observer une corruption mémoire dans un environnement jetable, jamais à construire un binaire de production.
#include <stdio.h>#include <string.h>
/* Vulnérable : aucune taille de destination n'est fournie à strcpy. */static void afficher_message(const char *entree) { char tampon[16];
strcpy(tampon, entree); /* Déborde si entree dépasse 15 caractères plus '\0'. */ printf("Message : %s\n", tampon);}
int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "Usage : %s <message>\n", argv[0]); return 1; }
afficher_message(argv[1]); return 0;}Pour observer le comportement brut sur une machine Linux isolée, avec GCC (GNU Compiler Collection), compiler sans optimisations et sans protections de pile :
# Réserver cette compilation à une machine virtuelle jetable, sans données sensibles.gcc -O0 -g -fno-stack-protector -fno-omit-frame-pointer -no-pie \ -o overflow-demo overflow-demo.c
# Une entrée courte fonctionne../overflow-demo bonjour
# Une entrée longue peut provoquer une corruption ou un crash../overflow-demo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe résultat exact varie. Le programme peut afficher une chaîne altérée, se terminer par un défaut de segmentation ou sembler fonctionner. Cette dernière issue est la plus pernicieuse : l’absence de crash ne prouve rien, elle signifie seulement que les octets écrasés n’ont pas encore produit un effet visible.
Si les octets écrits dépassent tampon, ils peuvent atteindre d’autres variables locales, le pointeur de base sauvegardé, puis l’adresse de retour. Lors de l’instruction de retour, le processeur charge cette adresse depuis la pile. Une valeur modifiée peut provoquer un crash, sauter vers une zone inattendue ou, si d’autres conditions sont réunies, transférer l’exécution vers un chemin choisi par l’attaquant.
La correction n’est pas de remplacer strcpy par une fonction tronquante au hasard. Certaines alternatives conservent des pièges de terminaison ou masquent une troncature métier. La décision doit être explicite : rejeter l’entrée trop longue, redimensionner une allocation contrôlée, ou transporter la longueur avec les données. Une API qui accepte (pointeur, longueur, capacité) rend la limite vérifiable. Une API qui devine la taille depuis un caractère terminal demande de la discipline humaine à chaque appel, ce qui est un mécanisme de sécurité étonnamment populaire pour quelque chose qui échoue si régulièrement.
Surface d’attaque et détournement du flux d’exécution
Section intitulée « Surface d’attaque et détournement du flux d’exécution »La source d’entrée ne change pas la nature du défaut. Argument de ligne de commande, paquet réseau, en-tête de fichier, variable d’environnement, message de file ou champ de protocole : dès qu’une longueur contrôlable atteint une copie non bornée, la surface existe.
L’exploitation complète exige souvent davantage qu’un débordement. Il faut connaître ou déduire la disposition mémoire, atteindre l’élément de contrôle visé, contourner les protections actives et disposer d’un chemin d’exécution utilisable. Cela ne réduit pas la sévérité du défaut. Cela décrit seulement les conditions qui séparent un crash local d’une compromission reproductible.
Les attaques modernes ne cherchent pas nécessairement à injecter du code dans le tampon. Quand les zones de données ne sont pas exécutables, un attaquant peut tenter de réutiliser des fragments de code déjà présents en mémoire, technique souvent appelée ROP (Return-Oriented Programming). Une fuite d’adresse facilite ce type de chaîne en réduisant l’incertitude introduite par la randomisation. Les corruptions de données non liées au retour restent également dangereuses : modifier un indicateur d’autorisation, une taille ou un pointeur peut produire un impact sans écraser d’adresse de retour.
La contre-mesure primaire reste l’élimination de l’écriture hors limites. Les mécanismes de compilation ne réparent pas une sémantique de copie incorrecte, ils détectent ou compliquent son exploitation. Les tests doivent couvrir les longueurs limites, les données sans caractère nul, les encodages inattendus et les chemins d’erreur. Pour le code C et C++, les analyseurs statiques, les tests instrumentés et les fuzzers doivent être intégrés à la chaîne de livraison, pas exécutés une fois avant la photo de fin de projet.
Canaris, ASLR et pages non exécutables
Section intitulée « Canaris, ASLR et pages non exécutables »Les protections modernes changent fortement le résultat pratique d’un débordement.
Un canari de pile est une valeur placée entre les variables locales et les données de contrôle. Avant le retour de fonction, le programme vérifie que cette valeur n’a pas changé. Si elle diffère, le processus s’arrête plutôt que de consommer une adresse de retour potentiellement corrompue. Cette défense détecte beaucoup d’écrasements contigus, mais elle ne couvre pas toutes les fonctions ni toutes les corruptions.
L’ASLR (Address Space Layout Randomization) randomise l’emplacement de régions mémoire telles que la pile, le tas et les bibliothèques chargées. Une adresse utile lors d’une exécution ne l’est donc pas forcément à la suivante. L’ASLR perd une large part de sa valeur en présence d’une fuite d’adresses, ou si le binaire est compilé sans code indépendant de sa position.
DEP (Data Execution Prevention), aussi nommé NX pour No-eXecute, interdit l’exécution d’instructions depuis des pages prévues pour les données, dont la pile dans une configuration saine. Cette barrière bloque le modèle historique où des octets injectés dans un tampon deviennent directement du code processeur. Elle ne bloque pas la réutilisation de code existant et ne corrige aucune corruption mémoire.
Le compromis central est net : C et C++ donnent un contrôle fin sur la mémoire, la disposition des données et les coûts d’exécution, mais font porter la preuve des bornes au code appelant. Ce coût est acceptable pour des composants où ce contrôle est réellement requis, à condition d’imposer une discipline d’API, une compilation durcie et des tests instrumentés. Pour les traitements exposés qui n’exigent pas ce contrôle, préférer un langage à sûreté mémoire réduit la classe entière de défauts. Ce n’est pas une religion de langage, c’est une réduction de surface d’attaque mesurable.
Exploitation, supervision et reprise après corruption mémoire
Section intitulée « Exploitation, supervision et reprise après corruption mémoire »Une politique de compilation doit activer les canaris, la génération de binaires indépendants de leur position et les protections de liaison disponibles sur la plateforme. La chaîne d’intégration continue doit vérifier ces propriétés sur les artefacts livrés, pas seulement dans les fichiers de construction. Une option absente dans un conteneur de compilation suffit à réintroduire un binaire mou au milieu d’une flotte prétendument durcie.
Les crashs liés aux canaris doivent remonter comme des événements de sécurité. Un arrêt brutal répétitif sur une même route réseau, un même type de fichier ou un même message mérite une investigation, même si le service redémarre. Le redémarrage restaure un processus propre, pas la confiance dans l’entrée qui vient de provoquer l’arrêt.
Les dumps mémoire constituent un autre piège. Une pile peut contenir des jetons de session, des mots de passe, des clés privées manipulées par une bibliothèque ou des données métier. Restreindre, chiffrer ou désactiver les dumps selon le besoin opérationnel est nécessaire. Une sauvegarde de dump non chiffrée est une exfiltration différée, avec un habillage forensic.
La rotation des secrets ne neutralise pas un débordement, mais elle limite l’impact si un dump ou une lecture mémoire expose des identifiants. Les secrets manipulés par un processus doivent avoir une durée de vie limitée, une rotation testée et une révocation exploitable. Pour relier ce risque aux autres frontières applicatives, voir aussi les failles et mécanismes d’exploitation et les principes de développement sécurisé.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans borne explicite à la copie, chaque entrée longue devient un test gratuit: les écarts de comportement (arrêt immédiat, sortie tronquée, exécution apparemment normale) forment un oracle qui aide à deviner la disposition mémoire sans disposer du code source. Les arrêts brutaux se rangent alors parmi les incidents de disponibilité, sans corrélation avec l’entrée reçue, et la trace utile disparaît au redémarrage, qui efface l’état mémoire avant toute qualification. Faute d’interfaces transportant la capacité, la correction reste locale et la même classe de défaut revient au prochain appel de copie ajouté, sans que rien ne le signale. Les cas limites non couverts (données sans caractère nul, longueurs frontières) laissent enfin passer des régressions qui ne se manifesteront qu’en production.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Quand un programme lance une fonction, il réserve une zone de mémoire temporaire pour cette fonction. Cette zone garde notamment les données locales et l’adresse à laquelle le programme doit reprendre après la fin de la fonction. Cette adresse est indispensable : sans elle, le processeur ne sait pas quelle instruction exécuter ensuite.
Dans l’exemple, le programme réserve seize caractères pour un message. La fonction de copie utilisée ne reçoit pas la taille de cette réserve. Elle continue donc à écrire tant qu’elle ne rencontre pas la fin du texte reçu. Si le texte est trop long, les caractères dépassent la zone prévue et remplacent des données voisines.
Selon l’organisation choisie lors de la compilation, les données remplacées peuvent inclure l’adresse de reprise. Quand la fonction se termine, le processeur lit alors une adresse modifiée. Il peut s’arrêter parce que cette adresse ne désigne rien de valable, ou continuer ailleurs. Un attaquant cherche à provoquer ce second cas.
Les protections actuelles vérifient qu’une valeur placée près des données locales n’a pas été modifiée, déplacent les zones mémoire à chaque lancement et interdisent souvent d’exécuter les données reçues. Elles rendent l’attaque plus difficile, mais ne retirent pas l’erreur du programme. La correction fiable consiste à contrôler la longueur avant chaque copie et à choisir des interfaces qui transportent explicitement cette longueur.
À lire aussi
- Comment corriger une attaque path traversalDémonstration d'une attaque path traversal sur un téléchargement vulnérable et des contrôles nécessaires pour empêcher la lecture ou l'écriture hors périmètre.
- Vérifier une chaîne de compilation reproductibleFondations de la compilation reproductible, des attaques de build et de la vérification moderne de la chaîne de compilation.
- Linux : définition, origine et rôle du noyauDéfinition de Linux : noyau système créé par Linus Torvalds en 1991, sous licence GPLv2, ce qu'il fait concrètement et pourquoi il équipe serveurs, Android et supercalculateurs.
- Rust et la sécurité mémoire par construction, garanties et limitesComment Rust rend une classe entière de fautes mémoire impossible à compiler grâce à la possession et aux emprunts, où cette garantie s'arrête et ce qu'il reste à auditer.
Pages voisines
La citent
- Langages informatiques : histoire, inventeurs et usages
- Rust et la sécurité mémoire par construction, garanties et limites
Comment un buffer overflow écrase la pile
Elle cite
: les deux pages se citent l’une l’autre.
