【无标题】

内存战争

摘要

:在用C或c++等低级语言编写的软件中,内存破坏漏洞是计算机安全中最古老的问题之一。这些语言缺乏安全性,因此攻击者可以改变程序的行为,或者通过劫持程序的控制流来完全控制程序。这个问题已经存在了30多年,已经提出了大量潜在的解决方案,但内存破坏攻击仍然构成严重的威胁。现实世界的漏洞显示,所有当前部署的保护措施都可以被击败。
本文通过描述在当今系统上成功的攻击来阐明这一问题的主要原因。通过建立内存破坏攻击的通用模型,对现有的各种保护技术的知识进行了系统化。使用这个模型,我们展示了哪些策略可以阻止哪些攻击。该模型识别了目前部署的技术的弱点,以及其他实施更严格政策的拟议保护措施。
我们分析了实施更严格政策的保护机制没有部署的原因。为了获得广泛采用,保护机制必须支持大量特性,并且必须满足大量需求。性能尤其重要,因为经验表明,只有开销在合理范围内的解决方案才会得到部署。
比较不同的可执行策略有助于新的保护机制的设计者在有效性(安全性)和效率之间找到平衡。我们确定了一些开放的研究问题,并提供了建议,以提高新技术的采用。

介绍

内存破坏漏洞是计算机安全中最古老的问题之一。用低级语言(如C或c++)编写的应用程序很容易出现这类bug。这种语言中内存安全性(或类型安全性)的缺乏使攻击者能够通过恶意地改变程序的行为甚至完全控制控制流来利用内存bug。最明显的解决方案是避免使用这些语言,并用类型安全的语言重写易受攻击的应用程序。不幸的是,这是不现实的,不仅因为现有的数十亿行C/ c++代码,而且因为性能关键程序(例如操作系统)需要底层特性。

记忆战争的一方是开发新的攻击和恶意攻击的进攻性研究人员,另一方是开发新的保护措施的防御性研究人员和试图编写安全程序的应用程序程序员。记忆战争实际上是进攻和防守之间的军备竞赛。根据MITRE的[1]排名,内存损坏错误被认为是最危险的三个软件错误之一。谷歌Chrome是用c++编写的最安全的网络浏览器之一,在2012年的Pwn2Own/Pwnium黑客竞赛中被攻击了四次。

在过去的30年里,已经开发了一套针对内存损坏攻击的防御系统。它们中的一些被部署在商品系统和编译器中,保护应用程序免受不同形式的攻击。堆栈cookie[2],异常处理程序验证[3],数据执行预防[4]和地址空间布局随机化[5]使得利用内存破坏漏洞变得更加困难,但是在所有这些当前部署的基本保护设置下,一些攻击向量仍然有效。returnorented Programming (ROP)[6],[7],[8],[9],[10],[11],信息泄露[12],[13]以及用户脚本和即时编译[14]的普遍使用,允许攻击者执行几乎任何攻击,尽管有所有的保护。

为了克服一个或多个可能的攻击向量,提出了多种防御机制。然而,由于以下一个或多个因素,它们中的大多数都没有在实践中使用:该方法的性能开销超过了潜在的保护,该方法与所有当前使用的特性不兼容(例如,在遗留程序中),该方法不健壮,提供的保护不完整,或者,该方法依赖于编译器工具链或源代码中的更改,而工具链不是公共可用的。

对于所有不同的攻击和拟议的防御,很难看出不同的解决方案有多有效,它们之间的比较如何,以及主要的挑战是什么。本文的目的是对先前提出的方法进行系统化和评价。通过建立内存破坏漏洞和利用技术的通用模型,对系统进行了系统分析。防御技术是根据它们减轻的exploit和它们试图阻止的exploit的特定阶段来分类的。评估基于健壮性、性能和兼容性。使用此评估,我们还讨论了成功部署新软件防御所需满足的通用标准。

一些相关的工作已经涵盖了不同的内存损坏攻击[15],提供了历史概述[16]或列出了不同的保护机制[17]。这篇知识论文的系统化扩展了相关的调查论文,开发了一种新的内存破坏攻击通用模型,并使用一套新的标准评估和比较了提出的防御机制,该标准还纳入了现实世界的采用。该文件的目的不是涵盖或提及每一个提出的解决方案,而是系统地识别和分析主要方法,梳理最有希望的建议,并指出根本问题和未解决的挑战。

•开发了一个内存破坏攻击的通用模型,并在此模型的基础上确定了不同的可执行安全策略;•通过将其执行策略与不同的攻击阶段相匹配,阐明当前使用的和先前提出的保护措施未受保护的攻击向量;•评估和比较提出的解决方案的性能、兼容性和健壮性;•讨论为什么许多提出的解决方案在实践中没有被采用,以及新解决方案的必要标准是什么。

本文的组织结构如下。第二节建立了攻击的主要模型,并根据其执行的策略对保护进行了分类。第三节讨论了目前部署的保护措施及其主要弱点。我们的评估标准在第四节中建立,并通过以下四节所涵盖的防御方法的分析来使用。第IX节通过比较分析进行总结,第X节对本文进行总结。

II.attacks

为了解决基于内存错误的攻击问题,我们首先需要了解执行这种攻击的过程。在本节中,我们建立了一个逐步的内存利用模型。我们将以讨论保护技术及其在此模型上执行的策略为基础。图1显示了利用内存错误的不同步骤。每个白色矩形节点代表成功利用的基石,底部的椭圆节点代表成功攻击。每个菱形代表了一个通向目标的可选路径的决定。虽然控制流劫持通常是攻击的主要目标,但也可以利用内存损坏来执行其他类型的攻击。

A. Memory corruption

由于本文中讨论的所有漏洞的根本原因都是内存损坏,因此每个漏洞都是从触发内存错误开始的。图1中的攻击的前两个步骤涉及初始内存错误。第一步使指针无效,第二步解除对指针的引用,从而触发错误。指针可以通过超出其所指向对象的边界或对象被释放而失效。指向已删除对象的指针称为悬空指针。对越界指针解引用会导致所谓的空间错误,而对悬空指针解引用会导致时间错误。

