Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why → how

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.

Security intermediate Apr 29, 2026 · updated Aug 25, 2026 · 11 min read

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.

find a buffer overflow the foothold overwrite the saved return address point it at a known address 0x7ffff7a52390 that address is system() inside the C library — and it was the same on every machine running the same binary on the same OS The bug is the foothold. The predictable layout is what makes it code execution. write the number down once, and exploitation reduces to plumbing: get the target to jump there
That one number is the running example for the rest of the post. The hidden assumption isn’t the bug — it’s the fixed address, and it is the assumption ASLR attacks.

2 · The naive fix, and why nothing would run

You can’t give every function its own random address.

randomise every function every internal reference breaks — and there are millions of them randomise the base same block, new spot everything inside slides together, so internal references stay correct — they were already relative
Virtual memory does the work: every process already sees its own private address space, and ASLR is a small change to where each chunk gets mapped. But look at what that buys the attacker back — and it is the whole rest of the post.

3 · The price of that fix

It hid one number per region, not one address.

libc, wherever it landed this run base system() a fixed offset, fixed at build leak any one pointer into this block, and the rest is addition One leaked pointer undoes a whole region’s worth of randomness.
Which is why the modern exploit is a two-step dance rather than a jump: first leak a pointer, then compute everything from it. Serious browser and kernel write-ups have an “info leak” stage near the top for exactly this reason.

4 · Where it leaks

A probabilistic defence with four well-known holes.

too few bits 32-bit systems gave about 16 bits for the library base 64-bit closed this one fork inheritance children share the parent’s layout, so crash-and-retry gets unlimited guesses info leaks any bug that reveals a pointer collapses that whole region the usual route side channels the hardware accidentally tells you where the kernel is partially mitigated since So ASLR doesn’t prevent exploitation. It raises the price. it forces the attacker to find and chain two bugs instead of one — which is why modern write-ups read as chains rather than single tricks on x86_64 Linux the mmap base gets 28 bits by default, so blind guessing is not the way in; the left-hand box is history, not advice
Note that the second box is the exception to “you must leak something”: where a layout can be re-tried, guessing competes with leaking. The standard mitigation is to re-randomise on exec, not on fork.

5 · Keep this card

The whole thing on one index card.

ASLR = virtual memory + a random base offset, per region, per process ∴ the offsets inside a region stay fixed — which is the catch
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. Where the analogy breaks: 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.

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:

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:

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.

Going deeper