Débordement du tampon de pile - Stack buffer overflow

Dans le logiciel, un débordement de tampon de pile ou un dépassement de tampon de pile se produit lorsqu'un programme écrit à une adresse mémoire sur la pile d'appels du programme en dehors de la structure de données prévue, qui est généralement un tampon de longueur fixe . Les bogues de débordement de tampon de pile sont causés lorsqu'un programme écrit plus de données dans un tampon situé sur la pile que ce qui est réellement alloué pour ce tampon. Cela entraîne presque toujours une corruption des données adjacentes sur la pile, et dans les cas où le débordement a été déclenché par erreur, cela entraînera souvent le plantage du programme ou son fonctionnement incorrect. Le débordement de tampon de pile est un type de dysfonctionnement de programmation plus général connu sous le nom de débordement de tampon (ou débordement de tampon). Le remplissage excessif d'un tampon sur la pile est plus susceptible de faire dérailler l'exécution du programme que le remplissage excessif d'un tampon sur le tas car la pile contient les adresses de retour pour tous les appels de fonction actifs.

Un débordement de tampon de pile peut être provoqué délibérément dans le cadre d'une attaque connue sous le nom d' écrasement de pile . Si le programme affecté s'exécute avec des privilèges spéciaux ou accepte des données provenant d'hôtes réseau non fiables (par exemple, un serveur Web ), le bogue est une vulnérabilité de sécurité potentielle. Si le tampon de pile est rempli de données fournies par un utilisateur non fiable, cet utilisateur peut corrompre la pile de manière à injecter du code exécutable dans le programme en cours et à prendre le contrôle du processus. Il s'agit de l'une des méthodes les plus anciennes et les plus fiables permettant aux attaquants d'accéder sans autorisation à un ordinateur.

Exploiter les débordements de tampon de pile

La méthode canonique pour exploiter un débordement de tampon basé sur la pile consiste à écraser l'adresse de retour de la fonction avec un pointeur vers les données contrôlées par l'attaquant (généralement sur la pile elle-même). Ceci est illustré strcpy()dans l'exemple suivant :

#include <string.h>

void foo(char *bar)
{
   char c[12];

   strcpy(c, bar);  // no bounds checking
}

int main(int argc, char **argv)
{
   foo(argv[1]);
   return 0;
}

Ce code prend un argument de la ligne de commande et le copie dans une variable de pile locale c. Cela fonctionne bien pour les arguments de ligne de commande inférieurs à 12 caractères (comme vous pouvez le voir dans la figure B ci-dessous). Tout argument de plus de 11 caractères entraînera une corruption de la pile. (Le nombre maximum de caractères sûrs est un de moins que la taille du tampon ici car dans le langage de programmation C, les chaînes se terminent par un caractère à octet nul. Une entrée de douze caractères nécessite donc treize octets à stocker, l'entrée suivie par l'octet zéro sentinelle. L'octet zéro finit alors par écraser un emplacement mémoire qui se trouve un octet au-delà de la fin du tampon.)

Le programme s'empile foo()avec diverses entrées :

A. - Avant la copie des données.
B. - "hello" est le premier argument de la ligne de commande.
C. - "AAAAAAAAAAAAAAAAAAAA\x08\x35\xC0\x80" est le premier argument de la ligne de commande.

Remarquez dans la figure C ci-dessus, lorsqu'un argument de plus de 11 octets est fourni sur la ligne de commande, foo()écrase les données de la pile locale, le pointeur de trame enregistré et, surtout, l'adresse de retour. Lorsqu'il foo()revient, il supprime l'adresse de retour de la pile et saute à cette adresse (c'est-à-dire qu'il commence à exécuter des instructions à partir de cette adresse). Ainsi, l'attaquant a écrasé l'adresse de retour avec un pointeur vers le tampon de pile char c[12], qui contient désormais des données fournies par l'attaquant. Dans un véritable exploit de débordement de tampon de pile, la chaîne de "A" serait à la place un shellcode adapté à la plate-forme et à la fonction souhaitée. Si ce programme avait des privilèges spéciaux (par exemple, le bit SUID défini pour s'exécuter en tant que superutilisateur ), alors l'attaquant pourrait utiliser cette vulnérabilité pour obtenir des privilèges de superutilisateur sur la machine affectée.

L'attaquant peut également modifier les valeurs des variables internes pour exploiter certains bugs. Avec cet exemple :

#include <string.h>
#include <stdio.h>

