Return-Oriented Programming: Bypassing DEP with Gadgets
How ROP chains existing code gadgets to defeat DEP and NX, why it beats classic shellcode injection, and how CFG and CET fight back.
For a long stretch of the 1990s and early 2000s, exploiting a memory-corruption bug followed one recipe: overwrite a return address, point it at a buffer full of your own machine code, and let the CPU run what you wrote. Data Execution Prevention ended that recipe. Return-oriented programming is how attackers kept going anyway.
The idea is elegant and slightly unsettling. If you cannot supply your own code, borrow the target's. A large program and its libraries contain millions of bytes of executable instructions. Buried inside them are countless short sequences that happen to end in a return. String those sequences together in the right order and you can compute almost anything, using only code the defender shipped and trusts.
Why classic code injection stopped working
The attack ROP replaces looked like this: a buffer overflow lets you write past a stack buffer, you overwrite the saved return address with a pointer into the buffer, and the buffer holds machine code (shellcode). When the function returns, execution lands in your bytes.
Data Execution Prevention, called NX (No-eXecute) at the hardware level and DEP on Windows, closed that path. It enforces a simple rule often written as W^X: a page of memory can be writable or executable, never both. The stack and heap, where attacker data lands, become non-executable. Now the CPU refuses to run the bytes in your buffer. The overwrite still works, but the payload is inert.
The attacker still controls the stack and still controls the return address. What they have lost is the ability to introduce new executable code. ROP is built entirely around that constraint.
Gadgets: the borrowed instruction set
A gadget is a short sequence of machine instructions that ends in a return instruction (ret on x86). For example, a gadget might pop a value off the stack into a register and then return. Another might add two registers and return. Another might write a register's contents to a memory address and return.
None of these were placed there for the attacker. They are fragments of ordinary compiled functions, and often they appear in the middle of longer instructions by accident, because x86 instructions have variable length and can be decoded starting from an unintended offset. A single large library like the C runtime yields thousands of usable gadgets.
The key property is the trailing return. On x86, ret pops an address off the stack and jumps to it. So if the attacker controls the stack, then after one gadget finishes and executes its ret, the next address on the stack decides where execution goes. Fill the stack with a sequence of gadget addresses and the CPU walks through them, one after another.
The unintended-decoding property is worth dwelling on, because it explains why gadgets are so plentiful. On x86 the instruction stream has no fixed alignment, so the CPU will happily begin decoding from the middle of a legitimate instruction if a jump lands there. A multi-byte instruction placed by the compiler for one purpose can contain, at some interior offset, the bytes of a completely different short instruction followed by a return. The compiler never emitted that second instruction; it exists only as a byte-level coincidence. Attackers scan the target's executable pages for exactly these hidden sequences. A large binary yields so many that whole automated tools exist to catalog available gadgets and assemble them into a working chain, which is why a defender cannot hope to remove gadgets one at a time.
How a chain executes
Picture the stack after a successful overflow. Instead of a single return address, the attacker has laid down a list: the address of gadget 1, then any data gadget 1 will pop, then the address of gadget 2, and so on. This list is the ROP chain.
Execution flows like this:
| Step | What happens |
|---|---|
| Overflow | The vulnerable write overruns a buffer and replaces the saved return address with the address of the first gadget. |
| Return 1 | The function returns, popping that address, and the CPU begins executing gadget 1. |
| Gadget runs | Gadget 1 performs its small action (say, load a register from the stack). |
| Return 2 | Gadget 1 hits its own ret, which pops the next address the attacker placed, sending control to gadget 2. |
| Repeat | The chain continues, each gadget doing one small step and handing off through its return. |
The stack has stopped being a record of where the program has been. It is now a program in its own right, written in the vocabulary of available gadgets. This is why ROP is described as a weird machine: the attacker computes with the target's instructions but their own control flow.
What a real chain usually does
Building a full payload purely from gadgets is possible but tedious. In practice, most ROP chains are short and have one job: undo the very protection that forced ROP in the first place.
The common pattern is to use gadgets to set up a call to a system function that changes memory permissions, such as the call that marks a region executable, or a function that allocates executable memory. The chain arranges the right arguments in registers or on the stack, calls that function to make the attacker's data region executable, then jumps into conventional shellcode sitting in that region. From there the exploit continues with ordinary code.
So ROP and injected shellcode are usually partners. ROP is the key that turns DEP off for one region; the shellcode is what runs once the door is open. You can read more on the payload side in our companion guide on shellcode.
Return-oriented programming has cousins. Jump-oriented programming (JOP) chains gadgets ending in indirect jumps instead of returns, sidestepping defenses that only watch the stack. Return-to-libc, an older technique, reuses whole library functions rather than fragments. All three share the core move of code reuse: never inject code, redirect the program into code it already contains.
The real obstacle: ASLR
DEP is what makes ROP necessary. Address Space Layout Randomization is what makes it hard. To place a gadget's address on the stack, the attacker has to know where that gadget lives in memory. ASLR randomizes the base addresses of the program and its libraries every run, so those locations are unknown ahead of time.
This is why serious exploits chain two bugs. A separate information-leak vulnerability reveals a single real address, from which the attacker computes the library's base and therefore every gadget offset. With that leak, the ROP chain can be built for the running process. Without it, ROP has the gadgets but cannot name them. The interplay of DEP, ASLR, and the need for a leak is covered in depth in our guide to ASLR and DEP.
How to defend against ROP
Defense works on two fronts: stop the memory-corruption bug that gives the attacker stack control, and make gadget chaining fail even when they have it.
- Remove the underlying bug. ROP needs a write primitive, usually a buffer overflow or a write-what-where condition (CWE-123). Memory-safe languages, bounds checking, and modern compiler hardening close the door ROP walks through.
- Keep ASLR strong and complete. Compile everything as position-independent so all modules randomize, including the main executable. A single module loaded at a fixed address hands the attacker a stable gadget source and defeats the whole scheme.
- Enable stack canaries. A canary between local buffers and the saved return address detects the overflow before the return executes, stopping the chain from ever starting.
- Deploy control-flow integrity. This is the direct counter to ROP. Microsoft Control Flow Guard (CFG) validates indirect call targets. Intel CET adds a hardware shadow stack that keeps a protected copy of return addresses and faults if a
rettries to go somewhere the real call did not come from. ARM offers Pointer Authentication (PAC) and Branch Target Identification (BTI) for the same purpose. - Layer the mitigations. No single control is complete. The point of the modern stack is that an attacker must defeat DEP, ASLR, canaries, and CFI together.
A hardware shadow stack (Intel CET) is the most direct structural answer to ROP. Because it keeps return addresses in memory the attacker cannot write through the normal stack overflow, an abnormal ret in the middle of a gadget chain triggers a fault. It does not fix the underlying bug, so pair it with the rest of the stack, but it turns the central mechanism of ROP into a reliable crash instead of a reliable exploit.
For a broader view of which of these controls exists on modern systems and how they fit together, see our Exploit Intelligence dashboard, which tracks how memory-corruption techniques and their mitigations trend across real published vulnerabilities.
A worked example: building a two-gadget chain
Concrete gadgets make the mechanism click. Imagine the attacker wants to place two specific values into two registers, because the function they intend to call reads its first two arguments from those registers. They have already scanned the target's loaded libraries and cataloged the gadgets available. Two of them are useful:
- Gadget A: a sequence that pops one value off the stack into the first argument register, then returns.
- Gadget B: a sequence that pops one value off the stack into the second argument register, then returns.
The attacker also knows the address of the function they want to call, and they know the address they want execution to reach afterwards.
Now they lay out the stack. Reading from where the corrupted return address sits and going upward, the layout is: the address of Gadget A, then the value they want in the first register, then the address of Gadget B, then the value they want in the second register, then the address of the target function.
Follow the flow. The vulnerable function returns and pops the address of Gadget A, so Gadget A runs. Gadget A pops the next stack slot, the first register value, into the first register, then hits its own return. That return pops the address of Gadget B, so Gadget B runs. Gadget B pops the following slot, the second register value, into the second register, then returns. That return pops the address of the target function, so the function is entered with both argument registers already set exactly as the attacker wanted.
Nothing here is injected code. Every instruction that executed was shipped by the compiler and lives on a legitimately executable page. The attacker supplied only data, the ordered list of addresses and values on the stack, and that data steered the program's own instructions into doing new work. This is the whole trick in miniature, and a real chain is the same idea repeated until it can flip a memory region to executable and jump into shellcode.
Detection and defensive signals
ROP is a runtime behaviour, and while prevention through the mitigation stack is the real answer, certain signals help a defender or a hardening tool notice a chain in progress.
- Returns that do not match a call. In normal execution every return corresponds to an earlier call. A shadow stack makes this checkable in hardware: a return whose target disagrees with the protected copy is a strong ROP signal and triggers a fault.
- A stack full of executable-page addresses. A legitimate stack holds return addresses, saved registers, and local data. A stack densely populated with pointers into code segments, especially into the middle of functions, is anomalous.
- Returns landing mid-instruction. Gadgets found by unintended decoding begin at offsets the compiler never treated as instruction boundaries. Execution that repeatedly enters code at unaligned or non-entry offsets is a classic code-reuse tell.
- Short bursts between returns. ROP executes a handful of instructions and then returns, over and over. An unusually high rate of return instructions with very few instructions between them differs from normal control flow and is one heuristic behavioural monitors watch.
- A permission-change call from an unusual context. Because most chains aim to mark memory executable, a call to the memory-protection function originating from a stack pivot or an odd caller is worth flagging.
Heuristic detection of ROP is possible but noisy, and a careful attacker can shape a chain to look less anomalous. The durable defenses are structural: a hardware shadow stack that makes an out-of-place return fault, control-flow integrity that rejects invalid targets, and strong ASLR that denies the attacker the gadget addresses in the first place. Treat behavioural detection as a backstop, not the front line.
Related techniques compared
ROP is one member of a family of code-reuse attacks. Understanding the neighbours clarifies what each defense actually covers.
| Technique | What it chains | Ends each unit with | What it evades |
|---|---|---|---|
| Return-to-libc | Whole library functions | A normal call/return | DEP, using existing functions |
| Return-oriented programming | Short gadgets ending in a return | A ret instruction | DEP, with far finer control than return-to-libc |
| Jump-oriented programming | Gadgets ending in an indirect jump | An indirect jmp | Defenses that only watch the stack and returns |
| Call-oriented programming | Gadgets ending in an indirect call | An indirect call | Some coarse control-flow checks |
| Sigreturn-oriented programming | A forged signal frame to set all registers at once | A signal return | Systems where the signal-return path is reachable |
The pattern across all of them is code reuse: never introduce new executable code, redirect the program into code it already contains. That shared root is why control-flow integrity, which constrains where execution may transfer regardless of how the transfer is spelled, is a more complete answer than any defense aimed at returns alone.
How ROP evolved
ROP did not appear from nowhere. It is the current point on a long line of code-reuse ideas that each answered a new defense. When stacks and heaps were executable, attackers simply injected shellcode and ran it, no reuse required. The first widely deployed answer to that was making data pages non-executable, and the first response to non-executable data was return-to-libc: overwrite the return address with the address of an existing library function, such as one that spawns a shell, and let a legitimate function do the work. Return-to-libc proved that reuse could defeat execution prevention, but it was coarse, limited to whole functions and awkward to chain.
Return-oriented programming generalized the idea. By working at the granularity of short gadgets ending in a return, it turned the target's own code into something close to a full instruction set, capable of arbitrary computation rather than a fixed menu of function calls. That generality is what made it the standard technique once execution prevention became universal. Defenders then deployed control-flow integrity and, on the stack side, hardware shadow stacks, which attack the return mechanism directly. Attackers answered again with jump-oriented and call-oriented variants that avoid returns, and with data-only attacks that never divert control flow at all.
The lesson in the sequence is that each defense reshapes the attack without ending it, and the reshaping tends to demand more from the attacker: more bugs chained together, more precise information leaks, more constrained gadget sets. The modern posture is to make every step so expensive that a working exploit requires a rare confluence of separate primitives, which is exactly what the layered mitigation stack is built to force.
Common misconceptions
"DEP makes exploitation impossible." DEP ends naive code injection. ROP was invented precisely to keep working under DEP by reusing existing executable code. DEP raises the cost, it does not close the door by itself.
"You need a huge binary to find gadgets." Even modestly sized programs, together with the libraries they load, typically yield thousands of gadgets thanks to unintended decoding on variable-length architectures. Gadget scarcity is rarely the attacker's limiting factor. Address knowledge, defeated by ASLR, usually is.
"ROP performs the whole attack." In practice most chains are short and do one thing: set up and call a function that makes a data region executable, then jump into conventional shellcode. ROP is the key that turns off the lock; the shellcode is what walks through.
"Control-flow integrity fixes the bug." CFI and shadow stacks stop the hijacked control flow that ROP relies on, but they do not repair the underlying memory-corruption vulnerability. Data-only attacks that never divert control flow can still cause harm, which is why memory safety at the source remains the deepest fix.
"A single mitigation is enough." Every layer in isolation has a documented bypass. ASLR falls to an information leak, canaries can be leaked and rewritten, CFI has data-only gaps. The security comes from stacking them so an attacker must line up a separate capability for each.
Frequently asked questions
Why is it called return-oriented programming? Because each borrowed instruction sequence, a gadget, ends in a return, and the return is what passes control to the next gadget. The attacker programs the machine by arranging return targets on the stack, so returns are the organizing principle of the whole technique.
What exactly is a gadget? A gadget is a short run of machine instructions already present in the target that ends in a return (or, in variants, an indirect jump or call). Each gadget performs one small operation, such as loading a register or writing to memory, and then hands control onward through its terminating instruction.
Why does DEP force attackers to use ROP? DEP marks the stack and heap non-executable, so attacker-supplied bytes there cannot run as code. ROP sidesteps this by not supplying code at all. It reuses instructions on pages that are already legitimately executable, which DEP permits.
How does ASLR make ROP harder? To put a gadget's address on the stack, the attacker must know where that gadget lives. ASLR randomizes module base addresses each run, so those locations are unknown in advance. Serious exploits therefore pair ROP with a separate information-leak bug that reveals one real address, from which every gadget offset is computed.
Is ROP the same as return-to-libc? They share the code-reuse idea, but return-to-libc calls whole library functions, while ROP chains short gadget fragments for much finer control. ROP can compute almost anything; return-to-libc is limited to what existing functions do. ROP is the more general and more powerful descendant.
How does a shadow stack stop ROP? A hardware shadow stack keeps a protected second copy of return addresses that the ordinary stack overflow cannot reach. When a gadget's return tries to send control to an address that disagrees with the shadow copy, the CPU faults. Since ROP depends on controlling returns through the writable stack, this turns the chain into a reliable crash.
Can memory-safe languages prevent ROP entirely? By removing the memory-corruption bugs that give an attacker control of the stack, memory-safe languages remove the precondition ROP needs. There is no overflow to hijack, so no chain to build. This is why eliminating the root-cause bug class is the strongest defense, with the mitigation stack buying time for code that is not yet memory-safe.
Related guides
Sources & further reading
Related guides
- Heap Spraying: Grooming Memory for Predictable Addresses
How heap spraying floods memory with attacker data to place a payload at a predictable address, its browser roots, and how to defend.
- Shellcode Explained: Payloads, Staging & Why NX Matters
What shellcode is, how staged and stageless payloads differ, and why DEP and ASLR make injection harder. Conceptual, no payloads.
- Type Confusion Explained: When Objects Are the Wrong Type
How type confusion makes a program treat memory as the wrong type, why that corrupts memory and enables code execution, and how to defend.