Buffer Overflow et shellcodes sous windows   Version imprimable de cet article Enregistrer au format PDF

Cet article va vous montrer comment rédiger des shellcodes basiques pour des applications possédant une faille sous windows.
Vous apprendrez aussi comment fonctionne votre processus possédant la faille en mémoire, et surtout pourquoi cette faille faille est exploitable.


par C@storus

Exploitation des failles de buffer overflow :

- I : Les buffer overflow concrètement, qu’est-ce que c’est ?

- II : Un buffer c’est quoi exactement ?

- III : Comment est-il possible de détourner l’exécution d’un programme ?

- IV : Les logiciels dont vous aurez besoin.

- V : Création de notre premier programme présentant une faille de type buffer overflow.

- VI : Qu’est-ce que l’adressage virtuel ?

- VII : Comment notre programme est-il charge en mémoire vive ?

- VIII : Etude via OllyGDB

- IX : Un shellcode, Qu’est-ce que c’est ?

- X : Création de notre premier shellcode.

- XI : Edition Hexadécimale.

- XII : Exécutons notre shellcode !

- XIII : Création de notre second shellcode !

- XIV : Exécutons notre second shellcode !

- XV : Si vous en êtes ici, c’est que vous avez réussi, félicitation !


I : Les buffer overflow concrètement, qu’est-ce que c’est ?

Un buffer overflow est une faille de sécurité au sein d’une application. Elle permet à un utilisateur malicieux de détourner le flux d’exécution du programme afin de pousser l’application à exécuter du code préalablement injecté.
Bref, tout cela est un peu trop académique, il faudrait un exemple plus palpable.
Imaginez, un développeur crée une application quelconque.
Lors de l’exécution de cette dernière, une saisie clavier est demandée.
Cette saisie ne vérifie pas le nombre de caractères entré.
Or le problème, c’est qu’en mémoire, cette application dispose d’un petit espace dans lequel elle stockera tous les caractères saisis.
Là où réside le problème, c’est que si cet espace (que l’on nommera buffer ou mémoire tampon)
est trop petit pour le nombre de caractères saisis, alors, l’application écrira tout de même les caractères, mais cela débordera du buffer (d’où l’expression buffer overflow).
Voilà ce qu’est simplement un buffer overflow au niveau technique.


II : Un buffer, c’est quoi exactement ?

Un buffer est, comme je l’ai dit précédemment, un petit espace mémoire qui servira à réceptionner des données.
Bref, c’est bien joli tout ça mais c’est encore un peu flou.
Voici un exemple de buffer en C :

  1. char buffer[50] ;

Ceci est un buffer.
Comme vous pouvez le constater, il s’agit d’une suite d’octets.
Ce qu’il faut savoir, c’est que cette suite d’octets n’est pas située n’importe où en mémoire.
En effet, cette dernière est située sur la pile du programme.
Ce qui nous amène à une nouvelle question, à savoir, qu’est-ce que la pile ?
C’est vrai après tout, le mot pile, c’est assez vague, on a du mal à se faire une idée.
En fait, on a appelé ça une pile puisque l’idée ressemblait à celle d’une pile d’assiette.
Vous voyez probablement comment une pile d’assiette fonctionne, on les empile les unes sur les autres, elles sont empilées de plus en plus haut.
Pour notre pile à nous, on considère aussi qu’elle est non pas constituée d’assiettes, mais d’octets. (pas très surprenant je sais…)
Il existe deux registres de votre processeur qui ont pour rôle de situer cette pile en mémoire, il s’agit du registre EBP et du registre ESP.

EBP pointe sur le bas de la pile.
ESP pointe sur le sommet de la pile.

Voici un petit schéma pour mieux visualiser :

On se rend compte de plusieurs choses :
- ESP possède une valeur plus petite que EBP.
- EBP possède une valeur plus grande que ESP.
- L’adresse des blocs de pile augmente de 4 en 4.
- Evidemment, ESP ne pointe pas sur 0 dans la réalité, dans le cas présent c’est pour représenter.

Si ESP est plus grand que EBP, c’est pour une raison simple, c’est parce que la pile grandit vers le bas. (Je sais, c’est déroutant au premier abord, mais c’est une convention établie).

Si l’adresse des blocs de pile augmente de 4 en 4 c’est parce que généralement chaque bloc de la pile est constitué de 4 octets. (Ceci est vrai uniquement sur les architectures 32 bits car les registres font 4 octets, sous OllyGDB la pile sera représentée comme une pile d’assiettes de 4 octets chacune).

Ainsi, on peut en conclure que créer un buffer de 50 octets, c’est soustraire 50 à ESP.
Étant donné que généralement on travaille en C, il est important de savoir quelques détails.
Concrètement en C, toute variable automatique (celles qui ne sont ni statiques, ni dynamiques, ni globales) sont crées sur la pile via des opérations de soustractions et d’additions sur le registre ESP.
La pile est en fait une sorte de zone temporaire dédiée à stocker quelques données à utilisation temporaire.
C’est pour cela que les variables automatiques des fonctions conviennent parfaitement à la pile.


III : Comment est-il possible de détourner l’exécution d’un programme ?

Un programme pour s’exécuter dispose d’un lot de registres qui lui permettent de manipuler les données qu’il comporte, voici une liste succincte des registres les plus importants :

