Sometimes you have a stack overflow and almost no gadgets — a stripped static
binary, no pop rdi, no useful PLT. Sigreturn-oriented programming turns the
kernel's own signal machinery into the most powerful gadget there is: one syscall
that loads every general-purpose register from memory you control.
- arch
- amd64-64-little
- linkage
- static, stripped
- gadgets
- a 'syscall' and a way to set rax — that's the whole budget
- NXon
- PIEoff
why a signal return is a gadget#
When the kernel delivers a signal, it pushes a frame onto the stack holding a
complete snapshot of the CPU — rax rdi rsi rdx rsp rip, the lot — so the handler
can run and the program can resume exactly where it was. Resuming is the syscall
rt_sigreturn (nr 15 on x86-64): it pops that frame and restores all of
it. The kernel doesn't check that a signal was ever actually delivered. So if you
control the stack and can invoke rt_sigreturn, you control the entire register
file in one shot:
the recipe#
Forge a sigcontext frame that sets up an execve("/bin/sh", 0, 0): rax=59,
rdi=&"/bin/sh", rsi=rdx=0, rip=&syscall_gadget. To trigger the sigreturn
you need rax=15 and a syscall. The cleanest way to land rax=15 with no
pop rax is the read trick — a read() that returns the number of bytes
you send, so sending exactly 15 bytes leaves rax=15:
|
pwntools' SigreturnFrame knows the exact byte layout of the frame, so you
just assign register fields and dump it with bytes(frame).
from a 'syscall' and nothing else, a shell
The forged frame lands execve("/bin/sh", 0, 0) straight from the kernel's
restore path:
segfault{the_kernel_set_my_registers_for_me}
why it's the gadget of last resort#
- it needs only one real gadget — a
syscall(ideallysyscall; ret) — plus a way to getrax=15. Static binaries are full ofsyscalls; - the frame is plain data, so a single large overflow both triggers the sigreturn and carries the frame behind it;
- chain it: each
rt_sigreturncan setripback to yoursyscallandrspforward to the next forged frame, so you can runmprotect→ shellcode, or any syscall sequence, register-perfect, with no other gadgets at all.
NX took your shellcode and a sparse binary took your ROP chain — SROP gives both back, because the richest "gadget" in any Linux binary is the kernel agreeing to restore whatever register state you left lying on the stack.