Les shellcodes x86 sur Windows seven :

Un shellcode est un code arbitraire injecté dans la mémoire comme par exemple dans le cas d’un buffer overflow permettant de faire ce que l’on veut sur le pc de la victime, on les appelle shellcode car ce type de code est souvent utilisé pour obtenir un shell ou une invite de commande pour prendre le contrôle total du pc distant. Les shellcodes sont constitués d’une suite d’instructions assembleur et peuvent donc faire tout ce que l’on souhaite.

L’écriture de shellcodes Windows nécessite de connaître les API Windows, de savoir dans quelle dll une fonction que l’on souhaite utiliser se trouve, de charger cette dll puis d’appeler directement l’adresse de la fonction.

Il faut savoir qu’un shellcode ne doit pas contenir de null byte ou octet nul en français, c’est à dire le caractère de terminaison d’une string en C, car dans un exploit, le shellcode est placé dans un tableau de char afin d’être comme je le disais : injecté, donc si un null byte se trouve dans cette chaîne, votre shellcode ne sera pas entièrement copié dans le buffer et ne pourra donc fonctionner normalement.

 

Shellcode statique :

Dans un premier temps on va créer un shellcode statique c’est à dire qui ne fonctionnera plus lorsque vous aurez rebooté votre seven et ceci à cause de l’aslr (Address Space Layout Randomization) introduit sur les OS de microsoft depuis la sortie de vista. Notre shellcode sera tout simple, il affichera une MessageBox avec un titre et un message, puis quittera.

On a donc besoin de l’adresse effective de LoadLibraryA qui se trouve dans kernel32.dll pour charger user32.dll, de l’adresse effective de MessageBoxA contenue dans cette dernière et de ExitProcess qui se trouve également dans kernel32.dll, qui elle est toujours chargée.

Voici les prototypes de nos fonctions :

1
2
3
HMODULE WINAPI LoadLibraryA(
__in LPCTSTR lpFileName
);


 

1
2
3
4
5
6
int WINAPI MessageBoxA(
__in_opt HWND hWnd,
__in_opt LPCTSTR lpText,
__in_opt LPCTSTR lpCaption,
__in UINT uType
);


 

1
2
3
VOID WINAPI ExitProcess(
__in UINT uExitCode
);


 

Pour trouver ces adresses je vais utiliser mon tool : ListExportedFunctions. Mais vous pouvez utiliser les /fonctions LoadLibrary() et GetProcAddress() pour les obtenir.

On obtient l’adresse 0x76F74BC6 pour LoadLibraryA, 0x7782FEAE pour MessageBoxA et 0x76F7734E pour ExitProcess.

En ce qui concerne les strings dont nous avons besoin pour nos fonctions, sachant que dans un shellcode on ne peut utiliser la section réservée aux données, celle nommée « .data », nous utiliserons une technique se servant des instructions jmp, call et pop, pour obtenir un pointeur vers chacune de nos strings.

Voici un exemple de cette technique :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.386
.model flat, stdcall

option casemap:none

.code

start:

jmp getString

getStringReturn:
pop ecx

getString:
call getStringReturn
db "hello world",0

end start


 

Le registre ecx contient maintenant un pointeur vers notre string « hello world ». Mais il y a un problème : cette string crée un null byte et comme dit plus haut, un shellcode ne doit pas en contenir.
Pour y remédier, nous allons utiliser un autre registre, le mettre à 0 avec l’instruction xor, ajouter un caractère à la fin de notre string, puis écraser ce caractère par le byte de poids faible de ce dernier registre. Avec cette technique, notre string sera bien terminée par un zéro mais sans null byte.

Exemple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.386
.model flat, stdcall

option casemap:none

.code

start:

xor ebx,ebx ; On met ebx à zéro.
jmp getString

getStringReturn:
pop ecx
mov [ecx+11],bl ; On écrase le '#' par bl, donc zéro.

getString:
call getStringReturn
db "hello world#"

end start


 

