The annoying gap in many 64-bit ROP problems: you have pop rdi, but no
pop rsi and no pop rdx, so you can't set up a three-argument call like
write(1, buf, n) or execve(path, argv, envp). Before you go hunting for
ret2dlresolve or a pivot,
check __libc_csu_init — the C runtime startup function the linker bundles into
nearly every dynamically-linked binary contains a near-universal argument-setting
gadget.
- arch
- amd64-64-little
- linkage
- dynamic, has __libc_csu_init
- gap
- pop rdi exists; pop rsi / pop rdx do not
the two halves#
The tail of __libc_csu_init disassembles into a pop block and a mov/call
block, back to back:
|
Chain them: gadget 1 loads the registers, gadget 2 spreads them into the
argument registers and calls through [r12+rbx*8].
driving it#
To call func(arg1, arg2, arg3):
rbx = 0,rbp = 1— so after the calladd rbx,1; cmp rbx,rbpis equal andjnefalls straight through (no second iteration);r12 = &ptrwhere*ptr == func— the call is indirect, through memory, so pointr12at a slot holding the target (a GOT entry works);r13 = arg3(→ rdx),r14 = arg2(→ rsi),r15 = arg1(→ edi).
After the call, execution hits the six pops again, so pad the stack with six
dummies before your next gadget.
|
args set, libc leaked, no pop rsi in sight
write(1, puts@got, 8) dumps a libc pointer; rebase, and the second stage is a
one_gadget or system:
segfault{the_runtime_left_me_a_gadget}
the two catches#
mov edi, r15dis 32-bit. The first argument is truncated to 32 bits — fine for anfdlike1, useless for a 64-bit pointer. So ret2csu is perfect forwrite(1, …, …)leaks; for a call needing a pointer inrdi(likeexecve("/bin/sh", …)), pair it with a realpop rdifor the first arg and let csu handlersi/rdx.- Modern binaries may not have it. Around glibc 2.34 the startup was
reorganised and
__libc_csu_initdisappeared from new builds. On those, fall back to other universal gadgets (__libc_start_mainfragments) or ret2dlresolve.
ret2csu is the reminder that a binary's gadget set is bigger than its own code —
the runtime glue the linker staples on carries a three-argument call primitive,
sitting there in nearly every dynamically-linked ELF, waiting for the day you
can't find a pop rdx.