0x00 前言
总结一下自己所知道的栈溢出中关于canary的做法,其实当看到canary时首先应该考虑是不是栈溢出,毕竟没什么特定条件一般绕不过的。
0x01 canary概述
在gcc编译参数与canary关系:
1 | -fstack-protector |
当你开启canary保护时,程序在运行时会在返回地址之前插入验证信息,当函数返回时会对插入的数据进行验证,看与之前是否相同,以此来判断是否产生了栈溢出。
以32位程序为例
程序从一个神奇的地方(gs:14h)取出4(eax)字节的值,如果是64位程序便是8字节(rax),插入到栈中。放到栈上以后,eax中的副本也会被清空(xor eax,eax)。
在函数基本执行完时,程序会再次从那个神奇的地方把canary的值取出来,和之前放在栈上的canary进行比较,如果发生栈溢出覆盖到了canary而导致canary发生了改变则跳到函数 ___stack_chk_fail
直接终止程序。
0x02 leak canary
1、格式字符串绕过canary
格式化字符串能够实现任意地址读写,具体的实现自己google吧,格式化字符串的细节不是此次讨论的重点。
开启了canary和NX
可以很明显的看到栈溢出和格式化字符串漏洞
因此我们只需要通过格式化字符串读取canary的值,然后在栈溢出的padding块把canary所在位置的值用正确的canary替换,从而绕过canary的检测。
poc:
1 | from pwn import * |
当然格式化字符串leak canary只是其中一种思路,因为格式字符串的作用是任意地址读写,所以也可以通过不必连续向栈上写这一特点,从而避开canary的检查。
2、printf puts函数leak canary
当你观察canary的值会发现所有canary的低位都会是\x00,这是canary设计时的规定,为了防止那些导致栈信息泄露的漏洞,当有个\x00截断时canary后面的数据就不会被泄露出来了。但即便如此依旧存在利用的可能性。
当我们溢出覆盖了Canary的\x00,如果还能够再printf,puts的话,那么你便可以leak出一堆栈的数据,我们对leak的数据进行相应截取便可以得到canary值。不过这种利用也是有条件的必须在栈溢出之后有一个printf,puts这样的打印的函数,并且我们之后还需要再一次的栈溢出去控制执行流(具体情况还需要自己判断)。
仍然是以上面那题为例,不过请把那个gets想成scanf,因为gets函数读取时会将结尾的 \n换成\0 ,leak会被截断的。
此刻栈上的画风大概是这样:
强调一下,上面的图只是帮你理解,canary并不一定与ebp地址相邻。
因此当你payload以(size of buffer)+1的大小压过去时,刚好可以将canary的\x00覆盖掉,之后的printf(或puts)打印时,至少可以leak出buffer+canary的内容,此时只要相应的截取再加个\x00,canary就有了。
具体poc我不贴了,太占篇幅了,我写着累你们看着也累。(其实还是因为懒)
0x03 ssp leak
上面我们说过,如果发生栈溢出覆盖到了canary而导致canary发生了改变则跳到函数 ___stack_chk_fail 直接终止程序。而ssp(Stack Smashing Protector ) leak正是通过故意触发canary保护执行函数___stack_chk_fail产生的。
先来看一下函数___stack_chk_fail源码:
代码非常简单就是输出出错信息,我们可以继续分析fortify_fail_abort:
呦吼~~ 看到那个__libc_argv[0]没,那是程序的参数里面装的是应用程序的路径,当canary出错报错输出中会打印出来,那如果我们在栈溢出时将它覆盖掉,那打印出来的会是什么呢?会打印我们所覆盖的相应的指针内容。所有在没有劫持到相应权限时,这个特点可以用来泄露内存中的内容。
给张图帮助理解:
参考自Pwning (sometimes) with style
至于题目看一下jarvis oj中 smashes。
我就不贴图具体分析了,反正很明显有个gets栈溢出,你要泄露远程主机上bss中的内容,所以用ssp leak泄露一下就可以。
poc:
1 | from pwn import* |
在网上看的一个大佬的写法,极其暴力,管你偏移是多少全部盖过去就好,反正总有一个在ret addr上。 莫名喜欢(~ ̄▽ ̄)~
关于这题还有一点要解释,虽然与ssp无关,就是你会发现覆盖的地址并不是ida里看的flag的地址,因为程序在canary打印flag之前会用memset将bss中放flag的地方(0x600D21)覆盖掉。至于为什么用(0x400D20)覆盖就可以打印出flag,因为ELF的重映射,当可执行文件比较小时,他的不同区段可能会被多次映射。用find找一下就可以看看到重映射的flag位置了。
0x04 fork爆破canary
这个问题准确来说是由linux的机制导致的,fork函数相当于自我复制,fork出来的子进程内存布局与父进程完全相同,因他们的canary值也相同。这样当我们子进程由于canary判断不正确导致程序crash后,父进程不会crash,我们就完全可以利用这样的特点,通过栈溢出对canary的最低位开始逐个覆盖,一个字节一个字节的将canary爆破出来。当然条件要求fork的次数足够多,支持到我们跑出canary。
爆破canary的Python代码:
1 | canary = '\x00' |
这是64位的爆破代码模板,32位也差不多,在之前提到过canary的最低位上是\x00,所以爆破64位时只需要爆破7个字节(32位3字节)便可以了。
不贴图细讲了(懒死我得了),做法大致就是上面讲的,这题写的应该是一个socket, 接受到请求后fork出一个子进程,和用户做交互,父进程继续监听端口。所以利用fork爆破出canary,之后就可以控制执行流,ret到那个读取flag的函数(0x400b76)再输出(0x400bc6)就可以了。
0x05 劫持__stack_chk_fail
上面已经说过当canary检查失败时,则执行函数 ___stack_chk_fail 进行报错并且终止程序。但是如果我们能够劫持该函数,让它不再执行,那么canary便失去作用,我们便可以随意进行栈溢出了。
至于劫持那就GOT表覆写咯,但这跟正常的GOT覆写又有有点区别,一般我们覆写GOT是GOT表绑定了真实地址之后,我们进行修改,以此让程序执行其他的函数。但如果 stack_chk_failed开始绑定了,那已经等于这个程序已经gg了。所以我们要覆写尚未执行的 stack_chk_failed的GOT表。
至于漏洞条件必须有一个可以向任意地址写的漏洞,且必须是在执行函数stack_chk_failed前就已经完成了写操作。讲到这里其实最容易想到就是格式化字符串的任意读写了,但如果有format string一般就直接leak canary了。所以有时对任意写漏洞还得自己留意创造。
当选择4进入函数Leetify()时
可以很明显的看出h和H置换的代码和其他的不一样,它置换时换成了三字节因此产生了栈溢出,而任意地址写的漏洞则出在下面的 strcpy(dest, &src);上。
此刻栈上的画风大概是这样:
所以我们只需要通过栈溢出将dest覆盖为GOT表中__stack_chk_fail的地址就可以将src的内容写入了,至于写入的内容便是你所需要的gadget的地址。至此函数__stack_chk_fail在未触发之前便已经被劫持了,canary也就失去作用了。
至于这题在绕过canary后的写法,看题目链接下面那一堆大佬的wp就好,这里只是总结canary,就不再赘述了。
0x06 修改canary
大致说一下TLS,这是一个结构体,还记得之前取出canary值的那个神奇的地方(gs:14h)吗?而段选择器gs正是指向TLS,0x14正是canary在TLS中的偏移。至于canary的值是通过系统随机数产生的,之后再通过gs寄存器将canary写入TLS,而用户程序则通过gs:14h来读取canary。(32位程序是gs:14h,64位则是fs:0x28) 等有时间我会另开一篇写一下Glibc中canary的实现(随缘随缘(〃` 3′〃))。
Glibc中canary的实现已更新
而在多线程中TLS将被放置在多线程的栈的顶部,因此我们能通过栈溢出对canary初始值进行更改,从而避开canary检查。
上面写的只是我目前所知的几种在栈溢出下关于canary的做法,以后遇到不知道的再补充吧。