Voici donc maintenant notre shellcode au complet :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
.386
.model flat, stdcall

option casemap:none

.code

start:

xor ebx,ebx ; Le registre ebx n'est jamais modifié.

; LoadLibraryA
jmp getUser32LibString

getUser32LibStringReturn:
pop ecx
mov [ecx+10],bl
mov eax,76F74BC6h
push ecx
call eax

; MessageBoxA
jmp getCaptionString

getCaptionStringReturn:
pop ecx
mov [ecx+9],bl

jmp getTextString

getTextStringReturn:
pop edx
mov [edx+11],bl
mov eax,7782FEAEh

push ebx
push ecx
push edx
push ebx
call eax

; ExitProcess
mov eax,76F7734Eh
push ebx
call eax

getTextString:
call getTextStringReturn
db "hello world#"

getCaptionString:
call getCaptionStringReturn
db "Shellcode#"

getUser32LibString:
call getUser32LibStringReturn
db "user32.dll#"

end start


 

Si vous essayez d’exécuter ce shellcode vous n’y arriverez pas, car il faut passer par un programme intermédiaire écrit en C que j’appellerai « shellcodetest.c », qui, chez moi est compilé avec gcc (mingw). Si vous souhaitez le compiler avec Visual Studio, il vous faudra utiliser la fonction VirtualProtect() sur la variable code.

1
2
3
4
5
6
7
8
char code[] = "Placez votre shellcode ici";

int main(int argc, char **argv)
{
int (*func)();
func = (int (*)()) code;
(int)(*func)();
}


 

Pour obtenir les bytes du shellcode à placer dans le buffer, vous pouvez utiliser l’outil GetShellcode. Après compilation et exécution de shellcodetest nous obtenons :

Seulement voilà, après un reboot, ce shellcode ne fonctionnera plus car l’adresse des fonctions écrites en dur ici auront changées, il nous faut donc une autre approche. Cette nouvelle approche est souvent appelée « shellcode générique » et c’est ce que nous allons voir maintenant.

 

Shellcode générique :

Au lieu d’utiliser un programme externe qui nous donne les adresses des fonctions que nous souhaitons utiliser, et de se contenter d’un copié/colleé de l’adresse dans le code assembleur, il nous faut les trouver directement dans le shellcode. Cette approche nécessite d’abord de connaître l’adresse de base de kernel32.dll.

Il existe plusieurs techniques pour trouver cette adresse dont une qui utilise la structure du PEB. On trouve le pointeur sur ce PEB à fs:[0x30].
Une fois dans cette structure à 0x0c, on tombe sur une autre structure nommée PEB_LDR_DATA. Dans cette dernière, à 0x1c, il y a une liste chaînée qui contient les modules chargés en mémoire.

Cette liste chaînée porte le nom de InitializationOrderModule. Sur windows XP & vista, kernel32.dll est toujours chargée en deuxième position, mais sur seven elle l’est en troisième. Une façon de rendre portable cette technique sur tous les OS de microsoft est de regarder à la fin de chaque nom de module que l’on parcourt (nom que l’on trouve à l’offset 0x20 sur chaque élément de InitializationOrderModule). Le nom de la dll/module doit faire 12 caractères (n’oublions pas que nous sommes en unicode).

Puis enfin à 0x08 sur chaque élément, on y trouve l’adresse de base du module.

Voici le code assembleur qui nous permet de trouver l’adresse de kernel32.dll :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.386
.model flat, stdcall

option casemap:none
assume fs:nothing

.code

start:

xor ecx, ecx ; ecx = 0
mov esi, fs:[ecx + 30h] ; esi = &(PEB) (FS:[0x30])
mov esi, [esi + 0ch] ; esi = PEB->Ldr
mov esi, [esi + 1ch] ; esi = PEB->Ldr.InInitOrder
next_module:
mov eax, [esi + 08h] ; eax = InInitOrder[X].base_address
mov edi, [esi + 20h] ; edi = InInitOrder[X].module_name (unicode)
mov esi, [esi] ; esi = InInitOrder[X].flink (module suivant)
cmp [edi + 12 * 2], cl ; module_name[12] == 0 ?
jne next_module ; Non : essayons le module suivant.

