编译型应用程序与应用程序架构攻防html
孟飞阳 20120228程序员
若是应用程序将用户可控制的数据复制到一个不足以容纳他们的内存缓存区,就会出现缓冲区溢出漏洞。因为目标缓冲区溢出,致使邻近的内存被用户数据覆写。攻击者能够根据漏洞的本质利用它在服务器上运行任意代码或执行其余未受权操做。多年来,缓冲区溢出漏洞一直在本地软件中广泛存在,并被视为本地软件开发者必须避免的“头号公敌”。算法
若是应用程序在未肯定大小固定的缓冲区容量足够大以前,就使用一个无限制的复制操做(如C语言中的Strcpy)将一个大小可变的缓冲区复制到另外一个大小固定的缓冲区中,每每就会形成缓冲区溢出。例如,下面的函数将字符串username复制到一个分配到栈上的大小固定的缓冲区中:shell
bool CheckLogin(Char* username,char* password){ char _username[32]; strcpy(_username,username); ... }
若是字符串username超过32字符,_username缓冲区就会溢出,攻击者将覆写邻近内存中的数据。在成功利用栈缓冲区溢出漏洞的攻击中,攻击者一般可以覆写栈上已保存的返回地址。当调用CheckLogin函数时,处理器将调用函数后执行的指令地址写入栈。结束CheckLogin函数后,处理器从栈中取出这个地址,返回执行这个指令。同时,CheckLogin函数分配到栈上已保存的返回地址旁边的_username缓冲区。若是攻击者可以令_username缓冲区溢出,他就能用他选择的一个值覆写缓冲区已保存的返回地址,让处理器访问这个地址,从而执行任意代码。数据库
从本质上讲,缓冲区溢出也是由前面描述的相同危险操做形成的,惟一不一样在于这时溢出的目标缓冲区分配在堆上,而不是在栈上:编程
bool CheckLogin(char* username,char* password){ char* _username = (char*) malloc(32); strcpy(_username,username); ...
一般,在堆缓冲区溢出中,目标缓冲区旁不是已保存的返回地址,而是其余以堆控制结构分隔的堆内存块。堆以一个双向链接表的形式执行:在内存中,每一个块的前面是一个控制结构,其中包含块的大小、一个指向堆上前一个块的指针以及一个指向堆上后一个块的指针。当堆缓冲区溢出时,邻近的堆块的控制结构被用户控制的数据覆写。浏览器
与栈溢出漏洞相比,利用这种漏洞实施攻击更要困难一些;可是,一种常见的利用方法是在被覆写的堆控制结构中写入专门设计的值,以在未来某个时间覆写任何一个关键的指针。控制结构已被覆写的堆块从内存中释放后,堆管理器须要更新堆块的链接表。要完成这项任务,它须要更新后一个堆块的反向连接指针,并更新前一个堆块的正向连接指针,以便连接表中的这两个指针指向彼此。为此,堆管理器使用被覆写的控制结构中的值。具体来讲,为更新后的一个块的反向连接指针,堆管理器废弃被覆写的控制结构中的正向连接指针,并在这个地址的结构中写入被覆写的控制结构中的反向连接指针的值。换句话说,就是一个用户控制的地址中写入一个用户控制的值。若是攻击者精心设计了他得溢出数据,他就能用它选择的值覆写内存中的任何指针,其目的是控制指针的执行路径,从而执行任意代码。一般,指针覆写的主要目标是随后被应用程序调用的函数指针的值,或者是在下次出现异常时被调用的异常处理器的地址。缓存
最新的编译器与操做系统已经采起了各类措施对软件进行保护,防止编程错误致使缓冲区溢出。这表示,现在现实世界中的溢出漏洞每每比这里描述的示例更难以利用。安全
若是编程错误使得攻击者能够在一个被分配的缓冲区以后写入一个字节(或少数几个字节),就会发生一种特殊的溢出漏洞。服务器
一些语言(如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的内存块中,用户提交的数据被保存为以空字节终止的字符串。这些数据被复制到一个检验操做中,所以不会直接形成溢出。然而,若是提交的是超长得输入,复制操做就会致使空终止符“丢失”,于是字符串会“流入”到内存邻近的数据中。所以,当应用程序解析请求参数时,它会一直解析到下一个空字节为止,所以就会解析出其余用户提交的参数。
向一个肯定目标发送超长的字符串并监控反常结果是查找缓冲区溢出漏洞的基本方法。有些时候,一些细微的漏洞只有经过发送一个特殊长度或者在较小的长度范围内的超长字符串才能检测出来。可是,许多时候,只需向应用程序发送一个超出其预计长度的字符串,就能够探查出漏洞。
程序员经常使用十进制或十六进制的约整数(如3二、100、102四、4096等)来建立固定大小的缓冲区。在应用程序中探查明显漏洞的一个简单方法就是,向肯定的每个目标数据发送超长字符串,而后监控服务器对反常输入的响应。
渗透测试步骤:
(1)向每个目标数据提交一系列稍大于经常使用缓冲区大小的字符串。如:1100、4200、33000
(2)一次针对一个数据实施攻击,最大程度的覆盖应用程序中的全部代码路径。
(3)可使用Burp Intruder中的字符块有效载荷来源自动生成各类大小的有效载荷。
(4)监控应用程序的响应,肯定全部反常现象。没法控制的溢出几乎能够确定会在应用程序中引发异常。在远程进程中探测什么时候出现这种异常至关困难,须要寻找的反常现象包括如下几项。
HTTP500状态码或错误消息,这时其余畸形(而非超长)输入不会产生相同的结果。
内从详细的消息,表示某个本地代码组件发生故障。
服务器收到一个局部或畸形响应
服务器的TCP连接未返回响应,忽然关闭。
整个Web应用程序中止响应。
(5)注意,若是一个堆溢出被触发,这可能会在未来而非当即致使系统崩溃。所以,必须进行实验,肯定一种或几种形成堆“腐化”的测试字符串。
(6)“一位偏移”漏洞可能不会造车系统崩溃,但可能会致使反常行为,如应用程序返回出人意外的数据。
有些时候,测试字符串可能会被应用程序自身或其余组件(如Web服务器)实施的输入确认检查所阻止。在URL查询字符串中提交超长数据时一般会出现这种状况,应用程序会再针对每一个测试字符串的响应中以“URL过长”之类的常规消息反映这一点。在这种状况下,应当进行实验,肯定URL容许的最大长度(通常约为2000个字符),并调整缓冲区大小,以使测试字符串符合这个要求。可是,即便实施了常规过滤,溢出可能依然存在;由于长度足够短、可以避开这种过滤的字符串也可能触发溢出。
其余状况下,过滤机制可能会限制一个特定参数中提交的数据类型或字符范围。例如,当将提交的用户名传送给一个包含溢出漏洞的功能时,应用程序会确认该用户名是否包含字母数字字符。为实现测试效率最大化,渗透测试员应当设法确保每一个测试字符串仅包含相关参数容许的字符。知足这种要求的一个有效方法是,截获一个包含应用程序所接受的数据的正常请求,而后使用其中已经包含的相同类型的字符,建立一个可能经过任何基于内容的过滤的长字符串,在使用这个字符串轮流测试每个目标参数。即便确信应用程序中存在缓冲区溢出漏洞,可是,要远程利用它执行任意代码仍然及其困难。
若是应用程序在执行某种缓冲区操做前对一个长度值运用某种算法,但却没有考虑到编译器与处理器整数计算方面的一些特色,每每就会出现与整数有关的漏洞。有两种类型的漏洞最值得关注:整数溢出与符号错误。
当对一个整数值进行操做时,若是整数大于它的最大可能值或小于它的最小可能值,就会形成整数溢出漏洞。这时,数字就会“回绕”,使得一个很是大的数字变得很是小,或者与之相反。下面之前面堆溢出漏洞的“修复”代码为例:
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的缓冲区,把用户名复制到它里面于是形成堆语出。这样,即便程序员试图确保目标缓冲区足够大,攻击者仍然可以制造溢出。
若是应用程序使用有符号和无符号的整数来表示缓冲区的长度,而且在某个地方混淆这两个整数,或者将一个有符号的的值与无符号的值进行直接比较,或者向一个仅接受无符号的值得函数参数提交有符号的值,都会出现符号错误。在上述两种状况下,有符号的值都会被当作其对应的无符号的值处理,也就是说,一个负数变成一个大整数。下面之前面栈溢出漏洞的修复“代码”为例:
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计算,并在请求中将它所属的字符串一块儿提交。可是,若是整数变量足够小(例如,很是短)且程序在服务器端计算它的长度,那么攻击者仍然能够经过向应用程序提交一个超长的字符串,借由整数溢出引入一个负值。
天然的,任什么时候候,只要客户向服务器提交整数值,咱们就能够在这些位置探查整数漏洞。一般这种行为发生在如下两种不一样的状况下。
(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)与上述查找缓冲区溢出漏洞时同样,应该监控应用程序响应中出现的反常事件。
若是用户可控制的输入被当作格式化字符串参数提交给一个可接受可能被滥用的格式说明符的函数(如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之类函数的所有或部分格式化字符串,他就能够利用上述行为覆写进程内存的重要部分,并最终执行任意代码。因为攻击者控制着格式化字符串,因此它可以控制函数输出的字节数量以及栈上被输入的字节数量覆写的指针。这样,攻击者就可以覆写一个已保存的返回地址或者一个指向异常处理器的指针,进而控制代码执行,就向在栈溢出中同样。
在远程应用程序中探查格式化字符串漏洞的最有效方法是,提交包含各类格式说明符的数据,并监控应用程序的任何反常行为。与不受控制地触发缓冲区溢出漏洞可能形成的后果同样,在一个易受攻击的应用程序探查格式化字符串漏洞可能会致使系统崩溃。
渗透测试步骤:
(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
与上述查找缓冲区溢出漏洞时同样,应该监控应用程序响应中出现的反常事件。
当评估某个应用程序的安全状态时,Web应用程序架构常常被忽略,但实际上它是一个重要的安全领域。在经常使用的分层架构中,若是没法隔离不一样的层次,攻击者就能够利用某个层次中的一个漏洞彻底攻破其余层次,进而控制整个应用程序。
设计不佳的分层架构可能受到如下三种类型的攻击:
(1)能够利用不一样层之间的信任关系扩大攻击范围,从一个层侵入到另外一个层
(2)若是不一样层之间没有彻底隔离,就能够利用某一层存在缺陷直接破坏另外一层实施的安全保护
(3)局部攻破一个层后,就能够直接攻击其余层的基础架构,从而将攻击扩大到其它层。
渗透测试步骤:
(1)对于在应用程序中已肯定的任何漏洞,应发挥想象,考虑如何利用这个漏洞实现渗透测试目标;这是贯穿全书的主题。无数针对Web应用程序实施的成功攻击,最初都是从利用一个内部影响有限的漏洞开始的。
(2)若是能再任何应用程序组件上执行任意命令,并可以与其余主机创建网络链接,考虑向网络与操做系统层面中的应用程序其余基础架构发动直接购机,以扩大攻击范围。
若是以严谨的方式执行多层架构,该架构就能够显著提升应用程序的安全,由于它可以将一次成功攻击的影响控制在局部。在前面描述的基本LAMP配置中,全部组件都在一台计算机上运行,攻破其中一个层就可能致使整个应用程序被彻底攻破。在更安全的架构中,攻击者攻破一个层,只能部分控制应用程序的数据与处理操做,于是其形成的影响有限,可能局限于被攻破的层中。
(1)尽可能减小信任关系
应用程序服务器应对特殊的资源与URL路径实施基于角色的访问控制。
数据库服务器层能够为应用程序的不一样用户和操做提供各类权限的帐户。
全部应用程序组件可使用拥有正常操做所需的最低权限的操做系统帐户运行。
(2)隔离不一样的组件
一个层不得读取或写入其余曾使用的文件
对不一样基础架构组件之间的网络级访问进行过滤,仅容许须要与不一样应用程序层彼此通讯的服务器
(3)应用深层防护
应根据配置与漏洞补丁,把每台主机上的技术栈的各个层面进行安全强化
应对保存在任何应用程序层中的数据进行加密,以防止攻破该层的攻击者轻松得到这些数据。
许多组织经过外部提供商向公众提供他们的Web应用程序,这些服务器包括组织经过其访问Web与数据库服务器的简单主机服务,以及表明组织主动维护应用程序的成熟应用程序服务器提供商。缺少能力与资源部署本身的应用程序的小型企业经常采用这种服务,但一些知名公司有时也使用这些服务来部署特殊的应用程序。
简单的共享主机配置中,一台Web服务器只需支付几个域名各不相同的虚拟Web站点。它经过HOST消息头达到这个目的;在HTTP1.1中,请求中必须包含该消息头。当浏览器提出一个HTTP请求时,请求中既包含一个HOST消息头,该消息头中含有相关URL中的域名;而后,请求被传送到与域名关联的IP地址中。若是解析几个域名获得相同的IP地址,在这个地址上的服务器仍然可以肯定请求但愿访问哪个Web站点。
许多ASP提供现成的应用程序,可由客户修改与定制使用。对于拥有大量业务、须要部署功能强大的、能为终端用户提供基本相同功能应用程序的行业,使用这种模型能够节省大量成本。
渗透测试步骤:
(1)检查为共享环境中的客户提供的、便于他们更新和管理内容与功能的访问机制。考虑如下几个问题。
远程访问机制是否使用一个安全的协议与通过适当强化的基础架构?
客户是否可以访问他们正常状况下不能访问的文件、数据及其余资源?
客户是否可以在主机环境中得到一个交互式的shell,并执行任意命令?
(2)若是使用一个全部权应用程序,以方便客户配置和定制共享环境,考虑是否可以以这个应用程序为攻击目标,攻破该环境自己及其中运行的全部应用程序
(3)若是可以在某个应用程序中执行命令、注入SQL脚本或访问任意文件,仔细研究,看是否可以以此扩大攻击范围,攻破其余应用程序
(4)若是渗透测试员正在攻击一个使用ASP主机的应用程序,且该应用程序由许多共享与定制组件构成,肯定其中的任何共享组件,如日志机制、管理功能以及数据库代码组件,常使用利用这些组件攻破应用程序的共享部分,进而攻破其余应用程序。
(5)若是全部共享环境使用一个经常使用的数据库,使用NGSSquirrel之类的数据库扫描工具,对数据库配置、补丁版本、表结构以及许可进行全面审查。数据库安全模型中存在的任何缺陷均可以被加以利用,将攻击范围由一个应用程序扩大到另外一个应用程序。
因为使用相同工具的客户怀有恶意企图以及不知情的客户在环境中引入的漏洞,所以,共享环境给应用程序安全带来了新的威胁。为解决这种双重威胁,在设计共享环境中必须仔细处理客户访问、隔离与信任关系,并实施并不直接适用于单主机应用程序的控制。
(1)保障客户访问的安全
(2)隔离客户功能,不能信任共享环境中的客户,认为他们仅建设没有漏洞的无害功能。
(3)隔离共享应用程序中的组件。