浅谈 Canary
浅谈 Canary
QcN3ep介绍
在每一个 pwn
手的溢出路上,总是有各种的保护措施来阻拦我们,其中第一难就是一只灵巧的金丝雀,它优雅的站在栈上,就像神话中的永远捉不到幸福鸟,一旦接近,就会被
__stack_chk_fail() 无情的 smash 掉……
扯偏了,我们回到正题……
Canary 是 Linux
系统的一种用于运行时检测栈溢出的保护措施,名字来自历史上矿工们用金丝雀来检测矿井中有毒气体的含量,恰如其名,Canary
会在攻击者劫持返回地址前就终止程序,防止被利用。
由于其实现简单、性能开销小、功能强大,现在已经成为保护措施的标配。
原理
Canary 的工作原理是在函数开始前在栈上插入一个随机数,像这样:
1 | +------------------+ 高地址 |
由于栈溢出是由于边界控制不严格导致的数据溢出,所以在覆盖返回地址等重要数据前,必然要将包括 Canary 在内的所有数据都一并覆盖,所以函数只需要在返回前检查一下 Canary 有没有被修改,就能判断出有没有发生栈溢出。
流程
在函数的开头,会有类似这样的代码,其逻辑是取出 fs 寄存器 0x28
偏移处的值,存放在紧邻 rbp 的地方。这个值总是一个以 \x00
结尾,目的是防止被输出函数意外的输出。
1 | mov rax, qword ptr fs:[0x28] |
在函数结束的地方,会有类似这样的代码:
1 | mov rax, [rbp - 8] |
这里就是在检查 Canary 和原来的值是否一致。
- 如果 Canary 没有被篡改,程序正常返回。
- 如果 Canary 已经被篡改,调用
__stack_chk_fail(),这个函数会打印出错误信息并终止程序。
绕过
虽然 Canary 可以有效防止栈溢出,但是也不是无懈可击的,目前对于绕过 Canary 已经有了相当成熟的手段。
泄露 Canary
- 如果程序中存在
printf(buf)这样的格式化字符串漏洞,我们可以指定偏移量来泄露出 Canary 的值。 - 如果程序中会使用
puts等函数打印缓冲区,我们可以先溢出到 Canary 的第一个字节,这样puts就会将 Canary 带出来,我们只需要接收输出并在后面补零即可。
爆破 Canary
虽然程序每次启动时 Canary 的值都会改变,但是使用 fork()
函数创建的子进程用有和主进程相同的 Canary,如果程序可以无限
fork,我们就可以遍历 Canary 的每一位,直到爆破出 Canary。
劫持
__stack_chk_fail@got
一个有趣的事实是,虽然 canary
被篡改会导致程序终止,但是负责终止程序的是位于 glibc 中的
__stack_chk_fail。由于它位于 glibc
中,所以和其他的函数一样,都需要延迟绑定到 .got
表中。倘若我们在调用 __stack_chk_fail
之前就把它覆盖为一个空函数,那么即使 canary
被篡改,程序也能正常进行下去。
