Welcome back to the second iteration of hack this newsletter, where we'll be covering Return-Oriented Programming (ROP).
Last time we covered how to exploit stack-based buffer overflows. I hope you enjoyed working through those challenges since it'll serve as your foundation for learning to ROP. If you haven't read the previous post, you can grab it here.
If you stubled upon this newsletter and want to level up your hacking, consider subscribing to take the red pill and see how far the rabbit hole goes 🐱💻.
Here's what we'll be covering in this post:
Wait, I can't just put shellcode on the stack and execute it?
ROP, how it works
Go Go Gadget Ropper
Resources for Learning
Challenges
Prerequisites
GDB
x86 Assembly
Understanding of basic buffer overflows
Wait, I can't just put shellcode on the stack and execute it?
To execute arbitrary code, you need a place to inject code and a way to execute said code. With a classic buffer overflow, you take advantage of the fact you can place code on the stack, then hijack the instruction pointer to jump to your code on the stack.
Thus, mitigations for arbitrary code execution attacks must stop one of these requirements. One solution implemented in Linux and Windows kernels is a Non-eXecutable (NX) stack, a.k.a Data Execution Protection (DEP) on Windows.
This works exactly like it sounds; it marks the stack as non-executable. If a buffer overflow exists and an attacker can place shellcode on the stack, any attempt to execute the shellcode will fail miserably.
ROP, How Does it Work?
Okay, we can't inject our own shellcode and execute it when NX is enabled. But like a virus, we can adapt our attack to bypass this restriction.
Although we can't execute code on the stack, there has to be executable code somewhere. With that in mind, we have two options: Putting our shellcode somewhere in memory where we can execute code and executing code that already exists in the program's memory.
The first option seems the most doable at first. We'll just find some memory lying around to write our shellcode and jump to that. This is possible, but it's rare to find a memory segment that is both writable and executable in the real world.
We're left with the second option that, as it turns out, is much easier (and cooler, imo). We could try to jump to code that already exists in the program's memory. It might not be clear how this can be done, but it's pretty simple when you understand the return address on the stack is just an address to other executable code. Specifically, this will be the remaining code of the calling function.
That being said, if we can control a return address and find the address of some code we want to execute, we'll have unLImiTeD pOWeR. But how does one find useful code in program memory?
Go Go Gadget Ropper
As I understand it, any instruction address is game when you're choosing where to jump to. However, some brilliant people realized that specific instruction patterns lend themselves to this attack: Gadgets. A gadget is essentially an address to an instruction that, when executed, eventually ends with a return (ret
) instruction.
The ret
instruction will pop the following address at the top of the stack and place it into the instruction pointer rip
for execution to jump to. This allows multiple gadgets to be chained together by placing them in tandem on the stack.
Almost like Lego, you can find these gadgets in program memory and chain them together to write your own program. This is Return Oriented Programming!
Note that your ROP chain will differ between x86 and x86-64 targets due to their different calling conventions.
Here's a visual of how this works:
When we overflow the stack buffer and start writing to the rest of the stack, we can start building our own chain of instructions of return addresses. Here's what's going to happen when we return from the function that overflowed the stack buffer:
1. Gadget 1 will execute
1. pop rdi
will place the stack's top value into the rdi
register, which in this case will be 0x00000001
.
2. Gadget 1 will return via ret
. At that point, the address of Gadget 2 will be the new return address, causing execution to jump to Gadget 2.
This is a pretty useless ROP chain, but we can add whatever and however many gadgets we can find to do useful things like popping shells.
Finding and using gadgets usually involves using tools like ropper or ROPgadget to spit out gadgets and their addresses to construct a ROP chain.
I'd say the easiest way to get a shell is via the ret2libc attack. This usually involves leaking the address of libc if possible, then using a tool like one_gadget to execute gadgets that will give you a shell. However, if you don't have a libc leak, you'll probably need to:
1. Write /bin/sh\\x00
somewhere in read/writable memory.
2. Place the sys_execve
syscall number (59
) in rax
.
3. Place the address of the /bin/sh\\x00
string in rdi
.
4. Place 0x00
in rsi
and rdx
.
5. Call syscall
There's a lot of stuff I didn't cover, but I hope this gives you a good conceptual foundation for building your own ROP chains. Now go forth and ROP!
Resources for Learning
Challenges
I highly recommend Max Kampar's ROP Emporium, which has 8 ROP challenges, starting with the most basic to advanced topics universal ROP.