- EAX registre d’accumulation : il est utilisé pour la plupart des calculs, MOV, ADD, SUB, DIV, MUL...
- EBP registre de base : il pointe sur la base de la pile.
- ECX registre compteur : il sert de compteur pour les boucles LOOP.
- EDX registre entrée sortie : il est employé pour certaines interruptions logicielles.
- CS registre Code Segment : il représente l’adresse du segment de code
- SS registre Stack Segment : il représente l’adresse du segment de pile
- EIP registre EIP : il représente l’adresse de la prochaine instruction à exécuter
ESP registre ESP : il pointe sur le sommet de la pile.

Dans ces registres, il y en a 3 qui nous intéressent particulièrement :
- EBP
- ESP
- EIP

Pour détourner l’exécution d’un programme, il faut modifier le registre EIP.
La question est, comment parvenir à cela en écrivant uniquement des données dans un buffer.
En réalité, il existe une manière plutôt simple. Encore faut-il en connaitre le fonctionnement.
En langage C, l’appel à une fonction pousse le programme à sauvegarder le registre EIP puis le registre EBP et enfin le registre EDI sur la pile puis à stocker dans EBP la valeur de ESP, puis soustraire à ESP le nombre d’octets nécessaires à la création de variables automatiques de la fonction à appeler, et enfin, à sauter à l’adresse de la fonction en question.

Une fois l’exécution de la fonction arrivée à terme, le programme ajoute à ESP le nombre d’octets constituant ses variables locales, ce qui le replace à sa position d’origine avant l’appel à la fonction, puis récupère EDI avec une instruction POP EDI puis EBP avec une instruction POP EBP, enfin, il récupère le EIP sauvegarder sur la pile en faisant un POP EIP (en réalité c’est souvent un RETN qui est utiliser, mais c’est équivalent à un POP EIP)
Ce qui permet de pousser le flux d’exécution du programme à retourner sur l’endroit ou elle était avant l’appel de la fonction.
Décrit comme cela, je sais, c’est pas clair, pas clair du tout même, voila pourquoi je vais vous décrire ces étapes une par une via des schémas :

On considère que nous somme dans le code de la fonction main, dans ce main, on appelle une fonction :

  1. void overflow(void)
  2. {
  3. char buffer[50] ;
  4. }

Télécharger

Concrètement voici comment tout ça est organisé :

Au moment de l’appel à la fonction overflow, le programme saute a l’adresse
mémoire de la fonction overflow, une fois le code de la fonction overflow exécuté, le programme retourne à la fonction main afin de poursuivre l’exécution de la suite de son code.
Mais concrètement, que se passe-il juste avant le saut a l’adresse du code de la fonction overflow ?
Tout d’abord le programme met en place une stratégie qui lui permettra de retrouver l’endroit ou il en était avant l’exécution de la fonction overflow mais aussi la pile telle qu’elle était, ainsi il peut continuer l’exécution du code qui suit l’appel à la fonction overflow.
Pour mettre cela en place, il faut qu’il trouve un moyen de retrouver les valeurs d’origine des registres ESP et EBP avant l’appel a overflow. Ainsi que la valeur du registre EIP.

Voici l’état de la pile avant la mise en place de la stratégie :

- La première étape de la stratégie est de sauvegarder EIP.
- La seconde est de sauter à l’adresse du code de la fonction overflow.
- La troisième est de sauver sur la pile EBP puis EDI.
- La quatrième est de donner à EBP la valeur de ESP.
- La cinquième est de soustraire à ESP le nombre d’octets nécessaires à la création des variables automatiques de la fonction qui va être exécutée.


Illustration de la première étape :

La sauvegarde de l’EIP est provoquée par l’appel CALL
Ce mnémonique permet de faire un push EIP puis un JMP a l’adresse du code de la fonction overflow.
Comme vous pouvez le constater, ESP lui aussi s’est vu modifier, en effet, lorsque l’on push quelque chose sur la pile, ESP est automatiquement modifier, pour être plus précis, on lui soustrait 4.


Illustration de la seconde étape :

La seconde étape est en réalité contenue dans la première, car la sauvegarde de EIP et le saut a l’adresse mémoire du code de la fonction overflow se font via l’instruction CALL simultanément.


Illustration de la troisième étape :

Dès que le programme est rentré dans la fonction overflow, il sauvegarde EBP puis EDI sur la pile grâce a l’instruction PUSH.
Une fois ceci effectué, il passe à l’étape quatre, c’est dans cette étape qu’il alloue sur la pile la mémoire nécessaire pour créer les variables automatiques de la fonction (arguments ainsi que variables locales.).


Illustration de la quatrième étape :

Donner à EBP la valeur de ESP revient a dire que la zone utilisable de la pile est désormais inexistante, en voici l’illustration :


Illustration de la cinquième étape :

Cette étape permet de soustraire à ESP la taille en octet appropriée pour être capable de stocker les variables automatiques de la fonction overflow.

Au final on se rend compte que l’espace utilisable de la pile a été décalé.
Cette technique permettra de retrouver le bon EDI, le bon EBP et le bon EIP lorsque la fonction aura terminé de s’exécuter afin de retourner dans la fonction appelante.

