零. 课程要点:
- 缓冲区溢出
- 缓冲区溢出攻击
- 缓冲区溢出防范
学完了数据的内存结构,以及函数调用的底层过程,我们就来看一下在各种操作系统和应用软件中广泛存在的漏洞:缓冲区溢出。
一. 缓冲区溢出
首先,缓冲区是什么?
缓冲区是一块连续的计算机内存区域,可以是堆栈(自动变量)、堆(动态内存)和静态数据区(全局或静态)。在C语言中,通常使用字符数组和内存分配函数实现缓冲区。如下图所示:
那么什么是缓冲区溢出?
缓冲区溢出就是指当计算机程序向缓冲区内填充的数据位数超过了缓冲区本身的容量,溢出的数据覆盖在其它的数据或受保护空间中。
由于C语言没有数组越界检查机制,当向局部数组缓冲区里写入的数据超过为其分配的大小时,就会发生缓冲区溢出。举个例子:
double fun(int i)
{
volatile double d[1] = {3.14};
volatile long int a[2];
a[i] = 1073741824;
return d[0];
}
当调用这个函数时,我们发现结果如下:
fun(0) = 3.14
fun(1) = 3.14
fun(2) = 3.1399998664856
fun(3) = 2.00000061035156
fun(4):Segmentation fault
上面就是一个典型的对数组的越界访问,为什么会出现上面的结果?只需要看一下栈中的数据内容就能明白:
当i=0
和i=1
时,1073741824存储于正确的位置,但是当i=2
时,a[2] = 1073741824
,虽然我们认为没有定义a[2]
,但是根据前面的知识,我们知道数组元素可以使用指针来访问,因此对数组的引用没有边界约束,因此数据覆盖了d[0]
的地方,造成了错误。如果i
更大,那就有可能在受保护的地方写入了数据,造成程序崩溃甚至被劫持。
二. 缓冲区溢出攻击
造成缓冲区溢出的原因是没有对栈中作为缓冲区的数组的访问进行越界检查。缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、应用软件中广泛存在。利用缓冲区溢出攻击,可导致程序运行失败、系统关机、重新启动等后果。
在前面的学习中我们知道在函数调用过程中,会把返回地址入栈,并在函数调用结束后取出并跳转。如果利用缓冲区溢出将函数返回地址修改为指向一段精心安排的恶意代码,从而改变程序正常流向,则可达到危害系统安全的目的。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。若该程序有root或suid执行权限,则攻击者就获得一个有root权限的shell,进而可对系统进行任意操作。
举个例子:
上面的代码原意是想打印出第一个参数,但是只分配了16个字节的大小。当main函数调用outputs时,会先保存返回地址,然后保存EBP旧值,重新形成栈底。然后分配16个字节的变量,并将参数入栈,准备调用strcpy函数。
若strcpy复制了25个字符到buffer中,并将hacker首址置于结束符‘\0’前4个字节,则在执行strcpy后,hacker代码首址被置于main栈帧返回地址处,当执行outputs代码的ret指令时,便会转到hacker函数实施攻击。
即假定hacker首址为0x08048411,那么我们构造一个字符串作为参数传入:
char code[]=
"0123456789ABCDEFXXXX"
"\x11\x84\x04\x08"
"\x00";
int main(void) {
char *argv[3];
argv[0]="./test";
argv[1]=code;
argv[2]=NULL;
execve(argv[0],argv,NULL);
return 0;
}
就可以转到hacker函数实施攻击。
三. 缓冲区溢出防范
函数 | 危险性 | 解决方案 |
---|---|---|
gets | 最高 | 禁用gets(buf),改用fgets(buf, size, stdin) |
strcpy | 高 | 检查目标缓冲区大小,或改用strncpy,或动态分配目标缓冲区 |
strcat | 高 | 改用strncat |
sprintf | 高 | 改用snprintf,或使用精度说明符 |
scanf | 高 | 使用精度说明符,或自己进行解析 |
sscanf | 高 | 使用精度说明符,或自己进行解析 |
fscanf | 高 | 使用精度说明符,或自己进行解析 |
vfscanf | 高 | 使用精度说明符,或自己进行解析 |
vsprintf | 高 | 改为使用vsnprintf,或使用精度说明符 |
vscanf | 高 | 使用精度说明符,或自己进行解析 |
vsscanf | 高 | 使用精度说明符,或自己进行解析 |
streadd | 高 | 确保分配的目标参数缓冲区大小是源参数大小的四倍 |
strecpy | 高 | 确保分配的目标参数缓冲区大小是源参数大小的四倍 |
strtrns | 高 | 手工检查目标缓冲区大小是否至少与源字符串相等 |
getenv | 高 | 不可假定特殊环境变量的长度 |
realpath | 高(或稍低,实现依赖) | 分配缓冲区大小为PATH_MAX字节,并手工检查参数以确保输入参数和输出参数均不超过PATH_MAX |
syslog | 高(或稍低,实现依赖) | 将字符串输入传递给该函数之前,将所有字符串输入截成合理大小 |
getopt | 高(或稍低,实现依赖) | 将字符串输入传递给该函数之前,将所有字符串输入截成合理大小 |
getopt_long | 高(或稍低,实现依赖) | 将字符串输入传递给该函数之前,将所有字符串输入截成合理大小 |
getpass | 高(或稍低,实现依赖) | 将字符串输入传递给该函数之前,将所有字符串输入截成合理大小 |
getchar | 中 | 若在循环中使用该函数,确保检查缓冲区边界 |
fgetc | 中 | 若在循环中使用该函数,确保检查缓冲区边界 |
getc | 中 | 若在循环中使用该函数,确保检查缓冲区边界 |
read | 中 | 若在循环中使用该函数,确保检查缓冲区边界 |
bcopy | 低 | 确保目标缓冲区不小于指定长度 |
fgets | 低 | 确保目标缓冲区不小于指定长度 |
memcpy | 低 | 确保目标缓冲区不小于指定长度 |
snprintf | 低 | 确保目标缓冲区不小于指定长度 |
strccpy | 低 | 确保目标缓冲区不小于指定长度 |
strcadd | 低 | 确保目标缓冲区不小于指定长度 |
strncpy | 低 | 确保目标缓冲区不小于指定长度 |
vsnprintf | 低 | 确保目标缓冲区不小于指定长度 |