canary分析

canary analysis

@(GCC | Glibc | kernel)[GNU System-Security] –zet

00 导引

由于stack overflow而引发的攻击非常普遍也非常古老, 相应地一种叫做canary的 mitigation技术很早就出现在gcc/glibc里, 直到现在也作为系统安全的第一道防线存在.

canary不管是实现还是设计思想都比较简单高效, 就是插入一个值, 在stack overflow发生的 高危区域的尾部, 当函数返回之时检测canary的值是否经过了改变, 以此来判断stack/buffer overflow是否发生.

canary的实现大约是1998年就在gcc里出现出现了第一个合并入upstream的patch, 只不过后 期的实现将主要的功能移入了Glibc, 并且调整了局部数据和canary的位置, 以扩大数据的受 保护范围. 因为canary的出现的很早, 所以本文涉及到的GNU toolchains源代码版本来说选择 非常自由, 我使用的是我熟悉的版本: GCC-4.8.2 & eglibc-2.19 & linux-4.8.

01 示例

在gcc的调用参数里有以下跟canary有关系:

-fstack-protector 对包含有malloc族系和内部的buffer大于8字节的函数使能canary.

-fstack-protector-all 对所有函数使能canary.

-fstack-protector-strong

对包含有malloc族系或者内部的buffer大于8字节的或者包含局部数组的或者包含对local frame地址引用的函数使能canary.

-fstack-protector-explicit 只对有明确stack_protect attribute的函数使能canary.

-fno-stack-protector 禁用canary.

下面首先给出一个例子, 展示一下canary在应用程序级别对代码的直接影响.

#include <stdio.h>

int main () {
        int local = 1;
        char buffer[8];
        buffer[7] = 2;
        puts("42\n");

        return 0;
}

$gcc -m32 -fstack-protector-all -g -o canary canary.c

/// gdb查看运行时的反汇编代码
///
   // classical prologue.
   |0x804846d <main at canary.c:3>          push   %ebp
   |0x804846e <main+1 at canary.c:3>        mov    %esp,%ebp
   |0x8048470 <main+3 at canary.c:3>        and    $0xfffffff0,%esp
   |0x8048473 <main+6 at canary.c:3>        sub    $0x20,%esp
   // %gs:0x14里面存储的就是canary值, 并将其插入在地址esp+1c处.
   |0x8048476 <main+9 at canary.c:3>        mov    %gs:0x14,%eax
   |0x804847c <main+15 at canary.c:3>       mov    %eax,0x1c(%esp)
   |0x8048480 <main+19 at canary.c:3>       xor    %eax,%eax
   // 下面的两行代码可以看出gcc对buffer和local的位置进行了重排, 按照一般情况来说
   // 局部变量的声明先出现的先分配, 也就是地址更大, 也就是说应该是被赋值为1的局部
   // 变量local的地址更大. 但是这里进行了调整.
   |0x8048482 <main+21 at canary.c:5>       movl   $0x1,0x10(%esp)
   |0x804848a <main+29 at canary.c:6>       movb   $0x2,0x1b(%esp)
   // 入参准备puts()的调用
   |0x804848f <main+34 at canary.c:7>       movl   $0x8048550,(%esp)
   |0x8048496 <main+41 at canary.c:7>       call   0x8048340 <puts@plt>
   |0x804849b <main+46 at canary.c:9>       mov    $0x0,%eax
   // 取出插入的canary值与(%gs:0x14)处的原值做比较.
   |0x80484a0 <main+51 at canary.c:10>      mov    0x1c(%esp),%edx
   |0x80484a4 <main+55 at canary.c:10>      xor    %gs:0x14,%edx
   |0x80484ab <main+62 at canary.c:10>      je     0x80484b2 <main+69 at canary.c:10>
   // 如果canary的值发生了篡改, 那么将调用__stack_chk_fail.
   |0x80484ad <main+64 at canary.c:10>      call   0x8048330 <__stack_chk_fail@plt>
   |0x80484b2 <main+69 at canary.c:10>      leave
   |0x80484b3 <main+70 at canary.c:10>      ret

