Four walls, and how to read a wall
You arrive at this guide already knowing the attack. Guide 1 showed the wound — a buffer overflow or a use-after-free hands you a controlled write. Guide 2 showed the kill — corrupt the saved return address, turn return-oriented programming into a whole program built from gadgets, and the CPU is yours. This guide is the defenders' answer. Over roughly twenty years they raised four major walls, and the single most useful habit you can build is to read each wall the way a good attacker does: not as "this stops exploitation," but as one precise rule that forbids one specific thing. The gap between what a mitigation actually says and what people assume it says is exactly where every next attack lives.
The four walls divide naturally into two pairs, and it helps to hold the shape in mind before the detail. The first pair attacks the attacker's primitives — the basic capabilities a write buys you. NX (no-execute) forbids running data as code; ASLR hides the addresses an attacker needs to aim. The second pair guards the targets directly — the values the CPU will later trust. Stack canaries detect a stomped return address before ret ever uses it; control-flow integrity refuses to jump anywhere the program was not designed to jump. None of the four is a cure. Each raises a price, and the whole field is an arms race precisely because every wall left a crack, and every crack grew its own attack.
NX and ASLR: starving the primitives
You met NX / DEP in guide 2, so here is just the precise shape. Every page of virtual memory carries permission bits, and the hardware enforces a writable-XOR-executable rule: the stack and heap, where attacker data lands, are writable and therefore not executable. The instant rip points at bytes on a writable page, the memory-protection machinery raises a fault and the process dies. This is cheap — the page tables already existed — and it cleanly ended the original "dump shellcode on the stack" attack. But re-read the rule: it never says you cannot control rip, only that you cannot run bytes you wrote as data. That is the loophole ROP marched through, reusing code already on executable pages. NX did not lose; it just turned out to forbid less than people hoped.
Address-space layout randomization (ASLR) attacks the loophole NX left. A ROP chain is a list of addresses — of gadgets, of libc functions like system() — so it only works if the attacker knows where those things sit. On an old system the layout was fixed: a gadget at 0x4005a0 stayed at 0x4005a0 every run, and an exploit could hardcode it. ASLR, shipped on Linux and Windows in the mid-2000s, randomizes the load base of the stack, the heap, the shared libraries, and (with PIE) the main program itself on every execution. The chain that worked yesterday now points into a different, unknown layout and lands on garbage. The attacker's most basic assumption — I know where things are — is gone.
two runs of the SAME program, with ASLR on:
run A: libc base = 0x7f3a2c000000 gadget = base + 0x4a3e0
run B: libc base = 0x7f91d8000000 gadget = base + 0x4a3e0
^^^^^^^^^^^^^^^^ different each run, unknown to attacker
the OFFSET (0x4a3e0) is fixed and known from the libc binary;
the BASE is the secret. leak ONE real address at run time,
subtract its known offset -> recover base -> recompute every gadget.The stack canary: a tripwire for the return address
NX and ASLR starve the attacker's primitives; the next two walls guard the targets directly. The stack canary — named after the canary in a coal mine, whose death warned of poison — defends the saved return address with a tripwire. The idea is disarmingly simple. A linear overflow that wants to reach the return address must cross everything between the local buffer and that address. So the compiler plants a secret guard value right there, between the locals and the saved return address, on the way in. On the way out, just before ret, it checks whether that guard value is still intact. A classic stack overflow flows upward and must trample the canary to reach the return address — so a corrupted canary is a near-certain sign the return address was hit too.
Picture the frame with the guard in place. The local buffer (say char buf[16]) sits lowest; an overflow starts there and writes upward through the other locals, then the canary word (a value like 0x...a3), then the saved rbp, and only then the return address at the top. The compiler's prologue stores the canary on the way in; the epilogue, just before ret, tests it: if the frame's canary no longer equals the secret, it calls __stack_chk_fail() and aborts; otherwise it returns. A linear overflow simply cannot reach the return address without crossing — and so wrecking — the canary on the way.
On Linux this is the compiler's stack protector — turn it on with gcc -fstack-protector-strong. The secret is a per-process random word fetched at startup (with a zero byte in it, so that string functions like strcpy() stop early and cannot easily copy the canary forward). If the epilogue finds the guard changed, it does not return — it calls __stack_chk_fail() and aborts the process immediately. That last detail matters: the canary does not repair the corruption, it converts a silent hijack into a loud, controlled crash. A crash is a denial of service, which is bad — but it is vastly better than the attacker quietly running code, so the trade is almost always worth taking.
Now read the canary as a precise rule and its cracks appear. It assumes the overflow is linear and contiguous — but a write that jumps over the canary defeats it without ever touching it. A use-after-free or an arbitrary write that targets the return address directly skips the guard entirely. And if the attacker can read the canary first — via an info leak — they simply rewrite the same value back over it, and the epilogue check passes. The canary is a strong, cheap defense against the textbook contiguous stack overflow, and almost useless against the non-linear writes and read-then-write leaks that modern exploits favor. That is the pattern of the whole arms race: a wall built for last decade's attack, sidestepped by this decade's.
CFI and the shadow stack: guarding the jump itself
Canaries guard one target, the return address, and only against one shape of write. The most ambitious wall, control-flow integrity (CFI), guards the jump itself. Its insight is structural: a correct program's control flow is not arbitrary. Every indirect call through a function pointer or C++ vtable is supposed to land on the start of some real function, and every return is supposed to go back to the exact site that called the current function. A hijack, by definition, violates one of these. So CFI computes, at compile time, the set of legal destinations for each indirect transfer, and inserts a runtime check: before taking an indirect call, verify the target is in the allowed set; if not, abort. The attacker can still corrupt the pointer — but the corrupted value now has to also be a legal destination, and most gadget addresses are not.
There is a natural split here. Forward-edge CFI guards indirect calls and jumps — the function-pointer and vtable transfers — by checking the target against the compile-time set. Backward-edge CFI guards returns, and the cleanest mechanism for that is the shadow stack. The idea is direct: keep a second, separate, protected stack that stores only return addresses. On every call, push the return address to both the normal stack and the shadow stack. On every return, compare the two; if the address on the normal stack does not match the shadow copy, something overwrote it — abort. Because the shadow stack holds nothing an overflow can reach (it lives elsewhere in memory and is hardware-protected), a smashed return address is caught with certainty, not probability — no secret to leak, unlike a canary.
These are not just papers — they ship. Intel CET provides a hardware shadow stack and an endbranch instruction that marks every legal indirect-call landing site, so the CPU itself faults on a stray jump. ARM offers pointer authentication (PAC), which signs a pointer's spare high bits with a secret key and verifies the signature before use, so a forged return address fails its check. Clang and GCC ship software CFI you can enable today. The honest caveat: forward-edge CFI is only as tight as its allowed-target set, and that set is often coarse — if many functions share one signature, the attacker still has a menu of legal targets to pick from. CFI shrinks the attack surface dramatically; it rarely shrinks it to zero.
Reading the whole board
Step back and the four walls line up as answers to four steps of guide 2's attack. Walk the chain and you can name, for each defense, the exact precondition it removes — and the exact thing the attacker must restore to win.
- NX removes "my injected bytes run." The attacker restores it by not injecting — reuse existing executable code via ROP, and NX never objects because every gadget lives on a legal executable page.
- ASLR removes "I know where gadgets are." The attacker restores it with an information leak — read one true runtime address, subtract its fixed offset, recover the random base, recompute the whole layout.
- The canary removes "I can overwrite the return address quietly." The attacker restores it by leaking the canary and writing it back unchanged, or by using a non-linear write that jumps over the guard entirely.
- CFI and the shadow stack remove "I can redirect a transfer anywhere." The attacker restores it by aiming only at legal destinations — exploiting a coarse allowed-set, or finding bugs that corrupt data rather than control flow.
Two honest conclusions close the loop. First, defense in depth is the point: each wall is individually beatable, but stacking NX, ASLR, canaries, and CFI means a single bug is rarely enough — the attacker now needs a chain of bugs (typically a leak to beat ASLR, plus a corruption that also satisfies CFI), and each added requirement is one more thing that might not exist in a given program. The price of an exploit, not its impossibility, is what these mitigations sell. Second, this is why the defensive frontier is moving upstream, to languages that refuse to produce the wound at all — which is exactly the contrast with Rust the foundations rung drew, and where guides 4 and 5 head next, into sandboxing and the side channels that bypass memory safety altogether.