Return-oriented programming (ROP) is the answer to “what do you do when the stack is no longer executable?” When Data Execution Prevention (DEP) marks stack and heap pages non-executable, shellcode injection stops working. ROP works around this by not injecting new code at all. Instead, it chains together snippets of code that already exist in the process (called gadgets), each ending in a ret instruction, using a carefully crafted stack to steer execution from one snippet to the next.
The technique was formalized in Hovav Shacham’s 2007 paper “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86)” . Prior work on return-into-libc had shown you could redirect execution to whole library functions; Shacham showed you could Turing-complete the process by chaining arbitrary gadgets.
Background you need first#
Stack-based buffer overflows#
The classic memory corruption: a function writes past the end of a fixed-size stack buffer, overwriting the saved return address and giving the attacker control of what happens after the function returns.
#include <stdio.h>
#include <string.h>
void foo(char *str) {
char buffer[64];
strcpy(buffer, str); // no bounds check
}
int main(int argc, char *argv[]) {
if (argc != 2) return 1;
foo(argv[1]);
return 0;
}Compile without modern hardening (-fno-stack-protector -no-pie -z execstack for a fully vulnerable binary) and this is exploitable with straight shellcode injection. With modern defaults, you get stack canaries, PIE, DEP, and ASLR, and shellcode injection stops working. ROP is one answer.
DEP (NX)#
Data Execution Prevention (called NX on non-Windows platforms) marks memory pages as either writable or executable, not both. Stack and heap default to non-executable, code sections default to non-writable. Direct shellcode injection into a buffer stops working because when execution reaches the buffer, the CPU refuses to execute the bytes there.
ASLR#
Address Space Layout Randomization shuffles the base addresses of the stack, heap, and loaded libraries between process runs. Without ASLR, you can hardcode system() at a known libc address. With ASLR, that address changes each run, so you need either an information leak to disclose the current layout or a bypass technique that doesn’t require knowing absolute addresses.
DEP and ASLR compose: ASLR makes it hard to find your gadgets, DEP makes it hard to inject new code. Modern exploit chains routinely defeat both, but each additional mitigation shrinks the attack surface.
What a gadget looks like#
A gadget is a short sequence of instructions ending in ret. The ret is what makes chaining possible: it pops the next address off the stack and jumps there, so the next gadget can go on running from a controlled stack.
0x08048400: pop eax
0x08048401: retTo use this gadget, the crafted stack needs the gadget’s address where the vulnerable function’s saved return address used to be, followed by the value you want in eax:
+------------+
| 0x08048400 | <- overwritten return address (gadget)
+------------+
| 0xDEADBEEF | <- value popped into eax
+------------+When the vulnerable function’s ret executes, control jumps to 0x08048400. pop eax loads 0xDEADBEEF into eax. The next ret pops the following stack entry and jumps there, ready to run the next gadget.
Chaining gadgets#
Real exploits chain multiple gadgets to set up register state and then trigger a syscall. The stack layout for a two-gadget chain that loads eax and then ebx:
Gadget 1 (0x08048400): pop eax ; ret
Gadget 2 (0x08048410): pop ebx ; ret+------------+
| 0x08048400 | <- Gadget 1 (pop eax)
+------------+
| 0xDEADBEEF | <- popped into eax
+------------+
| 0x08048410 | <- Gadget 2 (pop ebx)
+------------+
| 0xBAADF00D | <- popped into ebx
+------------+Real chains include gadgets for arithmetic, memory access, stack pivots (redirect the stack pointer to a controlled buffer), and register clearing. The goal is typically to set up a syscall or a call to a useful library function like system(), execve(), or mprotect().
Bypassing DEP#
DEP marks pages as non-executable. ROP defeats DEP by not needing to execute new code. Every gadget is in an existing executable page (usually libc or the binary itself), so the CPU is fine with executing them.
A common pattern: use ROP to call mprotect(addr, len, PROT_READ|PROT_WRITE|PROT_EXEC) to make a controlled page executable, then jump into it and run traditional shellcode. This is a hybrid that uses ROP briefly to defeat DEP, then falls back to shellcode.
Bypassing ASLR#
ROP alone doesn’t defeat ASLR. You need one of:
Information leak. A separate vulnerability (format string, uninitialized memory read, out-of-bounds read) that discloses a runtime address. Given one libc address, you can compute all other libc addresses because ASLR randomizes the base but not the internal layout.
Partial overwrite. Overwrite only the low bytes of a return address, since ASLR usually randomizes only the upper bits (the low 12 bits are page-aligned and unchanged). Works when the gadget you want is on the same page as an existing return target.
Non-ASLR modules. If any loaded library or the main binary is not PIE (position-independent), its addresses are fixed. Gadgets from that module work regardless of ASLR. Modern distributions build with PIE by default; older commercial software often does not.
Brute force. On 32-bit systems, the ASLR entropy is often small enough (8-16 bits) that guessing is feasible if the target restarts on crash (like a forking server). Not viable on 64-bit systems where ASLR entropy is 20+ bits.
Finding gadgets#
Modern tooling automates gadget discovery. The three most-used tools:
ROPgadget (widely used):
ROPgadget --binary target_binaryOutput looks like:
0x08048400 : pop eax ; ret
0x08048410 : pop ebx ; ret
0x08048420 : add eax, ebx ; ret
0x08048430 : int 0x80Ropper (Python, active development):
ropper --file target_binary --search "pop rdi"
ropper --file target_binary --chain "execve cmd=/bin/sh"Ropper’s --chain mode automatically generates chains for common goals, which speeds up exploit prototyping.
pwntools’ ROP module (Python library, the modern exploit development workhorse):
from pwn import *
context.binary = ELF("./target_binary")
libc = ELF("./libc.so.6")
rop = ROP([context.binary, libc])
rop.execve(next(libc.search(b"/bin/sh\x00")), 0, 0)
print(rop.dump())
payload = b"A" * 72 + rop.chain()pwntools’ ROP class understands calling conventions, ABIs, and gadget dependencies, which cuts the boilerplate for common patterns.
one_gadget finds “one-shot” gadgets in libc that spawn a shell in a single call, given some register or memory constraints. Useful when you have limited stack space:
one_gadget libc.so.6Worked example: execve /bin/sh on x86 Linux#
The classic ROP objective: call execve("/bin/sh", NULL, NULL) via syscall. On 32-bit x86 Linux, this is syscall 11, invoked with int 0x80. Registers: eax=11, ebx=&"/bin/sh", ecx=0, edx=0.
Assuming these gadgets exist in the target:
0x08048400 : pop eax ; ret
0x08048410 : pop ebx ; ret
0x08048420 : pop ecx ; ret
0x08048425 : pop edx ; ret
0x08048430 : int 0x80And assuming /bin/sh exists at 0x0804A080 (either statically in the binary, in an environment variable at known address, or written there via a previous stage), the ROP chain:
[buffer padding to reach saved return address]
| 0x08048400 | <- pop eax ; ret
| 11 | <- syscall number for execve on 32-bit x86
| 0x08048410 | <- pop ebx ; ret
| 0x0804A080 | <- pointer to "/bin/sh"
| 0x08048420 | <- pop ecx ; ret
| 0 |
| 0x08048425 | <- pop edx ; ret
| 0 |
| 0x08048430 | <- int 0x80Building this by hand for a first exploit teaches the mechanics. Building it by hand for the 100th exploit is a waste of time; use pwntools.
x86-64 differences#
The 64-bit conventions are different enough to matter:
- Syscall invocation is
syscall, notint 0x80. - Syscall numbers are different.
execveis 59 on x86-64, not 11. - Arguments go in registers by System V AMD64 calling convention:
rdi,rsi,rdx,r10,r8,r9. Soexecvewantsrax=59,rdi=&"/bin/sh",rsi=0,rdx=0. - Gadgets are typically
pop rdi ; ret,pop rsi ; ret,pop rdx ; ret. - The stack must be 16-byte aligned when calling libc functions (post-libc-2.28 assertions in some functions crash otherwise). A common workaround is inserting a lone
retgadget before the libc call to nudge alignment.
Advanced ROP variants#
The technique has spawned several variants that address specific defensive counters or expand what ROP can do.
Jump-Oriented Programming (JOP) replaces ret with indirect jumps (jmp reg or jmp [reg]). The chain is built around a dispatcher gadget that walks a table of gadget addresses instead of walking the stack. JOP defeats defenses that specifically look for ROP’s ret-heavy execution pattern.
Sigreturn-Oriented Programming (SROP), introduced by Bosman and Bos in 2014, abuses the Linux sigreturn syscall. Sigreturn restores an entire register state from a stack frame, so a single SROP gadget can set up all registers at once. Extremely powerful when it applies. Details in the original paper
.
Blind ROP (BROP) by Bittau et al. (2014) builds ROP chains against a remote server without a copy of the binary, using timing differences to discover gadgets one byte at a time. Works against forking servers that restart on crash. The BROP paper is worth reading in full.
Call-Oriented Programming (COOP) uses call instructions instead of ret or jmp, targeting virtual function calls in C++ programs. Defeats defenses focused on abnormal control flow at returns.
Modern mitigations#
The arms race has continued. Current defenses that make ROP progressively harder:
Intel CET (Control-flow Enforcement Technology) adds hardware support for:
- Shadow stacks: a separate stack the hardware maintains, checked at every
ret. Overwriting the saved return address in the main stack no longer redirects execution because the CPU compares against the shadow stack copy. Available on Tiger Lake (2020) and later, Windows and Linux both support it. - Indirect Branch Tracking (IBT): every indirect branch target must be marked with an
endbr64instruction. JOP gadgets that don’t start withendbr64fault immediately.
Control-Flow Integrity (CFI) enforces that indirect calls and returns only go to statically-valid targets. Clang has CFI (-fsanitize=cfi); Windows has Control Flow Guard (CFG) and the newer XFG (Extended Flow Guard).
ASLR entropy improvements. 64-bit systems have enough address space to make brute-force impractical even on forking servers.
Position Independent Executables (PIE) ensure the main binary is also randomized. On modern Linux distributions, PIE is default for new builds.
Stack canaries don’t stop ROP directly but make basic stack overflows harder to reach the return address without triggering the canary check first.
None of these are perfect. CET has bypass research already published. CFI can be defeated by carefully choosing valid-but-attacker-useful targets. ASLR still falls to information leaks. But each mitigation raises the bar, and the combination in a hardened modern system is legitimately difficult.
Where ROP fits today#
ROP is still a live technique in 2026 for:
- CTF challenges. Almost every pwn track leans on ROP; it’s the standard vocabulary.
- Kernel exploitation. Kernel exploits often chain ROP for KASLR bypass and privilege escalation.
- Older/embedded systems. IoT devices, industrial equipment, and legacy Windows/Linux systems often ship without CET, without PIE, or with weak ASLR. ROP is still the go-to.
- Browser and JIT exploitation. Chained through JIT-generated code and JavaScript-side vulnerabilities, ROP shows up in nearly every browser exploit chain that reaches remote code execution.
Where it’s fading:
- Modern desktop and server exploitation. CET, CFG, and CFI change the game on hardened systems. Exploit chains increasingly rely on data-only attacks, use-after-free primitives, and JIT abuse rather than classic ROP.
The technique itself remains foundational. Understanding ROP is a prerequisite for understanding modern exploit development, whether you’re building exploits or writing detections that recognize the patterns ROP produces in traffic and telemetry.
Reading and reference#
- Hovav Shacham, “The Geometry of Innocent Flesh on the Bone” (2007) - the original paper
- Ryan Roemer et al., “Return-Oriented Programming: Systems, Languages, and Applications” (2012) - comprehensive survey
- pwntools documentation - practical exploit development in Python
- ROP Emporium - hands-on ROP challenges from x86 basics through 64-bit
- Corelan exploit tutorials - the reference tutorial series that taught a generation of exploit developers
- Intel CET specification - the current defensive baseline