Loading content...
In traditional computing, memory layout was deterministic. Every time you ran a program, the code loaded at the same address, the stack started at the same location, and the heap grew from the same base. This predictability was convenient for debugging—but catastrophic for security.
Attackers exploiting vulnerabilities needed to know where to redirect execution. With deterministic layouts, this was trivial. The address of system functions like system() or exec() was the same across every installation of a program. Shellcode could be placed at predictable stack addresses. Even return-oriented programming (ROP) gadgets lived at fixed, known locations.
Address Space Layout Randomization (ASLR) shattered this predictability. By randomizing the base addresses of the stack, heap, libraries, and the executable itself, ASLR transforms exploitation from a science into a gamble. Attackers who once knew exactly where to jump now face a lottery with millions or billions of possible addresses.
First deployed in PaX patches for Linux in 2001 and adopted systemwide by Windows Vista (2006), macOS (Leopard, 2007), and all modern mobile operating systems, ASLR is now a foundational pillar of systems security. Understanding its implementation, limitations, and evolution is essential for any serious study of defensive computing.
By the end of this page, you will understand: • The threat model ASLR addresses and why it matters • How operating systems implement ASLR for different memory regions • Entropy calculations and what constitutes "sufficient" randomization • The evolution from partial to full ASLR (PIE, RELRO, etc.) • Information leak attacks and other bypass techniques • Real-world ASLR configurations across major platforms
To appreciate ASLR, we must understand the landscape it transformed. Before ASLR, exploiting a vulnerability often required just two pieces of information:
The second requirement was almost never a challenge. Here's why:
Process Memory Layout (Without ASLR)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0x00000000 ┌────────────────────────────┐ │ │ │ Reserved │ NULL page, kernel space │ │0x08048000 ├────────────────────────────┤ │ │ │ Program Code (.text) │ ← ALWAYS at 0x08048000 on 32-bit Linux! │ │ ├────────────────────────────┤ │ Program Data (.data) │ ← Fixed offset from .text ├────────────────────────────┤ │ BSS / Heap │ ← ALWAYS grows from same base │ ↓ │ │ ... │ │ ↑ │ │ Stack │ ← ALWAYS starts at 0xBFFFFFFF on 32-bit Linux! │ │0xBFFFFFFF ├────────────────────────────┤ │ Kernel Space │0xFFFFFFFF └────────────────────────────┘ On Windows 32-bit: - .text typically at 0x00400000 - kernel32.dll at 0x7C800000 - ntdll.dll at 0x7C900000 Attacker knowledge: ✓ system() is at 0x40069060 (known from libc version) ✓ Buffer is at 0xBFFFF760 (stack address, predictable) ✓ ROP gadgets at fixed offsets in libcThe Return-to-Libc Attack:
Even without the ability to execute injected shellcode (which DEP would later prevent), attackers could redirect execution to existing code. The most powerful target was system() in the C library:
12345678910111213141516171819202122232425
// The vulnerable functionvoid vulnerable(char *input) { char buffer[64]; strcpy(buffer, input); // Buffer overflow} // Attack payload construction:// // Layout of attack buffer:// [64 bytes padding][fake saved ebp][address of system()][return after system][ptr to "/bin/sh"]// ↑ ↑// 0x40069060 (known!) 0xBFFFF7A0 (predictable!)//// When vulnerable() returns:// 1. Pops "fake saved ebp" into EBP (don't care)// 2. Jumps to system() at 0x40069060// 3. system() sees its "return address" and argument on stack// 4. Executes system("/bin/sh")// 5. SHELL SPAWNED! // Without ASLR, the attacker:// 1. Finds system() address: objdump -T /lib/libc.so.6 | grep system// 2. Finds "/bin/sh" in libc: strings -t x /lib/libc.so.6 | grep "/bin/sh"// 3. Constructs payload with these fixed addresses// 4. Exploit works on every machine with same libc versionBefore ASLR, worms could spread using hardcoded addresses. The Code Red worm (2001) exploited a buffer overflow in IIS using a single fixed return address. The worm's payload worked on every vulnerable IIS installation because memory layout was identical.
ASLR introduces randomness into the process memory layout at load time. Instead of fixed addresses, each memory region is positioned at a randomly chosen base address within an allowed range. The key insight is that even small amounts of randomness impose significant costs on attackers.
Process Memory Layout (With ASLR)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Run 1: Run 2:━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━0x565d3000 [Executable] 0x5657a000 [Executable]0x7f12ab00 [libc.so] 0x7f45cd00 [libc.so]0x7f2345000 [ld.so] 0x7f5678000 [ld.so]0x7ffe8123 [Stack] 0x7ffabc45 [Stack]0x5634abcd [Heap] 0x5612345e [Heap] Same program, completely different layout! Attacker now faces: ✗ system() address? Unknown. Different each run. ✗ Stack buffer location? Unknown. Different each run. ✗ ROP gadget addresses? Unknown. Different each run. Probability of correct guess: - 32-bit with 8 bits entropy: 1 in 256 (still exploitable!) - 32-bit with 16 bits entropy: 1 in 65,536 (slow brute force) - 64-bit with 28 bits entropy: 1 in 268,435,456 (impractical!)ASLR randomizes five critical memory regions:
| Region | Randomization Method | Entropy Bits (64-bit) | Alignment Constraint |
|---|---|---|---|
| Executable (PIE) | Random base above 0x555555554000 | ~28 bits | Page-aligned (4KB) |
| Shared Libraries | Random high-memory placement | ~28 bits | Page-aligned (4KB) |
| Stack | Random stack base with guard page | ~22 bits | 16-byte aligned |
| Heap (brk) | Random heap start | ~13 bits | Page-aligned (4KB) |
| mmap regions | Random placement per mmap | ~28 bits | Page-aligned (4KB) |
The effectiveness of ASLR is measured in entropy bits—the log₂ of the number of possible base addresses. More entropy means more guessing for attackers.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
import math def calculate_brute_force_cost(entropy_bits): """Calculate expected attempts needed to guess correct address.""" possible_addresses = 2 ** entropy_bits expected_attempts = possible_addresses / 2 # Average case return expected_attempts def calculate_exploitation_time(entropy_bits, attempts_per_second): """Calculate time for brute-force attack.""" attempts_needed = calculate_brute_force_cost(entropy_bits) seconds = attempts_needed / attempts_per_second return seconds # Typical attack scenariosscenarios = [ ("Local exploit (fast fork server)", 8, 1000), # 1000 attempts/sec ("Remote exploit (slow network)", 16, 100), # 100 attempts/sec ("Modern 64-bit ASLR", 28, 1000), # 1000 attempts/sec] print("ASLR Brute Force Analysis")print("=" * 50) for name, entropy, rate in scenarios: attempts = calculate_brute_force_cost(entropy) time_sec = calculate_exploitation_time(entropy, rate) # Format time nicely if time_sec < 60: time_str = f"{time_sec:.1f} seconds" elif time_sec < 3600: time_str = f"{time_sec/60:.1f} minutes" elif time_sec < 86400: time_str = f"{time_sec/3600:.1f} hours" elif time_sec < 31536000: time_str = f"{time_sec/86400:.1f} days" else: time_str = f"{time_sec/31536000:.1f} years" print(f"{name}:") print(f" Entropy: {entropy} bits") print(f" Possible addresses: {2**entropy:,}") print(f" Expected attempts: {int(attempts):,}") print(f" Attack time at {rate}/sec: {time_str}") # Output:# Local exploit (fast fork server):# Entropy: 8 bits# Possible addresses: 256# Expected attempts: 128# Attack time at 1000/sec: 0.1 seconds ← TOO WEAK!## Remote exploit (slow network):# Entropy: 16 bits# Possible addresses: 65,536# Expected attempts: 32,768# Attack time at 100/sec: 5.5 minutes ← Maybe feasible## Modern 64-bit ASLR:# Entropy: 28 bits# Possible addresses: 268,435,456# Expected attempts: 134,217,728# Attack time at 1000/sec: 1.6 days ← Impractical!On 32-bit systems, address space constraints limit ASLR entropy to ~16-20 bits. The stack, heap, and libraries must fit within 4GB, leaving limited room for randomization. 64-bit systems have vastly more address space, enabling 28+ bits of entropy for each region—making brute force truly impractical.
Traditional executables are linked to load at a fixed address (like 0x400000 on 64-bit Linux). While ASLR randomizes libraries and the stack, the main executable remains at a known location—a significant weakness.
Position Independent Executables (PIE) solve this by compiling the executable to run at any address. Combined with ASLR, PIE randomizes the code segment, eliminating the last fixed address in the process.
PIE requires the compiler to generate position-independent code (PIC) for the executable:
1234567891011121314151617181920212223242526
# Traditional executable (NOT position independent)gcc -o vulnerable vulnerable.cfile vulnerable# vulnerable: ELF 64-bit LSB executable, x86-64, dynamically linked...# ^^^^^^^^^^# Fixed load address # Position Independent Executablegcc -fPIE -pie -o vulnerable vulnerable.cfile vulnerable # vulnerable: ELF 64-bit LSB pie executable, x86-64, dynamically linked...# ^^^^^^^^^^^^^^# Can load at any address # Modern GCC defaults (since ~GCC 6 / Ubuntu 16.10):# PIE is enabled by default! # Verify ASLR on running processcat /proc/$(pgrep vulnerable)/maps# Without PIE:# 00400000-00401000 r-xp ... /path/to/vulnerable ← FIXED!# 7f1234567000-... r-xp ... /lib/x86_64-linux-gnu/libc.so.6 ← Random # With PIE:# 555555554000-... r-xp ... /path/to/vulnerable ← RANDOM!# 7f1234567000-... r-xp ... /lib/x86_64-linux-gnu/libc.so.6 ← RandomPosition independent code cannot hardcode absolute addresses. Instead, it uses RIP-relative addressing—calculating addresses relative to the current instruction pointer:
1234567891011121314151617
; Traditional code (Non-PIE): Hardcoded absolute addressmov eax, DWORD PTR [0x601040] ; Load from fixed global variable ; PIE code: RIP-relative addressinglea rax, [rip + 0x200a3f] ; Calculate address relative to RIPmov eax, DWORD PTR [rax] ; Load from calculated address ; For function calls to external symbols (like printf): ; Non-PIE: Direct call through PLT at fixed addresscall 0x4004f0 <printf@plt> ; PIE: Call through RIP-relative PLT entrycall [rip + got_printf_offset] ; GOT @ rip+offset contains printf addr ; The magic: Code works at ANY base address because all; references are relative to the current instruction pointer.PIE introduces measurable but minimal overhead:
The overhead comes from:
Modern Linux distributions enable PIE by default. Debian, Ubuntu (since 17.10), Fedora, and Arch all ship PIE binaries. On x86-64, the performance impact is negligible. Combined with full ASLR, PIE ensures that no memory address in a process is predictable.
Each major operating system implements ASLR differently, reflecting varying security priorities, compatibility requirements, and technical constraints. Understanding these differences is crucial for security assessment and cross-platform development.
Linux ASLR (since kernel 2.6.12, 2005) is controlled via /proc/sys/kernel/randomize_va_space:
1234567891011121314151617181920212223
# Check current ASLR settingcat /proc/sys/kernel/randomize_va_space # Values:# 0 = ASLR disabled (vulnerable!)# 1 = Conservative: randomize stack, mmap, VDSO (default on older systems)# 2 = Full: randomize stack, mmap, VDSO, heap (current default) # Set to full ASLR (persistent via /etc/sysctl.conf)echo 2 > /proc/sys/kernel/randomize_va_space # Linux ASLR entropy (x86-64):# - mmap base: 28 bits # - Stack: 22 bits# - Heap: 13 bits (brk-based)# - PIE executable: 28 bits# - Shared libraries: 28 bits # Examine randomization across runs:for i in {1..5}; do cat /proc/self/maps | grep -E '(heap|stack|libc)' | head -3 echo "---"done| Feature | Linux (x64) | Windows 10+ (x64) | macOS (Apple Silicon) |
|---|---|---|---|
| Library randomization | Per-process | Per-boot (rebased) | Per-boot (shared cache) |
| Executable randomization | Per-process (PIE) | Per-process (DYNAMICBASE) | Per-process (always PIE) |
| Stack entropy | ~22 bits | ~17 bits | ~16 bits |
| Heap entropy | ~13 bits (brk) | Variable | High (zone randomization) |
| Kernel ASLR | Yes (KASLR) | Yes | Yes |
| High-entropy mode | Default | Requires /HIGHENTROPYVA | Default |
Windows randomizes DLL base addresses at system boot, not per-process. This means if an attacker can observe any process loading a common DLL (like kernel32.dll), they learn its address for ALL processes on that boot. This "system-wide ASLR" provides weaker security than Linux's per-process randomization.
ASLR is a powerful mitigation but not impenetrable. Attackers have developed numerous techniques to defeat or work around address randomization. Understanding these bypasses is essential for assessing real-world security.
The most common and powerful ASLR bypass is leaking a memory address. A single leaked pointer reveals the base address of an entire region:
1234567891011121314151617181920212223242526272829
#include <stdio.h> // Vulnerability: Format string bugvoid vulnerable(char *user_input) { char buffer[256]; snprintf(buffer, sizeof(buffer), user_input); // WRONG! printf("%s", buffer);} // Attack: User sends "%p %p %p %p %p %p %p %p"// Output: 0x7ffd1234567 0x0 0x7f41234567890 0x555555556000 ...// ^^^^^^^^^^^^^^^^^^// libc address! // With this leaked address:// 1. Calculate libc base: leaked_addr - known_offset = libc_base// 2. system() = libc_base + system_offset (from libc version)// 3. "/bin/sh" = libc_base + binsh_offset// 4. ROP gadgets = libc_base + gadget_offsets//// ASLR is now completely defeated for this process! // Example calculation:// Leaked: 0x7f4123456789// Offset of leaked symbol in libc: 0x12389// libc_base = 0x7f4123456789 - 0x12389 = 0x7f4123444400// system() offset in libc: 0x50d60// system() address = 0x7f4123444400 + 0x50d60 = 0x7f4123494d60On 64-bit systems, addresses are 8 bytes but often only the lower 3-4 bytes are attacker-controlled. If the attacker can overwrite only the low bytes of a pointer, they can redirect it within the same memory region without needing to guess the randomized high bytes:
1234567891011121314151617181920212223242526
// Original pointer: 0x00007f4123456789// ^^^^^^^ ^// Randomized Low bytes known relative to base // One-byte overwrite changes: 0x00007f41234567XX// Two-byte overwrite changes: 0x00007f412345XXXX// Three-byte overwrite: 0x00007f4123XXXXXX // Practical attack on off-by-one vulnerability:void vulnerable(char **ptr_table, int index, char *input) { char buffer[32]; int len = strlen(input); // Off-by-one: writes null byte one past buffer for (int i = 0; i <= len; i++) { // Should be < len buffer[i] = input[i]; } // The null byte overwrites the least significant byte of ptr_table[0] // ptr_table[0] was: 0x7f4123456789 (legitimate function pointer) // ptr_table[0] now: 0x7f4123456700 (attacker-controlled offset!) // Attacker chose input to position "00" over the LSB // The new address still points within the same library // Find a useful gadget at an address ending in 0x00...}Subtle attacks can infer ASLR layouts without direct memory reads:
ASLR is most effective when combined with other mitigations. An information leak defeats ASLR but doesn't bypass stack canaries. Code injection defeats stack canaries but is blocked by DEP. A comprehensive attack must defeat ALL layers—significantly raising the bar for exploitation.
Basic ASLR provides significant protection, but modern systems implement numerous enhancements to strengthen randomization and close bypass opportunities. Understanding these hardening techniques is essential for deploying robust defenses.
The Global Offset Table (GOT) contains pointers to library functions. Without RELRO, attackers who can write arbitrary memory often target the GOT to redirect calls:
123456789101112131415161718192021
# Partial RELRO (default)gcc -o program program.c# GOT is writable - vulnerable to GOT overwrite! # Full RELROgcc -Wl,-z,relro,-z,now -o program program.c# GOT is read-only after load - GOT overwrite blocked! # Check RELRO statuschecksec --file=program# RELRO: Partial RELRO <- Vulnerable# RELRO: Full RELRO <- Protected # How it works:# -z relro: Mark non-PLT GOT entries read-only# -z now: Resolve all symbols at load time (enables full GOT protection) # Memory layout with Full RELRO:# .got.plt: Read-only (contains external function addresses)# All dynamic linking resolved at load time# Attacker cannot redirect printf() to system() via GOT!The kernel itself can be randomized, protecting against attacks targeting kernel addresses:
1234567891011121314151617181920212223
# Linux Kernel ASLR # Check if KASLR is enabledcat /proc/cmdline | grep -o 'nokaslr' || echo "KASLR enabled" # KASLR randomizes kernel base address at boot# - Kernel text shifted by random offset# - Module addresses randomized# - Makes kernel exploits unreliable # Boot parameters:# kaslr - Enable kernel ASLR (default on modern kernels)# nokaslr - Disable (useful for debugging only!) # Kernel memory protection stack:# 1. KASLR - Randomize kernel base (~9 bits entropy)# 2. SMEP - Prevent kernel from executing user pages# 3. SMAP - Prevent kernel from accessing user pages# 4. KPTI - Kernel page table isolation (Meltdown mitigation) # Check kernel protections:grep -E 'smep|smap' /proc/cpuinfodmesg | grep -i 'page table isolation'| Flag | Purpose | Command |
|---|---|---|
| PIE | Executable ASLR | gcc -fPIE -pie |
| Full RELRO | GOT protection | gcc -Wl,-z,relro,-z,now |
| Stack Canaries | Stack smashing protection | gcc -fstack-protector-strong |
| Fortify Source | Buffer overflow detection | gcc -D_FORTIFY_SOURCE=2 |
| Stack Clash | Prevent stack-heap collision | gcc -fstack-clash-protection |
| CFI | Control flow integrity | clang -fsanitize=cfi |
123456789101112131415161718
#!/bin/bash# Maximum hardening compilation flags CFLAGS="-O2 -fstack-protector-strong -fstack-clash-protection -D_FORTIFY_SOURCE=2 -fPIE -Wformat -Wformat-security" LDFLAGS="-pie -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack" gcc $CFLAGS $LDFLAGS -o secure_program program.c # Verify all protectionschecksec --file=secure_program # Expected output:# RELRO: Full RELRO# Stack: Canary found# NX: NX enabled# PIE: PIE enabled# FORTIFY: EnabledMost modern distributions (Ubuntu 18.04+, Fedora 23+, Debian 10+) apply these hardening flags by default. If you're using an older system or custom build environment, ensure these protections are explicitly enabled for all security-sensitive software.
ASLR transformed exploit development from a deterministic engineering problem into a probabilistic challenge. While not unbreakable, it dramatically raises the cost and complexity of attacks.
What's Next:
ASLR prevents attackers from knowing where to redirect execution. But what if they still try to execute injected code at a guessed or leaked address? The next page explores DEP (Data Execution Prevention), which ensures that even if an attacker knows an address, they cannot execute data as code. Together, ASLR and DEP form a powerful defensive pair: ASLR randomizes where code is, and DEP enforces what can be code.
You now have a comprehensive understanding of ASLR—its mechanisms, implementations across operating systems, entropy calculations, bypass techniques, and hardening strategies. This knowledge is fundamental for both attacking and defending modern systems.