Le buffer overflow, ou dépassement de tampon, est parmi les vulnérabilités les plus anciennes et les plus exploitées. Malgré cette antériorité, ils restent aujourd’hui une menace importante.
En effet, que ce soit sur des serveurs ou des applications critiques, les conséquences d’un buffer overflow peuvent être dévastatrices. Dans cet article, nous explorerons en détail les principes du dépassement de tampon et les différents types d’attaques. Nous détaillerons également les méthodes d’exploitation, ainsi que les bonnes pratiques sécurité permettant de s’en protéger efficacement.
Guide complet sur le buffer overflow
Qu’est-ce que le dépassement de tampon (buffer overflow) ?
Un dépassement de tampon (buffer overflow) se produit lorsqu’un processus écrit en dehors de la mémoire qui lui est allouée. Cela peut écraser des données cruciales pour le bon fonctionnement du programme.
Les langages C et C++ sont particulièrement vulnérables en raison de leur gestion manuelle de la mémoire Aussi, ils n’intègrent pas de mécanismes pour empêcher les dépassements ou les accès non autorisés.
En revanche, les langages comme Java, Python ou Perl, sont moins exposés. Leur typage fort et leurs outils de gestion mémoire réduisent ces risques. Toutefois, ils ne sont pas totalement immunisés, et des dépassements de tampon restent possibles.
Un dépassement de tampon peut provoquer un crash du programme. Mais dans certains cas, un attaquant peut exploiter cette faille pour exécuter du code arbitraire. Cela lui permet parfois de prendre le contrôle total du système, selon les privilèges du processus ciblé.
Avant de plonger dans l’explication des dépassements de tampon examinons le fonctionnement de la mémoire d’un processus.
Comment fonctionne la mémoire dans un processus ?
La mémoire d’un processus peut être divisée en cinq segments principaux :
- Le code : Ce segment contient les instructions compilées du programme, prêtes à être exécutées en langage machine.
- Le segment de données : Il regroupe les variables globales initialisées. Il comporte deux zones : une zone en lecture seule, pour les constantes; et une zone en lecture-écriture, pour les variables modifiables.
- Segment BSS (Block Starting Symbol) : Il contient les variables globales et statiques initialisées à zéro ou sans valeur initiale définie dans le code.
- La pile (stack) : Elle est utilisée pour gérer les appels de fonction. Et elle contient, les paramètres des fonctions, leurs variables locales, ainsi que l’adresse de retour après l’exécution. Par ailleurs, la pile suit une organisation « Dernier Entré, Premier Sorti » (Last In, First Out, ou LIFO), ce qui signifie que les dernières données ajoutées sont les premières à être retirées.
- Le tas (heap) : Il s’agit de la zone dédiée à l’allocation dynamique de mémoire, réalisée par le programmeur. Par exemple, avec
malloc()
en C ounew()
en C++. La mémoire allouée persiste jusqu’à sa libération explicite.
Ce modèle de segmentation est essentiel pour comprendre les failles liées aux dépassements de tampon. Le schéma ci-dessous représente l’organisation de la mémoire pour un programme une fois compilé :
Pour exploiter les dépassements de tampon, deux zones sont particulièrement ciblées : la pile et le tas. Ce sont les endroits où ces vulnérabilités apparaissent le plus fréquemment.
Quels sont les types d’attaques et exploitations buffer overflow ?
Les dépassements de tampon se divisent en deux catégories : les heap-based buffer overflow (dépassements de tampon basés sur le tas) et les stack-based buffer overflow (dépassements de tampon basés sur la pile).
Pour les exemples d’attaques et exploitations ci-dessous, nous utilisons une architecture 32 bits. Les binaires ont été compilés sans mécanismes de sécurité modernes comme le canari (stack canary) et l’ASLR (Address Space Layout Randomization), afin de faciliter l’exploitation.
Notez que que sur une architecture 64 bits, l’exploitation de buffer overflows diffère, notamment à cause des changements dans la gestion des registres et des adresses mémoire.
Dépassement de tampon de pile (stack-based buffer overflow)
Pour comprendre un dépassement de tampon basé sur la pile, il est essentiel de comprendre le fonctionnement de la pile d’exécution.
Fonctionnement de la pile d’exécution
Lorsqu’une fonction est appelée, plusieurs étapes se déroulent :
- Ajout des arguments sur la pile : Les arguments nécessaires à l’exécution de la fonction sont placés sur la pile, selon la convention d’appel utilisée.
- Sauvegarde de l’adresse de retour : L’adresse à laquelle l’exécution doit revenir après la fin de la fonction est sauvegardée sur la pile. Cette valeur est copiée dans le registre EIP (Extended Instruction Pointer). Sur une architecture 64 bits, ce registre est appelé RIP (Re-Extended Instruction Pointer).
- Création d’un nouveau cadre de pile : Un cadre de pile (stack frame) est créé pour stocker les variables locales et d’autres informations spécifiques à la fonction. Le registre EBP (Extended Base Pointer) pointe vers le bas de ce cadre, tandis que le registre ESP (Extended Stack Pointer) pointe vers le sommet de la pile. Chaque appel de fonction génère un cadre distinct, ce qui facilite la gestion des appels imbriqués.
Attaque buffer overflow de pile
Prenons le code suivant :
L’attaque consiste à réécrire l’adresse stockée dans le registre EIP pendant l’exécution de la fonction hello()
. Cela permet de détourner le flux d’exécution du programme et d’appeler une fonction spécifique, comme admin_panel()
.
Lorsque l’exécution entre dans la fonction hello()
, la pile est structurée de la manière suivante :
- L’adresse de retour a été ajoutée à la pile
- Le registre EBP pointe à la base de la nouvelle « stack frame »
- Les allocations pour les variables locales ont été effectuées (ici buffer)
- Le registre ESP pointe au sommet de la « stack frame »
Si un utilisateur entre une chaîne de 80 caractères, cela dépasse la mémoire allouée aux variables locales (ici 76 caractères). La valeur pointée par le registre EBP est alors écrasée.
Si l’utilisateur saisit une chaîne plus longue, disons 84 caractères, l’attaque cible directement l’adresse de retour stockée dans EIP. En remplaçant cette adresse par une nouvelle, formatée en little-endian (ordre des octets inversé), on peut rediriger l’exécution vers une fonction spécifique, comme admin_panel()
.
Dépassement de tampon de tas (heap-based buffer overflow)
L’exploitation d’un dépassement de tampon sur le tas est légèrement différente de celle sur la pile. Bien qu’il s’agisse toujours d’un dépassement où une entrée dépasse la taille allouée pour le buffer, la zone mémoire concernée est différente. Ici, il s’agit d’une zone allouée dynamiquement.
Un dépassement de tampon sur le tas peut entraîner plusieurs conséquences :
- Modification de variables adjacentes : Les données qui débordent peuvent écraser des zones de mémoire proches, modifiant ainsi la logique du programme.
- Crash du processus : Si les données corrompent une structure mémoire critique, cela peut provoquer un comportement instable et un arrêt du programme.
Prenons l’exemple suivant pour mieux comprendre cette attaque :
Le code ci-dessus montre un exemple de dépassement de tampon sur le tas. Deux buffers, nommés buffer
et secret
, sont alloués dynamiquement en mémoire. Stockés sur le tas, ces deux buffers sont adjacents, ce qui crée une situation propice à une attaque.
La fonction gets()
est utilisée pour remplir le buffer
. Cependant, gets()
est vulnérable, car elle ne vérifie pas la taille des données entrantes. Si une chaîne de caractères trop longue est saisie, elle écrasera la valeur dans secret
.
L’adresse mémoire de buffer
est 0x00403160, et celle de secret
est 0x00403180. Si une chaîne de plus de 32 caractères (0x20 en hexadécimal) est saisie, elle dépasse la mémoire allouée à buffer
et commence à écraser secret
. En manipulant ces données, on peut forcer une nouvelle valeur dans secret
.
Selon l’architecture et l’implémentation de l’allocation dynamique, les deux variables peuvent ne pas être adjacentes en mémoire. Cela peut dépendre de la taille des buffers ou de l’alignement mémoire.
Buffer overflow et attaques DoS
Examinons maintenant un exemple de dépassement de tampon pouvant être exploité pour provoquer un déni de service.
Cet exemple est basé sur la CVE-2021-44790, qui concerne une attaque sur une variable de type size_t
. Cette variable est utilisée pour allouer un buffer, qui est ensuite manipulé dans la fonction memcpy()
.
Voici le code vulnérable :
En raison de l’overflow, le buffer alloué a une taille de 0, ce qui provoque une SEGMENTATION FAULT. Cela signifie que le programme tente d’accéder à une zone mémoire interdite ou inexistante.
Une variable de type size_t
est utilisée pour déterminer la taille du buffer à allouer. À cause de l’overflow, la taille allouée devient 0, ce qui conduit à une SEGMENTATION FAULT. Le programme essaie alors d’accéder à une zone mémoire qui n’existe pas ou qui est protégée, entraînant ainsi le crash du processus.
Pour reproduire cette vulnérabilité, nous avons installé la version vulnérable d’Apache (2.4.51) dans un environnement Docker. Nous avons configuré le serveur pour exploiter cette vulnérabilité, qui se manifeste lorsque le parseur multipart du module mod_lua
est utilisé. De plus, nous avons ajouté un fichier Lua à la racine du serveur. Ce fichier appelle la fonction r:parsebody()
et retourne simplement « Hello World ! ».
Une fois l’environnement configuré, nous vérifions que le serveur fonctionne correctement.
Nous avons utilisé la commande suivante pour exploiter la vulnérabilité :
Côté serveur, la taille du buffer alloué est calculée en fonction de la taille du corps de la requête et de la position du CLRF (\r\n), censée marquer la fin de la requête HTTP.
Cependant, en raison du dépassement de tampon, le buffer alloué devient de taille 0, ce qui provoque un comportement erroné.
Lorsque la requête est envoyée, le serveur plante, entraînant une erreur de SEGMENTATION FAULT côté serveur :
Après l’exploitation de cette vulnérabilité, le serveur devient inaccessible, empêchant ainsi l’exécution de tout service.
Buffer overflow et exécution de commande : StageFright
En juillet 2015, une vulnérabilité critique a été découverte dans la bibliothèque multimédia Stagefright, écrite en C++, utilisée par les téléphones Android des versions 2.2 (Froyo) à 5.1.1 (Lollipop).
À l’époque, environ 95 % des téléphones Android, soit environ 950 millions d’appareils, étaient vulnérables.
L’exploitation de cette faille ne nécessitait aucune interaction de l’utilisateur. Il suffisait à l’attaquant de connaître le numéro de téléphone de la victime. En envoyant un MMS ou tout autre fichier multimédia spécialement conçu, l’attaquant pouvait exécuter des commandes à distance sur le téléphone de la victime.
Impacts d’une attaque buffer overflow
Les conséquences d’un buffer overflow peuvent être variées :
- Crash du serveur : La corruption de la mémoire peut provoquer l’arrêt brutal du processus. Cela peut entraîner des comportements inattendus comme des boucles infinies ou des erreurs fatales.
- Exécution de commandes arbitraires : Une exploitation réussie peut permettre à un attaquant de lancer des commandes sur le système vulnérable.
- Ouverture à d’autres vulnérabilités : Une exécution de commande à distance peut servir de point d’entrée. L’attaquant peut exploiter d’autres failles pour escalader les privilèges, voler des données sensibles ou prendre le contrôle total du système.
Comment prévenir les attaques par dépassement de tampon ?
Il existe plusieurs méthodes pour se protéger contre les attaques par dépassement de tampon :
- ASLR (Address Space Layout Randomization) : Cette technique rend aléatoire la disposition des zones de mémoire (pile, tas, bibliothèques). Cela complique l’exploitation, car un attaquant doit deviner les adresses mémoire ciblées.
- NX (Non-Executable Stack) : Cette protection interdit l’exécution de code dans des zones non prévues pour cela, comme le tas et la pile. Même si un attaquant injecte du code malveillant, il ne pourra pas l’exécuter.
- SSP (Stack Smashing Protector) : Le SSP ajoute une valeur aléatoire (canary) avant l’adresse de retour de la pile. Si cette valeur est modifiée, le programme s’arrête pour prévenir l’attaque. Toutefois, des contournements existent.
- Fonctions sécurisées : Certaines fonctions comme
gets
,strcpy
,strcat
, oumemcpy
sont vulnérables. Il est conseillé d’utiliser des alternatives sécurisées pour éviter les dépassements. - Mises à jour régulières : Maintenir ses logiciels à jour est essentiel. De nouvelles vulnérabilités sont fréquemment découvertes. Par exemple, en décembre 2024, 18 CVE liées à des dépassements de tampon ont été publiées avec un score CVSS supérieur à 9.
Auteur : Théo ARCHIMBAUD – Pentester @Vaadata