Just a quick writeup for the elysium (200pts) challenge from the InsomniHack Teaser CTF 2015. We ended up making the 6th place during the CTF :)

We’re given a binary which, supplied with a port number, binds to that port and listens for incoming connections. Upon receiving an incoming connection it speaks a custom protocol with the client.

> file elysium
elysium: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=b1730cced4284ad9a58d8948eaa55eb3beaccd90, not stripped
> checksec elysium
RELRO           STACK CANARY      NX            PIE             FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     elysium

After spending some time reversing the protocol I wrote a small client to communicate with the server. Here’s a sample interaction with the server:

Armadyne robots controller
1) Get informations <name>
2) List units
3) Add medical units <count>
4) Add military units <count>
5) Add social units <count>
6) Add spy units <count>
7) Add shields units <count>
8) Add communication units <count>
9) Add transport units <count>
a) Add computer units <count>
*) Exit
> 2
Total units : 2925
Medical units : 320
Military units : 75
Social units : 630
Spy units : 227
Shield units : 329
Communication units : 250
Transport units : 312
Computer units : 782
Available units : 5267
1) Get informations <name>
2) List units
3) Add medical units <count>
4) Add military units <count>
5) Add social units <count>
6) Add spy units <count>
7) Add shields units <count>
8) Add communication units <count>
9) Add transport units <count>
a) Add computer units <count>
*) Exit
> a 100
Computer units added
1) Get informations <name>
2) List units
3) Add medical units <count>
4) Add military units <count>
5) Add social units <count>
6) Add spy units <count>
7) Add shields units <count>
8) Add communication units <count>
9) Add transport units <count>
a) Add computer units <count>
*) Exit
> 2
Total units : 3025
Medical units : 320
Military units : 75
Social units : 630
Spy units : 227
Shield units : 329
Communication units : 250
Transport units : 312
Computer units : 882
Available units : 5167

Bug #1

Notice the “Get informations” option in the menu? This let’s us read a file relative to a folder called ‘Elysium_citizenship_authorization’ in the current directory. Of course it does not check for path traversal so we can dump any file we want with this.

This is especially interesting since reading /proc/self/maps will allow us to bypass most of the ASLR of the target process (except the stack internal randomization). Also we can now retrieve the remote libc and get important offsets from there.

Bug #2

In the cipher_recv function we see this:

cipher_recv

‘decrypted_buffer’ is our decrypted message and ‘data_buffer’ is returned to the caller (contains just the user data without the hash). ‘cmp’ verifies that the given hash matches the other one given as hex string.

There’s a bug in there, did you spot it?

The programmer expected the first part of the message (the sha1 hash) to always be 40 characters in size. Since we fully control the position of the ‘:’ in the payload we can thus overflow the ‘hash_buffer’ which is located on the stack.

Exploitation of this bug turned out not to be so easy. Here’s why: See the two calls to memset() and free() above? The first argument to those functions lies on the stack as well, between the buffer we’re overflowing and the return address. Here’s the stack layout of the cipher_recv function:

.text:000016C2 var_6D           = byte ptr -6Dh
.text:000016C2 hash_buffer      = byte ptr -45h         # we're overflowing this one
.text:000016C2 buf              = byte ptr -1Ch
.text:000016C2 decrypted_buffer = dword ptr -18h        # get's freed
.text:000016C2 recv_buffer      = dword ptr -14h        # get's freed as well
.text:000016C2 var_10           = dword ptr -10h
.text:000016C2 data_buffer      = dword ptr -0Ch
.text:000016C2 socket           = dword ptr  8
.text:000016C2 len_ptr          = dword ptr  0Ch
.text:000016C2 decrypter        = dword ptr  10h

Now, free() will basically kill the process for any argument that’s not a valid pointer to a heap chunk or NULL (from the man page: “If ptr is NULL, no operation is performed.”).

The binary does some heap randomization after receiving a connection, preventing us from easily knowing the offsets of heap chunks from the start of the heap. However, it is possible to bypass this since the binary uses srand(time()) to initialize the prng, enabling us to calculate the same random number sequence as well. See this exploit for an example on how to do this.

I actually missed that fact during the CTF and so went another route:

Besides /proc/self/maps there is another nifty file in /proc:

   /proc/[pid]/syscall (since Linux 2.6.27)
        This file exposes the system call number and argument registers for the system call
        currently being executed by the process, followed by the values  of
        the stack pointer and program counter registers.  The values of all six argument 
        registers are exposed, although most system calls use fewer registers.

There’s a pointer to userland esp in there! Neat! That enables us to bypass the stack internal randomization and calculate exactly at which address the buffer we’re overflowing is located. At this point we’re able to bypass the two free()s by pointing each pointer to itself and pointing the length pointer to 0x00000004, causing memset to overwrite it’s argument pointer with zero. The subsequent call to free() will then become a nop.

At this point we have EIP control and can return into a little gadget to adjust the stack past the length pointer, then return into system() with the first argument pointing to our payload string on the stack (whose address we now know). I used a quick and dirty reverse shell payload for my exploit, see @OwariDa’s exploit for a better way to do this :)

Find the full exploit here.

> ./exploit.py 
[*] got target memory layout
[*] stack buffer @ 0xbf8c25a3
[*] controlled data @ 0xb7710104
[*] system() @ 0xb73c3100
[*] sending final payload...
[*] done, check your listener

And, in a different shell:

> nc -vl 0.0.0.0 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from [54.76.18.92] port 4444 [tcp/*] accepted (family 2, sport 54559)
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1001(esylium) gid=1001(esylium) groups=1001(esylium)
$ ls
elysium
Elysium_citizenship_authorization
reboot
$ ./reboot

--------------------------------------------------------
                 System Reboot >>> 5
--------------------------------------------------------

--------------------------------------------------------
                 System Reboot >>> 4
--------------------------------------------------------

--------------------------------------------------------
                 System Reboot >>> 3
--------------------------------------------------------

--------------------------------------------------------
                 System Reboot >>> 2
--------------------------------------------------------

--------------------------------------------------------
                 System Reboot >>> 1
--------------------------------------------------------

--------------------------------------------------------
                 System Reboot >>> 0
--------------------------------------------------------

--------------------------------------------------------
            INS{reb00t_R0pch4ain_FTW}
--------------------------------------------------------
$ 

Now all that’s left to do is to watch the movie some time … ;)