缓冲区溢出早在1972年就被发现,直到今天,还有很多程序员中招。用C语言不注意检查数组或者指针边界,一些C标准库里的函数使用不当,都有可能引起缓冲区溢出。
#include <stdio.h>
char *_gets(char *s)
{
int c;
char *dest = s;
int gotchar=0;
while ((c = getchar()) != '\n' && c!= EOF) {
*dest++ = c;
gotchar = 1;
}
*dest++ = '\0';
if (c == EOF && !gotchar)
return NULL;
return s;
}
void echo()
{
char buf[8];
_gets(buf);
puts(buf);
}
这段代码用库函数getchar()实现另一个库函数gets(),一个存在缓冲区溢出漏洞的版本_gets(),因为它没有办法确定用来保存输入的空间是否足够。如果空间足够小,像echo()里的8个字节,那么任何长度超过7个字符的输入都会导致数组越界。
_echo:
00000000000000b0 pushq %rbp
00000000000000b1 movq %rsp,%rbp
00000000000000b4 subq $0x10,%rsp
00000000000000b8 leaq __gets.eh(%rbp),%rax
00000000000000bc movq %rax,%rcx
00000000000000bf movq %rcx,%rdi
00000000000000c2 movq %rax,0xf0(%rbp)
00000000000000c6 callq __gets
00000000000000cb movq 0xf0(%rbp),%rax
00000000000000cf movq %rax,%rdi
00000000000000d2 callq _puts
00000000000000d7 addq $0x10,%rsp
00000000000000db popq %rbp
00000000000000dc ret
汇编代码(由i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1产生)说明了栈的组织方式。

一旦越界的字符很多,就会破坏调用者中保存的状态,导致程序进入异常状态。
所谓的缓冲区溢出攻击,就是利用这种情况。输入特定的字符串给程序,内容是一些可执行代码的字节编码,让程序执行外部代码,或者覆盖返回地址,让程序跳转到攻击代码,然后,攻击者就可以为所欲为。
缓冲区溢出攻击太过普遍,以致于现代的编译器和操作系统已经实现了一些机制来最小化这种攻击。GCC会尝试加入额外的代码以检测栈是否溢出,选项-fstack-protector说明:
-fstack-protector
Emit extra code to check for buffer overflows, such as stack smashing attacks. This is done by adding a guard variable to functions with vulnerable objects. This includes functions that call alloca, and functions with buffers larger than 8 bytes. The guards are initialized when a function is entered and then checked when the function exits. If a guard check fails, an error message is printed and the program exits.
实际上,为了得到上面展示的汇编代码,要使用-fno-stack-protector选项编译源码。这次不用这个选项重新编译。
_echo:
00000000000000b0 pushq %rbp
00000000000000b1 movq %rsp,%rbp
00000000000000b4 subq $0x20,%rsp
00000000000000b8 movq ___stack_chk_guard(%rip),%rax //计算canary
00000000000000bf movq (%rax),%rax
00000000000000c2 movq %rax,0xf8(%rbp) //存到栈上,%rbp-8
00000000000000c6 leaq 0xf0(%rbp),%rax
00000000000000ca movq %rax,%rcx
00000000000000cd movq %rcx,%rdi
00000000000000d0 movq %rax,0xe8(%rbp)
00000000000000d4 callq __gets
00000000000000d9 movq 0xe8(%rbp),%rax
00000000000000dd movq %rax,%rdi
00000000000000e0 callq _puts
00000000000000e5 movq ___stack_chk_guard(%rip),%rax //再次计算canary
00000000000000ec movq (%rax),%rax
00000000000000ef movq 0xf8(%rbp),%rcx //取出栈上的canary
00000000000000f3 cmpq %rcx,%rax //检查是否有变化
00000000000000f6 jne 0x000000fe
00000000000000f8 addq $0x20,%rsp
00000000000000fc popq %rbp
00000000000000fd ret
0000000000000fe callq ___stack_chk_fail //栈破坏!
新的栈结构图

这个新的canary,就是哨兵,由程序运行时随机产生。在从函数返回之前,程序检测这个值以确认栈未被破坏,达到避免攻击的目的。