Defenses: ASLR, DEP, and Stack Canaries

A stack buffer overflow used to be nearly game over for a program's security. An attacker who could smash the stack knew exactly where their shellcode lived in memory. The processor happily executed whatever bytes it found there. Three runtime mitigations changed that calculus. They did this without asking most application developers to change a single line of code: Address Space Layout Randomization (ASLR), Data Execution Prevention (DEP), and stack canaries. Each defense attacks a different assumption baked into classic Code Injection Attacks. Canaries attack the assumption that an overflow can silently overwrite a return address. DEP attacks the assumption that injected data can later be executed as code. ASLR attacks the assumption that an attacker knows where anything lives in memory. None of these defenses is individually sufficient. Understanding why each one can be bypassed motivates the next generation of exploitation techniques. This is especially true for Return-Oriented Programming. This article treats all three as a coherent system. We examine how they are implemented in the compiler, the OS loader, and the CPU. We look at what invariant each one enforces. We also examine the precise conditions under which that invariant breaks. We derive the entropy math behind ASLR. We walk through the canary-check state machine instruction by instruction. We trace a worked exploit attempt against a program protected by all three defenses simultaneously. By the end, you should be able to explain more than "these defenses exist." You should understand why a competent attacker treats them as raising the cost of exploitation rather than eliminating it. This is the exact insight that leads into information leaks, Return-Oriented Programming, and eventually full process Sandboxing and Isolation.

Key takeaways
  • Stack canaries insert a secret value before the return address and check it before returning, turning silent corruption into a detectable crash.
  • DEP/NX marks memory pages as either writable or executable but never both, blocking classic shellcode injection into stack or heap.
  • ASLR randomizes the base addresses of the stack, heap, libraries, and (with PIE) the executable itself, so absolute addresses are unpredictable.
  • Each defense targets one specific attacker assumption and can be defeated when that assumption is restored — via leaks, brute force, or non-control-data attacks.
  • Canaries do not stop non-return-address overwrites; DEP does not stop code-reuse attacks; ASLR does not stop info leaks or low-entropy platforms.
  • Together they form defense-in-depth: bypassing all three simultaneously is dramatically harder than defeating any one alone.
The core insight

Each mitigation neutralizes one specific assumption an exploit needs — a known address, an executable payload location, or an undetected overwrite — so real attacks must break the assumptions of two or more mitigations at once.

The idea

A memory-corruption exploit is a chain of assumptions. The attacker assumes they can overflow a buffer to overwrite adjacent memory (the vulnerability itself, discussed under Code Injection Attacks). Then they assume they know where their payload or a useful gadget lives (an addressing assumption). Then they assume the memory holding their payload is executable (an execution assumption). Finally, classic overflows assume the corruption of control data — like a return address — goes unnoticed until it is used (an integrity assumption).

Stack canaries attack the integrity assumption. The compiler places a secret, unpredictable value — the canary — directly below the saved return address on the stack. Any linear buffer overflow that reaches the return address must first overwrite the canary. Before the function returns, generated epilogue code re-reads the canary and compares it to the known-good value; a mismatch triggers immediate program termination rather than a return to attacker-controlled code.

DEP (Data Execution Prevention), also called NX (No-eXecute) or W^X (write XOR execute), attacks the execution assumption. Using a hardware feature — a no-execute bit in the page table entry — the operating system marks stack, heap, and other writable data regions as non-executable. Injected shellcode may still land in memory, but the CPU refuses to fetch instructions from that page, raising a fault instead.

ASLR attacks the addressing assumption. At process load time, the OS randomizes the base addresses of the stack, the heap, shared libraries, and (when the binary is compiled Position-Independent, PIE) the main executable image itself. An attacker who does not know these addresses cannot hardcode a return-to-libc target or a Return-Oriented Programming gadget chain, because the same exploit code will land at a different offset on every run — and almost certainly crash the process instead of hijacking it.

How it works

Stack canary mechanism. The compiler inserts prologue and epilogue instrumentation into every function whose stack frame contains a vulnerable buffer (heuristically: local arrays, especially of `char`). At process start, a random canary value is generated once — often derived from `/dev/urandom` — and stored in a fixed location (a thread-local `fs`/`gs` segment slot on Linux/x86, for example). Every protected function copies that master value onto its own stack frame at prologue time and re-checks it at epilogue time. ``` // Compiler-generated instrumentation (conceptual) GLOBAL_CANARY = read_random_word() // set once at process/thread start function f(args...): // ---- prologue ---- frame.canary = GLOBAL_CANARY // copy secret onto stack allocate_locals() // e.g., char buf[64] // ---- function body ---- do_work(args) // vulnerable code may run here // ---- epilogue ---- if frame.canary != GLOBAL_CANARY: call __stack_chk_fail() // abort: log + terminate else: return normally ``` A linear overflow of `buf` writes forward in memory toward the saved frame pointer and return address. Because the canary sits between the buffer and the return address, the overflow must overwrite the canary first — corrupting it — before it can reach the return address, and the epilogue check catches this before control transfer occurs.

DEP/NX mechanism. This is enforced by the memory management unit (MMU), not the compiler. The OS loader sets the NX bit in the page table entry for every page that is writable but should not be executable — the stack, the heap, and `.data`/`.bss` segments. Code pages (`.text`) are marked executable but read-only. ``` // Conceptual page table entry PTE { physical_frame: ... present: 1 writable: W no_execute: X // hardware NX/XD bit } // Loader policy (W^X) for each segment in binary: if segment.is_code: set(writable = 0, no_execute = 0) // R-X else: set(writable = 1, no_execute = 1) // RW-, never executable ``` When the instruction pointer attempts to fetch from a page with `no_execute = 1`, the CPU raises a general protection / page fault before executing a single injected instruction. The OS handler treats this as a fatal security violation and kills the process.

ASLR mechanism. At `exec()` time, the OS kernel chooses random offsets for each major memory region and applies them when constructing the process's virtual address space. ``` // Conceptual process loader logic function load_process(binary): stack_base = STACK_BASE_DEFAULT + random_offset(entropy_bits_stack) heap_base = HEAP_BASE_DEFAULT + random_offset(entropy_bits_heap) lib_base = LIB_MMAP_BASE + random_offset(entropy_bits_mmap) if binary.is_PIE: exe_base = PIE_BASE_DEFAULT + random_offset(entropy_bits_pie) else: exe_base = FIXED_LOAD_ADDRESS // legacy non-PIE: no randomization map_segments(binary, exe_base) map_stack(stack_base) map_heap(heap_base) for each shared_library in binary.dependencies: map_library(shared_library, lib_base + running_offset) return entry_point(exe_base) ``` Randomization typically happens once per process launch (or once per `exec`), not per instruction — so within a single running process, addresses are stable, but they differ across runs and across machines, defeating hardcoded exploit addresses.

Figure
Stack layout: canary and NX interacting with a buffer overflow
Stack frame under attack — canary intercepts the overwrite high address (top of frame) low address (buffer start) return address saved frame pointer canary (secret value) buffer: char buf[64] (vulnerable local array) overflow writes upward epilogue checks: canary == secret? if check passes but target page is NX → fault ASLR: absolute addresses of every region above shift per run

Complexity & correctness

Stack canary. Time overhead: O(1) additional instructions per protected function call — one write in the prologue, one compare-and-branch in the epilogue. Empirically this is a small constant-factor slowdown (typically a few percent) since it does not scale with buffer size or program size. Space overhead: O(1) extra word per stack frame. Correctness argument: the canary is placed at a fixed offset directly adjacent to (below, in growth order) the saved return address, chosen by the compiler's stack layout algorithm. A linear, contiguous overflow that starts inside the buffer and grows toward higher addresses must pass through the canary's memory location before reaching the return address's location. Since the canary value is secret (drawn from a high-entropy source, typically ≥32 bits, and containing a null byte to also break naive C-string copy overflows), the probability an attacker overwrites it with the correct value by guessing is at most 2−b2^{-b} for a bb-bit canary, assuming no leak of the canary itself. This is why the invariant "corruption reaching the return address implies corruption of the canary" holds for the overflow class canaries were designed against — but the invariant does not hold for overflows that jump directly to a target (arbitrary write primitives, format string writes, or overflows of a separate stack variable that is not adjacent to the canary).

