Security

Address Space Layout Randomization (ASLR)

Shuffle the deck every run so the attacker never knows where to jump

Address Space Layout Randomization (ASLR) is an OS defense that randomizes the base addresses of the stack, heap, libraries, and executable each run, so an attacker can't predict where to jump — forcing them to guess against 8–30+ bits of entropy.

  • First shippedPaX (Linux), 2001
  • Randomizesstack · heap · libs · PIE base
  • 64-bit lib entropy≈ 28–30 bits
  • 32-bit lib entropy≈ 8 bits (≈256)
  • Defeated bya single info leak

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

How ASLR works

Memory-corruption exploits are address-dependent. A buffer overflow that overwrites a return address has to overwrite it with something — the address of injected shellcode, of a useful library function like system(), or of the first gadget in a ROP chain. Every one of those requires the attacker to know, in advance, where that target sits in the victim's address space. For decades that was trivial: a given binary on a given OS loaded its stack, heap, and libraries at the same addresses on every machine, every boot, forever. You could read an address out of a debugger once and hardcode it into an exploit that worked everywhere.

ASLR breaks that assumption. At process startup the loader picks a fresh random offset for each major region of memory and slides the whole region by that amount. The stack starts somewhere random near the top of the address space; the heap and the memory-mapped region (where shared libraries land) get their own random bases; and if the executable was compiled as a Position-Independent Executable (PIE), the program's own code and globals get randomized too. The relative layout inside each region is unchanged — only the base moves — so the program runs identically. But the absolute addresses are now different on every single run.

The effect on the attacker is that the hardcoded address in their exploit now points at random garbage. The overwrite happens, the function returns into the attacker's chosen address, and that address is almost certainly not the gadget they wanted. Best case for them, the process crashes; the exploit fails, and the crash is a loud signal to defenders. ASLR doesn't fix the underlying bug — the overflow still happens — it removes the attacker's knowledge of where, which is the lever that turns a crash into code execution.

The mechanism: one random offset per region

The unit of randomization is the segment, not the byte. When the kernel maps a region, it offsets the base by a random multiple of the page size (4 KB on x86-64). It must be a page-aligned multiple because the MMU maps memory in page units, so the low 12 bits of any address are never randomized — they're the offset within a page and are fixed by the layout. That detail matters for attacks (see partial overwrites below).

The amount of randomness is measured in bits of entropy: if the base can land in any of 2k positions, that region has k bits of entropy, and a blind guess succeeds with probability 2−k. The entropy is bounded by how many page-aligned slots fit in the space the kernel is willing to randomize over:

entropy_bits = log2( randomization_range / page_size )

On 64-bit Linux the kernel randomizes the mmap base over a window large enough for roughly 28–30 bits, so a single blind guess at a library address succeeds about 1 in 228 ≈ 1 in 268 million. On 32-bit Linux the usable virtual space is tiny by comparison and the mmap window yields only about 8 bits — 256 possibilities — which a crash-and-retry loop chews through in seconds. That gulf is the entire reason 64-bit ASLR is meaningfully strong and 32-bit ASLR is mostly a speed bump.

