Aller au contenu

CWE-121 : débordement de tampon dans la pile

Quatorzième du CWE Top 25 2025. Le libellé officiel, Stack-based Buffer Overflow, se traduit par « débordement de tampon fondé sur la pile ». Le catalogue admet les raccourcis stack overflow et stack buffer overflow, et le premier est une calamité : il désigne également, dans la bouche de tout le monde, un phénomène entièrement différent. Nous y reviendrons, car c’est l’intérêt principal de cette entrée.

→ Lire l’explication complète de la marmotte

La définition du MITRE est strictement localisatrice : il s’agit d’un débordement de tampon dans lequel la zone écrasée est allouée sur la pile, c’est-à-dire une variable locale ou, plus rarement, un paramètre de fonction.

Rien de plus. La variante ne décrit ni une cause nouvelle, ni un mécanisme différent de la copie sans vérification de taille : elle dit où. Et ce « où » suffit à justifier une entrée propre, parce que la pile contient autre chose que des données.

La pile est la zone où un programme range, pour chaque appel de fonction en cours, ses variables locales et les informations nécessaires au retour : dont l’adresse de l’instruction à laquelle reprendre une fois la fonction terminée. Écraser au-delà d’une variable locale ne détruit donc pas seulement une autre variable : cela peut atteindre cette adresse de retour, et détourner le fil d’exécution du programme vers un endroit choisi. C’est historiquement la première technique d’exploitation documentée, et la raison pour laquelle cette variante a droit à son numéro.

CWE-121 est une variante à deux parents : CWE-787, l’écriture hors limites, et CWE-788, l’accès à une zone mémoire après la fin d’un tampon. Sa sœur CWE-122, le débordement dans le tas, occupe le rang 16 : même défaut, autre zone mémoire, autres conséquences.

Vient maintenant la confusion annoncée. L’expression « stack overflow » désigne couramment deux choses sans rapport :

  • le débordement de tampon dans la pile, soit CWE-121 : on écrit au-delà d’une variable locale ;
  • l’épuisement de la pile, qui porte le numéro CWE-674, « récursion non contrôlée » : une fonction s’appelle elle-même indéfiniment jusqu’à consommer toute la pile, et le programme s’arrête. Aucune écriture hors limites, aucune corruption, aucune prise de contrôle : un déni de service.

Le premier est une faiblesse de l’intégrité de l’exécution, le second une faiblesse de disponibilité. Les confondre dans un rapport conduit à mobiliser une équipe pour un plantage, ou à traiter avec légèreté une prise de contrôle.

Une troisième voisine, moins connue, mérite d’être citée parce qu’elle explique beaucoup de débordements modernes : CWE-680, « dépassement d’entier conduisant à un débordement de tampon ». Le programme calcule la taille à allouer, le calcul déborde la capacité du type entier, l’allocation est trop petite, et la copie qui suit est parfaitement bornée… par la mauvaise valeur. La vérification existait, elle portait sur un nombre faux.

Parce que les protections visent la conséquence, pas la cause. Les compilateurs insèrent depuis vingt ans une valeur sentinelle (le canari) entre les variables locales et l’adresse de retour, et vérifient à la sortie de la fonction qu’elle n’a pas changé. C’est très efficace : le programme s’arrête au lieu de sauter où l’attaquant voulait. Mais le défaut reste présent, la vulnérabilité reste publiée, et le classement la compte.

Parce que le code embarqué est souvent compilé sans ces protections. Microcontrôleurs, micrologiciels d’équipements réseau, systèmes temps réel : les options de durcissement y sont parfois absentes, parfois désactivées pour la performance, et le code y manipule massivement des tampons de taille fixe sur la pile.

Parce que les parseurs de protocoles continuent d’être écrits à la main. Chaque nouvel appareil connecté apporte sa pile logicielle propriétaire, qui lit des trames dans des tampons locaux. On réécrit tous les ans un défaut de 1988.