end start


 

Avec ce code, eax contient l’adresse de base de kernel32.dll. Il ne nous reste donc plus qu’à obtenir l’adresse de nos fonctions. On va refaire le même shellcode que dans l’exemple précédent, c’est à dire celui qui était statique.

On y va en deux fois :
– On trouve les adresses de LoadLibraryA et de ExitProcess qui se trouvent dans kernel32.dll
– On trouve l’adresse de MessageBoxA qui se trouve dans user32.dll.

Pour trouver l’adresse de nos fonctions, nous allons utiliser le PE format et parser les dll afin d’obtenir les adresses dans la table d’exportation. Pour en savoir plus sur le format PE, vous pouvez lire l’article Le format PE. Ce tutoriel ne suffit pas pour la suite car pour un shellcode on ne peut pas lire toutes les en-têtes, utiliser fread, fseek etc… Il nous faut donc connaître à l’avance les offsets de tout ce que l’on a besoin :

1
2
3
4
5
6
7
8
9
10
11
PIMAGE_DOS_HEADER->e_lfanew = 0x3c

PIMAGE_NT_HEADERS->OptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]->VirtualAddres = 0x78

PIMAGE_EXPORT_DIRECTORY->NumberOfNames = 0x18

PIMAGE_EXPORT_DIRECTORY->AddressOfNames = 0x20

PIMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals = 0x24

PIMAGE_EXPORT_DIRECTORY->AddressOfFunctions = 0x1c


 

Plutôt que de comparer dans le shellcode chaque nom de fonctions listé depuis la table d’exportation d’une dll avec un nom que l’on recherche, nous allons utiliser un système de hash.

C’est à dire, au départ créer un hash pour chaque nom de fonction que l’on souhaite utiliser, puis dans le shellcode à chaque nom de fonction parcouru on générera ce hash pour le comparer avec celui que l’on recherche. Cette technique permet de diminuer la taille de notre shellcode.

Ici le code qui permet de générer le hash d’une fonction: GenerateHash.

Pour ce qui va suivre, il faut faudra un minimum de connaissances en assembleur. Nous savons donc comment trouver l’adresse de kernel32.dll, qu’il faut parcourir la table d’exportation et comment la parcourir pour trouver les adresses de nos fonctions. Quant au nom de fonction, nous savons qu’il faut utiliser un hash pour la comparaison. On a donc tout ce qu’il nous faut pour rendre notre shellcode générique.

Voici notre shellcode qui affiche la MessageBox mais de façon générique :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
.386
.model flat, stdcall

option casemap:none
assume fs:nothing

.code

start:

jmp main

find_kernel32:
xor ecx, ecx ; ecx = 0
mov esi, fs:[ecx + 30h] ; esi = &(PEB) (FS:[0x30])
mov esi, [esi + 0ch] ; esi = PEB->Ldr
mov esi, [esi + 1ch] ; esi = PEB->Ldr.InInitOrder
next_module:
mov eax, [esi + 08h] ; eax = InInitOrder[X].base_address
mov edi, [esi + 20h] ; edi = InInitOrder[X].module_name (unicode).
mov esi, [esi] ; esi = InInitOrder[X].flink (module suivant).
cmp [edi + 12 * 2], cl ; module_name[12] == 0 ?
jne next_module ; Non : essayons le module suivant.
ret

