最近做到很大沙箱的题,后面都是靠 ROP 链来 ROW 得到 flag。其中 ROP 链构造,就会使用到 SROP,虽然之前看过,但是一直没碰到什么题又给忘记了。所以,这次再来好好学一下 SROP,并补做一下题。
SROP原理
SROP(Sigreturn Oriented Programming) 技术利用了类Unix系统中的Signal机制,如图(图来自Wiki):
- 当一个用户层进程P,发起了Signal时,控制权会进入 内核层;
- 内核会保存 进程P当前的执行上下文,将各类寄存器压入栈中,再栈顶压入 rt_sigreturn 地址,然后跳转执行 Signal Handler,即调用了 rt_sigreturn;
- Signal Handler 会处理中断,结束后会返回到内核层;
- 内核层恢复进程上下文,并将控制权返回给用户层。
内核保存的进程结构体是:ucontext_t,如下所示:
1 | // defined in /usr/include/sys/ucontext.h |
结构体中,有很多关于寄存器的操作,而 SROP 的关键就是修改结构体中关于 寄存器的值,从而修改程序执行流。
如果有 sigreturn
的 syscall
要执行,我们可以在 rsp
处开始 伪造 sig_frame
里的各类寄存器,实现 栈迁移(修改 rsp
),传入参数(修改 rdi
,rsi
,rdx
等),执行函数(修改 rsi
)。
如果没有 sigreturn
执行,可以先构造 rax
的系统调用号 为 15,并将rsp
处伪造好 sig_frame
,最后再去执行 syscall_ret
,实现 执行 sigreturn
函数。
示例程序1
该代码,来自参考1
1 | // compiled: |
程序总体流程就是读取用户输入,小于 ucontext_t
的值就退出,大于就执行 rt_sigreturn
调用。
EXP
1 | #!/usr/bin/python2 |
EXP中主要伪造了 rdi
、rsi
、rdx
、rax
这几个寄存器,将rax
设置为 execve
的系统调用号,将 rdi
设置为 /bin/sh
所在的地址,将 rsi
、rdx
设置为 execve
的参数 0,最终调用结果成功执行 execve
函数。
2016-360春秋杯 smallest
程序功能很直接,调用 read
函数读取 0x400 字节到 rsp
开始的位置。也即当执行完 read
函数后,会 retn
到 rsp
所指向的地址处继续执行。
EXP
1 | from pwn import * |
分析
传入3个main函数首地址
EXP
中首先发送了三个 main
函数的开始地址 0x4000b0
,当执行了 read
函数的syscall
之后,rsp
此时为开始地址,所以程序会跳转再去执行 read
函数。
执行write函数
执行 ret
指定,相当于 pop rsp; jmp rsp
,此时 rsp
增加 8 字节,但是其值仍然为 0x4000b0
。执行 read
函数时,修改 rsp
值得最后一位为 0xb3
,此时 rsp
的值即为 0x4000b3
,跳过了程序开始 xor rax, rax
对 rax
的检测。
同时由于刚执行的 read
函数返回值为 0x1
,则此时的 rax
的值也为 0x1
,其系统调用号 对应的是 write
函数,所以接下来再执行 syscall
,就会执行 write
函数。通过 write
函数,我们可以泄露栈地址,为我们后面布局 SROP
提供帮助。执行完 ret
指令后,rsp
为我们之前传入的第三个程序开始地址,所以程序又会跳转去执行 read
。
执行 read函数输入SROP
得到泄露的栈地址后,就可以向栈中布置 SROP。首先向栈中从 rsp
开始处的位置步入如下数据:
1 | sigframe = SigreturnFrame() |
此时栈中从 rsp
处的数据如下:
程序执行 ret
指令后,又会返回到程序开始处执行 read
函数。
此时,再输入 15字节payload
:
1 | ## set rax=15 and call sigreturn |
执行完 read
函数后,rax
变为 0x15,表示 sigreturn
的调用号;而 rsp
的值为 syscall_ret
的地址,并且紧邻的下一位即是我们伪造的 fake_sigframe
的开始地址。执行 ret
指令则会直接去使用 syscall
执行 sigreturn
。
执行 sigreturn 实现栈迁移
此时执行 sigreturn
函数时,会使用我们上面伪造的 fake_sigframe
来恢复寄存器。我们就可以修改 rsp
,rip
等寄存器的值,实现栈迁移。我们分析一下我们上面伪造的 fake_sigframe
里的值:
1 | sigframe = SigreturnFrame() |
rsp=stack_addr
,此处stack_addr
即为我们上面泄露的 栈地址,执行完 sigreturn
函数,我们的栈就迁移到 stack_addr
处。rip=syscall_ret
是执行完sigreturn
函数后,程序执行流会跳转到 syscall_ret
继续执行。 rax=SYS_read
是syscall
执行的系统调用为 read
函数。而rsi=stack_addr,rdx=0x400,rdi=0
,是将要执行的 read
函数的参数。
上图中,可以看到 sigreturn
执行完后,寄存器已经改变,栈顶指针也已经改变。
布置 getshell的SROP
接下来就需要向栈中继续部署 执行 execve('/bin/sh')
的 SROP。
1 | ## call execv("/bin/sh",0,0) |
和上面部署一致,都是需要将 先将栈顶布置为程序开始地址,随后 SROP
中布置 execve
的系统调用号、参数等寄存器。并且在最后固定偏移处布置好 ‘/bin/sh\x00
数据。
read
函数执行完后,栈顶 rsp
仍然为程序开始地址,会继续执行 read
函数:
和上面一样,将栈顶覆盖为 syscall ret
地址,然后填充为 0x15 字节。执行完 read
函数后,会去执行 syscall_ret
,此时 rax
为 0x15,即执行 sigreturn
函数:
执行完 sigreturn
,系统会直接执行 execve
函数,getshell
成功。
2020 V&N babybabypwn
程序分析
很明显执行了 sigreturn
函数,需要使用 SROP
攻击。
开启了沙箱,禁止了上面的函数。我们只能够使用 ORW 来读 flag。
利用分析
构造 SROP 实现栈迁移和read函数
首先构造一个 SROP
链,将栈 RSP
转到我们通过泄露的 libc
地址中的一个 libc
地址rop_addr
去,该地址段的权限必须 可读可写,我们在该地址段部署 ROP
链。同时将 rip
转到 read_addr
去,并将 rdi
设置为 0,rsi
设置为 rop_addr
,rdx
设为 0x400,则执行完 sigreturn
函数,将会执行 read(0,rop_addr, 0x400)
函数。
1 | sigframe = SigreturnFrame() |
此处有一个注意点,就是我们最后输入的 sigframe链去掉了前8字节,经过我调试当执行 sigreturn
函数时,系统栈在我们的 sigframe
数据前也就是 rsp
处有一个地址,也就是我们的 sigframe
数据并不是从 srp
开始的,所以往前移了8字节。
构造ORW链读取flag
在上一步实现栈迁移后,程序会执行read
函数,我们将我们构造的 ROW
的 ROP
链布置在rop_addr
处,此处关键是需要提前确定 flag
文件名字符串的位置,我们通常将 flag 文件名字符串放到 rop
链的最后。
1 | orw = flat([ |
整个 orw
链执行的函数已经写得十分清楚,最后即可 flag 输出。
EXP
1 | from pwn import * |
2020 V&N WarmUp
程序分析
程序在最里层的一个 read
函数,溢出了 0x10大小。
程序开启了沙箱,禁止执行 execve
函数,执行 write
函数时,其输出大小不能等于 0x10,左移32位不能等于0x0。
利用分析
首先 read2
的函数栈 就在 read1
函数栈的上面,也就是 read2
的 ret
地址和 read1
的 rsp
地址是连在一起的,可以在 read1
的 rsp
处布置 ROP
链,然后在 read2
处的 ret
地址处直接跳到 read1
的 rsp
处执行。
read1
的 rsp
处的 rop
就是常规的 ORW
链。
EXP
1 | from pwn import * |
参考
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2020/10/06/SROP攻击/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!