Title: How to create a shellcode on Linux x86 ? Author: Jonathan Salwan Web: http://www.shell-storm.org/ | http://twitter.com/shell_storm Date: 2010-05-30 Language: French Original version: http://howto.shell-storm.org/files/howto-1.php I - Présentation des shellcodes =============================== Un shellcode est une chaîne de caractères qui représente un code binaire exécutable capable de lancer n'importe quelle application sur la machine. La plupart du temps un shellcode ouvre un shell pour avoir un accès complet sur la machine. Généralement, les shellcodes sont injectés dans la mémoire de la machine via l'exploitation d'une faille type dépassement de tampon (buffer overflow). II - Comprendre les bases ========================= 1 - Les appels systèmes Un appel système (en anglais, system call, abrégé en syscall) est une fonction fournie par le noyau d'un système d'exploitation. Suivant les Os nos syscall seront appelés différemment. Exemple d'appel système fréquemment utilisé: open, write, read, close, chmod, chown ... Sur la majorité des systèmes d'exploitations, les appels système peuvent être utilisés comme de simples fonctions écrites en C. Par exemple pour l'appel système chown: extern int chown (__const char *__file, __uid_t __owner, __gid_t __group) Chaque appel système à une adresse, qui est attribuée par le système d'exploitation et qui lui est propre. Par exemple sous linux avec le kernel 2.6.31 l'adresse du syscall chown est 0xb6. Comment connaître cette adresse ? Une simple commande comme ci-dessous permet d'avoir l'adresse du syscall. Les adresses dans unistd_x.h sont en decimales. Pour un système 32 bits jonathan@archlinux [ shellcode ]# cat /usr/include/asm/unistd_32.h | grep chown #define __NR_lchown 16 #define __NR_fchown 95 #define __NR_chown 182 #define __NR_lchown32 198 #define __NR_fchown32 207 #define __NR_chown32 212 #define __NR_fchownat 298 Pour un système 64 bits jonathan@archlinux [ shellcode ]# cat /usr/include/asm/unistd_64.h | grep chown #define __NR_chown 92 __SYSCALL(__NR_chown, sys_chown) #define __NR_fchown 93 __SYSCALL(__NR_fchown, sys_fchown) #define __NR_lchown 94 __SYSCALL(__NR_lchown, sys_lchown) #define __NR_fchownat 260 __SYSCALL(__NR_fchownat, sys_fchownat) Comme vous pouvez le constater, si l'os est sous 32 ou 64 bits, l'adresse des syscalls change. III - Ecrire son premier shellcode ================================== En premier lieu nous allons créer un shellcode simple, qui va nous permettre d'effectuer une pause. Pour ça nous allons appeler la fonction _pause dont l'adresse est 29 ce qui donne 0x1d en hexadecimal (sous 32 bits). jonathan@archlinux [ ~ ]$ cat /usr/include/asm/unistd_32.h | grep pause #define __NR_pause 29 Une fois qu'on connait l'adresse du syscall, il nous reste plus qu'à connaître, ce qu'on doit mettre dans les registres. Pour cela référez vous à cette page => http://www.shell-storm.org/shellcode/files/syscalls.html Nous pouvons constater que pour _pause nous n'avons pas besoin de remplir les registres, juste un appel suffit, ce qui va donc est très court à programmer. jonathan@archlinux [ shellcode ]$ cat pause.s xor %eax,%eax mov $29,%al int $0x80 jonathan@archlinux [ shellcode ]$ as -o pause.o pause.s jonathan@archlinux [ shellcode ]$ ld -o pause pause.o ld: warning: cannot find entry symbol _start; defaulting to 08048054 jonathan@archlinux [ shellcode ]$ ./pause ^C jonathan@archlinux [ shellcode ]$ Expliquation ============ xor %eax,%eax <= On met le registre eax à 0 pour éviter les segments faults mov $29,%al <= On place 29 (l'adresse du syscall) dans le registre al int $0x80 <= On exécute Maintenant nous allons l'écrire C. Pour cela nous devons connaître l'équivalence des fonctions asm en hexadecimales ce qui va par la suite être notre shellcode. Comment avoir les équivalences en hexadecimal ? C'est simple, nous utilisons tout simplement l'outil objdump, ce qui donne: jonathan@archlinux [ shellcode ]$ objdump -d ./pause pause: file format elf32-i386 Disassembly of section .text: 08048054 <.text>: 8048054: 31 c0 xor %eax,%eax 8048056: b0 1d mov $0x1d,%al 8048058: cd 80 int $0x80 jonathan@archlinux [ shellcode ]$ Et voilà, donc en C le code sera: jonathan@archlinux [ shellcode ]$ cat pause_c.c #include void main(void) { char shellcode[] = "\x31\xc0\xb0\x1d\xcd\x80"; (*(void(*)()) shellcode)(); } jonathan@archlinux [ shellcode ]$ gcc -o pause_c pause_c.c jonathan@archlinux [ shellcode ]$ ./pause_c ^C jonathan@archlinux [ shellcode ]$ Votre premier shellcode fonctionne correctement. Maintenant nous allons étudier la fonction _write. Référons nous encore au site que j'ai soumis plus haut. Info registre: ============== %eax = 4 %ebx = unsigned int %ecx = const char * %edx = size Nous allons tous simplement écrire jonathan, regardons ce que donne les sources: jonathan@ArchLinux [shellcode]$ cat write.s ;_write xor %eax,%eax <= Pour éviter les segmentfaults xor %ebx,%ebx <= // // xor %ecx,%ecx <= // // xor %edx,%edx <= // // movb $0x9,%dl <= on place la taille de notre mot dans dl(edx) donc jonathan + \n | 8+1=9 pushl $0x0a <= on commence à empiler notre line feed (\n) = 0x0a push $0x6e616874 <= naht push $0x616e6f6a <= onaj movl %esp,%ecx <= on envoie %esp dans %ecx le registre qui contient la constante char de _write movb $0x1,%bl <= ici 1 pour %ebx, movb $0x4,%al <= et ici le syscall de _write donc 4 int $0x80 <= on exécute ;_exit xor %ebx,%ebx <= %ebx = 0 movb $0x1,%al <= %eax = 1 (syscall de _exit) int $0x80 <= on exécute Compilons et exécutons notre programme: jonathan@ArchLinux [shellcode]$ as -o write.o write.s jonathan@ArchLinux [shellcode]$ ld -o write write.o ld: warning: cannot find entry symbol _start; defaulting to 08048054 jonathan@ArchLinux [shellcode]$ ./write jonathan jonathan@ArchLinux [shellcode]$ Ecrivons notre shellcode en C pour cela, un petit objdump sera utile. jonathan@ArchLinux [shellcode]$ objdump -d write write: file format elf32-i386 Disassembly of section .text: 08048054 <.text>: 8048054: 31 c0 xor %eax,%eax 8048056: 31 db xor %ebx,%ebx 8048058: 31 c9 xor %ecx,%ecx 804805a: 31 d2 xor %edx,%edx 804805c: b2 09 mov $0x9,%dl 804805e: 6a 0a push $0xa 8048060: 68 74 68 61 6e push $0x6e616874 8048065: 68 6a 6f 6e 61 push $0x616e6f6a 804806a: 89 e1 mov %esp,%ecx 804806c: b3 01 mov $0x1,%bl 804806e: b0 04 mov $0x4,%al 8048070: cd 80 int $0x80 8048072: 31 db xor %ebx,%ebx 8048074: b0 01 mov $0x1,%al 8048076: cd 80 int $0x80 jonathan@ArchLinux [shellcode]$ On retrouve bien à droite les sources de notre code en asm puis l'équivalence des instructions en hexadecimal. jonathan@ArchLinux [shellcode]$ cat write_c.c #include void main(void) { char shellcode[] = "\x31\xc0\x31\xdb\x31\xc9" "\x31\xd2\xb2\x09\x6a\x0a" "\x68\x74\x68\x61\x6e\x68" "\x6a\x6f\x6e\x61\x89\xe1" "\xb3\x01\xb0\x04\xcd\x80" "\x31\xdb\xb0\x01\xcd\x80"; fprintf(stdout,"Lenght: %d\n",strlen(shellcode)); (*(void(*)()) shellcode)(); } Compilons et exécutons notre shellcode. jonathan@ArchLinux [shellcode]$ gcc -o write_c write_c.c jonathan@ArchLinux [shellcode]$ ./write_c Lenght: 36 jonathan jonathan@ArchLinux [shellcode]$ Et voila cela fonctionne parfaitement. Shellcode _write(1,"jonathan\n",9) + _exit(0) pour une taille de 36 bytes. IV - Références =============== [x] - http://www.shell-storm.org [1] - http://fr.wikipedia.org/wiki/Shellcode [2] - /usr/include/asm/unistd_32.h