《SoK:_Eternal_War_in_Memory》学习总结
萧禾财 Lv4

文章概述

SoK: Eternal War in Memory文章发表于2013年IEEE S&P会议,第一作者是来自Stony Brook University的László Szekeres。本篇文章对(2013年)常见的内存攻击方式和防护措施做了系统的分析和介绍。众所周知,C/C++程序中的内存破坏漏洞是最古老的漏洞之一,针对此问题诞生了众多防御方案,但目前采用的保护措施在实际攻击过程中都可以被解除或绕过。

通过这篇系统化论文,有如下的贡献:

  1. 针对Memory Corruption攻击构建通用模型,基于模型定义了多种可实施的安全策略。
  2. 对比安全策略设计及漏洞利用各阶段特征,发现部分攻击向量未被现有防御机制保护。
  3. 从性能、兼容性和可靠性评估及比较现有防御方案。
  4. 讨论现有防御方案的缺点、未被实际应用的原因,讨论未来新防御方案设计所需满足的要求。

攻击模型

我们根据攻击模型图,查看有哪些攻击和怎么去防范。

9461a3e8a3a01eaf620a7c4385f33619_2_Figure_1

内存攻击

当无效指针(越界指针或悬挂指针) (1)被解引用或读写时(2) 将导致内存安全被破坏(数据破坏,信息泄露),内存安全是各类攻击的根源。

其中解引用无效指针可以分两种情况:

解引用越界指针会导致spatial error(破坏空间安全)

解引用悬挂指针会导致temporal error(破坏时间安全)

无效指针的成因(①)

out-of-bounds pointer

  • 未对allocation failure检查使得null pointer被使用
  • 数组指针递增递减导致buffer overflow/underflow(缓冲区上溢/下溢)
  • indexing bugs(例如:integer overflow, truncation or signedness bug, or incorrect pointer casting)

dangling pointer

  • 异常处理程序处理对象时,释放了指针但未对此重新初始化,指针悬挂。
  • 全局指针指向某函数内的局部变量。(函数调用结束时,局部变量的空间被释放,指针悬挂)这种漏洞被称作useafter-free漏洞。

无效指针解引用的后果(②)

越界指针解引用(读、写)导致Spatial Error

使用指针从越界/攻击者控制区域读取内容,导致Value Corruption,例如:

  • 未检查从攻击者指定变量(直接/间接)读取的值,导致Data Corruption(进而可能导致控制流劫持)
  • 攻击者指定读指针内容被输出,构成信息泄露

使用指针写内容到越界/攻击者控制区域,导致内存覆写,例如:

  • 缓冲区溢出覆写数组指针使其越界,指针越界导致虚表破坏,被篡改的虚表解引用导致控制流劫持
  • Free攻击者控制的指针导致任意地址写
  • 使用越界指针写操作导致信息泄露

Dangling指针解引用(读、写)导致Temporal Error

使用dangling指针读(dangling指针所指新旧对象不匹配),导致攻击者非法访问内存,例如:

  • 当旧对象dangling指针指向新对象(与dangling指针所指内容被修改略有不同),旧对象虚函数被调用时,会查询虚函数,使用新对象值,导致虚函数corruption。此处temporal read error与spatial write error一样导致vtable破坏。
  • 此外,新对象内容在读取旧对象dangling指针操作时泄露。

使用dangling指针写,如越界指针一样破坏新对象中的指针或数据,例如:

  • Dangling指针指向局部变量或栈,对其写操作可能导致(后续新栈帧)返回地址等敏感数据覆写
  • Double-free是use-after-free的特例,悬空指针将被用于调用free()函数,此时新对象的内容将被错误的解释为元数据,这样将可以被任意数据的写

code corruption attack

内存安全破坏(①、②)能够直接修改内存代码(③),从而执行攻击者想要执行得代码(④),此时就会产生 code corruption attack .

代码完整性保护(③)通过将包含代码的内存页设定为只读页,可以很好的防范此类攻击。

但是对于自修改代码(selfmodifying code)和及时编译(Just-In-Time compilation)的情况,此策略就无能为力了。

Instruction Set Randomization (ISR)技术可以减轻代码注入执行可能性和现有代码的破坏。但是由于效率太低,一般不会采用。

Control-flow hijack attack

