- 缓冲区
- 缓冲区溢出
- 缓冲区溢出的 C 程序实例
- 缓冲区溢出错误的危害
- Linux IA32 的进程映像
- 缓冲区溢出的原理
缓冲区
缓冲区是一块用于存取数据的 内存,其位置和长度(大小)在编译时确定 或 在程序运行时动态分配。
缓冲区通常用于存储临时数据,例如字符串、数组或其他数据结构。
举个例子:
#include <stdio.h>
int main() {
// 静态分配整数数组,编译时确定大小
int staticIntArray[5] = {1, 2, 3, 4, 5};
printf("Static Int Array: %d\n", staticIntArray[2]);
// 动态分配整数数组,运行时确定大小
int *dynamicIntArray = (int *)malloc(8 * sizeof(int));
dynamicIntArray[0] = 10;
dynamicIntArray[1] = 20;
printf("Dynamic Int Array: %d\n", dynamicIntArray[1]);
// 记得释放动态分配的内存
free(dynamicIntArray);
return 0;
}
这个示例展示了整数数组的缓冲区。在实际应用中,缓冲区常常与输入操作、文件读写等结合使用。
缓冲区可以在栈(stack)和堆(heap)中。
缓冲区溢出
缓冲区溢出就好比一个杯子倒太多的水,溢出来,这叫溢出。
当 向缓冲区拷贝数据 时,若 数据的长度大于缓冲区的长度,则多出的数据将覆盖该缓冲区之外的(高地址)内存,从而 覆盖了邻近的内存,这就是所谓的 缓冲区溢出错误。
比如 C 语言中的字符串拷贝操作不检查字符串长度,则有可能发生缓冲区溢出错误。
缓冲区溢出的 C 程序实例
使用 gcc 编译器将文件编译并链接成一个名为 example 的可执行文件:gcc -o example ../src/example.c
这个命令会将 …/src/example.c 文件经过预处理、编译、汇编和链接的四个阶段,最终生成一个名为 example 的可执行文件,可以在终端中运行它。
在终端中运行 example 这个可执行文件:./example
程序输出如下:
address of BigBuffer=0xbffff35b
address of buf01=0xbffff37c
address of SmallBuffer=0xbffff38c
address of buf02=0xbffff39c
Original buf01='Buf01'
Original buf02='Buf02’
执行完 strcpy(SmallBuffer, BigBuffer)
后,程序输出:
buf01='Buf01’
buf02='67890123456789AB'
从 Original buf02='Buf02’
到 buf02='67890123456789AB'
,说明 buf02
的内容被覆盖了。
缓冲区溢出错误的危害
发生缓冲区溢出错误之后,
- 如果邻近的内存是空闲的(不被进程使用),则对系统的运行无影响;
- 但是,如果邻近的内存是被进程使用的数据,则可能导致进程的不正确运行;
- 特别的,如果 被覆盖的是函数的返回地址,那么攻击者通过 精心构造被拷贝的数据(即
BigBuffer
的内容),则有可能执行攻击者期望的代码。
作为对目标进程的一种攻击方式,早在 1980 年代初期就有人开始讨论缓冲区溢出攻击了。但真正付诸实践、引起广泛关注并且导致严重后果的最早事件是 1988 年的 Morris 蠕虫事件。
- Morris 蠕虫对 Unix 系统中 fingerd 的缓冲区溢出漏洞进行攻击,导致 了6000多台机器被感染,损失在$100 000(10万)至$10 000 000(1千 万)之间。
1996 年,Aleph One 在 Phrack 杂志第49期发表的论文(Smashing The Stack For Fun And Profit)详细描述了 Linux 系统中栈的结构和如何利用基于栈的缓冲区溢出。
- Aleph One 的论文是关于缓冲区溢出攻击的开山之作,作为经典论文至今仍然被众多人研读。
- Aleph One 给出了如何写执行一个 Shell 的(Exploit)代码的方法,并给这段代码赋予 Shellcode 的名称。
所谓编写 Shellcode,就是编译一段 使用系统调用 的简单的 C 程序,通过调试器抽取汇编代码,并根据需要修改这段汇编代码使之实现攻击者的目的。
Shellcode 是能实现攻击者目的,对目标(软件、硬件或网络等) 漏洞进行攻击的代码。
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\ty(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
输出如下:
由此可见:
- 可执行代码 fun1, fun2, main 存放在内存的低地址 .text 代码区,且 **先定义的函数的代码 **存放在 内存的低地址。
- 全局变量 (x, y, z) 也存放在内存的低地址,起始地址高于可执行代码的地址。初始化的 全局变量存放在较 低 的地址 .data inited,而 未初始化的 全局变量位于较 高 的地址 .bss uninited。
- 局部变量 位于 内存高地址区(
0xbfff eexx
),字符串变量放在高地址,其它变量先定义的放在低地址。 - 函数的入口参数的地址(>
0xbfff efxx
)更高,位于函数的局部变量更高的地址之上。 - main 函数从环境中获得参数,因此,环境变量位于最高的地址。
栈底(最高地址)位于 0xc000 0000
,环境变量和局部变量位于进程的栈区,函数的返回地址也位于进程的栈区。
Linux IA32 进程映像如下图:
有三种数据段:.text、.data 、.bss
- .text(文本区),任何尝试对该区的写操作会导致段错误。 文本区存放了程序的代码,包括 main 函数和其他函数。
- .data 和 .bss 都是可写的,它们保存全局变量。
- .data 段包含已初始化的全局变量
- .bss 段包含未初始化的全局变量
栈 是一个 后进先出(LIFO) 数据结构,往低地址增长,它保存本地变量、函数调用等信息。一般用 push 和 pop 对栈进行操作。老版本的 Linux 系统的进程栈底(最高地址)固定,为 0xc0000000
。 新版本的 Linux 系统采用了 栈底随机化技术,栈底(最高地址)动态变化。用以下命令 关闭栈底随机化:sudo /sbin/sysctl -w kernel.randomize_va_space=0
随着函数调用层数的增加,栈帧是一块块的向内存低地址方向延伸的;
随着进程中函数调用层数的减少,即各函数的返回,栈帧\会一块块地被遗弃而向内存的高地址方向回缩。
堆 的数据结构和栈不同,它是 先进先出(FIFO) 的数据结构,往高地址增长,主要用来保存程序信息和动态分配的变量。堆是通过 malloc 和 free 等内存操作函数分配和释放的。
函数被调用所建立的栈帧包含了下面的信息:
① 该 函数的返回地址。 IA32函数的返回地址都是存放在被调用函数的栈帧里。
② 调用函数的栈帧信息,即 栈顶和栈底(最高地址) 。
③ 为 该函数的局部变量 分配的空间。
④ 为 被调用函数的参数 分配的空间。
缓冲区溢出的原理
由于函数里局部变量的内存分配是发生在栈帧里的,所以如果在某一个函数内部定义了局部变量,则这个缓冲区变量所占用的内存空间是在该函数被调用时所建立的栈帧里。
由于对缓冲区的潜在操作(比如字串的复制)都是 从内存低地址到高地址 的,而 内存中所保存的函数返回地址往往就在该缓冲区的上方(高地址)——这是由于栈的特性决定的,这就为 覆盖函数的 返回地址 提供了条件。
当用大于目标缓冲区大小的内容来填充缓冲区时,就有可能 改写 保存在函数栈帧中的 返回地址,从而改变程序的执行流程,执行攻击者的代码。