Exploit Mitigations Explained: The Modern Defense Stack
A plain tour of the mitigation stack: DEP, ASLR, stack canaries, CFG, Intel CET, and sandboxing, what each stops, and why they only work in layers.
There was a time when a single memory-corruption bug meant a single step to full control: overwrite a return address, run your code, done. That world is gone, and it was dismantled deliberately, one mitigation at a time. Today an attacker with the same class of bug faces a stack of defenses, each built to break a different stage of the exploit. None of them is impregnable. Together they turn a one-step win into a chain of hard problems that usually requires multiple separate bugs to solve. This guide walks the modern stack layer by layer and shows how the layers reinforce each other.
The exploit as a sequence of steps
To see why layering works, it helps to see an exploit as a sequence rather than a single act. A typical memory-corruption exploit has to:
- Trigger a bug that corrupts memory, often an out-of-bounds write (CWE-787).
- Redirect control flow to code of the attacker's choosing.
- Run that code despite execution-prevention rules.
- Know the addresses everything lives at.
- Turn the initial code execution into meaningful control of the system.
Each mitigation targets one or more of these steps. The strategy of the stack is that an attacker must satisfy every step, so blocking any single step reliably would stop the exploit. Since no mitigation blocks its step perfectly, the stack instead makes every step expensive, and the costs compound.
DEP: stop injected code from running
Data Execution Prevention enforces that data pages, the stack and heap where attacker input lands, are not executable. The hardware NX bit backs it. When the attacker redirects execution into a buffer of their own bytes, the CPU refuses to run them and faults.
DEP breaks step 3 for the simplest attacks. Its known bypass is code reuse: instead of injecting new code, the attacker chains fragments of existing, legitimately executable code, which is what return-oriented programming does. So DEP does not end exploitation, it forces a harder technique, and that technique has its own requirement that the next layer attacks.
ASLR: hide the addresses
Address Space Layout Randomization randomizes the base addresses of the program and its libraries every run. It targets step 4. Code reuse needs to name the addresses of the gadgets or functions it reuses, and ASLR makes those addresses unknown ahead of time.
Its known bypass is an information leak: a second bug that discloses one real address, from which the attacker recomputes the rest. This is why serious exploits are multi-bug affairs, one flaw to leak an address and another to corrupt memory. DEP and ASLR are the foundation of the stack and are covered together in our ASLR and DEP guide, because neither is much use without the other.
DEP without ASLR leaves fixed addresses that make code reuse easy. ASLR without DEP lets the attacker skip addresses by injecting and running their own code. Only together do they force the hard path: defeat execution prevention through code reuse, and defeat randomization through a separate leak. That mutual reinforcement is the model for the whole stack. Each layer's bypass is another layer's job to block.
Stack canaries: catch the overflow early
A stack canary is a known value the compiler places between a function's local buffers and its saved return address. Before the function returns, it checks whether the canary is intact. A stack buffer overflow that reaches the return address has to pass through the canary first, corrupting it, and the check aborts the program before the poisoned return ever executes.
Canaries target step 2 for stack overflows specifically. Their known limits: they protect the stack, not the heap; an attacker who can leak the canary value can rewrite it correctly; and bugs that overwrite the return address without a linear overflow can skip past them. Still, they cheaply eliminate the most classic stack-smash. The buffer overflow guide covers the bug class canaries were built against.
Control-flow integrity: reject abnormal jumps
Control-flow integrity is the direct answer to code reuse, the technique DEP forced attackers into. The idea is to constrain where execution is allowed to transfer, so that the abnormal jumps and returns of return-oriented and jump-oriented programming become invalid and fault.
There are several implementations, and it is worth knowing them by name.
| Mitigation | Vendor / platform | What it constrains |
|---|---|---|
| Control Flow Guard (CFG) | Microsoft (Windows) | Validates indirect call targets against a set of legitimate function entry points. |
| Shadow stack (Intel CET) | Intel hardware | Keeps a protected second copy of return addresses; a ret that disagrees with it faults, breaking ROP. |
| Indirect Branch Tracking (Intel CET) | Intel hardware | Requires indirect jumps and calls to land on a marked valid target, breaking JOP. |
| Pointer Authentication (PAC) | ARM | Signs pointers so a tampered code pointer fails verification when used. |
| Branch Target Identification (BTI) | ARM | Restricts indirect branches to instructions marked as valid targets. |
CFI targets step 2 and step 3 together by making hijacked control flow itself illegal. The hardware shadow stack in Intel CET is especially significant against ROP, because return addresses live in memory the attacker cannot reach through the ordinary stack overflow, so a gadget chain's abnormal returns trigger faults. Like every other layer, CFI has bypasses, including data-only attacks that never divert control flow, which is exactly why it is one layer among several rather than the whole answer.
Sandboxing: contain the compromise
The layers above try to stop code execution. Sandboxing assumes they will eventually fail and asks a different question: when an attacker does run code inside a component, how much can that code reach.
A sandbox runs a risky component, a browser's rendering engine, a document parser, a media decoder, in a low-privilege, tightly restricted process. Even with full code execution inside the sandbox, the attacker cannot directly touch the file system, other processes, or the kernel. To turn that contained foothold into control of the machine, they need yet another bug, a sandbox escape, usually a separate vulnerability in the more privileged code the sandbox talks to. Sandboxing targets step 5, the conversion of code execution into system control, and it is the reason a single browser bug so rarely means a fully owned device on its own.
The defining property of the mitigation stack is that its strength is multiplicative. Each layer has a documented bypass, and an attacker who studies the field can name all of them. What they cannot easily do is line up a separate capability for every layer at once: a bug to corrupt memory, a leak to beat ASLR, a way past canaries, a control-flow trick that survives CFI, and a sandbox escape. Disable one layer for compatibility and you do not lose one fifth of the protection, you may hand the attacker the missing piece that makes the others solvable. The layers defend each other.
How to defend: run the whole stack
For defenders and builders, the practical guidance is less about inventing new protections and more about ensuring the existing stack is fully and correctly enabled, then adding what your platform allows.
- Enable every layer you have. Confirm DEP covers all data regions, that ASLR is complete with position-independent builds, that stack protection is compiled in, and that CFG or the platform's CFI is active. A partially enabled stack is a gapped stack.
- Turn on hardware mitigations. Where the CPU offers Intel CET or ARM PAC/BTI, enable it. The hardware shadow stack in particular changes the economics of ROP.
- Sandbox anything that parses untrusted input. Browsers, document readers, media pipelines, and network services that handle attacker-controlled data belong in the strongest sandbox available.
- Treat information leaks as critical. Because a single leak collapses ASLR and can expose canaries, out-of-bounds reads and memory-disclosure bugs deserve the same urgency as corruption bugs.
- Remove the root cause where you can. Memory-safe languages and secure coding eliminate the bug classes the whole stack exists to contain. Mitigations buy time and raise cost; safe code removes the need. See our secure coding practices guide.
- Keep compatibility opt-outs to a minimum. Every legacy exception that disables a mitigation for one component reopens a step in the chain. Audit them.
For how these mitigations track against the memory-corruption techniques that keep appearing in real exploit chains, our Exploit Intelligence dashboard shows which patterns attackers still rely on and where the stack is holding.
A worked example: what one bug has to overcome
Trace a single memory-corruption bug against the full stack to see the layering as an attacker experiences it. Suppose a program has an out-of-bounds write on the stack that lets an attacker overwrite adjacent memory, including a saved return address. Two decades ago that was the whole exploit. Now walk the same bug through the modern defenses.
The write reaches toward the saved return address, but a stack canary sits between the buffer and that address. A linear overflow corrupts the canary on its way, and the function's return-time check aborts the program before the poisoned return executes. To get past this, the attacker needs either a bug that writes without crossing the canary or a separate leak that reveals the canary value so they can rewrite it intact. One layer, one new requirement.
Suppose they have a leak and preserve the canary. They overwrite the return address to point at their payload. If that payload is injected bytes on the stack, DEP stops it: the stack is non-executable and the CPU faults on the first instruction. To proceed, the attacker abandons injection and turns to code reuse, building a ROP chain out of gadgets already present in the program. Second layer, second new requirement.
To place gadget addresses, they must know where the gadgets live, and ASLR has randomized every module's base address. So they need yet another bug, an information leak that discloses a real address, from which they compute the rest. Third layer, third requirement, and now the exploit needs at least two distinct vulnerabilities.
Say they have the leak and assemble the chain. Control-flow integrity now inspects the transfers. A shadow stack notices that a gadget's return goes somewhere the matching call never came from and faults; indirect-branch tracking rejects jumps that do not land on marked targets. To survive, the attacker must confine themselves to a data-only attack that never diverts control flow, a far narrower and harder path. Fourth layer, fourth requirement.
Even if code execution is achieved inside the component, sandboxing contains it. The compromised process cannot touch the file system, other processes, or the kernel without a further sandbox-escape bug in the more privileged code it talks to. Fifth layer, and a fifth distinct vulnerability.
The single bug that once meant instant control now demands a canary leak, an address leak, a code-reuse technique that survives control-flow integrity, and a sandbox escape, several separate primitives lined up together. That compounding is the entire design intent of the stack.
Detection and hardening signals
The stack is preventive, but its operation produces signals defenders and builders can act on.
- Mitigation faults as alarms. A shadow-stack violation, a control-flow-guard failure, or a canary-check abort marks an attempted exploitation technique. Each such fault is evidence that something tried to subvert control flow or memory, and it deserves to be logged and investigated rather than silently restarted.
- Compatibility opt-outs in the build. A binary marked without DEP, without ASLR, or without stack protection is a gap. Auditing the mitigation flags actually present in shipped binaries surfaces components that quietly disabled a layer.
- Non-PIE modules. An executable or library loaded at a fixed address defeats ASLR for the whole process by giving the attacker a stable gadget source. Finding modules that are not position-independent is a concrete hardening task.
- Leaks treated as low severity. Out-of-bounds reads and memory-disclosure bugs are frequently under-prioritized, yet a single leak collapses ASLR and can expose canaries. Triage that downgrades information disclosure is itself a weakness in the stack's effectiveness.
- Unsandboxed parsers. Any component that handles untrusted input outside a sandbox, a media decoder or document parser running with full user rights, is a missing last line. Inventorying which risky parsers are contained is a direct measure of the stack's completeness.
Because each layer converts a specific exploitation step into a fault, those faults are among the highest-signal events a system produces. A cluster of shadow-stack or control-flow-guard violations in one component is a strong indicator of an exploitation attempt in progress. Wiring these into monitoring, rather than treating them as ordinary crashes, turns the preventive stack into a detection surface as well.
How the mitigations map to exploit steps
The layers are easier to reason about as a grid against the steps of an exploit. This table shows which stage each mitigation attacks and the known bypass that keeps it from being a wall on its own.
| Mitigation | Exploit step it targets | Known bypass |
|---|---|---|
| DEP / NX | Running injected code | Code reuse (ROP, return-to-libc) |
| ASLR | Knowing addresses | Information-leak bug |
| Stack canary | Reaching the return address | Leak the canary, or a non-linear write |
| Control-flow integrity | Redirecting control flow | Data-only attacks |
| Sandboxing | Turning execution into system control | A separate sandbox-escape bug |
Read down the bypass column and the layering logic is obvious: every bypass is a different capability, requiring a different bug or condition. An attacker cannot reuse one primitive to clear the whole stack. Read across any single row and no mitigation is complete. The security lives in the combination, which is why removing one row does more damage than its share, it can supply the missing capability that makes a neighbour's bypass reachable.
How the stack was built up
The mitigation stack accreted over decades, each layer added in response to the technique the previous one forced. When stacks were executable, a buffer overflow ran injected shellcode directly, so non-executable data pages, DEP and the NX bit, were introduced to stop that. DEP pushed attackers to code reuse, which needed known addresses, so ASLR was added to hide them. Stack canaries came in to catch the most common linear overflows cheaply, before the return address is ever used.
Code reuse under DEP and ASLR still worked when attackers had a leak, so control-flow integrity was developed to make the hijacked transfers themselves illegal, first in software and then with hardware shadow stacks and branch tracking. And because no combination of these fully prevents code execution in a complex parser, sandboxing was layered around risky components to contain the damage when a bug does get through. The order matters: each addition assumes the ones before it and targets the technique they left open. That history is why disabling a layer for compatibility is more dangerous than it looks, since the remaining layers were designed on the assumption that it is present.
Common misconceptions
"One strong mitigation is enough." Every layer has a documented bypass, so any single mitigation can be defeated by an attacker who prepares for it. The protection is multiplicative across layers, and leaning on one is exactly the single point of failure the stack was built to avoid.
"Mitigations fix vulnerabilities." They do not remove the bug. They make the bug harder or more expensive to exploit. A patched or memory-safe codebase removes the flaw itself; mitigations buy time and raise cost while the flaw exists. The two strategies are complementary, and only source-level fixes end the bug class.
"ASLR alone stops exploitation." ASLR only hides addresses. Without DEP an attacker can inject and run code without needing to know library addresses at all, and with a single information leak ASLR is recomputed away. It is one interdependent layer, strongest when paired with DEP and behind a scarcity of leaks.
"Disabling a mitigation for one component is a small, local risk." Because the layers reinforce each other, an opt-out can hand an attacker the specific capability that makes the other layers' bypasses reachable. A non-ASLR module supplies stable gadgets for the whole process. Local opt-outs have process-wide consequences.
"Sandboxing means a bug is harmless." A sandbox contains code execution, it does not prevent it, and escaping requires only a further bug in the privileged code the sandbox talks to. Sandboxing greatly reduces impact, and it is a containment layer rather than a guarantee.
Frequently asked questions
Why are exploit mitigations layered instead of relying on one strong defense? Because every individual mitigation has a known bypass. Layering works by making each bypass require a different bug or condition, so an attacker must line up several distinct primitives at once. That compounding is far harder to achieve than defeating any single control.
What is the difference between DEP and ASLR? DEP marks data regions non-executable so injected code cannot run, targeting the step of executing attacker code. ASLR randomizes module addresses so the attacker cannot know where code and gadgets live, targeting the step of naming addresses. They are quoted as a pair because each covers the other's weakness.
Do stack canaries stop all buffer overflows? No. Canaries catch linear stack overflows that cross the canary before reaching the return address. They do not protect the heap, they can be defeated if the attacker leaks the canary value, and non-linear writes that skip the canary evade them. They cheaply remove the classic stack smash while leaving other cases to other layers.
What does control-flow integrity actually stop? It constrains where execution may transfer, so the abnormal returns and jumps of return-oriented and jump-oriented programming become invalid and fault. A hardware shadow stack is especially effective against ROP because it keeps return addresses out of the attacker's reach. Its main gap is data-only attacks that never divert control flow.
Why is an information leak treated as so serious? Because one leaked address collapses ASLR by letting the attacker recompute every module's layout, and a leaked canary value lets them rewrite it intact. A memory-disclosure bug that looks low impact on its own is often the key that unlocks the rest of an exploit chain, so it deserves the same urgency as a corruption bug.
Does sandboxing replace the other mitigations? No. Sandboxing is the outermost layer and assumes the others may fail. It contains code execution so a single bug in a parser does not mean control of the machine, but it depends on a separate escape bug being absent and works best behind the full stack that makes reaching code execution hard in the first place.
If I write in a memory-safe language, do I still need the stack? Memory-safe code removes the bug classes the stack exists to contain, which is the strongest position. In practice most systems mix safe and unsafe code, load native libraries, and run on shared platforms, so the mitigations still protect the parts that are not memory-safe. Safe code and the stack address the same problem from opposite ends.
The modern mitigation stack is the accumulated answer to thirty years of exploitation. No layer is a wall on its own. Assembled, they convert a once-trivial win into a chain of separate, hard problems, and every layer you leave fully enabled is one more problem the attacker has to solve.
Related guides
Sources & further reading
Related guides
- ASLR and DEP Explained: The Two Pillars of Memory Protection
How ASLR and DEP stop memory-corruption exploits, what each one actually blocks, and where their limits leave a way through.
- 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.
- 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.