第 8 章 32 位 Linux 系统的缓冲区溢出¶
缓冲区溢出攻击是最有效的攻击方式之一,往往被黑客利用以获得目标的控制权。虽然缓冲区溢出漏洞很久以前就被重视并加以防范,但是由于该方式的利用价值较高,一直被黑客研究利用,因此,溢出漏洞将长期存在并严重影响系统的安全。
由于目前的 Linux 系统使用了地址随机化机制以防止攻击者通过缓冲区溢出漏洞执行任意代码,为了快速观察到实验结果,需要用以下命令关闭地址随机化机制:
sudo sysctl -w kernel.randomize_va_space=0
8.1 缓冲区溢出概述¶
缓冲区是一块用于存取数据的内存,其位置和长度(大小)在编译时确定或在程序运行时动态分配。栈(stack)和堆(heap)都是缓冲区。
当向缓冲区拷贝数据时,若数据的长度大于缓冲区的长度,则多出的数据将覆盖该缓冲区之外的(高地址)内存,从而覆盖了邻近的内存,这就是所谓的缓冲区溢出错误。如果缓冲区溢出错误能被攻击者利用,则称为缓冲区溢出漏洞。
如果 C 语言中的字符串拷贝操作不检查字符串长度,则有可能发生缓冲区溢出错误。
缓冲区溢出的 C 程序实例(example.c)¶
char BigBuffer[] = "012345678901234567890123456789AB"; //32 Bytes
char buf01[16];
char SmallBuffer[16];
char buf02[16];
printf(" address of BigBuffer=%p\n", BigBuffer);
printf(" address of buf01=%p\n", buf01);
printf("address of SmallBuffer=%p\n", SmallBuffer);
printf(" address of buf02=%p\n", buf02);
strcpy(buf01, "Buf01");
strcpy(buf02, "Buf02");
printf("Original buf01='%s'\n", buf01);
printf("Original buf02='%s'\n", buf02);
strcpy(SmallBuffer, BigBuffer);
puts("After strcpy is done,");
printf("buf01='%s'\nbuf02='%s'\n", buf01, buf02);
$ gcc -o example ../src/example.c
$ ./example
address of BigBuffer=0xbffff35b
address of buf01=0xbffff37c
address of SmallBuffer=0xbffff38c
address of buf02=0xbffff39c
Original buf01='Buf01'
Original buf02='Buf02'
After strcpy is done,
buf01='Buf01'
buf02='67890123456789AB'
$
缓冲区溢出错误的危害¶
- 发生缓冲区溢出错误之后,如果邻近的内存是空闲的(不被进程使用),则对系统的运行无影响;
- 但是,如果邻近的内存是被进程使用的数据,则可能导致进程的不正确运行;
- 特别的,如果被覆盖的是函数的返回地址,那么攻击者通过精心构造被拷贝的数据(即 BigBuffer 的内容),则有可能执行期望的任何代码。
缓冲区溢出攻击的发展历史¶
作为对目标进程的一种攻击方式,早在 1980 年代初期就有人开始讨论缓冲区溢出攻击了。但真正付诸实践、引起广泛关注并且导致严重后果的最早事件是 1988 年的 Morris 蠕虫事件。
Morris 蠕虫对 Unix 系统中 fingerd 的缓冲区溢出漏洞进行攻击,导致了 6000 多台机器被感染,损失在 $100 000(10 万)至 $10 000 000(1 千万)之间。
Morris 蠕虫事件引发了工业界和学术界对缓冲区溢出漏洞的关注。
1989 年以来,有大量的研究人员对 Unix 系统下的缓冲区溢出漏洞进行研究并取得了丰富的研究成果,其中比较著名的有 Spafiord 和来自 L0pht Heavy Industries 的 Mudge。
1996 年,Aleph One 在 Phrack 杂志第 49 期发表的论文(Smashing The Stack For Fun And Profit)详细描述了 Linux 系统中栈的结构和如何利用基于栈的缓冲区溢出。
Aleph One 的论文是关于缓冲区溢出攻击的开山之作,作为经典论文至今仍然被众多人研读。Aleph One 给出了如何写执行一个 Shell 的(Exploit)代码的方法,并给这段代码赋予 Shellcode 的名称。
所谓编写 Shellcode,就是编译一段使用系统调用的简单的 C 程序,通过调试器抽取汇编代码,并根据需要修改这段汇编代码使之实现攻击者的目的。
受到 Aleph One 的启发,在 Internet 上出现了众多的关于缓冲区溢出攻击的论文,以及关于避免缓冲区溢出攻击的安全编程方法。
也有研究者分析了 Unix 类操作系统的一些安全属性,如 SUID 程序、Linux 栈结构和功能等,并研究出了一些抵抗缓冲区溢出攻击的方法,如地址随机化技术、栈不可执行技术和堆栈保护(Stack Guard)技术等。
在 1998 年之前,人们认为 Windows 系统虽然存在缓冲区溢出漏洞,但是无法利用这些漏洞执行攻击者的代码,其根本原因就在于 Windows 系统中的进程堆栈地址的不固定。然而,1998 年出现的利用动态链接库实现进程跳转的技术改变了这一观念。
进程跳转技术巧妙利用了动态链接库中的 call esp 或 jmp esp 指令,使溢出后的执行流程从动态链接库跳转到攻击者可控制的缓冲区,这样就可以执行攻击者的代码。
缓冲区溢出攻击技术已经相当成熟,是入侵(渗透)攻击的主要技术手段之一。
8.2 Linux IA32 缓冲区溢出¶
运行于 Intel 32 位 CPU(或兼容 Intel CPU,如 AMD)的 Linux 操作系统称为 Linux IA32。
32 位的 Linux 被广泛应用于桌面操作系统中。目前,常用的操作系统有 Fedora-i386 和 Ubuntu-i386,它们均基于 IA32 架构。
实验演示环境:32 位的 Ubuntu 16.04(演示略)
8.2.1 Linux IA32 的进程映像¶
为了进行缓冲区溢出攻击,必须分析目标程序的进程映像。
进程映像是指进程在内存中的分布。
可执行程序的进程映像与操作系统及版本有关,也与生成该程序的编译器有关。
进程有 4 个主要的内存区:代码区、数据区、堆栈区和环境变量区。
例程 1:mem_distribute.c
#include <stdio.h>
#include <string.h>
int fun1(int a, int b) { return a + b; }
int fun2(int a, int b) { return a * b; }
int x = 10, y, z = 20; //全局变量
int main(int argc, char *argv[]) {
char buff[64], buffer02[32]; //局部变量
int a = 5, b, c = 6; //局部变量
printf("(.text)address of\n\t fun1=%p\n\t fun2=%p\n\t main=%p\n", fun1, fun2, main);
printf("(.data inited)address of\n\t x(inited)=%p\n\t z(inited)=%p\n", &x, &z);
printf("(.bss uninited)address of\n\t y(uninit)=%p\n\n", &y);
printf("(stack)of\n\t argc=%p\n\t argv=%p\n\t argv[0]=%p\n", &argc, &argv, argv[0]);
printf("(Local variable) of\n\tbuff[64]=%p\n\tbuffer02[32]=%p\n", buff, buffer02);
printf("(Local variable) of\n\t a(inited)=%p\n\t b(uninit)=%p\n\t c(inited) =%p\n\n", &a, &b, &c);
return 0;
}
$ gcc -o mem ../src/mem_distribute.c
$ ./mem
(.text)address of
fun1=0x804846b
fun2=0x8048478
main=0x8048484
(.data inited)address of
x(inited)=0x804a020
z(inited)=0x804a024
(.bss uninited)address of
y(uninit)=0x804a02c
(stack) of
argc =0xbfffefe0
argv =0xbfffef4c
argv[0]=0xbffff267
(Local variable) of
buff[64] =0xbfffef7c
buffer02[32]=0xbfffef5c
(Local variable) of
a(inited)=0xbfffef50
b(uninit)=0xbfffef54
c(inited)=0xbfffef58
由此可见:
- 可执行代码 fun1,fun2,main 存放在内存的低地址,且按照源代码中的顺序从低地址到高地址排列(先定义的函数的代码存放在内存的低地址)。
-
全局变量(x,y,z)也存放内存的低地址,位于可执行代码之上(起始地址高于可执行代码的地址)。
初始化的全局变量存放在较低的地址,而未初始化的全局变量位于较高的地址。
-
局部变量位于内存高地址区(0xbfff efxx),字符串变量放在高地址,其它变量从低地址到高地址依次(先定义的放在低地址)存放。
-
函数的入口参数的地址(> 0xbfff efxx)更高,位于函数的局部变量更高的地址之上。
main函数从环境中获得参数,因此,环境变量位于最高的地址。
由 (3) 和 (4) 可以推断出,栈底(最高地址)位于 0xc000 0000,环境变量和局部变量位于进程的栈区。进一步的分析知道,函数的返回地址也位于进程的栈区。