La faille est très simple dans le principe, mais pour découvrir une telle faille tout seul, cela relève de la prouesse.
Le principe c’est que désormais, nous avons un buffer alloué dans la pile, ce buffer est situé à une adresse mémoire moins grande que celle de la sauvegarde du EIP sur la pile.
Lorsque une fonction d’entré sortie agit sur un buffer, par exemple read, scanf, ou encore gets.
L’écriture dans le buffer se fait d’octet en octet, en incrémentant de un à chaque fois l’adresse du buffer dans laquelle elle va écrire.
Si on demandait a une fonction telle que scanf ou read ou gets d’écrire plus de données dans buffer qu’il ne peut en contenir, alors, les écritures déborderaient du buffer.
Là où ça nous intéresse, c’est que sur la pile ça débordera vers le bas.
En direction de notre sauvegarde de EIP donc.
Nous allons donc pouvoir modifier la sauvegarde de EIP grâce à l’écrasement des données de la pile provoqué par l’écriture de plus de données dans le buffer qu’il ne peut en contenir.


Illustration du débordement de buffer :

En fait, toute la subtilité de cette attaque est de trouver l’endroit précis dans la pile où se situe la sauvegarde de l’EIP et de l’écraser afin de pouvoir modifier l’adresse de retour une fois l’exécution de la fonction overflow terminée.
Car lorsque la fonction overflow arrive à son terme.
L’inverse exact des opérations effectuées avant d’exécuter la fonction overflow seront exécutées.

soit :

  1. ADD ESP, 50
  2. POP EDI
  3. POP EBP
  4. RETN ;(ou POP EIP)

Télécharger

au lieu de :

  1. CALL overflow ;(équivalent a PUSH EIP+5 puis JMP overflow)
  2. PUSH EBP
  3. PUSH EDI
  4. SUB ESP, 50

Télécharger


IV : Les logiciels dont vous aurez besoin :

- OllyGDB
- PEditor
- HexEdit
- DevCpp

Le premier est un puissant debbuger, le second est un éditeur de fichiers exécutables au format PE (portable exécutable) il s’agit du format des exécutables windows.
Le troisième permet d’éditer des fichiers en affichant leur contenu sous la forme hexadécimale ce qui nous sera très utile pour y stocker notre code à injecter.
Le quatrième est un IDE base sur le compilateur C/C++ MinGW qui est une adaptation de gcc pour windows.


V : Création de notre premier programme présentant une faille de buffer overflow.

Sous DevCpp créez un projet console en C.
Puis recopiez ce code source :

  1. #include <stdio.h>
  2. void overflow(void)
  3. {
  4. char buffer[50] = { 0} ;
  5. FILE *fp = fopen("fichier_exploit.txt", "r");
  6. fread(buffer, sizeof(char), 200, fp);
  7. fclose(fp);
  8. }
  9.  
  10. int main(void)
  11. {
  12. overflow();
  13. return (0) ;
  14. }

Télécharger

Comme vous pouvez le constater, notre programme appel une fonction qui possède un buffer de 50 caractères, dans cette fonction, on cherche à lire les 200 premiers octets du contenu d’un fichier nomme « fichier_exploit.txt » ouvert en lecture seule.
Le buffer overflow saute aux yeux, on veux stoquer 200 octets dans un buffer qui ne peut en contenir que 50.
Créer un fichier texte dans le même dossier que votre programme, et appelez le « fichier_exploit.txt », Mettez 200 caractères ‘A’ à l’intérieur, sauvegarder le fichier, puis compilez votre programme et enfin lancez le.

Vous devriez obtenir une jolie erreur du type :

Cliquez sur « click here » afin d’avoir d’avantage d’informations.
Vous devriez avoir ceci :

Vous pouvez voir que la suite de nombres indiquée par l’offset est étrange : 41414141
En réalité ce nombre indique l’adresse mémoire a laquelle votre programme a rencontrer une erreur de segmentation.
En réalité, 41 est la valeur du caractère A en hexadécimal.
Nous avons en fait écrasé la sauvegarde du registre EIP avec les caractères A contenu dans notre fichier.
En revanche, nous ne savons pas quels sont les caractères exacts qui permettent d’écraser cet EIP.
Pour cela il existe deux méthodes :

- La première est la méthode dite du bucheron :)
- La seconde bien plus sérieuse et fiable est celle du debbuger.

En gros, la première, son principe c’est de tester comme un gros porc en enlevant et en ajoutant des caractères dans le fichier comme un goret en faisant planter le programme des dizaines de fois à la suite pour savoir a quel endroit précisément on commence à écraser la sauvegarde du registre EIP.
Cette méthode est sale, et fastidieuse.
C’est pour cela que je vais vous montrer la seconde.
Celle du debbuger.
Mais tout d’abord, je dois vous donner quelques bases théoriques à propos de la mémoire virtuelle.


VI : C’est quoi l’adressage virtuel ?

L’adressage virtuel est une capacité des Systèmes d’exploitation contemporains.
Elle permet aux programmes d’utiliser des adresses qui en réalité ne sont pas des adresses mémoire physique, c’est pour cela qu’on dit qu’elles sont virtuelles.

