NX enabled means the stack isn't executable, so you can't run your own bytes —
you run the program's. Return-oriented programming chains together short
instruction sequences that end in ret ("gadgets") into a new program. The
skill is half finding gadgets and half not needing the ones that aren't there.
the search tools#
Two do almost the same job; I keep both because their search syntax differs and sometimes one parses a weird binary the other chokes on.
|
| need | command |
|---|---|
| every gadget | ROPgadget --binary ./chal |
| one specific gadget | ropper -f ./chal --search "pop rdi; ret" |
| a string's address | ROPgadget --binary ./chal --string "/bin/sh" |
| an auto execve chain | ROPgadget --binary ./chal --ropchain |
| chain with bad bytes excluded | ropper -f ./chal --chain execve --badbytes 000a |
search libc, not just the binary
A small statically-linked binary has few gadgets; libc has a universe of them.
Once you've leaked a libc base, run the same searches against ./libc.so.6 and
rebase the offsets. Most "impossible, no gadgets" situations are just looking
in the wrong object.
one_gadget#
The single most time-saving libc tool. It finds the addresses where one jmp
lands you in an execve("/bin/sh", ...) — no chain required — and, crucially,
prints the constraints that must hold at that moment:
Read the constraints, don't just spray the offsets. Half of "the one_gadget
doesn't work" is a register that isn't NULL when you jump. If none fit, set up
the registers first or fall back to a system("/bin/sh") chain.
let pwntools assemble it#
Hand-packing a chain is error-prone; ROP does the bookkeeping and even finds
simple gadgets itself:
|
rop.call("system", [arg]) figures out it needs pop rdi; ret, finds it, and
orders the chain. rop.find_gadget(["pop rdi", "pop rsi", "ret"]) asks for a
specific one. rop.migrate(new_stack) pivots when the buffer is too short for
the whole chain.
when the gadgets aren't there#
Three techniques for the awkward binary — no pop rdx, no /bin/sh, almost no
gadgets at all. Each folded; open the one you're stuck on.
ret2csu — control rdx without a pop
Older glibc-linked binaries contain __libc_csu_init, whose tail has a
universal gadget: a pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
feeding a mov rdi, r13; mov rsi, r14; mov edx, r15d; call [r12+rbx*8]. That
lets you set the first three argument registers and call through a pointer —
the classic way to get rdx (the syscall's third arg) when there's no
pop rdx; ret anywhere.
|
Newer toolchains drop __libc_csu_init, so check first — ROPgadget --only "pop|ret"
and look for the six-pop gadget.
SROP — fake a whole register state
If you can call sigreturn (or set rax = 15 and hit a syscall), the kernel
will restore every register from a sigcontext you placed on the stack. One
gadget, total register control:
Ideal for tiny static binaries: you don't need argument gadgets, just a
syscall and a way to set rax.
ret2dlresolve — call a libc function you didn't import
No libc leak and no useful PLT entries? Forge the relocation structures the
dynamic linker uses, and make _dl_runtime_resolve resolve system for you.
pwntools builds the fake Elf64_Rela / Elf64_Sym / string:
Works best on No PIE, Partial RELRO binaries — Full RELRO resolves everything at load and slams this door.
the movaps tax
glibc's system/printf run movaps against a 16-byte-aligned stack. Land
with rsp % 16 == 8 and they SIGSEGV inside libc, which looks exactly like
a wrong offset. Insert one extra bare ret gadget before the call to re-align.
It's the single most common "my chain is correct but it crashes."