void foo(char *bar)
{
   float My_Float = 10.5; // Addr = 0x0023FF4C
   char  c[28];           // Addr = 0x0023FF30

   // Will print 10.500000
   printf("My Float value = %f\n", My_Float);

    /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       Memory map:
       @ : c allocated memory
       # : My_Float allocated memory

           *c                      *My_Float
       0x0023FF30                  0x0023FF4C
           |                           |
           @@@@@@@@@@@@@@@@@@@@@@@@@@@@#####
      foo("my string is too long !!!!! XXXXX");

   memcpy will put 0x1010C042 (little endian) in My_Float value.
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

   memcpy(c, bar, strlen(bar));  // no bounds checking...

   // Will print 96.031372
   printf("My Float value = %f\n", My_Float);
}

int main(int argc, char **argv)
{
   foo("my string is too long !!!!! \x10\x10\xc0\x42");
   return 0;
}

Différences liées à la plate-forme

Un certain nombre de plates-formes ont des différences subtiles dans leur implémentation de la pile d'appels qui peuvent affecter la façon dont un exploit de débordement de tampon de pile fonctionnera. Certaines architectures de machines stockent l'adresse de retour de niveau supérieur de la pile d'appels dans un registre. Cela signifie que toute adresse de retour écrasée ne sera pas utilisée avant un déroulement ultérieur de la pile d'appels. Un autre exemple de détail spécifique à une machine qui peut affecter le choix des techniques d'exploitation est le fait que la plupart des architectures de machine de style RISC ne permettent pas un accès non aligné à la mémoire. Combinée à une longueur fixe pour les opcodes de la machine, cette limitation de la machine peut rendre le saut à la technique ESP presque impossible à implémenter (à la seule exception où le programme contient en fait le code improbable pour sauter explicitement au registre de la pile).

Des piles qui grandissent

Dans le cadre des débordements de tampon de pile, une architecture souvent discutée mais rarement vue est une architecture dans laquelle la pile croît dans la direction opposée. Ce changement d'architecture est fréquemment suggéré comme solution au problème de débordement de la mémoire tampon de la pile car tout débordement d'une mémoire tampon de la pile qui se produit dans le même cadre de pile ne peut pas écraser le pointeur de retour. Une enquête plus approfondie sur cette protection revendiquée révèle qu'il s'agit au mieux d'une solution naïve. Tout débordement qui se produit dans un tampon à partir d'une trame de pile précédente écrasera toujours un pointeur de retour et permettra une exploitation malveillante du bogue. Par exemple, dans l'exemple ci-dessus, le pointeur de retour pour foone sera pas écrasé car le débordement se produit en fait dans le cadre de la pile pour memcpy. Cependant, étant donné que le tampon qui déborde pendant l'appel à memcpyréside dans une trame de pile précédente, le pointeur de retour pour memcpyaura une adresse mémoire numériquement plus élevée que le tampon. Cela signifie qu'au lieu du pointeur de retour pour fooêtre écrasé, le pointeur de retour pour memcpysera écrasé. Tout au plus, cela signifie que la croissance de la pile dans le sens opposé modifiera certains détails de la façon dont les débordements de tampon de pile sont exploitables, mais cela ne réduira pas de manière significative le nombre de bogues exploitables.

Régimes de protection

Au fil des ans, un certain nombre de schémas d' intégrité du flux de contrôle ont été développés pour empêcher l'exploitation malveillante du débordement de la mémoire tampon de la pile. Ceux-ci peuvent généralement être classés en trois catégories :

  • Détectez qu'un débordement de la mémoire tampon de la pile s'est produit et empêchez ainsi la redirection du pointeur d'instruction vers du code malveillant.
  • Empêchez l'exécution de code malveillant depuis la pile sans détecter directement le débordement de la mémoire tampon de la pile.
  • Randomisez l'espace mémoire de telle sorte que la recherche de code exécutable devienne peu fiable.

Empiler les canaris

Les canaris de pile, nommés pour leur analogie avec un canari dans une mine de charbon , sont utilisés pour détecter un débordement de la mémoire tampon de la pile avant que l'exécution d'un code malveillant ne puisse se produire. Cette méthode fonctionne en plaçant un petit entier, dont la valeur est choisie aléatoirement au démarrage du programme, en mémoire juste avant le pointeur de retour de pile. La plupart des débordements de mémoire tampon écrasent la mémoire des adresses mémoire inférieures vers les adresses mémoire supérieures, donc pour écraser le pointeur de retour (et ainsi prendre le contrôle du processus), la valeur Canary doit également être écrasée. Cette valeur est vérifiée pour s'assurer qu'elle n'a pas changé avant qu'une routine n'utilise le pointeur de retour sur la pile. Cette technique peut augmenter considérablement la difficulté d'exploiter un débordement de tampon de pile car elle oblige l'attaquant à prendre le contrôle du pointeur d'instruction par des moyens non traditionnels tels que la corruption d'autres variables importantes sur la pile.

