Exploitation de Buffer Overflows

Introduction

Je me suis intéressé dernièrement à l’exploitation de vulnérabilités type « buffer overflow ».

Les « buffer overflows », ou en français « dépassement de tampons » est un bug dans un programme: des données sont écrites dans un espace mémoire, mais l’espace mémoire est en réalité trop petit pour contenir toutes les données, par conséquent, ces données sont écrite au delà de l’espace mémoire du tampon.

le dépassement de mémoire tampons peut survenir dans plusieurs zone de la mémoire, en particulier la stack, la heap et la mémoire statique.

Cette article va, dans un premier temps, s’intéresser aux dépassements de tampon sur la pile mémoire (Stack-Based buffer overflows). car ils sont plus simples et plus efficaces à exploiter que les autres types de dépassements de tampon.

Github: https://github.com/gaspard-v/bufferoverflows-exemple.git

Stack-Based Buffer Overflows

La pile mémoire

Le dépassement de tampon de la pile mémoire survient à un endroit très important de la mémoire d’un ordinateur: la pile (stack).

Pour comprendre l’importance de cette zone mémoire, il faut comprendre à quoi elle sert.

La pile sert à stocker les adresses de retour des fonctions.
Les piles mémoires plus avancées stocks plus de données, comme des variables, des tampons mémoire, les paramètres de fonctions ou encore les valeurs retournées.

Prenons par exemple ce morceau de code C

int x = 0;

void addition(int i, int j)
{
  x = i+j;
  return;
}

int main()
{
  int i = 10;
  int j = 20;
  addition(i, j);
  return 0;
}

ce code fait tout simplement une addition de deux nombre, et met se résultat dans la variable globale « x ».

Maintenant, nous allons remplacer le noms de ces fonctions par leur adresse. Par soucis de simplicité, les adresse seront les numéros de ligne, et modifier l’instruction « return » par un équivalent « jump »

int x = 0

void 0x00000003(int i, int j) // fonction addition
{
  x = i+j;
  goto 0x00000014; //on saute à la 14eme ligne
}

int 0x00000009() //fonction main 
{
  int i = 0;
  int j = 0;
  0x00000003(i, j);
  return 0; // 14eme ligne
}

l’instruction return peut être comparé à une instruction « jump » ou « goto ». à la différence que « jump » et « goto » saute à une zone mémoire fixe et déjà connu à l’avance.
Ce n’est pas le cas de return, car celui-ci saute à un endroit de la mémoire qui peut changer.

int x = 0

void 0x00000003(int i, int j) //fonction addition
{
  x = i+j;
  goto ??; //deux fonction appelle la fonction addition, ou doit il jumper pour reprendre le cours normal de l'exécution ?
}

int 0x00000009() //fonction truc
{
  int i = 0;
  int j = 0;
  0x00000003(i, j);
  return 0; // 14eme ligne
}

int 0x00000017() //fonction machin
{
  int k = 12;
  int l = 87;
  0x00000003(k, l);
  return 0; //22eme ligne
}

Cette exemple illustre le problème, deux fonctions appelle la même fonction. on ne peut pas sauter à une adresse fixe.
Une des solution est de créer une nouvelle variable.

int x = 0
int adresse_retour = 0;

void 0x00000004(int i, int j) //fonction addition
{
  x = i+j;
  goto adresse_retour ; //problème résolu !
}

int 0x00000010() //fonction truc
{
  int i = 0;
  int j = 0;
  adresse_retour = 0x00000016 //ici on spécifie la 16eme ligne.
  0x00000004(i, j);
  return 0; // 16eme ligne
}

int 0x00000019() //fonction machin
{
  int k = 12;
  int l = 87;
  adresse_retour = 0x00000025 //ici on spécifie la 25eme ligne.
  0x00000004(k, l);
  return 0; //25eme ligne
}

Voila ! la pile mémoire fonctionne donc plus au moins comme ça.

Cependant, ce n’est pas une unique variable qui est utilisée pour stocker les adresses de retours, mais plutôt une enchainement de variables sur un pile en mode LIFO (Last In First Out, Dernier Arrivé, Premier Sorti).

schéma d’une pile mémoire

les lettres représente les adresses mémoire.

Sur nos ordinateurs et nos smartphone, la stack ne fonctionne pas exactement comme cela. D’autre données sont empilées sur la stack, comme par exemple les variable déclaré dans la fonction.
Ces variables ne peuvent être accédées uniquement pendant que la fonction s’exécute.

Le principe du stack based buffer overflow est donc de remplir un tampon qui est sur la stack jusqu’à ce qu’on écrase l’une des sauvegarde des adresses mémoires.

Code vulnérable

Le C/C++, ainsi que l’assembleur sont parfait car ils ne vérifient pas la taille des buffers. C’est principalement à cause de ces langages que l’exploitation de vulnérabilités existent.

Les failles les plus courantes sont: le dépassement de tampons, l’use after free, la confusion de type. Tous ces failles sont extrêmement dangereux car ils permettent, d’une manière ou d’une autre, de réécrire une adresse dans la mémoire.

Laisser un commentaire