今天要为大家推荐的文章是来自西北大学邢新宇研究组投稿并发表于USENIX Security 2024的工作ShadowBound: Efficient Heap Memory Protection Through Advanced Metadata Management and Customized Compiler Optimization,文章主要研究软件中的堆内存保护机制。
背景介绍
在软件开发领域中,C和C++等不安全语言的广泛使用引入了大量潜在的漏洞,特别是在堆内存管理方面。尽管现在已有多种防御机制能够对时序(Temporal)和空间(Spatial)内存漏洞进行保护,但这些机制通常由于性能开销大,导致在实际应用中不够实用。例如,AddressSanitizer主要用于漏洞检测和调试,并不能保护软件免受内存漏洞利用的影响。然而,目前已有相对成熟的针对时序内存漏洞的防御机制,如PUMM、FFMalloc和MarkUs等,这些机制不会带来很高的性能开销。
为了应对空间内存漏洞,作者提出了一种新的防御机制——ShadowBound。与其他方法不同,ShadowBound不会改变内存分配算法,因此可以与各种时序内存漏洞防御机制结合使用。ShadowBound通过在指针计算位置插入边界检查,而不是在传统的指针解引用位置进行检查,从而优化了性能。这种方法能够让编译器在指针算术位置进行检查时获取更多上下文信息,从而进行更有效的优化。因此,ShadowBound不仅能够有效防御空间内存漏洞,还能保持较低的性能开销。
Metadata Design
ShadowBound利用影子内存来跟踪每个指针对应的堆内存块的边界。将程序堆内存的每8个字节映射到影子内存中的8个字节。在这种影子内存表示中,前4个字节用于存储从对齐的8个字节到相应堆块起始位置的长度的八分之一;相反,后4个字节用于记录从对齐的8个字节到相应堆块结束位置的长度的八分之一。影子内存的创建和初始化在分配操作之后立即进行。
这种设计的可行性源于两个基本观察:
-
几乎所有主流分配器默认情况下都进行8字节或16字节对齐分配。这种内在行为确保了任何有效的8字节对齐内存地址总是属于同一个内存块。因此,不需要为每个字节维护边界信息。
-
这些分配器的最大单次分配大小限制为8 GB(33位)。基于上述观察,可以推断,任何8字节对齐块与相应块的起始或结束之间的长度总是8的倍数。因此,30位足以存储这些长度信息,并且可以使用8字节来存储两个长度值。
与传统检查方法在指针解引用处插入检查指令不同,ShadowBound在指针算术操作处插入这些检查。LLVM提供了两种类型的指针算术指令。getelementptr(GEP)指令使用基指针和一组索引来计算结果指针。如果这些索引未正确检查,攻击者可以利用GEP执行越界访问。bitcast B 指令 允许将基指针从一种类型转换为另一种类型的结果指针。然而,如果指针不能充分容纳目标类型,则可能导致溢出问题。下图提供了ShadowBound如何检查这两种类型指令的示例。在代码的第3行,尝试将void*指针转换为int*指针。为了确保结果指针至少可以容纳一个整数,ShadowBound在第2行插入了检查。在第6行生成了一个新指针,ShadowBound插入了检查以确保新指针和旧指针位于同一个内存块内。
Compiler Optimization
在实现 ShadowBound 的过程中,作者进行了多项优化,以确保其高效性。第一个优化是运行时驱动的检查消除,这种优化基于一个简单的想法:如果每个堆块有无限空间,越界访问将变得不可能,从而使所有的边界检查变得多余且可消除。这一概念的核心在于运行时环境能够为编译器提供额外信息,使其能够消除静态分析技术无法解决的特定边界检查。通过利用运行时信息,一些优化变得可行,从而使编译器能够移除不必要的边界检查,显著提升系统效率。然而,为每个块分配无限或非常大的空间是不现实的,因为这可能导致高内存开销。因此,ShadowBound选择了一种改进的方法来平衡时间开销和内存开销。具体来说,ShadowBound为每个堆块保留了固定的n字节,称为保留空间。然后,ShadowBound将尝试使用运行时提供的保留空间找到所有可消除的边界检查。
由于ShadowBound为每个指针算术指令插入边界检查。边界检查包含一个基(base)指针参数,并生成一个结果(result)指针。这两个指针用作传递给边界检查的参数。特别地,如果在编译时可以确认结果指针与基指针之间的偏移量小于n字节,并且结果指针将不会在另一个边界检查中用作基指针,ShadowBound可以安全地移除该边界检查。这是因为ShadowBound已经为每个堆块保留了n字节,确保每个可能在边界检查中用作基指针的指针至少有n字节的可用空间。作者用如下示例来说明这一优化是如何工作的。在函数bar中,参数c在第10到12行生成了三个指针,这些指针的偏移量都小于8。如果将n设置为8,ShadowBound可以安全地移除这些检查。然而,不能消除第13行的检查,因为指针c + 1被传递给另一个函数,这表明它可能在该函数中用作基指针进行边界检查。如果ShadowBound消除了已经指向保留空间的指针的边界检查,可能会导致假阴性。
除了这个优化,ShadowBound 还实现了定向边界检查,安全模式识别,元数据提取合并和冗余检查消除等优化手段。定向边界检查通过分析指针偏移量的正负来优化检查次数;安全模式识别则通过识别常见的代码模式来减少不必要的检查;元数据提取合并通过合并来自同一基指针的多个检查以避免重复的内存加载;而冗余检查消除则通过分析指针操作之间的关系来移除不必要的检查。这些优化共同作用,使得 ShadowBound 能够在提供强大安全保障的同时保持较低的性能开销。这些优化的详细细节可以参考论文。
Evaluation
在实验部分,作者对 ShadowBound 进行了全面评估,以验证其有效性和性能。首先,作者通过 34 个实际应用程序的漏洞测试,展示了 ShadowBound 在防止堆内存越界利用方面的有效性,这些漏洞涉及广泛的应用领域,包括服务器、视频编码器、语言解释器、常用库和 UNIX 实用程序。在所有案例中,ShadowBound 成功地防止了漏洞的利用。其次,作者使用 SPEC CPU2017 和 SPEC CPU2006 基准测试套件对 ShadowBound 的性能进行了评估,结果显示其时间开销分别为 5.72% 和 10.58%,内存开销也保持在可接受范围内。作者还在 Nginx、Chakra 和 Chromium 等实际应用中测试了 ShadowBound,验证了其在大型和复杂软件中的实用性和低开销。特别是在 Nginx 的测试中,ShadowBound 仅引入了 6.47% 的平均延迟开销,表明其在高负载网络服务中的实用性。
此外,作者还进行了消融研究,通过逐步禁用各项优化来评估每种优化对性能的贡献,结果表明所有优化共同作用将时间开销从 99.69% 降至 5.72%,显著提高了系统的整体性能。通过这些实验,SHADOWBOUND 展现了其在提供强大堆内存保护的同时,保持较低的性能开销和广泛的实际应用适应性。
原文链接:https://www.dataisland.org/paper/shadowbound.pdf
开源代码:https://github.com/cla7aye15I4nd/shadowbound
个人主页:https://www.dataisland.org/
原文始发于微信公众号(安全研究GoSSIP):G.O.S.S.I.P 阅读推荐 2024-06-17 ShadowBound