DEP/NX. Time overhead: effectively zero in the steady state — the check is a single hardware bit test performed by the MMU on every instruction fetch, already part of normal page-table-walk hardware. Space overhead: zero additional memory; NX reuses an existing bit in the page table entry format. Correctness argument: the invariant enforced is "no page is simultaneously writable and executable" (W^X). Because the CPU — not software — checks the NX bit on every fetch, this invariant is enforced even against arbitrary corruption of kernel data structures the attacker cannot directly touch from user space. The guarantee is airtight for the specific attack of injecting fresh instruction bytes and jumping to them. It says nothing about reusing already-executable, already-present code — the entire premise behind return-to-libc and Return-Oriented Programming, which route control flow through legitimate `.text` pages that are (correctly) marked executable.

ASLR. Time overhead: negligible — a handful of extra random-number-generation and offset-arithmetic operations at process load time, not per-instruction. Space overhead: zero extra memory; it changes where things are mapped, not how much. Correctness argument (entropy math): Let a region's base address be randomized over 2n2^{n} possible aligned positions ("n bits of entropy" — on 64-bit Linux, stack entropy is commonly around 28–33 usable bits depending on configuration; on 32-bit systems it can be as low as 8 bits for some regions). If an attacker must guess the correct base to build a working exploit and has no other information, the probability of a single blind guess succeeding is

P(success)=2−n.P(\text{success}) = 2^{-n}.

Crucially, ASLR's real-world strength depends on whether failed guesses are cheap to retry. In a forking network daemon that doesn't re-randomize per connection (a "brute-forceable" service, historically exploited via `fork()`-server ASLR bypass), an attacker can attempt kk guesses; if each wrong guess merely crashes and restarts the child (inheriting the same randomized base from the parent), the effective success probability across kk attempts is

P(success within k tries)=1−(1−2−n)k≈k⋅2−n for k≪2n,P(\text{success within } k \text{ tries}) = 1 - (1 - 2^{-n})^k \approx k \cdot 2^{-n} \text{ for } k \ll 2^n,