当执行了代码完整性的策略后,攻击者就会尝试使用内存安全破坏(①、②)修改代码指针(③)指向(恶意的)shellcode/gadgets(④),并让跳转指令目标为被篡改的代码指针(⑤),成功执行shellcode/gadgets(⑥),从而成功劫持控制流。

代码指针完整性保护旨在保护代码指针不被破坏(③)。地址空间随机化技术(Address Space Randomization)让攻击者难以将代码指针指向shellcode/gadgets(④)。控制流完整性(Control-flow Integrity)保护旨在检测间接跳转处的corruption(⑤)。Non-executable Data(不可执行数据,W⊕X,程序内容要么可写,要么可执行)和指令集随机化(ISR)拒绝shellcode/gadgets被执行(⑥)。

Jump Oriented Programming(面向跳转编程,包括”return-to-libc” attack,Return Oriented Programming)通过重写现存的代码并将其通过指令链接起来并执行,可以绕过W⊕X和ISR的保护(⑥)

此外若要让攻击更具意义,攻击者通常需要进行系统调用、提权(例如文件访问)等。权限、强制访问控制、沙箱策略(SFI、 XFI、Native Client)能限制攻击者所攻陷程序的能力,但本文所关注的是如何保护程序不被攻陷。

Data-only attack

内存安全破坏(①、②)能篡改安全关键数据变量(包括代码、代码指针)(当然安全关键是语义定义,实际上需要保护所有变量完整性)(③),让数据修改为攻击者指定值(④),当该数据被使用时(⑤),data-only attack成功。

数据完整性(自然包括代码、代码指针完整性保护)保护旨在保护数据不被篡改(③),数据空间随机化(包括地址空间随机化)对数据引入额外的熵值,使攻击者无法使用指定数据(无法正确验证)(④),数据流完整性保护(包括控制流完整性保护)检测数据流是否被破坏。

Information leak

内存安全破坏(①、②)能输出(③)攻击者指定的内存数据(④)。

信息泄露能被用来绕过ASLR(Address Space Layout Randomization 技术就是通过加载程序的时候不再使用固定的基址加载,从而干扰shellcode定位的一种保护机制。)等概率性防御技术。

全面的数据空间随机化旨在防止攻击者成功地将数据泄露(④)

常用的防御方案及实际的漏洞利用

现在最常用的的防范措施有:stack smashing protection, DEP/W⊕X and ASLR.

Stack smashing protection / 控制流完整性保护方案(⑤)

Stack smashing protection,SafeSEH、SEHOP可以在间接跳转时检查代码指针(返回地址和异常处理句柄,此例未涉及保护虚表和函数指针)

但是他们有缺点,由于他们只保护一些特殊的指针(即堆栈上返回地址和异常处理程序指针),所以可以被绕过的。

DEP/W⊕X / 跳转目标权限控制(⑥)

DEP/W⊕X能防御代码注入攻击。

DEP无法防御ROP攻击。

ASLR / 避免定位跳转目标(④)

ASLR利用地址空间随机化技术避免攻击者定位恶意代码。

ASLR的代码地址由于低熵等原因仍可能被攻击者预测,同时信息泄露页能泄露ASLR之后的代码地址。

研究方法及评价指标

9461a3e8a3a01eaf620a7c4385f33619_12_Table_II

保护方案实用性评价指标

保护能力

一个保护技术的有效性取决于具体实施的策略(Enforced policy),体现在其所能防御的攻击,其精确度包括False negatives(漏报率)和False positives(误报率,实用环境中难以允许误报存在)。

成本

  • 性能开销。性能开销增幅低于5%或10%的方案容易被广泛采用。性能测试除I/O-bound benchmark外,更应关注(攻击所关注的)CPU-bound client-side benchmark。
  • 内存开销。其引入增长的元数据会造成内存开销,某些利用影子内存的防御机制会使内存开销翻倍。

兼容性

  • 源代码级别兼容性(Source compatibility)。好的防御方案应不需要人工介入来调整源代码(移植或注释已有程序)
  • 二进制级别兼容性(Binary compatibility)。好的防御方案应当能直接兼容已有的二进制程序(已经编译的程序),这有利于推广部署。此外,有的库文件受完整性保护,不允许二进制代码修改。
  • 模块化支持(Modularity support)。由于动态库等模块非常重要,好的基于编译器的防御方案应当能独立编译并保护单独的模块,好的二进制重写保护方案应该支持单独加固单个二进制文件。

保护方法

现行的保护措施可以分两类:概率性保护,确定性保护。

