编译型应用程序与应用程序架构攻防
孟飞阳 20120228
13、攻击编译型应用程序
13.1 缓冲区溢出漏洞
如果应用程序将用户可控制的数据复制到一个不足以容纳他们的内存缓存区,就会出现缓冲区溢出漏洞。由于目标缓冲区溢出,导致邻近的内存被用户数据覆写。攻击者可以根据漏洞的本质利用它在服务器上运行任意代码或执行其他未授权操作。多年来,缓冲区溢出漏洞一直在本地软件中普遍存在,并被视为本地软件开发者必须避免的“头号公敌”。
13.1.1 栈溢出
如果应用程序在未确定大小固定的缓冲区容量足够大之前,就使用一个无限制的复制操作(如C语言中的Strcpy)将一个大小可变的缓冲区复制到另一个大小固定的缓冲区中,往往就会造成缓冲区溢出。例如,下面的函数将字符串username复制到一个分配到栈上的大小固定的缓冲区中:
bool CheckLogin(Char* username,char* password){
char _username[32];
strcpy(_username,username);
...
}
如果字符串username超过32字符,_username缓冲区就会溢出,攻击者将覆写邻近内存中的数据。在成功利用栈缓冲区溢出漏洞的攻击中,攻击者通常能够覆写栈上已保存的返回地址。当调用CheckLogin函数时,处理器将调用函数后执行的指令地址写入栈。结束CheckLogin函数后,处理器从栈中取出这个地址,返回执行这个指令。同时,CheckLogin函数分配到栈上已保存的返回地址旁边的_username缓冲区。如果攻击者能够令_username缓冲区溢出,他就能用他选择的一个值覆写缓冲区已保存的返回地址,让处理器访问这个地址,从而执行任意代码。
13.1.2 堆溢出
从本质上讲,缓冲区溢出也是由前面描述的相同危险操作造成的,唯一不同在于这时溢出的目标缓冲区分配在堆上,而不是在栈上:
bool CheckLogin(char* username,char* password){
char* _username = (char*) malloc(32);
strcpy(_username,username);
...
通常,在堆缓冲区溢出中,目标缓冲区旁不是已保存的返回地址,而是其他以堆控制结构分隔的堆内存块。堆以一个双向连接表的形式执行:在内存中,每个块的前面是一个控制结构,其中包含块的大小、一个指向堆上前一个块的指针以及一个指向堆上后一个块的指针。当堆缓冲区溢出时,邻近的堆块的控制结构被用户控制的数据覆写。
与栈溢出漏洞相比,利用这种漏洞实施攻击更要困难一些;但是,一种常见的利用方法是在被覆写的堆控制结构中写入专门设计的值,以在将来某个时间覆写任何一个关键的指针。控制结构已被覆写的堆块从内存中释放后,堆管理器需要更新堆块的连接表。要完成这项任务,它需要更新后一个堆块的反向链接指针,并更新前一个堆块的正向链接指针,以便链接表中的这两个指针指向彼此。为此,堆管理器使用被覆写的控制结构中的值。具体来说,为更新后的一个块的反向链接指针,堆管理器废弃被覆写的控制结构中的正向链接指针,并在这个地址的结构中写入被覆写的控制结构中的反向链接指针的值。换句话说,就是一个用户控制的地址中写入一个用户控制的值。如果攻击者精心设计了他得溢出数据,他就能用它选择的值覆写内存中的任何指针,其目的是控制指针的执行路径,从而执行任意代码。通常,指针覆写的主要目标是随后被应用程序调用的函数指针的值,或者是在下次出现异常时被调用的异常处理器的地址。
最新的编译器与操作系统已经采取了各种措施对软件进行保护,防止编程错误导致缓冲区溢出。这表示,如今现实世界中的溢出漏洞往往比这里描述的示例更难以利用。
13.1.3 “一位偏移”漏洞
如果编程错误使得攻击者可以在一个被分配的缓冲区之后写入一个字节(或少数几个字节),就会发生一种特殊的溢出漏洞。
一些语言(如C)并不单独记录一个字符串的长度,字符串结束部分用一个空字节表示(也就是说,用零的ASCII字节编码表示)。如果一个字符串“丢失”了它的空终止符,它的长度就会增加,并一直到内存的下一个字节(它碰巧为零)结束。这种无意的结果经常会再应用程序中造成反常行为与漏洞。
我们曾在一个硬件设备的Web应用程序中发现这种漏洞。该应用程序包含一个页面,它接受POST请求的任意参数,并返回HTML表单,其中以隐藏字段的形式包含那些参数的名称与参数值。例如:
POST /formRelay.cgi HTTP/1.0
Content-Length:3
a = b
HTTP/1.1 200 OK
Date:THU,02 NOV 2006 14:53:13 GMT
Content-Type :text/html
Content-Length:278
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=iso-8859-1">
</head>
<form name="FORM_RELAY" action="page_cgi" method="POST">
<input type="hidden" name="a" value="b">
</form>
<body onLoad="document.FORM_RELAY.submit();">
</body>
</html>
因为某种原因,真个应用程序都需要使用这个页面处理各种用户输入,其中许多为敏感数据。然而,如果用户提交的数据等于或超过4096B,那么返回的表单中还包括在向页面提出的前一个请求中提交的参数,即使这些参数由另外一名用户提交。
确定这种漏洞后,我们就可以继续向这个易受攻击的页面提交超长数据,解析收到的响应,记录其他用户提交给页面的每一个数据,包括登陆证书和其他敏感信息。
造成这种漏洞的根本原因是,在4096B的内存块中,用户提交的数据被保存为以空字节终止的字符串。这些数据被复制到一个检验操作中,因此不会直接造成溢出。然而,如果提交的是超长得输入,复制操作就会导致空终止符“丢失”,因而字符串会“流入”到内存邻近的数据中。因此,当应用程序解析请求参数时,它会一直解析到下一个空字节为止,因此就会解析出其他用户提交的参数。
13.1.4 查找缓冲区溢出漏洞
向一个确定目标发送超长的字符串并监控反常结果是查找缓冲区溢出漏洞的基本方法。有些时候,一些细微的漏洞只有通过发送一个特殊长度或者在较小的长度范围内的超长字符串才能检测出来。但是,许多时候,只需向应用程序发送一个超出其预计长度的字符串,就可以探查出漏洞。
程序员常常使用十进制或十六进制的约整数(如32、100、1024、4096等)来创建固定大小的缓冲区。在应用程序中探查明显漏洞的一个简单方法就是,向确定的每一个目标数据发送超长字符串,然后监控服务器对反常输入的响应。
渗透测试步骤:
(1)向每一个目标数据提交一系列稍大于常用缓冲区大小的字符串。如:1100、4200、33000
(2)一次针对一个数据实施攻击,最大程度的覆盖应用程序中的所有代码路径。
(3)可以使用Burp Intruder中的字符块有效载荷来源自动生成各种大小的有效载荷。
(4)监控应用程序的响应,确定所有反常现象。无法控制的溢出几乎可以肯定会在应用程序中引起异常。在远程进程中探测何时出现这种异常相当困难,需要寻找的反常现象包括以下几项。
HTTP500状态码或错误消息,这时其他畸形(而非超长)输入不会产生相同的结果。
内从详细的消息,表示某个本地代码组件发生故障。
服务器收到一个局部或畸形响应
服务器的TCP链接未返回响应,突然关闭。
整个Web应用程序停止响应。
(5)注意,如果一个堆溢出被触发,这可能会在将来而非立即导致系统崩溃。因此,必须进行实验,确定一种或几种造成堆“腐化”的测试字符串。
(6)“一位偏移”漏洞可能不会造车系统崩溃,但可能会导致反常行为,如应用程序返回出人意外的数据。
有些时候,测试字符串可能会被应用程序自身或其他组件(如Web服务器)实施的输入确认检查所阻止。在URL查询字符串中提交超长数据时通常会出现这种情况,应用程序会再针对每个测试字符串的响应中以“URL过长”之类的常规消息反映这一点。在这种情况下,应当进行实验,确定URL允许的最大长度(一般约为2000个字符),并调整缓冲区大小,以使测试字符串符合这个要求。但是,即使实施了常规过滤,溢出可能依然存在;因为长度足够短、能够避开这种过滤的字符串也可能触发溢出。
其他情况下,过滤机制可能会限制一个特定参数中提交的数据类型或字符范围。例如,当将提交的用户名传送给一个包含溢出漏洞的功能时,应用程序会确认该用户名是否包含字母数字字符。为实现测试效率最大化,渗透测试员应当设法确保每个测试字符串仅包含相关参数允许的字符。满足这种要求的一个有效方法是,截获一个包含应用程序所接受的数据的正常请求,然后使用其中已经包含的相同类型的字符,创建一个可能通过任何基于内容的过滤的长字符串,在使用这个字符串轮流测试每一个目标参数。即使确信应用程序中存在缓冲区溢出漏洞,但是,要远程利用它执行任意代码仍然及其困难。
13.2 整数漏洞
如果应用程序在执行某种缓冲区操作前对一个长度值运用某种算法,但却没有考虑到编译器与处理器整数计算方面的一些特点,往往就会出现与整数有关的漏洞。有两种类型的漏洞最值得关注:整数溢出与符号错误。
13.2.1 整数溢出
当对一个整数值进行操作时,如果整数大于它的最大可能值或小于它的最小可能值,就会造成整数溢出漏洞。这时,数字就会“回绕”,使得一个非常大的数字变得非常小,或者与之相反。下面以前面堆溢出漏洞的“修复”代码为例:
bool CheckLogin(char* username,char* password){
unsigned short len=strlen(username) + 1;
char* _username = (char*)malloc(len);
strcpy(_username,username);
..
在这段代码中,应用程序求出用户提交的用户名的长度,增加一个长度安置字符串最后的空字节,在给他分配一个相应长度的缓冲区,然后将用户名复制到这个缓冲区内。如果使用正常长度的输入,这段代码就能够正常运行。但是,如果用户提交一个65535个字符的用户名,就会造成整数溢出。一个长度较短的整数包含16位,它足以保存0~65535。如果提交一个长度位65535的字符串,程序会在这个字符串后面增加一个长度,使得这个值“回绕”而变成0。于是应用程序为他分配一个长度为0的缓冲区,把用户名复制到它里面因而造成堆语出。这样,即使程序员试图确保目标缓冲区足够大,攻击者仍然能够制造溢出。
13.2.2 符号溢出
如果应用程序使用有符号和无符号的整数来表示缓冲区的长度,并且在某个地方混淆这两个整数,或者将一个有符号的的值与无符号的值进行直接比较,或者向一个仅接受无符号的值得函数参数提交有符号的值,都会出现符号错误。在上述两种情况下,有符号的值都会被当做其对应的无符号的值处理,也就是说,一个负数变成一个大整数。下面以前面栈溢出漏洞的修复“代码”为例:
bool CheckLogin(char* username,int len,char* password){
char _username[32]="";
if(len < 32)
strcpy(_username,username,len);
...
这这段代码中,函数以用户提交的用户名和一个表示其长度的有符号整数为参数。程序员在栈上建立一个固定大小的缓冲区,检查用户名的长度是否小于缓冲区的大小,如果是这样,就执行计数缓冲区复制,确保缓冲区不会溢出。
如果len参数为负数,这段代码就能够正常运行。然而,如果攻击者能够向函数提交一个负值,那么程序员的保护性检查就会失效。仍然可以成功将它与32进行比较,因为编译器会把这两个数字当做有符号的整数处理。因此,这个负值被提交给strncpy函数,成为它的计数函数。因为scrncpy仅接受无符号的整数为参数,所以编译器将len值隐含的转换成这种类型;因而负值被当做一个大得整数处理。如果用户提交的用户名字符串长度大于32B,那么缓冲区就会溢出;这种情况和标准栈溢出类似。
通常,实施这种攻击必须满足一个前提,即长度参数由攻击者直接控制。例如,它由客户端JavaScript计算,并在请求中将它所属的字符串一起提交。但是,如果整数变量足够小(例如,非常短)且程序在服务器端计算它的长度,那么攻击者仍然可以通过向应用程序提交一个超长的字符串,借由整数溢出引入一个负值。
13.2.3 查找整数漏洞
自然的,任何时候,只要客户向服务器提交整数值,我们就可以在这些位置探查整数漏洞。通常这种行为发生在以下两种不同的情况下。
(1)应用程序通过查询字符串参数、cookie或消息主体,以正常形式提交整数值。这些数字一般使用标准的ASCII字符,以十进制表示。这时,表示一个同样被提交的字符串长度的字段使我们测试的主要目标。
(2)另外,应用程序可能提交嵌入到二进制数据对象中的整数值。这些数据可能源自一个客户端组件,如ActiveX控件;或者通过客户在隐藏表单字段或cookie中传送。在这种情况下,与长度有关的整数漏洞更难以发现。他们一般以十六进制的形式表示,通常出现在与其关联的字符串或缓冲区之前。请注意,上述二进制数据可能会通过Base64或类似的方案编码,以便于通过HTTP传送。
渗透测试步骤:
(1)确定测试目标后,需要提交适当的有效载荷,以触发任何漏洞。轮流向每一个目标数据发送一系列不同的值,分别表示不同有符号与无符号整数值的边界情况。例如:
Ox7f与Ox80(127与128)
Oxff与Ox100(255与256)
Ox7ffff与Ox8000(32767与32768)
Oxffff与Ox10000(65535与65536)
Ox7fffffff与Ox80000000(2147483647与2147483648)
Oxffffffff与Ox0(4294967295与0)
(2)如果被修改的数据以十六进制表示,应该发送每个测试字符串的little-endian与big-endian版本,例如,ff7f与7fff。如果十六进制数字以ASCII形式提交,应该使用应用程序自身使用的字母字符,确保这些字符被正确编码
(3)与上述查找缓冲区溢出漏洞时一样,应该监控应用程序响应中出现的反常事件。
13.3 格式化字符串漏洞
如果用户可控制的输入被当做格式化字符串参数提交给一个可接受可能被滥用的格式说明符的函数(如C语言中的printf系列函数),就会产生格式化字符串漏洞。这些函数接受的参数数量不定,其中可能包含不同的数据类型,如数字和字符串。提交给函数的格式化字符串中包含的说明符告诉函数:变量参数中应包含何种数据,以及这些数据以什么格式表示。
例如,下面的代码输出一条包含以十进制表示的count变量值得消息:
printf("The value of count is %d",count.);
最危险的格式说明符为%n。这个说明符不会导致什么数据被打印。相反,它使得已经输出的字节数量被写入到以相关变量参数提交给函数的指针地址中。例如:
int count=43;
int written=0;
printf("The value of count is %d%n.\n",count,&written.);
printf("%d bytes were printed.\n",written);
它输出:
The value of count is 43.
24 bytes were printed.
如果格式化字符串中的说明符比提交给函数的变量参数多,而函数又无法探查到这一点,那么它就会继续处理调用栈中的参数。
如果攻击者能够控制提交给printf之类函数的全部或部分格式化字符串,他就可以利用上述行为覆写进程内存的重要部分,并最终执行任意代码。由于攻击者控制着格式化字符串,所以它能够控制函数输出的字节数量以及栈上被输入的字节数量覆写的指针。这样,攻击者就能够覆写一个已保存的返回地址或者一个指向异常处理器的指针,进而控制代码执行,就向在栈溢出中一样。
13.3.1 查找格式化字符串漏洞
在远程应用程序中探查格式化字符串漏洞的最有效方法是,提交包含各种格式说明符的数据,并监控应用程序的任何反常行为。与不受控制地触发缓冲区溢出漏洞可能造成的后果一样,在一个易受攻击的应用程序探查格式化字符串漏洞可能会导致系统崩溃。
渗透测试步骤:
(1)轮流向每个目标参数提交包含大量格式化说明符%n与%s的字符串:
%n%n%n%n%n%n%n%n%n%n%n%n%n%n%n%n
%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s
注意,基于安全考虑,一些格式化字符串操作可能会忽略%n说明符。相反,提交%s说明符将会使函数废弃栈上的每一个参数,如果应用程序易于受到攻击,就可能会导致非法访问。
Windows FormatMessage函数以一种不同的方式使用printf系列函数中的说明符。为测试调用这个函数是否易于受到攻击,应该使用以下字符串:
%1!n!%2!n!%3!n!%4!n!%5!n!%6!n!%7!n!%8!n!%9!n!%10!n! etc...
%1!s!%2!s!%3!s!%4!s!%5!s!%6!s!%7!s!%8!s!%9!s!%10!s! etc...
记得将%字符URL编码成%25
与上述查找缓冲区溢出漏洞时一样,应该监控应用程序响应中出现的反常事件。
14、攻击应用程序架构
当评估某个应用程序的安全状态时,Web应用程序架构经常被忽略,但实际上它是一个重要的安全领域。在常用的分层架构中,如果无法隔离不同的层次,攻击者就可以利用某个层次中的一个漏洞完全攻破其他层次,进而控制整个应用程序。
14.1.1 攻击分层架构
设计不佳的分层架构可能受到以下三种类型的攻击:
(1)可以利用不同层之间的信任关系扩大攻击范围,从一个层侵入到另一个层
(2)如果不同层之间没有完全隔离,就可以利用某一层存在缺陷直接破坏另一层实施的安全保护
(3)局部攻破一个层后,就可以直接攻击其他层的基础架构,从而将攻击扩大到其它层。
渗透测试步骤:
(1)对于在应用程序中已确定的任何漏洞,应发挥想象,考虑如何利用这个漏洞实现渗透测试目标;这是贯穿全书的主题。无数针对Web应用程序实施的成功攻击,最初都是从利用一个内部影响有限的漏洞开始的。
(2)如果能再任何应用程序组件上执行任意命令,并能够与其他主机建立网络连接,考虑向网络与操作系统层面中的应用程序其他基础架构发动直接购机,以扩大攻击范围。
14.1.2 保障分层架构的安全
如果以严谨的方式执行多层架构,该架构就可以显著提高应用程序的安全,因为它能够将一次成功攻击的影响控制在局部。在前面描述的基本LAMP配置中,所有组件都在一台计算机上运行,攻破其中一个层就可能导致整个应用程序被完全攻破。在更安全的架构中,攻击者攻破一个层,只能部分控制应用程序的数据与处理操作,因而其造成的影响有限,可能局限于被攻破的层中。
(1)尽量减少信任关系
应用程序服务器应对特殊的资源与URL路径实施基于角色的访问控制。
数据库服务器层可以为应用程序的不同用户和操作提供各种权限的账户。
所有应用程序组件可以使用拥有正常操作所需的最低权限的操作系统账户运行。
(2)隔离不同的组件
一个层不得读取或写入其他曾使用的文件
对不同基础架构组件之间的网络级访问进行过滤,仅允许需要与不同应用程序层彼此通信的服务器
(3)应用深层防御
应根据配置与漏洞补丁,把每台主机上的技术栈的各个层面进行安全强化
应对保存在任何应用程序层中的数据进行加密,以防止攻破该层的攻击者轻松获得这些数据。
14.2 共享主机与应用程序服务提供商
许多组织通过外部提供商向公众提供他们的Web应用程序,这些服务器包括组织通过其访问Web与数据库服务器的简单主机服务,以及代表组织主动维护应用程序的成熟应用程序服务器提供商。缺乏能力与资源部署自己的应用程序的小型企业常常采用这种服务,但一些知名公司有时也使用这些服务来部署特殊的应用程序。
14.2.1 虚拟主机
简单的共享主机配置中,一台Web服务器只需支付几个域名各不相同的虚拟Web站点。它通过HOST消息头达到这个目的;在HTTP1.1中,请求中必须包含该消息头。当浏览器提出一个HTTP请求时,请求中既包含一个HOST消息头,该消息头中含有相关URL中的域名;然后,请求被传送到与域名关联的IP地址中。如果解析几个域名得到相同的IP地址,在这个地址上的服务器仍然能够确定请求希望访问哪一个Web站点。
14.2.2 共享的应用程序服务
许多ASP提供现成的应用程序,可由客户修改与定制使用。对于拥有大量业务、需要部署功能强大的、能为终端用户提供基本相同功能应用程序的行业,使用这种模型可以节省大量成本。
渗透测试步骤:
(1)检查为共享环境中的客户提供的、便于他们更新和管理内容与功能的访问机制。考虑以下几个问题。
远程访问机制是否使用一个安全的协议与经过适当强化的基础架构?
客户是否能够访问他们正常情况下不能访问的文件、数据及其他资源?
客户是否能够在主机环境中获得一个交互式的shell,并执行任意命令?
(2)如果使用一个所有权应用程序,以方便客户配置和定制共享环境,考虑是否能够以这个应用程序为攻击目标,攻破该环境本身及其中运行的所有应用程序
(3)如果能够在某个应用程序中执行命令、注入SQL脚本或访问任意文件,仔细研究,看是否能够以此扩大攻击范围,攻破其他应用程序
(4)如果渗透测试员正在攻击一个使用ASP主机的应用程序,且该应用程序由许多共享与定制组件构成,确定其中的任何共享组件,如日志机制、管理功能以及数据库代码组件,常使用利用这些组件攻破应用程序的共享部分,进而攻破其他应用程序。
(5)如果所有共享环境使用一个常用的数据库,使用NGSSquirrel之类的数据库扫描工具,对数据库配置、补丁版本、表结构以及许可进行全面审查。数据库安全模型中存在的任何缺陷都可以被加以利用,将攻击范围由一个应用程序扩大到另一个应用程序。
14.2.4 保障共享环境的安全
由于使用相同工具的客户怀有恶意企图以及不知情的客户在环境中引入的漏洞,因此,共享环境给应用程序安全带来了新的威胁。为解决这种双重威胁,在设计共享环境中必须仔细处理客户访问、隔离与信任关系,并实施并不直接适用于单主机应用程序的控制。
(1)保障客户访问的安全
(2)隔离客户功能,不能信任共享环境中的客户,认为他们仅建设没有漏洞的无害功能。
(3)隔离共享应用程序中的组件。
来源:oschina
链接:https://my.oschina.net/u/2391658/blog/712852