在当前网络与分布式系统安全中,被广泛利用的50%以上都是缓冲区溢出,其中最著名的例子是1988年利用fingerd漏洞的蠕虫。而缓冲区溢出中,最为危险的是堆栈溢出,因为入侵者可以利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址,带来的危害一种是程序崩溃导致拒绝服务,另外一种就是跳转并且执行一段恶意代码,比如得到shell,然后为所欲为。我在这里演示一下堆栈溢出的原理。
首先,介绍一下,与堆栈有关的一些概念:动态内存有两种,堆栈(stack),堆(heap)。堆栈在内存上端,堆在内存下端,当程序执行时,堆栈向下朝堆增长,堆向上朝堆栈增长。通常,局部变量,返回地址,函数的参数,是放在堆栈里面的。
低地址
局部变量
旧的基指针
返回地址
函数的参数(左)
函数的参数(。。。)
函数的参数(右)
高地址
我们可以写一个小程序测试:
#include "string.h"
void test(char *a);
int main(int argc, char* argv[])
{
char a[] = “hello”;
test(a);
return 0;
}
void test(char *a)
{
char* j;
char buf[6];
strcpy(buf,a);
printf("&main=%p\n",&main);
printf("&buf=%p\n",&buf);
printf("&a=%p\n",&a);
printf("&test=%p\n",&test);
for ( j=buf-8;j<((char *)&a)+8;j++)
printf("%p: 0x%x\n",j, *(unsigned char *)j);
}
Main定义一个字符串hello,然后调用test函数,在test函数中,有一个长度为6的局部字符串变量buf,然后把复制参数a复制到buf中,这里因为没有a的长度小于等于buf的长度,所以并没有溢出buf。然后显示各个函数,参数,局部变量的地址,以及局部字符串变量buf和参数a之间的地址,我们看到:
&main=0040100A
&buf=0012FF14
&a=0012FF28
&test=00401005 //这里说下,不同人的机子结果可能不太一样,即使是同
0012FF0C: 0xcc 一台机子,在不同的时间,不同的环境下,结果也会不
0012FF0D: 0xcc 一样,再寻找函数返回地址时候,可以根据参数来判断
0012FF0E: 0xcc 例如,本例可以根据参数a字符串的地址,找到函数main
0012FF0F: 0xcc 的返回地址,也就是0x0012FF24开始,是main的函数的
0012FF10: 0xcc 在调用test函数后要执行的指令的地址,即0x0040b834
0012FF11: 0xcc
0012FF12: 0xcc
0012FF13: 0xcc
0012FF14: 0x68 h 这里就是buf了!
0012FF15: 0x65 e
0012FF16: 0x6c l
0012FF17: 0x6c l
0012FF18: 0x6f o
0012FF19: 0x0
0012FF1A: 0xcc
0012FF1B: 0xcc
0012FF1C: 0x1c 这里是
0012FF1D: 0xff 两个
0012FF1E: 0x12 旧的
0012FF1F: 0x0 基指针,不管他
0012FF20: 0x80
0012FF21: 0xff
0012FF22: 0x12
0012FF23: 0x0
0012FF24: 0x34 这个就是
0012FF25: 0xb8 返回地址了
0012FF26: 0x40 和main的地址很
0012FF27: 0x0 接近吧!
0012FF28: 0x78 这个是
0012FF29: 0xff 参数a,即
0012FF2A: 0x12 a字符串的
0012FF2B: 0x0 地址
0012FF2C: 0xe
0012FF2D: 0x0
0012FF2E: 0x0
0012FF2F: 0x0
由于c编译器不会自己做边界检查的,所以,如果buf中的内容足够长,而不是hello,那么很有可能覆盖掉原来的返回地址,那么程序就会跳转到其他地方,为了试验,我们定义一个简单的函数echo,让test返回的时候跳转到echo。
用printf("&echo=%p\n",&echo); 我已经知道了echo的地址是0x0040100f,从上面的例子已经知道从0012ff14到0012ff27要覆盖多少数据,数一下就知道了 改写如下:
for(i=0;i<16;i++) a='x';//覆盖不重要的部分,为了达到返回地址
a[16]=0xf; //在这里改写了返回地址
a[17]=0x10;
a[18]=0x40;
a[19]=0x00;//一方面高字节正好是00,同时00又是字符串的结尾
test(a);
return 0;
}
void test(char *a)
{
char* j;
char buf[6];
strcpy(buf,a); //分配的缓冲区只有6,结果却有19,溢出了!
printf("&main=%p\n",&main);
printf("&buf=%p\n",&buf);
printf("&a=%p\n",&a);
printf("&echo=%p\n",&echo);
printf("&test=%p\n",&test);
for ( j=buf-8;j<((char *)&a)+8;j++)
printf("%p: 0x%x\n",j, *(unsigned char *)j);
}
void echo()
{
printf("haha!\n");
printf("haha!\n");
printf("haha!\n");
printf("haha!\n");
}
结果,我们看到地址显示完以后,出现了echo函数里面的haha!\nhaha!\nhaha!\nhaha!\n,说明溢出跳转成功,但是,在结束的时候出现了程序崩溃,这是因为echo找不到它的返回地址导致的。
#include "string.h" //这里做实验的时候要注意,buf和返回地址的间隔不同的环境
void test(char *a); 可能也不同。所以在调试的时候,要先找到返回地址和buf起
void echo(); 始地址的间隔,然后再覆盖就OK了。这里的实验是16个间隔