概率性防御方法

基于随机化和加密技术,例如:

Instruction Set Randomization(ISR)

指令集随机化基本出发点是阻止恶意代码顺利完成攻击。在编译时对可执行程序的机器码进行加密操作,在指令执行前,再对每条加
密过的指令进行解密。通过这个过程,使得攻击者植入的攻击代码不能完成预期的功能。

指令随机化方法对于代码注入式攻击有很好的防御效果,但是由于使用虚拟机导致负荷过重,另外对数据流攻击无能为力(数据流攻击对象是并没有加密的数据),所以又有了数据随机化(DSR)方法的诞生。其次非可执行页面权限的应用也可很好的防止shellcode注入。

Address Space Randomization(ASR)

地址空间随机化 (ASR) 通过随机化代码和数据的位置以及潜在的有效负载地址来减轻控制流劫持攻击。

地址空间布局随机化(Address Space Layout Randomization)技术是现在最表现突出的ASR技术。

旨在通过随机化(堆、栈、代码段、库)避免间接跳转目标为攻击负载。如非全部代码数据段被随机化,ASR可能被绕过,实际应用中除库以外的模块往往未开启PIE(位置无关的可执行文件)以规避10%的性能损失。32位系统由于低熵难抵抗暴力攻击、去随机化(如堆喷射、JIT喷射)攻击和指针覆写,64位系统高熵ASR能被信息泄露绕过。

由于W⊕X广泛部署,(可执行的)攻击负载几乎只能存在于代码段。对此,函数级别、指令级别的随机化被用于增加ROP(gadgets获取)难度(但对return-to-libc无效)。但信息泄露仍能轻易获取指令、gadget及函数的地址,从而导致ROP攻击成功。

Data Space Randomization(DSR)

数据空间随机化 (DSR) 通过随机化(加密)内存内容(利用不同的密钥加密所有的变量,指针)的方式减轻所有类型攻击。

DSR的方案的平均开销为15%,但不支持二进制兼容,不支持模块化处理。即使用了DSR技术的模块不能兼容未保护的模块。

确定性防御方法

Reference Monitor检查策略

通过观察程序的执行,并在程序即将违反给定的安全策略时停止它。

包括:

  • High-level,例如在syscall层面施加的文件系统权限检查
  • Low-level,例如内存安全及控制流完整性检查

Reference Monitor实现方式包括:

  • 硬件实现,开销低,但不能添加新功能。
  • 动态插桩(插桩指在目标程序嵌入代码)依靠动态二进制插桩技术,在运行过程中插桩检查代码,开销大
  • 静态插桩依靠编译器插桩及静态二进制重写技术,在生成目标程序时插桩检查代码,开销比动态插桩小,比基于硬件实现方案大

内存安全策略

对于完整的内存安全,必须防止出现空间和时间错误,并且不会出现误报现象。

对于类型安全语言通过在数组访问时检查对象边界和使用自动垃圾收集(回收内存空间)来强制执行空间和时间安全。

但是现实是,还有很多利用类型不安全语言写的代码在使用,下面的策略只要是利用引入lowlevel reference monitors保障内存安全。

空间内存安全(边界检测)

  • 使用包含元数据的数据结构表示指针(fat-pointers)并进行边界检测会改变内存布局并使二进制程序难被兼容,而使用哈希表/影子内存记录指针边界元数据的SoftBound方法的程序无法有效兼容不支持该扩展的库(库操作指针后无法更新元数据)(开销67%)。(在指针解引用时检查是否越界②)
  • 将对象边界用元数据表示(只在对象创建销毁时需要更新)并用于越界检测,其关键在于指针运算确保指针指向正确的对象(①)。该方法(BBC)有效兼容二进制库,可减少误报,但导致漏报。(开销60%以上)

时间内存安全

空间安全不能防御Use-after-free 和 double-free 漏洞

  • 使用特殊的动态内存分配器(Special allocators)让被释放的虚存只能被相同类型(及对齐)的对象使用,能一定程度避免时间内存安全破坏。(彻底拒绝虚存重用虽能避免UAF等攻击,但过于浪费。)

  • 基于对象的方法(Object based approaches)使用影子内存记录被释放内存能一定程度保障时间内存安全,但无法避免内存重新被分配使用后的时间安全问题。(类似基于对象的边界检查会记录对象元数据。动态转换器Valgrind造成10倍性能开销,编译时插桩方案AddressSanitizer造成73%性能开销)

  • 基于指针的方法如CETS在全局字典存储每个对象的有效性,并为每个对象分配唯一ID,所有指针与对象ID关联。该方法保护时间安全产生的开销为48%,联合SoftBound(保护空间安全)产生开销为116%。CETS与SoftBound难以有效兼容未受保护库(二进制兼容问题)。