Pile non exécutable

Une autre approche pour empêcher l'exploitation du débordement de la mémoire tampon de la pile consiste à appliquer une politique de mémoire sur la région de la mémoire de la pile qui interdit l'exécution à partir de la pile ( W^X , "Write XOR Execute"). Cela signifie que pour exécuter le shellcode à partir de la pile, un attaquant doit soit trouver un moyen de désactiver la protection d'exécution de la mémoire, soit trouver un moyen de placer sa charge utile de shellcode dans une région non protégée de la mémoire. Cette méthode devient de plus en plus populaire maintenant que la prise en charge matérielle de l'indicateur de non-exécution est disponible dans la plupart des processeurs de bureau.

Bien que cette méthode fasse définitivement échouer l'approche canonique de l'exploitation du débordement de tampon de pile, elle n'est pas sans poser de problèmes. Tout d'abord, il est courant de trouver des moyens de stocker le shellcode dans des régions de mémoire non protégées comme le tas, et donc très peu de besoin de changement dans le mode d'exploitation.

Même s'il n'en était pas ainsi, il existe d'autres moyens. La plus accablante est la méthode dite de retour à la libc pour la création de shellcode. Dans cette attaque, la charge utile malveillante chargera la pile non pas avec le shellcode, mais avec une pile d'appels appropriée afin que l'exécution soit redirigée vers une chaîne d'appels de bibliothèque standard, généralement avec pour effet de désactiver les protections d'exécution de la mémoire et de permettre au shellcode de s'exécuter normalement. Cela fonctionne parce que l'exécution ne se dirige jamais vers la pile elle-même.

Une variante du retour à la libc est la programmation orientée retour (ROP), qui définit une série d'adresses de retour, dont chacune exécute une petite séquence d'instructions machine triées sur le volet dans le code de programme ou les bibliothèques système existantes, séquence qui se termine par un retour. Ces soi-disant gadgets accomplissent chacun une simple manipulation de registre ou une exécution similaire avant de revenir, et les enchaîner permet d'atteindre les objectifs de l'attaquant. Il est même possible d'utiliser une programmation orientée retour « sans retour » en exploitant des instructions ou des groupes d'instructions qui se comportent un peu comme une instruction retour.

Randomisation

Au lieu de séparer le code des données, une autre technique d'atténuation consiste à introduire une randomisation dans l'espace mémoire du programme en cours d'exécution. Étant donné que l'attaquant doit déterminer où réside le code exécutable pouvant être utilisé, une charge utile exécutable est fournie (avec une pile exécutable) ou une charge est construite en utilisant la réutilisation du code comme dans ret2libc ou la programmation orientée retour (ROP). La répartition aléatoire de la disposition de la mémoire empêchera, en tant que concept, l'attaquant de savoir où se trouve le code. Cependant, les implémentations ne vont généralement pas tout randomiser ; généralement, l'exécutable lui-même est chargé à une adresse fixe et donc même lorsque ASLR (address space layout randomization) est combiné avec une pile non exécutable, l'attaquant peut utiliser cette région fixe de la mémoire. Par conséquent, tous les programmes doivent être compilés avec PIE (exécutables indépendants de la position) de manière à ce que même cette région de la mémoire soit randomisée. L'entropie de la randomisation est différente d'une implémentation à l'autre et une entropie suffisamment faible peut en soi être un problème en termes de force brute sur l'espace mémoire qui est randomisé.

Exemples notables

  • Le ver Morris en 1988 s'est propagé en partie en exploitant un débordement de tampon de pile dans le serveur Finger Unix . [1]
  • Le ver Slammer s'est propagé en 2003 en exploitant un débordement de tampon de pile dans le serveur SQL de Microsoft . [2]
  • Le ver Blaster en 2003 s'est propagé en exploitant un débordement de tampon de pile dans le service Microsoft DCOM .
  • Le ver Witty s'est propagé en 2004 en exploitant un débordement de tampon de pile dans l' agent de bureau BlackICE d' Internet Security Systems . [3]
  • Il existe quelques exemples de Wii permettant l'exécution de code arbitraire sur un système non modifié. Le "Twilight hack" qui consiste à donner un long nom au cheval du personnage principal dans The Legend of Zelda: Twilight Princess , et "Smash Stack" pour Super Smash Bros. Brawl qui consiste à utiliser une carte SD pour charger un fichier spécialement préparé dans le éditeur de niveau en jeu. Bien que les deux puissent être utilisés pour exécuter n'importe quel code arbitraire, ce dernier est souvent utilisé pour simplement recharger Brawl lui-même avec les modifications appliquées.

Voir également

Les références