Attaques par débordement de tampon en C++ : guide pratique
Snyk Security Research Team
28 juillet 2022
0 minutes de lectureUn débordement de tampon est un type d’erreur d’exécution qui permet à un programme d’écrire au-delà de la fin d’un tampon ou d’un tableau — d’où le terme « débordement » — et de corrompre la mémoire adjacente. Comme la plupart des bogues, un débordement de tampon ne se manifeste pas à chaque exécution du programme. La vulnérabilité se déclenche plutôt dans certaines circonstances, par exemple lorsque l’utilisateur saisit des données inattendues.
Une attaque par débordement de tampon consiste à exploiter une vulnérabilité de débordement de tampon — généralement par un acteur malveillant qui cherche à obtenir un accès ou des informations. Dans cet article, nous expliquons comment se produit un débordement de tampon et vous montrons comment protéger votre code C++ contre ces attaques.
Exemple d’attaque par débordement de tampon
Pour comprendre comment se produit un débordement de tampon, examinons le code suivant. Il effectue une simple vérification de mot de passe et est vulnérable à une attaque par débordement de tampon :
L’extrait de code demande à l’utilisateur de saisir un mot de passe (ligne 14). Il compare ensuite cette saisie au mot de passe enregistré (ligne 23), qu’il a préalablement chargé (ligne 12). Si les deux correspondent, l’accès est accordé à l’utilisateur.
En pratique, nous lirions le mot de passe depuis un fichier à l’aide de std::fscanf, mais, pour simplifier cet exemple, nous le lirons plutôt depuis une chaîne constante. Idéalement, nous stockerions également une version salée et hachée de notre mot de passe au lieu de l’original. Pour simplifier, nous utiliserons toutefois un mot de passe en texte brut.
Un mécanisme de ce type pourrait servir à déverrouiller la version shareware — ou d’essai — d’une application, ou à accorder à l’utilisateur l’accès à des informations et fonctionnalités de l’application après saisie du mot de passe d’un administrateur.
Exécutons le programme pour voir ce qui se passe :
Comme prévu, la saisie du bon mot de passe nous donne accès à l’application. À l’inverse, un mot de passe incorrect entraîne un refus d’accès :
Jusqu’ici, tout fonctionne comme prévu. Cependant, le bon mot de passe n’est pas le seul moyen d’accéder à cette application. Voyons ce qui se passe si nous saisissons « sunshinesunshine » à la place :
C’est inattendu. Nous avons saisi une chaîne très différente du bon mot de passe « rictro » — et l’accès nous a tout de même été accordé.
L’explication, c’est que nous avons réussi à lancer une attaque par débordement de tampon contre le programme en question. Pour comprendre comment cela s’est produit, décommentons les affichages de débogage des lignes 18 à 21 et relançons l’application.
Commençons par l’exécuter avec la bonne saisie :
La sortie ne présente rien de notable.
Essayons un mot de passe incorrect :
Une fois encore, l’application se comporte comme prévu.
Voyons maintenant ce qui se passe lorsque nous saisissons notre mot de passe spécial, « sunshinesunshine ».
Le mot de passe n’a plus la valeur « rictro » : il contient maintenant « sunshine ».
Nous utilisons deux tableaux de 8 octets : password stocke le mot de passe de l’application et input stocke la saisie de l’utilisateur. Le compilateur utilisé dans cet exemple (GCC 10.3) place password huit octets après input (0x7ffc5581e4b0 - 0x7ffc5581e4a8 = 8), de sorte que les tableaux sont adjacents en mémoire. Notez qu’un autre compilateur peut produire des résultats différents.
Les mots de passe de moins de huit caractères produisent des blocs mémoire qui ressemblent à ceci :

En revanche, si nous saisissons « sunshinesunshine », la mémoire ressemble à ceci :

