本文共 8577 字,大约阅读时间需要 28 分钟。
安全是高质量软件的重点关注方面,最让人害怕、最多被误解的就是缓冲区溢出。现在,提及缓冲区溢出足以让人们停下来,仔细听。太频繁了,技术细节丢失在抄本中,大部分人们对于这种基础的、值得关注的方面离开了。为了解决这个问题,Visual C++ .NET引入了安全检查来帮助开发者确定缓冲区溢出。
什么是缓冲区溢出?
缓冲区是一块内存,通常是数组的形式。当没有校验数组的长度时,可能会写出缓冲区的边界。如果这样的行为发生的地址比缓冲区的内存地址高,称为缓冲区溢出;类似的,如果这样的行为发生的地址比缓冲区的内存地址低,称为缓冲区下溢。缓冲区下溢的发生率明显少于缓冲区溢出,但是,正如本文的后面所描述,它确实发生过。向一个正在运行进程注入代码的缓冲区溢出被称为可以用缓冲区溢出。
一些文档化的函数,例如strcpy,gets,scanf,sprintf,strcat等,本身很容易受到缓冲区溢出的攻击,所以不推荐使用他们。一个简单的例子说明了这些函数的危险性:
assert(strlen(pStr) < _MAX_PATH)
就能够在
debug
版本下发现这个错误,但是
release
版本不行。用这些容易受到攻击的函数被认为是坏的实践。技术上来讲更不容易受到攻击的相似的函数确实存在,如
strncpy
,
strncat
和
memcpy
。这些函数的问题是开发者来验证缓冲区的长度,而不是编译器。下面的函数展示一个普遍的错误:
#define BUFLEN 16 void vulnerable2(void) { wchar_t buf[BUFLEN]; int ret; ret = MultiByteToWideChar(CP_ACP, 0, "1234567890123456789", -1, buf, sizeof(buf)); printf("%d\n", ret); }
这种情况下,字节的个数用来标示缓冲区的大小,而不是字符的个数,于是发生了缓冲区溢出。为了修正这个可攻击点,MultiByteToWideChar的最后一个参数因该使用
sizeof(buf)/sizeof(buf[0])
. vulnerable1 和vulnerable2都是很普遍的错误,并且可以很容易的预防。然而,如果由于代码Reviewer的疏忽,这些潜在的安全漏洞可能发布到产品中。这就是为什么Visual C++ .NET引入了安全检查,它可以阻止vulnerable1 和vulnerable2函数中的缓冲区溢出向容易受到攻击的应用程序注入恶意代码。
X86栈的分析
为了理解如何利用缓冲区溢出以及安全检查如何工作,必须完全理解堆栈的布局结构。在X86体系下,堆栈向下增长,意味着新创建数据的地址小于早期压入栈中元素的地址。每一个函数创建一个新的、有如下布局的栈帧,注意高内存地址在列表的顶部:
· Function parameters
· Function return address
· Frame pointer
· Exception Handler frame
· Locally declared variables and buffers
· Callee save registers 从以上布局很明显的可以看出,缓冲区溢出有可能覆盖掉比该缓冲区分配的早的变量,异常处理帧,栈帧指针,返回地址,函数参数。为了接管程序的执行,一个值必须写进数据中,该数据的值被装载进EIP寄存器中。函数的返回地址就是一个这样的数据。典型的利用缓冲区溢出是覆盖函数返回地址,让函数的返回指令将返回地址加载到EIP中。数据元素按照如下方式存入栈中。函数调用之前将函数的参数压入栈中。参数从右到左被压入栈中。CALL指令将函数的返回地址压入栈中,它存储EIP寄存器的当前值。栈帧指针是前一个EB寄存器的值,当没有发生FPO优化时,也压入栈中。因此,栈帧指针不总是在栈中。如果函数包括了try/catch 或者任何其他形式的异常处理结构,编译器将在栈中包含异常处理信息。之后,是局部声明的变量和分配的缓冲区。这些分配的顺序可能根据优化实施而作改变。最后是调用者保存的寄存器如ESI,EDI,EBX,如果他们在函数执行时被使用。
运行时检查
缓冲区溢出是c、c++程序员普遍犯的错误,也是潜在的最危险的。Visual C++ .NET提供了工具,它可以使开发者在开发阶段很容易发现这些错误并修正。Visual C++ 6.0中的/GZ开关在Visual C++ .NET中的/RTC1中获得了新生。/RTC1开关是/RTsu的别名,其中s代表堆栈检查,u代表未初始化变量检查。所有在堆栈中分配的缓冲区在边界处设置了标签。因此,缓冲区溢出、下溢可以被捕捉。尽管小的缓冲区溢出可能不会改变程序的执行,它可以改变附近的数据,而这都不会被觉察到。
运行时检查对于那些不仅想写安全的代码、而且关心编写正确代码的基本问题的开发者很有帮助。然而运行时检查仅仅工作在debug版本下,该特性从没有设计为在产品代码中可用。但是,在产品代码中进行这样的检查有很明显的价值。做这些运行时检查需要一小部分的性能损失。结果,Visual C++ .NET引入了/GS开关。
/GS开关做什么
/GS开关在缓冲区和返回地址间提供了一个“Speed bump”或cookie。如果一个溢出覆盖了返回地址,那么它也覆盖了放在缓冲区和他之间的Cookie,新的堆栈布局如下:
· Function parameters
· Function return address
· Frame pointer
· Cookie
· Exception Handler frame
· Locally declared variables and buffers
· Callee save
Cookie在以后会更详细的检查。随着这些安全检查的加入,函数的执行也改变了。首先,当一个函数执行时,第一条要执行的指令在函数的prolog中。至少,prolog为堆栈中的局部变量分配空间,例如如下指令:
sub esp,20h
这条指令为函数中的局部变量预留了32字节。当函数使用/GS开关编译时,函数prolog将预留另外的4个字节,三个如下另外的指令:
当使用/GS 编译时,安全检查也放在epilog 中:
__security_check_cookie例程是很直观的。如果cookie没有被改变,它执行RET指令并结束函数调用。否则,它调用report_failure函数,report_failure接着调用__security_error_handler(_SECERR_BUFFER_OVERRUN, NULL)
。这些函数都定义在
C
运行库(
CRT
)的源文件
seccook.c
中。
错误处理器
使这些安全检查起作用需要 CRT 的支持。当安全检查失败时,程序的控制需要交给 __security_error_handler ,以下是它的处理概要:
缺省情况下,安全检查失败的应用程序弹出显示信息为 ” Buffer overrun detected! ” 的对话框,当关闭对话框后终止应用程序。 CRT 库提供给开发者定制不同的、能够更好的处理缓冲区溢出的处理器功能。函数 __set_security_error_handler 通过在变量 user_handler 中 存储用户提供的处理器的方式来安装 handler 。以下例子说明:
有时,开发者或许想重写函数__security_error_handler,而不是用函数_set_security_error_handler来达到同样的目的。重写容易出错,主处理器非常重要,如果没有正确的实现将导致危险的结果。
Cookie的值
Cookie是一个和指针同样大小的随机数,意味着在X86体系下,cookie是4个字节长。它的值存储在CRT全局变量__security_cookie中。它的值由在CRT seccinit.c文件中的函数__security_init_cookie来初始化。Cookie的随机性来自于CPU处理器的计数器。每一个影像文件(使用/GS编译的DLL、EXE )在装载时有一个不同的cookie。
当时用/GS编译器开关编译程序时可能有两个问题。第一、不包含CRT支持的程序
将缺少随机的cookie,因为CRT初始化时调用__security_init_cookie。如果在装载时cookie没有被初始化,如果有缓冲区溢出,应用程序还是有可能被攻击。为了解决这个问题,应用程序在启动时需要显示的调用__security_init_cookie。第二、调用文档化的函数来初始化的
遗留的应用程序可能遇到不可预期的安全检查失败。例如下面的例子:
性能影响
必须平衡程序的性能和安全检查。Visual C++编译器组致力于将性能影响降低到最小。大多说情况下,性能影响不超过2%。实际上,经验显示对大多说的应用程序、包括高性能的服务器端程序来讲,性能影响是微乎其微的。
使性能不受影响的最重要的因素是只有那些容易受到攻击的函数才执行安全检查。现在,容易受到攻击的函数为在堆栈中分配缓冲区的函数。字符串缓冲区如果分配多于四个字节、缓冲区中每个元素时1到2个字节,就容易受到攻击。小缓冲区不容易受到攻击并且限制进行安全检查的函数的数量就限制了代码的增长。大部分的可执行程序因为适用/GS编译引起的代码增长时微乎其微的。
例子
因此,/GS开关并没有修正缓冲区溢出,但是它可以阻止攻击者利用缓冲区进行攻击。当时用/GS开关编译vulnerable1 、vulnerable2时,溢出就不会被利用。缓冲区溢出发生在最后一个动作的函数可以免于被攻击。因为如果溢出发生在函数执行的早期,安全检查或者没有机会执行、或者安全检查本身已被攻击,象如下例子。
例子1
例子2
例子3
近来一些很流行的攻击都是利用异常处理。其中最流行的Code Red病毒出现在2001年夏。Window XP已经创建了一个环境,在该环境下,通过异常处理进行攻击将会更难,因为异常处理函数的地址不在栈中,而且调用异常处理函数之前清除所有的寄存器的值。
例子4
容易受到攻击的代码可能受到另外的攻击如,发生在堆中的缓冲区溢出,它不能够被/GS开关检查出来。数组索引越界攻击,它对数组中的某一个位置进行存取,而不是对数组进行连续写入,这样的问题/GS开关也不能检查出来。一个未检查的数组索引可以访问内存的任意部分,而不会修改cookie的内容。另外一种未检查的索引是有符号、无符号的不匹配。如果索引是有符号整数,简单的验证索引小于数组的大小也是不够的。最后,/GS开关不能够检查缓冲区下溢。
结论
很明显,缓冲区溢出是应用程序的一个非常关键的缺陷。没有比写出紧凑、安全的代码更重要。尽管大多数观点认为,少量的缓冲区溢出很难被发现。在编写安全代码方面,/GS开关对开发者是很有帮助的。但是,它解决不了代码中的缓冲区溢出的问题。尽管安全检查在某种程度上阻止了缓冲区被利用,但程序仍然终止了,一种拒绝服务的攻击,特别是服务器端代码。使用/GS开关是一种安全的方法来减少在没有意识到的情况下,受到缓冲区溢出的攻击。
尽管存在能够标记可能受到攻击的代码的工具,例如本文所讨论的,但是他们都是由缺点的。被好的代码Review人员检查过得好的代码比什么都更可靠。Michael Howard 和David LeBlanc 的< Writing Secure Code>在编写高度安全的应用程序方面,提供了很多其他的、可以降低被攻击的方法。
转载地址:http://nlhqa.baihongyu.com/