0x00 前言
这道题目对我来说还是有些难度的。
通过这道题目,我学到了两种漏洞利用手段:ret2__libc_csu_init
、SROP
。
题目在BUUCTF可以下到。
0x01 分析
1 | Arch: amd64-64-little |
关键函数是vuln
和gadgets
函数。
先来看vuln函数
,IDA的伪代码界面只能看出来这是个syscall,看不到函数的具体参数,所以来看汇编。
转换一下就是read(0,&rsp-0x10,0x400) 、 write(1,&rsp-0x10,0x30)
这里可以通过溢出来劫持RIP。
再来看gadgets函数
这里需要注意两个地方
mov rax,15
和mov rax,59
这里涉及到Linux系统调用
15对应的是sys_rt_sigreturn系统调用
59对应的是sys_execve系统调用
所以这道题目可以有两种办法来做。
第一种办法是依靠59对应的execve,然后想办法执行execve(“/bin/sh”,0,0)
第二种办法是依靠15对应的sys_rt_sigreturn,借助sigreturn frame来执行execve(“/bin/sh”,0,0)
0x02 方法一 ret2__libc_csu_init
借助59对应的sys_execve
利用思路
由于程序没有给我们/bin/sh
,我们需要通过read函数手动输入,但是我们需要知道我们的输入存在什么地方了?可以知道输入是存在栈上了,所以可以先泄露栈地址。
当我们拿到栈地址并且知道/bin/sh\x00
的地址,就可以通过rop执行execve(“/bin/sh”,0,0),其中传参所用寄存器依次是rdi、rsi、rdx,分别对应/bin/sh、0 、 0 ,而要执行系统调用 还需要rax = 59。syscall可以在IDA中找到。
Try it !
Leak stack addr
1 | main = 0x4004ED |
关于这个payload有一点需要说明
由于程序中调用完syscall后就直接进行了retn,而且此时rsp == rbp,所以我们payload中p64(main)就对应的是返回地址。
通过调试
1 | 0x7fffffffdda0: 0x0068732f6e69622f 0x6262626262626262 <- rsp-0x10==/bin/sh\x00 + 'b'*8 |
可以看到0x7fffffffddc0位置存着一处栈地址,并且这个地址离我们输入的/bin/sh\x00的距离为
0x00007fffffffdeb8 - 0x7fffffffdda0 = 0x118,这个偏移是固定的,那么我们可以通过write函数打印出这个地址,然后减去这个偏移,就能得到/bin/sh的地址。
1 | offset = 0x118 |
ret2__libc_csu_init
1 | execv = 0x4004E2 |
除了pop_rdi_ret是用的ROPgadget --binary ciscn_s_3 --only 'pop|ret'
找到的
其他都可以在IDA里面找到
这道题目的核心也就在__libc_csu_init
里面的loc_400596
与loc_400580
了
r13能传给rdx
r14传给rsi
r15d能传给edi
最后call [r12 + rbx*8],然后比较rbx和rbp是否相等,如果不等,则循环执行loc_400580
这个payload构造的非常巧妙,反正我自己是构造不出来
1 | payload = '/bin/sh\x00' + 'b'*8 + p64(p6r) |
注意第二行,有几点需要说明。
- 首先是让rbx和rbp都为0(只要保证rbx+1不等于rbp就行),这样是有目的的,是为了能够过会执行
0x40058D add rbx, 1
以后 rbx不等于rbp,然后执行jnz short loc_400580
从而再次进入循环来执行call qword ptr [r12+rbx*8]
- 其次是p64(binsh + 0x50),这个是要传给rsi的,binsh + 0x50正好对应的就是p64(execv),第一次执行
call qword ptr [r12+rbx*8]
时,由于rbx = 0
,所以就相当于执行call [r12]
即call [rsi]
,也就是执行execv = 0x4004E2对应的mov rax,59;ret
。注意这里的call指令,相当于jmp 到 r12 + rbx*8这个地址里存放的地址,以这里为例,这就相当于 jmp 0x4004E2 - 注意到前面是r15d传给edi,而不是rdi。r15d指的是r15的低32位,edi也是rdi的低32位,因此为了防止rdi里面曾存有其他数据占用了高32位,需要先通过p6r令rdi = 0 ,所以payload第二行后面的对应rdi位置的是p64(0)
payload的执行流程
实际上这个payload的执行流程由于涉及到call qword ptr [r12+rbx*8]
而变得挺有趣而且巧妙的,可以调试跟一下,挺好玩的
首先当我们向程序输入payload以后,程序会返回到p64(p6r)来执行一系列pop和ret,p64(p6r)的ret会返回到p64(movcall)从而进入一个我们故意设置的循环(利用的是故意让rbx不等于rbp从而执行cmp rbx, rbp;jnz short loc_400580
)
此刻从整体来看, RSP是指向栈中p64(execv)
对应的位置的。ps : 由于在执行call qword ptr [r12+rbx*8]
的时候RSP也会变化并在call返回是还原RSP本来的值,但为了分析payload的执行流程,忽略call时RSP的变化从整体来看比较好 。
第一次执行完call qword ptr [r12+rbx*8]
,由于经过了add rbx,1
,所以此时rbx=1,rbp=0,它俩不相等,所以循环到loc_400580执行第二次循环。
而这次就会执行call [rsi + 8]
,也就是执行pop_rdi_ret
,由于此时RSP指向的是栈中p64(execv)的位置,那么当我们pop rdi的时候,会将RSP指向的值即p64(execv)传给rdi,同时RSP += 8,此刻RSP指向栈中p64(pop_rdi_ret)的位置,当执行ret的时候,RIP便指向了p64(pop_rdi_ret),同时 RSP += 8,从而跳出了我们设置的循环,此刻RSP指向栈中p64(binsh)对应的位置。
逃离了call qword ptr [r12+rbx*8]
这个循环怪圈以后,通过pop rdi,将binsh 传给了rdi,然后ret到syscall执行系统调用。
payload很巧妙,能够利用循环并且跳出循环
可惜我太菜了,自己构造不出来,自己构造出来的一个看似能打通的实际上打不通。。。
完整EXP1.py
1 | from pwn import * |
0x03 方法二 SROP
利用思路
与方法一相同,都要先泄露栈,目的是为了泄露我们输入的/bin/sh的存放地址。
然后伪造sigreturn frame 来执行execve(“/bin/sh”,0,0)
Try it!
Leak stack addr
和方法一相同
1 | from pwn import * |
伪造sigreturn frame 来执行execve(“/bin/sh”,0,0)
pwntools里面有sigreturn frame 相关模块。
1 | sigreturn = 0x4004DA |
这个模块用起来相当方便啊,只需要指定各个寄存器的值就好了。
关于这个sigreturn,有点高深,具体可以去搜一下,不过应对CTF的SROP题目的话,会用这个SigreturnFrame模块应该就够了。
有一点需要说明
context(arch='amd64', os='linux')
,这个一定要指明,不然会报错。
完整EXP2.py
1 | from pwn import * |
0x04 写在最后
这道题真的用了挺长时间的,加上更博客也用了不少时间,不过又调试了一遍又有了新收获新理解。
参考链接:
最后要感谢各位师傅们的博客以及BUUCTF