此文摘抄于veritas501的博文
http://veritas501.space/2017/04/28/%E8%AE%BAcanary%E7%9A%84%E5%87%A0%E7%A7%8D%E7%8E%A9%E6%B3%95/
里面讨论了我所知的几种关于canary的玩法,我目前不知道的就等我以后什么时候知道了再补充吧。
1. 先说说canary
canary直译就是金丝雀,为什么是叫金丝雀?
17世纪,英国矿井工人发现,金丝雀对瓦斯这种气体十分敏感。空气中哪怕有极其微量的瓦斯,金丝雀也会停止歌唱;而当瓦斯含量超过一定限度时,虽然鲁钝的人类毫无察觉,金丝雀却早已毒发身亡。当时在采矿设备相对简陋的条件下,工人们每次下井都会带上一只金丝雀作为“瓦斯检测指标”,以便在危险状况下紧急撤离。
而程序里的canary就是来检测栈溢出的。
检测的机制是这样的:
1.程序从一个神奇的地方取出一个4(eax)或8(rax)节的值,在32位程序上,你可能会看到:
在64位上,你可能会看到:
总之,这个值你不能实现得到或预测,放到站上以后,eax中的副本也会被清空(xor eax,eax)
2.程序正常的走完了流程,到函数执行完的时候,程序会再次从那个神奇的地方把canary的值取出来,和之前放在栈上的canary进行比较,如果因为栈溢出什么的原因覆盖到了canary而导致canary发生了变化则直接终止程序。
在栈中大致是这样一个画风:
2. 绕过canary - 格式化字符串
格式化字符串能够实现任意地址读写,具体的实现可以参考我blog中关于格式化字符串的总结,格式化字符串的细节不是本文讨论的重点。
大体思路就是通过格式化字符串读取canary的值,然后在栈溢出的padding块把canary所在位置的值用正确的canary替换,从而绕过canary的检测。
示例程序:
|
|
在第一次scanf的时候输入“%7$x”打印出canary,在fun中利用栈溢出控制eip跳转到getflag。
poc:
|
|
3. 绕过canary - 针对调用方式一致
linux下的canary有个明显的特性:那就是每个使用了canary的函数,其canary的值是完全一样的。其实原理很简单,从上面给出的x86和x64的两种canary生成图示可以看出,linux的canary总是和large gs:14h或者fs:28h有关。这也就意味着,只要这个值不变,那么canary的值也不会变。显然,程序运行之后,这个地址的值不会发生改变。
那么,这也产生了一种攻击方法,通过存在漏洞的函数之外的函数,间接打印或者求出canary,那么这个canary可以运用到任意函数中(包括漏洞函数)。
漏洞利用样例就是pwnbale.kr中的md5 calculator那道题
题目描述:
We made a simple MD5 calculator as a network service.
Find a bug and exploit it to get a shell.Download : http://pwnable.kr/bin/hash
hint : this service shares the same machine with pwnable.kr web serviceRunning at : nc pwnable.kr 9002
sph7 已经将这个问题完美解决,这里基本上摘抄于其原文
ida打开文件,发现关键函数是process_hash,代码如下
|
|
这里的问题在与给g_buf分配了1024bytes的空间,但是只给u分配了512bytes的空间,而1024字节的base64解码之后的长度为768,所以这里有一个栈溢出。但是代码中有栈cookie,所以光靠这里是不能利用的。
继续看代码,发现另一个函数
|
|
在这个函数中,栈cookie被用来生成了一个hash值,而这个hash值会在交互中给出,结合题目中提到的提示,bin服务和pwnable.kr运行在同一台机器上,也就是说时间相同,这样就可以反算出栈cookie,从而get shell了。
大概思路清晰之后,开始完成exp,首先是用c算出栈cookie
|
|
完成cookie的计算,分析到这里就可以写exp了,主要思路是溢出掉v3,用验证码和时间计算出栈cookie,最后调用system("/bin/sh")
|
|
4. 绕过canary - 针对fork的进程
对fork而言,作用相当于自我复制,每一次复制出来的程序,内存布局都是一样的,当然canary值也一样。那我们就可以逐位爆破,如果程序GG了就说明这一位不对,如果程序正常就可以接着跑下一位,直到跑出正确的canary。
另外有一点就是canary的最低位是0x00,这么做为了防止canary的值泄漏。比如在canary上面是一个字符串,正常来说字符串后面有0截断,如果我们恶意写满字符串空间,而程序后面又把字符串打印出来了,那个由于没有0截断canary的值也被顺带打印出来了。设计canary的人正是考虑到了这一点,就让canary的最低位恒为零,这样就不存在上面截不截断的问题了。
示例程序:
|
|
poc脚本:
|
|
5. 故意触发canary - ssp leak
这题可以参考jarvis oj中 smashes一题的解题方法中的前一半。
这里我偷个懒,直接把之前写的wp扔过来了,反正原理都在题里了。
题目描述:
Smashes, try your best to smash!!!
nc pwn.jarvisoj.com 9877
首先查看保护
|
|
有canary,有nx
ida找到关键函数:
|
|
首先,函数使用了gets(的某种形态?)来获取输入,好处是我们可以输入无限长度的字符串,坏处是发送过去的字符串的尾部会以\n
结尾,所以无法绕过canary。
纵观整个程序,似乎没有什么地方能够绕过canary,也没有什么地方能打印flag。
但如果你换个思路,我们故意触发canary的保护会怎么样?
事实上,就有一种攻击方法叫做SSP(Stack Smashing Protector ) leak
。
如果canary被我们的值覆盖而发生了变化,程序会执行函数___stack_chk_fail()
一般情况下,我们执行了这个函数,输出是这样的:
我们来看一下源码
__stack_chk_fail :
|
|
fortify_fail
|
|
可见,__libc_message 的第二个%s
输出的是argv[0],argv[0]是指向第一个启动参数字符串的指针,而在栈中,大概是这样一个画风
所以,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值。
听起来很美妙,我们可以试试看。
先写如下poc:
|
|
输出结果令我们满意
|
|
但是,当我们把地址换成flag的地址时,却可以发现flag并没有被打印出来,那是因为在func_1函数的结尾处有这样一句:
|
|
所以,无论如何,等我们利用canary打印flag的时候,0x600D20上的值已经被完全覆盖了,因此我们无法从0x600D20处得到flag。
这就是这道题的第二个考点,ELF的重映射。当可执行文件足够小的时候,他的不同区段可能会被多次映射。这道题就是这样。
可见,其实在0x400d20处存在flag的备份。
因此,最终的poc为:
|
|