20199125 2019-2020-2 《网络攻防实践》综合实践
论文信息
论文题目:Enhancing Memory Error Detection for Large-Scale Applications and Fuzz Testing
作者:Wookhyun Han (KAIST)Byunggill Joe(KAIST)Byoungyoung Lee(Purdue University)Chengyu Song(University of California,Riverside)Insik Shin(KAIST)
所属会议:Network and Distributed Systems Security (NDSS) Symposium 2018
摘要
内存错误是导致内存不安全语言(包括C和C ++)流行的最常见漏洞之一。一旦被利用,它很容易导致系统崩溃(即,拒绝服务攻击)或使对手完全破坏受害系统。本文提出了一种实用的内存错误检测器MEDS。 MEDS通过近似两个理想的特性(称为无限间隙和无限堆)显着增强了其检测能力。 MEDS的近似无限间隙在对象之间建立了较大的不可访问的存储区域(即4 MB),并且近似无限堆使MEDS可以充分利用虚拟地址空间(即45位存储空间)。 MEDS实现这些特性的关键思想是一种新颖的用户空间内存分配机制MEDSALLOC。 MEDSALLOC利用页面别名机制,该机制允许MEDS最大化虚拟内存空间利用率,但最小化物理内存使用量。为了突出MEDS的检测功能和实际影响,我们进行了评估,然后与Google最新的检测工具AddressSanitizer进行了比较。 MEDS对Chrome和Firefox中的四个真实漏洞的检测率提高了三倍。更重要的是,当用于模糊测试时,在相同的测试时间内,MEDS能够比AddressSanitizer识别出68.3%的内存错误,突出了其在软件测试领域的实际情况。在性能开销方面,与包括Chrome,Firefox,Apache,Nginx和OpenSSL在内的实际应用程序的本机执行和AddressSanitizer相比,MEDS分别降低了108%和86%。
一、导言
由于内存不安全语言如C和C++的普及,内存错误是最常见的软件错误之一,尤其是在浏览器和OS内核等大规模软件中。从安全角度来看,内存错误也是最严重的错误之一,它们很容易导致系统崩溃(即拒绝服务攻击),甚至允许对手完全控制易受攻击的系统(即任意代码执行和权限提升)。在过去的几十年里,人们提出了许多解决方案来防止与内存错误相关的攻击。这些防御技术可以分为两个大的方向:利用缓解技术和内存错误检测器。
利用漏洞缓解技术的重点是防止攻击者利用内存错误执行恶意活动。由于这些技术往往具有较低的运行时性能开销(<10%),因此最广泛部署的机制属于这一类,例如数据执行预防(DEP)、地址空间布局随机化(ASLR)和控制流完整性(CFI)。但是,它们的局限性也很明显:可以通过新的利用技术轻松地绕过它们-从代码注入到面向返回的程序到高级ROP攻击到纯数据攻击,攻击者始终能够寻找新的创造性方法来利用内存错误。
另一方面,存储器错误检测器旨在检测根本原因。由于这些检测技术可以在第一时间阻止攻击的发生,因此它们能够防止所有与内存错误相关的攻击。不幸的是,实现这一目标并非没有代价。首先,这些技术往往具有相对较高的性能开销,从基于硬件的方法的30%到基于纯软件的方法的100%不等。第二,其中一些在支持C和
C ++的全部语言功能方面存在困难。
尽管存在缺点,我们相信内存错误检测器是从根本上防止内存错误相关攻击的更有希望的方向。更具体地说,为了打败现有的攻击,我们已经积累了大量的漏洞缓解技术。例如,最新的Windows系统(Windows 10)部署了以下漏洞缓解技术:DEP、ASLR、stack guard、control flow guard、return flow guard等。但是,由于攻击者现在转向纯数据攻击和信息泄漏攻击,必须添加新的缓解技术。问题是,即使每个单独的缓解技术的性能开销可能很低,但累积的开销仍然可能很高,特别是对于击败纯数据攻击(例如,数据流完整性)和信息泄漏(例如,动态信息流跟踪)。与内存错误检测器相比,它们仍然不能提供强大的安全保证。
由于上述原因,我们提出了MEDS,该系统可增强基于Redzone的内存错误检测的可检测性。特别是,现有的内存错误检测器可以分为两个方向:基于Redzone和基于指针。基于Redzone的检测器在内存对象之间插入未定义的内存,并禁止访问未定义的区域。基于指针的检测器跟踪每个指针的功能,并在访问对象时检查该功能。通常,基于Redzone的检测器与C / C ++功能具有更好的兼容性,但其检测内存错误的能力不如基于指针的检测器。
MEDS背后的关键思想是,可以利用完整的64位虚拟地址空间来近似分配的内存区域之间的“无限”间隙(以便检测空间错误)和“无限”堆(以避免避免重用已释放的内存,并且检测时间错误)。更重要的是,MEDS在不增加物理内存使用量的情况下实现了这一目标。 MEDS通过新的内存分配器MEDSALLOC实现了这一想法。
MEDSALLOC使用用户空间页面别名机制(即,物理和虚拟内存页面之间的别名)来管理内存池,从而在最大程度地减少虚拟内存使用的同时最大化虚拟地址的利用率。与基于最新的基于Redzone的内存错误检测器AddressSanitizer 相比,“无限”的差距使MEDS能够检测更多的空间内存错误,这些错误表现出更大的出界偏移。 MEDS还可以更有效地检测时间内存错误,因为它充分利用了可用的虚拟地址空间进行分配,因此虚拟地址不太可能被重用。
我们已经实现了基于LLVM工具链的MEDS,并在各种实际大型应用程序(包括Chrome、Firefox、Apache、Nginx和OpenSSL)上评估了MEDS的原型。首先,我们在一组单元测试中评估了MEDS,MEDS能够正确地检测所有测试的内存错误。然后,我们使用Chrome和Firefox中的四个实际内存损坏漏洞测试MEDS,MEDS的检测率是由Google开发的最先进的内存错误检测工具AddressSanitizer(ASAN)的三倍。MEDS平均带来了中等的运行开销,MEDS的速度降低了108%,与ASAN相当;它使用的内存增加了212%。
利用MEDS的检测能力,它可以用于检测生产服务器或模糊基础设施中的潜在内存错误,同样,ASAN已经被广泛地部署和使用。为了清楚地说明这一点,我们使用AFL进行了模糊测试,目标是12个实际应用程序。综上所述,MEDS在帮助大多数目标应用程序的模糊化内存错误检测能力方面明显优于ASAN—平均提高68.3%,范围从1%到256%,具体取决于应用程序(如表四所示)。考虑到AFL和ASAN在实际模糊测试中的巨大普及,这些结果也表明了MEDS的强大实际影响。与AFL结合使用,MEDS可以增强模糊测试的检测能力,明显优于目前最先进的记忆错误检测工具ASAN。我们注意到ASAN是GCC(从v4.8开始)和LL VM/Clang(从v3.1开始)两条主线的一部分,许多主要供应商和开源社区在调试和模糊测试方面都非常依赖ASAN。
总之,本文做出了以下贡献:
设计:我们设计MEDS,一种新的增强了检测能力的存储器错误检测器。MEDS的核心是MEDSALLOC,这是一种新的内存分配程序,它(1)充分利用64位虚拟地址空间,在对象之间提供“无限”的间隔,避免重用释放的虚拟地址;以及(2)利用一种新的内存混叠方案来最小化物理内存开销。
实现和评估:我们实现了一个基于LLVM工具链的MEDS原型,并成功地将其应用于一组大型的现实应用程序,包括Chrome、Firefox、Apache、Nginx和OpenSSL。我们评估了MEDS的几个方面,包括(1)它的兼容性,(2)它对人工和真实攻击的检测能力,以及(3)它的运行性能和内存开销。
实际影响:根据我们在模糊测试(使用AFL)中的评估,MEDS在检测记忆错误方面明显优于ASAN。我们计划使用开源MEDS,以便软件供应商和开源社区能够从使用MEDS中受益。如我们的评估所示,MEDS已经足够成熟,可以发布并用于实际应用。
二、背景和挑战
A、 内存错误
内存错误一般有两种类型:空间错误和时间错误。空间内存错误是指访问已分配内存边界之外的内存。此类错误可能是由许多类型的软件错误引起的,包括缺少边界检查,边界检查不正确,内存分配不足,类型混淆等。临时内存错误可进一步分为两个子类别:读取未初始化的内存和访问已释放的内存。读取未初始化的内存可能会出现问题,因为其值要么不可预测,要么可以被攻击者控制。访问已释放的内存是有问题的,因为可以重新分配已释放的内存以存储另一个可能由攻击者控制的内存对象。
Hicks[15]将内存错误的定义形式化为两种类型:
1.对未定义内存的访问:如果内存区域未分配(超出界限)、未初始化或已释放,则该内存区域未定义。虽然这个定义很简单,但并不现实。为了支持这个定义,任何两个分配区域之间的间隔必须是无限的(即无限的间隔),并且释放的内存区域永远不能被重用(即无限堆)。
2.与指针的功能冲突:第二个定义将指针与访问base和end之间的内存的功能相关联。功能只能通过合法操作(如分配)来创建,因此获得的地址是不可伪造的;并且在释放相应的内存区域时被撤销(即没有功能)。内存错误可以定义为访问指针功能之外的内存。
图1:在ASAN中使用Redzone和shadow内存进行基于Redzone的检测。一开始有三个被分配的对象(最左边),然后obj1被释放(中间)。如果隔离区由于重复分配而耗尽,则可以重用释放的空间(最右边)。
根据上述定义,现有的检测内存错误的方法通常可以分为两个方向:(1)基于redzone的检测,它在对象之间插入未定义的内存并检测对未定义区域的访问;(2)基于指针的检测,它跟踪每个指针的功能,并在访问对象时检查功能。两个方向各有利弊:一般来说,基于RealZeon的内存错误检测器与C/C++语言特征和线程模型有较好的兼容性,因此可以应用于浏览器等大规模软件。基于指针的检测器通常存在兼容性问题。例如,SoftBound与某些特定的CPU基准测试不兼容,而且据报道,GCC对Intel MPX(内存保护扩展)的支持也存在兼容性问题。另一方面,基于指针的解决方案通常具有更好的检测能力,因为实现对象之间的无限间隙和从不重用释放的内存是不现实的。因为我们旨在构建一种实用的工具,以利用各种语言功能来支持大规模C / C ++程序,所以我们选择遵循基于redzone的方向,并且本节将重点介绍基于redzone的检测。我们将在§VIII中详细描述基于指针的检测。
B、 基于Redzone的内存错误检测
基于redzone的检测器在有效内存对象之间插入未定义的内存区域(也称为redzone)。然后,这些检测器设置机制来捕获访问redzone的尝试(例如,没有虚拟页面权限),以便能够检测到对该区域的访问。一般来说,基于redzone的方法有两个关键设计因素,即(1)如何设置redzone以提高检测率和(2)如何实际检测对redzone的访问尝试。例如,为了检测时间错误,DieHard[5]及其继承者DieHard用magic值填充新分配的内存和释放的内存,希望以后使用magic值会导致可捕获的错误。它们还会在分配的内存区域周围添加Redzone以检测空间错误。越界读取的捕获方式与检测时间错误的捕获方式相同。通过检查释放内存时是否修改了redzones的magic值来捕获越界写入。分页堆用两个没有访问权限的额外内存页(每个方向一个)包围分配的区域,这样超出限制的访问将触发页面错误。Valgrind[25]使用有效值位和有效地址位来捕获读取未定义内存和越界访问。
AddressSanitizer(ASAN)是迄今为止最成熟的基于redzone的内存错误检测器,Clang和GCC都支持它。它展示了检测精度和性能之间的良好平衡,能够处理大型复杂软件,如Google Chrome和Firefox。ASAN高效的关键在于它如何使用shadow内存[shadow memory]表示Redzone(如图1所示)。shadow memory是一个位向量,显示有效/无效的内存地址。shadow memory中的一个位表示目标应用程序虚拟内存空间中的一个字节,shadow内存中的位0表示有效,1表示无效。ASAN强制所有内存读写操作必须首先引用shadow内存,以检查目标地址的有效性(即shadow内存中的相应位应为0)。为了检测出越界访问,ASAN用Redzone包围所有内存对象(包括堆栈和全局对象)。此外,为了检测释放后的使用情况,ASAN在释放对象时将整个释放区域标记为redzone。然后ASAN保持隔离区的固定大小(默认为256mb),以避免重用释放的内存(即ASAN不会真正释放释放的内存区域,而是将这些区域保持在隔离区中,直到隔离区变满)。例如,当对象obj3被释放时,通过将相应的shadow内存位更新为无效,相应的区域被标记为redzone(如图1-1所示)。这个释放的区域将保留在隔离区内,以避免重复使用。
虽然这种方法类似于Valgrind的有效地址位,但ASAN的特殊shadow内存地址方案使检查速度更快。具体地说,ASAN使用直接映射方案来定位影子存储器,因为它只需对虚拟地址执行位移操作来获得相应的shadow存储器位置。这实际上需要为shadow内存保留一定的虚拟地址空间,但由于定位相应的shadow内存只需要简单的位移位指令,因此效率很高。ASAN在实践中已经显示出与现有代码非常好的兼容性。它在支持像浏览器这样的大型软件方面没有问题,它是谷歌基于云的模糊化平台的默认内存错误检测器。
现有Redzone探测器的局限性。如前所述,基于redzone的内存错误检测器的可检测性取决于(1)对象之间的redzone有多大;(2)释放的对象作为redzone保留多长时间。具体地说,如果越界访问落入另一个分配的内存区域,或者在空闲访问落入重新分配的内存区域之后使用,则无法检测到错误。不幸的是,现有的基于redzone的内存错误检测器都不能正确地实现或近似无限间隙和无限堆的需求,因此它们的可检测性受到限制。例如,默认情况下,ASAN在16字节和2048字节的范围内设置redzone,跳过这个redzone可以很容易地绕过它。此外,考虑到堆的无限大,其隔离区的默认大小只有256MB;因此,如果程序保持(或攻击者诱使程序保持)分配内存对象以强制重用,则也可以绕过对时间错误的检测。
为了阐明这些局限性,图1展示了内存布局及其通过影子内存执行的redzone。开头有三个分配的对象,obj1,obj2和obj3(最左侧)。在此设置下,假设程序执行了指针算术运算,即p = p + idx,其中p是最初指向obj1基址(即圈1)的指针,而idx是整数变量。然后进一步假设程序使用p取消引用。通过检查影子内存位,ASAN可以分别正确地确定当idx为零(即圈1)时解除引用是有效的,而当idx是obj1的大小(即圈2)时取消引用是无效的。但是,如果idx大于obj1的大小,则可以根据影子存储位允许取消引用,尽管不应这样做(即圈3)。此外,尽管将释放的obj1区域保留在隔离区中(即黑圈1之后),但是如果由于重复分配而耗尽了隔离区,则可以重新使用该区域(即,在黑圈2之后可用于objX)。因此,如果在释放obj1之后使用p进行另一个内存取消引用,则可能导致使用后释放(即圈4)。
为了了解这个限制的真实含义,我们还测试了四个真实的漏洞,发现ASAN很容易被绕过(见表一)。我们注意到,在ASAN中扩大这些参数从根本上来说是一个挑战,因为它将显著增加内存使用。
三、问题范围和目标
问题范围:本工程重点讨论了用户空间C/C++程序内存错误检测问题。我们假设操作系统内核、所有固件和所有硬件都是可信计算基础(TCB)。我们不考虑针对我们的TCB或从我们的TCB内部发起攻击。我们也不考虑攻击除内存错误或其他语言中的内存错误以外的漏洞(例如,程序集和动态生成的代码)。我们不限制目标程序可以使用哪种语言功能,也不限制内存错误可能发生的位置该漏洞可能存在于主可执行文件或任何链接库中。我们也不限制攻击者如何利用该漏洞。
目标:正如第二节所讨论的,不同的记忆检测器在检测记忆错误方面有不同的能力,其中一些检测器只能检测空间错误,一些检测器可以检测空闲但未初始化的记忆后的使用,一些检测器可以检测所有类型的错误。它们的检测率也各不相同,有的只能提供概率检测,有的可以提供确定性检测,但可以绕过,有的可以检测出它们能检测到的所有错误的发生,从而可以提供强大的内存安全保证。
在这项工作中,我们的目的是提高对大规模C/C++程序内存错误的检测能力。这个声明有两个目标。首先,我们的目标是处理大型程序,如流行的服务器应用程序和浏览器。我们之所以选择它们作为目标,是因为它们的重要性和安全解决方案必须切实可行才能产生实际影响的信念。其次,我们希望为大型程序提供比现有解决方案更好的可检测性。然而,提供较低的运行时性能开销并不是我们的主要目标,我们将尽力降低性能开销,但是当可检测性和性能之间存在权衡时,我们将选择可检测性。
评估指标:鉴于目前的现状,为了实现我们的目标,我们可以尝试解决基于指针的解决方案的兼容性问题,或者尝试提高基于redzone的解决方案的可检测性。这项工作探索了第二个方向,我们的评估指标是:(1)MEDS必须能够运行所有的程序,最先进的重分区解决方案,ASAN可以处理;(2)MEDS必须能够检测出比ASAN更多的内存错误;(3)运行时性能和内存开销必须与ASAN相当。
四、 设计
本节介绍MEDS的设计。A部分说明了MEDS的设计概况。然后,B部分引入了MEDSALLOC,一种具有页面别名的新内存分配器。然后,C部分描述了MEDS如何管理和执行不可访问的内存区域redzone。D部分描述了如何通过MEDSALLOC分配所有内存对象(包括堆、堆栈和全局对象),以便MEDS全面地为所有类型的内存对象提供redzone。最后,E部分给出了用于MEDS的用户级写时拷贝方案。
A、概述
MEDS采用基于redzone的方法来检测内存错误,因为它提供了两个不同方向之间的最佳兼容性(§II)。顾名思义,基于redzone的方法通过在内存对象之间插入redzone(未定义的内存区域)并将释放的内存对象标记为redzone来检测内存错误。因此,基于redzone的内存错误检测器的可检测性取决于它能多接近两个理想属性:
P1:无限间隙:检测所有的空间错误,两个内存对象之间的Redzone必须是无限的,这样,没有绑定的访问将始终落入Redzone。
P2:无限堆:为了检测所有暂时性错误,必须始终从新的虚拟地址分配一个新的内存对象,这样在执行期间释放的区域(redzone)就不会被重用。
不幸的是,由于当前计算体系结构所施加的硬件资源(物理和虚拟内存空间)有限,完全满足这些属性是不可行的。因此,最先进的基于redzone的检测工具在安全风险和资源消耗之间做出了实用的设计权衡。例如,默认情况下ASAN[31]只在内存对象之间插入256字节的redzone来检测空间错误,而只维护256 MB的堆隔离区来检测时间错误。放大这两个参数中的任何一个都会导致不适合大型程序的大量物理内存使用。为了清楚地说明这一点,我们尝试使用这些放大的设置来尝试ASAN:对于redzone大小,ASAN包括硬编码断言和限制这些参数的设计决策,因此我们无法运行;对于隔离区,ASAN在提供较大隔离区大小的情况下,会很快耗尽所有物理内存空间,因为失去记忆而被杀。因此,如果空间内存错误发生在超出redzone大小的范围内,则无法检测到这种内存访问冲突。类似地,当隔离区已满时,释放的内存将被回收,从而导致无法检测到的时间错误。我们在第六节中的评估清楚地表明了这一局限性,因为Chrome和Firefox中的四个现实世界的漏洞通过稍微修改一个输入很容易被绕过。
通过充分利用64位虚拟地址空间,MEDS改进了对这两个理想属性的近似。具体来说,64位虚拟地址空间为我们提供了一个很好的机会:(1)增加对象之间的redzone大小;(2)减少虚拟地址重用。然而,挑战在于如何将物理内存的使用量降到最低。MEDS通过页面别名和redzones的新组合克服了这一挑战。页别名表示虚拟内存页和物理内存页之间的有意别名(即将一组不同的虚拟页映射到同一物理页),这是一种常用的技术,用于减少物理页的使用,例如in copy-on-write(CoW)和same-page merge[4]。但是,使用页面别名的redzone强制通常会显著增加碎片。这是因为内存对象分配的粒度不同于页面访问权限的粒度。也就是说,同一虚拟页中的所有对象必须共享相同的访问权限。这使得在单个虚拟页同时包含有效对象和redzone时执行访问检查变得复杂。如PageHeap[20]所建议的,克服这一问题的一种方法是将所有redzone虚拟页(即仅包含redzone而没有任何有效对象)映射到单个物理页,同时将分配粒度增加到页级别(即在单个虚拟页中最多分配一个对象),代价是生成内部分裂。因此,MEDS的目标是在不浪费物理内存的情况下,过度提供虚拟内存空间,以同时满足P1和P2。为此,我们为MEDS设计了新的Redzone方案。由于MEDS密集地利用了巨大的虚拟地址空间,简单地采用ASAN的基于shadow内存的redzones需要不切实际的物理内存空间来存储shadow内存本身。因此,MEDS协调页面访问权限设置以及基于shadow memory的redzone,以有效地管理和强制所有无效内存空间的redzone。
B、MEDSALLOC:具有页别名的内存分配器
为了实现上述思想,我们设计了一个新的用户空间分配器MEDSALLOC,它维护虚拟和物理页面之间的特殊映射以及redzone设置。使用MEDSALLOC,我们可以为每个内存对象提供虚拟视图,就像它们不与其他对象共享页面一样。因此,当对象被紧密地压缩在物理内存空间中时,那些对象被稀疏地放置在虚拟内存空间中(图2)。这使得MEDS能够在低内存开销的情况下满足P1的要求目标程序现在可以在对象之间看到大的redzone,但是由于redzone实际上没有专门用于redzone的物理内存支持,因此这些只会占用少量内存。值得注意的是,MEDSALLOC只使用shadow内存在子页面级别(红色框)标记redzone,页面级别的间隙(在图2中表示为点)仍然由页面表权限标记。这进一步减少了shadow内存的内存占用。
图2:具有基于shadow内存的redzone的别名内存页
图3: 带有redzone的MEDS页面别名方案的示例
为了满足P2,MEDSALLOC维护虚拟内存空间的分配池,并始终尝试将新分配的对象映射到新的虚拟地址,以便充分利用整个虚拟地址空间以避免地址重用。请注意,MEDSALLOC不需要与ASLR兼容,因为MEDS可以提供比ASLR更强的安全性保证。尽管MEDS是内存错误检测器,但在利用了内存错误之后,ASLR在统计上会有所帮助。因此,MEDSALLOC可以简单的顺序方式利用虚拟地址。
本节的其余部分首先提供有关页面别名机制的详细信息,因为它们是MEDSALLOC的关键启用功能。然后我们将介绍更多关于MEDSALLOC的设计细节,它采用使用全局和本地分配器的两层方案(如图4所示)。
页别名:页别名涉及虚拟内存页和物理内存页之间的有意别名,以便将多个虚拟页映射到同一物理页。使用页面别名,可以有多个内存视图(通过多个虚拟页面)指向同一内存内容(由同一物理页面支持)。实际上,页面别名通常用于减少物理页面的使用,例如写时拷贝(CoW)和同一页面合并。
图4:MEDSALLOC的工作流程。
由于此页面别名机制必须依赖于虚拟内存管理,因此其实现因底层架构和内核而异。对于运行Linux的x86-64体系结构(以及x86),可以通过调用mmap()和mremap()系统调用来实现页面别名。响应mmap()请求,Linux内核创建一个新的虚拟内存页,该页映射到一个新的物理内存页。这里,如果指定了MAP_SHARED标志,内核允许以后共享映射(即,新的物理内存页可以由同一进程中的多个虚拟内存页或其子进程映射)。然后,我们使用mremap在同一进程的虚拟地址空间中创建其他别名虚拟页。假设mmap建立了虚拟页面V1和物理页面P1之间的映射。当调用mremap()时,内核将新的虚拟页V2映射到相同的物理页P1,而不移除V1和P1之间的旧映射,其中(1)old_address和new_address分别指向V1和V2,(2)old_size等于零,以及(3)MAP_FIXED标志被设置。请注意,此行为在手册页中没有记录,但在[35]中有描述。
图3显示了这个别名过程的一个示例。如果用户进程调用设置了MAP_SHARED标志的mmap(),内核将为该进程创建新的虚拟页并返回此类虚拟页的基址圈1。之后,当用户调用mremap(),其中旧的_address参数指向mmap()返回的地址时,内核会在新的_address○2处创建一个别名页。这个别名可以根据用户进程的请求重复多次,内核会返回另一个新的别名虚拟页面圈3。
全局分配器:为了提高分配器的性能(通过减少锁的使用),现代堆分配器有一个全局分配器(即每个进程分配器),它为运行的进程管理可用的虚拟地址空间,分区,然后将虚拟地址空间分配给本地的每个线程分配器(如图所示4-1页)。这里,MEDSALLOC与传统堆分配器设计的关键区别在于,MEDSALLOC从不从内核请求实际的物理内存;相反,它只将虚拟地址分发给本地分配器,并从内核接管管理可用虚拟地址的职责。这种设计选择使MEDS满足P2。当MEDS寻找一个未映射的虚拟空间时,全局分配器不试图重用最近释放的虚拟空间,而是总是从最后一个分配地址开始,并遵循单调的方向,这样它就可以完全循环整个虚拟地址空间,并尽可能晚地延迟地址重用。同样,因为作为内存错误检测器,MEDS提供了比ASLR更强的安全保证,所以MEDSALLOC不需要随机化分配的虚拟地址。
本地分配器:本地分配器(即每个线程分配器)维护从全局分配器分配的虚拟内存页,并将虚拟页映射或别名为适当的物理内存页。也就是说,每个本地分配器实际上从内核提交物理内存页分配。此外,为了使有效的物理内存用于小对象分配(即小于页大小),每个物理内存页都由多个空闲列表管理,这些空闲列表将一个页分成多个内存槽。我们为这个自由列表使用了一个大小类分配方案,类似于tcmalloc[28]-类由分配大小决定,每个类都有自己的自由列表。区别在于,在tcmalloc中,free list用于管理映射的虚拟页(即虚拟物理页对);但在MEDSALLOC中,free list仅管理物理页。本地分配器(1)使用这些空闲列表查找具有适当和空内存槽的物理页以进行分配,(2)从保留池中提取虚拟内存页,以及(3)将虚拟页与物理页别名。
例如,在本地分配器初始化期间(即,在加载目标应用程序之后,在执行任何目标程序代码之前),它在全局分配器的帮助下保留一个虚拟地址块(如256 MB),并创建一个本地虚拟页池(如图4-1所示)。接下来,当从线程接收到分配请求时,本地分配器从本地虚拟页池中选择一个可用的虚拟内存页(如图42所示)。接下来,为了找到一个可用的物理页面,它扫描一个对应于分配大小的空闲列表,并选择一个可用的物理页面,并将其映射到上面的可用虚拟页面(如图4-3所示,它分配了objk)。如果空闲列表是第一次使用,因此没有与之关联的物理页,则本地分配器使用带有(MAP|SHARED|MAP|FIXED)标志的mmap()syscall将新的物理页映射到虚拟页。另一方面,如果空闲列表有一个相关的物理内存,它只需使用页面别名(即mremap)重用该物理页面。在对象分配之后,从本地虚拟页池分配额外的虚拟页,以设置预先配置的大小(例如,1 MB)的redzone,确保P1。
解除分配:解除分配对象时,MEDSALLOC将关联的物理内存页返回到空闲列表。如果物理页与任何活动对象都没有关联(即,当使用物理页的所有对象都被释放时),则从空闲列表中删除物理页。在此之后,MEDSALLOC只需取消映射对象区域,物理页将自动返回内核,因为没有与物理页关联的虚拟页。
图5:MEDS的Redzone管理(每字节粒度)伪代码算法。
图6:内存(de)分配的Redzone管理
优化:MEDS在分配大于页面大小的对象时使用优化方案(即4kb,如果使用的是大页面,则为2mb)。特别是,由于几乎没有执行页别名的优势,因此物理页将被这些对象完全占用,因此没有空间用于别名,因此我们直接从物理页分配这些对象,而无需通过自由列表进行搜索。
C、Redzone管理和执行
我们需要额外的机制(我们称之为redzone管理和强制)来检测空间内存错误。MEDSALLOC本身不提供访问控制。例如,在图4中,使用指向objk的地址,还可以访问其他对象,包括obj1和obj2。为了捕获这样一个违规访问,可以简单地采用ASAN中执行的基于shadow内存的redzone强制。然而,由于MEDS使用比ASAN大得多的Redzone,因此用于维护Redzone的shadow内存使用将导致高内存消耗。由于这个问题,简单的Redzone方案不适用于MEDS治疗。因此,MEDS采用了两种不同的redzone管理方案:页级redzone和子页级redzone,其中只有子页级redzone实际上用shadow memory表示。在下面,我们首先描述MEDS如何管理这两个不同的redzone,然后解释MEDS如何执行redzone(即检测对redzone的内存访问)。
管理Redzone:为了管理Redzone,MEDS基本上在运行时拦截目标应用程序调用的所有分配和释放函数,并更新shadow内存。这里的一个特殊挑战是内存消耗,如果这些Redzone都是使用shadow内存表示的话。也就是说,MEDS通过设计在内存对象之间产生非常大的Redzone。如果MEDS为整个redzone提交专用的shadow内存,那么shadow内存本身将占用大量物理内存。
为了解决这一难题,我们首先将redzone分为两种不同的类型,一种是页面级redzone(即虚拟页面之间的间隙),另一种是子页面级redzone(即页面内的间隙)。然后,我们利用这样一个观察结果,即一个shadow内存页正好控制32 KB内存(即8个虚拟页),这意味着我们的页级redzones(4 MB)消耗128页粒度的影子内存。由于页级redzones中的每个字节都是不可访问的,因此相应的shadow内存页将填充1。但是,我们不需要为页面级的Redzone分配单独的shadow页,我们可以简单地将这些shadow页保留为未映射的,因此对这些shadow页的检查将始终触发一个页面错误,该错误将由我们的信号处理程序捕获。
基于上述观察,MEDS只在shadow内存中保留子页面级的redzone,而页面级的redzone不强制任何物理内存使用。图5显示了有关MEDS如何管理影子内存的伪算法,其中虚拟内存更改的快照如图6所示。在对象分配时(图5-(a)),由于分配的虚拟页被用作页级的redzone(即其对应的shadow内存未映射),将第一个mmap新的物理页从内核映射到其shadow内存页,并将整个页初始化为无效(第8行)。然后,MEDS将相应的shadow内存(即要分配的对象地址范围,从addr到addr+size)设置为有效(第12行)。
在对象解除分配时(图5-(b)),MEDS首先标记使用mprotect无法解除分配的虚拟页(第7行)。在MEDS中,页面级redzones上的此权限设置始终是可行的,因为所有虚拟页面始终与虚拟内存空间中的单个对象独占关联。此外,MEDS没有显式地将相应的shadow内存空间标记为无效,而是将shadow内存空间取消映射,将其标记为Redzone(第10行)。同样,由于关联的shadow内存不可访问,访问此已释放内存区域的任何尝试都将通过页面错误检测到。
图7:在加载和存储指令时使用内存访问检测的Redzone强制(每字节粒度)。
执行Redzone。MEDS通过实施redzones来确保所有的内存访问都是有效的。所有内存访问都得到适当保护的安全保证是,检测到任何访问尝试接触到Redzone,因为(1)MEDS显式检查shadow内存(对于子页级的Redzone)或(2)MEDS隐式捕获页错误事件(对于页级的Redzone)。更具体地说,MEDS检测所有的内存访问指令,包括加载和写入,这样只有在通过shadow内存检查有效性之后才允许访问。
图7说明了MEDS仪器如何加载和存储指令。对于加载指令(图7-(a)),MEDS首先检查shadow内存中要访问的给定地址(第5行)。如果给定的地址指向页级redzones,MEDS将在加载相应的卷影内存位时捕获页错误,因为这样的卷影内存空间是不可访问的。如果shadow内存位已正确加载,但指示无效(即子页级Redzone),则MEDS不允许执行原始加载指令(第6行)。对于这两种违规尝试,无论是通过捕获页面错误事件还是检测无效的卷影内存位,MEDS都会报告有关违规的详细信息,以便开发人员或用户能够轻松了解访问违规的原因。如果该位指示有效,MEDS允许执行原始的加载操作(第7行),这样程序执行语义对于良性加载操作保持不变。存储指令的处理方式与加载。
优化:与ASAN类似,对于memset()和memcpy()这样的内存内部函数,MEDS使用其参数检查其安全性,而不是检查这些内存内部函数中所有重复加载/存储指令的安全性。但是,由于其Redzone较小,在检查缓冲区的开始、结束和中间之后,如果所有检查都成功,ASAN仍然需要检查缓冲区所有字节的shadow值。但是,由于MEDS在对象之间使用的间隔要大得多,因此我们只需要检查开始、结束和对齐良好的字节(例如,4mb对齐,这是MEDS的当前默认redzone大小)。
D、内存对象分配
MEDS使用MEDSALLOC(§IV-B)分配所有内存对象,使得所有内存对象都被近似无限间隙包围,其分配池遵循近似无限堆的概念。通常,根据对象在何处被分配堆、堆栈和全局对象,可以有三种不同类型的内存对象。当每种对象类型通过不同的分配机制时,MEDS按照每个分配类型适当地满足其分配过程,以便使用MEDSALLOC分配所有内存对象。
堆对象:堆对象通过一组有限的运行时函数(例如malloc、calloc等)进行分配。与ASAN类似,我们在这些函数上安装截取钩子,这样MEDS可以控制分配过程。然后,当从用户程序接收到堆分配请求时,MEDS简单地利用MEDSALLOC返回一个别名内存对象。
堆叠对象:堆栈对象在相应函数的堆栈帧内分配。与堆对象不同,MEDS在处理堆栈对象时采用不同的方法,这取决于它们是隐式分配还是显式分配。对于隐式分配的堆栈对象,如返回地址和溢出寄存器,由于对它们的访问总是安全的,MEDS不需要用redzones来保护它们(也称为安全堆栈)。另一方面,MEDS使用MEDSALLOC将显式分配的堆栈对象(即堆栈变量)迁移到堆空间中,以便轻松地利用MEDSALLOC的特性用redzone保护它们。对于函数中的每个堆栈对象,MEDS在相应函数的开头插入运行时函数alloc_stack_obj(size,alignment)。此函数使用给定大小的MEDSALLOC执行动态堆分配,同时观察已分配对象的对齐约束。MEDS还在函数的结尾插入另一个运行时函数free_stack_obj(ptr),它在函数返回之前正确地释放堆栈对象(位于MEDS下的堆空间中)。MEDS还将这个自由运行时函数调用注册到异常处理链,以便在堆栈因异常而展开时释放堆栈对象。这样,MEDS将所有堆栈变量放在heap中,MEDSALLOC始终在heap中执行其分配。
全局对象:与堆栈和堆对象不同,全局对象的地址位于加载程序时。更准确地说,在ELF可执行文件的情况下,ELF加载器映射在ELF格式的程序头部分中指定的虚拟内存页。
利用MEDSALLOC的一个简单的设计决策是为每个全局对象创建别名内存页,同时将加载程序映射的数据页视为压缩的物理内存页。然而,我们发现这在不影响兼容性的情况下是不可行的。因为在Linux内核中实现的内置ELF加载程序在映射数据内存页时总是分配MAP_PRIVATE(而不是分配MAP_SHARED),所以这些内存页不能被别名化。我们可以通过以下两种方法解决此问题:1)使用用户级自定义ELF加载程序而不是使用内置ELF加载程序;2)只需修改内置ELF加载程序以指定MAP_SHARED。这两种解决方法都可能对兼容性产生负面影响。使用自定义加载程序为最终用户设置执行环境可能会很麻烦,或者通常不建议修改底层内核。
因此,为了保持兼容性,MEDS实现了全局对象的用户级重新分配方案。加载目标程序后,在执行程序的原始入口点之前,MEDS枚举全局对象列表,并使用MEDSALLOC重新分配每个全局对象。如果全局对象需要初始化(即具有非零字节的数据),MEDS也会相应地复制这些底层数据。从现在起,全局对象的位置已经迁移到堆空间,MEDS通过引用ELF中的重新定位表,将所有引用(指向原始全局对象)重新定位到堆空间中重新分配的引用。虽然这个全局对象的方案看起来性能昂贵,但我们强调这个过程只需要执行一次,因此它只会给程序加载过程增加一次性的固定成本。
E、用户级写时拷贝(CoW)
如B部分所述,MEDS使用带有MAP_SHARED标志的mmap()syscall将物理页与多个虚拟页别名,但这种方法与Linux内核的CoW方案存在兼容性问题。更具体地说,调用fork()syscall时,子进程与其父进程共享相同的物理页。然后,内核CoW执行延迟复制并取消对修改后的物理页面的共享。但是,映射了MAP_SHARED的页面不适用于CoW,因为内核解释说,父进程和子进程之间应该共享这样的页面。因此,对于受MEDS保护的进程,fork()将破坏进程之间的正常隔离保证。
为了解决这个问题,MEDS设计了一个用户级的copy-onwrite机制。首先,MEDS截获所有类似fork的系统调用。在fork()之前,MEDS将所有由MEDS分配的MAP_SHARED虚拟页标记为不可写。fork()之后,当进程尝试写入任何此类页时,预安装的信号处理程序将通过页面错误捕获该尝试。然后,MEDS分配一个新的物理页,将其映射到一个临时虚拟地址(与MAP_共享),硬拷贝旧物理页中的内容,取消映射旧物理页,将新物理页重新映射到旧虚拟地址,取消映射临时地址中的新物理页,然后将控制权传递回进程以继续写操作。这种机制也可以在内核级别通过添加页面别名的专用标志来实现,但是我们选择实现用户级别的解决方案,以避免安装内核扩展以获得更好的兼容性。
五、 实施
我们已经基于LLVM编译器项目(版本4.0)实现了MEDS的原型。 MEDS在总共10,812行c和c ++代码中实现。总体而言,MEDS将目标应用程序的C或C ++源代码作为输入,并生成可执行文件。仪表模块实现为额外的LLVM传递。运行时库模块基于LLVM中的清理程序例程。所有标准分配和解除分配功能均由MEDSALLOC委托。写入时复制(COW)通过挂接fork()并安装自定义信号处理程序来捕获无效的内存访问尝试来实现。
论文中主要实现的技术包括MEDSALLOC内存分配机制和页内存访问错误捕捉机制。主要基于LLVM编译器项目,项目整体是一个LLVM extra pass,通过对目标项目的C/C++代码进行编译插桩,并生成可执行文件,运行时动态库主要还是基于LLVM的sanitizer规则来做的。MEDSALLOC基本上会把所有的内存分配和回收函数hook,用来完成程序内存对象以及影子内存的分配和监控操作。代码量10,812行。MEDS会与ASLR发送冲突,所以需要关闭ASLR。
构建支持MEDS的编译器
$ make
Build Using Docker
uploading-image-367079.png
用docker build -t 文件名称 . 这个命令构建镜像文件image ,后面那个“.”表示当前目录,之后运行
注意:meds后面的“.”,"."是该命令必须得加的参数,意思是在当前目录下找Dockerfile文件, 勿忘
# build docker image $ docker build -t meds
# run docker image $ docker run --cap-add=SYS_PTRACE -it meds /bin/bash
测试MEDS
MEDS的测试运行原始的ASAN测试用例以及MEDS特定的测试用例。
- 将ASAN的测试用例复制到
llvm/projects/compiler-rt/test/meds/TestCases/ASan
- MEDS特定的测试用例
llvm/projects/compiler-rt/test/meds/TestCases/Meds
运行测试
$ make test
Testing Time: 30.70s Expected Passes : 183 Expected Failures : 1 Unsupported Tests : 50
使用MEDS堆分配以及ASan堆栈和全局构建应用程序
- 给定一个测试程序
test.cc
$ cat > test.cc
int main(int argc, char **argv) { int *a = new int[10]; a[argc * 10] = 1; return 0; }
test.cc
可以使用选项构建-fsanitize=meds
$ ./test
==90589==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x43fff67eb078 at pc 0x0000004f926d bp 0x7fffffffe440 sp 0x7fffffffe438
WRITE of size 4 at 0x43fff67eb078 thread T0
#0 0x4f926c in main (/home/wookhyun/release/meds-release/a.out+0x4f926c)
#1 0x7ffff6b5c82f in __libc_start_main /build/glibc-bfm8X4/glibc-2.23/csu/../csu/libc-start.c:291
#2 0x419cb8 in _start (/home/wookhyun/release/meds-release/a.out+0x419cb8)
Address 0x43fff67eb078 is a wild pointer.
SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/wookhyun/release/meds-release/a.out+0x4f926c) in main
Shadow bytes around the buggy address:
0x08807ecf55b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x08807ecf55c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x08807ecf55d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x08807ecf55e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x08807ecf55f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x08807ecf5600: fa fa fa fa fa fa fa fa fa fa 00 00 00 00 00[fa]
0x08807ecf5610: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x08807ecf5620: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x08807ecf5630: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x08807ecf5640: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x08807ecf5650: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==90589==ABORTING
关于选件
-
fsanitize=meds
:使用MEDS启用堆保护(使用ASAN保护堆栈和全局堆) -
mllvm -meds-stack=1
:使用MEDS启用堆栈保护 -
mllvm -meds-global=1 -mcmodel=large
:使用MEDS启用全局保护- 这也需要
--emit-relocs
在LDFLAGS
- 这也需要
-
示例:使用MEDS保护堆/堆栈并使用ASAN全局保护
$ clang -fsanitize=meds -mllvm -meds-stack=1 test.c -o test
- 示例:使用MEDS保护堆/全局和使用ASAN保护堆栈
$ clang -fsanitize=meds -mllvm -meds-globals=1 -mcmodel=large -Wl,-emit-relocs test.c -o test
- 示例:使用MEDS保护堆/堆栈/全局
$ clang -fsanitize=meds -mllvm -meds-stack=1 -mllvm -meds-globals=1 -mcmodel=large -Wl,--emit-relocs
六、 评价
实验配置:MEDS配置为4 MB的Redzone和80 TB的隔离区。ASAN被配置为具有从16字节到2048字节到redzone2的默认参数,以及256 MB的隔离区参数。如前所述,由于大量使用物理内存,放大ASAN的参数最终会出现内存不足的问题。
实验装置:我们所有的评估都是在Intel(R)Xeon(R)CPU E5-4655 v4@2.50GHz(30MB缓存)和512GB RAM上执行的。我们用Linux 4.4.0 64位运行Ubuntu16.04。我们使用MEDS构建了以下五个应用程序进行评估:Chrome浏览器(58.0.2992.0)、Firefox浏览器(53.0a1)、Apache web服务器(2.4.25)、Nginx web服务器(1.11.8)和OpenSSL库(1.0.1f)。
A、兼容性
MEDS的关键目标之一是在运行目标应用程序时保持兼容性,特别是对于大型商品程序。为了检查这种兼容性,我们运行了受尊敬的供应商提供的基本功能单元测试:Chrome中的2242个测试用例,Firefox中的781个测试用例,Nginx中的1772个测试用例。MED通过了所有这些单元测试,这意味着MED真正满足复杂程序的兼容性要求。
B、针对攻击漏洞的可检测性
回想一下,MEDS通过近似无限间隙和堆的概念来检测内存错误。在本小节中,我们首先在一组导致内存损坏的简单单元测试中测试med的检测能力。然后我们使用realworld漏洞来查看MEDS在实际用例中的检测能力。最后,我们展示了各种各样的度量来证明MEDS近似对无限间隙和堆的有效性。
内存错误单元测试:为了看看MEDS是否能检测到所有不同类型的内存错误,我们运行了LLVM ASAN中可用的一组单元测试。它有50个单元测试用例,包括堆栈溢出、堆溢出、空闲后使用等。除了这些用例之外,为了更好地比较MEDS和ASAN,同时展示MEDS的局限性,我们还增加了以下四个测试:两个堆溢出案例分别访问ASAN或MEDS的redzone(4mb);和两个在空闲情况后使用的堆,分别分配小于或大于隔离区大小。在所有这些测试中,一个简单的易受攻击的程序使用触发内存错误的特定输入运行,如果程序正确停止并报告错误,则测试通过。所有的病例中,除了有一种MEDS可以通过所有的记忆测试。正如预期的那样,这个异常情况是堆溢出访问超过MEDS的redzone大小(4mb)。至于ASAN,由于Redzone和隔离区面积小,未能发现3例病例。根据这些单元测试结果,MEDS的检测能力可以说是ASAN的超级集合。(ASAN取redzone的最小和最大大小。然后,根据分配大小,ASAN在这个最小和最大范围内选择redzone大小。)
朱丽叶测试套件(Juliet Test Suite):NIST提供朱丽叶测试套件[6],该套件是为测试软件保证工具的有效性而开发的。每个测试用例都有两个版本,一个是对有漏洞的坏函数的调用(以便度量误报),另一个是对修补了漏洞的好函数的调用(以便度量误报)。我们特别关注Juliet中与内存损坏相关的测试用例,总共11414个测试用例:3124个堆栈缓冲区溢出测试(CWE 121),3870个堆缓冲区溢出测试(CWE 122),1168个缓冲区包销测试(CWE 124),870个缓冲区过读测试(CWE 126),1168个缓冲区欠读测试(CWE 127),820个双自由度测试(CWE 415),394个测试在空闲后使用(CWE 416)。我们使用MEDS和ASAN编译了所有这些测试用例,并根据可检测性测量了假阳性和假阴性。在大多数情况下,MED和ASAN均显示0假阳性和假阴性。然而,有时这两个测试用例都显示了假阴性,范围从0到288。我们分析了这些假阴性案例的细节,发现这些测试案例涉及随机内存访问(即访问地址是通过随机时间种子函数计算的)。换句话说,这些随机访问可能会跳过这两种方案强制的redzone大小,从而导致误报。
为了更好地比较检测能力并了解其与随机访问相关的实际意义,我们用以下约束修改了这288个实例:(1)再分配1000个对象(目前Juliet测试只为每个测试分配一个对象)和(2)将随机访问限制在堆栈/堆段的范围内。第一个约束考虑实际的运行环境,在这种环境中,大多数实际程序在运行时分配大量内存对象。第二个约束考虑了一般编程实践—实际上指针值主要是从现有对象的地址推导出来的。我们将这些变化应用到288个测试用例中,并运行了一百万次来测量检测概率。我们的结果显示,MEDS检测到98%的这些,而ASAN检测到35%。尽管朱丽叶试验的这种改进可以说是有利于MEDS的,但我们仍然相信MEDS的这种突出的检测概率足以证明它在检测能力上比ASAN有了显著的提高。
检测真实世界的内存错误。为了更好地理解MEDS是否能够真正检测到实际用例中的内存错误,我们针对Chrome和Firefox等流行应用程序中的一组漏洞发起了内存崩溃攻击。对于每个漏洞,我们首先将目标应用程序的源代码回滚到易受攻击的版本,然后使用ASAN和MEDS构建应用程序以比较检测能力。
表一:MEDS对现实世界漏洞的检测能力:S—空间内存错误;T—时间内存错误;W—写入违规;R—读取违规;✓—检测到;▲—部分检测(难以绕过);△—部分检测(易绕过)
如表1所示,MEDS在Chrome和Firefox中增强了对ASAN的空间和时间内存错误的检测能力。事实上,该表不仅证明了MEDS近似无限间隙和堆的有效性,而且也证明了它的局限性。在CVE-2016-1653中,由于该漏洞提供的违规访问范围有限,小于4MB(即小于MEDS的Redzone大小),因此MEDS能够完全检测到,而ASAN则无法。然而,对于其余三种情况,因为它们提供了对指针的完全控制(即,完全的任意内存读写漏洞),MEDS也像ASAN一样被绕过。我们仍然注意到绕开MEDS比ASAN更困难,因此在MEDS中它们分别标记为▲和ASAN中标记为△。
近似的有效性:MEDS通过近似无限的间隙和堆来提高检测能力,但显然由于内存资源有限,应该有一定的上限。因此,我们研究这些限制在检测能力方面的实际影响。特别是,内存访问的偏移大小直接影响基于redzone的检测的有效性。实际上,这个偏移量大小与分配的对象大小密切相关,因为中间指针算法只涉及在同一个对象内移动指针。因此,指针运算可能导致的差异大多小于相关的对象大小。因此,我们测量了我们评估的所有应用程序中每个分配的大小,发现所有对象都小于4MB,这意味着4MB的redzone可以提供相当好的检测能力。此测量还显示了ASAN的局限性,因为11%的对象大于256字节(即ASAN的默认redzone大小),并且访问11%对象的例程可能被滥用以绕过redzone大小。如前文§IV所述,由于内存不足问题,在ASAN中放大此参数不适合大规模应用。值得注意的是,通过严格地将指针算法限制为最大对象大小(即在这些应用程序中为4MB),可以进一步增强MED。这将使MEDS真正达到无限的差距。
MEDS还通过循环64位虚拟内存空间来近似无限堆。更准确地说,单靠MEDS无法充分利用这样的64位空间,但它目前使用80 TB的虚拟内存空间—考虑到x86中47位的用户陆地虚拟地址空间(总计128 TB),它为影子内存预留了16 TB,另外16 TB用于MED的内部内存分配,还有16 TB为Linux保留堆栈。因此,由于MEDS在分配超过80tb的对象后开始重用虚拟内存空间,因此我们尝试从最终用户的角度预测触发此操作所需的时间。具体来说,我们运行了Chrome和Firefox的MEDS应用版本,每5分钟使用同一个标签访问网站;运行Apache和Nginx,每秒处理25000个请求(并发级别为50)。根据我们的运行结果(表二),Chrome、Firefox和Apache分别在49分钟、160分钟和141分钟后开始重用地址空间。我们认为这是一个相当长的时间,不会干扰最终用户的体验,尤其是考虑到大多数用户会频繁关闭并创建新的选项卡时。Nginx很快就耗尽了虚拟地址空间,但它只花了4分钟就被回收了。我们怀疑这是因为Nginx设计用于重复重分配内存。虽然这不是一个理想的情况,但在这种情况下,Nginx进程可以在达到这个虚拟地址回收时间之前频繁地重新生成。
表二:MEDS上虚拟地址回收的频率:H—堆对象别名;HS—堆和堆栈对象别名;HSG对所有对象(包括堆、堆栈和全局对象)进行别名化。注意,对于Chrome和Firefox,ASAN上的第一个虚拟地址重用是在初始化时快速完成的。
表三:具有微基准的MEDS和ASAN的检测性能。
C.模糊测试中的可检测性
为了演示MEDS在执行模糊测试时检测内存错误的有效性,我们使用微基准测试和实际应用程序运行了模糊测试。我们使用American Fuzzy Lop(AFL)作为模糊测试框架[38],它是实践中最受欢迎的模糊测试工具之一。首先使用AFL对目标程序进行检测,以启用其基于反馈的模糊功能,然后对它们与MEDS和ASAN进行检测,以比较检测能力。
模糊化微基准程序:在此评估中,我们开发并测试了两个简单但现实的易受攻击的程序,分别表现出缓冲区溢出或释放后使用漏洞。编写这些测试程序旨在突出MEDS的有效性,特别是在非线性内存违例情况(即,超出Redzone的大小)和具有大量内存分配的时间违例情况(即,超出隔离区的大小)方面)。因此,这些可能并不代表所有内存错误情况的常规检测能力。但是,由于这些易受攻击的代码是从真实世界的漏洞中获取并简化的,因此我们认为此测试在内存错误检测方面仍具有实际意义,我们将在实际应用中进一步展示这些错误。
关于缓冲区溢出漏洞的第一种情况是由分配大小上的整数溢出引起的。它占用画布的宽度和高度并分配画布。之后,程序将偏移量,大小和数据写入画布。计算画布大小时会出现整数溢出。第二种情况有一个售后使用漏洞。最初,它具有一组指针,每个指针都指向一个堆对象。然后,该程序将使用一个整数值k,该值将释放k个对象。释放对象之后,它比释放的对象分配更多的新对象,并尝试访问指向堆的指针之一。
我们已经对微基准测试执行了10次,表三显示了遇到第一次崩溃的平均时间,以及每小时的平均崩溃次数。在我们的微基准测试中,MEDS遇到的第一次崩溃比ASAN早12倍。在使用ASAN运行MEDS首次崩溃时,ASAN通常无法检测到该漏洞。此外,在模糊测试期间,MEDS的平均崩溃次数是ASAN的3倍。结果表明,MEDS可以比ASAN更快地找到目标漏洞。换句话说,MEDS在检测性能方面很有效。
模糊的真实程序:为了清楚地展示MEDS在增强模糊测试功能方面的实际情况,我们还使用实际程序运行AFL。表IV显示了在每个程序模糊测试六个小时时的结果。我们从GitHub和Debian存储库中收集了一组目标应用程序,其受欢迎程度与受欢迎程度对(GitHub中的forks和star的数量)和安装排名(Debian存储库中的26,762个应用程序)相关,分别。这些应用程序都是最新版本,因此从该测试中发现的错误都是新错误,我们已经在与相应的开发社区联系以报告这些问题。应用程序的复杂性用代码行(LoC)表示。执行总数表示绒毛测试六个小时内执行的实例数。由于可以通过许多不同的输入触发相同的内存错误,因此AFL仅使崩溃显示唯一的执行路径,这称为唯一崩溃。
在独特崩溃总数方面:总体MEDS在增强我们运行的所有目标应用程序的模糊检测的内存错误检测功能方面均优于ASAN,平均提高了68.3%,范围从1%到256%。实际上,这些结果特别有趣,因为MEDS在执行速度上并不比ASAN更好(尽管有时MEDS比ASAN更快),因为它很大程度上取决于应用程序的运行时特性(即内存分配行为)。该执行速度可以从执行总数中得出。例如,在PH7的情况下,MEDS的速度要比ASAN慢一些(即慢7%)。与MEDS一起运行时,七个应用程序的速度较慢,但是,在模糊测试期间会发生更多独特的崩溃。使用MEDS运行时,五个应用程序(即lci,picoc,swftools,exifprobe和jhead)更快。其中,就每次执行的唯一崩溃而言,MEDS在两个应用程序(即exifprobe和jhead)中速度较慢。我们怀疑这是因为MEDS到达的时间比ASAN早,AFL在这两个应用程序中探索更多的执行路径时已经饱和。饱和后,MEDS花费了其余的模糊测试周期,比ASAN花费了更多的周期,因为MEDS具有更快的执行速度,而没有发现新的独特崩溃。换句话说,MEDS发现大多数独特的崩溃都比ASAN快,但是在剩余的模糊时间上却没有发现更多的崩溃,因为AFL变得饱和了。对于其余三个应用程序(即lci,picoc和swftools),使用MEDS运行时,每次执行时它们具有更高的唯一崩溃。
表四:对实际应用进行模糊处理以比较ASAN和MEDS的内存错误检测功能。 α表示GitHub中的(叉子数量,星数),β表示Debian人气竞赛的安装排名。使用AFL模糊器将每个应用程序模糊化6个小时[38]。
即使对于在MEDS中执行速度较慢的七个应用程序(即PH7,ImageMagick,wren,espruino,tinyvm,猛禽和metacam),MEDS仍然能够找到比ASAN更多的独特崩溃。这意味着,提供增强的检测功能的优势胜于降低执行速度的劣势,从而使MEDS总体上提高了模糊测试的性能(就发现更多独特的崩溃而言)。
我们相信,这清楚地证明了MEDS在ASAN之上提高了内存错误检测能力。考虑到AFL和ASAN在执行真实世界的模糊测试中的广泛普及,这些结果也表明MEDS的强大实际影响力-与AFL一起使用时,MEDS的原型可以帮助模糊测试过程,其性能远优于最新的内存错误检测工具ASAN。
D.性能开销
MEDS的安全服务显然带有成本,这主要影响两个性能因素:运行速度和物理内存使用率。
运行时速度:造成MEDS运行时速度开销的主要因素是:(1)它执行额外的指令以检查所有内存加载和存储指令; (2)由于MEDS利用了更多的虚拟地址空间,因此会有更多的TLB未命中; (3)每个对象分配都需要调用mremap syscall来进行页面别名。
为了更好地理解这些方面,我们为应用程序运行了基准测试-表V显示了Chrome,Firefox,Apache和Nginx的运行结果,表VI显示了OpenSSL的运行结果。对于Chrome和Firefox,我们使用了Octane基准测试[14];对于Apache和Nginx,我们使用Apache基准测试[13],该基准每秒可处理25,000个请求。对于OpenSSL,我们使用OpenSSL的speed命令通过SHA1 [12]加密内存块。对于每次运行,我们应用了三种不同的MEDS设置,以更好地了解对象覆盖范围对性能的影响。换句话说,带有H的MEDS列表示MEDS保护堆对象(即,所有堆对象均已使用MEDSALLOC分配)。类似地,HS表示堆和堆栈对象,HSG表示所有对象类型,包括堆,堆栈和全局对象。
平均而言,与基线相比,MEDS将MEDS-H的执行速度减慢约27%,将MEDS-HS的执行速度减慢94%,将MEDS-HSG的执行速度减慢108%。首先,随着MEDS增加对象覆盖范围(从堆对象类型到所有对象类型),由于MEDS会丢失更多TLB并调用更多系统调用,因此执行速度逐渐降低。 Nginx的MEDS-H和MEDSHS之间的性能变化尤其明显(即24%至250%)。这是因为Nginx在运行时分配了大量的堆栈对象,这又会为MEDS带来大量的分配(调用函数时)和释放(当函数返回时)。但是,堆栈对象分配不是基线的性能瓶颈,因为它只需要移动堆栈指针即可为对象保留和释放堆栈内存空间。
与ASAN相比,MEDS降低了执行速度约11%,73%和86%。由于MEDS与ASAN相比,在指令化指令方面没有带来可观的开销(即都检查影子内存位),因此我们检查了其他性能因素-TLB未命中(表VII)和被调用的系统调用次数(表VIII)。总体而言,MEDS确实导致了更多的TLB丢失(即,平均比ASAN多499%),并且调用了更多的系统调用(即,平均比ASAN多32倍)。但是,就可检测性而言,我们认为这是MEDS增强安全服务的合理费用。
表五:MEDS的运行时性能(Octane得分,ApacheBench每秒请求数;越高越好)的开销,以及ASAN和比较的基线。总体而言,与基准相比,MEDS的平均执行速度降低了108%,而对ASAN的执行速度则降低了86%。
表六:MEDS的OpenSSL性能(每秒处理的KB数)以及基线和ASAN。较大的块大小可减少ASAN和MEDS的开销。尤其是,块大小对于MEDS的性能至关重要。
表七:运行基准测试时的TLB利用率
表八:运行基准测试时调用的系统调用数
表九:MEDS的物理内存使用,以及用于比较的基线和ASAN。总体而言,MEDS平均使用的物理内存比基准多218%,并且比ASAN使用多68%。
物理内存:MEDS会占用更多的物理内存,因为它保留了影子内存以及页面别名映射信息所需的额外元数据。如表IX中所示,MEDS-H,HS和HSG分别平均比基线多了133%,200%和212%的物理内存使用。特别是,尽管MEDS在OpenSSL中施加了432%,在Apache中施加了301%,但其余四个应用程序平均施加了109%。这是因为OpenSSL中的所有内存分配都是较小的分配(即8到32字节),而MEDS会为每个对象元数据附加8字节以保留别名信息。此外,OpenSSL不会在评估期间取消分配内存对象。因此,对应的影子存储器页面被映射到物理存储器页面。表VII中的TLB未命中也捕获了此运行时特征-OpenSSL激烈地扩展了虚拟地址空间,因此TLB未命中率最高。相反,ASAN平均比基准强加了95%,因为ASAN实际上为Redzone和隔离区分配了物理内存空间。我们相信这证明了页面别名机制的有效性,因为MEDS在利用巨大的虚拟地址空间时不会强加不切实际的物理内存使用。
七、讨论
潜在的用例:在本文中,我们认为MEDS的用例对于增强内存错误检测功能具有普遍意义,因此我们尝试中和MEDS的用例。一种特定的用例是部署MEDS,以减轻大规模应用程序的内存损坏攻击。由于MEDS确实满足兼容性要求(因为它可以运行包括Chrome,Firefox,Nginx和Apache的大型程序)并且与其他检测工具相比增强了检测能力,因此特别适合于检测本身,它非常适合这些情况。但是,MEDS引入的性能开销可能是个问题,因此可能不适用于对性能有严格要求的应用程序。
MEDS的其他用例将增加模糊测试:正如我们在§VI-C中所示,MEDS明显优于最新的内存错误检测工具ASAN。认识到模糊测试的重要性,当今绝大多数供应商在其具有大量计算资源的常规软件开发周期中采用模糊测试。例如,谷歌报告说,他们将专用的数百个虚拟机群集用于模糊测试,该虚拟机同时运行约6,000个Chrome实例。由于MEDS在相同的计算时间下比ASAN能够发现更多的内存错误,因此我们认为MEDS不仅可以节省计算资源,而且可以在开发周期的早期通知内存错误。
内核级别的性能改进支持:本文着重于保持MEDS的兼容性,尤其是在不引入基础操作系统Linux的新功能的情况下。如前所述,如果将来可以利用一些内核更改,则可以进一步提高MEDS的性能。例如,当实现用户级的写时复制(COW)(§IV-E)时,可以修改内核以维护页面别名的特殊标志。这将需要在mremap()系统调用中添加一些其他标志。通过这样做,MEDS不需要实现相对昂贵的用户级COW机制,从而减少了MEDS的运行时开销。作为另一个示例,MEDS必须在加载时间§IV-D时重新分配所有全局对象列表。这是因为内核始终在用于全局对象的内存页面中分配MAP_PRIVATE,从而禁止将其用于页面别名机制。如果我们可以在Linux内核中提供另一个ELF加载程序,它为那些内存页面指定MAP_SHARED,则可以避免此冗余分配阶段。
八、其他相关工作
基于指针的内存错误检测:基于指针的检测技术会跟踪指针功能,并根据这些功能检查内存访问的有效性。根据指针功能的存储位置,基于指针的检测器可以进一步分类为基于胖指针和基于不相交元数据。 CCured是基于胖指针的方法的代表作品,其中不安全(WILD)指针通过与指针本身一起存储的功能进行了扩展。基于软件胖指针的方法的一个缺点是它们破坏了内存布局与不受保护的代码的兼容性,这需要消除特殊的硬件支持(例如CHERI )。 SoftBound是基于不相交元数据的方法的代表作品,其中功能存储在专用表中。尽管这种方法不会破坏对象的内存布局,但是访问元数据通常会更昂贵。英特尔MPX 是最新的英特尔处理器中引入的一项新的基于硬件的安全功能,它实质上是SoftBound的硬件实现。有几项工作和运用了基于指针的方法,开销较低。 SGXBound 使用32位指针值存储对象的上限,而地址上限则存储对象的下限。但是,由于它们假定应用程序在内存有限的Intel SGX上运行,因此它仅使用32位虚拟地址。
基于指针的方法的一个关键限制是它与C / C ++语言功能的有限兼容性。为了正确运行,这些方法必须在指针之间正确传播功能,这对于某些语言功能而言并不容易。结果,它们都遭受了向后兼容性问题,尤其是对于C ++程序。例如,CCured仅支持C的有限功能,而SoftBound的原型无法编译SPEC CPU基准测试套件中的所有C基准测试,因此要支持Chrome和Firefox等大型复杂软件,还有很长的路要走。甚至像英特尔MPX之类的商品功能,在运行Chrome浏览器时也会产生误报。由于此问题与覆盖所有不同的C / C ++语法用例以及对Intel MPX应用优化的困难紧密相关,因此尚不清楚是否可以在不久的将来解决。
内存错误利用和缓解:内存错误(最终)将使攻击者能够执行任意内存读写。然后,攻击者可以利用这些功能来发起不同的攻击。例如,一个简单的堆栈缓冲区溢出错误可能使攻击者可以覆盖(1)使用恶意shellcode的堆栈内容和(2)返回地址,从而在函数返回时导致任意代码执行。基于这些功能的滥用方式,Szekeres等。文献将现有攻击分为四类:代码破坏攻击,控制流劫持攻击,仅数据攻击和信息泄漏。然后针对每种特定的利用策略,开发一套相应的缓解机制。例如,开发了代码完整性测量(例如,代码签名)和数据执行保护(DEP)以克服代码损坏攻击。提出了许多防止控制流劫持攻击的技术,包括堆栈cookie ,影子堆栈,控制流完整性(CFI),vtable指针完整性和代码。指针完整性。缓解技术的问题在于,任意读写功能过于强大,通常使攻击者能够找到一种发起攻击的新方法。
九、结论
本文提出了MEDS,以增强内存错误的可检测性。 MEDS通过利用64位虚拟地址空间来近似无限间隙和无限堆来实现此目的。一种新颖的分配器MEDSALLOC使用页面别名方案来近似上述属性,同时将物理内存开销降至最低。我们对使用大型现实程序的MEDS的评估表明,MEDS具有良好的兼容性和可检测性,并且运行时开销适中。