-
实验指导书内容
-
缓冲区溢出的原理
缓冲区是一块连续的计算机内存区域,可保存相同数据类型的多个实例。缓冲区可以是堆栈(自动变量)、堆(动态内存)和静态数据区(全局或静态)。在C/C++语言中,通常使用字符数组和malloc/new之类内存分配函数实现缓冲区。溢出指数据被添加到分配给该缓冲区的内存块之外。缓冲区溢出是最常见的程序缺陷。
栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持,但由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此也给系统安全带来隐患。若将函数返回地址修改为指向一段精心安排的恶意代码,则可达到危害系统安全的目的。此外,堆栈的正确恢复依赖于压栈的EBP值的正确性,但EBP域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改EBP值,则程序的行为将变得非常危险。
由于C/C++语言没有数组越界检查机制,当向局部数组缓冲区里写入的数据超过为其分配的大小时,就会发生缓冲区溢出。攻击者可利用缓冲区溢出来窜改进程运行时栈,从而改变程序正常流向,轻则导致程序崩溃,重则系统特权被窃取。
若将长度为16字节的字符串赋给acArrBuf数组,则系统会从acArrBuf[0]开始向高地址填充栈空间,导致覆盖EBP值和函数返回地址。若攻击者用一个有意义的地址(否则会出现段错误)覆盖返回地址的内容,函数返回时就会去执行该地址处事先安排好的攻击代码。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。若该程序有root或suid执行权限,则攻击者就获得一个有root权限的shell,进而可对系统进行任意操作。
除通过使堆栈缓冲区溢出而更改返回地址外,还可改写局部变量(尤其函数指针)以利用缓冲区溢出缺陷。
注意,本文描述的堆栈缓冲区溢出不同于广义的“堆栈溢出(Stack OverFlow)”,后者除局部数组越界和内存覆盖外,还可能由于调用层次太多(尤其应注意递归函数)或过大的局部变量所导致。
- 缓冲区溢出的防范
防范缓冲区溢出问题的准则是:确保做边界检查(通常不必担心影响程序效率)。不要为接收数据预留相对过小的缓冲区,大的数组应通过malloc/new分配堆空间来解决;在将数据读入或复制到目标缓冲区前,检查数据长度是否超过缓冲区空间。同样,检查以确保不会将过大的数据传递给别的程序,尤其是第三方COTS(Commercial-off-the-shelf)商用软件库——不要设想关于其他人软件行为的任何事情。
若有可能,改用具备防止缓冲区溢出内置机制的高级语言(Java、C#等)。但许多语言依赖于C库,或具有关闭该保护特性的机制(为速度而牺牲安全性)。其次,可以借助某些底层系统机制或检测工具(如对C数组进行边界检查的编译器)。许多操作系统(包括Linux和Solaris)提供非可执行堆栈补丁,但该方式不适于这种情况:攻击者利用堆栈溢出使程序跳转到放置在堆上的执行代码。此外,存在一些侦测和去除缓冲区溢出漏洞的静态工具(检查代码但并不运行)和动态工具(执行代码以确定行为),甚至采用grep命令自动搜索源代码中每个有问题函数的实例。
但即使采用这些保护手段,程序员自身也可能犯其他许多错误,从而引入缺陷。例如,当使用有符号数存储缓冲区长度或某个待读取内容长度时,攻击者可将其变为负值,从而使该长度被解释为很大的正值。经验丰富的程序员还容易过于自信地"把玩"某些危险的库函数,如对其添加自己总结编写的检查,或错误地推论出使用潜在危险的函数在某些特殊情况下是"安全"的。
本节将主要讨论一些已被证明危险的C库函数。通过在C/C++程序中禁用或慎用危险的函数,可有效降低在代码中引入安全漏洞的可能性。在考虑性能和可移植性的前提下,强烈建议在开发过程中使用相应的安全函数来替代危险的库函数调用。
以下分析某些危险的库函数,较完整的列表参见表3-1。
1. gets
该函数从标准输入读入用户输入的一行文本,在遇到EOF字符或换行字符前,不会停止读入文本。即该函数不执行越界检查,故几乎总有可能使任何缓冲区溢出(应禁用)。
gcc编译器下会对gets调用发出警告(the `gets' function is dangerous and should not be used)。
2. strcpy
该函数将源字符串复制到目标缓冲区,但并未指定要复制字符的数目。若源字符串来自用户输入且未限制其长度,则可能引发危险。规避的方法如下:
1) 若知道目标缓冲区大小,则可添加明确的检查(不建议该法):
View Code
2) 改用strncpy函数:
View Code
若szSrc比szDst大,则该函数不会返回错误;当达到指定长度(dwDstSize-1)时,停止复制字符。第二句将字符串结束符放在szDst数组的末尾。
3) 在源字符串上调用strlen()来为其分配足够的堆空间:
View Code
4) 某些情况下使用strcpy不会带来潜在的安全性问题:
View Code
即使该操作造成szDst溢出,但这几个字符显然不会造成危害——除非用其它方式覆盖字符串“Hello”所在的静态存储区。
安全的字符串处理函数通常体现在如下几个方面:
•显式指明目标缓冲区大小
•动态校验
•返回码(以指明成功或失败原因)
与strcpy函数具有相同问题的还有strcat函数。
3. strncpy/strncat
该对函数是strcpy/strcat调用的“安全”版本,但仍存在一些问题:
1) strncpy和strncat要求程序员给出剩余的空间,而不是给出缓冲区的总大小。缓冲区大小一经分配就不再变化,但缓冲区中剩余的空间量会在每次添加或删除数据时发生变化。这意味着程序员需始终跟踪或重新计算剩余的空间,而这种跟踪或重新计算很容易出错。
2) 在发生溢出(和数据丢失)时,strncpy和strncat返回结果字符串的起始地址(而不是其长度)。虽然这有利于链式表达,但却无法报告缓冲区溢出。
3) 若源字符串长度至少和目标缓冲区相同,则strncpy不会使用NUL来结束字符串;这可能会在以后导致严重破坏。因此,在执行strncpy后通常需要手工终止目标字符串。
4) strncpy还可复制源字符串的一部分到目标缓冲区,要复制的字符数目通常基于源字符串的相关信息来计算。这种操作也会产生未终止字符串。
5) strncpy会在源字符串结束时使用NUL来填充整个目标缓冲区,这在源字符串较短时存在性能问题。
4. sprintf
该函数使用控制字符串来指定输出格式,该字符串通常包括"%s"(字符串输出)。若指定字符串输出的精确指定符,则可通过指定输出的最大长度来防止缓冲区溢出(如%.10s将复制不超过10个字符)。也可以使用"*"作为精确指定符(如"%.*s"),这样就可传入一个最大长度值。精确字段仅指定一个参数的最大长度,但缓冲区需要针对组合起来的数据的最大尺寸调整大小。
注意,"字段宽度"(如"%10s",无点号)仅指定最小长度——而非最大长度,从而留下缓冲区溢出隐患。
5. scanf
scanf系列函数具有一个最大宽度值,函数不能读取超过最大宽度的数据。但并非所有规范都规定了这点,也不确定是否所有实现都能正确执行这些限制。若要使用这一特性,建议在安装或初始化期间运行小测试来确保它能正确工作。
6. streadd/strecpy
这对函数可将含有不可读字符的字符串转换成可打印的表示。其原型包含在libgen.h头文件内,编译时需加-lgen [library ...]选项。
char *strecpy(char *pszOut, const char *pszIn, const char *pszExcept);
char *streadd(char *pszOut, const char *pszIn, const char *pszExcept);
strecpy将输入字符串pszIn(连同结束符)拷贝到输出字符串pszOut中,并将非图形字符展开为C语言中相应的转义字符序列(如Control-A转为“\001”)。参数pszOut指向的缓冲区大小必须足够容纳结果字符串;输出缓冲区大小应为输入缓冲区大小的四倍(单个字符可能转换为\abc共四个字符)。出现在参数pszExcept字符串内的字符不被展开。该参数可设为空串,表示扩展所有非图形字符。strecpy函数返回指向pszOut字符串的指针。
streadd函数与strecpy相同,只不过返回指向pszOut字符串结束符的指针。
考虑以下代码:
View Code
打印输出\t\n,而不是所有空白。
-
strtrns
该函数将pszStr字符串中的字符转换后复制到结果缓冲区pszResult。其原型包含在libgen.h头文件内:
char * strtrns(const char *pszStr, const char *pszOld, const char *pszNew, char *pszResult);
出现在pszOld字符串中的字符被pszNew字符串中相同位置的字符替换。函数返回新的结果字符串。
如下示例将小写字符转换成大写字符:
View Code
以上代码使用malloc分配足够空间来复制argv[1],因此不会引起缓冲区溢出。
8. realpath
该函数在libc 4.5.21及以后版本中提供,使用时需要limits.h和stdlib.h头文件。其原型为:
char *realpath(const char *pszPath, char *pszResolvedPath);
该函数展开pszPath字符串中的所有符号链接,并解析pszPath中所引用的/./、/../和'/'字符(相对路径),最终生成规范化的绝对路径名。该路径名作为带结束符的字符串存入pszResolvedPath指向的缓冲区,长度最大为PATH_MAX字节。结果路径中不含符号链接、/./或/../。
若pszResolvedPath为空指针,则realpath函数使用malloc来分配PATH_MAX字节的缓冲区以存储解析后的路径名,并返回指向该缓冲区的指针。调用者应使用free函数去释放该该缓冲区。
若执行成功,realpath函数返回指向pszResolvedPath(规范化绝对路径)的指针;否则返回空指针并设置errno以指示该错误,此时pszResolvedPath的内容未定义。
调用者需要确保结果缓冲区足够大(但不应超过PATH_MAX),以处理任何大小的路径。此外,不可能为输出缓冲区确定合适的长度,因此POSIX.1-2001规定,PATH_MAX字节的缓冲区足够,但PATH_MAX不必定义为常量,且可以通过pathconf函数获得。然而,pathconf输出的结果可能超大,以致不适合动态分配内存;另一方面,pathconf函数可返回-1表明结果路径名超出PATH_MAX限制。pszResolvedPath为空指针的特性被POSIX.1-2008标准化,以避免输出缓冲区长度难以静态确定的缺陷。
应禁用或慎用的库函数如下表所示:
表3-1
函数
危险性
解决方案
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
低
确保目标缓冲区不小于指定长度
标签:函数,char,字符串,缓冲区,长度,溢出 From: https://www.cnblogs.com/TonySSS/p/16977653.html