在第1步中,攻击者可能会利用各种编程错误迫使指针超出界限。例如,通过触发未检查的分配失败,该指针可以变成空指针(在内核空间中,nullpointer解引用是可利用的[18])。如果在循环中对数组指针进行过度递增或递减运算而没有进行适当的绑定检查,就会发生缓冲区溢/下溢。如果攻击者控制了数组的索引,但边界检查缺失或不完整,则会导致索引错误,指针可能指向任何地址。索引错误通常是由整数相关的错误引起的,比如整数溢出、截断或签名错误,或不正确的指针转换。最后,我们讨论的内存错误可能会损坏指针。这由图1中的向后循环表示。

作为一种替代方法,攻击者可以使指针“悬空”,例如,利用不正确的异常处理程序释放对象,但不重新初始化指向对象的指针。临时内存错误被称为use - after-free漏洞,因为悬空指针指向的内存区域被返回(释放)给内存管理系统后,悬空指针被解除引用(使用)。大多数攻击的目标是堆中分配的对象,但是当给全局指针赋值时,指向局部变量的指针也可以从局部作用域“逃逸”。当函数返回并删除堆栈上的局部变量时,这些转义的指针将悬空。

接下来,我们将展示在第2步中读取或写入无效指针时,如何利用越界指针或悬空指针执行利用模型中的第三步。第三步是一些内部数据的损坏或泄漏。
当攻击者通过解除对指针的引用将值从内存读入寄存器时,该值可能被破坏。考虑下面的跳转表,其中定义下一个函数调用的函数指针从一个数组中读取,而不进行边界检查。
攻击者可以使指针指向他或她控制下的位置,并转移控制流。任何其他间接读取的变量都可能受到攻击。

除了数据损坏,通过攻击者指定的指针读取内存泄漏信息,如果该数据包含在输出中。这种攻击的典型例子是printf格式字符串错误,其中格式字符串由攻击者控制。通过指定格式字符串,攻击者创建无效的指针,并读取(或写入)任意的内存位置。
如果攻击者控制的指针被用来写入内存,那么任何变量,包括其他指针甚至代码,都可以被覆盖。缓冲区溢出和索引错误可以被用来覆盖敏感数据,例如返回地址或虚拟表(vtable)指针。破坏虚函数表指针是图1中向后循环的一个例子。假设在第一轮中,缓冲区溢出使数组指针出界,在第二轮中(在第3步中)破坏了内存中附近的虚函数表指针。当损坏的虚函数表指针被解引用时(在步骤2中),一个虚假的虚函数指针将被使用。重要的是要看到,对于一个内存错误,会通过破坏其他指针而引发越来越多的内存错误。还可以利用攻击者控制的指针调用free()来执行任意的内存写入[19]。写解引用也可能被用来泄漏信息。例如,攻击者可以通过破坏err_msg指针泄露上述代码行的任意内存内容。

当悬浮指针在第2步中被解引用时,时间错误可以类似于空间错误被利用。可利用时间错误的一个约束是,已分配对象(旧对象)的内存区域被另一个对象(新对象)重用。新旧对象之间的类型不匹配会允许攻击者访问非预期的内存。

让我们首先考虑读取带有旧对象类型但指向由攻击者控制的新对象的悬空指针。当调用旧对象的虚函数并查找虚函数指针时,新对象的内容将被解释为旧对象的虚函数表指针。这允许虚函数表指针的损坏,类似于利用空间写错误,但在这种情况下,悬浮指针只在读取时被解引用。这种攻击的另一个方面是,新对象可能包含敏感信息,当通过旧对象类型的悬空指针读取时,这些信息可能被泄露。

同样,通过悬空指针进行写入也可以作为越界指针,破坏新对象中的其他指针或数据。当悬空指针是指向局部变量并指向堆栈的转义指针时,它可能被用来覆盖敏感数据,例如返回地址。Double-free是释放后使用漏洞的一种特殊情况,悬浮指针被用来再次调用free()。在这种情况下,攻击者控制的新对象的内容将被错误地解释为堆元数据,这也可用于任意内存写入[19]。

内存错误通常允许攻击者以意想不到的方式读取和修改程序的内部状态。我们展示了在我们的内存利用模型中,前两个步骤的任何组合都可以用来破坏内部数据和泄漏敏感信息。此外,损坏其他指针可能会触发更多的内存错误。使这些错误成为可能的编程错误,例如缓冲区溢出和双释放,在C/ c++中很常见。在使用这种低级语言进行开发时,边界检查和内存管理都完全由程序员负责,这是非常容易出错的。

上述错误违反了内存安全策略。C和c++本身就是内存不安全的。根据C/ c++标准,编写超出界限的数组、解引用空指针或读取未初始化的变量会导致未定义的行为。由于发现和修复所有的编程错误是不可行的,我们需要自动解决方案来在现有程序中实施内存安全或在其后期阶段停止攻击。减轻一组给定攻击步骤的策略在图中由白色方框周围的彩色区域表示。图中还显示了讨论执行给定策略的方法的节号,在策略名称的上方。在第VI节中,我们讨论了在第1(2)步中通过加强内存安全来阻止任何攻击的方法。在接下来的小节中,我们将讨论不同攻击路径的步骤,并确定减轻给定步骤的策略。如图所示,一些策略包括其他较弱的策略。

B. Code corruption attack
修改程序执行的最明显的方法是使用上面提到的错误之一来覆盖内存中的程序代码。代码完整性策略强制不能编写程序代码。如果所有包含代码的内存页都被设置为只读,则可以实现代码完整性,这是所有现代处理器都支持的。不幸的是,代码完整性不支持自修改代码或即时(JIT)编译。如今,每个主流浏览器都包含了用于JavaScript或Flash的JIT编译器。对于这些用例,不能完全强制执行代码完整性,因为有一个时间窗口,在此期间生成的代码位于可写页面上。

