Canary
Canary(金丝雀)
保护原理
Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。
Canary保护的原理是在栈上 rbp 附近放置一个随机数,等到函数执行结束,就会 检测 该位置存放的随机数是否被改动(覆盖),如果是说明发生了栈溢出,可能导致rbp,ret address 被修改导致异常行为,会中止程序的运行。
High Address | |
+-----------------+
| args |
+-----------------+
| return address |
+-----------------+
rbp => | old ebp |
+-----------------+
rbp-8 => | canary value |
+-----------------+
| local variables |
Low Address | |
具体细节
如何开启/关闭canary保护:
gcc -o test test.c // 默认情况下,不开启Canary保护
gcc -fno-stack-protector -o test test.c //禁用栈保护
gcc -fstack-protector -o test test.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码
-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启)
编译一个例子来看看:

用IDA来看看吧,省点事,直接看有canary保护的那个:

在程序启用canary保护编译后,在函数的开头部分就会有一段汇编:
//AT&T风格
var_8 = qword ptr -8
mov rax, fs:28h
mov [rbp+var_8], rax
//Intel风格
mov rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax
它的作用是取出fs寄存器0x28处的值,并将其存放在栈上%rbp-0x8的位置;
//AT&T风格
mov rdx, [rbp+var_8]
sub rdx, fs:28h
jz short locret_11BF
call ___stack_chk_fail
locret_11BF:
leave
retn
//Intel风格
mov rdx, qword ptr [rbp + var_8]
sub rdx, qword ptr fs:[28h]
test rdx, rdx
jz short locret_11BF
call ___stack_chk_fail
locret_11BF:
leave
retn
并且函数返回之前,会将栈上该位置的值取出,再与 fs:0x28 的值进行异或操作;
如果异或的结果为 0,说明 Canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。
如果此时canary已经被非法修改了,则程序执行流会走向___stack_chk_fail这个函数;
__stack_chk_fail 也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,具体如下:
eglibc-2.19/debug/stack_chk_fail.c
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}
这意味可以通过劫持 __stack_chk_fail 的 got 值劫持流程或者利用 __stack_chk_fail 泄漏内容 (参见 stack smash);
进一步,对于 Linux 来说,fs 寄存器实际指向的是当前栈的 TLS 结构,fs:0x28 指向的正是 stack_guard:
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
uintptr_t sysinfo;
uintptr_t stack_guard;
...
} tcbhead_t;
如果存在溢出可以覆盖位于 TLS 中保存的 Canary 值那么就可以实现绕过保护机制;
事实上,TLS 中的值由函数 security_init 进行初始化:
static void
security_init (void)
{
// _dl_random的值在进入这个函数的时候就已经由kernel写入.
// glibc直接使用了_dl_random的值并没有给赋值
// 如果不采用这种模式, glibc也可以自己产生随机数
//将_dl_random的最后一个字节设置为0x0
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
// 设置Canary的值到TLS中
THREAD_SET_STACK_GUARD (stack_chk_guard);
_dl_random = NULL;
}
//THREAD_SET_STACK_GUARD宏用于设置TLS
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
并且更有意思的一点是canary总是 以字节‘\x00’做结尾 ;
这么设计的本意是为了保证 Canary 可以 截断字符串,从而防止连带输出 ,可以看看gdb的结果:

可以看到,这里已经把canary的值插入到栈上了,并且是低位\x00;
但是存在溢出的时候也可以**控制覆盖 Canary 的低字节**,来打印出剩余的 Canary 部分。
canary是很好的防溢出手段,它几乎不影响程序的正常运行;并且不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部;当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。
但是这并不意味着canary可以阻止所有的栈溢出利用。
Canary 与 Windows 下的 GS 保护(汇编的时候会看到gs:0x14)都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它 几乎并不消耗系统资源 ,所以现在成了 Linux 下保护机制的标配。