Ainsi, un programme A et un programme B peuvent utiliser des adresses mémoires virtuelles qui ont la même valeur pour manipuler des variables et qui pourtant ne feront pas référence au même emplacement physique dans la RAM.
Un très bon exemple serait de lancer un programme deux fois, et de lui faire afficher l’adresse d’une de ses variables, l’adresse serait la même car elle serait virtuelle, pourtant les octets physiques en RAM auxquels elles feraient référence ne seraient pas communs aux deux processus.

C’est le système d’exploitation qui par un jeu de tables d’adresses ainsi que de tables de processus pourra orchestrer tout cela.
Là vous vous demandez probablement pourquoi je vous raconte cela.

En fait, sous windows XP, un exécutable, lorsque il est compilé possède une adresse dans son en-tête qui sera lue par l’OS lorsque celui-ci tentera de l’exécuter.
l’OS utilisera cette adresse afin de se dire : "Ce programme là, je le charge à partir de l’octet X dans son segment de mémoire virtuelle."
Cette adresse est appelée ImageBase.
Et elle ne varie jamais.

Autrement dit, toutes les adresses manipulées par vos programmes sous windows XP ne varient jamais.
Tout est toujours à la même place, par corrélation cela signifie que si vous parvenez a faire fonctionner une fois votre shellcode sur un programme, alors votre shellcode fonctionnera toujours sur ce programme même si votre ordinateur redémarre . En revanche, il est fort probable que si votre programme est recompilé votre shellcode pourrait ne plus fonctionner.
Car le compilateur change parfois l’image Base ou encore certaines adresses virtuelles utilisées par votre programme.

Voici un exemple par schéma :

Bien sur, ce n’est pas un schéma réaliste, mais il illustre simplement le fait que les adresses virtuelles des programmes A et B ne sont pas les mêmes dans la mémoire physique.


VII : Comment notre programme est-il chargé en mémoire vive.

Chaque exécutable sous windows possède un header, ce header permet de donner des informations importantes au sujet de l’exécutable en question.
Ce qui nous intéresse nous, c’est le ImageBase, c’est une adresse virtuelle qui permet de dire à partir de quelle adresse notre programme souhaite être chargé.
Ensuite, notre programme est chargé relativement à cette image base.
Tout d’abord en mémoire, on trouve le segment de code, puis le segment de donnés, et enfin le segment de pile.
Ces trois segments sont indispensables au bon fonctionnement de notre exécutable.

Le segment de code :

Il contient tout simplement notre code, rien d’extraordinaire.

Le segment de données :

Il est tout de suite bien plus intéressant, c’est dans ce segment que sont stockés les blocs de mémoire alloués dynamiquement (via malloc).
Ce bloc croit vers le bas et les données qu’il contient sont permanents jusqu’à ce qu’on décide nous même de les détruire ou que le programme touche à sa fin.

Le segment de pile :

Il sert comme nous l’avons précédemment dit à stocker la plupart du temps des informations temporaires, il permet d’assurer les retours de fonctions et donc toutes les capacités récursives du programme, ce bloc croit vers le haut.
Étant donné que le segment de données et le segment de pile grandissent en se rapprochant l’un de l’autre, si une fonction récursive part en récursion infinie, il arrivera forcement un moment ou il rencontrera une erreur de segmentation car il entrera en collision avec le segment de données.


VIII : Etude via OllyDBG

Revenons à nos moutons.
Nous allons scruter le contenu de notre exécutable via OllyGDB et ainsi comprendre à quel moment, pourquoi et comment exploiter ce plantage.
Lancez OllyGDB .
Allez dans l’onglet File puis Open, enfin, sélectionnez l’exécutable que vous voulez examiner.
Donc en l’occurrence, celui que nous venons de compiler.

Je vais tout d’abord vous expliquer le principe d’OllyGDB.
OllyGDB est un debugger très puissant, probablement le plus connu sous windows.
Il permet d’exécuter vos programmes pas à pas, d’examiner la pile en détail ainsi que les registres, et en plus, il est gratuit et possède une interface graphique.

Nous allons rechercher dans notre code l’endroit où l’on fait appel à fread
C’est à cette ligne de code précise que nous allons placer un breakpoint afin de pouvoir comprendre ce qui se passe exactement.
Scrutez le code dans OllyGDB, vous devriez voir un endroit ou il y a marquer fread comme ci-dessous :

J’ai entouré d’une ellipse violette l’endroit où est marqué fread.
Vous devez sélectionner cette ligne en cliquant dessus avec le bouton gauche de la souris puis presser la touche f2 afin d’y placer un breakpoint.
une fois que c’est fait, lancer l’exécution avec le petit bouton lecture en haut de la fenêtre d’OllyGDB.
L’exécution s’arrêtera sur le breakpoint.

A ce moment là on peut constater quelque chose d’intéressant.
Ci-dessous vous pouvez voir un gros plan sur le dump de la pile (il s’agit de la fenêtre en bas a droite dans OllyGDB)

Ce que j’ai encadré en rouge est l’ensemble des arguments qui seront utilisés par fread.
On peut voir :
ptr = 0022FF10, qui désigne notre buffer où stocker les données.
size = 1, qui désigne la taille d’un élément que l’on souhaite lire.
n = C8, qui désigne le nombre d’octets a lire (C8 fait 200 en décimal)
stream = msvcrt.77C5FCE0, qui désigne l’adresse du file descriptor dans lequel on veut lire.

