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 (关闭 / 开启 / 全开启)

编译一个例子来看看:

image-20250109222021725

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

image-20250109223412720

在程序启用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的结果:

image-20250109230351349

可以看到,这里已经把canary的值插入到栈上了,并且是低位\x00;

但是存在溢出的时候也可以**控制覆盖 Canary 的低字节**,来打印出剩余的 Canary 部分。

canary是很好的防溢出手段,它几乎不影响程序的正常运行;并且不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部;当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。

但是这并不意味着canary可以阻止所有的栈溢出利用。

Canary 与 Windows 下的 GS 保护(汇编的时候会看到gs:0x14)都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它 几乎并不消耗系统资源 ,所以现在成了 Linux 下保护机制的标配。