C. Control-flow hijack attack
大多数情况下,内存破坏利用人员试图通过转移程序的控制流来控制程序。如果代码完整性是强制的,那么这个替代选项尝试使用内存错误破坏步骤3中的代码指针。代码指针完整性策略旨在防止代码指针的损坏。我们将在第VIII-A节讨论该策略的潜在代码指针目标和局限性。
假设攻击者可以由于缓冲区溢出而访问和修改返回地址。对于一个成功的攻击,攻击者还需要知道正确的目标值(即有效负载的地址)。我们将其表示为图1中的第4步。如果控制流劫持的目标(要跳转到的代码地址)不是固定的,攻击者不能指定目标,攻击在这一步失败。这一特性可以通过使用地址空间随机化在内存地址中引入熵来实现。我们将在第V-A节讨论随机化地址空间的技术。
假设一个代码指针(例如,一个函数指针)在前四个步骤中被成功破坏。第五步是执行需要将损坏的指针加载到指令指针中。指令指针只能通过执行间接控制流传输指令来间接更新,例如,间接函数调用、间接跳转或函数返回指令。从源代码定义的控制流转移执行违反了控制流完整性(CFI)策略。在第8 - b节中,我们讨论了通过在间接控制转移中发现腐败来执行不同的CFI政策的保护措施。
控制流劫持利用的最后一步是执行攻击者指定的恶意代码。典型的攻击将所谓的shellcode注入到内存中,并将执行转移到这段代码。非可执行数据策略可以防止这种利用,该策略可以使用内存页的可执行位强制执行,从而使数据内存页(如堆栈或堆)不可执行。非可执行数据和代码完整性的组合导致W⊕X (Write XOR Execute)[4]策略,表示页面可以是可写的,也可以是可执行的,但不能同时是可写的。实际上所有的现代CPU都支持设置不可执行的页面权限,所以结合不可写的代码,执行W⊕X是廉价和实用的。但是在JIT编译或自修改代码的情况下,W⊕X不能完全执行。出于完整性的考虑,我们注意到另一种随机化方法,指令集随机化(ISR)也可以通过加密来减少注入代码的执行或现有代码的破坏。但是由于对页面权限的支持,速度慢得多的ISR变得不那么重要了,由于篇幅有限,我们将不在本文中详细介绍它。
为了绕过非可执行数据策略,攻击者可以重用内存中的现有代码。重用的代码可以是现有的函数(“return-to-libc”攻击),也可以是代码中随处可见的小指令序列(gadget),它们可以被链接在一起执行有用的(恶意的)操作。这种方法称为面向返回编程(Return Oriented Programming, ROP),因为攻击使用结束返回指令将函数或小工具的执行链起来。面向跳转编程(JOP)是这种攻击的一般化,它利用间接跳转和链接。此时没有策略可以阻止攻击,因为无法阻止有效和已经存在的代码的执行。最近的研究集中在减少代码重用的技术上。研究人员提出了通过编译器[20]或二进制重写[21]从代码中消除有用代码块(用于ROP)的技术。虽然这些解决方案增加了ROP难度,但它们并没有消除所有有用的小工具,也没有阻止对完整功能的重用。由于这些原因,我们不会更详细地介绍这些技术。
一旦攻击者指定的代码开始执行,我们就认为控制流劫持攻击成功。要进行有意义的攻击,攻击者通常需要进行系统调用,可能还需要高级权限(例如,文件访问)。我们将不讨论仅限制攻击者访问的高级策略,包括权限、强制访问控制或由SFI[22]、XFI[23]或Native Client[24]强制实施的沙箱策略。这些策略可以限制不可信程序(或插件)或攻击者在危及可信程序后可能造成的损害。我们的重点是防止受信任程序的入侵,通常是通过广泛的访问(例如,ssh/web服务器)。

D. Data-only attack
劫持控制流并不是成功攻击的唯一可能性。一般来说,攻击者的目标是恶意地修改程序逻辑以获得更多的控制、获得特权或泄漏信息。这个目标可以在不修改与控制流明确相关的数据的情况下实现。例如,考虑在没有管理员特权的情况下登录到系统后,通过缓冲区溢出对isAdmin变量的修改。
这些特定于程序的攻击也被称为“非控制数据攻击”[25],因为代码和代码指针(控制数据)都没有损坏。破坏的目标可以是内存中的任何安全关键数据,例如,配置数据、用户标识或密钥的表示。

此次攻击的步骤与前一次相似,只是针对的是腐败。在这里,目标是破坏步骤3中的一些安全关键变量。由于安全关键是一个语义定义,因此必须保护所有变量的完整性,以阻止这一步骤中的攻击。我们称这种策略为数据完整性,它自然包括代码完整性和代码指针完整性。数据完整性方法试图通过强制执行一些近似于内存安全的方法来防止数据损坏。我们将在VII-A中介绍执行此类策略的技术。

在代码指针的情况下,攻击者需要知道应该用什么来替换损坏的数据。通过使用数据空间随机化将熵引入所有数据的表示,可以防止获取这一知识。数据空间随机化技术扩展和概括了地址空间随机化,我们将在V-B节介绍它们。

类似于代码指针损坏,一旦损坏的变量被使用,仅数据攻击就会成功。使用正在运行的示例,if (isAdmin)语句必须在不检测损坏的情况下成功执行。作为控制流完整性的推广,使用任何损坏的数据都是对数据流完整性的违反。我们在第VII-B节中讨论了该政策的执行。

E. Information leak
我们展示了任何类型的内存错误都可能被用来泄漏内存内容,否则将从输出中排除这些内容。这通常用于规避基于随机和秘密的概率防御。真实的漏洞利用信息泄露[13],[26]绕过ASLR。除了内存安全之外,唯一可能减少信息泄漏的策略是完全数据空间随机化。我们将在第五节讨论数据空间随机化的有效性,以及信息泄漏如何被用来绕过其他建立在秘密之上的概率技术。

3.当前使用的保护和现实世界的漏洞