(Attention ! Les adresses que j’obtiens pour ce tutorial ne seront probablement pas les mêmes que les vôtres, ne les recopiez donc pas bêtement, mais suivez la démarche que j’indique afin de relever vos propres valeurs.)

Ce qui nous intéresse, c’est le premier argument, c’est-à-dire ptr.
la valeur qu’il contient est l’adresse à partir de laquelle il commencera à stocker les octets lus.
0022FF10 est une adresse contenue dans le segment de pile.
Ci-dessous j’ai détouré la zone qui désigne le buffer, donc à partir de l’adresse 0022FF10 :

Comme vous pouvez le constater, toute cette plage d’octets est initialisée à 0, ce qui est le témoin du fait que c’est notre buffer, car nous avons fait exprès de l’initialiser a 0 .
Désormais, étant donné le fait que notre programme est arrêté au breakpoint, nous pouvons l’exécuter pas à pas, ce que nous allons faire sans plus tarder, ainsi, nous pourrons voir l’effet du read sur notre pile et le buffer.
Pour exécuter une instruction, cliquer sur ce bouton : "inserer ici img_15"
Une fois que vous avez appuyer dessus vous pouvez constater l’ampleur du désastre dans la pile :

Tout a été écrasé par des 41 !
Autrement dis, tout a été écrasé par la valeur hexadécimale de nos ‘A’…
Et pourtant, pour le moment, ça ne plante pas, ce qui est tout a fait normal.
Cela plantera au moment ou notre programme tentera de ressortir de la fonction overflow, car la sauvegarde du EIP a été corrompue par nos ‘A’.

Désormais, nous allons exécuter notre code jusqu’à arriver à l’instruction RETN, c’est elle qui est chargée de récupérer la sauvegarde de l’EIP et de charger cette valeur dans le registre EIP.
Placez donc un nouveau breakpoint sur l’instruction RETN cette fois, comme ci-dessous :

Poursuivez l’exécution avec le bouton lecture, l’exécution s’arrêtera sur le second breakpoint, donc sur RETN.
A ce moment la, vous pourrez constater quelque chose d’intéressant sur la pile :

On peut constater que le sommet de la pile est sur l’adresse 0022F5C.
C’est les 4 octets pointés par cette adresse qui seront POP par RETN pour pouvoir remplir EIP.
En fait, RETN prend les 4 premiers octets pointés par ESP afin de les placer dans EIP.
Nous connaissons l’adresse du buffer qui est 0022FF10 ainsi que celle à laquelle est stockée la sauvegarde de EIP qui est 0022FF5C.
(Ces valeurs ne sont problablement pas les mêmes que les vôtres, prenez le temps de relever les vôtres)
On sait donc par déduction que pour pouvoir écraser la sauvegarde de EIP, il faudra remplir
(0022FF5C – 0022FF10) octets, ce qui fait 4C en hexa et donc 76 en décimal.
(Pour vos calculs optez pour la calculette windows, elle est si gracieusement fournie :))
C’est-à-dire que à partir du 77ieme octet nous commencerons à écraser la sauvegarde de EIP.
Donc, les octets numéro 77 ; 78 ; 79 ; 80 dans le buffer permettent d’écraser la sauvegarde de l’EIP.
Pour tester, remplaçons dans notre fichier texte le 77ieme 78ieme 79ieme et 80ieme caractère afin de voir si nos calculs sont bons.
Mettons BCDE pour tester à la place de AAAA.
Puis sauvegardez votre fichier.

La valeur de B est 42 en hexa, la valeur de C en hexa est 43, celle de D est 44 et celle de E est 45.
Théoriquement notre sauvegarde de EIP devrait valoir 42434445.
Réinitialisez votre programme sous OllyGDB en faisant Ctrl + f2.
Replacez a nouveau un breakpoint sur fread.
Et lancez l’exécution via le bouton de lecture, une fois l’exécution bloquée sur fread, exécutez l’instruction et observez la pile !

On peut voir notre 45444342 se balader au milieu des 41.
Mais, c’est étrange, c’est écrit à l’envers, en effet, il faut écrire les caractères dans l’ordre inverse pour qu’il soit charge à l’endroit.
C’est plutôt bon à savoir, désormais, nous savons comment modifier notre EIP, nous pouvons donc faire sauter notre programme où bon nous semble lorsque l’instruction RETN est exécutée.
C’est dans cette phase qu’intervient le shellcode.


IX : Un shellcode, c’est quoi ?

Un shellcode c’est un ensemble d’octets qui représentent en réalité un lot d’instructions machine directement exécutables par le processeur.
A l’origine, on appellait ça shellcode parce que ça avait pour but de donner un accès root à un shell.
C’est une suite d’octets de ce type que nous allons devoir injecter dans notre buffer, puis, une fois ceci fait, nous pourrons donner à la sauvegarde de l’EIP la valeur de l’adresse dans le buffer où commence l’exécution de notre shellcode.


X : Création de notre premier shellcode.

