我们都听说过缓冲区漏洞,那么具体原理是怎样的呢?
首先我们看这个例子:
#include<stdio.h>
int main(){
int i=0;
int a[]={1,2,3,4,5,6,7,8,9,10};
for(i=0;i<=11;i++){
printf("Hello World!\n", i);
}
}
我们使用Deepin Linux 15.5 64位操作系统,GCC 6.4.0编译器进行编译,得到了非常正确的结果:
12个"Hello World!",这是我们预料之中的。因为我们让i从0循环到11,i的值也从0,一点点+1,到了11,所以自然就打印出了11个hello world!。
但是,倘若我们在for语句中加一句赋值语句呢?请看下面的代码:
#include<stdio.h>
int main(){
int i=0;
int a[]={1,2,3,4,5,6,7,8,9,10};
for(i=0;i<=11;i++){
a[i]=0; //加上了这句赋值语句
printf("Hello World!\n", i);
}
}
继续使用我们的GCC编译运行,结果却出乎预料:
输出成了死循环!
这段代码很明显,数组a越界了。可能有的同学在平时的编程中也会遇到过这样的情况,某个数组越界后程序的流程发生了严重的改变,那么是什么原因造成的这种改变呢?我们就上面的例子进行分析。
首先我们查看一下编译器在编译我们的程序时生成的汇编指令:
我们使用 "gcc 文件名.c -S "命令生成汇编指令文件,然后打开"文件名.s":
我们看到了这一段:
pushq %rbp
......
subq $48, %rsp
movl $0, -4(%rbp)
movl $1, -48(%rbp)
movl $2, -44(%rbp)
movl $3, -40(%rbp)
movl $4, -36(%rbp)
movl $5, -32(%rbp)
movl $6, -28(%rbp)
movl $7, -24(%rbp)
movl $8, -20(%rbp)
movl $9, -16(%rbp)
movl $10, -12(%rbp)
movl $0, -4(%rbp)
jmp .L2
没有学过汇编看不懂上面的也无所谓,下面这种写法可以直接在运行结果中查看变量存放的位置:
#include<stdio.h>
int main(){
int i=0;
int a[]={1,2,3,4,5,6,7,8,9,10};
for(i=0;i<=11;i++){
printf("%p->%d\n", &(a[i]), a[i]);
}
}
运行结果:
看到最后一个数,也就是a[11]的值,等于11,这个值与i的值一模一样,那么会不会是a[11]就等于i呢?我们继续实验:
#include<stdio.h>
int main(){
int i=100;
int a[]={1,2,3,4,5,6,7,8,9,10};
for(i=0;i<=11;i++){
printf("&i = %p\t&a[11] = %p\n", &a[11], &i);
}
}
运行结果:
我们发现,a[11]就是i!
其实函数的局部变量在运行的时候是在堆栈中保存的,堆栈的特征是先进后出。分析上面的汇编代码或者那个输出地址的C语言代码我们可以知道,在内存中,变量的存放位置是:
a[0]--a[9],i
至于a[10],可能是GCC编译器在编译过程中特意防止越界而留出的一块地址。而a[11]自然也就是a[10]后面的地址,也就是i。
在循环中,赋值语句a[11]=0等同于i=0,这样就会导致for循环不断进行,也就解释了为什么会死循环。
在一些大型程序的编写时,必须要检查数组边界(包括字符串),否则用户输入的内容一旦超过数组所能容纳的最大容量,后面正好还有这存放其他变量的存储空间,就很容易把其他变量的空间地址覆盖,等同于用户非法的修改了变量。这样就是一个缓冲区溢出漏洞。一旦被有心人精心计算,加以利用,就会造成十分严重的后果。