论文题目: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 2018html
内存错误是致使内存不安全语言(包括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%。web
因为内存不安全语言如C和C++的普及,内存错误是最多见的软件错误之一,尤为是在浏览器和OS内核等大规模软件中。从安全角度来看,内存错误也是最严重的错误之一,它们很容易致使系统崩溃(即拒绝服务攻击),甚至容许对手彻底控制易受攻击的系统(即任意代码执行和权限提高)。在过去的几十年里,人们提出了许多解决方案来防止与内存错误相关的攻击。这些防护技术能够分为两个大的方向:利用缓解技术和内存错误检测器。算法
利用漏洞缓解技术的重点是防止攻击者利用内存错误执行恶意活动。因为这些技术每每具备较低的运行时性能开销(<10%),所以最普遍部署的机制属于这一类,例如数据执行预防(DEP)、地址空间布局随机化(ASLR)和控制流完整性(CFI)。可是,它们的局限性也很明显:能够经过新的利用技术轻松地绕过它们-从代码注入到面向返回的程序到高级ROP攻击到纯数据攻击,攻击者始终可以寻找新的创造性方法来利用内存错误。docker
另外一方面,存储器错误检测器旨在检测根本缘由。因为这些检测技术能够在第一时间阻止攻击的发生,所以它们可以防止全部与内存错误相关的攻击。不幸的是,实现这一目标并不是没有代价。首先,这些技术每每具备相对较高的性能开销,从基于硬件的方法的30%到基于纯软件的方法的100%不等。第二,其中一些在支持C和
C ++的所有语言功能方面存在困难。shell
尽管存在缺点,咱们相信内存错误检测器是从根本上防止内存错误相关攻击的更有但愿的方向。更具体地说,为了战胜现有的攻击,咱们已经积累了大量的漏洞缓解技术。例如,最新的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%。bash
利用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已经足够成熟,能够发布并用于实际应用。
内存错误通常有两种类型:空间错误和时间错误。空间内存错误是指访问已分配内存边界以外的内存。此类错误多是由许多类型的软件错误引发的,包括缺乏边界检查,边界检查不正确,内存分配不足,类型混淆等。临时内存错误可进一步分为两个子类别:读取未初始化的内存和访问已释放的内存。读取未初始化的内存可能会出现问题,由于其值要么不可预测,要么能够被攻击者控制。访问已释放的内存是有问题的,由于能够从新分配已释放的内存以存储另外一个可能由攻击者控制的内存对象。
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中详细描述基于指针的检测。
基于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的用户级写时拷贝方案。
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。
为了实现上述思想,咱们设计了一个新的用户空间分配器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)。特别是,因为几乎没有执行页别名的优点,所以物理页将被这些对象彻底占用,所以没有空间用于别名,所以咱们直接从物理页分配这些对象,而无需经过自由列表进行搜索。
咱们须要额外的机制(咱们称之为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大小)。
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中的从新定位表,将全部引用(指向原始全局对象)从新定位到堆空间中从新分配的引用。虽然这个全局对象的方案看起来性能昂贵,但咱们强调这个过程只须要执行一次,所以它只会给程序加载过程增长一次性的固定成本。
如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特定的测试用例。
llvm/projects/compiler-rt/test/meds/TestCases/ASan
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
$ clang -fsanitize=meds -mllvm -meds-globals=1 -mcmodel=large -Wl,-emit-relocs test.c -o test
$ 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具备良好的兼容性和可检测性,而且运行时开销适中。