find_func_address:
pushad
mov ebp, [esp + 024h] ; 24 = tous les registres push par le pushad (0x20) + l'adresse de base du module empilé avant l'appel de cette routine.
mov eax, [ebp + 03ch] ; PIMAGE_DOS_HEADER->e_lfanew
mov edx, [ebp + eax + 078h] ; RVA de PIMAGE_NT_HEADERS->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]->VirtualAddres
add edx, ebp ; On y ajoute l'adresse de base de la dll.
mov ecx, [edx + 018h] ; PIMAGE_EXPORT_DIRECTORY->NumberOfNames
mov ebx, [edx + 020h] ; RVA de PIMAGE_EXPORT_DIRECTORY->AddressOfNames
add ebx, ebp ; On y joute l'adresse de base de la dll.

find_func_address_loop:
jecxz find_func_address_finished
dec ecx ; Décrémente ecx.
mov esi, [ebx + ecx * 4] ; RVA d'un nom de fonction dans esi.
add esi, ebp ; On y ajoute l'adresse de base de la dll.

prepare_hash:
xor edi, edi ; edi = 0
xor eax, eax ; eax = 0
cld ; Clear direction flag: pour être sûr que ça incrémente (de gauche à droite) pendant l'utilisation de lodsb.

hash:
lodsb ; Charge un byte de esi (qui contient le nom d'une fonction) dans al et incrémente esi.
test al, al ; On regarde si le byte est à zéro.
jz hash_finished ; Si oui on a atteint la fin du nom de la fonction.
ror edi, 0dh ; Rotation de 13 bits vers la droite de la valeur courante (edi contient le hash).
add edi, eax ; Ajout du caractère au hash.
jmp hash ; On continue.

hash_finished:
compare_hash:
cmp edi, [esp + 028h] ; 28 = tous les registres push par le pushad (0x20) + l'adresse de base du module empilé avant l'appel de cette routine + le hash à trouver.
jnz find_func_address_loop ; Ce n'est pas le bon nom de fonction, on va au prochain.
mov ebx, [edx + 024h] ; RVA de PIMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals
add ebx, ebp ; On y ajoute l'adresse de base de la dll.
mov cx, [ebx + 2 * ecx] ; Ordinal de la fonction courante.
mov ebx, [edx + 01ch] ; RVA de PIMAGE_EXPORT_DIRECTORY->AddressOfFunctions
add ebx, ebp ; On y ajoute l'adresse de base de la dll.
mov eax, [ebx + 4 * ecx] ; RVA de l'adresse de la fonction.
add eax, ebp ; On y ajoute l'adresse de base de la dll = adresse effective.
mov [esp + 01ch], eax

find_func_address_finished:
popad ; Retrouve la valeur de tous les registres, eax contient l'adresse de la fonction grace au "mov [esp + 01ch], eax".
ret

main:
sub esp, 12 ; On alloue l'espace sur la stack pour contenir l'adresse de LoadLibraryA, ExitProcess et MessageBoxA.
mov ebp, esp ; ebp devient notre frame pointeur. Ex : call ebp+4 pour call LoadLibraryA.
; call ebp+8 pour call ExitProcess.
; call ebp+12 pour call MessageBoxA.

call find_kernel32
mov edx, eax ; On sauvegarde l'adresse de kernel32.dll dans edx.

; On cherche l'adresse de LoadLibraryA :
push 0ec0e4e8eh ; Le hash.
push edx ; L'adresse de base de la dll (kernel32.dll).
call find_func_address
mov [ebp+4], eax ; ebp = Adresse de LoadLibraryA.

; On cherche l'adresse de ExitProcess :
push 073e2d87eh ; Le hash.
push edx ; L'adresse de base de la dll (kernel32.dll).
call find_func_address
mov [ebp+8], eax ; ebp+4 = Adresse de ExitProcess.

; On get la string user32.dll :
xor ebx, ebx
jmp get_user32
get_user32_return:
pop eax
mov [eax+10], bl ; On termine la string sans null byte.

; On appel LoadLibraryA.
push eax ; la string user32.dll
call dword ptr [ebp+4]

mov edx, eax ; edx contient maintenant l'adresse de base de user32.dll.

; On cherche l'adresse de MessageBoxA :
push 0bc4da2a8h ; Le hash.
push edx ; L'adresse base de la dll (user32.dll).
call find_func_address
mov [ebp+12], eax ; ebp = adresse de MessageBoxA.

xor ebx,ebx ; Le registre ebx n'est jamais modifié (convention d'appel stdcall).