Le site traite le mécanisme sous le bon titre : comment un buffer overflow écrase la pile décrit l’organisation de la pile, l’adresse de retour et ce que l’écrasement permet. Rien à ajouter ici.

Pour les défenses, ASLR, DEP, NX, canaris et CFI est la page de référence : elle explique précisément ce que chaque protection empêche, et c’est la lecture qui permet de comprendre pourquoi cette variante a cessé d’être une prise de contrôle facile sans cesser d’être une vulnérabilité. Rust et la sécurité mémoire par construction décrit l’autre voie, celle qui supprime la classe.

Activer et vérifier les protections du compilateur. Canaris, pile non exécutable, renforcement des fonctions de copie, et surtout contrôle que les options sont bien actives dans les binaires livrés : l’écart entre la configuration de compilation réputée et les options réellement passées au compilateur est un classique des audits.

Éviter les tampons de taille fixe sur la pile pour tout ce qui vient de l’extérieur. Une allocation dimensionnée sur la donnée reçue (avec une borne maximale explicite) supprime la coïncidence entre une entrée longue et une variable locale.

Vérifier les calculs de taille eux-mêmes, en se souvenant de CWE-680 : une multiplication de deux valeurs reçues peut déborder avant d’avoir servi à allouer. Les fonctions d’allocation qui prennent un nombre d’éléments et une taille unitaire, et refusent le produit excessif, existent pour cette raison.

Une équipe qui ne distingue pas le débordement de pile de l’épuisement de pile classe les deux au même endroit du suivi, et les traite avec la même priorité : généralement celle du plantage, c’est-à-dire basse. Le défaut d’intégrité reste alors en production, protégé par les seuls canaris du compilateur, qui transforment l’exploitation en arrêt du service. C’est un filet de sécurité, pas une correction : il tombe dès que le binaire est compilé sans l’option, ce qui est le cas de la plupart des composants embarqués, et dès que l’écrasement atteint une donnée utile avant le canari. Le coût ne se révèle qu’à l’exploitation, et il est maximal : le détournement du fil d’exécution donne à l’attaquant les privilèges du processus, sur un appareil qui n’a souvent ni journalisation ni moyen de détection, et dont le correctif suppose une mise à jour de micrologiciel que personne ne déploiera.

Quand un programme appelle une fonction, il range temporairement sur une pile tout ce dont cette fonction a besoin : ses variables de travail, et l’adresse de l’instruction à laquelle il devra revenir une fois le travail terminé. Cette pile fonctionne comme une pile d’assiettes : on empile en entrant dans une fonction, on dépile en sortant.

Si l’une des variables de travail est un espace de taille fixe et que le programme y recopie plus de données que prévu, le surplus écrase ce qui se trouve juste à côté sur la pile : et il s’y trouve, entre autres, l’adresse de retour. Celui qui a fourni les données trop longues peut donc écrire à cet endroit l’adresse de son choix : au moment de revenir, le programme ne revient pas où il devait, mais où l’attaquant l’envoie.

C’est le défaut que le catalogue CWE appelle CWE-121. Attention à un piège de vocabulaire : l’expression anglaise « stack overflow » désigne aussi un phénomène tout autre, l’épuisement de la pile par une fonction qui s’appelle elle-même sans fin. Celui-là ne fait que planter le programme ; il porte un autre numéro.

Depuis une vingtaine d’années, les compilateurs placent une valeur témoin entre les variables et l’adresse de retour, et vérifient avant chaque sortie de fonction qu’elle n’a pas bougé. Si elle a changé, le programme s’arrête volontairement. Cette protection a rendu l’attaque beaucoup plus difficile, mais elle ne corrige pas le défaut : elle en change la conséquence, en remplaçant une prise de contrôle par une panne. Et elle est absente d’une bonne partie des logiciels embarqués dans les appareils, qui sont justement ceux qu’on ne met jamais à jour.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.