引言
在csapp(深入理解计算机系统)
一书中提到了对抗缓冲区溢出的方法,主要有三点:
- 栈随机化
- 栈破坏检测
- 限制可执行代码区域
今天我们主要来了解一下Linux系统是如何应对缓冲区溢出的;
目录
- 什么是缓冲区溢出
- 缓冲区溢出带来的危害
- 栈随机化
- 栈破坏检测
1.什么是缓冲区溢出
相信大家对于缓冲区溢出都不陌生,比如一个int a[10]
的数组,当我们想为第11个元素赋值时就会发生溢出例如a[11] = 11;
,C语言
中并没有规定不能这样赋值,此时编译器也是不会报错的,C语言
选择相信程序员让程序员自己来检查数组的溢出,java
则带有对于缓冲区溢出的检测,溢出有的时候并不会带来危害;但是总有些心怀不轨的人利用溢出做坏事;
2.缓冲区溢出的危害
在说危害之前,我们先来说说函数是怎样被调用然后被执行的
int add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int a = 1;
int b = 2;
int c = add(a, b);
printf("c = %d\n", c);
return 0;
}
我们就用这个很简单的程序来简单解释一下程序是怎样调用函数的;
我们先执行main()
函数的内容,当执行到c = add(a, b);
这条语句时main()
函数会调用add()
函数,因为程序都是在栈中运行的,所以他会先将要传入的参数保存在寄存器中,保存参数的寄存器只有7个当参数数量超过了7个以后会将参数压栈,然后将call add
下一条指令的地址压栈,然后创建被调用函数的局部变量,然后给传入的参数分配内存,然后开始执行被调用函数的指令;
基本的攻击就是通过改变保存的指令地址,改成攻击程序的指令地址,执行完add函数以后他就会不会返回main
函数就会去执行攻击程序;
3.栈随机化
栈随机化的实现方式是:程序开始时,在栈上分配一段0~n
字节之间的随机大小的空间,程序不使用这段空间,但是他会导致程序每次执行时后续的栈的位置发生了变化,分配的n要足够大才能获得足够多的栈地址变化,但是又要足够小,不至于浪费太多的空间;
画个图吧:
每次n的值都不同,所以导致每次程序开始的地方不同;
那么为什么这样能防止溢出攻击呢,攻击者要插入代码,就需要指向这段代码的指针,就需要知道这个指针存放的地址,如果每一次运行的地址都一样的话,那么很容易就能得到这个指针的地址,
在Linux 系统中栈随机化已经成为了一种标准行为,他是更大技术的一种,这类技术被称为地址空间布局随机化简称ASLR
举个栗子:
// 观察ASLR(地址随机化)
#include <stdio.h>
int main()
{
int a = 1;
float b = 1.0;
char c = 'a';
printf("a = %p, b = %p, c = %p\n", &a, &b, &c);
return 0;
}
当我们在没有关闭ASLR
时它的运行结果是这样的:
我们可以看到每一次的变量地址都不一样;
但是当我们关闭ASLR
后:
我们可以发现每一次执行代码,他的地址都是一样的;
这个ASLR
好像在windows
中并没有,当我们在windows
下的vc++6.0
上运行这个程序时每一次代码的地址都是一样的;
4.栈破坏检测
如果说栈随机化是第一道防线,那么栈破坏检测就是第二道防线,在gcc4.1
以后的版本加入了一种栈保护机制
他在被调用函数的局部变量和传入参数之间添加一个特殊的值,被称为金丝雀值
,也称为哨兵值
,是在程序每次运行时随机产生的,攻击者没有简单的办法能知道这个值是多少,当这个值被改变了,程序就会在函数返回时终止;
下面我们用一个例子来说明这个东西:
#include <stdio.h>
int try(int a, int b)
{
int c;
c = a + b;
int *p = &a;
printf("%d\n", c);
return 0;
}
int main()
{
int a = 1;
int b = 2;
try(a, b);
return 0;
}
我们首先使用这段代码来获取金丝雀值的位置
;
我们通过查看反汇编代码来查找他的相对位置;
编译时gcc -fstack-protector-all 5.c
加入-fstack-protector-all
就会产生这个金丝雀值
如果不加-all
就只有在局部变量为char
时才会产生这个哨兵
;
编译完成过后执行objdump -S a.out
就能获得反汇编的代码了:
看到6c1
那一行可以看出来金丝雀值
被存放在了rbp-0x8
这个地方,而我们看到a的值
被存放在了rbp-0x24
这个位置;
这里我们就要说到函数传递参数的问题了;
我们来看722到738
这几行,就是局部变量的赋值加参数的传递;现在大家只用知道,参数a, b
通过esi
和edi
这两个寄存器传递到了函数中;
知道了a
的地址和哨兵
的地址我们尝试修改哨兵
的值来看看会发生什么;
#include <stdio.h>
int try(int a, int b)
{
int c;
c = a + b;
int *p = (int *)((char *)&a+28);
*p = 3;
printf("%d\n", c);
return 0;
}
int main()
{
int a = 1;
int b = 2;
try(a, b);
return 0;
}
执行结果:
为什么还会打印3,因为对于哨兵
的检测发生在函数返回时,看try
函数的反汇编代码的6f9
行;就是检测哨兵值有没有被改变;
他发生在调用printf
函数以后;