; On get la string pour le titre :
jmp get_caption
get_caption_return:
pop esi
mov [esi+9], bl

; On get la string pour le message :
jmp get_text
get_text_return:
pop edi
mov [edi+11], bl

; On call MessageBoxA :
push ebx
push esi
push edi
push ebx
call dword ptr [ebp + 12]

; On call ExitProcess :
push ebx
call dword ptr [ebp + 8]

get_user32:
call get_user32_return
db "user32.dll#"

get_caption:
call get_caption_return
db "Shellcode#"

get_text:
call get_text_return
db "hello world#"

end start


 

N’oubliez pas que pour l’essayer, il faut obtenir les bytes avec par exemple GetShellcode et utiliser shellcodetest.c

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

char code[] = "\xEB\x68\x33\xC9\x64\x8B\x71\x30\x8B\x76\x0C\x8B\x76\x1C\x8B\x46\x08\x8B\x7E\x20\x8B\x36\x38\x4F\x18\x75\xF3\xC3\x60\x8B\x6C\x24\x24\x8B\x45\x3C\x8B\x54\x28\x78\x03\xD5\x8B\x4A\x18\x8B\x5A\x20\x03\xDD\xE3\x34\x49\x8B\x34\x8B\x03\xF5\x33\xFF\x33\xC0\xFC\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x03\xF8\xEB\xF4\x3B\x7C\x24\x28\x75\xE1\x8B\x5A\x24\x03\xDD\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDD\x8B\x04\x8B\x03\xC5\x89\x44\x24\x1C\x61\xC3\x83\xEC\x0C\x8B\xEC\xE8\x8E\xFF\xFF\xFF\x8B\xD0\x68\x8E\x4E\x0E\xEC\x52\xE8\x9B\xFF\xFF\xFF\x89\x45\x04\x68\x7E\xD8\xE2\x73\x52\xE8\x8D\xFF\xFF\xFF\x89\x45\x08\x33\xDB\xEB\x31\x58\x88\x58\x0A\x50\xFF\x55\x04\x8B\xD0\x68\xA8\xA2\x4D\xBC\x52\xE8\x71\xFF\xFF\xFF\x89\x45\x0C\x33\xDB\xEB\x25\x5E\x88\x5E\x09\xEB\x2E\x5F\x88\x5F\x0B\x53\x56\x57\x53\xFF\x55\x0C\x53\xFF\x55\x08\xE8\xCA\xFF\xFF\xFF\x75\x73\x65\x72\x33\x32\x2E\x64\x6C\x6C\x23\xE8\xD6\xFF\xFF\xFF\x53\x68\x65\x6C\x6C\x63\x6F\x64\x65\x23\xE8\xCD\xFF\xFF\xFF\x68\x65\x6C\x6C\x6F\x20\x77\x6F\x72\x6C\x64\x23";

int main(int argc, char **argv)
{
int (*func)();
func = (int (*)()) code;
(int)(*func)();
}


 

On obtient exactement pareil :

On a donc d’abord créé un shellcode qui fonctionnera uniquement sur notre pc et sans reboot, pour finir par un qui fonctionnera (normalement) en tout temps et sur n’importe quel windows.

Ce contenu a été publié dans Shellcode, avec comme mot(s)-clef(s) , , , . Vous pouvez le mettre en favoris avec ce permalien.

Une réponse à Les shellcodes x86 sur Windows seven :

  1. P’tit greetz à SylTroX*66, Xylitol, Marwindows et Sh0ck, les quatre gars sympathoches que je connais depuis peu de temps. Surtout à Sh0ck qui semble être amoureux des shellcodes. C’est débile, mais bon. (j’déconne ;)).

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*

Protected by WP Anti Spam

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.