Notre premier shellcode va être très basique, il mettra juste notre programme en boucle infinie, le détournant donc totalement de ce qu’il devait faire à l’origine, puisque désormais il ne s’arrêtera plus.
Pour concevoir un shellcode il existe plusieurs possibilités, soit vous connaissez les opcodes (valeur hexa d’instruction assembleur) par cœur et les écrivez directement.
Soit vous écrivez votre bout de code en assembleur, le compilez et vous récupérez vos opcodes directement dans l’exécutable via un désassembleur.

Personnellement, moi, je trouve ça plutôt fastidieux, c’est pourquoi je préfère utiliser OllyDBG pour m’aider.

En effet, avec OllyDBG vous pouvez assembler du code en directement ! Et donc récupérer les opcodes allègrement pour ensuite les injecter à votre manière où bon vous semble.

Pour cela, relancez l’exécution de votre exécutable sous OllyDBG via Ctrl+f2.
Puis, sélectionnez une zone, n’importe laquelle dans votre segment de code (fenêtre en haut à gauche)
(sélectionner la zone en gardant le bouton gauche de la souris enfoncée)
Une fois votre zone de code sélectionnée (elle devrait normalement apparaitre en grisée).

Faites un clic droit dessus, dans le menu contextuel sélectionnez Binary, un sous menu textuel apparait dans lequel vous devez cliquer sur « Fill With NOPs »
Normalement toute votre zone de code sélectionnée se retrouve convertie en une suite d’instructions NOPs.
L’instruction NOP force le processeur à……
Bha à ne rien faire en fait, NOP ca signifie NO Operations.
C’est dans ce bloc de NOP qu’on va créer notre shellcode.

Notre shellcode sera super simple, voici ses instructions assembleur :
adresse_bouclage :

  1. MOV EAX, adresse_bouclage
  2. JMP EAX

Télécharger

C’est tout.
Il s’agit en fait d’une boucle.
La seule difficulté va être de déterminer l’adresse que l’on devra mettre à la place de adresse_bouclage.
Le plus ca va être de donner l’adresse du début du buffer avec 4 ou 5 octets de plus histoire de viser bien dedans, donc :

Cliquez sur une ligne de NOP, puis pressez la touche Espace.
Une petite fenêtre s’ouvre, elle vous permet d’éditer l’instruction assembleur courante.
Mettez :

  1. MOV EAX, 0022FF15

Pressez la touche entrer
Mettez

  1. JMP EAX

Pressez la touche entrer

Voilà, votre shellcode est construit, désormais, vous n’avez plus qu’a lire les opcodes :

On voit que les opcodes de nos deux instructions sont B8 15 ff 22 00 FF E0
Il ne reste plus qu’à mettre ca dans notre fichier texte sous forme hexadécimale, c’est là que le logiciel HexEdit va nous être très utile.


XI : Edition Hexadecimale.

C’est la partie la plus chiante, mais c’est aussi la dernière avant de pouvoir tester notre exploit.
Ouvrez HexEdit.
Cliquez sur File, puis sur open, et selectionnez votre fichier texte qui doit contenir votre shellcode, parce que pour le moment il ne contient que des A et BCDE, pas top le shellcode…
Une fois que c’est fait, vous devriez vous retrouver devant une fenêtre de ce genre :

Comme vous pouvez le voir, notre fichier contient des A qui équivalent à 41 en hexa, et BCDE qui équivaux a 42434445.
La première étape va être de remplacer tout les 41 par des 90, 90 équivaux a l’instruction machine NOP.
Pour cela, mettez vous tout d’abord en mode insertion (pressez la touche insert sur votre clavier)
Puis changez tout ces 41 en 90.

Au final vous devriez obtenir ceci :

Maintenant, on va caller notre shellcode un peu avant le 42434445.
Moi je l’ai placé comme ceci :

Maintenant il ne nous reste plus qu’a mettre la veritable valeur de la sauvegarde EIP que l’on souhaite.
Il faut la placer vers le debut du buffer, je vous rappelle que le buffer débute à l’adresse 0022FF10.
Disons donc a l’adresse 0022FF15, ainsi on sautera directement dans la plage de NOP, et le programme executera tranquillement tous les NOP jusqu’à tomber sur notre shellcode, ensuite il sautera a l’adresse 0022FF15 à cause du jmp, ect… indéfiniment.
Je vous rappelle qu’il faut ecrire l’adresse en ordre inverse, non pas 0022FF15 mais 15FF2200.

En modifiant cela dans notre fichier, vous devriez avoir ceci :

Sauvegardez le fichier.


XII : Exécutons notre shellcode !

Voici venu le moment de tester notre shellcode !
Retournez dans OllyDBG et restartez le programme (Ctrl + f2)
Maintenant, mettez à nouveau un breakpoint sur fread.
Exécutez le programme, vous tombez sur le breakpoint.
Exécutez l’instruction fread.
et continuez jusqu’au RETN.
Voici ce que cela me donne sur la pile :

Le RETN s’apprête à charger l’adresse 0022FF15 dans EIP.
l’exécution va donc jumper dans mon buffer pour y exécuter le shellcode qui s’y trouve.

En exécutant l’instruction suivante je me retrouve dans mon buffer à exécuter les NOP que j’ai placé :

Pour finalement aboutir à mon shellcode :

