软件防御:缓解栈损坏漏洞的技术解析

本文深入探讨了栈损坏漏洞的防御技术,包括GS编译保护、SEHOP异常处理保护及其演进历程。详细分析了从Windows XP到Windows 8.1的防御机制改进,以及当前栈漏洞利用的现状与未来挑战。

软件防御:缓解栈损坏漏洞

引言

栈损坏漏洞是最古老的内存安全利用形式之一,早期多个高调漏洞利用均属此类。因此,通过审视当前软件防御在这一古老问题上的现状,来开启本“软件防御”系列似乎颇为合适。

缓解基于栈的损坏漏洞

栈损坏的最常见形式——缓冲区溢出超出为其分配的栈空间——自Windows XP以来已通过基于编译器的/GS功能得到一定程度的缓解。每个模块随机值的副本(即/GS cookie)被放置在栈局部变量和栈元数据(包括返回地址)之间。在使用返回地址之前,程序验证此本地/GS cookie副本的完整性:如果其值与每个进程的主值不匹配,则假定发生了溢出,程序终止。

多年来,该防御机制的局限性推动了多项改进;主要改进总结如下表:

改进方面 描述
GS保护范围 出于性能原因,仅对函数子集进行GS cookie保护。初始保护范围主要针对字符数组,攻击者通常通过网络提供恶意字符串,程序未能正确处理。Visual Studio 2010中的GS增强将保护范围扩展到具有更通用数据结构的函数。Visual Studio 2013在此基础上进一步保护指针数组。
性能优化 使用/GS保护函数会带来代码大小成本(设置GS cookie的前导代码和验证其值的尾声代码)以及执行这些额外指令的运行时间成本。扩展GS保护范围的成本已通过新的编译器优化部分抵消:如果优化器可以证明导致GS cookie的缓冲区使用是安全的(即所有对这些变量关联内存的写入都在其分配的栈空间范围内),则消除GS cookie。
启用/GS 启用/GS通常仍仅在不到10%的函数上插入GS检查——尽管这显然因每个应用程序中局部变量的使用而异。

通过异常滥用规避GS检查

上述提到的一个重要方面值得进一步讨论——cookie检查仅在函数结束时发生。这意味着在栈损坏和GS检查之间存在一个时间窗口,攻击者可以寻求获得控制权。一种首选方法是触发异常并滥用由此产生的异常处理过程。

在x86上,与SEH关联的异常元数据存储在栈上:这包括应调用的处理程序代码的指针。如果攻击者可以使用初始栈损坏修改此SEH元数据——将其选择的地址替换函数指针——那么当异常分发代码运行时,它将把控制权转移到攻击者控制的地址。

在x86平台上缓解异常元数据滥用

编译时技术再次可以提供帮助——/SAFESEH有效地创建异常处理程序地址的白名单。在上述损坏的SEH元数据场景中,攻击者修改的“指向异常处理程序的指针”地址不在白名单上,从而击败了此利用技术的简单形式。然而,这是昂贵的:所有代码都需要重新编译才能受益——因为程序中的任何非SAFESEH代码都会引入一个“具有未知SAFESEH白名单的代码”模块——出于应用程序兼容性目的,该模块内的任何地址都被假定在白名单上。

Windows XP SP2包含了大多数使用/SAFESEH重新编译的OS二进制文件——但即使这样也不足够。任何第三方浏览器插件(未使用/SAFESEH编译)提供了规避/SAFESEH验证的潜在手段。

SEHOP

SEHOP最初在Windows Vista SP1(默认关闭)和Windows Server 2008(默认开启)中支持,提供了更稳健的解决方案。我们之前详细描述了SEHOP,但其基本思想总结如下。当异常发生时,SEHOP遍历栈上的整个SEH元数据结构列表,并验证其终止于特殊的“已知良好处理程序地址”。攻击者对其中一个结构的任何损坏都会覆盖指向下一个SEH元数据结构的向前指针,从而破坏列表的完整性,因此被SEHOP检查检测到。

