一、概述
栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary(以下统一使用canary)。
gcc在4.2版本中添加了-fstack-protector和-fstack-protector-all编译参数以支持栈保护功能,4.9新增了-fstack-protector-strong编译参数让保护的范围更广。以下是-fstack-protector和-fstack-protector-strong的区别:
Linux系统中存在着三种类型的栈:
- 应用程序栈:工作在Ring3,由应用程序来维护;
- 内核进程上下文栈:工作在Ring0,由内核在创建线程的时候创建;
- 内核中断上下文栈:工作在Ring0,在内核初始化的时候给每个CPU核心创建一个。
二、 应用程序栈保护
1. 栈保护工作原理
下面是一个包含栈溢出的例子:
- /* test.c */
- #include <stdio.h>
- #include <string.h>
- int main(int argc, char **argv)
- {
- char buf[16];
- scanf("%s", buf);
- printf("%s\n", buf);
- return 0;
- }
我们先禁用栈保护功能看看执行的结果
- [root@localhost stackp]# gcc -o test test.c -fno-stack-protector
- [root@localhost stackp]# python -c "print 'A'*24" | ./test
- AAAAAAAAAAAAAAAAAAAAAAAA
- Segmentation fault <- RIP腐败,导致异常
当返回地址被覆盖后产生了一个段错误,因为现在的返回地址已经无效了,所以现在执行的是CPU的异常处理流程。我们打开栈保护后再看看结果:
- [root@localhost stackp]# gcc -o test test.c -fstack-protector
- [root@localhost stackp]# python -c "print 'A'*25" | ./test
- AAAAAAAAAAAAAAAAAAAAAAAAA
- *** stack smashing detected ***: ./test terminated
这时触发的就不是段错误了,而是栈保护的处理流程,我们反汇编看看做了哪些事情:
- 0000000000400610 <main>:
- 400610: 55 push %rbp
- 400611: 48 89 e5 mov %rsp,%rbp
- 400614: 48 83 ec 30 sub $0x30,%rsp
- 400618: 89 7d dc mov %edi,-0x24(%rbp)
- 40061b: 48 89 75 d0 mov %rsi,-0x30(%rbp)
- 40061f: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax <- 插入canary值
- 400626: 00 00
- 400628: 48 89 45 f8 mov %rax,-0x8(%rbp)
- 40062c: 31 c0 xor %eax,%eax
- 40062e: 48 8d 45 e0 lea -0x20(%rbp),%rax
- 400632: 48 89 c6 mov %rax,%rsi
- 400635: bf 00 07 40 00 mov $0x400700,%edi
- 40063a: b8 00 00 00 00 mov $0x0,%eax
- 40063f: e8 cc fe ff ff callq 400510 <__isoc99_scanf@plt>
- 400644: 48 8d 45 e0 lea -0x20(%rbp),%rax
- 400648: 48 89 c7 mov %rax,%rdi
- 40064b: e8 80 fe ff ff callq 4004d0 <puts@plt>
- 400650: b8 00 00 00 00 mov $0x0,%eax
- 400655: 48 8b 55 f8 mov -0x8(%rbp),%rdx <- 检查canary值
- 400659: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
- 400660: 00 00
- 400662: 74 05 je 400669 <main+0x59> # 0x400669
- 400664: e8 77 fe ff ff callq 4004e0 <__stack_chk_fail@plt>
- 400669: c9 leaveq
- 40066a: c3 retq
我们看到函数开头(地址:0x40061f)处gcc编译时在栈帧的返回地址和临时变量之间插入了一个canary值,该值是从%fs:0x28里取的,栈帧的布局如下: 在函数即将返回时(地址:0x400655)检查栈中的值是否和原来的相等,如果不相等就调用glibc的_stackchk_fail函数,并终止进程。 2. canary值的产生 这里以x64平台为例,canary是从%fs:0x28偏移位置获取的,%fs寄存器被glibc定义为存放tls信息的,我们需要查看glibc的源代码: 结构体tcbheadt就是用来描述tls的也就是%fs寄存器指向的位置,其中+0x28偏移位置的成员变量stackguard就是canary值。另外通过strace ./test看到在进程加载的过程中会调用arch_prctl系统调用来设置%fs的值, 产生canary值的代码在glibc的dlmain和_libcstart_main函数中: dlrandom是一个随机数,它由dlsysdepstart函数从内核获取的。dlsetupstackchkguard函数负责生成canary值,THREADSETSTACK_GUARD宏将canary设置到%fs:0x28位置。 在应用程序栈保护中,进程的%fs寄存器是由glibc来管理的,并不涉及到内核提供的功能。 3. x32应用程序栈保护 解读完了x64的实现,我们来看看x32下面的情况,我们还是使用上面例子的代码在x32的机器上编译,得到下面的代码: