Return-Oriented Programming (ROP)

Created On 28. Jun 2021

Updated: 2021-06-28 02:33:19.566284000 +0000

Created By: acidghost

As mentioned in Shellcoding and Memory Errors, in modern systems most binaries will be protected against execution. By default, the code in the stack and heap are not executable. If the code can't be injected and executed, then often the concept of code reuse is applied.
Code reuse was firstly observed with the return-to-libc vulnerability. With buffer overflows, the return address and the arguments can be overwritten. With return-to-libc, it was when initially when with stack overflow a vulnerable function would be returned where custom arguments could be passed. However, that was quite some time ago and nowadays with all the current mitigations it might be slowly dying, because modern architectures don't take arguments on the stack. However, this is not the end game.

Code reuse in AMD64

In Memory Errors and Core Dump, we reused part of a function by overflowing the buffer and jumping to win. We didn't need to use the 1337 argument if we controlled the return address completely which allowed to jump right past the check. With ROP the program's own code is used against it.

Binary Lego

In the program below the main function takes 16 bytes and reads up to 128, after which jumps to foo and from there to bar. open("/flag", 0); won't be executed, since the function returns to bar where a different file /notflag is read with sendfile.

int main() {
	char name[16];
	read(0, name, 128);
}
int foo() {
	return open("./flag", 0);
}
int bar() {
	int x = open("/notflag", 0);
	sendfile(1, x, 0, 1024);
}

compile with

$ gcc -fno-stack-protector -no-pie -o rop-ex rop-ex.c

Vulnerability

To exploit this program, let's disassemble it first and check the main function.

objdump - M intel -d rop_easy

The stack frame is 16 bytes in size (at sub rsp, 0x10). We have the returned base pointer and the saved return address at the end. The goal is the overwrite all these and then fill in the saved return address where we want to jump to. See the foo function:
small

Here we will want to jump to the open call. After that, we would need to fill some value that will be poped into rbp. Further let's see the bar function:

After this we would want to return to sendfile, that will read our /flag. We can use pwntools craft the exploit in such way:

import pwn
p = pwn.process("./rop-ex")
p.send(
        # here we fill up the "name"
	b"A"*16 + 
        # here we overwrite the saved base pointer in main's stack frame
        b"B"*8 + 
        # here we are overwriting the saved return address to point to foo's open call
        pwn.p64(0x4011ac) +
        # this will get popped by foo into rbp before it returns
        b"C"*8 + 
        # return to the sendfile call in bar
        pwn.p64(0x4011ec)
)
p.readall()

Return Oriented Programming

Initially the code of a program is reused by controlling the return address as in the example above. However, in ROP, the specific return address will be chained to a gadget, which is a set of instructions. When a gadget returns, it can return to any address, which can be the next gadget as well. Like this, arbitrary actions can be triggered in a ropchain. Instructions are chained using ret instruction. This is often called Weird Machine, because of the act how something as accidental turing completeness gets achieved from existing meta-instructions in memory.

Techniques

In some programs, you might find a lot of useful gadgets. However, sometimes there might be only one that is usable. What do we do then?

Resources on the stack

__________________________________________________________

| gadget address 1 | target address | address 3 | target | address 4
__________________________________________________________

If we know where the ropchain is on the stack, we can include such resources as 'target' on the stack. However, how to prevent the 'target' string from being interpreted as gadget addresses?
Some gadgets might unbreak the ropchain, however this is no problem. There are some fix up gadgets that for example are created with the use of addresses that aren't needed. Let's see a few.

  • pop r12; pop rdi; pop rsi; ret - skip until ret
  • add rsp, 0x40; ret - de-allocate hex 40, a function that has a stack frame of 40 bytes, at the beginning of the func subtracts 0x40 from rsp and at the end ads them back on (can be used to skip over garbage gadget).

Store a value in a register-popping gadget for example with:

  • "gadget address": pop rax; ret - this will pop rax off the stack then return to gadget 2. Rax will be set now.
__________________________________________________________

| gadget address | desired rax value | gadget address 2 | target | gadget address 4
__________________________________________________________

Common and rare gadgets

Some of the most common gadgets are:

  • ret - similar to nopsled
  • leave; ret - leave does mov rsp, rbp; pop rbp (so rbp has to point to an address that makes sense, otherwise it will be corrupted)
  • pop REG; ret - restore callee-saved registers before returning
  • mov rax, REG; ret - setting the return value before returning

A rare gadget would be lea, where addresses might be stored into registers. lea gadgets are rare because they are usually at the beginning of a function and not at the end, where it is needed for gadgets to be. What can be done?

  1. push rsp; pop rax, ret - will get the stack address into rax
  2. add rax, rsp; ret - will get rsp into rax
  3. xchg rax, rsp; ret - swap rax and rsp DANGEROUS

When the stack address is known, later gadgets can dynamically compute the necessary addresses on the stack instead of having them hard coded. Then a stack leak might not be needed.

  • Stack Pivot

A stack pivot is used when no appropriate gadgets can be found in a chain. An example is:

xchg rax, rsp
ret
pop rsp
... 
ret

This will pivot the stack to point somewhere else. Imagine, a chain of gadgets is linear:

__________________________________________________________

| pop rsp | *some argument* | ret |
__________________________________________________________

However a stack pivot will "break" the linear flow and will continue somewhere else from where all the other gadgets are pointing.

__________________________________________________________

| xchg rax, rsp | ret | pop rsp | *some argument* is skipped since rsp is pointing somewhere else | ret |
__________________________________________________________
  • Data Transfer

