用于阻止缓冲区溢出攻击的 Linux 内核参数与 gcc 编译选项

时间:2024-11-16 20:34:49

先来看看基于 Red Hat 与 Fedora 衍生版(例如 CentOS)系统用于阻止栈溢出攻击的内核参数,主要包含两项:

kernel.exec-shield

可执行栈保护,字面含义比较“绕”,

实际上就是用来控制能否执行存储在栈

中的代码,其值为1时表示禁止;为0时表示允许;默认为1,表示禁止执行栈

中的代码,如此一来,即便覆盖了函数的返回地址导致栈溢出,也无法执行

shellcode

查看与修改系统当前的可执行栈保护参数:

[root@localhost 桌面]# sysctl -a | grep -e exec

ernel.exec-shield = 1

[root@localhost 桌面]# cat /proc/sys/kernel/exec-shield

1

[root@localhost 桌面]# sysctl -w kernel.exec-shield=0

kernel.exec-shield = 0

[root@localhost 桌面]# cat /proc/sys/kernel/exec-shield

0

注意:

一,只有在学习和测试栈溢出攻击的原理时,才建议关闭可执行栈保护机制

二,在基于 Debian 与 Ubuntu 衍生版

(例如 BackTrack 5 Release 3)的系统上,不支持可执行栈保护机制:

root@bt:~# uname -a

Linux bt 3.2.6 #1 SMP Fri Feb 17 10:40:05 EST 2012 i686 GNU/Linux

root@bt:~# sysctl -a | grep -e kernel.exec

error: permission denied on key 'vm.compact_memory'

error: permission denied on key 'dev.parport.parport0.autoprobe'

error: permission denied on key 'dev.parport.parport0.autoprobe0'

error: permission denied on key 'dev.parport.parport0.autoprobe1'

error: permission denied on key 'dev.parport.parport0.autoprobe2'

error: permission denied on key 'dev.parport.parport0.autoprobe3'

error: permission denied on key 'net.ipv4.route.flush'

error: permission denied on key 'net.ipv6.route.flush'

root@bt:~#

或许是社区的开发人员认为,有下面另一种叫做堆栈地址随机化的机制,就足够应对缓冲区溢出攻击了,也可能是由于 Debian / Ubuntu 面向运行桌面应用居多的用户群体,它并不像销售给企业公司用户的 Red Hat 类发行版那样,对安全性的要求更为严格,才能保护用户的服务器,资产安全

kernel.randomize_va_space

堆栈地址随机初始化,很好理解,就是在每次将程序加载到内存时,进程地

址空间的堆栈起始地址都不一样,动态变化,导致猜测或找出地址来执行

shellcode 变得非常困难,它和可执行栈保护的区别在于,后者即便是找到了

地址也是无法执行的;所有的 2.6 以上版本的  Linux 内核都支持并启用了

这一项特性;

查看与修改系统当前的堆栈地址随机初始化参数:

[root@localhost 桌面]# sysctl -a | grep randomize

kernel.randomize_va_space = 2

[root@localhost 桌面]# cat /proc/sys/kernel/randomize_va_space

2

[root@localhost 桌面]# sysctl -w kernel.randomize_va_space=0

kernel.randomize_va_space = 0

[root@localhost 桌面]# cat /proc/sys/kernel/randomize_va_space

0

参数值为2时,表示启用随机地址功能;0表示关闭;

基于 Debian 与 Ubuntu 衍生版默认支持并且启用了随机地址功能:

1

2

root@bt:~# sysctl -a | grep -e kernel.randomize

kernel.randomize_va_space = 2

下面,我们在一个启用了随机地址功能的机器上,查看执行同一个程序4次加载的动态链接共享库的入口地址(linux-gate.so.1),发现每次的入口地址都不同,验证了随机地址的效果:

[root@centos6-5 桌面]# cat /proc/sys/kernel/randomize_va_space

2

[root@centos6-5 桌面]# ldd /bin/ls | grep linux-gate.so.1

linux-gate.so.1 =>  (0x00dff000)

[root@centos6-5 桌面]# ldd /bin/ls | grep linux-gate.so.1

