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.
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 ;)).