Ropchains need sometimes to move data around. Below is a common gadget that applies this:

add byte [rcx], al
pop rbp
ret

This requires the gadget to set rcx and al. One problem is that gadgets that set rcx are extremely rare, however, following a similar pattern, there might be other helpful gadgets that might allow data transfer. While this is not used often, if the stack is too limiting, it can be used to build another parallel ropchain.

  • Syscalls

To get syscalls in normal programs might be tricky. They are rare in such programs and one workaround is to call library functions instead. Remember, when the ropchain starts, there are a lot of useful addresses everywhere in the code, heap, stack in registers and all over the stack.

  • Magic Gadget

If we are lucky we might be able to jump partway into system() and trigger /bin/sh. When system() runs it has to set up a call execve("/bin/sh", {"/bin/sh", "-c" command}, env);. Or even do more! For example trigger execve(something); and create something file that reads the data. This location in system() is called the magic gadget.

ROP Tools


listing gadgets with rp++
There are few tools available that can help list gadgets. One issue is that these tools might not always list all of the more complex gadgets because they have specific requirements how the gadgets have to be positioned, however they are still great to quickly see what is available in a binary.

rp++

rp++ is a nice tool to find gadgets. It can be run with rp++ -f ./file -r2. Using regex to filter appropriate gadgets becomes very useful as well and with rp++ it can be used the following way to filter the ones that are needed rp++ -f ./file -r2 | grep -P "pop rdx" - in the following example we are looking for gadgets that contain pop rdx.
Other tools are ropper, xrop and one gadget.

ROPing with Pwntools

Pwntools has a nice functionally to work with gadgets. After loading up a file that is recognized as process a rop object can be created with

rop = pwn.ROP(process.elf)

Pwntools recognizes simple gadgets as pop rax or ret for instance. It will list the gadget if the process is followed up by a register.
smallx1
Another way to list gadgets is by looking for a chain with two instructions in such way:

rop.find_gadget(['pop rbp', 'ret'])

Then if the process has a gadget that contains pop rbp; ret it will output something as:

Gadget(0x00000, ['pop rbp', 'ret], ['rbp'] 0x8)

Rop is similar to shellcoding, with the difference that the commands run in a ropchain. If the process has the corresponding gadgets an exit can be triggered as easy as:

process.write(b'a'*{distance_to_input_buffer} + pwn.p64(rop.rax.address) + pwn.p64(60) + pwn.p64(rop.rdi.address) + pwn.p64(42) + pwn.p64(rop.syscall.address))

This will load 60 as argument for exit into rax (assuming there will be a pop rax; ret) and the argument 42 into rdi (assuming there is a pop rdi; ret) and then chain the instructions together with a syscall, which will instruct the process to exit with a code 42. See more on how to rop with pwntools here https://docs.pwntools.com/en/stable/rop/rop.html

Common Issues

With limited overflow size and inability to input NULL bytes, the stack will be very limiting. However we can still trigger one gadget! This is were the magic gadget might come in handy.

  • ASLR

In Memory Errors, we've seen how ASLR can cause different issues. One technique around this, is to loop the program and as long it stays alive, little by little information can be leaked. Let's say we loop it to the setup of the read call and since the state of the program might be corrupted, with each loop it might read more data and allow larger ropchains. Another thing is in case there is any disclosure, an address might get leaked. A stack pivot might used as well to point somewhere else. Also don't forget, you never know if you can find the magic gadget :wink:

  • Stack Canaries

This one is a ROP Nightmare! See more in Memory Errors.

Anti-ROP Techniques

There are more quite effective academic solutions against ROP. However, they are rarely used in real world applications.

  • removing ROP gadgets

G-FREE: Defeating Return-Oriented Programming through GADGET-less Binaries
While this might be an effective technique, it still follows to be seen where it actually was deployed outside of the initial academic paper. The reason is simple. Removing all or a big part of ROP gadgets probably requires a separate operations team who would be polishing the stripped software? Honestly, no idea, but it does sound like a very big headache :confused:

  • detecting ROP in progress

kBouncer: Efficient and Transparent ROP Mitigation - used in Windows
ROPecker: A Generic and Practical Approach for Defending Against ROP Attacks
To bypass a ROP in progress, a stealth technique might be used to trick the system that a ROP is not happening.

The idea of control flow integrity is whenever a hijackable control flow transfer occurs, make sure its target is something it's supposed to be able to return to. This triggered a race with Counter-CFI techniques:

B(lock)OP

The idea is to ROP but on a block level with larger chunks of the program.

J(ump)OP

Use indirect jumps instead of returns to control execution flow.

C(all)OP

Use indirect calls instead of returns to control execution flow.

S(ignareturn)ROP

Use sigreturn syscall instead of returns.

D(ata)OP

Carefully overwrite the program's data to puppet it instead of hijacking control flow.

  • Intel Edition

In September 2020 a measure that applies Control Flow Integrity was released in processors with Control Flow Enforcement Technology (CET). It adds the endbr64 instruction. This forces all indirect jumps to end with an enbr64 instruction or the program will terminate. However, this can be still bypassed by more advanced ROP techniques such as BOP and SROP.

References

https://pwn.college/modules/rop
https://codearcana.com/posts/2013/05/28/introduction-to-return-oriented-programming-rop.html
https://nebelwelt.net/blog/20160913-ControlFlowIntegrity.html
https://docs.pwntools.com/en/stable/rop/srop.html

Section: Binary Exploitation (PWN)

Back