实际中的例子有助于辨识清楚前面所定义的术语。在这一节里将分析曾多次提到的缓冲区溢出攻击模式。当然,根据系统上
下文环境的不同,缓冲区溢出的危险性也各不相同。偶尔的缓冲区溢出只是技术层次上的一个bug(但确实是个问题),并不会导致不可接受的危险;但是,很多
时候,缓冲区溢出是很重要的现象,本书将利用整整一章(第7章)的篇幅讨论它。现在,先利用真实的例子来显示某个攻击模式是如何转变成一次攻击的。在这一
部分将会展示一些代码,而读者则可以扮演成一名攻击者,利用本书所提供的代码,然后编译这些代码,并运行编译后的代码,看看会发生什么。正如将要看到的,
这个例子十分有趣。
在2001年2月,Microsoft公司给它最新的C++编译器(Visual
C++.NET或者可以叫做Visual C++ 7)增添了一个安全特性 (Chris
Ren,一名数字研究者,发现了这个脆弱点,并对本节内容提供了很大的帮助)。要能实施这次攻击,首先需要找到带有这个脆弱点的编译器版本。
这个新的安全特性是希望能够防止某些形式的缓冲区溢出攻击,从而自动保护某些易受攻击的源代码。由这个新特性提供的
保护功能,允许开发者能像平常那样不停地使用易受攻击的字符串函数,例如strcpy()(这经常被看作出现bug的标志),但却会受到保护以防止栈溢
出。这个新的特性是基于Crispin
Cowan的叫做“栈保护(StackGuard)”的创造性思想,当在构建标准本地代码时可以使用这个特性(不是.NET的中间件语言)[Cowan等
人,1998]。注意到这个新特性是利用受到保护的编译器,从而保护编译的程序。换句话说,利用这个特性应该能帮助开发者开发更安全的软件。然而在其初始
的版本中,所谓的Microsoft安全特性反而导致了对安全的错误理解,因为它很容易被攻击!Microsoft在面对安全漏洞时,总是显出已经找到了
有效的方法来保证系统安全的样子(过去他们一直都是这样做的)。
栈保护并不是一个很好的可以阻止缓冲区溢出攻击的方法。事实上,栈保护是在有相当严格的环境限制中才开发出来的。而Cowan仅仅是对gcc代码生成器进行了一些修补,这样就不需要全新的编译器,或者是对gcc编译器从头到尾进行重建。
Microsoft的安全特性是在面对潜在的攻击时,利用叫做“安全错误处理程序”的功能调用以防止代码受到攻击。
而某次攻击能很快被确认的事实则显示了攻击模式概念的强大作用。由于实施了安全错误处理程序,Microsoft的安全特性本身也就成了被攻击的对象。
哈!是不是比较可笑?攻击者可以发起有特殊目的的攻击,去攻击所谓的保护程序,从而可以很直接地击溃保护机制。当然这样的攻击方法属于一种新的攻击模式。
还有很多种知名的不是基于栈保护的用来防止缓冲区溢出攻击的代码编译生成方法。而Microsoft选择的是不良的
并且是缺乏适应性的解决方案,这就是典型的设计层缺陷,从而导致了非常严重的潜在的对用新编译器编译的代码的攻击。换句话说,Microsoft的编译器
在某种程度上来说成了脆弱点之源。
开发者和系统架构师需要有严格的软件安全概念——
例如对源代码的复查,而不是依赖于运行时的编译器特性,期望能阻止某些类型的字符串缓冲区溢出攻击。静态分析工具,例如
Cigital的SourceScope和开放源码的ITS4,能够而且应该被用来检查在C++源码中的潜在问题,这也正是Microsoft所希望修补
的被破解的特性。事先把这些潜在的问题从代码中去除,会比在运行时尽力发现这些问题好得多7。
正如比尔?盖茨Gates在2002年1月的备忘录中提到的,Microsoft一直致力于提高软件的安全性;但是,如果其自身的安全措施仍有系统结构上的安全问题,那么Microsoft就还需要进行相当多的努力来提高其本身的软件安全性。
和Microsoft息息相关的栈保护的另一机制,是其检查机制。然而,这种检查机制可以利用很多其他的方法迂回替
代。Cigital使用的攻击Microsoft机制的方法既不新鲜,也不需要其他额外的专门技术。如果Microsoft公司的研究人员曾经逐字逐句地
分析过栈保护的周边环境,那么他们就会意识到会存在这样的攻击。
2.5.1 攻击的技术细节
在Visual C++.NET(Visual C++ 7.0)的/GS编译选项中,可以让开发者在构建应用程序时使用一个叫做“安全缓冲检查”的选项。在2001年,至少有两篇Microsoft的文章介绍过这个技术8,
一篇文章的作者是Michael Howard,另一篇文章的作者是Brandon
Bray。通过阅读有关/GS选项的文档,以及对选择了这个选项后编译器生成的二进制代码进行分析后,Cigital的研究员们发现/GS选项中的上述功
能实际上就是Win32端口的栈保护。这也已经被Immunix的研究员们独立地证明了。
对未经检测的栈缓冲区溢出,可以使得攻击者以很多的方式“劫持”程序的运行路径。一个众所周知的也是经常被使用的攻
击模式,就是在栈中用攻击者想使用的地址覆盖原先程序的返回地址,这样,当程序被攻击时,就不会跳转到执行原先设定的功能模块,攻击者可以在程序的跳转地
址处设置攻击代码,这样程序就会执行这些攻击代码。
栈保护的创造者们最先提出的思想是在功能模块的入口也就是返回地址前设定一个标志(canary),这样,可以利用
标志字段的值检查返回地址是不是被替换过。后来,他们又对其方法进行改进,利用标志字段的值和功能模块入口的返回地址进行异或(XOR)操作,防止攻击者
绕过标志字段从而改写其返回地址[Cowan等,1998]。栈保护措施被证明在运行时,在某些情况下,可以检测到某些类型的缓冲区溢出攻击并阻止这些攻
击。一个类似的工具(StachShield)则利用单独的栈结构保存程序返回地址,这也能在某些情况下消除缓冲区溢出攻击。
修改函数的返回地址并不是“劫持”程序的惟一方法。在文章phrack569中还讨论了其他可以绕过
缓冲保护的工具(例如栈保护或者StackShield)的一些可能攻击方法。下面是攻击模式的一些要点:如果在易受攻击的缓冲区之后的栈中有一指针类型
的变量,同时变量指向的区域将来会被用户数据填充的话,那么就有可能通过改写这个指针变量执行攻击。攻击者首先必须改写这些指针变量,使其指向攻击者所希
望指向的内存地址。这时则需要由攻击者提供指针变量值。攻击者所选择的理想的内存区域应该是在之后执行的程序中会调用的函数指针。文章Phrack介绍了
如何在全局偏移表(global offset
table,GOT)中找到上述的函数指针。在实际中使用这种方法,绕过栈保护的攻击实例请参见:http:
//www.securityfocus.com/archive/1/83769。
2.5.2 对Microsoft栈保护的概述
有关Microsoft的/GS的实现细节可以在如下三个CRT文件中得到,它们分别是seccinit.c、seccook.c以及secfail.c。通过检查选择了/GS选项后的编译器生成的指令代码,可以发现其他更多的信息。
在CRT_INIT的调用中首先会产生一个“security
cookie”标志。同时还有一个新的库调用_set_security_error_handler,可以用来安装用户定义的处理程序。而用户处理程序
的函数指针将会存储在全局变量user_handler中。当从其他功能模块中跳出时,编译器产生的指令代码就会跳转到在seccook.c文件中定义的
函数_security_check_cookie。如果securityc
cookie文件被修改,则会调用在secfail.c文件中定义的_security_error_handler。
_security_error_handler中的代码首先检查是否已经安装了用户定义的处理程序。如果安装了,则会调用用户定义的处理程序,否则显示
系统默认的“Buffer Overrun Detected”消息,同时程序结束。
在执行中,至少会有一个问题。在Windows中,不存在像GOT这样可写的变量区,所以即使给定了前面所述的栈的
大体情况,攻击者也不是很容易就能找到可用的函数指针。然而,由于能够得到user_handler变量,因此攻击者无需花费更多时间就能找到一个很好的
目标。
2.5.3 绕过Microsoft的安全特性
先看下面一段代码:
Code
1
2#include <stdio.h>
3#include <string.h>
4/**//*
5request_data, in parameter which contains user supplied encoded string like
6"host=dot.net&id=user_id&pw=user_password&cookie=da".
7user_id, out parameter which is used to copy decoded 'user_id'.
8password, out parameter which is used to copy decoded 'password'
9*/
10void decode(char *request_data, char *user_id, char *password){
11 char temp_request[64];
12 char *p_str;
13 strcpy(temp_request, request_data);
14 p_str = strtok(temp_request, "&");
15 while(p_str != NULL){
16 if (strncmp(p_str, "id=", 3) == 0){
17 strcpy(user_id, p_str + 3 );
18 }
19 else if (strncmp(p_str, "pw=", 3) == 0){
20 strcpy(password, p_str + 3);
21 }
22 p_str = strtok(NULL, "&");
23 }
24}
25/**//*
26Any combination will fail.
27*/
28int check_password(char *id, char *password){
29 return -1;
30}
31/**//*
32We use argv[1] to provide request string.
33*/
34int main(int argc, char ** argv)
35{
36 char user_id[32];
37 char password[32];
38 user_id[0] = '\0';
39 password[0] = '\0';
40 if ( argc < 2 ) {
41 printf("Usage: victim request.\n");
42 return 0;
43 }
44 decode( argv[1], user_id, password);
45 if ( check_password(user_id, password) > 0 ){
46 //Dead code.
47 printf("Welcome!\n");
48 }
49 else{
50 printf("Invalid password, user:%s password:%s.\n", user_id, password);
51 }
52 return 0;
53} 函数decode包含了没有经过检查的缓冲区temp_request,通过使其溢出,则可以修改参数user_id和password。
假如此程序由/GS选项编译的话,就不太可能利用溢出函数decode的返回地址,从而修改程序的执行路径。然而,
却有可能使函数decode的参数user_id溢出,从而使其指向上面所提到的user_handler变量。所以,当调用strcpy
(user_id,
p_str+3);时,可以给user_handler分配一事先设置好的变量值。例如,使其指向内存区中的printf(“welcome!\
n”);,当缓冲区溢出被检测到时,则执行用户安装的安全处理程序,从而就会执行printf
(“welcome!\n”);。所使用的攻击字符串则可以是如下格式:
id=[location to jump to]&pw=[any]AAAAAAA…AAA[address of user_handler]
在面对已经编译了的并受到“保护”的二进制代码时,只要有一点逆向工程的知识,那么想发现user_handler在内存中的地址实在是一件微不足道的事情。所导致的结果就是:原先想象中可以得到保护的程序,对上面所述的攻击实在是脆弱不堪。
2.5.4 解决方案
有很多可选解决方案,可以防止如上所述的攻击模式的攻击。最好的解决方法是让开发者选择类型安全的语言,例如Java或者C#;接下来较好的解决方法,是在动态编译时检查运行时会调用的字符串函数(虽然会影响性能)。对于有限制的软件来说,这些解决方案不一定都有用。
修改/GS也是可行的方法。下面所述的每一种修改,其目标都是希望得到栈中更好的数据完整性。
(1) 更加仔细地检查标志字段,以确保栈中变量的完整性。如果在栈中有某个变量处在缓冲区之后,那么在使用这个变量之前必须对其进行完整性检查。也可以对变量进行数据依赖性分析从而控制检查的频率。
(2)
通过重新调整栈的结构,以确保栈中变量的完整性。只要有可能,非缓冲的局部变量都应该放在缓冲区变量之前。更进一步讲,由于函数的参数被安排在了局部缓冲
区的后面(假如有的话),它们也应该同样地放在缓冲区之前。在函数的入口地址处,局部缓冲之前,应该保留额外的栈空间,这样才能保存所有参数的副本。在函
数体中每次用到这些参数都应该用其最新的副本代替。这样的解决方法至少已经在IBM的一项工程10中得到应用。
(3)
通过提供可控写(managed-writable)机制,以确保全局变量的完整性。经常的,一些重要的全局变量会由于程序错误或是滥用全局变量使其被破
坏。利用可控写机制可以把这些全局变量存放在只读区域中。当有必要修改这些变量时,对只读区域的内存访问权限可以转变成可写。而当修改完成后,访问权限又
转变成只读。利用这样的机制,当对受保护的变量有非预期的“写”时,会违背存储访问权限。对那些在进程生命周期中只被赋值一次或者两次的变量,对这些变量
进行写控制的耗费是可以忽略不计的。
Microsoft在其后续的编译器版本中已经多多少少地采用了这些思想。
2.5.5 攻击回顾
到目前为止,对于这有趣的攻击应该很清楚了:Microsoft为了阻止发生标准的攻击,本想利用新的安全特性进行
保护,结果却在Microsoft编译器中埋下了安全脆弱点的种子。最有意思的是,可以利用和以前一样的攻击模式,来攻击已经受到所谓安全保护的编译器。
问题出来了:在调用新的安全特性的时候,原先不容易受到攻击的字符串函数变得更加容易受到攻击。这对软件安全来说是个噩梦,对软件攻击来说则是个喜讯11。
在这一缺陷公布的两年后,至少有两个破解组织发现了利用/GS进行两段跳板式的攻击。正如前面提到的,所谓的安全机制现在成了攻击者的立足点。