如何利用GCC编译选项检测栈溢出
Stack smashing是堆栈缓冲区溢出(stack buffer overflow)的一个时髦称谓。它表示利用代码中存在的缓冲区溢出bug而发起的攻击。在早期,这完全是程序员的责任,他们要确保代码中不存在缓冲区溢出的问题。但是随着时间推移,技术的不断发展,现在像gcc这样的编译器已经有编译选项用来确保缓冲区溢出问题不被攻击者利用来破坏系统或者程序。
有一次当我试图重现一个缓冲区溢出的问题时我才了解到这些编译选项。我是在Ubuntu 12.04上进行试验的,gcc版本为4.6.3。我所做的很简单:
#include <stdio.h>
#include <string.h>
int main(void)
{
int len = 0;
char str[10] = {0};
printf("\n Enter the name \n");
gets(str); // Used gets() to cause buffer overflow
printf("\n len = [%d] \n", len);
len = strlen(str);
printf("\n len of string entered is : [%d]\n", len);
return 0;
}
在上面的代码中,我故意使用gets()函数来接收字符串,之后计算字符串的长度,并输出到标准输出—默认是屏幕。这里的想法是输入超过10个长度的字符串,由于gets()函数并不检测数组边界,所以它将会把字符写入到10个以外的地址,这样就会发生缓冲区溢出。我运行程序之后的结果如下所示:
$ ./stacksmash
Enter the name
TheGeekStuff
len = [0]
len of string entered is : [12]
*** stack smashing detected ***: ./stacksmash terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x45)[0xb76e4045]
/lib/i386-linux-gnu/libc.so.6(+0x103ffa)[0xb76e3ffa]
./stacksmash[0x8048548]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xb75f94d3]
./stacksmash[0x8048401]
======= Memory map: ========
08048000-08049000 r-xp 00000000 08:06 528260 /home/himanshu/practice/stacksmash
08049000-0804a000 r--p 00000000 08:06 528260 /home/himanshu/practice/stacksmash
0804a000-0804b000 rw-p 00001000 08:06 528260 /home/himanshu/practice/stacksmash
0973a000-0975b000 rw-p 00000000 00:00 0 [heap]
b75af000-b75cb000 r-xp 00000000 08:06 787381 /lib/i386-linux-gnu/libgcc_s.so.1
b75cb000-b75cc000 r--p 0001b000 08:06 787381 /lib/i386-linux-gnu/libgcc_s.so.1
b75cc000-b75cd000 rw-p 0001c000 08:06 787381 /lib/i386-linux-gnu/libgcc_s.so.1
b75df000-b75e0000 rw-p 00000000 00:00 0
b75e0000-b7783000 r-xp 00000000 08:06 787152 /lib/i386-linux-gnu/libc-2.15.so
b7783000-b7784000 ---p 001a3000 08:06 787152 /lib/i386-linux-gnu/libc-2.15.so
b7784000-b7786000 r--p 001a3000 08:06 787152 /lib/i386-linux-gnu/libc-2.15.so
b7786000-b7787000 rw-p 001a5000 08:06 787152 /lib/i386-linux-gnu/libc-2.15.so
b7787000-b778a000 rw-p 00000000 00:00 0
b7799000-b779e000 rw-p 00000000 00:00 0
b779e000-b779f000 r-xp 00000000 00:00 0 [vdso]
b779f000-b77bf000 r-xp 00000000 08:06 794147 /lib/i386-linux-gnu/ld-2.15.so
b77bf000-b77c0000 r--p 0001f000 08:06 794147 /lib/i386-linux-gnu/ld-2.15.so
b77c0000-b77c1000 rw-p 00020000 08:06 794147 /lib/i386-linux-gnu/ld-2.15.so
bfaec000-bfb0d000 rw-p 00000000 00:00 0 [stack]
Aborted (core dumped)
令我惊讶的是,运行环境居然可以检测到缓冲区溢出的情况。你可以在输出信息上看到“检测到栈溢出”(stack smashing detected)的信息。这促使我去探索缓冲区溢出是如何被检测到的。
当我探索原因时,我发现了gcc的一个编译选项:-fstack-protector,以下是关于这个选项的描述:
-fstack-protector
启用该选项后编译器会产生额外的代码来检测缓冲区溢出,例如栈溢出攻击。这是通过在有缺陷的函数中添加一个保护变量来实现的。这包括会调用到alloca的函数,以及具有超过8个字节缓冲区的函数。当执行到这样的函数时,保护变量会得到初始化,而函数退出时会检测保护变量。如果检测失败,会输出一个错误信息并退出程序。
!注意:在Ubuntu 6.10以及之后的版本中,如果编译时没有指定-fno-fstack-protector, -nostdlib或者-ffreestanding选项的话,那么这个选项对于C,C++,ObjC,ObjC++语言默认是启用的。
所以,你会发现gcc已经使用插入附加代码的方式来检测缓冲区溢出的问题。我想到的下一个问题是,我从来没有在编译时加入这个编译选项,这个功能是怎样启用的?然后我读到最后两行,在Ubuntu6.10之后的版本上,此功能已经默认启用了。
下一步,我决定使用-fno-fstack-protector选项来取消这个栈溢出检测功能。我对同样的代码编译之后运行,使用和之前一样输入,下面是我的做法以及运行结果:
$ gcc -Wall -fno-stack-protector stacksmash.c -o stacksmash
$ ./stacksmash
Enter the name
TheGeekStuff
len = [26214]
len of string entered is : [12]
可以看到,一旦使用了这个编译选项(根据前面的编译选项说明,这里-fstack-protector是不会默认开启的),使用相同的输入,运行环境根本无法检测到缓冲区溢出的问题,len的值已经被破坏了。
当然,如果你对gcc很陌生,你也应该理解我们之前讨论过的最常用的gcc编译选项。