Inutile de vous démontrer que cela boucle.
Le MOV EAX, 22FF15 et le JMP EAX font leur œuvre sans problèmes.
Lancez votre programme hors de OllyDBG, exécutez le normalement en fait, et Oh miracle !
Il ne plante pas et ne se ferme pas !
On dirait presque qu’il est coincé dans une boucle infinie :D.
C’était un exemple très très basique de Shellcode, son intérêt est véritablement minimal, mais désormais vous connaissez les bases permettant de créer un buffer overflow.
J’ai tout de même décidé de vous expliquer comment aller plus loin en exécutant carrément une API de windows.
API signifie Application Programming Interface, windows en possède beaucoup. Par exemple pour afficher une fenêtre à l’écran, et c’est justement ce que nous allons faire.
Afficher une Message Box à l’écran requiert sous windows l’appel a l’API :

  1. WINUSERAPI int WINAPI MessageBoxA(HWND,LPCSTR,LPCSTR,UINT);

Cette API prend en paramètre 4 arguments.
HWND qui est un entier représentant le descripteur de la fenêtre parente qui affiche la MessageBox.
LPCSTR qui est un pointeur de chaine de caractère représentant le contenu de la message box.
LPCSTR qui est un pointeur de chaine de caractère représentant le titre de la message box
UINT qui est un entier représentant le type de bouton que l’on veut et le type de la message box.


XIII : Création de notre second shellcode !

Pour appeler cette API, nous devrons faire en sorte que notre exécutable ai chargé la dll qui la contient.
Puis, nous devrons pousser les arguments de l’API sur la pile dans l’ordre inverse, ainsi lorsque l’API les POP, elle les récupère dans l’ordre.
Enfin nous devrons faire un CALL adresse_de_l’API.
Pour trouver cette adresse, nous devrons la rechercher nous même.

La première chose à faire est de savoir dans quelle dll est contenu MessageBoxA, pour cela une petite recherche sur internet suffit, le site MSDN permet d’obtenir tout ces petits renseignements importants au sujet des APIs.

En l’occurrence MessageBoxA est contenue dans la dll user32.dll.
Il va donc falloir la charger dans notre exécutable via une autre API, qui se nomme LoadLibraryA
Cette dernière est contenue dans la dll kernel32.dll, et heureusement, cette dll est chargée par défaut dans tout les processus windows, y compris dans notre programme de test.

Le prototype de LoadLibraryA est comme suit :

  1. WINBASEAPI HINSTANCE WINAPI LoadLibraryA(LPCSTR);

Elle prend en paramètre un pointeur de chaine de caractère représentant le nom de la dll à charger.
Même principe que pour MessageBoxA, il faut avant son appel pousser sur la pile son argument et déterminer son adresse.

Tout d’abord, il faut déterminer l’adresse de MessageBoxA.
Pour cela le logiciel PEditor va nous être d’une grande aide.
Ce dernier permet d’explorer les headers des dll afin d’en extraire certaines informations cruciales.
Notamment l’image base de la dll ainsi que la relative virtual address de la fonction MessageBoxA.
Pour cela, commencez par aller dans votre répertoire :

C :\Windows\System32}}

recherchez la dll user32.dll, copiez la dans le répertoire de PEditor.
Maintenant ouvrez PEditor cliquez sur Browse, et sélectionnez la copie de user32.dll dans le dossier de PEditor.

Là, vous devriez obtenir ceci :

La première information importante est l’image Base,
Elle permet de savoir à partir de quel endroit la dll est chargée en mémoire.
En l’occurrence, elle est chargée en :
7E 41 00 00 (important, vous devez noter celle que vous vous avez, la mienne n’est pas forcement la même que la votre)

Maintenant, il nous faut savoir à quelle relative address se situe la fonction MessageBoxA.
Pour cela, cliquez sur directory
Une nouvelle fenêtre s’ouvre, dans cette dernière cliquez sur Exports.
Une nouvelle fenêtre apparait avec une liste de fonction, recherchez celle qui s’appelle MessageBoxA.

Une fois trouvé, vous pouvez voir le champs RVA (Relative Virtual Address)
Cette adresse est relative à l’image Base.
Ainsi, grâce à l’image base et à la RVA de la fonction, on peut déterminer l’adresse sur laquelle on doit faire un CALL.
Image Base + RVA = Adresse Call
soit
7E410000 + 000407EA = 7E4507EA

Nous devrons donc faire un CALL 7E4507EA afin d’appeler la fonction MessageBoxA.

C’est cool, on sait comment appeler MessageBoxA maintenant, sauf que pour pouvoir l’appeler, il faut d’abord charger sa dll, soit user32.dll, via LoadLibraryA.
Mais pour nous éviter le calvaire de devoir trouver l’adresse de LoadLibraryA, nous allons demander à OllyDBG qui est super fort et qui peut nous donner l’adresse d’une api dont il possède la dll chargée en mémoire dans le processus qu’il exécute.
Il se trouve que LoadLibraryA est contenu dans kernel32.dll et que par chance kernel32.dll est chargée par tout programme s’exécutant sous windows ! Super non ?...
Bon bref, en gros on fait pareil que pour notre shellcode de tout a l’heure on sélectionne un bout de code on le remplit de NOP, et on commence a écrire notre shellcode dans OllyDBG.
Soit :

  1. MOV EAX, pointeur_chaine_user32.dll
  2. PUSH EAX
  3. MOV EAX, LoadLibraryA
  4. CALL EAX
  5. MOV EAX, 0
  6. PUSH EAX
  7. MOV EAX, pointeur_chaine_box
  8. PUSH EAX
  9. PUSH EAX
  10. MOV EAX, 0
  11. PUSH EAX
  12. MOV EAX, 7E4507EA
  13. CALL EAX
  14. MOV EAX, adresse_bouclage
  15. JMP EAX

