浅谈 Canary

介绍

在每一个 pwn 手的溢出路上,总是有各种的保护措施来阻拦我们,其中第一难就是一只灵巧的金丝雀,它优雅的站在栈上,就像神话中的永远捉不到幸福鸟,一旦接近,就会被 __stack_chk_fail() 无情的 smash 掉……

扯偏了,我们回到正题……

Canary 是 Linux 系统的一种用于运行时检测栈溢出的保护措施,名字来自历史上矿工们用金丝雀来检测矿井中有毒气体的含量,恰如其名,Canary 会在攻击者劫持返回地址前就终止程序,防止被利用。
由于其实现简单、性能开销小、功能强大,现在已经成为保护措施的标配。

原理

Canary 的工作原理是在函数开始前在栈上插入一个随机数,像这样:

1
2
3
4
5
6
7
8
9
10
+------------------+  高地址
| 返回地址 |
+------------------+
| 保存的 RBP |
+------------------+
| Canary |
+------------------+
| 局部变量 |
+------------------+ 低地址

由于栈溢出是由于边界控制不严格导致的数据溢出,所以在覆盖返回地址等重要数据前,必然要将包括 Canary 在内的所有数据都一并覆盖,所以函数只需要在返回前检查一下 Canary 有没有被修改,就能判断出有没有发生栈溢出。

流程

在函数的开头,会有类似这样的代码,其逻辑是取出 fs 寄存器 0x28 偏移处的值,存放在紧邻 rbp 的地方。这个值总是一个以 \x00 结尾,目的是防止被输出函数意外的输出。

1
2
mov    rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax

在函数结束的地方,会有类似这样的代码:

1
2
3
4
5
6
mov     rax, [rbp - 8]
sub rax, fs:28h
jnz short loc
call ___stack_chk_fail
loc:
retn

这里就是在检查 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 被篡改,程序也能正常进行下去。