根据上面的代码可以画出如下的stack结构.

        High
        Address |                 |
                +-----------------+
                | arg1            |
                +-----------------+
                | return address  |
                +-----------------+
        ebp =>  | old ebp         |
                +-----------------+
     esp+1c =>  | canary value    |
                +-----------------+
     esp+1b =>  | char[7]         |
                | ...             |
     esp+14 =>  | char[0]         |
                +-----------------+
     esp+10 =>  | local           |
                +-----------------+
        Low     |                 |
        Address

上面的分析可以清楚地看出canary的原理, 下面将分析canary在gcc和glibc里的实现细节.

02 细节

canary的实现分为两部分, gcc编译时选择canary的插入位置, 以及生成含有canary的汇编代 码, glibc产生实际的canary值, 以及提供错误捕捉函数和报错函数. 也就是gcc使用glibc提供 的组件, gcc本身并不定义. 这样会让canary的值会是一个运行时才动态知道的值, 而不能通过 查看静态的bianry得到.

gcc

在gcc里跟canary的实现分为两部分: 引入canary值的比较的代码, 这是通过对外部变量 (__stack_chk_guard)引用来实现, 以及插入canary比较出错时输出异常的代码, 这是通过对 外部定义的异常函数(__stack_chk_fail)的调用来实现的.

也就是说gcc只是声明和使用了__stack_chk_guard/__stack_chk_fail, 并没有定义. 定义是 在glibc里.

__stack_chk_guard和__stack_chk_fail的插入是在gcc将GIMPLE转换为RTL的pass里分别通 过函数default_stack_protect_guard()和ix86_stack_protect_fail()构建手动的tree, 然 后调用expand_normal()自动转换为RTL再插入待分析的用户代码来进行的.

在gcc中这部分的实现代码的调用栈如下:

// 注意由下至上的调用序.
#0  ix86_stack_protect_fail () at ../../gcc/config/i386/i386.c:37607
#0  default_stack_protect_guard () at ../../gcc/targhooks.c:635
#1  stack_protect_prologue () at ../../gcc/function.c:4646
#2  gimple_expand_cfg () at ../../gcc/cfgexpand.c:4641
#3  execute_one_pass () at ../../gcc/passes.c:2333
#4  execute_pass_list () at ../../gcc/passes.c:2381
#5  expand_function () at ../../gcc/cgraphunit.c:1640
#6  output_in_order () at ../../gcc/cgraphunit.c:1833
#7  compile () at ../../gcc/cgraphunit.c:2037
#8  finalize_compilation_unit () at ../../gcc/cgraphunit.c:2119
#9  c_write_global_declarations () at ../../gcc/c/c-decl.c:10118
#10 compile_file () at ../../gcc/toplev.c:557
#11 do_compile () at ../../gcc/toplev.c:1864
#12 toplev_main () at ../../gcc/toplev.c:1940
#13 main () at ../../gcc/main.c:36

gcc对__stack_chk_guard的使用其实还涉及到跟linux kernel的相关的部分.

gcc-4.8.2/gcc/config/i386/gnu-user.h

#line 153
// 也就是其实__stack_chk_guard的值是由kernel提供的一个随机数.
// 其实这也是最基本的情况, glibc还实现了不由kernel提供的另外的代码.
// gs寄存器是一个跟TLS(Thread Local Storage)有关系的寄存器, 一般情况下TLS肯定开启使用的,
// 所以最普遍的情况就是__stack_chk_guard由kernel提供.
#ifdef TARGET_LIBC_PROVIDES_SSP
/* i386 glibc provides __stack_chk_guard in %gs:0x14.  */
#define TARGET_THREAD_SSP_OFFSET    0x14

gcc使用了__stack_chk_guard和__stack_chk_fail之后就完成了canary实现的协议的编译器 的那部分, 接着查看glibc的细节.