应用最广泛的保护机制有堆叠粉碎保护、DEP/W⊕X和ASLR。例如,Windows平台也提供了一些特殊的机制,例如,保护堆元数据和异常处理程序(SafeSEH和SEHOP)。
堆栈粉碎保护[2]检测本地基于堆栈的缓冲区的缓冲区溢出,它覆盖保存的返回地址。通过在返回地址和函数条目的本地缓冲区之间放置一个随机值(称为cookie或canary),可以在函数返回之前检查cookie的完整性,从而检测溢出。
SafeSEH和SEHOP还在使用异常处理程序指针之前验证它们,这使得它们与堆栈cookie一起成为一种控制流完整性解决方案。这些技术提供了最弱的保护:它们只在间接跳转的一小部分子集之前进行检查,专注于检查某些特定代码指针的完整性,即保存的返回地址和堆栈上的异常处理程序指针。此外,可以绕过这些检查。例如,cookie可以检测到缓冲区溢出攻击,但不能检测到直接覆盖(例如,利用索引错误)。
DEP/W⊕X可以防止代码注入攻击,但不能防止ROP等代码重用攻击。ROP漏洞可以自动生成[27],像C库这样的大型代码库通常为图灵完备性[10]、[11]提供足够的小工具。ASLR作为最广泛应用的地址空间随机化技术,提供了最全面的保护。它可以随机分配各种内存段(包括数据和代码)的位置,因此即使攻击者希望重用某个gadget,它的位置也是随机的。虽然一些ASLR实现有特定的弱点(例如,代码区域留在可预测的位置,由于低熵,可能发生去随机化攻击),但针对它的基本攻击是信息泄漏[12]。

正如在上一节的攻击模型中所描述的,任何内存损坏错误都可以转换为信息泄漏漏洞,该漏洞可用于获取当前的代码地址。需要泄漏的地址来构造最终的exploit有效负载。当攻击远程目标(即服务器)时,获取这些信息曾经是非常具有挑战性的。然而,今天对于许多目标客户来说,这已经不是问题了。Web浏览器、PDF查看器和办公应用程序运行用户控制的脚本(JavaScript、ActionScript、VBScript),这些脚本可用于在目标机器的运行时动态构建exploit有效负载。表一列出了VUPEN[28]最近发布的一些漏洞,它们利用信息泄露和ROP绕过ASLR和W⊕X。在所有的例子中,控制流都是在间接调用指令时被劫持的(在破坏函数指针或虚函数表之后),所以堆栈cookie不是问题。在所有情况下,地址泄漏都是通过利用任意的内存写入来破坏另一个指针(第四列给出了更多的提示)。泄漏内存内容的一种常见方法是在读取(在用户脚本中)字符串对象之前重写它的长度字段(例如JavaScript)。如上一列所示,对于浏览器目标,使用用户脚本泄露当前地址并构建漏洞,而对于ProFTPD,通过破坏指向状态消息的指针将泄漏的信息发送回网络。

方法和评价标准

以前确定的保护技术可以分为两大类:概率保护和威慑保护。概率解决方案,如指令集随机化,地址空间随机化,或数据空间随机化,建立在随机化或加密的基础上。所有其他方法都通过实现低级引用监视器[29]来执行确定性安全策略。引用监视器观察程序的执行,并在程序即将违反给定的安全策略时将其停止。传统的引用监视器执行更高级别的策略,如文件系统权限,并在内核中实现(例如,系统调用)。

引用监视器执行较低级别的策略,例如内存安全或控制流完整性,可以通过两种方式有效实现:在硬件中或通过将引用监视器嵌入到代码中。例如,W⊕X策略(代码完整性和不可执行数据)现在由硬件强制执行,因为现代处理器同时支持不可写和不可执行的页面权限。对安全策略的硬件支持导致的开销可以忽略不计。替代硬件支持的方法是在代码中动态或静态地添加引用监视器。

由于向硬件添加新特性是不现实的,从这一点上讲,我们只关注转换现有程序以实施各种策略的解决方案。动态(二进制)工具(例如,Valgrind [30], PIN [31], DynamoRIO[32],或libdetox[33])可用于在运行时动态地将安全检查插入到不安全的二进制文件中。动态二进制插装支持任意转换,但由于动态转换过程,会带来一些额外的减速。然而,简单的引用监视器可以用较低的开销实现(例如,在[33]中SPEC CPU2006中,一个影子堆栈的开销低于6.5%的性能)。更复杂的参考监控器,如污染检查[34]或ROP探测器[35],会导致开销超过100%,而且不太可能在实践中部署。静态插装静态内联引用监视器。这可以通过编译器或静态二进制重写来完成。内联引用监视器可以实现任何安全策略,而且通常比动态解决方案更有效,因为检测不是在运行时执行的。

接下来,我们讨论执行低级策略的解决方案的主要属性和需求。这些性质决定了所提出方法的实用性,更准确地说,它是否适合广泛采用。我们在讨论给定的性质时提出了实用性的要求。

A. Protection
执行政策。这种保护的力度被它所执行的政策所遏制。解决方案执行的确切策略决定了其有效性。不同的技术在不同的级别执行不同类型的策略(例如,内存安全或数据流完整性)。在我们已经确定的四种攻击中,策略的实际效用可以通过它可以防止的攻击来描述。策略的细微差别允许或防止个别攻击。一种方法的准确性由假阴性和假阳性之间的关系决定。

假阴性。假否定(保护失败)的可能性取决于政策的定义。对于概率逼近,攻击成功的概率总是> 0,而对于确定性解,攻击成功的概率≥0。如第三节所示,该保护所依赖的秘密不仅可以被猜测,还可以被泄露。

假阳性。对于任何实际的解决方案来说,避免错误警报(例如,不必要的崩溃)是一个非常严格的要求。在生产环境中,正常运行时造成故障是不可接受的。此外,兼容性问题不应该导致任何错误警报。

B. Cost
性能开销。解决方案的成本主要是由它引入的性能开销决定的。除了安全,最重要的要求是速度。

为了度量性能,可以同时使用cpu绑定和I/ o绑定基准测试。CPU绑定基准测试(如SPEC[36])更具挑战性,因为I/ o绑定的程序在内核中花费更多时间,相对降低了用户空间CPU开销的影响。尽管一些建议报告在选定的基准测试程序或I/ o绑定的服务器应用程序中取得了不错的成绩,但如果使用cpu绑定的基准测试,它们的开销要高得多。我们建议广泛采用针对cpu绑定的客户端程序的保护方法,这些程序是当今攻击的主要目标。