which is why low-entropy 32-bit ASLR (n ≈ 8–16 bits) is brute-forceable in seconds to minutes, while 64-bit ASLR with n ≈ 28+ bits makes blind brute force impractical — but not impossible in principle, and trivial if an information leak reveals even one address in the target region (since offsets within a region are typically fixed, one leaked pointer defeats that region's ASLR entirely).

Honest tradeoffs. None of these is a proof of security; each is a probabilistic or structural obstacle. Canaries only guard sequential overflows that must cross their memory location — they do nothing against arbitrary-write bugs, heap overflows into function pointers, or non-adjacent stack variable corruption. DEP does nothing against code-reuse attacks that never inject new code. ASLR does nothing once any pointer is leaked, and does nothing against attacks that require no addresses at all (like some data-only attacks). This is why practitioners describe them collectively as raising the bar, not closing the door — the natural segue into why Return-Oriented Programming emerged as a direct response to DEP, and why info leaks became a first-class exploit primitive as a direct response to ASLR.

Worked example

Consider a vulnerable C function compiled with all three mitigations enabled (`-fstack-protector-all`, NX enabled by the loader, ASLR enabled by the kernel): ```c void handle_request(char *input) { char buf[64]; strcpy(buf, input); // classic unbounded copy — the vulnerability } ``` Trace the attacker's attempt, step by step, against the fully mitigated binary:

StepAttacker actionDefense encounteredResult
1Sends 64-byte input filling `buf` exactlynone triggered yetcopy completes, no overflow yet
2Sends 200-byte input, overflowing `buf` into adjacent stack memoryStack canary — overflow must cross the canary slot to reach the return addresscanary bytes overwritten with attacker data (garbage, since attacker doesn't know the secret)
3Function returns; epilogue executes `if (canary != secret)`Stack canary checkmismatch detected → `__stack_chk_fail()` called → process aborts immediately, return address never used
4Attacker adapts: crafts payload preserving the correct canary bytes by leaking the canary via a separate bug (e.g., a format-string read)Canary bypassed for this attemptoverflow now reaches and overwrites the return address successfully
5Attacker sets return address to the start of their shellcode, injected inside `buf` on the stackDEP/NX — stack page is marked non-executableCPU fetch from stack page faults; process killed before any injected instruction runs
6Attacker pivots: instead of shellcode, targets a `system()` libc function or ROP gadget already present in executable memoryDEP is now irrelevant (no new code injected) — but ASLR randomizes libc's base address every runattacker does not know libc's/gadget's absolute address this run; hardcoded address guess lands in unmapped or wrong memory → crash
7Attacker adds an information-leak bug (or a print of a stack/heap pointer) to disclose one address at runtimeASLR defeated for that region once a pointer is knownattacker computes the true libc base from the leak, builds a working ROP chain, succeeds

Row 7 is the point of the whole exercise: all three defenses were individually sound and individually bypassed one at a time, each bypass requiring an additional capability beyond the original overflow — a canary leak, an already-present-code target, and an address leak, respectively. That escalation, needing a leak plus a code-reuse chain, is precisely the machinery formalized in Return-Oriented Programming.

Common pitfalls & traps

PitfallWhy it happensConsequence
Assuming canaries protect all stack variablesCanary only guards the return address / saved registers adjacent to itOverflow into a nearby function pointer or `array[i]`-style adjacent variable (non-adjacent to canary in memory layout) bypasses detection entirely
Treating DEP as "no code execution possible"Confusing "no injected code" with "no attacker control flow"Ignores code-reuse (ROP/JOP) attacks entirely — false sense of completeness
Assuming ASLR entropy is uniform across platforms32-bit address space has far fewer bits to randomize than 64-bitBrute-forcing ASLR on 32-bit or misconfigured 64-bit systems is practical in seconds
Forgetting non-PIE binariesOnly the main executable segment needs `-fpie`/`-pie` to be randomizedA non-PIE binary linked against ASLR'd libraries still has a fixed, guessable code base — a stable ROP source
Reusing canary/secret across forked children without re-randomizing`fork()` duplicates the parent's memory, including its already-chosen canary and layoutA forking server lets an attacker brute-force one byte of the canary at a time across many connections
Believing one leaked pointer is "low severity"Underestimating that offsets within a region are fixed once base is knownA single info leak of any address in a region defeats ASLR for the entire region, not just that pointer

Pioneers & history

Origins · 1 minCrispin Cowan and how this was discovered
CC
Crispin Cowan
1957–2021 · Creator of StackGuard, the first widely deployed stack canary system

Crispin Cowan led the StackGuard project at Oregon Graduate Institute/Immunix in the late 1990s, introducing the compiler-inserted "canary word" as a practical, low-overhead defense against the stack-smashing epidemic documented in the seminal "Smashing the Stack for Fun and Profit." StackGuard's design — a random secret checked at function return — became the template for GCC's `-fstack-protector` and Microsoft's `/GS` compiler flag, both still in daily use across virtually all compiled software today.

History
1996
— Aleph One's "Smashing the Stack for Fun and Profit" popularizes stack buffer overflow exploitation.
1997
— Crispin Cowan's StackGuard introduces the canary-based return address protection technique.
2000
s — PaX project introduces practical ASLR and NX-emulation (PAGEEXEC) for Linux kernels.
2003
— AMD and Intel introduce hardware NX/XD bits, enabling OS-level DEP without performance-costly software emulation.
2004–2005
— Windows XP SP2 and modern Linux distributions ship ASLR and DEP as default, broadly deployed mitigations.

Where this matters

Every mainstream operating system and compiler toolchain in production today implements all three defenses by default. GCC and Clang enable stack-protector variants (`-fstack-protector-strong` is default on many Linux distributions); MSVC ships `/GS` by default on Windows. The Linux kernel, Windows, macOS, iOS, and Android all implement ASLR for user-space processes and increasingly for kernel space itself (KASLR). Hardware NX/XD support is universal on modern x86-64 and ARM64 CPUs, and OS loaders universally apply W^X policy to process memory maps. Beyond general-purpose OS security, these mitigations are foundational building blocks referenced by security certifications, secure coding standards (CERT C, MISRA), and compliance regimes for embedded and automotive software. Browsers (Chrome, Firefox, Safari) layer additional JIT-hardening and Control-Flow Integrity on top of these baseline defenses precisely because JIT-compiled code creates writable-and-executable regions that undermine plain W^X — a tension that motivates more advanced isolation techniques. Mobile platforms (iOS, Android) combine ASLR/DEP/canaries with code signing and hardware-backed pointer authentication (ARM PAC) as an additional layer. Understanding exactly what these three mitigations do — and precisely what they leave open — is the direct prerequisite for understanding both Return-Oriented Programming, the technique developed specifically to survive DEP, and modern Sandboxing and Isolation architectures, which assume individual mitigations will eventually be bypassed and instead confine the blast radius of a successful exploit.

Ask the tutor: How does a leaked stack or heap pointer get turned into a full working ROP exploit chain in practice?

Do stack canaries protect against all buffer overflows?

No. Canaries only detect overflows that are contiguous and must pass through the canary's stack location to reach the return address or saved registers. They do nothing for heap overflows, overflows that corrupt a different local variable without reaching the canary, or arbitrary-write primitives that jump straight to a target address without a linear walk through memory. They also assume the canary's secret value itself is never disclosed; if a separate bug (like a format-string vulnerability or an out-of-bounds read) leaks the canary, an attacker can simply reproduce it in their overflow payload and the check will pass trivially.

Can DEP be bypassed without disabling it?

Yes, and this is the standard modern approach: return-to-libc and <a class="wiki-link" href="/wiki/cs-sec-return-oriented-programming" data-track="wiki_article.body.wiki_link">Return-Oriented Programming</a> attacks never inject new executable bytes at all. Instead they reuse code that is already present and legitimately marked executable — library functions like `system()`, or short instruction sequences ("gadgets") ending in a return instruction, chained together purely by manipulating the stack's sequence of return addresses. DEP's guarantee ("no fetch from writable memory") remains fully intact and unbroken throughout; the attack simply never needs to fetch from writable memory in the first place.

Why is 32-bit ASLR considered weak compared to 64-bit ASLR?

The amount of usable entropy for randomizing a memory region is bounded by how many address bits are available beyond what's needed for the region's size and alignment constraints. A 32-bit address space (4 GB total) leaves very few spare bits after accounting for the kernel, existing mappings, and page alignment — often only 8 to 16 bits for some regions. A 64-bit address space, despite not using all 64 bits practically, leaves tens of bits of genuine entropy (commonly 28+ bits for the stack, more for `mmap`-based regions). Since brute-force cost grows as 2n2^n, the gap between 16 bits (65,536 tries, feasible in seconds over a fast network) and 28+ bits (hundreds of millions of tries) is the practical difference between an ASLR bypass being trivial versus infeasible by brute force alone.

If a program uses all three defenses, is it safe from memory-corruption exploits?

No single combination of runtime mitigations provides a complete guarantee; they raise the cost and complexity of exploitation rather than eliminating the underlying bug. A sufficiently capable exploit chain typically combines an information leak (defeating ASLR for at least one region), a code-reuse technique like Return-Oriented Programming (sidestepping DEP), and a corruption path that avoids canary-protected memory (sidestepping stack protection) or leaks the canary directly. The underlying memory-safety bug — the buffer overflow, use-after-free, or type confusion — is the root cause; these mitigations are compensating controls, not a substitute for fixing or avoiding the bug in the first place, e.g., via memory-safe languages or bounds-checked containers.

Why don't canaries add noticeable performance overhead if they run on every function call?

The instrumentation is extremely lightweight: one extra memory write in the prologue (copying an already-loaded register value onto the stack) and one memory read plus a comparison and conditional branch in the epilogue. Both operations are O(1), independent of buffer size or program complexity, and modern CPUs execute such simple register/memory operations in a fraction of a nanosecond, often overlapped with other pipeline work. Empirical measurements on real workloads typically show overhead in the low single-digit percentages, which is why stack protection is enabled by default in most modern compilers rather than treated as an opt-in performance tradeoff.

How does ASLR interact with Position-Independent Executables (PIE)?

Ordinary ASLR randomizes the stack, heap, and shared library (`mmap`) base addresses, but a non-PIE executable is still loaded at a fixed, predictable base address for its own code and data, because non-PIE binaries hardcode absolute addresses that only work at one fixed load location. PIE compilation generates code using only relative addressing, allowing the OS loader to place the executable itself at a randomized base too — closing what would otherwise be a stable, guessable source of ROP gadgets even with full ASLR enabled elsewhere. Most modern Linux distributions and macOS now compile user-space binaries as PIE by default for exactly this reason.

What comes after these three mitigations if an attacker can still find a way through?

Because leaks defeat ASLR and code-reuse defeats DEP, the security community developed further layers: Control-Flow Integrity (CFI) restricts indirect jumps/calls to a validated set of legitimate targets, directly countering Return-Oriented Programming gadget chaining; hardware features like ARM Pointer Authentication cryptographically sign return addresses and function pointers; and process-level Sandboxing and Isolation (seccomp filters, containers, site isolation in browsers) assumes an exploit may eventually succeed and instead limits what a compromised process can actually do to the rest of the system.

Before this

Where this leads