Télécharger

Pour le moment les adresses importent peu, mettez des 00000000 à la place, on cherche en fait juste à savoir quelle taille fera notre shellcode afin de savoir où l’insérer. Voici ce que j’obtiens sous OllyDBG :

Ce qui donne le shellcode suivant :

B8 00 00 00 00 50 B8 7B 1D 80 7C FF D0 B8 00 00 00 00 50 B8 00 00 00 00 50 50 B8 00 00 00 00 50 B8 EA 07 45 7E FF D0 B8 00 00 00 00 FF E0

Le shellcode fait donc une taille de 46 octets.
Il nous faut trouver un endroit dans notre fichier pour placer ce shellcode.
Et autre détail important, il faut placer ce bout de shellcode après la sauvegarde de l’EIP, car dans le cas contraire, lors de l’appel aux API, la pile serait modifiée car rappellez vous, elle grandit vers le bas, donc elle écraserait sans pitié votre shellcode.
Moi j’ai décidé de le placer comme ceci :

Maintenant, il faut placer les chaines de caractères en dur, comme user32.dll et le message de notre MessageBoxA, pour des raisons de simplicité, j’ai décidé d’utiliser le même message pour le titre et le contenu de la MessageBox. Et dans mon cas, j’ai décidé de mettre « Bonjour le Monde ! ».
Précision importante, il est nécessaire de placer aussi les chaines de caractères en dur derrière la sauvegarde de l’EIP.
Cela permettra de les preserver car la pile sera utilisée par MessageBoxA pour afficher la boite de message à l’écran.
Or, comme la pile grandit vers les adresses basses (elle grandit vers le bas je l’avait dit au début)
Si on place les chaines de caractères à une adresse mémoire plus élevée que celle où pointe ESP, nos chaines ne risquent rien, sinon elles seraient effacées.

Désormais, il faut localiser ces chaines en mémoire afin de récupérer leur adresse et utiliser ces dernières dans notre shellcode.
La technique est simple, on remarque dans l’éditeur hexa que les colonnes et les lignes sont numérotées.
La chaine user32.dll commence à l’offset 90
La chaine Bonjour le monde ! Commence a l’offset A0
Il suffit donc de faire l’addition avec l’adresse du buffer pour obtenir l’adresse de chaque chaine.
La chaine user32.dll commence a l’adresse 00 22 FF 10 + 90 = 00 22 FF A0
La chaine Bonjour le Monde ! commence a l’adresse 00 22 FF 10 + A0 = 00 22 FF B0

Nous devons aussi determiner l’adresse du saut pour le bouclage, nous souhaitons sauter dans les quelques NOPs situés entre la sauvegarde de l’EIP et le shellcode.
l’offset de la ligne des NOPs est 50, disons 55 pour tomber en plein dedans.
L’adresse du saut sera donc 00 22 FF 10 + 55 = 00 22 FF 65
Maintenant on remplace les adresses dans le shellcode.
Ah oui, j’oubliais, pensez aussi à changer la valeur de la sauvegarde du EIP,
Moi j’obtiens ceci :

Ça y est, notre shellcode est enfin prêt et peut être testé !


XIV : Exécutons notre second shellcode !

Hé bien, maintenant, vous pouvez tester votre shellcode !
Votre shellcode est capable de charger en mémoire une librairie de liens dynamiques, mais aussi d’exécuter une API contenue dans cette dll.
Concrètement, les shellcodes sont très puissant, ils sont même meurtriers si on sait les utiliser avec malice.

En effet, c’est grâce à des buffers overflow que se propagent la plupart des vers.

Par exemple sasser, qui était un des plus célèbres virus en son temps ou encore blaster utilisaient ce moyen de contamination, ils se propageaient par le réseau internet et infectaient toutes les machines qui comportaient une faille de buffer overflow au niveau d’un programme serveur qui écoutait sur un certain port. Le vers envoyait ses données ainsi que son shellcode au travers du net en direction du port de la machine à infecter.
Le vers pouvait ainsi injecter son shellcode dans le serveur, le shellcode lui téléchargeait une copie du vers afin de l’exécuter sur la machine à infecter, une fois ceci fait, la propagation continuait.
La grande puissance de ce genre d’attaque est qu’elle est exponentielle.
Plus il y a d’ordinateurs infectés et plus il y aura de nouvelles personnes infectées.


XV : Si vous en êtes ici, c’est que vous avez réussi, félicitation !

Super, vous venez de terminer le tutorial sur les buffers overflow.
J’espère que je vous aurais permis d’apprendre de nombreuses choses à ce sujet et que vous avez pris plaisir à me lire.
Bon courage pour la suite.

Cordialement C@storus.

Documentations publiées dans cette rubrique Documentations publiées dans cette rubrique

© 2010 - 2016
Root Me : plateforme d’apprentissage dédiée au Hacking et à la Sécurité de l’Information