什么是堆栈保护技术?

堆栈保护技术(即Stack canary)是用于防护栈溢出攻击的一种保护机制:在栈上的返回地址跟ebp之前加上一个标志位canary,返回时通过验证这个canary是否被改写,从而判断程序是否被栈溢出攻击。

如何开启堆栈保护技术?

用gcc编译时,可以用以下参数来设置:

1
2
3
4
5
-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all 启用保护,为所有函数插入保护
-fstack-protector-strong
-fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护
-fno-stack-protector 禁用保护

canary的产生

在开启了堆栈保护技术程序中,我们再ida经常可以看到用如下的指令来获取canary:

1
mov     rax, fs:28h

其中,fs其实是指向当前栈的TLS结构:

1
2
3
4
5
6
7
8
9
10
11
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;

fs+0x28h即为stack_guard。

而TLS结构是由security_init()初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

security_init()是ld文件中的一个子函数,在ida中可以看到,初始化canary为如下指令(以2.23版本为例):

1
2
3
4
5
6
7
8
.text:0000000000003C25                 mov     rdx, cs:qword_225E68
.text:0000000000003C2C mov rax, [rdx]
.text:0000000000003C2F xor al, al
.text:0000000000003C31 mov fs:28h, rax
.text:0000000000003C3A mov rax, [rdx+8]
.text:0000000000003C3E mov fs:30h, rax
.text:0000000000003C47 mov cs:qword_225E68, 0
.text:0000000000003C52 mov cs:qword_225C70, rax

其中,cs:qword_225E68即对应对应于存放_dl_random的地址。

(注:fs寄存器的内容可以通过在gdb中直接输入fsbase得到。)

而_dl_random的地址来自:

1
2
3
4
.text:0000000000019741                 cmp     [rsp+78h+var_49], 0
.text:0000000000019746 jz loc_1983C
.text:000000000001974C mov rax, [rsp+20h]
.text:0000000000019751 mov cs:qword_225E68, rax

这里下个内存断点来跟进(直接关闭aslr):

1
2
pwndbg> watch *(0x7fffffffde80+0x20)
Hardware watchpoint 5: *(0x7fffffffde80+0x20)

结果跟进到 ► 0x7ffff7df0786 <_dl_sysdep_start+518> jmp _dl_sysdep_start+304 <0x7ffff7df06b0>里面来,在ida下看看:

1
2
3
4
5
.text:0000000000019778 loc_19778:
.text:0000000000019778 mov rax, [rdx+8]
.text:000000000001977C mov [rsp+78h+var_49], 1
.text:0000000000019781 mov [rsp+20h], rax
.text:0000000000019786 jmp loc_196B0

可以看到rsp+0x20的值来自[rdx+8]。分析整个函数,可以得知rdx的值是来自于_dl_sysdep_start函数的第一个参数(有一定的偏移,其值为0x7fffffffdf70,而_dl_random的值为0x7fffffffe2c9)。

继续追溯可以找到也是_dl_start来自的第一个参数(同样是0x7fffffffdf70)。

最终追溯到ld中start函数的rsp:0x7fffffffdf70。

绕过堆栈保护方法

canary绕过方式总结下有这几种:

  1. 直接通过相邻的变量puts或者printf,然后leak出来
  2. 逐位爆破
  3. 通过覆盖TLS结构(地址可以通过与libc的偏移计算得出)中的canary,从而绕过canary保护
  4. 劫持___stack_chk_fail的got表项
  5. 劫持栈上*** stack smashing detected ***: ./canary terminated中对应./canary处的指针,在发生smashing detected时泄漏出关键信息