Canary



0.Introduction

由于stack overflow而引发的攻击非常普遍也非常古老, 相应地一种叫做canary的 mitigation技术很早就出现在gcc/glibc里, 直到现在也作为系统安全的第一道防线存在。

canary不管是实现还是设计思想都比较简单高效, 就是插入一个值, 在stack overflow发生的 高危区域的尾部, 当函数返回之时检测canary的值是否经过了改变, 以此来判断stack/buffer overflow是否发生.

Canary与windows下的GS保护都是防止栈溢出的有效手段,它的出现很大程度上防止了栈溢出的出现,并且由于它几乎并不消耗系统资源,所以现在成了linux下保护机制的标配

1.Linux’s Canary

Test

在gcc的以下参数与canary有关系:

0x08048476 65a114000000 mov eax, dword gs:[0x14] ; [0x14:4]=1
系统从内核中取出生成的随机数值Canary,并且通过
0x0804847c 8944241c mov dword [esp + 0x1c], eax
防止在栈上
根据上面的代码可以画出如下的stack结构.

上面的分析可以清楚地看出canary的原理

SourceCode

gcc只是声明和使用了__stack_chk_guard __stack_chk_fail,并没有定义. 定义是在glibc里.这样整个Canary就是一个随机值,并且在静态的binary不可见,是一个运行值

__stack_chk_guard__stack_chk_fail的插入是在gcc将GIMPLE转换为RTL的pass里分别通 过函数default_stack_protect_guard()和ix86_stack_protect_fail()构建手动的tree, 然 后调用expand_normal()自动转换为RTL再插入待分析的用户代码来进行的.
在gcc中这部分的实现代码的调用栈如下:

gcc对__stack_chk_guard的使用其实还涉及到跟linux kernel的相关的部分.

gcc使用了__stack_chk_guard__stack_chk_fail之后就完成了canary实现的协议的编译器的那部分, 接着查看glibc的细节.
首先查看用户代码canary出错时的glibc错误输出代码:

上面的代码非常简单就是输出出错信息.我们可以继续分析fortify_fail:

libcargv[0]应用程序的参数,在Canary出错报错输出中会打印出应用程序的路径,这正是__libc_argv[0]的内容

如果栈溢出到能够覆盖__libc_argv[0],那么Canary就会报错打印相应的指针内容
在没有劫持到相应权限的情况下,这个特点可以用来泄露内存中的内容,这种技术就被称之为SSP leak
具体参考:Pwning (sometimes) with style

我们继续分析glibc对于整个canary机制的实现过程的代码.
首先给出跟canary相关的调用栈:

security_init初始化canary的值到%gs:0x14, 所以这个函数是真正的关键所在.

由此就完成了glibc对canary的值的写入工作.
上述过程就是一个简略的Canary的生成过程

2.Bypassing Canary

leak Canary

leak Canary 时利用了Canary的一个设计中的弱点(不过Canary也必须这样设计)
Canary值有一个重要的特点就是最后一个字节为”\x00″,我们利用Canarydump来查看一些进程的Canary如下图:

我们可以看到存在的Canary的最后一个字节都是”\x00″,这个特点是为了保证Canary有字符串截断的效果

但是如果我们覆盖了Canary的最后一个字节,如果还能够再打印的话,那么整个Canary也就被我们leak出来了,我们相应地对打印的字符串进行截取就可以得到真实的Canary值
不过这种利用也是有条件是在栈溢出之后必须有一个printf,puts这样的打印的函数才可以,并且我们需要重复利用漏洞

brute Canary

Canary在设计中存在一个缺陷,这个缺陷或许不是由于Canary的设计问题,而是linux的机制导致的,那就是子进程会继承父进程的Canary
这样当我们子进程由于Canary判断不正确导致程序crash后,父进程不会Crash
我们就完全可以利用这样的特点,彻底逐个字节将Canary爆破出来
在著名的offset2libc绕过linux64bit的所有保护的文章中,作者就是利用这样的方式爆破得到的Canary:
这是爆破的Python代码:

就是通过这样的方式逐Byte爆破得到的Canary

Hijack __stack_chk_fail

我们已知Canary失败的处理逻辑是要进入到stack_chk_failed函数中,它在该函数中完成报错输出以及程序的abort
但是如果我们能够劫持该函数,让它不在完成该功能,那么Canary就形同虚设,我们可以肆意做栈溢出了

种技术并不是我们一般方式的Hijack GOT表,一般我们HijackGOT是GOT表绑定了真实地址之后,我们修改它,让程序执行其他的函数
Got表中绑定真实地址必须是在执行过一次后才Lazy binding上,而stack_chk_failed才会被lazy binding上,程序已经Crash掉了

我们需要Overwrite的尚未执行过的stack_chk_failed的GOT表项,此时GOT表中应该存贮这stack_chk_failed PLT[1]的地址

这样我们需要修改它一条无关的地址,让它不再执行相应的功能即可

3.Exploitations

我们给出的示例的源代码如下:

编译成32bit的程序,开启NX,ASLR,Canary保护

FristWay

首先通过覆盖Canary最后一个”\x00″字节来打印出4位的Canary
之后,计算好偏移,将Canary填入到相应的溢出位置,实现Ret到getshell函数中

SecondWay

Hijack __stack_chk_fail
__stack__chk_fail函数的plt地址附近找到一条ret 指令,让程序即使出错也不abort,从而实现BOF利用,return 到getshell中即可
我们已知__stack__chk_fail的plt地址为:0x8048450,在没有lazy binding前它中存放的应该时plt的一条指令的地址,我们就在这附近搜索,寻找ret指令,或者其他相关指令

在0x0804840E找到一条ret指令
我们利用格式化字符串漏洞更改got表为该地址即可

4.Afterword

Canary 与windows下的GS保护相同,都是为了防止栈溢出做的保护,但就保护的强度来说是很强的,虽然我们讲了很多中绕过Canary的绕过技术,但是都是在特定的环境下的,在真实的环境中,Canary开启以后,做栈溢出将会变得十分困难,很多经典的漏洞利用方式,诸如最经典的ROP,Ret2libc等等,在遇到Canary都会失效

也正因为,Canary是所有Attack的大敌,所以研究Bypass的人,以及Bypass的方式也就很多

  1. 第一种Bypass的方式被被称之为leak Canary,它有效利用了Canary设计中的缺陷,最后一个字节时\x00的特点,能够顺利将Canry泄露出来,从而为下一次栈溢出制造条件
  2. 第二种方式是比较常用的一种方式,ButreCanary爆破Canary,由于服务器在提供服务时多是fork出子进程提供服务,并且子进程继承父进程的Canary,那这样多次链接测试服务器的Canary就成了可能,许多的通用的LInux下的绕过染过技术都用了这个原理
  3. 第三种是从另一个方式做Got的Overwrite,我们选择Canry的处理函数__stack_chk_fail作为目标,让它失效,程序就不会Abort,这样就可以再做栈溢出了

上述的三种方式都有一个共同的主旨,就是让Canry失效,无论是泄露它,爆破它,还是彻底干掉它都是基于这个思想的
当然Canary也有对自身的补强,比如让父进程与子进程Canary不同的renewssp
同样,我们也可以通过Canary的报错输出来泄露内存,这种方式被称之为SSp leak,也是比较常用的
具体可以参考:ASSP

5.reference

renewssp
Linux GLibC Stack Canary Values
Canary分析
BYPASSING STACKGUARD AND STACKSHIELD

发表评论