我们在第IX节中的比较分析表明,引入开销超过10%的技术往往不会在生产环境中得到广泛采用。一些人认为,为了被业界采用,平均开销应该小于5%,例如微软蓝帽大赛[37]的规则就证实了这一观点。

内存开销。内联监视器通常会引入和传播某种元数据,这也会带来巨大的内存开销。一些保护机制(尤其是使用影子内存的机制)甚至可以使程序的空间需求增加一倍。然而,对于大多数应用程序来说,这不是什么大问题,而是运行时性能的问题。

C. Compatibility
源的兼容性。如果一种方法不需要手动修改应用程序源代码以从保护中获益,那么它就是源代码兼容的(或源代码无关的)方法。即使是最小的人工干预或工作的必要性,也会使解决方案不仅不可伸缩,而且成本过高。大多数业内专家认为,需要移植或注释源代码的解决方案是不切实际的。

二进制兼容性。二进制兼容性允许与未修改的二进制模块兼容。转换后的程序仍然应该链接到未修改的库。向后兼容性是支持遗留库的一个实际需求。使用不受保护的库可能会使程序的某些部分可被利用,但允许增量部署。此外,例如在Windows平台上,系统库是完整性保护的,因此不容易更改。

模块化的支持。支持模块化意味着单独的模块(例如库)被单独处理。基于编译器的解决方案应该支持单独的模块编译,而二进制重写器应该支持单独加固每个文件(主可执行文件或库)。由于动态链接库(.dll和.so)是现代操作系统不可缺少的,所以所有实际的保护措施也必须支持它们。

概率方法

概率方法依赖于随机和秘密。有三种主要的方法:指令集随机化、地址空间随机化和数据空间随机化。图1显示了指令集随机化(ISR)[38]减轻了基于代码损坏和shell代码注入的攻击。通过只读页面权限可以防止代码损坏,而通过不可执行的页面权限可以防止shellcode注入。由于硬件的改进,ISR已经过时了。地址空间随机化(ASR)通过随机化代码和数据的位置以及潜在有效载荷地址来缓解控制流劫持攻击。数据空间随机化(DSR)通过随机化(加密)内存内容,概率地缓解所有攻击。

地址空间随机化
地址空间布局随机化(ASLR)[5],[39]是最著名的内存地址随机化技术。ASLR随机排列不同的代码和数据存储区域的位置。如果负载在虚拟内存空间中的地址不固定,攻击者就无法可靠地转移控制流。ASLR是目前部署的针对劫持攻击的最全面的保护。转移的跳转目标可以是数据区域中的一些注入负载,也可以是代码部分中的现有代码。这就是为什么每个内存区域必须是随机的,包括堆栈、堆、主代码段和库。如果不是所有代码和数据段都是随机的,那么总是可以绕过该保护。例如,在大多数Linux发行版中,只有库代码的位置是随机的,但是主模块位于一个固定的地址。大多数程序不被编译为位置独立可执行文件(PIE),以防止平均性能下降10%[40]。

此外,在32位机器上,虚拟内存空间允许的最大可能熵对于暴力或去随机化攻击[41]是无效的。去随机化通常是通过简单地用负载的重复副本填充内存来实现的,这被称为堆喷涂或jit喷涂[14],[42]。另一个潜在的攻击向量是部分指针覆盖。通过覆盖指针的最低有效字节,可以成功地将其修改为指向附近的地址[43]。
即使所有内容都是随机的且具有非常高的熵(例如,在x64机器上),信息泄漏也会完全破坏这种保护。信息泄露是针对概率技术的主要攻击向量,如图1所示,如果(某种程度上)内存安全没有被强制执行,它们总是可能发生的。
自W⊕X广泛部署以来,运行域名化的重点已经变成了代码。如图1中的步骤6所示,代码重用攻击成为主要威胁。为了增加代码位置的熵,研究者提出了函数[44]和函数[45]内部指令的排列。自转换指令重定位(STIR)[46]在启动时对二进制文件的基本块进行随机重排序。虽然这些技术使ROP攻击更加困难,但它们通常不能防止返回到libc攻击。这些技术还假设代码重用(ROP)利用需要几个小工具,在这种情况下提供的熵足够高。然而,有时一个小工具就足以实施一次成功的攻击。通过信息泄露,单个指令、小工具或功能的地址相对容易获得。

在地址空间和数据空间随机化之间的边界地带,有一种技术是指针加密。Cowan等人[47]提出了PointGuard,它加密内存中的所有指针,只在它们加载到寄存器之前解密。这种技术可以被认为是ASLR的对偶,因为它也在地址中引入了熵,但在“数据空间”中:它加密存储的地址,即指针的值。为了加密指针,PointGuard对所有指针使用相同键的异或操作。由于它只使用了一个密钥,因此通过从内存中泄漏一个已知的加密指针,可以很容易地恢复该密钥[12]。然而,阻止PointGuard被广泛采用的主要原因是它既不兼容二进制代码,也不兼容源代码。

Data Space Randomization
Bhatkar和Sekar引入了数据空间随机化(DSR)[48],以克服PointGuard的弱点,提供更强的保护。与PointGuard类似,DSR随机存储在内存中的数据表示,而不是位置。它加密所有变量,而不仅仅是指针,并且使用不同的密钥。对于变量v,生成一个键或掩码mv。当变量被存储和从内存中加载时,代码被用来屏蔽和取消屏蔽变量。由于多个变量可以通过同一个指针解引用来存储和加载,因此等价的“指向”集合中的变量必须使用相同的键。这些集合的计算需要在插装之前进行静态指针分析。这种保护更加强大,因为对所有变量进行加密不仅可以防止控制流被劫持,而且还可以防止仅针对数据的攻击。此外,使用多个键可以防止PointGurad案例中描述的微小信息泄漏,但不是所有情况下都是[12]。
在自定义基准测试中,DSR的平均开销为15%。解决方案不兼容二进制。受保护的二进制文件将与未修改的库不兼容。此外,每当需要点到分析时,模块化将是一个问题。不同的模块不能单独处理,因为点到图必须全局计算。为了克服这个问题,作者提出计算部分点到图的独立模块,并把全局图的计算留给动态链接器。