Le terminateur nul \0 est écrit au-delà de la fin de password, remplaçant ce qui se trouve alors sur la pile.
Cela se produit parce que std::cin (ligne 15) n’effectue aucune vérification des limites. Il lit les données saisies dans la console jusqu’à rencontrer un saut de ligne — autrement dit, jusqu’à ce que l’utilisateur appuie sur Entrée — sans vérifier que le tampon de destination est assez grand pour contenir la saisie.
Comme nous comparons uniquement les huit premiers caractères de password et de input à l’aide de std::strncmp (ligne 23), afin d’éviter de lire au-delà de la fin des tableaux, nous obtenons une correspondance qui ne devrait pas exister.
Comment prévenir les débordements de tampon en C++
Comparé à d’autres langages de programmation de haut niveau, le C++ est particulièrement vulnérable aux débordements de tampon, car une grande partie de son écosystème, y compris certaines parties de la bibliothèque standard C++, utilise encore des pointeurs bruts. Nous pouvons utiliser des tampons gérés comme std::vector ou std::string dans notre propre code, mais nous perdons les vérifications des limites dès que nous utilisons des API de style C qui nous obligent à transmettre vector::data ou string::c_str.
Appliquez les bonnes pratiques de programmation en C++
La meilleure façon de prévenir les débordements de tampon consiste à utiliser des API qui n’y sont pas vulnérables. En C++, cela signifie privilégier les tampons et les chaînes gérés plutôt que les tableaux et les pointeurs bruts.
À privilégier | À éviter |
|---|---|
|
|
|
|
|
|
|
|
Nous pouvons corriger notre exemple d’application à l’aide de std::string.
Examinons la version corrigée. Notez les modifications apportées aux lignes 4, 10 et 24 :
std::string surcharge l’opérateur d’extraction (>>), ce qui lui permet de lire les flux en toute sécurité. Pour cela, il interroge le flux afin de connaître la taille des données saisies, alloue suffisamment de mémoire, puis lit les données. Il nous est ainsi impossible de provoquer un débordement du tampon, quelle que soit la longueur de la saisie. Au pire, nous pourrions manquer de mémoire.
Si vous devez utiliser des API de style C, essayez d’utiliser leurs équivalents « sécurisés », s’ils existent. Ces versions vérifient les limites, acceptent un paramètre size supplémentaire et refusent de lire ou d’écrire au-delà de celui-ci.
À privilégier | À éviter |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Utilisez AddressSanitizer pour prévenir les débordements de tampon
Outre les bonnes pratiques de programmation, des outils automatisés peuvent aider à détecter les débordements de tampon. AddressSanitizer (ASan) est l’un des plus populaires. Il est pris en charge par tous les principaux compilateurs, notamment Visual Studio (v16.9 et versions ultérieures), GCC (v4.8 et versions ultérieures), Clang (v3.1 et versions ultérieures) et Xcode (v7.0 et versions ultérieures). La détection des erreurs d’accès mémoire peut être activée avec l’option /fsanitize=address dans Visual Studio et l’option -fsanitize=address dans GCC/Clang.
ASan remplit deux fonctions :
Il ajoute quelques octets de « mémoire empoisonnée » autour de tous les objets de la pile et de toutes les allocations sur le tas en remplaçant
mallocpar une version modifiée.Le compilateur insère du code dans votre application pour détecter toute tentative d’accès à la mémoire empoisonnée.
Nous avons vu précédemment comment nos tableaux input et password sont disposés en mémoire lors d’une compilation avec GCC 10.3. Si nous activons la détection des erreurs d’accès mémoire, leur disposition peut changer et ressembler à ceci :

Les symboles ✗ représentent la mémoire empoisonnée placée autour de nos tableaux.
Le code inséré par le compilateur contient une logique qui détecte toute tentative d’accès à la mémoire empoisonnée. Prenons un simple extrait de code avant transformation :
Après son traitement par ASan, le code ressemble à ceci :
Pour déterminer si l’application tente d’accéder à de la mémoire empoisonnée — en implémentant la fonction is_poisoned —, ASan utilise la « mémoire fantôme ». Il s’agit d’une région mémoire distincte qui stocke des métadonnées sur la mémoire réelle de l’application.
En pratique, l’algorithme prend en compte davantage de facteurs que ne le permet le cadre de cet article, mais cette implémentation native simplifiée illustre le principe.
Imaginez un simple tableau de bits dans lequel chaque octet auquel l’application accède correspond à un bit dans la mémoire fantôme indiquant si cet octet est empoisonné.
Une simple déclaration pourrait ressembler à ceci :
Pour mettre en œuvre la mémoire fantôme, la déclaration pourrait être transformée comme suit :
L’utilisateur déclare sur la pile un char array de 6 octets. Le compilateur crée plutôt un tableau de 8 octets et renvoie un pointeur vers son milieu, en ajoutant 1 octet de chaque côté. Il écrit ensuite le motif binaire 10000001 dans le bitset shadow_memory, en utilisant l’adresse temp comme index. Cela indique que les 6 octets de password sont sains, tandis que les deux octets qui les entourent sont empoisonnés.
Le compilateur peut alors facilement déterminer si nous essayons d’accéder à de la mémoire empoisonnée. Prenons l’extrait suivant :
Maintenant que vous comprenez les principes de base de la détection des erreurs d’accès mémoire, voyons ce qui se passe si nous transmettons la saisie malveillante « sunshinesunshine » à notre application vulnérable d’origine, cette fois avec la détection activée :
Notez deux choses :
L’écart entre nos tableaux
inputetpasswordest passé de 8 à 32 octets (0x7ffc5581e4c8-0x7ffc5581e4a8= 32), afin de laisser de la place à la mémoire empoisonnée.Le compilateur détecte une corruption de mémoire à l’adresse
0x7ffc5581e4b0, exactement 8 octets aprèsinput, ce qui correspond au premier octet empoisonné rencontré.
Protégez vos projets C++
Dans cet article, nous avons présenté les principes de base des attaques par débordement de tampon en C++ et les meilleures façons de protéger vos projets. En résumé, un débordement de tampon est une vulnérabilité qui permet à un programme d’écrire au-delà de la fin d’un tampon, entraînant une corruption de la mémoire — laquelle peut ensuite être exploitée pour obtenir l’accès à des applications ou informations protégées.
Parmi les langages de haut niveau, le C++ est particulièrement vulnérable aux débordements de tampon, car de nombreuses API utilisent encore des pointeurs bruts et ne vérifient pas les limites. Vous pouvez limiter ce risque en utilisant des tampons et des chaînes gérés plutôt que des API de style C. Si vous devez utiliser une API de style C, privilégiez ses versions sécurisées, qui acceptent un paramètre de taille supplémentaire. Les attaques par débordement de tampon peuvent également être prévenues à l’aide d’outils de détection des erreurs d’accès mémoire, qui repèrent les défauts et les dépassements de mémoire.
Pour en savoir plus sur la sécurité en C++, consultez notre introduction accessible aux vulnérabilités en C/C++ et découvrez les vulnérabilités de traversée de répertoires en C/C++.
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis de capture du drapeau en regardant à la demande notre atelier virtuel d’initiation.
