[WiShMaster - Partie 1] Introduction à l'écriture de shellcodes en C

Par Benjamin Caillat, Mastère Spécialisé Sécurité ESIEA
Le 16/09/2006


Résumé : Les ressources pour les shellcodes, en assembleur, sont légions sur le net ; l’objectif est içi de montrer les problématiques liées à l'écriture de code en C et leur compilation dans le but d'en extraire un shellcode (complexe) depuis le binaire généré.



Le web recèle maintenant de nombreuses ressources permettant d’obtenir assez rapidement un shellcode.

L’outil Metasploit ( lien ), par exemple, contient une base impressionnante de 75 shellcodes pour différentes plateformes directement utilisables. Une simple recherche sur google permet également de récupérer une liste conséquente de documents, d’articles et de codes commentés offrant une bonne base à la création d’un shellcode.

Ces différents éléments sont généralement orientés vers l’écriture en assembleur de shellcodes de taille très réduite exécutant des opérations relativement simples : ajout d’un utilisateur, écoute sur un port, reverse connect, …

Cependant, il existe de nombreux cas nécessitant des shellcodes effectuant des opérations beaucoup plus complexes. Par exemple, lors d’une exploitation entièrement en mémoire, un premier shellcode de taille très réduite exécuté via une faille pourra effectuer le téléchargement en mémoire d’un second shellcode offrant des fonctionnalités avancées.

Ou encore, lors du développement d’une application utilisant des techniques d’injection de thread.
La question se pose alors : comment obtenir de tels shellcodes ? Du fait de leur complexité, leur écriture directement en assembleur peut s’avérer relativement longue et fastidieuse.

Il serait beaucoup plus pratique d’écrire un code en C, de le compiler et d’extraire le shellcode du binaire généré. L’objectif de ce dossier est de montrer les problématiques liées à cette approche, d’introduire un exemple de solution, puis de présenter un développement personnel générant des shellcodes à partir de code source C. Par la suite, nous nous placerons dans le cas de Windows.


Principe de l’écriture de shellcodes en C


Analyse du code assembleur généré par les compilateurs


Analysons dans un premier temps le binaire généré par la compilation d’un programme basique affichant le message « Hello world » dans une boite de message et appelant une fonction interne « MyFunc » :


void MyFunc(int a)
{
printf("%d", a);
}

int _tmain(int argc, _TCHAR* argv[])
{
MessageBox(NULL, "Hello world", "Hello world", MB_OK);
MyFunc(7);

return 0;
}



La compilation sous Windows conduit à la génération d’un exécutable au format PE constitué de quelques entêtes contenant des informations sur le fichier et de plusieurs sections contenant le code à exécuter, les données initialisées et celles non-initialisées – désignées dans la suite par données (non) initialisées – et d’autres informations utilisées par Windows lors du chargement de l’exécutable (table d’importation, …)

De manière très schématique, l’exécutable produit a la structure suivante :






La section « .text » contiendra notamment le code des fonctions « main » et « MyFunc », la section « .rdata » la chaîne « Hello world » et la table d’importation. Le compilateur utilisé est celui fourni en standard dans la suite Visual Studio, en activant l’optimisation de taille (option /O1).

Analysons maintenant le code assembleur généré.


MessageBox(NULL, "Hello world", "Hello world", MB_OK);
00401012 6A 00 push 0
00401014 B8 00614000 mov eax,offset string "Hello world" (406100h)
00401019 50 push eax
0040101A 50 push eax
0040101B 6A 00 push 0
0040101D FF15 D4604000 call dword ptr [__imp__MessageBoxA@16 (4060D4h)]
MyFunc(7);
00401023 6A 07 push 7
00401025 E8 D6FFFFFF call MyFunc (401000h)
0040102A 59 pop ecx



Plusieurs remarques peuvent être faites sur ce code :

- Dans la deuxième instruction, nous trouvons la valeur 406100h qui correspond à l’adresse de la chaîne « Hello world », codée « en dur »,

- L’appel à MessageBoxA utilise un call à l’adresse contenue dans l’entrée « __imp__MessageBoxA » de la table d’importation, une table de pointeurs de fonctions remplie par le loader de Windows lors de la création du processus :


004060D4 77D504EA 00000000 00000000 00000000
004060E4 44FFD423 00000000 00000002 00000082



La fonction MessageBoxA se trouve réellement en 0x77D504EA :


77D504EA 8BFF mov edi,edi
77D504EC 55 push ebp
77D504ED 8BEC mov ebp,esp
77D504EF 833D BC04D777 cmp dword ptr ds:[77d704bc],0



- L’appel à la fonction MyFunc est basé sur un adressage relatif : La valeur 0xFFFFFFD6 représente la différence entre l’adresse de la fonction appelée (401000h) et l’adresse de la prochaine instruction (40102Ah) : 401000h-40102Ah = 0xFFFFFFD6.


Premier essai de shellcode

Imaginons que nous formions un buffer constitué du code de la fonction « main » suivi de celui de « MyFunc » :







Supposons maintenant que nous injections ce bloc dans un processus quelconque à une adresse quelconque et que nous transférions l’exécution dessus.

Dans l’espace mémoire de ce processus, l’adresse 406100h correspondra à des données tout autres que la chaîne « Hello world » ou même à une zone mémoire non allouée.

De même, l’adresse 4060D4h ne contiendra sûrement pas l’adresse de MessageBoxA ; le call conduira donc à une adresse arbitraire.