通用防御方法

这类方法的策略(重在阻止攻击)弱于内存安全(重在检测攻击),无法抵抗信息泄露,但性能开销较低。通用体现在其着眼于所有数据(包括代码、指针)

  • 数据完整性保护如Young’s system能防御(如越界)写操作导致的数据破坏。利用影子内存标记不安全(可能越界)指针所指不安全对象(1代表不安全对象,0代表其它),在不安全指针写操作时基于影子内存中标记验证操作地址的确为不安全对象,避免破坏(写操作)不安全对象外的其它内存。缺点是不会检查读操作指针,还有一定的误报率(将敏感变量识别为不安全对象)。开销50%~100%。
  • 指向集合完整性保护(Integrity of points-to sets)如Write Integrity Testing(WIT,写操作完整性测试),基于源码指针分析,对每个指针及所指对象集合分配ID(对间接跳所用指针分配ID并在影子内存记录有效目标地址集合),杜绝写指针指向其对应集合外的地址。开销5%~25%。BinArmor基于动态跟踪指针指向集合(导致漏报、误报),使用二进制重写实施类似WIT的策略,开销180%。
  • 数据流完整性保护 对(如用于指定间接跳目标地址的)读操作指令利用可达指令集(the reaching definition sets)分析数据来源(哪些指令会写操作指定跳转地址),为合法指令指定集合并分配ID,并在读操作时检查目标地址由合法集内的指令赋予。开销50%~100%。

控制流劫持防御

代码指针完整性保护代码指针不被破坏(侧重于防篡改),控制流完整性检测被破坏的指针,共同抵御控制流劫持攻击。

  • 代码指针完整性保护(数据完整性子集)非常重要,但缺乏内存安全将无法有效提供保护,Global Offset Table(全局偏移表)、虚表中指针无需重写,设置只读即可有效保护。其它指针则不然,需要可写性,可被篡改用于控制流劫持。此外,恶意值(有别于指针)的加载(读)可劫持控制流(如UAF中加载新对象的虚表中指针)。
  • 控制流完整性保护
    • Dynamic return integrity。Canary/Stack cookies通过在返回地址和局部变量之间放置一个秘密值检测stack smashing破坏栈帧中返回地址信息(开销不足1%,无兼容性问题)。影子栈使用(未受保护的)影子内存记录栈信息并在return前检查返回地址是否被篡改(开销5%,兼容性问题不大)。RAD保护影子栈安全,10倍开销。
    • 静态控制流图完整性。Abadi CFI构建静态控制流图保护函数调用和函数返回,在跳转目标处(代码完整性保护)标记ID(类似指向集合ID)确保跳转目标合法。W⊕X能助其避免伪造跳转目标(使用恶意数据地址冒充并标记ID)。针对return的静态先验目标集合包含过多目标,弱于影子栈实施动态调用栈。此外将所有指向集合合并的方法,无需指针分析便能记录所有函数地址,支持库的模块化转换和互换,但集合过大,建议采用影子栈保护。开销15%~45%,使用影子栈将额外造成10%开销。无法二进制兼容,W⊕X使其无法用于JIT场景。

总结

只有开销较低且兼容性强的方法才可能被广泛部署。除了基于指针的完全内存安全保护方案,其它方案均没有一个很好的健壮性支持。

由于W⊕X的应用,后续研究更关注ROP攻击。随机化是当时(2013年)比较新颖的方法。但JIT的流行使W⊕X难以应用,(攻击者通过JIT提供恶意脚本)随机化方法也容易被轻易打败。更强的策略如DI、CFI需要被深入研究。

作者建议学术界和工业界频繁交流,研究员基于开源平台设计新的防御方案~

  • 本文标题:《SoK:_Eternal_War_in_Memory》学习总结
  • 本文作者:萧禾财
  • 创建时间:2022-11-04 16:32:11
  • 本文链接:https://ipartmentxhc.github.io/2022/11/04/《SoK-Eternal-War-in-Memory》学习总结/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!