Title: How to create a shellcode on Linux x86 ?
Author: Jonathan Salwan <submit ! shell-storm.org>
Web: http://www.shell-storm.org/ | http://twitter.com/jonathansalwan
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<stdio.h>
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 <stdio.h>
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