内存安全

强制内存安全停止所有内存破坏利用。为了完整的记忆安全,空间和时间错误必须防止没有假阴性。类型安全语言通过在数组访问时检查对象边界和使用自动垃圾收集(程序员不能显式地销毁对象)来强制空间和时间安全。我们的重点是通过嵌入低级引用监视器来转换现有的不安全代码,以实施类似的策略。插装可以在源代码、中间表示或二进制级别。

A.带有指针边界的空间安全
实现完全空间安全的唯一方法是跟踪指针的边界(它可以指向的最低和最高有效地址)。cured[49]和Cyclone[50]通过将指针表示扩展为包含额外信息的结构来使用“胖指针”。不幸的是,这些系统需要源代码注释,因此对于大型代码库来说是不切实际的。此外,改变指针表示会改变内存布局,这会破坏二进制兼容性。
SoftBound[51]通过从指针拆分元数据来解决兼容性问题,因此指针表示形式保持不变。使用哈希表或影子内存空间将指针映射到元数据。该代码被用于传播元数据,并在解除指针引用时检查边界。对于新指针,边界设置为所指向对象的起始地址和结束地址。每个指针解引用时的运行时检查确保指针停留在边界内。在利用模型的第二步中,这些检查阻止了所有空间错误。

当且仅当每个模块都受到保护时,基于指针的边界检查能够在没有误报或误报的情况下实现空间安全。正式证明了软边界方法可以提供完整的空间视觉检测。不幸的是,SoftBound的性能开销很高,平均为67%。虽然基于指针的方法(如SoftBound)对不受保护的库提供了有限的兼容性,但很难实现完全的兼容性。例如,考虑一个由受保护模块创建的指针。如果该指针被未受保护的模块修改,相应的元数据就不会更新,从而导致误报。我们在表II中总结了我们在论文结尾所涉及的主要方法的属性。

B.带有物体边界的空间安全
由于基于指针的方法存在兼容性问题,研究人员提出了基于对象的替代方法。这些系统将边界信息与对象关联,而不是将边界信息与指针关联。只知道分配区域的边界不足以在指针解引用时捕获错误,因为我们不知道指针是否指向正确的对象。因此,基于对象的技术主要关注指针算法(模型中的第1步),而不是解引用(第2步)来保护指针的边界。二进制兼容性是可能的,因为元数据只在对象创建和删除时更新。考虑前面的例子。这次的元数据与对象关联,而不是与指针关联。如果指针在未受保护的模块中更新,则元数据不会不同步。

然而,这种方法的一个问题是,只要不解除对指针的引用,指针就可以合法地出界。例如,在对数组进行循环的最后一次迭代时,指针通常会离开数组一次,但不会解引用。第一个用于加强空间安全的二进制兼容对象解决方案是Jones and Kelly (J&K)[52]的GCC补丁,它通过在已分配对象中填充一个额外的字节来解决这个问题。当指针超出一个字节以上时,这仍然会引起假警报。CRED[53]后来为这个问题提供了一个更通用的解决方案。
基于对象的方法的主要问题是它们不能提供完全的空间安全。错误的否定可能会发生,因为对象或结构内部的内存损坏仍然没有被检测到。这是因为C标准允许在struct字段中使用指针运算。例如,memset (&strct 0 sizeof (strct));需要允许指针遍历整个结构。
J&K的性能开销很大,高达11-12倍。CRED将这一开销降低到大约2倍,但通过将检查的数据结构减少为字符数组。Dhurjati等人通过构建一种名为“自动池分配”的技术扩展了J&K的工作。自动池分配基于静态点到分析对内存进行分区。分区允许使用一个单独的、小得多的数据结构来存储每个分区的边界元数据,这可以将开销进一步降低到120%左右。
[56]是目前最快的基于对象的边界检查器之一。BBC用内存换取性能,为每个对象添加填充,使其大小为2的幂,并将其基址对齐为其(填充的)大小的倍数。这个属性允许一个紧凑的边界表示和一种查找对象边界的有效方法。BBC的作者声称,他们的解决方案比之前提到的Dhurjati基于自动池分配的优化方法快两倍左右。BBC的平均性能开销是SPECINT 2000基准的60%。PAriCheck[57]是与BBC同时开发的。它填充对象并将对象对齐为2的幂,以便进行有效的边界检查。它的性能成本和内存开销略好于BCC。
基于对象的方法的动机是与不受保护的库保持兼容,以减少假阳性。如果在未受保护的库中发生分配或回收分配,则通过拦截malloc和free来设置元数据。对于在未受保护的库中创建的每一个其他对象,都使用默认值,从而允许进行任意的运算。

C. Temporal safety
空间安全本身并不能防止所有的漏洞。使用后免费和双免费的漏洞仍然没有被前面讨论的边界检查器检测到。人们提出了许多方法来加强时间安全。
1)特殊的分分器:näıve防止释放后使用漏洞的方法是永远不重用相同的虚拟内存区域,但这将是过度浪费。特殊的内存分配器,如Cling[58],被设计用来在没有显著的内存或性能开销的情况下阻止悬浮指针攻击。Cling是malloc的替代品,它只允许地址空间在相同类型和对齐的对象之间复用。此策略不防止通过悬空指针解除引用,但强制类型安全内存重用,防止所描述的释放后使用攻击。动态内存分配器替换当然不能防止对本地堆栈分配对象的不安全重用。
2)基于对象的方法:也许在实践中最广泛使用的检测内存错误的工具是Valgrind的Memcheck[30]工具和AddressSanitizer[59]。这些工具试图通过标记在影子内存空间中被取消分配的位置来检测释放后使用的bug。通过这种方式可以检测到正在访问一个新取消分配的位置。然而,这种方法在为另一个指针重新分配区域后无法检测到错误:该区域再次注册,无效访问仍然未检测到。上一小节中描述的基于对象的边界检查器提供了相同的保护,因为取消分配会使元数据表中的对象失效。Valgrind作为一个动态翻译器,平均会导致10倍的减速,而Ad- dress杀毒器在编译时通过检测代码导致73%的减速。可靠地检测“释放后使用”攻击的唯一方法是将时间信息与指针而不是对象关联起来。
3)基于指针的方法:使用指针不仅维护边界,而且维护分配信息,从而实现完全内存安全。分配信息告诉所指向的对象是否仍然有效。仅仅与每个指示对象有效性的指针保持额外的位是不够的,因为当对象被释放时,所有指向它的指针都必须被找到并更新。CETS[60]扩展了SoftBound,并通过消除上述näıve思想的冗余来解决这个问题。有效性位只存储在全局字典中的一个位置。每个新对象获得一个唯一标识符,用作字典的键,指针与这个唯一ID相关联。字典的特殊数据结构允许快速简单地使对象失效,也允许快速查找以检查对象的有效性。如果空间安全也被强制执行,那么CETS被正式证明可以强制执行时间安全。换句话说,与SoftBound一起,CETS加强了内存安全。单是执行时间安全的仪器的平均执行开销为48%。在使用SoftBound强制执行完整的内存安全时,SPEC CPU基准测试的开销平均为116%。作为一种基于指针的解决方案,当涉及到不受保护的库时,CETS会遇到与SoftBound相同的二进制兼容性问题。