Il faut de plus noter qu’il est tout à fait possible que la librairie partagée user32.dll, contenant la fonction MessagBoxA, ne soit pas chargée dans ce processus.

Au niveau de l’appel de la fonction interne, la situation n’est guère meilleure. En effet la valeur de l’adressage relatif a été calculée par le compilateur pour un agencement très précis des fonctions.

Lors de l’exécution du programme d’origine, l’espace mémoire a l’allure suivante :






Cet agencement a cependant été modifié lors de la formation de notre « shellcode ». Le saut relatif conduira alors lui aussi à des instructions inconnues :






Une telle exécution conduira par conséquent à un résultat indéterminé ou à un plantage du processus.

Le code binaire généré par la compilation d’un code C « normal » ne peut donc être directement extrait et utilisé comme shellcode.


Ecriture de code C produisant un shellcode

Trois types d’opérations conduisent à la génération d’un code binaire non relocalisable et non autonome :

- Les références aux données (non)initialisées qui contiennent une adresse en « dur »,
- Les appels de fonctions internes, qui contiennent une adresse relative,
- Les appels aux fonctions importées, qui contiennent une adresse absolue représentant le pointeur de fonction dans la table d’importation.

Une solution possible pour éviter ces valeurs fixes est d’utiliser une structure contenant des pointeurs vers les fonctions internes/importées et toutes les données (non)initialisées. Cette structure, appelée dans la suite GLOBAL_DATA, serait par exemple ajoutée à la fin du shellcode.

Les appels de fonctions seraient alors effectués via les pointeurs de fonctions et les références aux données (non) initialisées porteraient sur les champs de cette structure.

En reprenant notre exemple de code, la structure GLOBAL_DATA serait typiquement :






Au niveau du code la déclaration peut être par exemple :


typedef INT (*MessageBoxATypeDef) (HWND, CHAR *, CHAR *, UINT);
typedef INT (*printfTypeDef) (CHAR *, ...);
typedef VOID (*MyFuncTypeDef) (INT);

typedef struct __GLOBAL_DATA
{
MessageBoxATypeDef MessageBoxA;
printfTypeDef printf;
MyFuncTypeDef MyFunc;
CHAR szMsg[12];
} GLOBAL_DATA, * LPGLOBAL_DATA;



Le code :


MessageBox(NULL, "Hello world", "Hello world", MB_OK);
MyFunc(7);



Devient alors :


pGlobalData->MessageBoxA(NULL, pGlobalData->szMsg, pGlobalData->szMsg, MB_OK);
pGlobalData->MyFunc(7);



Après compilation, nous obtenons :


pGlobalData->MessageBoxA(NULL, pGlobalData->szMsg, pGlobalData->szMsg, MB_OK);
00401001 A1 C0864000 mov eax,dword ptr [pGlobalData (4086C0h)]
00401006 8D48 0C lea ecx,[eax+0Ch]
00401009 6A 00 push 0
0040100B 51 push ecx
0040100C 51 push ecx
0040100D 6A 00 push 0
0040100F FF10 call dword ptr [eax]
pGlobalData->MyFunc(7);
00401011 A1 C0864000 mov eax,dword ptr [pGlobalData (4086C0h)]
00401016 6A 07 push 7
00401018 FF50 08 call dword ptr [eax+8]
0040101B 83C4 14 add esp,14h



Nous constatons que toutes les références sont basées sur pGlobalData. Ce code devient donc relocalisable et sans référence externe (puisque la structure GLOBAL_DATA fait partie du shellcode).

Cette technique impose cependant plusieurs contraintes :

- Tout d’abord, le shellcode doit avant tout pouvoir retrouver l’adresse de la structure GLOBAL_DATA puis initialiser les champs correspondant aux pointeurs de fonctions,

- Ensuite, l’écriture du code s’avère relativement fastidieuse : l’utilisation d’une fonction importée nécessite la déclaration d’un nouveau type, l’ajout du pointeur dans la structure GLOBAL_DATA et la modification du mécanisme d’initialisation pour éventuellement charger la dll et retrouver l’adresse de la fonction réelle,

- L’ajout d’une chaîne de caractères requiert de calculer sa longueur puis de déclarer un tableau de « char » dans la structure GLOBAL_DATA de la taille adéquate. Cette opération devient particulièrement longue lors de l’ajout de traces de debuggage,

- Enfin il est important de noter qu’une ligne de code ne respectant pas ces règles ne produira aucune erreur à la compilation. En revanche le code binaire généré contiendra une référence non relocalisable qui conduira à un plantage lors de l’exécution et à de longues séances de debuggage.

L’écriture de shellcodes pour Windows par cette solution semble donc techniquement possible, mais reste une opération relativement délicate et fastidieuse.

Autres ressources dans ce même dossier :

[WiShMaster - Partie 2] Principe de shellcodisation avec WiShMaster (1) - lien

[WiShMaster - Partie 3] Principe de shellcodisation avec WiShMaster (2) - lien

[WiShMaster - Partie 4] Principe et Fonctionnement - RConnect/WiShMaster Vs firewalls personnels (1) - lien

[WiShMaster - Partie 5] Principe et Fontionnement - RConnect/WiShMaster Vs firewalls personnels (2) - lien

[WiShMaster - Partie 6] Principe et Fonctionnement - RConnect/WiShMaster Vs firewalls personnels (3) - lien

[WiShMaster - Partie 7] Résultat de RConnect avec des firewalls personnels et conclusion - lien