Windows 7添加了对每个进程选择加入SEHOP的支持。

Windows 8和Windows 8.1中的SEHOP

Windows 8提高了标准,因为对于构建为在Windows 8及更高版本上运行的应用程序(更准确地说,对于任何子系统版本大于或等于6.2的应用程序——详见/SUBSYSTEM),SEHOP默认启用。

在Windows 8.1中,SEHOP通过几种方式进一步改进。

首先,SEHOP现在有64种可能的独特FinalExceptionHandler值可供选择,而不是之前使用的系统DLL中的一个处理程序地址。用于验证异常处理帧链的实际独特FinalExceptionHandler值在进程启动时随机选择,并且每个进程不同。这样做的优点是SEHOP不再具有系统范围的共享秘密,而是每个进程的秘密,因此本地攻击者不再假定他们知道在本地机器上单独进程中通过帧验证检查所需的独特值。所有可能的FinalExceptionHandler值也是有效的SafeSEH处理程序。

其次,Windows 8.1添加了对内核模式SEHOP的支持并默认启用。与用户模式的增强版SEHOP类似,内核模式SEHOP有64种可能的FinalExceptionHandler地址,因此仅披露内核基地址不足以击败内核SEHOP;必须能够读取任意内核内存才能做到这一点。

GS局限性和未来工作

迄今为止描述的GS设计的有效性——即使它应用于每个栈帧并且假设栈内存损坏发生后总是达到cookie检查——仅限于cookie值被更改的情况。这是检测到的内容。因此,重要的基于栈的漏洞类别仍然存在,例如针对性的攻击者控制的写入栈地址,或栈下溢——即写入方向朝向分配的起始地址。

Visual Studio 2012更新了/GS以发出范围检查,以缓解我们通过MSRC看到的最常见类型的针对性写入场景。

最近发布的趋势数据显示,成功利用基于栈的漏洞的数量仅代表我们客户面临的问题集合中的一小部分。

GS、SafeSEH和SEHOP构成的障碍以及越来越多开发人员和IT专业人员选择加入这些措施可能是这背后的部分原因。

静态分析工具的投资也可能起到了一定作用,其中一些通过编译器警告(如C4789)和代码分析警告(如C6200)提供给开发人员。基于栈的缓冲区的生命周期往往比其堆对应物短:它限于一个函数的执行——尽管可能有许多被调用者——使得在整个程序中完全分析其使用更加可行。相比之下,指向堆分配的指针经常存储在多个对象中,程序的使用模式更复杂,使得分析更难以相同精度跟踪堆缓冲区的使用。

尽管一些未缓解的基于栈的漏洞类别偶尔在实践中出现——例如MS08-067,这是一个基于栈的数组的下溢,这些是相对孤立的例子。随着攻击者焦点转向堆,改进那里防御措施的优先级也增加了。

结论

利用基于栈的损坏漏洞是最古老的内存安全利用形式之一。历史显示了一系列为应对攻击者在该领域的创新而开发的缓解改进。我们注意到一系列演变:

  • 缓解的稳健性和完整性随时间改进,包括Visual Studio 2013中/GS对指针数组的保护,以及Windows 8.1中SEHOP从系统范围秘密转向每个进程秘密。
  • 挫败异常处理程序滥用的对策从昂贵的(从工程角度)措施(如需要重新编译的/SAFESEH)演变为基于OS的SEHOP,可以按进程应用于现有应用程序。
  • 默认设置随时间演变;例如,用户模式SEHOP现在对于为Windows 8和Windows 8.1设计的应用程序默认开启,内核模式SEHOP在Windows 8.1中既是新的又默认启用。

已达到相对成熟的状态,报告的或利用的基于栈的漏洞较少。这是否意味着我们“完成了”?——绝非如此!相反,攻击者的注意力似乎转向了其他不太成熟的领域。随着攻击者焦点的转移,防御也随之转移。下一篇文章将更仔细地查看与堆上内存损坏相关的一些进展。

Tim Burrell, MSEC Security Science

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计