canary绕过
转载自ctf wiki
一、Canary
介绍
Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。
我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法(栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行(栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。
由于 stack overflow 而引发的攻击非常普遍也非常古老,相应地一种叫做 Canary 的 mitigation 技术很早就出现在 glibc 里,直到现在也作为系统安全的第一道防线存在。
Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。
Canary 与 Windows 下的 GS 保护都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它几乎并不消耗系统资源,所以现在成了 Linux 下保护机制的标配。
Canary 原理
在 GCC 中使用 Canary
可以在 GCC 中使用以下参数设置 Canary:
1 |
|
Canary 实现原理
开启 Canary 保护的 stack 结构大概如下:
1 |
|
当程序启用 Canary 编译后,在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中ebp-0x8 的位置。
这个操作即为向栈中插入 Canary 值,代码如下:
1 |
|
在函数返回之前,会将该值取出,并与 fs:0x28 的值进行异或。如果异或的结果为 0,说明 Canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。
1 |
|
如果 Canary 已经被非法修改,此时程序流程会走到 __stack_chk_fail
。__stack_chk_fail
也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,定义如下。
1 |
|
这意味可以通过劫持 __stack_chk_fail
的 got 值劫持流程或者利用 __stack_chk_fail
泄漏内容(参见 stack smash)。
进一步,对于 Linux 来说,fs 寄存器实际指向的是当前栈的 TLS 结构,fs:0x28 指向的正是 stack_guard。
1 |
|
如果存在溢出可以覆盖位于 TLS 中保存的 Canary 值那么就可以实现绕过保护机制。
事实上,TLS 中的值由函数 security_init 进行初始化。
1 |
|
Canary 绕过技术
序言
Canary 是一种十分有效的解决栈溢出问题的漏洞缓解措施。但是并不意味着 Canary 就能够阻止所有的栈溢出利用,在这里给出了常见的存在 Canary 的栈溢出利用思路,请注意每种方法都有特定的环境要求。
泄露栈中的 Canary
Canary 设计为以字节 \x00
结尾,本意是为了保证 Canary 可以截断字符串。
泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。
这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露 Canary,之后再次溢出控制执行流程。
利用示例
存在漏洞的示例源代码如下:
1 |
|
编译为 32bit 程序并关闭 PIE 保护 (默认开启 NX,ASLR,Canary 保护)
1 |
|
首先通过覆盖 Canary 最后一个 \x00
字节来打印出 4 位的 Canary
之后,计算好偏移,将 Canary 填入到相应的溢出位置,实现 Ret 到 getshell 函数中
1 |
|
one-by-one 爆破 Canary
对于 Canary,虽然每次进程重启后的 Canary 不同(相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的, 并且
通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。我们可以利用这样的特点,彻底逐个字节将 Canary 爆破出来。
在著名的 offset2libc 绕过 linux64bit 的所有保护的文章中,作者就是利用这样的方式爆破得到的 Canary:
这是爆破的 Python 代码:
1 |
|
劫持__stack_chk_fail函数
已知 Canary 失败的处理逻辑会进入到 __stack_chk_fail
ed 函数,__stack_chk_fail
ed 函数是一个普通的延迟绑定函数,可以通过修改 GOT 表劫持这个函数。
参见 ZCTF2017 Login,利用方式是通过 fsb 漏洞篡改 __stack_chk_fail
的 GOT 表,再进行 ROP 利用
覆盖 TLS 中储存的 Canary 值
已知 Canary 储存在 TLS 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。
参见 StarCTF2018 babystack
二、例题
pwn04——来自ctfshow的pwn入门
checksec一下
开启了nx,canary,部分relro等保护措施,
运行一下附件,发现可以把你输入的东西打印出来,还是重复两次,因此可以借此把canary泄露出来。(其实应该也看出来了吧,这里也有着很明显的字符串漏洞,所以还有一种解法就是去算canary的相对字符串的偏移,然后把它泄露出来)进入ida,函数很少,左边也能发现给出了系统函数,说明只要能正常覆盖返回地址就可以获取shell,点开buf进入栈中查看到canary的位置
计算一下buf到var_c的偏移,覆盖到Var_C的最后一位数,因为是0x00会造成截断,所以一同覆盖,到时候减去覆盖的值就行了
其实这题貌似就是上面当做例题的题目,可能是出题人也读过ctf wiki并且照着样子出的,tql!
Mary_Morton——来自攻防世界进阶区
checksec 一下,查看保护
开启了部分relro,nx,canary
首先,主函数是个无限的循环结构,不停的可以进入三个分支,在第二个分支中有着字符串漏洞,第三个分支中有着栈溢出漏洞,如果只是简单处理,那就是先用字符串漏洞泄露canary的值,再用栈溢出进行跳转获取flag
在gdb中断点到printf中可以算出,canary是栈上的第18个变量,以及64位传参中前六个是寄存器传参,所以canary相对于字符串是第23个参数,也就是要写成%23$p,然后就正常算栈溢出偏移即可。exp如下:
但是我在攻防世界的wp中看见,有个大佬还拥有其他两个方式,方式1:输入2,利用格式化字符串将printf的got地址修改为system的plt地址,再次输入2,输入’/bin/sh\x00’,相当于执行system(‘/bin/sh\x00’)方式2:输入2,利用格式化字符串将exit的got地址修改为sub_4008DA函数地址(该函数可以直接执行cat./flag),再次输入3,调用sub_4008DA函数catflag。详细可以到攻防世界自行查看,到大佬tql!!!!!!
- 本文作者:ShouCheng
- 本文链接:http://shoucheng3.github.io/2021/03/28/2021-03-28-canary%E7%BB%95%E8%BF%87/index.html
- 版权声明:本博客所有文章均采用 BY-NC-SA 许可协议,转载请注明出处!