linux-gate.so.1 =>  (0x00503000)

[root@centos6-5 桌面]# ldd /bin/ls | grep linux-gate.so.1

linux-gate.so.1 =>  (0x00213000)

[root@centos6-5 桌面]# ldd /bin/ls | grep linux-gate.so.1

linux-gate.so.1 =>  (0x004b5000)

[root@centos6-5 桌面]#

同理,关闭随机地址功能,连续5次查看执行程序时加载的 linux-gate.so.1 入口地址:

[root@centos6-5 桌面]# sysctl -w kernel.randomize_va_space=0

kernel.randomize_va_space = 0

[root@centos6-5 桌面]# ldd /bin/ls | grep linux-gate.so.1

linux-gate.so.1 =>  (0x00110000)

[root@centos6-5 桌面]# ldd /bin/ls | grep linux-gate.so.1

linux-gate.so.1 =>  (0x00110000)

[root@centos6-5 桌面]# ldd /bin/ls | grep linux-gate.so.1

linux-gate.so.1 =>  (0x00110000)

[root@centos6-5 桌面]# ldd /bin/ls | grep linux-gate.so.1

linux-gate.so.1 =>  (0x00110000)

[root@centos6-5 桌面]# ldd /bin/ls | grep linux-gate.so.1

linux-gate.so.1 =>  (0x00110000)

再来看看 GCC 4.1 版本以后引入的两个编译参数:

(默认情况下,编译时不会指定这两个参数,用于阻止缺乏安全编码意识的程序员写出存在缓冲区溢出漏洞的程序,

但是站在逆向工程的角度来看,指定这两个参数,特意构造有漏洞的程序,对于学习和理解栈溢出的原理还是有帮助的;

再次提醒,这里介绍的两个编译参数必须同时打开,而且还要同时禁用前面讲的可执行栈保护与栈地址随机初始化,满足这4个条件后,一般而言,就可以测试 shellcode 的效果了

GCC 编译选项  -fno-stack-protector

禁用栈保护功能,默认是启用的;

gcc 的栈保护机制是指,在栈的缓冲区(大小通常由程序员给定)写入内容前

,在结束地址之后与返回地址之前,放入随机的验证码,由于栈帧是从内存高

址段向内存低址段增长的(回顾第一张图),结束地址在高址段;返回地

址在低址段,栈溢出时,会从高址段向低址段覆盖数据,因此如果想要覆盖返

回地址的内容,必定先覆盖结束地址,验证码,才能覆盖返回地址,

因此可以通过比较写入缓冲区前后的验证码是否发生改变,来检测并阻止溢出

攻击

GCC 编译选项  -z execstack

启用可执行栈,默认是禁用的;

该选项与 ld 链接器有关:ld 链接器在链接程序的时候,如果所有的 .o 文

件的堆栈段都标记为不可执行,那么整个库的堆栈段才会被标记为不可执行;

相反,即使只有一个 .o 文件的堆栈段被标记为可执行,那么整个库的堆栈段

将被标记为可执行,

换言之,默认是所有的 .o 文件的堆栈段都标记为不可执行

检查堆栈段可执行性的方法是:

1

[root@centos6-5vm /]# readelf -lW stack-overflow1-test | grep GNU_STACK

找出在输出中有无 E 标记;有 E 标记就说明堆栈段是可执行的,其中,

stack-overflow1-test  为我们编写的简单栈溢出测试程序,源码如下:

#include <stdio.h>

greeting(char *temp1, char *temp2){

char name[20];

strcpy(name, temp2);

printf("hello  %s  %s\n", temp1, name);

}

main(int argc, char *argv[]){

greeting(argv[1], argv[2]);

printf("bye  %s  %s\n", argv[1], argv[2]);

}

这是一个典型的存在缓冲区溢出漏洞的程序,greeting 函数定义了一个只有20字节大小的字符数组(缓冲区),strcpy 函数不检查用户输入的第二个参数(由 main 函数的第二个参数传递)是否超过20字节,就写入这个缓冲区中

我们用 gcc 默认参数配置来编译这个源文件,然后检查堆栈段的可执行性:

[root@centos6-5vm /]# gcc -v -Wall  -o stack-overflow1-test stack-overflow1-test.c

(***************省略部分输出*************

GNU C (GCC) 版本 4.4.7 20120313 (Red Hat 4.4.7-4) (i686-redhat-linux)

由 GNU C 版本 4.4.7 20120313 (Red Hat 4.4.7-4) 编译,GMP 版本 4.3.1,MPFR 版本 2.4.1。

GGC 准则:--param ggc-min-expand=100 --param ggc-min-heapsize=131072

Compiler executable checksum: 5f02f32570d532de29ae0b402446343a

stack-overflow1-test.c:2: 警告:返回类型默认为‘int’

stack-overflow1-test.c: 在函数‘greeting’中:

stack-overflow1-test.c:4: 警告:隐式声明函数‘strcpy’

stack-overflow1-test.c:4: 警告:隐式声明与内建函数‘strcpy’不兼容

stack-overflow1-test.c: 在文件层:

stack-overflow1-test.c:8: 警告:返回类型默认为‘int’

stack-overflow1-test.c: 在函数‘main’中:

stack-overflow1-test.c:11: 警告:在有返回值的函数中,控制流程到达函数尾

stack-overflow1-test.c: 在函数‘greeting’中:

stack-overflow1-test.c:6: 警告:在有返回值的函数中,控制流程到达函数尾

COLLECT_GCC_OPTIONS='-v' '-Wall' '-o' 'stack-overflow1-test' '-mtune=generic' '-march=i686'

as -V -Qy -o /tmp/ccv9qpvk.o /tmp/ccT7rHyO.s

GNU assembler version 2.20.51.0.2 (i686-redhat-linux) using BFD version version 2.20.51.0.2-5.36.el6 20100205

***************省略部分输出*************

可以看到, -Wall  选项(参考前文解释)显示所有的警告和错误信息,对于增加程序的可移植性非常有帮助,例如它指出在源码的二行,greeting 自定义函数没有定义返回类型,将采用默认返回类型:int

另外,在 greeting 函数中,调用 strcpy 函数前未声明和定义(在程序中调用 strcpy 函数需要包含系统头文件 string.h)

同样,我们没有为 main 函数指定返回类型

在上面的例子中,虽然每个警告都非致命的语法或词法错误,但是 -Wall 选项确实可以“强制”培养程序员的良好编程习惯

言归正传,检查生成的二进制可执行文件:

[root@centos6-5vm /]# readelf -lW stack-overflow1-test | grep GNU_STACK

GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

其中没有 E 标记,这说明该程序即便存在溢出漏洞,但是由于 GCC 的堆栈段不可执行保护机制,该漏洞也没有太大被利用的可能性

我们“故意”关掉 GCC 的堆栈段不可执行保护机制:指定 -z execstack  选项,再次编译源文件:

[root@centos6-5vm /]# gcc -v -Wall -z execstack -o stack-overflow1-test stack-overflow1-test.c

[root@centos6-5vm /]# readelf -lW stack-overflow1-test | grep GNU_STACK

GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4

这次的输出中,带有 E 标记,说明堆栈段是可执行的,

再次强调,在 CentOS6.5 中,要真正能利用这个程序测试你编写的 shellcode ,需要执行下面操作:

[root@localhost 桌面]# sysctl -w kernel.randomize_va_space=0

kernel.randomize_va_space = 0

[root@localhost 桌面]# sysctl -w kernel.exec-shield=0

kernel.exec-shield = 0

[root@localhost 桌面]# gcc -v -fno-stack-protector -z execstack -g -o stack-overflow1-test stack-overflow1-test.c

ubuntu –buffer-overflow

Ubuntu下面的GCC默认开启了Stack Smashing Protector,如果想在这个系统中学习缓冲区溢出的原理,在编译时要加上fno-stack-protector选项,否则运行时会出现*** stack smashing detected ***: xxx terminated,而不是期望的Segmentation fault。同时还需要加上允许栈执行的选项。

gcc -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -ggdb -o xxx xxx.c

execstack