ASLR: why we shuffle memory before every run
Attackers used to know exactly where your code lived in memory. ASLR reshuffles it every run, so an exploit has to learn the layout before it can use it — which is why modern exploit chains start by leaking a pointer.
On this page
The picture version
Five pictures for a reader who has never written an address down. The prose below fills in the seams the pictures skip.
1 · The problem
One number on a piece of paper turned a bug into a shell.
2 · The naive fix, and why nothing would run
You can’t give every function its own random address.
3 · The price of that fix
It hid one number per region, not one address.
4 · Where it leaks
A probabilistic defence with four well-known holes.
5 · Keep this card
The whole thing on one index card.
Why it exists
If you’ve ever pointed a memory editor at a single-player game — hunt down the address holding your gold, freeze it, buy everything — you have already met Address Space Layout Randomization without being introduced. Write the address down, quit, relaunch, and it’s wrong. Not “the gold moved” wrong: the entire map of the process moved. Which is why trainers that keep working across launches stopped shipping hardcoded addresses and instead look up a module’s base at runtime and add a fixed offset to it — the offsets are stable, the base is not. That reshuffle is ASLR, and it exists because people much less friendly than game modders were also writing addresses down.
Here’s the address they were writing down — the same kind of number, in the same kind of notebook, for a much worse purpose. For decades, exploiting a memory bug followed a depressingly reliable script. Find a buffer overflow. Overwrite the saved return address on the stack. Point it at a known function — say, system() in libc — sitting at a fixed address that was the same on every machine running the same binary on the same OS. Hit run. Shell. That one number, system()’s address, is the running example for the rest of this post.
The fixed-address part is the hidden assumption that made the whole pipeline work. If the attacker can write down, on paper, “system lives at 0x7ffff7a52390,” exploitation reduces to plumbing: get the target to jump there. The bug is the foothold; the predictable layout is what turns the foothold into code execution.
ASLR attacks that assumption directly. Instead of putting libc at the same virtual address every time, the system rolls dice at process start and slides it somewhere random. Same for the stack, the heap, and (with PIE) the main executable itself. On Linux the work is split — the kernel randomizes the stack, the heap break, and the base the shared-library mappings start from; the dynamic loader places the libraries themselves relative to it. The bug still exists. The hardcoded 0x7ffff7a52390 is now wrong. Jumping to it lands in the middle of nothing, and the process dies instead of spawning a shell.
That’s the entire pitch: turn “exploit-once, exploit-everywhere” into “you have to work out the layout first, on this machine, this run.” Usually that means leaking a pointer — and the exceptions, where an attacker can keep guessing against one layout instead, are in the seams section below.
Why it matters now
Every mainstream OS — Linux, macOS, Windows, iOS, Android — supports ASLR and enables it by default, with one caveat worth knowing: on Windows, whether a particular binary gets randomized has historically depended on it being built with /DYNAMICBASE (or on a policy that forces randomization), so “the OS has ASLR” and “this program is randomized” aren’t quite the same claim. It’s one of the load-bearing assumptions of the modern security model: bugs in C and C++ codebases are still common, and ASLR is much of the reason they don’t all turn into trivial RCEs.
It also matters because attackers adapted. The modern exploit isn’t “jump to a known address” — it’s a two-step dance: first leak a pointer to defeat ASLR, then use that leak to compute the real addresses of the gadgets you wanted. Serious browser and kernel exploit write-ups routinely have an “info leak” stage near the top, precisely because ASLR forces one.
The short answer
ASLR = virtual memory + a random base offset per region per process
Picture to keep: a set of shelves that gets slid to a new random spot on the wall every time the program starts — the books on each shelf keep their exact order and spacing, so if you can locate one book you have located all of them. (The analogy breaks in one place: nothing is hidden from the program, which knows all its own addresses. The only party inconvenienced is someone who wrote an address down in advance.)
At process start, random offsets are picked and each major memory region — stack, heap, libraries, and (under PIE) the executable — is slid to a fresh location. The code keeps working because position-independent code reaches its own functions and data by offsets from wherever it happens to be loaded, and because the loader fixes up the handful of references that genuinely need absolute addresses. An address an attacker wrote down ahead of time gets no such fix-up.
How it works
Start from the naive fix and let it break.
Naive attempt: pick a fresh random address for every function, every
buffer, every library, on every run. Then system() really is unfindable.
Why it breaks: nothing would run. A compiled binary is full of internal
references — call this function, load that string — and rewriting every one
of them at load time would be enormously expensive, and it would break the pile of
assumptions that compiled code, linkers and debuggers already make about
where things sit relative to each other.
The fix: randomize bases, not internals. This is where virtual memory does the work. Every process already sees its own private address space; the kernel and dynamic loader decide where each chunk of that space gets mapped. ASLR is a small change to that decision — instead of always mapping libc at the same base, pick a random base within some allowed range — and everything inside the block slides with it, so internal references stay correct because they were already relative.
A rough sketch of which blocks get slid on a typical Linux process:
- Stack — the kernel adds a random offset (some kilobytes) to the stack base when the process starts.
- mmap region / shared libraries —
ld.somaps libc, libpthread, etc. starting from a randomized base, so every library inside gets shifted as a block. - Heap (brk) — the heap break is offset randomly from the end of the executable’s data segment.
- Main executable — only randomized if it was compiled as a PIE. Older non-PIE binaries sit at a fixed address, which is why the major distributions eventually moved to building packages as PIE by default — Fedora from 23, Ubuntu across the whole archive from 17.10. Debian and Red Hat made the shift more gradually and without a single announced cut-over.
But that fix has a price, and it is the whole rest of this post: the
offset from libc’s base to system is fixed by the build of libc, so once
you know the base, system()’s address falls out by addition. A single
leaked libc pointer is enough to defeat library ASLR for the rest of the
exploit. ASLR didn’t hide one address; it hid one number per region.
Where it leaks
The honest version of the ASLR story is that it’s a probabilistic defense with several well-known holes:
- Entropy. On 32-bit systems there just aren’t enough random bits. Shacham et al. measured 16 bits of entropy for the library mapping on the 32-bit Linux ASLR of the day — about 65,000 possible bases, i.e. a few minutes of guessing against a target that survives wrong guesses. 64-bit address spaces leave room for enough entropy that pure brute force stops being the attack.
- Fork inheritance.
fork()copies the parent’s address space, so all children share the parent’s randomization. A crash-and-retry attack against a forking daemon (classic Apache-style prefork) effectively gets unlimited guesses against one layout. The standard mitigation is re-randomizing onexec, not onfork. - Info leaks. Any bug that reveals a pointer — a format-string bug, an out-of-bounds read, an uninitialized struct sent over the wire — collapses ASLR for whatever region that pointer belongs to. This is why “info leak primitive + write primitive” is the standard exploit recipe today.
- Side channels. There’s published research on defeating kernel ASLR via timing and microarchitectural side channels (the broad family that includes KASLR bypasses around the Meltdown era). I’m being deliberately vague on specifics because the details vary by CPU and have been partially mitigated; the shape is “the hardware accidentally tells you where the kernel is.”
So ASLR doesn’t prevent exploitation — it raises the cost. It forces a leak. And forcing a leak forces the attacker to find and chain two bugs instead of one, which is why modern exploit write-ups read as chains rather than single tricks.
Which brings us back to the game trainer. It handled ASLR the same way a modern exploit usually does — refuse to hardcode, find the base at runtime, add the offset — and that’s not a coincidence. It’s the same problem, and the general shape of the answer is the same: learn one address from the running process.
You started with ASLR = virtual memory + a random base offset per region per process. What did this post add? — + the offsets inside a region stay fixed, and that clause is why one leaked pointer is usually enough to
undo a whole region’s worth of randomisation. On a 64-bit target with a
fresh layout per run, that leak is the cheap path; where a layout can be
re-tried — 32-bit address spaces, forking daemons — guessing competes with
it.
How much randomness, and since when
Two things worth pinning down, because vague versions of both circulate.
The entropy. On Linux the mmap region’s randomisation is controlled by
vm.mmap_rnd_bits. On x86_64 the kernel allows 28 to 32 bits and defaults
to the bottom of that range, 28 — so roughly 268 million possible bases for
the shared-library mapping. That is enough that blind guessing is not a
practical attack, which is the whole reason the 32-bit numbers below are a
historical note rather than a current one. It is also per-region and
per-architecture: the stack, the heap and the executable each get their own
draw, and a 32-bit or embedded target may get far less.
The dates. PaX had ASLR on Linux first — its design documents are dated 2003. OpenBSD 3.4, in November 2003, was the first widely used system to enable it by default, which OpenBSD still claims on its own innovations page. Mainline Linux followed in 2.6.12 (June 2005), and Windows in Vista. Apple’s public record is thinner: OS X 10.7 fully supported PIE executables and 10.8 definitely had kernel ASLR, but Apple never published a clean “ASLR arrived in release X” milestone, so this post doesn’t quote one.
Check yourself
Before you go — a daemon accepts connections and fork()s a fresh worker
for each one. Your exploit guesses libc’s base; a wrong guess crashes the
worker, the parent keeps serving. The box is 64-bit with ASLR on. Does the
64-bit entropy save it?
Answer
No, and the reason is fork inheritance rather than entropy. Every worker is
a copy of the parent’s address space, so all of them share one
randomization. Each crash is a free guess against the same layout, and the
attacker can keep guessing until they hit. Entropy only helps when a wrong
guess costs you a fresh layout. The standard mitigation is to re-randomize
by exec-ing a new process rather than serving from forked children.
And one more — a binary compiled without PIE runs on a system with ASLR enabled. The attacker leaks one libc pointer. What did the leak buy them, and what did they already have?
Answer
The leak buys the libc base, and from there every libc address including
system(), because the internal offsets never moved. What they already had
is the main executable: without PIE it loads at a fixed address on every
run, so all of its code and gadgets were known before the leak. That’s
the practical argument for PIE-by-default — a non-PIE binary hands the
attacker a permanently un-randomized region to build from.
Famous related terms
- PIE —
PIE = executable + position-independent code— without this, the main binary sits at a fixed address and ASLR only protects libraries. - KASLR —
KASLR ≈ ASLR for the kernel— randomizes where the kernel itself is mapped; bypassed in spirit by Meltdown-class side channels. - ROP —
ROP = gadgets + a crafted stack— the technique ASLR most directly defangs, because gadget addresses are what gets randomized. - DEP / W^X —
DEP ≈ no executing data pages— ASLR’s older sibling; together they’re why “just inject shellcode” stopped working.
Going deeper
- The PaX project’s ASLR design documentation — the primary source for “which regions did the original design randomize, and what did its authors already know the limits were.”
- Shacham et al., On the Effectiveness of Address-Space Randomization (CCS 2004) — the explainer for “how many bits of entropy do you actually need,” including the measured 16 bits and the brute-force argument that killed 32-bit ASLR.
- Google Project Zero — the rabbit hole: pick any exploit write-up and read the info-leak stage to see what defeating ASLR looks like on a real target rather than in the abstract.