From vulnerability to exploit
A memory-safety bug is not yet an exploit. The bridge is hijacking control flow — and ASLR, DEP, and stack canaries each remove one girder, so a modern exploit must defeat several mitigations at once, not one bug.
On an authorized CTF box, your fuzzer feeds 600 bytes into a program that reserved 64. It crashes. The debugger stops on a fault, and you notice something quietly catastrophic: the instruction pointer now reads 0x4141414141414141 — the hex for AAAA…. The bytes you sent didn’t just corrupt data; they landed in the slot the CPU reads to decide what to run next. At that moment the program is no longer running its own logic — it is about to run yours. Nobody handed you that control. The program leaked it, one byte at a time, because it never checked how much it was copying. The distance from “it crashed” to “it runs my code” is the entire craft of exploitation — and almost all of modern defense is about making that distance impossibly far.
By the end of this lesson you’ll know how a memory-safety bug becomes control of the instruction pointer, and how ASLR, DEP, and stack canaries each remove one of the things an exploit needs — so a single bug is rarely enough anymore.
Authorization first — this is mechanism, not a weapon
Everything below is the theory of how an exploit forms, taught so a defender can reason about why mitigations exist and why a “low-severity” memory bug can still be game-over. There is no working exploit code here, and there is no reason there should be: you do not need a payload to understand the mechanism, and you should never point one at a system you don’t own or aren’t explicitly authorized to test. The legitimate places to actually watch a bug turn into control are a CTF box, a deliberately vulnerable VM you deployed, or an in-scope engagement with written authorization. In MITRE ATT&CK terms, what we’re describing — turning a flaw into the ability to run attacker-chosen instructions — sits at the boundary of Initial Access and Execution, and the foothold it buys is Persistence (TA0003): once code runs, the attacker’s next goal is to make sure it keeps running. We study it to break the chain, not to forge it.
A vulnerability is potential energy; an exploit is the release
A vulnerability is a property of code: somewhere, the program will do the wrong thing with input it didn’t validate. An exploit is a specific input that converts that property into attacker-chosen behavior. The two are not the same, and conflating them is the most common junior mistake in severity triage. A buffer overflow that only ever crashes the process is a denial-of-service bug. The same overflow, if it can steer the instruction pointer, is remote code execution. Same root cause, two wildly different severities — and which one you have depends entirely on what the corrupted memory controls.
The classic example is the stack buffer overflow. A function reserves a fixed-size buffer on the stack — say 64 bytes for a username. Right next to that buffer, by the architecture’s calling convention, sits the saved return address: the location the CPU will jump to when this function finishes. If the program copies input into the 64-byte buffer without checking the length, input longer than 64 bytes keeps writing — past the buffer, into adjacent stack slots, and eventually over that saved return address. The CPU has no idea anything is wrong. When the function returns, it loads whatever now sits in that slot into the instruction pointer and jumps there. If the attacker controls those bytes, the attacker controls where execution goes next. That is the whole trick: memory safety is what keeps “data the program reads” and “addresses the program trusts” from being the same bytes — and a memory-safety bug erases that wall.
Controlling the pointer is necessary, not sufficient
Owning the instruction pointer answers where execution goes. The attacker still needs something there worth jumping to. Historically the answer was simple and brutal: put the malicious instructions (the “shellcode”) into the same buffer you just overflowed, and point the return address back at the buffer. Your data is your code; the stack is both. This worked because, on early systems, the stack was both writable and executable, and addresses were predictable — the buffer sat at the same place on every run, so you could hardcode where to jump. Two assumptions, both of which the next twenty years of mitigations set out to destroy.
This is the right mental model for the whole exploitation arms race. An exploit isn’t one thing; it’s a small list of preconditions that all have to hold at once:
| Exploit needs… | Why | Mitigation that removes it | Attacker’s counter-move |
|---|---|---|---|
| To reach the return address | Overwrite the slot uncaught | Stack canary (random guard value) | Leak or guess the canary first |
| To run code it injected | Execute bytes in the buffer | DEP / NX (no-execute pages) | Reuse existing code (ROP) |
| To know where to jump | Hardcode a target address | ASLR (randomized layout) | Leak an address to derandomize |
| To call a function as a target | Jump into the middle of code | CFI (control-flow integrity) | Find a valid call target to abuse |
DEP and ASLR: the two girders that changed the game
DEP (Data Execution Prevention), also called NX (no-execute), attacks the “your data is your code” assumption directly. Memory pages get a flag: writable or executable, not both. The stack and heap — where attacker input lands — are marked no-execute. Now when the hijacked instruction pointer jumps into the buffer full of shellcode, the CPU faults: those bytes are data, and data doesn’t run. DEP is nearly free (it’s a hardware bit) and it killed the simplest exploit class outright. The attacker’s answer was return-oriented programming (ROP): instead of injecting new code, chain together tiny snippets of the program’s own already-executable code — short sequences ending in a ret instruction, called “gadgets” — to assemble the desired behavior out of legitimate, executable pieces. ROP doesn’t violate DEP because it never executes data; it only ever runs code that was already marked executable. The bug shifted the cost of exploitation, it didn’t end it.
ASLR (Address Space Layout Randomization) attacks the other assumption: predictable addresses. Every time the program loads, the OS shuffles where the stack, heap, and libraries live in memory — randomizing the high bits of their base addresses. ROP needs to know the exact address of each gadget; ASLR means the attacker no longer knows where anything is. A hardcoded jump target that was correct yesterday points at garbage today, and a wrong guess crashes the process instead of exploiting it. This is why modern exploits almost always need an information leak as a first stage: a separate bug (or the same bug used to read rather than write) that discloses one real runtime address. From that single leaked address, the attacker computes the base and de-randomizes the whole module — ASLR falls the moment one address leaks. That’s the deep lesson: ASLR doesn’t make exploitation impossible, it makes it require a second bug, and “needs two bugs not one” is a massive practical increase in difficulty.
▸Why this works
Why is 64-bit ASLR so much stronger than 32-bit, for the same algorithm? Entropy. On 32-bit systems the randomized region only had room for roughly 8–16 bits of randomness in the base address — a few thousand to tens of thousands of possibilities. An attacker who can retry a network service can simply brute-force that: spray guesses until one lands, in seconds to minutes. 64-bit systems have far more address space to randomize into — commonly 28+ bits of entropy — turning a feasible brute force into hundreds of millions of attempts, which a crash-on-failure service makes hopeless. Same mitigation, but the address-space size is what makes it actually bite. It’s a clean example of how a defense’s real strength often lives in a parameter, not the idea.
Why this matters for a defender
You will rarely write a stack overflow exploit. You will constantly make decisions that depend on understanding this chain. When a dependency ships a CVE described as a “heap buffer overflow,” the question that sets your urgency is exactly the one this lesson trains: can this corruption reach control flow, or does it only crash? When you choose a language, you’re choosing whether memory safety is the compiler’s job (Rust, Go, managed runtimes bound every access) or yours (C and C++ trust you to count bytes, and a single miscount is the whole story above). When you harden a build, the flags that turn on stack canaries, DEP, ASLR, and CFI are not checkboxes — each one deletes a specific girder from the table above, and the value of “defense in depth” here is literal: an attacker now has to defeat several independent mechanisms in one shot, and missing any one of them turns their RCE back into a crash.
A C service has a stack buffer overflow. The build already enables DEP/NX. An attacker wants reliable code execution. Which single addition to the build most raises the bar against this specific bug?
A buffer overflow in a service can corrupt memory but, on this build, can only ever crash the process — it cannot redirect execution. How should you classify it?
Why do modern exploits against an ASLR-enabled target so often require an information leak as a first stage?
Order the stages of a classic stack-overflow exploit chain, from the bug to attacker code running:
- 1 Input longer than the buffer is copied without a length check
- 2 The overflow writes past the buffer over the saved return address
- 3 The function returns, loading attacker bytes into the instruction pointer
- 4 Execution jumps to attacker-chosen code (injected shellcode or a ROP chain)
- 01Walk through how a stack buffer overflow turns from a crash into control of the instruction pointer, and name the one control that breaks the chain.
- 02Explain how DEP and ASLR each remove a different precondition an exploit needs, and how attackers answered each one.
A vulnerability is a property of code — it will mishandle unvalidated input — while an exploit is a specific input that converts that property into attacker-chosen behavior. The bridge between them, for a memory-safety bug, is control-flow hijack: a stack buffer overflow writes past its buffer over the adjacent saved return address, and when the function returns the CPU loads attacker bytes into the instruction pointer and jumps there. Whether a given overflow is a mere crash (DoS) or remote code execution depends entirely on whether the corruption reaches control flow. Three mitigations each delete one precondition: DEP/NX makes injected data non-executable (answered by ROP, which reuses existing executable gadgets), ASLR randomizes addresses so the attacker can’t hardcode targets (answered by an information leak that discloses one real address and de-randomizes the rest), and stack canaries guard the return slot so a contiguous overwrite is caught before return. Because each mitigation removes a different girder, a modern exploit has to defeat several independent mechanisms in one shot — which is why a single memory bug is rarely enough, and why a defender’s real leverage is choosing memory-safe languages, enabling every hardening flag, and reading each new memory-corruption CVE through the one question that sets its severity: can this corruption reach control flow, or does it only crash?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.