Dépassement de tampon - Définition et Explications

Source: Wikipédia sous licence CC-BY-SA 3.0.
La liste des auteurs est disponible ici.

En informatique, un dépassement ou débordement de tampon (en anglais, buffer overflow) est un bogue pouvant être exploité pour violer la politique de sécurité d’un système. Cette technique est couramment utilisée par les pirates informatiques.

Lorsque le bogue est provoqué inintentionnellement, le comportement de la machine devient complètement (Le complètement ou complètement automatique, ou encore par anglicisme complétion ou autocomplétion, est une fonctionnalité...) imprévisible. Cela se manifeste souvent par un blocage du programme, voire de tout (Le tout compris comme ensemble de ce qui existe est souvent interprété comme le monde ou l'univers.) le système.

Lorsque le bogue est exploité à des fins malveillantes, la stratégie (La stratégie - du grec stratos qui signifie « armée » et ageîn qui signifie « conduire » - est :) du pirate est de détourner le programme bogué en lui faisant exécuter un code externe, hostile ou non.

Plus techniquement, le principe est de profiter de l’accès à certaines variables du programme, souvent par le biais de fonctions telles scanf() (analyse de chaîne (Le mot chaîne peut avoir plusieurs significations :) de caractères) ou strcpy() (copie de chaîne de caractères) en langage C, qui ne contrôlent pas la taille de la chaîne à enregistrer dans un tampon, afin d’écraser la mémoire (D'une manière générale, la mémoire est le stockage de l'information. C'est aussi le souvenir d'une information.) du processeur (Le processeur, ou CPU (de l'anglais Central Processing Unit, « Unité centrale de traitement »), est le composant de l'ordinateur qui exécute les programmes informatiques. Avec la mémoire notamment, c'est l'un des...) jusqu’à l’adresse de retour de la fonction en cours d’exécution. On peut ainsi choisir quelles seront les prochaines instructions exécutées par le processeur. Le code introduit est généralement exécuté avec les droits du programme détourné.

Afin d’éviter ces dépassements, certaines fonctions ont été réécrites pour prendre en paramètre (Un paramètre est au sens large un élément d'information à prendre en compte pour prendre une décision ou pour effectuer un calcul.) la taille du buffer dans lequel les données (Dans les technologies de l'information (TI), une donnée est une description élémentaire, souvent codée, d'une chose, d'une transaction d'affaire, d'un...) sont copiées, et éviter ainsi de copier plus qu’il ne contient. Ainsi strncpy() est une version de strcpy() qui prend la taille du buffer. Dès que les techniques de débordement de tampon commencèrent à se généraliser, l’équipe de FreeBSD (FreeBSD est un système d'exploitation UNIX libre. Le nom vient de l'association d'une part de free qui signifie à la fois « libre » et « gratuit » dans l'anglais courant, et d'autre...) cessa tout nouveau développement pendant plusieurs mois (Le mois (Du lat. mensis «mois», et anciennement au plur. «menstrues») est une période de temps arbitraire.) afin de colmater toutes les brèches de ce genre dans leur code source (Le code source (ou les sources voire le source) est un ensemble d'instructions écrites dans un langage de programmation informatique de haut niveau,...). Celle de Linux (Au sens strict, Linux est le nom du noyau de système d'exploitation libre, multitâche, multiplate-forme et multi-utilisateur de type UNIX créé par Linus Torvalds, souvent désigné comme le noyau Linux. Par extension,...) temporisa un peu plus.

strncpy présente l'inconvénient majeur d'être peu efficace, puisqu'elle remplit toute la fin du tampon de zéros. Theo de Raadt et Todd Miller ont conçu strlcpy et strlcat qui ne présentent pas ce défaut. Disponible initialement sous OpenBSD (OpenBSD est un système d'exploitation libre de type Unix, dérivé de 4.4BSD. Créé en 1994 par Theo de Raadt, il est issu de la séparation avec NetBSD, le...), ces fonctions tendent à se répandre dans divers logiciels (rsync, kde (KDE est un projet de logiciel libre historiquement centré autour d'un environnement de bureau pour systèmes UNIX. Ce projet a évolué et il se compose maintenant d'un ensemble de technologies : des...), etc.). Seul Linux, et plus particulièrement Ulrich Drepper, font de la résistance active...

Cas particulier: débordement entier

Il est fréquent d'allouer dynamiquement des tableaux de structure de données, ce qui nécessite de calculer la taille totale comme un produit taille_el * nombre_el. Si on n'y prend pas garde, un tel produit peut donner un nombre (La notion de nombre en linguistique est traitée à l’article « Nombre grammatical ».) trop grand pour être représentable (débordement de capacité). On se retrouve alors avec une zone mémoire allouée de taille inférieure à ce qu'on pense avoir alloué. C'est un cas très particulier de dépassement de tampon (En informatique, un dépassement ou débordement de tampon (en anglais, buffer overflow) est un bogue pouvant être exploité pour violer la...), qui peut être détourné par un pirate.

Ces failles de sécurité ont été exploitées de façon courante depuis le début des années 2000, en particulier dans OpenSSH, et dans les bibliothèques de lecture de pratiquement tous les types d'image.

Détails techniques sur architecture (L’architecture peut se définir comme l’art de bâtir des édifices.) 80x86 (Intel)

Un programme en exécution (on parle de processus) découpe la zone de mémoire adressable en zones distinctes :

  • la zone de code où sont stockées les instructions du programme en train (Un train est un véhicule guidé circulant sur des rails. Un train est composé de plusieurs voitures (pour transporter des personnes) et/ou de plusieurs wagons (pour transporter des marchandises), et peut être...) de s’exécuter.
  • la zone des données où sont stockées les données que manipule le programme. Cette zone est généralement à son tour découpée en plusieurs zones (variables statiques, initialisées ou non, etc.)
  • la zone de la pile
  • la zone du tas.

Les deux dernières zones servent (Servent est la contraction du mot serveur et client.) de mémoire temporaire au code. Leur spécificité est qu’elles sont toutes les deux dynamiques au contraire des autres. Ce qui veut dire que leur taille peut varier tout au long de l’exécution d’un processus.

La zone du tas correspond aux variables déclarées en dehors de toute fonction (globale) et la zone de la pile correspond aux variables locales à une fonction. À noter que le tas contient aussi toutes les données déclarées dynamiquement (c.-à-d. malloc en C ou new en C++).

La zone de la pile est nécessaire aux sous-fonctions (variables locales et passage d’arguments). Elle se comporte comme son nom l’indique, i.e., dernier entré, premier sorti. Les variables déclarées au sein d’une fonction sont empilées à leur déclaration et dépilées à la fin de la fonction.

La convention générale d'appel de fonctions, telle qu'utilisée par la plupart des langages (autres que le langage machine), est la suivante:

  1. Empiler les arguments de la fonction (dans l’ordre inverse (En mathématiques, l'inverse d'un élément x d'un ensemble muni d'une loi de composition interne · notée multiplicativement, est un élément y tel que x·y = y·x = 1, si 1 désigne...) généralement) via l’instruction push
  2. Appeler la fonction, via l’instruction call avec comme argument l’adresse mémoire de la sous-routine.

Une fonction est un sous-ensemble (En mathématiques, un ensemble A est un sous-ensemble ou une partie d’un ensemble B, ou encore B est sur-ensemble de A, si tout élément du sous-ensemble...) de code ayant un comportement particulier : il est possible à tout moment de retourner de cette fonction, via l'instruction (Une instruction est une forme d'information communiquée qui est à la fois une commande et une explication pour décrire l'action, le comportement, la méthode ou la tâche qui devra commencer, se terminer, être...) ret. L’exécution du code doit alors reprendre à l’instruction suivant l’appel de fonction.

Pour ce faire, l’instruction call enregistre l’état du programme avant l’appel de fonction, i.e., elle stocke l’adresse à laquelle devra retourner la fonction à sa terminaison. Lors de l’exécution de l’instruction ret qui permet à une sous-fonction de se terminer, le processeur récupère alors l’adresse (Les adresses forment une notion importante en communication, elles permettent à une entité de s'adresser à une autre parmi un ensemble d'entités. Pour qu'il...) de retour qu’il a précédemment stockée et le processus peut continuer son exécution. Cette adresse de retour est aussi stockée dans la pile.

La procédure d’appel de fonction devient alors :

  1. Empiler les arguments de la fonction (push)
  2. Appeler la fonction (call), ce qui se décompose du point (Graphie) de vue (La vue est le sens qui permet d'observer et d'analyser l'environnement par la réception et l'interprétation des rayonnements lumineux.) du processeur en :
    1. Sauvegarde (En informatique, la sauvegarde (backup en anglais) est l'opération qui consiste à dupliquer et à mettre en sécurité les données contenues dans un système informatique.) de l’adresse de retour
  3. Entrée de la fonction (par convention) :
    1. Sauvegarde de l'état actuel de la pile (registre ebp)
    2. Initialisation d'une nouvelle pile " locale "
    3. Allocation des variables locales (manipulation de esp)
  4. Exécution de la fonction
  5. Sortie de la fonction (par convention) :
    1. Restauration de l'état précédent de la pile
  6. Exécution de l’instruction ret :
    1. Récupération de l’adresse de retour et branchement à cette adresse

Illustration

Soit l’extrait de programme C suivant (volontairement simplifié) :

 
 #include  
 void foo(char *str) 
 { 
 char buffer[32]; 
 strcpy(buffer, str); 
 /* ... */ 
 } 
 int main (La main est l’organe préhensile effecteur situé à l’extrémité de l’avant-bras et relié à ce dernier par le poignet. C'est un organe destiné à saisir et manipuler des objets. Chez...)(int argc, char *argv[]) 
 { 
 if (argc > 1) { 
 /* appel avec le premier argument de la ligne de commandes */ 
 foo(argv[1]); 
 } 
 /* ... */ 
 return 0; 
 } 
 

Ce qui est traduit par un compilateur C (ici Gcc), par (syntaxe Intel) :

 
 push ebp              ; entrée de la fonction 
 mov ebp,esp           ; 
 sub esp,40            ; 40 octets sont "alloués" (32 + les 2 variables qui serviront 
 ; à l'appel de strcpy) 
 mov eax,[ebp+0x8]     ; argument de la fonction (str) 
 mov [esp+0x4],eax     ; préparation de l'appel de fonction: 2e argument 
 lea eax,[ebp-0x20]    ; ebp-0x20 contient la variable (En mathématiques et en logique, une variable est représentée par un symbole. Elle est utilisée pour marquer un rôle dans une formule, un prédicat ou un algorithme.      En...) locale 'buffer' 
 mov [esp],eax         ; 1er argument 
 call strcpy 
 ; sortie de la fonction 
 leave                 ; équivalent à mov esp,ebp et pop ebp 
 ret 
 

Voici l'état de la pile et de ces deux registres (ebp et esp) juste avant l'appel de la fonction strcpy

État de la pile avant l'appel à la fonction strcpy
État de la pile avant l'appel à la fonction strcpy

Ainsi, strcpy copiera le contenu de str dans buffer. strcpy fonctionne sans aucune vérification: il copiera le contenu de str jusqu'à trouver un caractère de fin de chaîne (caractère nul).

Si str fait plus de 32 octets de long, strcpy continuera à copier le contenu de la chaîne au-delà de la zone allouée par la variable locale buffer. C’est ainsi que les variables stockées dans la pile pourront être écrasées et en particulier l’adresse de retour de la fonction.

Débordement de la pile : la fonction strcpy copie la chaîne vers la zone mémoire indiquée et finit par dépasser la zone allouée et écraser l'adresse de retour de la fonction foo
Débordement de la pile : la fonction strcpy copie la chaîne vers la zone mémoire indiquée et finit par dépasser la zone allouée et écraser l'adresse de retour de la fonction foo

C’est ce qui est généralement considéré comme un bogue du programme : le programmeur (En informatique, un développeur (ou programmeur) est un informaticien qui réalise du logiciel en créant des algorithmes et en les mettant en œuvre dans un langage de programmation.) a, par exemple, prévu une certaine taille de stockage qu’il juge (Le juge peut être un professionnel du droit, désigné ou élu pour exercer son office. Il peut également être un simple citoyen appelé temporairement à rendre la justice : c'est notamment le cas...) maximale (256 octets pour stocker le nom d’un fichier ( Un fichier est un endroit où sont rangées des fiches. Cela peut-être un meuble, une pièce, un bâtiment, une base de données informatique. Par...) devraient être suffisants) et l’appelant passe en paramètre une chaîne plus longue que prévu. L’adresse de retour de la pile est écrasée et à la terminaison de la fonction (ret), le processeur tente de brancher vers une adresse non prévue : le résultat est généralement une adresse en dehors de la plage (La Plage est un film anglo-américain réalisé par Danny Boyle en 2000 et adapté du roman The Beach d'Alex Garland) adressable et le programme " plante " en renvoyant un message (La théorie de l'information fut mise au point pour déterminer mathématiquement le taux d’information transmis dans la communication d’un message par un canal de communication, notamment en présence de parasites appelés...) d’erreur (segmentation fault).

Un utilisateur malveillant peut utiliser ce comportement à ses fins. Par exemple : en connaissant la taille du tampon (dans l’exemple précédent 32 octets), il peut vouloir écraser l’adresse de retour pour la faire pointer vers un code à lui, de manière à prendre le contrôle (Le mot contrôle peut avoir plusieurs sens. Il peut être employé comme synonyme d'examen, de vérification et de maîtrise.) du programme. De cette manière, il a les droits d’exécution associés au programme et peut dans certains cas accéder à des ressources critiques.

La forme d’attaque la plus simple consiste à passer (Le genre Passer a été créé par le zoologiste français Mathurin Jacques Brisson (1723-1806) en 1760.) dans une chaîne un code assembleur suivi d’une adresse de retour vers ce même code. Le but du code est généralement d’avoir le plus de possibilités : sur les systèmes Unix (UNIX (marque déposée officiellement comme UNIX, parfois aussi écrit comme Unix avec des petites capitales) est le nom d'un système d'exploitation multitâche et multi-utilisateur créé en 1969,...), cela consiste généralement à ouvrir un shell.

La forme de défense la plus simple consiste à modifier un peu la source du compilateur pour qu’il insère irrégulièrement des codes NOP de façon imprévisible, puis à utiliser ce nouveau compilateur pour recompiler le noyau et les applications (opérations de routine en Linux). Cela ne ralentit que peu les programmes, mais complique énormément la tâche du pirate qui ne sait plus aussi bien quelles adresses il doit viser.

Dans l’exemple précédent, si str contient un code malveillant sur 32 octets suivis de l’adresse de buffer, au retour de la fonction, le processeur exécutera le code contenu dans str.

Pour que cette " technique " soit efficace, il y a cependant deux contraintes :

  • le code malveillant ne doit contenir aucun caractère nul, sans quoi strcpy() s’arrêtera de copier, mais pas memcpy().
  • l’adresse de retour ne doit pas non plus contenir de caractère nul

Saut indirect

La technique précédente nécessite généralement pour l’attaquant de deviner l’adresse de retour vers le code malveillant. Ceci est assez contraignant, car cela demande généralement des essais successifs, ce qui n’est pas une méthode très " discrète ". De plus, il y a de fortes chances que l’adresse de la pile change d’une version à l’autre du programme vulnérable et d’un système à l’autre.

Le but du pirate est généralement de pouvoir exploiter une faille sur le plus de versions possible du même programme (afin peut-être de concevoir un virus (Un virus est une entité biologique qui nécessite une cellule hôte, dont il utilise les constituants pour se multiplier. Les virus existent sous une forme...) ou ver). Pour s’affranchir des précédentes contraintes, il doit trouver une méthode qui lui permette de brancher sur son code en se préoccupant le moins possible de la version du système et du programme vulnérable tout en étant le plus discret possible (le moins possible de tentatives).

Il est possible dans certains cas, de se servir du contexte (Le contexte d'un évènement inclut les circonstances et conditions qui l'entourent; le contexte d'un mot, d'une phrase ou d'un texte inclut...) d’exécution du système cible. Par exemple, sur des systèmes Windows (Windows est une gamme de systèmes d'exploitation produite par Microsoft, principalement destinées aux machines compatibles PC. C'est le remplaçant de MS-DOS. Depuis les années 1990, avec la sortie de Windows 95, son succès commercial pour...), la plupart des programmes, même les plus simples " contiennent " un ensemble (En théorie des ensembles, un ensemble désigne intuitivement une collection d’objets (les éléments de l'ensemble), « une multitude qui peut...) de primitives systèmes accessibles au programme (DLL). Il est possible de trouver des bibliothèques dont l’adresse mémoire lors de l’exécution change peu en fonction de la version du système. Le but pour l’attaquant est alors de trouver dans ces plages mémoires des instructions qui manipulent la pile et lui permettront d’exécuter son code.

Par exemple, en supposant que la fonction strcpy manipule un registre processeur (eax) pour y stocker l’adresse source. Dans l’exemple précédent, eax contiendra une adresse proche de l’adresse de buffer au retour de strcpy. Le but de l’attaquant est donc de trouver dans la zone mémoire supposée " fixe " (la zone des DLL par exemple) un code qui permet de sauter vers le contenu de eax (call eax ou jmp eax). Il construira alors son buffer en plaçant son code suivi de l’adresse d’une instruction de saut (call eax ou jmp eax). Au retour de strcpy, le processeur branchera vers une zone mémoire contenant call eax ou jmp eax et puisque eax contient l’adresse du buffer, il branchera de nouveau vers le code et l’exécutera...

Autres techniques

Il existe bien d’autres techniques d’exploitation de telles failles.

La deuxième difficulté pour l’attaquant est la construction du code d’exploitation appelé shellcode. Dans certains cas, le code doit être construit sur un jeu de caractères réduit : chaîne Unicode (Unicode est une norme informatique, développée par le Consortium Unicode, qui vise à donner à tout caractère de n’importe quel système d’écriture un nom et un identifiant...), chaîne alphanumérique, etc. En pratique, ces limitations seules n'arrêteront pas un pirate déterminé. On recense des cas avérés de piratage utilisant du code machine limité aux caractères légaux d'une chaîne Unicode (mais il faut pouvoir exécuter du code automodifiant.

D’autres techniques existent pour exploiter les variables contenues dans d’autres parties de la mémoire, en particulier les attaques sur les variables du tas, appelées heap overflow (dépassement de tas).

Prévention (La prévention est une attitude et/ou l'ensemble de mesures à prendre pour éviter qu'une situation (sociale, environnementale, économique..) ne se...)

Pour se prémunir de telles attaques, plusieurs options sont offertes au programmeur. Les différentes techniques de prévention font débat (Un débat est une discussion (constructive) sur un sujet, précis ou de fond, annoncé à l'avance, à laquelle prennent part des individus ayant des avis, idées, réflexions ou opinions divergentes pour...). En voici quelques-unes :

Protections logicielles

  • Utiliser un autre langage que le C/C++, qui ne comprend aucun mécanisme de vérification de dépassement (Un dépassement est le fait de rouler pendant un instant, en général relativement court, à côté d’un autre véhicule à...) des bornes. Utiliser éventuellement des outils externes qui permettent en mode développement de tester les cas " litigieux ", par exemple la bibliothèque Electric Fence ou Valgrind,
  • Bannir de son utilisation les fonctions dites " non protégées ". Préférer par exemple strncpy à strcpy qui effectue un contrôle de taille. Les compilateurs récents peuvent prévenir le programmeur s’il utilise des fonctions " à risque ", même si l’exploitation reste possible,
  • Protéger, côté système, la pile :
    • Rendre la pile non exécutable,
    • Mettre en place un mécanisme de vérification de la pile ([technique du canari (Le Canari est la forme domestiquée du Serin des Canaries (Serinus canaria). La sélection de ces oiseaux au sein des élevages a permis de créer de nombreuses races et variétés en privilégiant...)] le principe est de stocker une clé, générée à l’exécution entre la fin de la pile et la valeur de retour. Si cette clé est modifiée, l’exécution est avortée (disponible en option dans les compilateurs C récents - éventuellement avec recours à des patchs). La principale critique étant que l’appel de fonction est ralenti.

Aucune de ces solutions ne s’est pour l’instant imposée dans le monde (Le mot monde peut désigner :) du développement industriel. Pourtant, ces attaques représentent une part encore importante des failles permettant le développement de vers ou virus, ou d'attaques manuelles.

Protections matérielles

  • les processeurs récents, 64 bits notamment, implémentent des protections efficaces (technologies NX Bit et XD bit).
Cet article vous a plu ? Partagez-le sur les réseaux sociaux avec vos amis !
Page générée en 0.232 seconde(s) - site hébergé chez Amen
Ce site fait l'objet d'une déclaration à la CNIL sous le numéro de dossier 1037632
Ce site est édité par Techno-Science.net - A propos - Informations légales
Partenaire: HD-Numérique