栈溢出中关于canary的总结

0x00 前言

总结一下自己所知道的栈溢出中关于canary的做法,其实当看到canary时首先应该考虑是不是栈溢出,毕竟没什么特定条件一般绕不过的。

0x01 canary概述

在gcc编译参数与canary关系:

1
2
3
4
5
6
7
8
9
10
11
-fstack-protector
对包含有malloc族系和内部的buffer大于8字节的函数使能canary.
-fstack-protector-all
对所有函数使能canary.
-fstack-protector-strong
对包含有malloc族系和内部的buffer大于8字节的函数
或包含对局部变量地址引用的函数能canary
-fstack-protector-explicit
只对有明确stack_protect attribute的函数使能canary.
-fno-stack-protector
禁用canary.

当你开启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吧,格式化字符串的细节不是此次讨论的重点。

bamboofox-pwn200为例

开启了canary和NX

可以很明显的看到栈溢出和格式化字符串漏洞

因此我们只需要通过格式化字符串读取canary的值,然后在栈溢出的padding块把canary所在位置的值用正确的canary替换,从而绕过canary的检测。

poc:

1
2
3
4
5
6
7
8
9
10
from pwn import *
#p = process('./pwn')
p = remote('bamboofox.cs.nctu.edu.tw',22002)
leak = '%15$x'
p.sendline(leak)
canary = int(p.recv(),16)
sh = 0x804854D
payload = 'a'*40+p32(canary)+p32(sh)*20
p.sendline(payload)
p.interactive()

当然格式化字符串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
2
3
4
5
from pwn import*
s=remote('pwn.jarvisoj.com',9877)
s.sendline(p64(0x400d20)*200)
s.sendline()
print s.recvall()

在网上看的一个大佬的写法,极其暴力,管你偏移是多少全部盖过去就好,反正总有一个在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
2
3
4
5
6
7
8
9
canary = '\x00'
for j in range(7):
for i in range(256):
p.send(padding + canary + chr(i))
a = p.recvuntil(判断程序是否crash)
if 'recv' in a:
canary += chr(i)
break
print canary.encode('hex')

这是64位的爆破代码模板,32位也差不多,在之前提到过canary的最低位上是\x00,所以爆破64位时只需要爆破7个字节(32位3字节)便可以了。

题目参2017 NJCTF  messager

不贴图细讲了(懒死我得了),做法大致就是上面讲的,这题写的应该是一个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了。所以有时对任意写漏洞还得自己留意创造。

题目参2015 0ctf flagen

当选择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检查。

题目参2018 starctf  babystack

wp1                             wp2

参考资料

上面写的只是我目前所知的几种在栈溢出下关于canary的做法,以后遇到不知道的再补充吧。

栈溢出中关于canary的总结》有4个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注