Because the whole region shares one offset, an attacker who learns the true runtime address of any object in a region instantly recovers the base, and from the base recovers every other address in that region (the internal offsets are public — they're in the binary). This is the load-bearing weakness of base-only randomization: leak one pointer, de-randomize a whole library.

When ASLR helps — and when it doesn't

  • Against remote, one-shot exploits — where the attacker has no oracle and no info leak, full ASLR on a 64-bit target makes a reliable single-attempt exploit statistically infeasible.
  • As one layer of defense-in-depth — paired with DEP/NX (no executable data) and stack canaries (detect overflow before return), ASLR forces an attacker to chain multiple bugs rather than exploit one.
  • It does NOT help against an info-leak bug — if the same vulnerable program also leaks a pointer (a format-string bug, an uninitialized-read, an out-of-bounds read), the attacker reads the base and ASLR is bypassed in that one exploit.
  • It does NOT help if you can retry cheaply — a forking network server that respawns each connection at the same randomized base (fork without re-exec on Linux inherits the parent's layout) lets an attacker brute-force or use a crash oracle (BROP).
  • It does NOT help non-PIE code — a main executable compiled without -fPIE -pie loads at a fixed base even with ASLR enabled, handing the attacker a non-randomized gadget supply.

ASLR vs other memory-safety mitigations

ASLRDEP / NX (W^X)Stack canaryCFIShadow stackMemory-safe lang
What it stopsPredicting addressesRunning injected codeStack overflow before returnCalling unintended targetsCorrupting return addressesThe bug itself
Defense edgeAll address useCode injectionBackward edge (ret)Forward edge (jmp/call)Backward edge (ret)Spatial + temporal
Runtime cost≈ 0% (load-time only)≈ 0% (MMU bit)1–5% (prologue/epilogue check)1–10% (software)≈ 0% (Intel CET hardware)0–2× (GC / bounds)
Beaten byOne info leakCode reuse (ROP/ret2libc)Leaking or skipping the canaryAllowed-target gadgetsForward-edge attacks (JOP)unsafe / FFI escape hatches
Per-process overheadNoneNone8 bytes / frameMetadata tablesSeparate shadow regionHeader / tag per object
Needs recompile?PIE for the main binaryNoYes (-fstack-protector)YesCET-aware toolchainRewrite in the language

None of these is sufficient alone, and that's the point. ASLR removes the attacker's knowledge of layout; DEP removes the option of bringing their own code; canaries and shadow stacks catch the corruption; CFI constrains where control can flow. A modern exploit has to defeat all of them at once, which is why real-world chains now routinely start with a separate info-leak bug just to undo ASLR before the real attack begins.

What the numbers actually say

  • 64-bit blind guess: 1 in ~268 million. With 28 bits of mmap entropy, a single blind attempt at a libc address lands correctly with probability 2−28. At one attempt per second a brute force averages over 4 years — and every wrong guess crashes the process, which monitoring catches long before then.
  • 32-bit blind guess: ~256 tries at today's 8 bits. Modern 32-bit Linux gives the mmap base only ~8 bits, so a crash-and-retry loop exhausts the 256 possibilities in seconds. Historically the gap was already fatal: the 2004 paper by Shacham et al. (On the Effectiveness of Address-Space Randomization) brute-forced PaX ASLR's 16 bits of mmap entropy on i386 and popped a real Apache target in about 216 seconds on average. Low entropy is not security.
  • One leaked pointer = full bypass. Because a region shares one base, leaking a single 8-byte pointer collapses 28 bits of uncertainty to zero. The cost of the bypass is one info-leak primitive, not 228 guesses.
  • Runtime cost: effectively zero. Randomization happens once, at load time, when the loader picks offsets and applies relocations. There is no per-instruction or per-call tax, unlike CFI (1–10%) or bounds-checked languages. This near-free cost is why ASLR is on by default essentially everywhere.
  • Page granularity leaves 12 bits fixed. The low 12 bits (4 KB page offset) of every address are never randomized, which is exactly what a partial-overwrite attack exploits.

A JavaScript model of ASLR entropy

You can't run real ASLR in a browser, but you can model the core mechanic — random per-region bases and the success probability of a blind guess — to build intuition:

const PAGE = 0x1000;                 // 4 KB pages → low 12 bits fixed

// Lay out a process with a random base per region.
function randomizeLayout(entropyBits) {
  const slots = 2 ** entropyBits;    // number of page-aligned positions
  const base = (Math.floor(Math.random() * slots) * PAGE) + 0x7f0000000000;
  return {
    base,
    // Internal offsets are PUBLIC — fixed in the binary, not randomized.
    system:  base + 0x52290,
    binsh:   base + 0x1b45bd,
    gadget:  base + 0x0a3c4,
  };
}

// Attacker hardcodes addresses from ONE observed run, then attacks fresh runs.
function attack(entropyBits, attempts) {
  const observed = randomizeLayout(entropyBits);   // recon run
  let hits = 0;
  for (let i = 0; i < attempts; i++) {
    const victim = randomizeLayout(entropyBits);   // fresh randomized base
    if (victim.system === observed.system) hits++; // exact guess required
  }
  return { hits, attempts, rate: hits / attempts };
}

console.log(attack(8,  1_000_000));   // 32-bit-ish: ~1/256 → thousands of hits
console.log(attack(28, 1_000_000));   // 64-bit-ish: ~1/2.7e8 → almost always 0

// An info leak collapses all of it: one true pointer reveals the base.
function bypassViaLeak(leakedSystemAddr) {
  const base = leakedSystemAddr - 0x52290;   // subtract the known offset
  return { base, binsh: base + 0x1b45bd, gadget: base + 0x0a3c4 };
}

The takeaway the model makes visible: brute force scales as 2entropy and is hopeless at 28 bits, but bypassViaLeak is O(1) — a single leaked address minus a known offset recovers everything. That asymmetry is why every serious exploit chases a leak rather than guessing.

Inspecting real ASLR in Python

On Linux you can observe ASLR directly. Each run remaps libraries to a new base; reading /proc/self/maps twice shows the shift. This script measures the empirical entropy of your system's library base:

import subprocess, re, math

def libc_base():
    """Read this process's libc base from /proc/self/maps."""
    with open("/proc/self/maps") as f:
        for line in f:
            if "libc" in line and "r-xp" in line:          # executable libc mapping
                return int(line.split("-")[0], 16)
    return None

def sample_bases(n=2000):
    """Re-exec a tiny program n times; collect distinct libc bases."""
    bases = set()
    code = ("import sys\n"
            "for l in open('/proc/self/maps'):\n"
            "  if 'libc' in l and 'r-xp' in l:\n"
            "    print(l.split('-')[0]); break")
    for _ in range(n):
        out = subprocess.run(["python3", "-c", code],
                             capture_output=True, text=True).stdout.strip()
        if out:
            bases.add(int(out, 16))
    return bases

bases = sample_bases()
# Bases are page-aligned; the varying high bits carry the entropy.
varying = {b >> 12 for b in bases}           # drop fixed 12-bit page offset
print(f"distinct bases seen: {len(bases)}")
print(f"≈ {math.log2(len(bases)):.1f} bits observed (lower bound)")

# Disable ASLR for a single command to prove the mechanism:
#   setarch -R python3 -c "open('/proc/self/maps').read()"   # -R = no randomize
# With -R the libc base is identical every run.

Run it on a 64-bit box and even a few thousand samples will all be distinct — you're sampling a 28-bit space, so collisions are vanishingly rare. Run the same idea inside a 32-bit container and you'll start seeing repeats quickly, because 256 slots fill up fast. The setarch -R trick (or writing 0 to /proc/sys/kernel/randomize_va_space) pins the base, which is exactly how exploit developers and debuggers work against a deterministic layout before re-enabling ASLR.

Variants worth knowing

PIE (Position-Independent Executable). ASLR for shared libraries has been free since libraries are inherently position-independent. Randomizing the main binary requires compiling it as PIE so its code uses PC-relative addressing. Without PIE, the executable is a fixed island the attacker can build a chain from even under ASLR. Most distributions now build PIE by default.

KASLR (Kernel ASLR). The same idea applied to the kernel image at boot, defending against local privilege escalation. It has a rough history with CPU side channels — Meltdown, prefetch timing, and TLB attacks have all leaked the kernel base — which is what pushed kernels toward KPTI (page-table isolation) and finer-grained kernel randomization (FG-KASLR).

Fine-grained / function-level randomization. Instead of one offset per segment, permute the order of functions (or even basic blocks) so that leaking one pointer no longer reveals the rest. It closes the leak-one-know-all weakness but costs binary size, load time, and code locality, so it's deployed selectively.

Re-randomization. Periodically re-shuffle the layout of a long-running process so a leaked address goes stale before it can be used. Research systems (e.g., Shuffler) re-randomize on the order of milliseconds; the hard part is fixing up every live pointer atomically.

Entropy boosters. Patches like Linux's increased mmap entropy and OpenBSD's per-boot library shuffling and stack-gap randomization push the bit count higher, directly shrinking the brute-force and heap-spray success rate.

Common bugs and misconceptions

  • Assuming ASLR fixes the bug. It doesn't. The overflow, use-after-free, or type confusion is still there. ASLR only removes the attacker's address knowledge; the moment they regain it (via a leak), the original bug is fully exploitable.
  • Shipping a non-PIE binary and assuming you're covered. If checksec reports "No PIE," your executable's code and globals are at a fixed address regardless of the OS ASLR setting — a ready-made gadget source.
  • Fork-without-exec servers. On Linux, fork() children inherit the parent's exact memory layout. A pre-forking server that doesn't exec() hands every connection the same randomized base, enabling brute force and crash-oracle attacks like BROP.
  • Ignoring the fixed low 12 bits. Page-granular randomization leaves the bottom 12 bits unrandomized, which is exactly what a partial-overwrite attack abuses: overwrite only the low byte(s) of a pointer to retarget within the same page without needing the random high bits.
  • Trusting low-entropy ASLR. 8-bit ASLR on 32-bit systems is brute-forceable in seconds. Treating it as equivalent to 28-bit 64-bit ASLR is a category error.
  • Forgetting that one leak de-randomizes a whole region. Because a segment shares a single base, leaking any pointer into it (return address, canary, vtable, GOT entry) reveals every other address in that segment.

Frequently asked questions

How much does ASLR actually randomize?

It depends on the architecture and segment. On 32-bit Linux, the shared-library (mmap) base gets only about 8 bits of entropy — roughly 256 possibilities, brute-forceable in seconds. On 64-bit Linux the library base gets about 28–30 bits and the PIE executable base gets around 28 bits, which is why modern exploits pair ASLR-defeat with an information leak instead of brute force.

What's the difference between ASLR and DEP/NX?

DEP/NX makes data pages non-executable so injected shellcode can't run. ASLR hides where the existing code and data live so the attacker doesn't know what address to jump to. They're complementary: DEP forces attackers into code reuse (ROP), and ASLR makes finding the reusable code's address hard. Neither alone is sufficient; together they raise the bar substantially.

Does my program need to be compiled as PIE for ASLR to protect it?

Yes for the main executable. Without Position-Independent Executable (PIE), the program's own code and global data load at a fixed address even when ASLR is on, leaving a non-randomized island of gadgets. Shared libraries are always position-independent, but a non-PIE main binary is a free, fixed base for the attacker.

How do attackers bypass ASLR?

The dominant method is an information leak: a bug that prints or returns a single pointer (a stack canary, a return address, a vtable pointer). Because everything in a region shares one randomized base, leaking one address reveals the whole layout. Other bypasses include brute force on low-entropy 32-bit systems, partial-overwrite attacks that only change the low bytes left unrandomized, and heap-spray on huge address spaces.

What is KASLR and how is it different?

KASLR is ASLR applied to the operating-system kernel — the kernel image and its mappings load at a randomized offset at boot. It defends against local privilege-escalation exploits the same way userspace ASLR defends applications, but it has historically been undermined by CPU side channels (Meltdown, prefetch, TLB timing) that leak kernel addresses, which is why mitigations like KPTI were added.

Why does ASLR only randomize the base address and not every variable?

Relocating each segment by a single random offset keeps all the relative offsets inside it intact, so the loader's relocation work stays O(number of segments) rather than O(every pointer), and code can use cheap PC-relative addressing. The cost is that one leaked pointer de-anonymizes the entire segment — fine-grained randomization (per-function, per-instruction) closes that gap but is far more expensive and rarely deployed.