glibc

首先查看用户代码canary出错时的glibc错误输出代码:

eglibc-2.19/debug/stack_chk_fail.c
#line 24
void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}

上面的代码非常简单就是输出出错信息.

接着我们将分析glibc对于整个canary机制的实现过程的代码.

首先给出跟canary相关的调用栈:

#0  security_init () at rtld.c:854
// 相当于glibc/dynamic-linker的main
#1  dl_main () at rtld.c:1818
#2  _dl_sysdep_start () at ../elf/dl-sysdep.c:249
#3  _dl_start_final () at rtld.c:331
#4  _dl_start () at rtld.c:557
// glibc/dynamic-linker入口
#5  _start () from /lib/ld-linux.so.2

// security_init初始化canary的值到%gs:0x14, 所以这个函数是真正的关键所在. 我们在
// 下面的代码里会添加注释详细解释.
static void
security_init (void)
{
  /* Set up the stack checker's canary.  */
  // _dl_random的值其实在进入这个函数的时候就已经由kernel写入了. 也就是说glibc直
  // 接使用了_dl_random的值并没有给赋值, 进入下面的函数会看到其实如果不是采用TLS
  // 这种模式支持, glibc是可以自己产生随机数的. 但是做为普遍情况来说, _dl_random就
  // 是由kernel写入的. 所以_dl_setup_stack_chk_guard()的行为就是将_dl_random的最
  // 后一个字节设置为0x00.
  uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
#ifdef THREAD_SET_STACK_GUARD
  // TLS会进入这里. macro的定义及其展开详见下面.
  THREAD_SET_STACK_GUARD (stack_chk_guard);
  // 删掉部分代码
  // ...
  /* We do not need the _dl_random value anymore.  The less
     information we leave behind, the better, so clear the
     variable.  */
  _dl_random = NULL;
}

// 这个宏只是一个过渡, header的定义见下面.
//
/* Set the stack guard field in TCB head.  */
#define THREAD_SET_STACK_GUARD(value) \
  THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

// header的定义.
// TLS相关的数据结构, 注意元素stack_guard的偏移是20也就是0x14.
//
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;
  uintptr_t pointer_guard;
  int gscope_flag;
#ifndef __ASSUME_PRIVATE_FUTEX
  int private_futex;
#else
  int __unused1;
#endif
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[4];
  /* GCC split stack support.  */
  void *__private_ss;
} tcbhead_t;

// 这个是会将canary的值写入%gs:0x14. 进行了删减之保留主要部分.
//
/* Same as THREAD_SETMEM, but the member offset can be non-constant.  */
# define THREAD_SETMEM(descr, member, value) \
  ({if (sizeof (descr->member) == 4)                              \
       asm volatile ("movl %0,%%gs:%P1" :                         \
                     : "ir" (value),                              \
                       "i" (offsetof (struct pthread, member)));  \
   })

由此就完成了glibc对canary的值的写入工作.

linux kernel

linux初始化gs, 就是跟TLS相关的寄存器, TLS相关的部分i386比较复杂, 由于跟canary没有太 大关系, 具体其他细节可以参考下面的源文件的注释部分描述.

linux-4.8/arch/x86/include/asm/stackprotector.h
#line 99
static inline void load_stack_canary_segment(void)1
{
#ifdef CONFIG_X86_32
    asm("mov %0, %%gs" : : "r" (__KERNEL_STACK_CANARY) : "memory");
#endif
}

到这里我们就基本上解释清楚了canary的这条代码线.

03 总结

通过本文可以比较清楚地知道canary的值的流向, kernel初始化了跟TLS相关的寄存器gs, 并 且提供了canary这个随机值, glibc写入%gs:0x14这个保存随机值的位置并且提供变量定义和 打印函数定义, 最上面是gcc插入对canary的值的引用和出错函数到用户代码里.

live long and prosper.