Pwn-SROP

Pwn-SROP

pwn第12部分学习

PWN-SROP

原理解析

SROP - CTF Wiki

SROP(Sigreturn Oriented Programming,信号返回导向编程) 是一种二进制漏洞利用(Pwn)技术。

它利用了类 Unix 系统(如 Linux)中的信号(Signal)处理机制以及 sigreturn 系统调用,来达到任意代码执行或控制寄存器的目的。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:

image-SROP原理

SROP 的核心思想概括为一句话:利用内核恢复信号上下文的机制,一次性捏造并控制 CPU 的所有通用寄存器。

1. 信号处理与 sigreturn 机制

在 Linux 中,当进程收到一个信号(如 SIGSEGV、SIGINT)时:

  1. 保存上下文:内核会暂停当前程序,将当前所有寄存器的状态(称为 sigcontext / ucontext 结构体)打包压入用户态栈中。
  2. 执行 Handler:内核把控制权交给信号处理函数(Signal Handler)。
  3. 恢复上下文(核心 point):处理结束后,程序会调用 sigreturn 系统调用(系统调用号在 x86-64 上为 15 / 0xf,x86 上为 119)。内核接收到该系统调用后,会从当前栈顶读取刚才保存的结构体,并将其中的值直接覆盖写回 CPU 寄存器,然后恢复程序执行。
x64的关键结构体
c
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 有合法性校验
};
就有点和ret2dlresolve一样,先是保存了栈中当前的寄存器存储,然后跑到内核里边去执行命令

看看他两的区别吧


SROP和ret2dlresolve保存寄存器的机制区别

技术谁在保存/恢复?存放在哪里?目的
ret2dlresolve动态链接器(ld-linux.so 中的 _dl_runtime_resolve 汇编代码)用户态栈(通过 push 压栈)为了在解析符号时不破坏调用函数前的寄存器状态(传递参数用)。
SROPLinux 内核(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. 足够大的可控栈空间写入 Frame1. 启用了延迟绑定(No RELRO 或 Partial RELRO) 2. 存在可控且已知的内存区域(如可写 BSS 段)用于伪造重定位结构体
主要伪造的结构内核态栈帧结构体 ucontext_t / sigcontextELF 符号解析结构体(.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_addr
  • rdx = 0x400
  • rsp = 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 = 0
  • rdx = 0
  • rip = syscall_addr

再次触发 sigreturn,内核解包后直接执行 execve("/bin/sh", NULL, NULL),拿到 Root/Shell。


5.攻击模板

python
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 程序结构分析

image-20261009205136295

参数参数描述
NX enable shellcode使用汇编代码会失效,不具有X执行权限。那syscall汇编可以忽略了。
canary found栈开启了金丝雀保护,可能需要获取金丝雀的值进行栈溢出的绕出保护
SHSTK影子栈保护开启,普通的写操作指令(如 mov、memcpy)无法向该内存区域写入,只有 call、ret 等特定控制流硬件指令或内核态专有指令才能操作。防御ROP

0x02 函数分析

read_exact -> 0x401815

函数解释 强制从标准输入(stdin,文件描述符 0)读取指定长度(a2 字节)的数据,少读一个字节都不行;如果不满或出错,直接终止程序。

image-20261009210123811

confirm_print_job -> 0x0401878

image-20261009210222059z

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

main->0x04018A2

image-20261009210027886

image-20261009213134993

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

image-20261009213302268

在程序里边ASCII输入数字的值是31起步的,read_exact的逻辑是读取前两个字节,按照小端序规则填进栈中变量v4

输入的1 \n就导致了31 0a在程序显示的是 0x0a31超过了0x400,所以还得使用p16去发送字节

简单写一个脚本

python
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()

image-20261009214346227

说明判断成功了,继续分析

read_exact(&print_queue, v4);

print_queue地址指向的是bss段(0x04A9A80),发现这里具有超大的空间存储。说明payload需要写到这里进行执行

image-20261009214715973

image-20261009215153517

那这个程序的攻击流程可以定下来了。

这题没有libc文件,没有后门函数,ret2dlresolve缺乏.dynsym,那栈溢出只剩下SROP用法了


0x03 攻击流程

image-20261009220452533


0x04 payload

一个适合初学计算机专业的小白教程
博客本地管理后台使用教程

评论区

评论加载中...