在x86体系结构下, C++/C对数组引用不进行任何边界检查,而且函数调用过程中局部变量和状态信息(例如寄存器值和返回指针)都存放在栈中。当这两种情况结合到一起的时候,就有可能导致严重的程序错误,一个对越界的数组元素的写操作会破坏存储在栈中的状态信息,而当程序使用这个被破坏的状态,并试图重新加载寄存器或执行ret指令(返回调用函数)时,就会产生严重的错误。
在C++/C语言中,数组元素被依次分配在一组连续的地址空间中,且长度固定不变。在表达式中使用数组名时,该名字会自动转换为指向数组第一个元素的指针。当在编程时没有检查数组下标,并且引用了越出数组边界的元素时,就会导致缓冲区溢出。除了程序员自己注意细节,并彻底测试自己的程序之外,没有别的办法可以防止数组越界。
在x86体系结构中,栈是向低地址方向增长的,入栈是减小栈指针的值,并存放到寄存器中,出栈则是从存储器中读,并增加栈指针的值。
图1 栈结构图
char *gets(char *s)
{
int c;
char *dest = s;
while((c=getchar() != ’/n’ && c != EOF)
*dest ++= c;
*dest ++= ’/0’;
If(c == EOF)
Return NULL;
return s;
}
void echo()
{
char buf[4];
gets(buf);
puts(buf);
}
我们来看这个gets函数的实现,它从标准输入读入一行,在遇到一个“/n”字符或者某个错误情况时停止。它将这个字符串拷贝到参数s指明的位置,并在字符串结尾加上null字符。在函数echo中调用了gets,读入标准输入,并送出到标准输出。
gets的问题出在它使用了一个字符串数组的指针,没有办法确定是否为保存整个字符串分配了足够的空间。在我们的echo函数中,我们故意将缓冲区设得非常小,只有四个字节长。任何长度超过3个字符的字符串都会导致写越界。
图2 栈示意图
如图所示,字符数组保存在状态信息下方的四个字节 ,所有对buf[4]~buf[7]的写操作都会破坏状态信息的保存值。当程序随后试图以它为栈指针进行恢复时,所有后来的栈引用都是非法的。而所有对buf[8]~buf[11]的写操作都会导致返回地址被破坏,当在函数结尾执行ret指令时,程序会“返回”到错误的地址。这个示例说明了缓冲区溢出可能导致程序出现严重的错误。
缓冲区溢出的一个更加致命的使用是让程序执行它本来不愿意执行的函数,利用这一漏洞可以通过计算机网络攻击系统的安全。通常,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为漏洞入侵代码(exploit code)。另外,还有一些字节会用一个指向缓冲区中那些可执行代码的指针覆盖掉返回指针。所以,执行ret指令的效果就是跳到漏洞入侵代码段。
第一种攻击形式为漏洞入侵代码使用系统调用启动一个shell程序,提供给攻击者一组操作系统的函数。另一种攻击形式,漏洞入侵代码会执行一些未授权的任务,修复对栈的破坏,然后第二次执行ret指令,看上去好像正常返回给调用者。
1、 在程序的自由存储区中创建并使用动态分配的数组,在C语言中使用malloc(C++中为new)操作符实现,在自由存储空间中创建的动态数组对象是没有名字的,程序员只能通过其地址间接访问堆中的对象。如果程序员能够准备计算出运行时需要的数组长度,就不必再担心因数组变量具有固定的长度而造成的溢出问题。
2、 在C++程序中,采用vector类型和迭代器取代一般的数组和指针访问。利用end操作可以返回迭代器指向vector“末端元素的下一个”,从而充当一个“哨兵”的作用,防止越界调用。
1、 Randal E. Bryant等,”Computer System A Programmer’s Perspective”
2、 Stanley B. Lippman等,”C++ Primer (Fourth Edition)”