Lion Cipher Lab
ret2csu: borrow __libc_csu_init gadgets to control rdi, rsi, and rdx (in your own lab)
- Penetration Testing / Red Team
- Advanced
- ~90 min
- Free · no account required
Why it matters
The exploitation ladder so far has quietly assumed the RIGHT gadgets exist. ret2libc needed a `pop rdi ; ret` to set the first argument and that was enough, because system("/bin/sh") only takes one. But the moment you need to call something that takes TWO or THREE arguments — execve(path, argv, envp), or a challenge's ret2win() that checks three magic values, or mprotect(addr, len, prot) to make a page executable — you suddenly need to control rsi and rdx as well, and a stripped binary very often contains no `pop rsi ; ret` and no `pop rdx ; ret` at all. ret2csu is the classic answer to…
What you'll learn
- understanding WHEN you reach for ret2csu: a non-PIE binary that is stripped of convenient `pop rsi ; ret` / `pop rdx ; ret` gadgets, but where you still need to control MORE than just rdi to make a call (execve, or a ret2win that checks three arguments) -- the gap between ret2libc (which only needs rdi) and SROP (which needs a syscall primitive)
- what __libc_csu_init actually is: a small initialisation routine the compiler/CRT links into almost every non-PIE C binary to run the program's constructors, and why the exact instruction sequence it ends with is an accidental register-loading goldmine
- the two gadgets, by heart -- Gadget 1 (the 'pop many'): `pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret`, and Gadget 2 (the 'mov ; call'): `mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call [r12 + rbx*8]`
- how the two chain: Gadget 1 loads controlled values into r13/r14/r15 and a call-target pointer into r12, then Gadget 2 copies r15->rdx, r14->rsi, r13d->edi and CALLs the address stored AT [r12] -- so r12 must be a pointer to a pointer, not the function address itself
- the rbx=0 / rbp=1 loop-exit trick: Gadget 2 is followed by `add rbx,1 ; cmp rbp,rbx ; jne <loop>`, so setting rbx=0 and rbp=1 makes the loop run the call EXACTLY ONCE and then fall through cleanly instead of looping forever
- the reproducibility gotcha that breaks naive attempts: glibc 2.34 REMOVED __libc_csu_init, so a binary you compile on a current distro may not contain these gadgets at all -- which is why this lab uses the ROP Emporium ret2csu challenge binary that is guaranteed to carry them
- the judgment and the 'better' path: why compiler-generated CRT code is an unintended-gadget goldmine, how pwntools' ROP() automates the whole csu dance (`rop.call(...)`), and how PIE / newer glibc / a real `pop rdx` change which technique (ret2libc, ret2csu, or SROP) is actually the cheapest way to control your argument registers
Steps in this lab
- Read this first: a downloaded challenge binary, run only on your own machine
- Where the arc left you — and the exact gap ret2csu fills
- What __libc_csu_init actually is (and why it is a gadget goldmine)
- The reproducibility trap: glibc 2.34 REMOVED __libc_csu_init
- Get the Linux toolchain, pwntools, and the ret2csu challenge binary
- Confirm the two gadgets really exist in this binary
- Find the offset and the target you must call
- Chain the two gadgets: load rsi and rdx, then set rdi and call the target
- The pro way: let pwntools' ROP() assemble (and automate) the csu chain
- When ret2csu is the right rung — and how it is defended
- What you learned — and the next rung