_________________ _____ _ ______ ____ __ __ _ _ _________________ _.;'_______ ______ _____ __ __ _ __ _ __ _ _ [' [#.19] _|_| Buffers overflow : be right back \_________/_/ So CalleD _ ______ __ _ _ le 6-eme passager _/_/ [By:_rix] \ ___________________ __________________ _________ ______/_/_ __ _ __ _ _ _,] =========================== Apres l'excellente explication de __2 a propos des stacks smashings, il serait interessant de continuer et peut-etre d'essayer de voir un peu plus pratiquement comment on procede. Tout d'abord un petit rappel: Nous savons donc que la technique consiste a ecraser l'adresse de retour de la procedure, et cela en remplissant un buffer de donnees du programme qui a ete cree sur la pile. Tout ca est bien beau, mais sa pas l'air trop facile a programmer et a trouver :) TROUVER DES BUFFERS OVERFLOWS SUR LA PILE: ========================================== Le moyen le plus evident d'acceder a un buffer pour aller y ecrire est bien evidemment via les commandes de saisies habituelles d'un programme. Or, il se fait qu'en C, les librairies standards d'entree/sortie n'effectuent justemment aucune verification sur la taille des donnees ajoutees dans le buffer. [no bound-checking] Ca veut dire quoi ? Rappelez vous un petit peu les dernieres lecons du tutorial C de Wyzeman ;)) Vous vous souvenez que pour demander une valeur, on utilisait la petite fonction scanf(). Par exemple: char buffer[10]; scanf("%s",buffer); Reflechissons un ti peu. On voit donc que le buffer a une taille de 10. Donc ca veut dire que si on tape 11 caracteres...hehe vous devinez la suite ;) Une autre fonction fort utilisee pour la saisie en C est la fonction gets(). C'est en fait la meme fonction que scanf("%s",buffer), mais elle s'appelle comme ceci: gets(buffer); Ensuite, il y a toute une serie de fonctions qui permettent de traiter des chaine des caracteres: strcat(buffer1,buffer2) : cette fonction ajoute la chaine du buffer2 dans le buffer1. Donc si buffer1 est trop petit... :) strcpy(buffer1,buffer2) : cette fonction copie la chaine du buffer2 sur la chaine du buffer1.si le buffer est trop petit.;) Puis, il y a aussi les programmeurs qui utilisent des fonctions de lecture caractere par caractere, mais qui se disent: "ohhh, mes mot de passe ne seront jamais plus grand que 8, la personne n'a pas besoin de se planter, elle n'a qu'a taper un mot de passe plus petit que 8 !" lol :))) Voici quelques fonctions de lecture des caracteres: getc,getchar,... D'autres methodes, encore. Les commandes de gestion de fichier texte. Beaucoup de programmes Unix permettent de prendre des informations dans des fichiers au format texte. Il vous suffit d'essayer dans ces cas-la, de faire un fichier texte avec une ligne de 1000 caracteres, et vous verrez tres vite si le programme une fonction critique de lecture ;) Etant donner que toutes ces fonctions sont donc generalement utilisees avec des variables locales (sur la pile), il y a toute une serie de pistes possibles pour trouver des buffers overflows. Pour vous donner une idee, n'oubliez pas non plus que pas mal de programmes Unix sont fournis avec le code source, d'ou meme pas besoin de trop reflechir, suffit d'analyser le code source ;)))) Un ti exemple: Il y a e peu pres 2 ans je pense, un buffer overflow a ete trouve dans la commande SERVER du protocole IRC des serveurs IRC Undernet, qui n'acceptait pas plus qu'un certain nombres de caracteres. Chouette hein ! hehe ;) Ca voulait dire qu'en gros, vous pouviez planter tout le serveur Undernet, ou executer des commandes sur la machine meme du serveur. Hehe ! Je n'ai pas eu cette chance ouin, je l'ai apprit trop tard lol ;))))) Si vous voulez essayer d'en trouver, par exemple, le mieux consiste a charger le programme sur votre ordi, bien tranquillement, et de tester cela chez vous bien au chaud sans embeter personne ;) Ensuite, vous essayer toutes les saisies possibles, avec de longues chaines de caracteres. Sous Windows, suffit de foutre une bonne fois pour toute une longue chaine (1000 caracteres...) dans votre clipboard et de la coller a chaque fois, ca peut aller tres vite ;) Si le programme plante, avec une belle erreur, du style "erreur de page" ou un truc comme ca, c'est que vous venez probablement de trouver une petite mine d'or :)))) D'ou ca vient ? Ben vous avez tapez n'importe quoi dans votre buffer, donc ca a ete ecrasé l'adresse de retour avec n'importe quoi, et sous les systemes du genre Windows/linux sur Intel, ce n'est pas souvent autorisé de sauter a n'importe quelle adresse :) Voici quelques programmes connus dans lesquel on a trouvé des buffers overflows dernierement: - Internet Explorer 4.0 (connaissez vous ? ;) - Outlook Express (mailer de Micromachin ;) - wu-ftpd (serveur FTP Linux) - sendmail (programme pour envoyer des mails sous Unix) - Windows NT (paths trop longs) - IIS 4.0 (serveur Web sous Windows NT) OU EST CACHEE CETTE FOUTUE STACK ? ================================== Ben oui c'est bien pratique tout ca, mais le probleme est qu'on a souvent besoin d'avoir une idee a peu pres de l'adresse de retour que l'on va devoir remettre. Et la ca a l'air de se compliquer ;((( Dans les systemes Intel, pour les systemes multitaches genre Windows, Linux,.. la memoire est geree de maniere virtuelle. Ca veut dire que chaque programme pense qu'il est le seul en memoire, et qu'il a toute la place qu'il veut. Ca veut dire aussi que c'est le systeme qui se charge de faire les conversions d'adresses necessaires, etc... Donc nous n'avons pas exactemment besoin de connaitre l'emplacement exact de notre programme en memoire, juste de savoir dans "son bloc de memoire a lui" ou la stack se trouve. C'est deja un peu plus facile :) 2emement, c'est le compilateur qui definit les adresses de toutes les donnees contenues dans le fichier EXE a l'execution. Ca veut dire qu'une fois qu'on sait ou la pile commence, elle commencera toujours a cette endroit la, meme 10 ans plus tard ;) Mais ou c'est encore plus interessant, c'est que comme tout les programmes se pensent seuls en memoire, ils peuvent utiliser chacun les memes adresses "numeriques" virtuelles. Et le processeur Intel se sert justemment de cette particuliarité pour pouvoir switcher les taches plus facilemment. Ca veut dire que la pile d'un programme Windows/Linux se trouve toujours a la meme adresse virtuelle. Comment trouver cette adresse ? Il y a plusieurs moyens. Je vais parler ici des moyens sous Windows, mais ces moyens doivent aussi exister sous Linux, grace a des utilitaires comme GDB. Le 1er moyen est de sortir un bon gros debugger sous Windows, par exemple W32DASM (si vous ne connaissez pas, prenez le, c'est le meilleur ;) Vous lancer le debugging du programme dont vous voulez trouver la pile, puis vous allez tout simplement voir le contenu du registre ESP (registre de pile), tout au debut ,des que le programme est chargé. Le 2eme moyen est de creer un petit programme sous Windows, qui affiche au demarrage le contenu du registre ESP. Cela est assez facilemment programmable avec les fonctions d'inclusions d'operandes assembleur dans divers compilateurs C/C++: unsigned long esp(void) { asm mov eax,esp } void main() { printf("0x%x\n",esp()); } LE BUFFER ========= Maintenant que nous savons avec exactitude ou se trouve notre stack, il faudrait savoir ou dans la pile se trouve notre buffer. Le, il n'y a pas de solution radicale et d'adresse exacte: il faut chercher. C'est souvent ca le plus difficile a faire pour exploiter un buffer overflow. Mais il y a plusieurs petites idees qui peuvent nous aider a ne pas tapez completement au hasard. De nouveau, notre cher debugger peut nous aider, en faisant du pas a pas, a voir la maniere dont le registre ESP evolue au cours d'une execution pas a pas par exemple. Mais on peut aussi faire un semblant de calcul. Le nombre total de valeurs a tester ne sera de toute maniere jamais astronomique. Le nombre de procedures appelees les une dans les autres, n'excede pas souvent 50 appels. Ca fait 50*4=200 bytes. La dedans, il y a toutes les variables locales, le plus souvent des valeurs numeriques, donc 4 bytes a peu pres pour chacune. En comptant 50 variables locales dans chaque procedure (ce qui est deje enorme), on arrive a 200 bytes par procedure de nouveau. On peut eventuellement rajouter quelques buffers de caracteres dans les procedures, et on peut arriver a un total de 500 bytes par procedure. Etant donné que notre buffer overflow ne se trouve pas souvent dans la 50eme procedure appelee, supposons dans la 25eme par exemple: 25*4+500*25=12600 bytes. Or tout les bytes sur la pile sont alignes sur des valeurs de 4 bytes, donc ca nous fait 12600/4=3150 valeurs. Ca parait beaucoup, mais je vous rappelle qu'on a ici supposé un programme avec 25 procedures emboitees les unes dans les autres, et avec 50 variables locales dans chacune, ce qui est deje un programme assez enorme hehe ;)) En pratique, certains petits programmes utilisent une pile avec maximum 50 valeurs, ce qui est minime. Le probleme est que notre adresse de retour doit se trouver exactemment au bon endroit. Si le processeur saute un byte trop haut ou un byte trop bas, il plantera completement. Nous allons donc essayer d'adopter une "structure" pour notre buffer overflow, qui devrait maximiser nos chances de reussite. Tout d'abord, nous allons essayer d'utiliser l'instruction NOP. L'instruction NOP des processeurs Intel est une instruction qui n'execute rien :) A quoi cela peut-il nous servir ? Tout simplement, a eviter de connaitre l'adresse exact du debut de notre buffer. Si nous placons une serie d'instructions NOP au debut de celui-ci, nous pourrons assez facilement deviner ou se trouve plus ou moins le debut. Si le processeur ne tombe pas exactemment sur le debut, il executera des NOP jusqu'au debut des autres instructions utiles, et ne plantera donc pas. Ensuite, nous ne savons pas non plus exactemment a quel endroit se trouve notre valeur de retour sur la pile. Mais nous savons qu'une adresse de retour est de 4 bytes, et qu'elle doit se trouver apres la fin de notre buffer. Nous devons donc essayer d'aligner nos adresses de retour sur une valeur multiple de 4. Nous allons aussi ecrire plusieurs fois cette adresse de retour a la fin de notre chaene, pour avoir plus de chance que le processeur tombe dessus lors du retour de procedure. Nous allons donc ecrire notre buffer overflow de la maniere suivante: NNNNNNNNNNNNNNNNNNNNNNNNNNNccccccccccccccccccccccccaaaRRRRRRRRRRRRRRRRRRRRR Ou N represente une instruction NOP (codee 90h en assembleur). c represente le code reel de notre exploit. a represente des bytes d'alignement (de 0 a 3 suivant le besoin). R represente l'adresse approximative du debut de notre buffer. (approximative puisque il y a nos petits NOP devant ;)))) Pour calculer ce "R" (adresse approximative du buffer), nous devons partir de l'adresse contenur dans ESP, et y ajouter une certaine valeur, suivant le nombre de variables locales et de procedures appelees. Donc, pour realiser un buffer overflow, nous devons donc essayer de nous creer un petit outil dans lequel on entre les parametres suivants: -facteur d'alignement (0 a 3): va modifier le nombre de "a" apres notre code. -adresse approximative du debut du buffer: a placer dans les "R", et egale a la valeur du ESP de depart, duquel on a soustrait un certain nombres de byte (car la pile descend ;) -taille supposee du buffer: de cette maniere notre outil saura combien de NOP inserer au debut du buffer, en soustrayant la longueur de notre code "c" de la longueur totale supposee du buffer. Il est aussi interessant de noter que pour des raisons basees sur l'hexadecimal, beaucoup de programmeurs creent des buffers de tailles multiple de 2 et meme plus generalement multiples de 16. Cet outil est bien evidemment dependant du programme que l'on cherche a smasher. En effet, dans le cas d'un serveur FTP par exemple, cet outil doit d'abord ouvrir la connection, puis faire le login, etc et enfin entrer la commande qui provoque le buffer overflow, avec notre chaine de buffer overflow dedans, adaptee selon les parametres. Maintenant, il nous reste le principal: Creer le code du programme que l'on veut foutre dans le buffer. Pour cela, c'est bien evidemment un programme en assembleur que vous allez devoir ecrire. Une fois ce petit programme bien teste de maniere independante du buffer overflow, il faut le dumper, c'est a dire le coder sous forme de valeur hexadecimal (WIN32DASM fournit les dumps d'un programme executables), car ce sont ces valeurs hexadecimals que nous allons devoir entrer dans le buffer. Il faut aussi remarquer que notre code va etre executé, mais lors de sa terminaison, il va vouloir aller rechercher la valeur de retour sur la pile ou il va tout simplement continuer a executer ce qui se trouve en memoire, ce qui dans les 2 cas provoquera un plantage tres rapide du systeme ;) Donc, il est preferable de creer un petit code qui boucle indefiniment, ou qui n'autorise pas une sortie, de maniere a eviter ce probleme. Voila, cela devient presque un jeu d'enfant de tester les buffers overflows lol ;) Une fois les parametres exacts trouves, sauver les 10 fois hehe ;) En effet, ces valeurs dans un programme normalement constitué ne varieront pas, du moins si vous executer toujours la meme sequence initialise de login etc dans votre outil. LES ADRESSES LOCALES ==================== Comment faire pour utiliser des variables dans notre propre code ? Par exemple, nous voulons que notre code lance un programme dont nous placons le nom dans une chaine de caracteres. Il nous faut donc l'offset exacte de la chaine de caractere dans notre code. Pour cela, on va utiliser quelques instructions en assembleur, qui seront bien utiles. L'instruction CALL, qui appelle une procedure, possede une propriete interessante. Elle laisse en effet l'adresse de l'instruction suivant l'instruction appelante sur la pile. Normalement cette adresse est utilisee par le RET, pour revenir a la procedure appelante. Mais le processeur ne sait pas si ce qui suit le CALL est une instruction ou bien des donnees diverses. De plus, l'instruction JMP permet de specifier des deplacements relatifs, c'est a dire de ne pas donner l'adresser reel du saut, mais bien de dire que le saut se trouve a par exemple 10 bytes de l'adresse courante. Nous allons donc proceder de la maniere suivante: Nous allons donc organiser notre code ("ccccc" dans le schema precedent) de la maniere suivante: JPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPCCssssssssssssssssssssss Ou J represente un saut relatif (JUMP SHORT) vers C P represente les instructions de notre code proprement-dit C represente une instruction CALL suivie de l'adresse du debut de notre code reel (=adresse mise dans "R" sur la pile tantot+2, a cause du JMP SHORT du debut de notre programme, qui est code sur 2 octets). s represente les caracteres de notre chaine dont on veut connaitre l'adresse exacte. Que va-t-il se passer ? JPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPCCssssssssssssssssssssss || || ||_______________(1 ->)_________________|| | | |_______________(2 <-)__________________| Le processeur va executer notre JMP SHORT jusqu'au CALL. Le CALL va placer l'adresse de ce qui le suit (justemment l'adresse de notre chaine ;) sur la pile, puis va sauter a l'instruction qui suit notre JMP SHORT au debut de notre code. Pour recuperer l'adresse de notre chaine, on a plus qu'a faire un petit POP EAX par exemple, et l'adresse de notre chaine se trouve dans EAX :))) PETITES PROBLEMES ================= Il y a certains petits problemes qui peuvent encore apparaitre lors de la realisation du buffer overflow. Le 1er probleme est le cas du foutu caractere 00h ;) Si dans notre code de programme a placer dans le buffer, nous avons un caractere 00h, cela peut poser probleme. En effet, en C, le caractere 00h indique une fin de chaine, et donc la suite de notre code pourrait ne sera pas etre chargee dans certains cas. Il n'est pas complique d'eviter ces caracteres, il suffit de chipoter avec les commandes assembleur utilisees dans le code. Par exemple, a la place de faire: mov eax,0 on peut faire: sub eax,eax ou encore xor eax,eax etc... ;) Le 2eme probleme est l'utilisation de la pile. En effet, si nous voulons utiliser la pile pour mettre des parametres, nous risquons d'aller ecraser notre code lui-meme !!! Or, toutes les fonctions APIs de Windows par exemple, necessite le passage des parametres sur la pile ! On risque donc de tout casser ;) Pour eviter cela, il suffit tout simplement au debut de notre code de modifier la pile pour qu'elle commence en dessous du code de notre programme. Pour cela, il suffit de placer une instruction SUB ESP,T Ou T represente une valeur qui est plus grande que la taille de notre buffer (taille fournie tantot comme parametre dans notre petit "outil"). Comme cela, la pile continuera en dessous de notre petit programme, sans aucun probleme. Voile, j'espere que vous voyez maintenant un peu mieux comment programmer pratiquement des buffers overflows... Maintenant il ne vous restent plus qu'a les trouver hehe ;) Pour un prochain article, on va ptet envisager avec __2 de vous proposer un code de buffer overflow complet, on verra ;) Ciaaaaaaaooooooooooooooo Rix-Agressor-Shogun