7通用的攻击防御
数据完整性和数据流完整性是比内存安全更弱的策略。它们旨在防止控制数据(劫持)和非控制数据攻击,但不是防止信息泄露等。前一种策略可以防止数据损坏,而后一种策略可以检测数据损坏。

A. Data Integrity
数据完整性解决方案强制执行近似于spa内存完整性。这些技术集中于最常见的攻击,即通过出界指针进行写操作。它们不强制时间安全,并且它们只保护无效的内存写入,而不是读取。此外,它们仅近似于前面介绍的边界检查器所强制的空间完整性,以最小化性能开销。在所有情况下,这种近似都是由于在测量之前进行的静态指针分析造成的。
1)“安全”对象的完整性:Yong等人[61]提出的技术首先识别“不安全指针”的子集。如果指针可能出界,则被认为是不安全的,例如,因为它是一个计算值(p[i])。静态指针分析识别不安全指针及其指向集,即它们潜在的目标对象。我们将标识的点的并集称为不安全对象。在创建不安全对象时,将其标记在阴影内存区域中的每个字节,并在释放时清除它。在对不安全指针进行写解引用之前插入检查,检查该位置是否在影子内存中标记。这可以防止不属于不安全对象的内存区域中的任何数据被破坏。

这个策略不仅足以保护那些从未通过指针访问过的变量,还足以保护保存的返回地址等。然而,敏感变量仍然可以被识别为不安全对象,因此仍然不受保护。作者还提到,在他们的基准套件中的101个函数指针中,有两个最终属于不安全的对象,这意味着在发生内存错误的情况下,这些值可能会被破坏。由于读取是未检查的,所以当通过坏指针读入寄存器时,任何值都可能被损坏。这也允许某些控制流劫持攻击、程序特定攻击和信息泄漏攻击。

在SPEC 2000基准测试中,Yong的系统报告的运行时开销在50-100%之间变化。未插装的库会引起兼容性问题。如果在转换后的模块中解除对指针的引用,访问由未受保护的模块创建的对象,则会触发误报。对于堆对象,可以通过包装内存分配函数来标记阴影内存中已分配的每个区域来缓解这个问题。

2)从点到集的完整性:前面的技术限制指针解引用,只写不安全的对象。写完整性测试(WIT)[62]进一步加强了上面的策略,通过限制每个指针的解引用,只写对象在它自己的点-设置。当然,指针分析只会导致指针可能指向的对象集合的保守近似。计算出的不同的点到集与不同的ID号相关联,这些ID号用于标记阴影存储区域中的对象。Yong的方法只使用两个id: 1表示不安全的对象,0表示其他所有对象,而WIT将属于不同点的对象标记为具有不同id的集合。
此外,WIT还会检查间接调用,以阻止先前的策略可能导致的劫持攻击。它通过计算间接调用指令所使用的指针的指向集并将它们与id关联来保护间接调用。id被放置在影子内存中,用于有效的代码目标地址,并且,在间接写入的情况下,在每次间接调用之前检查id。与相同ID关联的函数仍然是可互换的。
由于区分不同的点集,WIT执行的策略比Yong的方法更强,但分配给同一个ID的对象仍然容易受到攻击。由于WIT不能保护读取,当读取到寄存器时,数据可能会被破坏,信息泄漏也可能发生。由于缺少读检查,函数指针也可能被损坏。这就是为什么水辅注射检查间接调用,而不是为了检测腐败。检查间接控制转移的目标使水辅注射成为控制流程完整性方法,这在第8 - b节中有介绍。请注意,虽然检查了调用,但没有检查返回。这是因为返回值只能通过写操作被破坏,因为从不会通过解除对指针的引用来读取它们,因此它们被认为是受保护的。由于WIT也不处理时间错误,通过转义的悬空指针重写回邮地址仍然是可能的,但这样的错误在实践中很少出现。

据报告,对于SPEC基准测试而言,WIT的性能开销约为5- 25%。这种方法不是二进制兼容的。使用非检测库可能会创建错误警报,因为它们在分配时不维护对象id。与DSR或其他处理不同点到集的解决方案一样,模块化也是一个问题,因为得到的id依赖于全局点到集图。当WIT在编译时工作时,BinArmor[63]的目标是对二进制重写实施类似的策略。由于策略所需的指针分析在二进制文件中是不可行的,因此系统试图通过运行和跟踪带有各种输入的程序来动态地识别潜在的不安全解引用及其有效目标。这种方法既不能保证没有假阴性,也不能保证没有假阳性,其性能开销可能高达180%。

