From a corrupted byte to a stolen CPU
Guide 1 left you holding a wound: a stack buffer overflow writes past the end of a local array and tramples whatever sits above it on the stack. That is the damage. This guide is about the attack — turning that damage into the attacker running code of their choosing. The pivot from one to the other is a single, precise idea called control-flow hijacking: making the program jump somewhere it was never supposed to go. A crash is loud and useless to an attacker; a hijack is silent and total, because once you choose where the CPU goes next, you choose what the program does.
To see the target, recall how a function returns. When code calls a function, the CPU pushes a return address onto the stack — the address of the instruction to resume at when the callee finishes. The function does its work in a stack frame sitting below that saved address, and its local arrays live inside that frame. The epilogue ends with a single instruction, ret, which does exactly one thing: it pops that saved address off the stack into the program counter (the rip register) and the CPU starts fetching there. That ret is the entire game. It trusts a value on the stack completely, and an overflow gets to write the stack.
So the classic attack writes itself. The overflow keeps writing past the local array, up through the frame, until it lands on top of the saved return address and overwrites it with an address the attacker picked. The function finishes, ret pops the attacker's value into rip, and the CPU obediently fetches the next instruction from wherever the attacker said. No bug in ret; ret did its job perfectly. The bug was upstream — an unchecked copy let untrusted bytes reach a place the hardware trusts without question. That is the heart of every control-flow hijack: corrupt a value the CPU will later load into rip.
The old answer, and the wall that stopped it
Once you can redirect rip, the obvious next question is: *to where?* The original 1990s answer was beautifully blunt: put your own machine code into the buffer you are overflowing, then point the return address at it. The attacker's bytes do double duty — first as a long pad that fills the array and overwrites the saved address, and at the same time as a tiny program (a payload often called shellcode, because it classically launched a shell). ret loads rip with an address that points back into the stack, into the very bytes the attacker just supplied, and the CPU executes them. Code injection in one stroke.
Then a wall went up that broke this cleanly. Modern CPUs and operating systems mark each page of memory with permissions, and they enforce a rule called data execution prevention (DEP, or NX for "no-execute"): a page may be writable or executable, but not both at once. The stack and the heap — where attacker data lands — are writable and therefore not executable. Code pages — the program's actual instructions — are executable but not writable. So shellcode dumped onto the stack is just data the CPU refuses to run; the instant rip points at it, the hardware raises a fault and the process dies. The straightforward injection attack was, for a wide class of programs, finished.
Reusing the code that is already there
Here is the crack NX leaves open. It forbids running attacker data as code — but the victim program is full of code that is already executable and already trusted: its own functions, and crucially the entire C library, libc, mapped into every process. That code is on executable pages, perfectly legal for the CPU to run. So the attacker stops trying to supply code and starts trying to reuse it. The first version of this idea is return-to-libc: instead of pointing the overwritten return address at injected bytes, point it at the start of a real libc function — say system() — and arrange the stack so that function finds the arguments it expects. ret jumps into genuine, executable library code, and the attacker has turned the program's own building blocks against it.
Return-oriented programming (ROP) is return-to-libc taken to its limit, and it is one of the most elegant ideas in offensive security. Instead of calling whole functions, the attacker hunts for tiny fragments of existing code called gadgets: a handful of useful instructions that happen to end in a ret. For example, somewhere in libc there might be the two-byte sequence pop rdi ; ret — an instruction that loads a value into the rdi register, immediately followed by a return. By itself it does almost nothing. But its trailing ret is the magic: it makes the gadget chainable.
Now watch the whole machine turn over. The attacker does not overwrite the return address with one value — they overwrite the entire region above it with a carefully ordered list of gadget addresses, interleaved with data the gadgets will consume. The first ret jumps to gadget one. Gadget one runs its instruction and hits its own ret, which pops the next address off the stack and jumps to gadget two. Gadget two runs and rets into gadget three. The stack has stopped being a stack of frames; it has become a program, and ret has become its instruction-fetch step. The attacker writes software in a language whose only words are fragments of the victim's own binary.
the stack, AFTER an overflow, read top-to-bottom as ret pops it: [ &gadget1 ] <- ret jumps here first: pop rdi ; ret [ 0x... ptr ] <- popped into rdi by gadget1 (e.g. "/bin/sh") [ &gadget2 ] <- gadget1's ret jumps here: pop rsi ; ret [ 0 ] <- popped into rsi by gadget2 [ &system ] <- gadget2's ret jumps here: call system(rdi) ... each ret = "fetch next address from the stack and go"; the stack has become the attacker's program counter.
Why this is hard to stop, and what it really needs
Two facts make ROP genuinely powerful, and it is worth being honest about both. First, a large binary contains a surprising abundance of gadgets — researchers have shown that a big enough program is effectively Turing-complete under ROP, meaning a chain can express arbitrary computation, not just "call system()." Worse, on a variable-length instruction set like x86, gadgets can be found mid-instruction: jump rip into the middle of a longer encoding and the bytes decode into a different, unintended instruction ending in ret. The defender wrote no such gadget; the CPU finds it anyway in the gaps between instructions. Second, and this is the deep point, ROP never violates NX — every byte it executes lives on a legitimately executable page. The mitigation that killed code injection does nothing here.
But be equally honest about what ROP demands, because this is exactly where the next guide's defenses bite. To write addresses of real gadgets, the attacker must know those addresses — where libc and the program actually sit in memory. On an old system the layout was fixed, so a gadget at 0x4005a0 was at 0x4005a0 on every run, and an attack could hardcode it. The single most important counter is to take that knowledge away: address-space layout randomization (ASLR) shuffles where the stack, the heap, and libc are loaded on every execution, so the addresses the chain depends on are different — and unknown — each time. A ROP chain aimed at last run's addresses now lands on garbage and crashes.
Building one, step by step
To make the abstraction concrete, here is the actual workflow of building a return-to-libc or ROP attack against a vulnerable program — not to arm anyone, but because seeing the steps is the fastest way to truly understand which assumption each defense in guide 3 is attacking. Every step is a precondition; remove any one of them and the chain falls apart.
- Find the overflow. Locate a write you control that runs past a buffer's end — the unchecked copy from guide 1. Without a memory-corruption primitive there is no attack at all; this is the wound everything else builds on.
- Measure the offset. Work out exactly how many bytes lie between the start of the buffer you overflow and the saved return address above it. Get this wrong by even one byte and you overwrite the wrong thing — this is where an off-by-one turns into a missed shot.
- Leak an address (if ASLR is on). Use a second bug — an over-read, a format-string read — to learn one true runtime address, then subtract its known offset to recover where libc and your gadgets actually sit this run. This step exists only because ASLR hid the addresses.
- Collect gadgets. Scan the now-located executable pages for the fragments you need — pop rdi ; ret to set an argument, a ret to align the stack — and the function you ultimately want to call, such as system(). These come from code already in the binary, so NX never objects.
- Lay out the payload. Build the byte string: padding to reach the return address, then the ordered list of gadget addresses interleaved with the data each gadget consumes, exactly as the small stack sketch above showed. This list is the program you are about to run.
- Deliver and trigger. Feed the payload through the same input that caused the overflow, then let the vulnerable function return. Its ret loads your first gadget into rip, the chain runs itself, and control is yours.
Step back and the whole arc snaps into focus. A memory-corruption bug gives you a write; the saved return address gives you a target; ret gives you a fetch step; NX forces you off injection and onto reuse; gadgets give you an instruction set carved from the victim's own code; ASLR fights back by hiding the addresses; and an info leak fights that back. Every line of this chain is a precise rule meeting its precise loophole. Hold that mental model, because guide 3 walks through the defenders' replies — canaries, control-flow integrity, and the shadow stack — and each one is best understood as an attack on exactly one of the steps you just saw.