ret2csu

ret2csu

TL;DR: The compiler-emitted __libc_csu_init function in any non-PIE x86_64 ELF contains two gadgets that set rbx, rbp, r12–r15 and then call [r12+rbx*8] — enough to satisfy the SysV calling convention without an exhaustive gadget search.

What it is

ret2csu is a return-orientated programming primitive published by Marco-Alvarez (HITB 2018) that abuses the loop epilogue and prologue inside __libc_csu_init, the static initialiser GCC links into every x86_64 ELF using glibc. Because every binary contains it, the gadget pair is “universal” — exploits can call any imported function (e.g. system, execve) without finding pop rdi, pop rsi, pop rdx gadgets individually.

Preconditions / where it applies

  • Non-PIE x86_64 ELF (or PIE with a binary-base leak)
  • glibc-linked binary built with classic SysV _init — most pre-LTO targets
  • Stack-pivot or buffer overflow primitive that can place at least ~7 qwords of controlled data

Technique

The two gadgets:

1
2
3
4
5
6
7
8
9
10
11
12
gadget_pop:
  pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret

gadget_call:
  mov rdx, r15
  mov rsi, r14
  mov edi, r13d
  call qword [r12 + rbx*8]
  add rbx, 1
  cmp rbp, rbx
  jne gadget_call_start
  ; epilogue restores rbx..r15 and returns

Chain layout (right-to-left as it gets pushed):

  1. gadget_pop
  2. 0 → rbx
  3. 1 → rbp (so cmp rbp, rbx+1 is equal and loop exits)
  4. &GOT[target] → r12 (pointer to fn pointer)
  5. arg1 → r13 (lands in edi — 32-bit!)
  6. arg2 → r14 (→ rsi)
  7. arg3 → r15 (→ rdx)
  8. gadget_call
  9. epilogue padding (7 qwords)
  10. next-stage return (e.g. ret2libc to system("/bin/sh"))

Common follow-up: call gets@plt first to read a second-stage chain into .bss, then pivot.

Detection and defence

  • PIE + FORTIFY + RELRO: PIE forces the gadget to depend on a leak; FULL RELRO removes writable GOT
  • Compile-time hardening (-Wl,-z,now, -fcf-protection=full) — Intel CET IBT marks indirect-call targets and breaks the call [r12+rbx*8] step on CET-capable CPUs
  • Newer GCC + LLD strip __libc_csu_init when --no-call-graph-profile-sort and _init/_fini are inlined; that’s why the technique fails on many recent binaries
  • Static signatures: ROP detectors flag the 7-pop epilogue followed by the indirect call

References

Related: ret2libc, srop, heap-exploitation-linux