# Les Stack Buffer Overflow sur Linux
Exploitation d'un Stack buffer overflow sur Linux
Je vais expliquer dans cet article comment exploiter un stack buffer overflow avec shellcode sur Linux.
Introduction ou petit rappel
Un buffer overflow comme son nom l'indique est un débordement de buffer. En programmation C un buffer est un tableau de char, il peut être déclaré de deux façons :
- char buffer[256];
- char *buffer = malloc(256);
Ces deux variables buffer sont des tableaux à 256 cases donc des buffers pouvant contenir 256 caractères au maximum.
Pour bien comprendre la différence entre ces deux types de buffer overflow, il faut d'abord comprendre comment sont géré le code et les variables une fois compilé et exécuté.
Organisation de la mémoire
Quand un programme est exécuté, il est chargé en mémoire dans une partie qui lui est spécialement réservée. Cette partie de mémoire est divisée en plusieurs portions :
- Les Sections
- La Stack
- La Heap
Celons la façon dont une variable est déclarée, global, local, static, constante, initialisée ou non, elle se retrouve dans une de ces portions.
Voici les différents sections :
- La section .data contient les variables globales initialisées ainsi que toutes les static initialisées.
- La section .bss contient les variables globales non-initialisées ainsi que toutes les static non-initialisées.
- La section .rdata contient les variables constantes globales initialisées et toutes les static constantes initialisées.
- La section .text contient les instructions, le code.
La stack contient toutes les variables local et non static ainsi que les arguments de fonctions.
La heap contient toutes les variables allouées dynamiquement avec malloc par exemple.
Un programme vulnérable
Voici la source d'un programme vulnérable que l'on va appeler vuln.c :
|
|
Durant la réalisation de ce tutorial, j'ai compilé vuln.c de cette façon :
|
|
L'erreur ce trouve dans l'utilisation de la fonction strcpy dont le prototype est :
|
|
Cette fonction copie la chaîne de caractère contenu dans la variable "src" dans la variable "dest" sans vérifier si cette dernière en est capable.
La variable "dest" correspond dans notre code à la variable "buffer". C'est donc une variable local à la fonction "vuln" et non static. La variable buffer ce trouve donc dans la stack et peut accueillir 128 caractères au maximum.
Un peu d'assembleur
Quelques notions d'assembleur sont nécessaire pour l'apprentissage d'un stack overflow et encore plus pour la partie shellcode.
En asm il existe des registres, un registre est un espace mémoire dont la taille dépend de votre architecture 16 bits, 32 bits ou 64.
Cet espace est situé à l'intérieur du processeur et est donc très rapide d'accès.
Il en existe plusieurs, en voilà 3 intéressants, les registres en 32 bits et 64 bits "EIP" / "RIP", "ESP" / "RSP" et "EBP" / "RBP" qui sont des pointeurs et contiennent donc des adresses.
- Le registre EIP en 32 bits RIP en 64 est le "Instruction Pointeur", il pointe sur la prochaine instruction à exécuter.
- Le registre ESP en 32 et RSP en 64 bits est le "Stack Pointeur", il pointe sur le dernier élément posé sur la pile.
- Le registre EBP en x86 et RBP en x86_64 est le "Base Pointeur", ou "Frame Base Pointeur", il pointe sur la base d'un "cadre de pile", un "stack frame".
Il est églament intéressant de savoir plusieurs petites choses sur la stack :
Première chose: la stack fonctionne sur le principe LIFO qui signifie "Last In, First Out" donc dernier entré, premier sorti.
Elle est souvent schématisé comme une pile d'assiette, la dernière assiette déposée sera la première à être retirée.
On empile sur la stack avec l'instruction "push" et on dépile avec "pop".
Deuxième chose : si vous avez déjà programmé en C vous devez savoir que les variables locales et non static à une fonction se trouvant donc dans la stack disparaissent à la fin de cette fonction, ceci grâce à ce que l'on appel: le prolog et l'epilog utilisé pour gérer les "Stack Frame".
Dernière chose: la stack croit vers les adresses basses, c'est à dire que plus on va vers le haut de la stack, plus les adresses sont basses.
Disassembling
Ouvrez vuln.exe dans GDB, puis désassemblez la fonction main puis la fonction vuln avec la commande disass
|
|
Dans le code assembleur de la fonction main() désassemblée à l'adresse 0x080484f6 il y a l'appel à la fonction vuln() grâce à l'instruction CALL qui fait deux choses :
- Sauvegarde du registre EIP sur la stack. EIP pointe donc sur l'instruction qui suit le call
- Sauter à l'adresse où commence la fonction vuln()
Dans le code de la fonction vuln() les 2 premières lignes représentent le prolog et les deux dernières l'epilog.
Voici le prolog en détail :
|
|
La première instruction sauvegarde la valeur actuel de EBP sur la stack, puis la seconde crée le stack frame de la fonction vuln().
Le stack frame est en fait un espace sur la stack qui contient tout ce qui est nécessaire pour le fonctionnement de la fonction, chaque fonction a son stack frame.
L'epilog en détail :
|
|
L'epilog est connu aussi sous cette forme :
|
|
L'epilog fait l'inverse du prolog. Il détruit le stack frame de la fonction, les variables local et non static créées dans la fonction sont donc perdu. Ensuite, il redonne au registre EBP la valeur qu'il avait au départ, celle que le prolog à empiler et pour finir l'instruction RET redonne à EIP la valeur que le CALL à sauvegarder sur la pile. La prochaine instruction sera donc celle ce trouvant à cette adresse.
Principe et logique
Dans GDB, exécutez vuln avec comme but de créer un débordement sur le buffer. Donnez donc à titre de test 200 'a' comme argument.
La fonction strcpy() va copier ces 200 'a' dans buffer alors qu'il peut en contenir 128 au maximum, il va donc y avoir un débordement.
GDB affichera le message : "Program received signal SIGSEGV, Segmentation fault." et la valeur de EIP sera : "0x61616161 in ?? ()".
|
|
Comme nous le voyons dans la sortie de GDB, les adresses contenues dans EBP et EIP valent "0x61616161".
0x61 correspond au caractère 'a' en hexadecimal, nous avons donc réussi grâce au débordement à overwriter les valeurs de EBP et de EIP par des 'a'.
Ceci parce que le remplissage de buffer ce fait du haut de la stack vers le bas de la stack, donc des adresses basse vers les adresses haute et EBP et EIP ont été empiler avant le remplissage de buffer, ils ce trouvent donc dans le bas de la stack.
Ce qui fait planter le programme c'est qu'il n'y a aucune instruction à l'adresse 0x61616161.
Il nous faut maintenant déterminer exactement combien de 'a' sont necessaire pour réécrire EIP, car le but étant d'y mettre une adresse pour sauter dans un espace ou nous aurons placé un code arbitraire destiné à faire ce que nous voulons, ce code est appelé "shellcode".
Une adresse représente 4 octets, donnez alors maintenant à vuln dans GDB 128+4 'a' pour remplir buffer et overwriter EBP + 'bcde' pour overwriter EIP. "bcde" vaut "0x62636465" en hexadécimal :
|
|
EIP est bien écrasé par "bcde" (0x65646362)
Exploitation
Une exploitation classique consiste à faire exécuter un shellcode, il faut donc overwriter EIP avec une adresse où on trouve ce shellcode.
Pour y parvenir, il existe plusieurs technique.
Voyons une technique, exploitant la fonction strcpy() qui pour rappel retourne comme valeur un pointeur vers le buffer de destination.
En assembleur, les retours de fonction se trouvent dans le registre EAX
Nous pouvons donc avoir un plan d'exploitation comme celui-ci : shellcode + padding + address vers un call eax.
Pour trouver ce call eax, je vais utiliser objdump :
|
|
Nous pouvons overwriter EIP avec l'adresse 0x08048403.
Dans mon exemple le shellcode exécute un shell et fait 36 bytes.
|
|
Nous avons bien obtenu un shell... Nous avons donc réalisé un Stack Buffer Overflow \o/