Fonctionnement de la heap

Comment est gérée la heap ?

Posté par Matthieu le 14 juillet 2019

Bonjour à tous,

Suite à mon article précédent traitant l'exploitation de binaires (merci pour les retours positifs), j'ai eu quelques remarques quant aux prérequis nécessaires pour aborder cet article. Eh oui, je me suis permis de faire un article traitant d'un sujet d'un niveau "avancé" sans parler des bases préalablement. Si pour vous "Stack", "Heap" et "Overflow" sont similaires à des incantations macabres de sataniste, je vous redirige vers les articles de Pixis qui traite tout ça dans son super blog HackNDo.

Cependant, j'ai décidé de retourner aux bases en vous parlant de la heap, nous verrons ici :

  • Qu'est ce que la heap ?
  • Comment sont gérées les donnés dans la heap ?
  • Perspectives d'exploitation

Qu'est ce que la heap ?

Comme un schema vaut mieux qu'un long discours :

En effet, la heap est une zone mémoire tout comme la stack, elle n'a pas de taille fixe et grossit par le haut (donc des adresses de plus en plus basses). Contrairement à la stack, elle est directement gérée au bon vouloir du développeur (via des appels malloc() , calloc() , ...) il choisit d'allouer ou de libérer des blocs à son bon vouloir.

Pour résumer simplement, la heap est une zone mémoire permettant de stocker de la donnée.

Elle a cependant son mode de fonctionnement bien à elle, plus complexe que la stack ...

Comment sont gérées les donnés dans la heap ?

Afin d'expliquer le fonctionnement des allocations mémoires dans la heap, j'ai jugé préférable de créer un programme que voici :

#include stdlib.h
#include string.h
#include stdio.h

struct data{
char name[64];
};


int main(int argc, char **argv)
{
struct data *d;
d = malloc(sizeof(struct data));
printf("Ma structure data est a : %p \n", d);
strcpy(d->name, argv[1]);
return 1;
}

Comme vous pouvez le voir, j'ai défini une structure du nom de data, je la stocke dans la heap et je copie l’argument de mon programme directement dans l'espace que j'ai alloué sur la heap. Je vous invite maintenant à regarder comment ça se passe de plus près :

malloc() alloue size octets, et renvoie un pointeur sur la mémoire allouée. Si size vaut 0, malloc() renvoie soit NULL, soit un pointeur unique qui pourra être passé ultérieurement à free() avec succès.

malloc() est actuellement notre centre d’intérêt, il retourne un pointeur vers la zone mémoire qui a été allouée, regardons maintenant ce qui se trouve réellement dans cette zone mémoire

gdb-peda$ b* main
Breakpoint 1 at 0x8049182
gdb-peda$ r toto
Starting program: /root/heap/heap toto

Nous posons un point d'arrêt logiciel sur la fonction main() (b* main) et nous lançons le programme avec pour argument "toto" (r toto)


0x80491a5 : push 0x40
=> 0x80491a7 : call 0x8049050

Voici le fameux call malloc(), il prend comme argument 0x40 ce qui est cohérent car il s'agit de la taille du buffer destiné à "name". Une fois le call effectué, eax vaut 0x804d160 ce qui correspond au pointeur vers l'espace alloué (rappelez-vous du man !).

Poursuivons notre exécution jusqu'à arriver au strcpy().

Allons voir ce qui se trouve dans cette zone :


gdb-peda$ x/w 0x804d160
0x804d160: 0x4f544f54

Nous retrouvons notre "TOTO" (en hexadécimal : 4f544f54). Mais ce n'est pas tout, comment la heap connait-elle la taille du chunk qui est alloué à cette structure ? Et bien remontons un petit peu :

gdb-peda$ x/3w 0x804d160-0x4
0x804d15c: 0x00000051 0x4f544f54 0x00000000

D'où sort ce 0x51 ? En réalité il s'agit de la taille du chunk alloué pour notre structure, nous pouvons vérifier cette information en regardant le code source de la libc qui définit un chunk mémoire dans la heap de cette manière :

struct chunk {
int prev_size;
int size;
struct chunk *fd;
struct chunk *bk;
};

un int est dédié à définir la taille de notre chunk (ici 81). De plus, si nous regardons 81 octets plus loin dans la heap, nous remarquons que la zone est en effet allouée pour quelque chose d'autre :

gdb-peda$ x/3w 0x804d160+0x51
0x804d1b1: 0x74732061 0x74637572 0x20657275

Afin de confirmer notre travail jusqu’à présent, j'ai modifié le programme de sorte qu'il fasse deux allocs et deux strcpy dans le but de voir le résultat dans la heap :


gdb-peda$ x/100w 0x804d150
0x804d150: 0x00000000 0x00000000 0x00000000 0x00000051
0x804d160: 0x4f544f54 0x00000000 0x00000000 0x00000000
0x804d170: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d180: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d190: 0x00000000 0x00000000 0x00000000 0x00000000
0x804d1a0: 0x00000000 0x00000000 0x00000000 0x00000051
0x804d1b0: 0x42424242

Nous avons bien notre deuxième allocation qui s'est effectuée 0x51 octets plus loin ...

Bonus : Villoc

Pour ceux du fond qui n'auraient toujours pas compris le fonctionnement de la heap, je pose mon dernier joker sur la table en vous présentant Villoc[1], un outil qui m'a été proposé par Geluchat. Cet outil python permet de suivre l'évolution de la heap de manière graphique, pratique pour les exploits de type Heap overflow.

l'outil s'utilise simplement de cette manière :


ltrace ./target |& villoc.py - out.html;

Voici le résultat obtenu quand on l'utilise sur mon petit binaire d'exemple :

On voit bien les deux appels à malloc() et l'état de la heap après ces appels

Perspective d'exploitation

Tout comme les buffer overflows classiques sur la stack, nous rencontrons ici la même problématique : en cas d'absence de contrôle, nous pouvons continuer à écrire dans la heap et écraser ce que bon nous semble! Même si , contrairement à la stack, nous ne pourrons pas contrôler systématiquement le flow d’exécution du programme, il existe tout de même certains cas où nous pourrions nous servir de cette vulnérabilité pour parvenir à nos fins. Protostar propose une série d’exercices portant sur l'exploitation de la heap, si les retours sont bons nous pourrions nous intéresser à ces challenges sur le blog d'inf0sec.fr

Merci d'avoir suivi cet article, je suis disponible sur twitter (@inf0sec1) pour d’éventuelles questions.


Happy Hacking

[1] Villoc : https://github.com/wapiflapi/villoc
36 Coeur(s)