B. Data-flow Integrity
由Castro等人[64]提出的数据流完整性(data - flow Integrity, DFI)在数据被使用之前通过检查读取指令来检测数据的损坏。DFI根据写读位置的最后一条指令来限制读。在程序分析术语中,DFI强制达到定义集。指令的到达定义集是一组指令,它可能最后一次根据控制流图写入(定义)给定指令所使用的值。例如,该策略确保isAdmin变量最后是由源代码定义的写入指令写入的,而不是由某个流氓攻击者控制的写入。或者它确保返回器使用的返回地址最后是由相应的调用指令写入的。DFI还建立在静态点分析的基础上,以便计算全局到达定义集。与WIT类似,得到的到达定义集被分配一个唯一的ID。每个写入内存的位置在影子内存中用写入指令的ID标记。在每次读取之前,检查这个ID是否为静态计算的允许ID集的元素。
前面讨论的解决方案检查每一个间接的存储器写,所以阴影存储器区域被自动保护。相反,数据流完整性策略规定只检测读取。不幸的是,为了保护其元数据的完整性,DFI还必须检查所有间接写入,并确保它们的目标地址在影子内存区域之外。
在SPEC 2000基准测试中,该技术的性能开销在50-100%之间变化。与以前的解决方案类似,它不兼容二进制文件,因为在不受保护的库中缺乏元数据维护可能会导致误报。

VIII. CONTROL-FLOW HIJACK DEFENSES
以下两项政策只集中于空车劫持。代码指针完整性旨在防止代码指针损坏,而控制流完整性检测它。

A.代码指针完整性
虽然某些代码指针的完整性可以而且应该得到保护,但单独强制代码指针完整性是不可行的。不可变代码指针,比如全局偏移表(Global Offset Table)或虚函数表(vtable)中的指针,可以很容易地通过将它们保存在只读内存页中来保护它们。然而,大多数代码指针,如程序员定义的函数指针或保存的返回地址,必须保持可写。此外,即使可以强制内存中所有代码指针的完整性,劫持攻击仍然是可能的,通过利用错误的间接内存读取将错误的值加载到寄存器中。例如,大多数“释放后使用”的漏洞通过悬空指针读取“错误的”虚拟函数表来转移控制流,而这根本不涉及覆盖内存中的代码指针。从这个讨论可以看出,在使用代码指针之前检测代码指针损坏会更好。

B. Control-flow Integrity
控制流完整性(CFI)解决方案执行了一些关于间接控制传输的策略,从而减少了第5步中的劫持攻击。请注意,直接控制转移不能被转移,因此它们不需要保护。
1)动态返回完整性:最著名的控制流劫持攻击是在tack上的“stack smashing”[65]。堆栈粉碎利用局部变量中的缓冲区溢出来覆盖堆栈上的返回地址。堆栈cookie或金丝雀[66]是针对这种攻击提出的第一个解决方案。在返回地址和本地变量之间放置一个秘密值(cookie/canary)。如果返回地址被缓冲区溢出覆盖,cookie也会改变,这是由放置在返回指令之前的检查检测到的。堆栈cookie不能保护间接调用和跳转,容易受到直接覆盖攻击和信息泄露。然而,堆栈cookie很受欢迎,并被广泛部署,因为性能开销可以忽略不计(小于1%),而且没有引入兼容性问题。

影子堆栈[67]可以解决一些金丝雀的问题,如信息泄露和直接覆盖。为了消除对秘密的依赖,保存的返回地址也被推到一个单独的影子堆栈,因此在函数返回时,可以将影子副本与原始返回地址进行比较。简单地复制并在返回之前检查它是否仍然匹配会使攻击更加困难,即使在影子堆栈不受保护的情况下,因为攻击者必须在两个不同的位置破坏返回地址。为了保护影子堆栈本身,RAD[68]提出使用保护页或切换写权限来保护影子堆栈区域。虽然前者不能防止直接覆盖,但后者会导致10倍的减速。为了估计无保护的阴影堆栈机制的性能开销,我们实现了一个LLVM插件,在SPEC2006基准测试中,它的平均开销为5%。影子堆栈机制也必须处理兼容性问题,例如,处理异常。然而,我们相信通过谨慎的实现可以避免误报。

2)静态控制流图完整性:为了防止所有控制流被劫持,不仅要保护返回,而且要保护间接调用和跳转。第七节介绍了水注射如何识别和执行每条呼叫指令的有效目标集(即待设置点)。Abadi等人[69]最初提出了这一思想以及控制流完整性(Control-flow Integrity)一词。他们的工作主要集中在静态地确定调用和函数返回的有效目标,从而强制产生的静态控制流图。与WIT不同,WIT将ID存储在受保护的影子内存中,CFI的作者建议将它们存储在代码本身中,通过将ID放置在目标位置,这样它就可以被代码完整性保护。为了避免兼容性问题,可以将id编码到指令中,如果插入指令,将不会影响代码的语义。在跳转到目标地址之前,将检测调用和返回以检查目标地址是否具有正确的ID。注意,这也需要非可执行数据,以防止伪造有效目标。

至于返回,执行任何静态预先确定的有效目标集比执行影子堆栈执行的动态调用堆栈更弱。在运行时,函数返回总是只有一个正确的目标,但由于函数可以从多个调用点调用,静态确定的集合将把它们都包括为有效目标。
强制唯一的点到集的间接控制传输的另一个问题是模块化支持,就像前面介绍的所有基于指针分析的解决方案一样。精确的点到集合只能全局确定,这使得模块化和动态库重用具有挑战性。这就是为什么该解决方案对于单片内核[70]或管理程序[71]非常有效的主要原因,在这些内核中,每个模块都是静态链接在一起的,但没有被部署到动态链接的应用程序中。一种较弱但更实用的政策是将间接控制转移限制为它们所有点到集的并(参见第VII-A节Yong等人)。原始的CFI实现也使用这种方法,这意味着所有间接调用的函数都使用相同的ID进行标记。这种策略的优点是它甚至不需要指针分析,因为它足以枚举所有地址被获取的函数。这是一种更为保守的策略,但它允许库的模块化转换和交换。对于许多函数,这种策略意味着允许的返回目标集必须包括程序中的所有调用点。由于这过于宽松,作者建议使用阴影堆栈机制来代替检查返回。
Abadi实现的平均性能开销为15%,而最大测量值高达45%。对返回使用影子堆栈机制的实现有额外10%的开销。这个解决方案也不是二进制兼容的,因为它依赖于W⊕X策略,所以在JIT编译的情况下不能强制执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值