高级ROP链构造方法(一)-- SROP
在不同版本的 Unix 系统中被使用了 40 多年的 Signal 机制,存在一个很容易被攻击者利用的设计缺陷,针对这种攻击的手法叫做SROP,和传统的 ROP 攻击相比显得更加简单,可靠,可移植,虽然离提出的时间已经过去了许久,但在CTF的赛场上仍然存在着SROP的攻击手法,不过在CTF的SROP更多的是作为一种辅助ROP来使用,让
exp
的编写更加的简便,比如ORW+
SROP。
此攻击手法首次提出是在安全顶会Oakland 2014,原文看的太难受了,看看会议PPT就好了,SROP也算作比较高级一点的ROP了,接下来慢慢看它是怎么攻击的
Signal 机制
在开始介绍SROP之前,肯定要介绍Signal 机制,就拿出老生常谈的一张Signal 信号的调用流程图:
①内核向进程发送Signal 信号,此进程挂起并进入用户态程序,进行ucontext save
,即往栈上压入ucontext
和siginfo
,主要是将所有寄存器压入栈中,以及压入 Signal 信息
最后压入指向 sigreturn
的系统调用地址,需要注意到的一点是,这一切的操作都是在栈上进行,也就是说这段区域在某种程度上是可控的,这就是问题的所在!!!
②跳转到注册过的 signal handler
中处理相应的 Signal。因此,当 signal handler
执行完之后,就会执行 sigreturn
代码
③signal handler
返回后,内核为执行 sigreturn
系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop
回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn
的调用号为 77,64 位的系统调用号为 15
攻击原理
上面已经讲到保存的参数(包括寄存器等信息)都是处于用户态中,并且可以进行读写,再加上内核并不会检查它的参数是否给改变,所以我们就可以伪造这些寄存器的参数,当系统执行完 sigreturn
系统调用之后,会执行 pop
指令来恢复相应寄存器的值,当执行到 rip
时,就会将程序执行流指向 syscall
地址,既然能进入系统调用,那么是不是我们只要在对应的寄存器上伪造我们想要的值就能进入任何想要的系统调用,答案是肯定的!下面又是老生常谈的一张图,结果很明显,如果用下列寄存器的值来执行系统调用就能getshell
:
那要形成调用链呢?回想原始的ROP,是不是配合ret
就可以形成ROP链,所以这里也是,不过对应的栈顶也要指向下一个伪造syscall
的位置:
除了手动去布置寄存器的值之外,pwntool
还有一个集成的工具—-SigreturnFrame()
模块,在CTF的题目中经常会使用到它,其实它就是把栈帧里面每个寄存器的值就标记好了,我们只需要生成一个SigreturnFrame()
模块,然后再往这个模块里面放入寄存器的值就可以了,下面是在i386
上调用mprotect
,具体可以参考链接:
Sigreturn Oriented Programming
1 | 'i386') context.clear(arch= |
下面就用一道题来看看SigreturnFrame()
模块怎么用吧!
题目
360chunqiu2017_smallest
看下保护,就开了NX
1 | Arch: amd64-64-little |
程序短的离谱,就一个系统调用为read(0,rsp,400h)
,但是这里没有SROP的系统调用号15呀?那该怎么调用它呢?答:read
函数返回值会到rax
中:
ssize_t read ^[1]^ (int fd, void *buf, size_t count);
成功返回读取的字节数,出错返回-1并设置errno
,如果在调read
之前已到达文件末尾,则这次read
返回0。
1 | .text:00000000004000B0 start proc near ; DATA XREF: LOAD:0000000000400018↑o |
现在已经直到了触发SROP的方法,我们都知道用execve
来getshell
是需要传入/bin/sh
地址的,但在此题目当中既没有这个字符串:
1 | ➜ one_gadget smallest |
同时又是静态链接:
1 | ➜ file smallest |
所以需要写入到某个地方,并且这个地方的地址是可知的,看下vmmap
只有stack
能读写,同时这里面并没有(可以用系统调用):mprotect
来修改页属性
1 | pwndbg> vmmap |
既然只能写栈,那就得先泄露栈地址,怎么泄露呢?刚刚说到我们可以修改它的系统调用,对吧!所以我们读入一个字符来修改rax
为1,同时还要防止它执行.text:00000000004000B0 xor rax, rax
,不然一切还是白费了!,那怎么办呢?回到第一个的read
,我们反汇编看一下,欸嘿!它是往返回地址上写的欸(IDA 7.5已经帮我们分析出来了):
1 | signed __int64 start() |
直接往&retaddr
写入值,就劫持控制流,首先我们肯定是要让它成功读入一个字节并从0x4000B3
开始执行,这样就执行了write(1,buf,0x400)
(edx
和esi
都没动),由于它只执行一次read
,所以要往&retaddr
多写一点,就可以持续控制执行流,读完之后就跳到0x4000B0
,又回来程序入口,这次就读入\xb3
,修改返回的地址绕过清零rax
的语句,就执行了write(1,buf,0x400)
泄露栈地址:
1 | payload = p64(0x4000B0)*3 |
泄露完栈地址之后,就可以往栈上写/bin/sh
执行execve
啦!,但是在此之前还有一个问题就是栈顶的位置并不是指向我们的SROP的,所以还要做一次迁移到sigframe
处,调用SYS_read
来读入execve
的sigframe
顺便栈迁移:
1 | sigframe = SigreturnFrame() |
再次SROP就能执行execve("/bin/sh",0,0)
完整exp:
1 | from pwn import * |
参考链接: