# Shellcode x86 sur Windows
Écriture de shellcode x86 sur Windows
Dans cet article, nous allons voir comment écrire un shellcode x86 sur Windows.
Introduction
Un shellcode est un code arbitraire. Il est injecté dans la mémoire comme par exemple dans le cas d'un buffer overflow. Ils sont appelés shellcodes car ce type de code est souvent utilisé pour obtenir un shell ou une invite de commande dans le but de prendre le contrôle du pc vulnérable.
Les shellcodes sont constitués d'une suite d'instruction assembleur et peuvent donc faire tout ce que nous souhaitons.
L'écriture d'un shellcode Windows nécessite de connaître les API Windows, de savoir dans quelle DLL une fonction que nous souhaitons utiliser ce trouve, de charger cette DLL, puis d'appeler cette fonction par son adresse.
Généralement, pour qu'un shellcode soit entièrement copié dans la mémoire, comme par exemple dans le cas d'un Buffer Overflow, il ne doit absolument pas contenir de null byte, c'est à dire le caractère '\0', celui terminant une string en langage C.
Shellcode statique
Dans un premier temps, nous allons créer un shellcode statique, c'est à dire que ce shellcode ne fonctionnera plus lorsque vous aurez redémarré votre Windows. 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.
Nous avons donc besoin de connaître les adresses effective des fonctions suivantes:
- LoadLibraryA qui se trouve dans kernel32.dll pour charger user32.dll
- MessageBoxA contenue dans user32.dll pour afficher la boîte de dialogue
- ExitProcess résidant dans kernel32.dll pour quitter proprement
Voici les prototypes de nos fonctions:
|
|
|
|
|
|
Pour trouver ces adresses, nous allons utiliser les fonctions LoadLibrary() et GetProcAddress() réunis dans ce petit outil que j'ai appelé GetAddress et dont voici la source :
|
|
Nous obtenons l'adresse 0x76F74BC6 pour LoadLibraryA, 0x7782FEAE pour MessageBoxA et 0x76F7734E pour ExitProcess.
En ce qui concerne les chaînes de caractères, nous utiliserons une technique se servant des instructions assembleur jmp, call et pop pour obtenir un pointeur vers chacune de nos chaînes de caractères.
Voici un exemple de cette technique :
|
|
Avec cet exemple, le registre ecx contient un pointeur vers la string "hello world".
Mais il y a un problème : le null byte utilisé pour terminer la chaîne 'h' 'e' 'l' 'l' 'o' ' ' 'w' 'o' 'r' 'l' 'd' '\0'. Pour y remédier, nous allons utiliser un autre registre, le mettre à 0 avec l'instruction XOR, ajouter un caractère superflu à la fin de notre string, puis écraser ce caractère par le byte de poids faible de ce registre.
Avec cette technique, notre shellcode sera sans null byte :
|
|
Voici donc maintenant notre shellcode au complet:
|
|
Pour essayer ce shellcode, il nous faut passer par un programme intermédiaire. Voici le programme que j'utilise, je l'ai appelé shellcodetest
|
|
Pour obtenir les bytes qui vont former le shellcode depuis l'exe généré par l'assembleur masm et ainsi le placer dans le buffer, nous utiliserons un outil appelé GetShellcode dont voici la source :
|
|
Après compilation et exécution de shellcodetest nous obtenons :
Comme expliqué plus haut, après un reboot de votre PC, ce shellcode ne fonctionnera plus. L'adresse des fonctions écrites en dur ici auront changées. Il nous faut donc une autre approche.
Cette nouvelle approche est appelé "shellcode générique" et c'est ce que nous allons voir maintenant.
Shellcode générique
Au lieu d'utiliser un programme externe comme GetAddress pour obtenir les adresses des fonctions que nous souhaitons utiliser et de se contenter de copier/coller ces adresses dans le code assembleur. Nous les chercherons directement dans le shellcode.
Cette approche nécessite d'abord de connaître l'adresse de base de kernel32.dll. Il existe plusieurs technique pour trouver cette adresse dont une qui utilise la structure PEB.
Nous trouvons le pointeur sur ce PEB à fs:[0x30].
Une fois dans cette structure à 0x0C, nous tombons 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.
Selon la version de Windows, kernel32.dll est chargée en deuxième ou troisième position.
Une façon de rendre cette technique portable sur toutes les versions de Windows, est de regarder à la fin de chaque nom de module qui l'on parcourt, nom que l'on trouve à l'offset 0x20 sur chaque élément de InitializationOrderModule si le nom de la dll ou du module comporte les 12 caractères de "kernel32.dll".
Puis, enfin, à 0x08 sur chaque élément, nous trouvons l'adresse de base du module.
Voici le code assembleur qui nous permet de trouver l'adresse de kernel32.dll
|
|
Avec ce code, le registre EAX contient l'adresse de base du module kernel32.dll.
Il ne nous reste donc plus qu'à obtenir l'adresse de nos fonctions.
Nous allons refaire le même shellcode que dans l'exemple précédent, c'est à dire l'exemple du shellcode statique.
Nous allons y aller en deux fois :
- Nous allons chercher les adresses de la fonction LoadLibraryA et de la fonction ExitProcess qui se trouvent toutes les deux dans kernel32.dll
- Puis, nous chercherons l'adresse de la fonction MessageBoxA qui se trouve quant à elle dans user32.dll
Pour trouver les adresses de nos fonctions, nous allons utiliser le format PE et analyser les dll afin d'obtenir les adresses dans la table d'exportation.
Sachant que dans un shellcode nous ne pouvons pas lire toutes les en-têtes, ou utiliser fread, fseek, etc. Il nous faut donc connaître à l'avance les décalages ou offsets de tout ce que l'on a besoin:
- 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
Autre astuce, 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 nous recherchons, nous allons utiliser un système de hash.
C'est à dire, au départ, créer un hash pour chaque nom de fonction que nous souhaitons utiliser. Puis dans le shellcode à chaque nom de fonction parcouru nous générerons à nouveau ce hash pour le comparer avec celui que nous recherchons. Cette technique permet de diminuer la taille de notre shellcode.
Voici le code permettant de générer le hash d'une fonction:
|
|
Nous savons donc comment trouver l'adresse de kernel32.dll. Nous savons aussi qu'il faut parcourir la table d'exportation, et surtout comment la parcourir. Nous savons également qu'il faut générer un hash pour la comparaison des noms de fonctions.
On a donc tout ce qu'il nous faut pour rendre notre shellcode générique.
Voici le shellcode qui affiche la MessageBox mais de façon générique :
|
|
En utilisant GetShellcode.exe pour obtenir les bytes, nous obtenons :
Conclusion
Nous avons donc d'abord créé un shellcode affichant une MessageBox qui fonctionnera uniquement sur notre pc et sans reboot, pour finir par un autre shellcode qui fonctionnera normalement en tout temps et sur n'importe quel Windows.