导语:在我们的最新研究中,我们创建了一种称为“安全链接”的安全机制,以保护`malloc()`的单链接列表不被攻击者篡改。我们**成功地**向核心开放源代码库的维护者介绍了我们的方法,现在它已集成到最常见的标准库实现中:glibc(Linux),以及流行的嵌入式对等版本:uClibc-NG。
0x01 基本描述
我们在Check Point Research从事的每个研究项目的目标之一就是对软件的工作方式有深入的了解:它们包含哪些组件?他们容易受到哪些攻击?攻击者如何利用这些漏洞?更重要的是,我们如何防范此类攻击?
在我们的最新研究中,我们创建了一种称为“安全链接”的安全机制,以保护malloc()的单链接列表不被攻击者篡改。我们成功地向核心开放源代码库的维护者介绍了我们的方法,现在它已集成到最常见的标准库实现中:glibc(Linux),以及流行的嵌入式对等版本:uClibc-NG。
重要的是要注意,安全链接不是万能的法宝,它可以阻止针对现代堆实现的所有利用尝试。但是,这是朝正确方向迈出的又一步,根据我们过去的经验,这种特殊的缓解措施会阻止我们多年来展示的几项重大漏洞利用,从而将“易受攻击的”软件产品转变为“无法利用的”产品。
0x02 研究背景
从二进制利用的早期开始,堆内部数据结构一直是攻击者的主要目标。通过理解的方式堆的malloc()和free()工作中,攻击者能够利用堆缓冲区的漏洞,如线性缓冲区溢出,开发一个更强大的原始利用诸如任意写。
在2001年的Phrack文章中详细介绍了此示例:Vudo Malloc Tricks。本文探讨了多个堆实现的内部,并描述了现在所谓的“不安全-取消链接”。修改双向链接列表(例如Small-Bins)的FD和BK指针的攻击者可以使用该unlink()操作来触发任意写入,从而在目标程序上执行代码。
http://phrack.org/issues/57/8.html
实际上,2005年的glibc版本2.3.6嵌入了对这一已知利用漏洞原语的修复程序,称为“安全解除链接”。这个精巧的修复程序在从列表取消链接之前,先验证了双向链接节点的完整性,如图1所示:
图1:操作时安全解除链接-检查p->fd->bk == p。
尽管这一利用原语已于15年前被阻止,但当时没有人想出类似的解决方案来保护单链列表的指针。利用这一弱点,攻击者将重点转移到了这些不受保护的单链接列表,例如快速绑定和TCache(线程缓存)。破坏单个链接列表将使攻击者获得任意Malloc原语,即在任意内存地址中的受控受控分配。
在本文中,我们修复了将近20年的安全漏洞,并展示了我们如何创建一种安全机制来保护单链接列表。
在深入研究安全链接的设计之前,让我们回顾针对易受攻击的快速绑定的示例利用。在此类利用技术方面具有丰富经验的读者应该随时跳到“ 安全链接简介”这一节。
0x03 CVE-2020-6007
在研究智能灯泡的过程中,我们发现了一个基于堆的缓冲区溢出漏洞:CVE-2020-6007。通过利用此高危漏洞,我们说明了攻击者如何利用Fast-Bins的不受保护的单链接列表将基于堆的线性缓冲区溢出转换为功能更强大的Arbitrary-Write。
在我们的测试案例中,堆是一个简单的dlmalloc实现,或更具体地说,是编译为32位版本的uClibc(微型LibC)的“ malloc-standard”实现。图2显示了此堆实现所使用的元数据:
图2:malloc_chunk堆实现中使用的结构。
笔记:
· 分配和使用缓冲区时,前两个字段存储在用户的数据缓冲区之前。
· 当释放缓冲区并将其放置在快速绑定中时,也会使用第三个字段,它指向快速绑定的链接列表中的下一个节点。该字段位于用户缓冲区的前4个字节。
· 当缓冲区被释放并且没有放置在快速绑定中时,第三个字段和第四个字段都将用作双向链接列表的一部分。这些字段位于用户缓冲区的前8个字节。
快速合并是各种大小的“块”的阵列,每个“块”都包含一个单链列表,最多不超过特定大小。最小bin大小包含最多0x10字节的缓冲区,下一个最大缓冲区包含0x11至0x18字节的缓冲区,依此类推。
溢出思路
在不深入研究细节的情况下,我们的漏洞为我们提供了一个小的但可控制的基于堆的缓冲区溢出。我们的总体计划是溢出位于快速绑定中的相邻空闲缓冲区,图3显示了缓冲区在溢出之前的情况:
图3:我们的受控缓冲区(蓝色)放置在释放的缓冲区(紫色)之前。
图4显示了我们的溢出后相同的两个缓冲区:
图4:我们的溢出修改了释放缓冲区的size和ptr字段(以红色显示)。
使用溢出,我们修改我们希望的是快速绑定记录的单链接指针。通过将此指针更改为我们自己的任意地址,我们可以触发堆以为现在有一个新的释放块存储在那里,图5显示了Fast-Bin损坏的单链接列表,该列表在堆中看起来如下所示:
图5:以红色标记的快速绑定损坏的单链列表。
通过触发一系列大小与相关快速绑定匹配的分配序列,我们获得了Malloc-Where原语。有关构建完整代码执行漏洞的其余技术细节以及完整的研究,将在以后的博客文章中发布。
注意:某些人可能会说,我们获得的Malloc-Where原语受到限制,因为虚拟“ Free Chunk”应以与当前Fast-Bin匹配的size字段开头。但是,此附加的验证检查仅在glibc中实现,而在uClibc-NG中却没有。因此,对于我们的Malloc-Where原语,我们没有任何限制。
0x04 安全链接简介
在完成了这个研究之后,在36C3开始之前,我还有一些时间可以用来做准备,我的计划是解决最近的CTF比赛中的一些题目。近十年来,我一直在以相同的方式利用基于堆的缓冲区溢出,始终以堆中的单链接列表为目标。即使在CTF挑战中,我仍然专注于TCache的易受攻击的单链接列表,当然,有某种方法可以减轻这种流行的利用原语。
这就是安全链接概念的出现。安全链接利用了地址空间布局随机化(ASLR)中的随机性(现在已广泛部署在大多数现代操作系统中)来“签名”列表的指针。与块对齐完整性检查结合使用时,此新技术可保护指针免遭劫持尝试。
我们的解决方案可防止现代漏洞中经常使用的3种常见攻击:
· 部分指针覆盖:修改指针的低字节(Little Endian)。
· 全指针覆盖:劫持指向所选任意位置的指针。
· 未对齐的块:将列表指向未对齐的地址。
威胁建模
在我们的威胁模型中,攻击者具有以下功能:
· 堆缓冲区上的受控线性缓冲区上溢/下溢。
· 相对任意写入堆缓冲区。
重要的是要注意,我们的攻击者不知道堆位于何处,因为堆的基地址与mmap_baseASLR 一起被随机分配了(下一节中将对此主题进行更多介绍)。
我们的解决方案提高了标准,并阻止了攻击者基于堆的利用尝试。部署后,攻击者必须具有堆泄漏/指针泄漏形式的其他功能。我们保护的一个示例场景是位置相关的二进制文件(不带ASLR的加载),在解析传入的用户输入时会发生堆溢出。在我们之前的研究示例中,就是这种情况。
到现在为止,攻击者仅依靠二进制文件的固定地址,就能够在没有任何堆泄漏的情况下利用此类目标,并且仅对堆的分配进行最少的控制。当我们将堆分配重定向到目标二进制文件中的固定地址时,我们可以阻止此类利用尝试,并利用堆的ASLR获得随机基址。
保护措施
在Linux机器上,通过mmap_base以下逻辑将堆随机化:
random_base = (((1 << rndbits )-1 )<< PAGE_SHIFT )
rndbit 在32位Linux计算机上默认为8,在64位计算机上默认为28。
我们将单链接列表指针存储为L的地址。现在,我们定义以下计算:
Mask := (L >> PAGE_SHIFT)
根据上面显示的ASLR公式,移位将来自存储器地址的第一个随机位定位在掩码的LSBit处。
这将我们引向我们的保护方案。我们用P来表示单链接列表指针,方案如下所示:
· PROTECT(P) := (L >> PAGE_SHIFT) XOR (P)
· *L = PROTECT(P)
代码实现:
#define PROTECT_PTR(pos, ptr, type) \ ((type)((((size_t)pos) >> PAGE_SHIFT) ^ ((size_t)ptr))) #define REVEAL_PTR(pos, ptr, type) \ PROTECT_PTR(pos, ptr, type)
这样,来自地址L的随机位被放置在存储的受保护指针的LSB的顶部,如图6所示:
图6:被屏蔽的指针P’被随机位覆盖,如红色所示。
此保护层可防止不知道随机ASLR位(以红色显示)的攻击者将指针修改为受控值。
但是,如果你注意的话,你很容易看到我们在安全取消链接机制方面处于不利地位。尽管攻击者无法正确劫持指针,但我们也受到限制,因为我们无法检查指针是否发生了修改,这是进行额外检查的地方。
堆中所有已分配的块均与已知的固定偏移对齐,该偏移通常在32位计算机上为8个字节,在64位计算机上为16个字节。通过检查每个reveal()ed指针是否对齐,我们添加了两个重要的层:
· 攻击者必须正确猜测对齐位。
· 攻击者无法将块指向未对齐的内存地址。
在64位计算机上,这种统计保护会导致攻击尝试失败16次中的15次失败。如果回到图6,我们可以看到受保护的指针的值以零字节0x3结尾,这意味着攻击者必须在溢出中使用值0x3,否则攻击者将破坏该值并使对齐检查失败。
即使是单独使用,此对齐检查也可以防止已知的利用原语,例如本文中描述的原语,该原语描述了如何将Fast-Bin指向malloc() hook以立即获取代码执行。
旁注:在intel CPU上,glibc在32位和 64位体系结构上仍使用0x10字节的对齐方式,这与我们上面刚刚描述的常见情况不同。这意味着对于glibc,我们在32位上提供了增强的保护,并且可以统计地阻止16次攻击尝试中的15次。
实际示例
图7显示了我们提交给glibc的初始补丁的片段:
图7:来自补丁程序初始版本的示例代码片段,发送给glibc。
自从清理了补丁之后,我们仍然可以看到,保护glibc的TCache所需的代码修改既小又简单。
基准测试
基准测试表明,所添加的代码总计对的2-3条ASM指令free()和对的3-4条ASM指令总计malloc()。在glibc上,即使在GCP中的单个vCPU上总计10亿次(!)malloc()/ free()操作,更改也可以忽略不计。测试在该库的64位版本上运行。以下是在malloc-simple同一GCP服务器上的大小为128(0x80)字节的缓冲区上运行glibc的基准测试后的结果:
图8: glibc malloc-simple测试的基准测试结果。
每个测试的较快结果以粗体标记。正如我们所看到的,结果几乎是相同的,并且在一半的测试中,补丁版本的速度更快,这实际上没有任何意义。这通常意味着服务器上的噪声水平远高于所添加功能对整体结果的实际影响。简而言之,这意味着我们新功能所增加的开销可以忽略不计,这是个好消息。
再举一个例子,对tcmalloc(gperftools)基准测试的最坏影响是1.5%的开销,而平均值仅为0.02%。
这些基准测试结果归因于所建议机制的精简设计:
· 该保护没有内存开销。
· 保护不需要初始化。
· 不需要特殊的随机性来源。
· 该保护仅使用L,P并且在需要protect()编辑或reveal()编辑指针时都存在。
需要特别注意的是,快速绑定和TCache都使用单链接列表来保存数据,并且在glibc的文档中进行了详细说明。它们仅支持put / get API,并且没有通用功能遍历整个列表并保持完整。尽管确实存在这样的功能,但它仅用于收集malloc统计信息(mallinfo),因此访问单链接指针的额外开销可以忽略不计。
回顾线程模型
对齐检查减少了攻击面,并要求快速绑定或TCache块指向对齐的内存地址。如上所述,这直接阻止了已知的利用程序变体。
就像安全取消链接(用于双向链接的列表)一样,我们的保护依赖于以下事实:攻击者不知道合法的堆指针是什么样。在双链列表中,攻击者可以伪造一个内存结构,并且知道一个有效的堆指针是什么样子,可以成功地伪造一对有效的FD/ BK不会触发任意写原语的指针,但是允许块位于攻击者控制的地址。
在单链列表方案中,由于保护层依赖于从部署的ASLR继承的随机性,因此没有指针泄漏的攻击者将无法完全控制覆盖的指针。建议PAGE_SHIFT将随机位放在存储的指针的第一位上。连同对齐检查一起,这从统计角度上阻止了攻击者更改已存储的单链接指针的最低位/字节(Little Endian)。
Chromium’s MaskPtr()
我们的目的是将安全链接合并到实现各种包含单链接列表的堆的开源库代码包中。一种这样的实现是Google的tcmalloc(线程缓存Malloc),当时它仅作为gperftools存储库的一部分开源。在将补丁提交给gperftools之前,我们决定看看Chromium的Git存储库,以防万一他们可能使用其他版本的tcmalloc。事实证明,他们做到了。
我们将直接查看结果:
· gperftools自2007年以来已经发布了公共发行版,最新版本是2.7.90。
· Chromium的tcmalloc看起来好像是基于gperftool的2.0版本。
· 2020年2月,我们已经向所有开放源代码提交了补丁程序之后,谷歌发布了官方的TCMalloc GitHub存储库,该存储库与之前的两种实现都不相同。
在检查Chromium的版本时,我们发现他们的TCache现在不仅基于双向链接列表(现在称为FLFree List),不是基于单链接列表(最初称为SLL),还添加了一个独特的功能MaskPtr()。从2012年开始仔细研究他们的问题,显示了以下代码片段:
inline void* MaskPtr(void* p) { // Maximize ASLR entropy and guarantee the result is an invalid address. const uintptr_t mask = ~(reinterpret_cast(TCMalloc_SystemAlloc) >> 13); return reinterpret_cast(reinterpret_cast(p) ^ mask); }
该代码与我们的PROTECT_PTR实现非常相似。此外,该补丁的作者特别提到:“此处的目标是防止在漏洞利用程序中传播自由列表。”
看起来Chromium的安全团队在Chromium的tcmalloc中引入了自己的安全链接版本,他们在8年前就做到了,这给人留下了深刻的印象。
通过检查他们的代码,我们可以看到他们的掩码基于代码部分(TCMalloc_SystemAlloc)中的(随机值)指针,而不是实现中使用的堆位置。另外,它们将地址移位硬编码值13,并且还反转其掩码的位,由于未能找到有关其设计选择的文档,因此我们可以从代码中读取到,位反转用于确保结果为无效地址。
通过阅读他们的日志,我们还了解到他们估计此功能的性能开销不到2%。
与我们的设计相比,Chromium的实现暗含了一个额外的内存引用(对代码功能)和一个额外的asm指令,用于位翻转。不仅如此,它们的指针掩码无需额外的对齐检查即可使用,因此代码无法在不导致进程崩溃的情况下预先捕获指针修改。
0x05 堆分配器研究
我们实施并测试了补丁程序,以将建议的缓解措施成功集成到最新版本的glibc(ptmalloc),uClibc-NG(dlmalloc),gperftools(tcmalloc)和更高版本的Google全新TCMalloc。此外,我们还指出了Chromium的开发团队使用我们提交给gperftools的版本的“安全链接”,以期我们某些gperftools特定的性能提升可以融入Chromium的版本中。
当我们开始研究安全链接时,我们相信将安全链接集成到这3个(现在为4个)主导库中,将会导致其他库在开源社区和行业内的封闭软件中得到更广泛的采用。自2012年以来,基本版本的安全链接已嵌入Chromium中,这一事实证明了该解决方案的成熟性。
glibc(ptmalloc)
状态:集成。将于2020 年8月以2.32版发布。 激活:默认情况下为open。 GNU glibc项目的维护者非常合作并且反应迅速。主要的障碍是签署法律文件,这将使我们作为公司的雇员可以将GPL许可的源代码捐赠给GNU的存储库。一旦解决了这个问题,过程就非常顺利了,我们的补丁程序的最新版本已提交到库中,可以在即将发布的版本中使用。
我们要感谢glibc的维护者在整个过程中的合作。他们愿意将“默认情况下”的安全功能集成到他们的项目中,这真是令人心动,特别是与我们最初的期望以及我们从其他存储库获得的响应相比。
uClibc-NG(dlmalloc)
状态:已在v1.0.33版本中发布。 激活:默认情况下为open。 向uClibc-NG提交我们的功能非常容易,并且已将其立即集成到此commit中。我们可以自豪地说,安全链接已作为uClibc-NG版本v1.0.33的一部分发布。如果我们回到对智能的研究,此功能将阻止我们的利用,并迫使我们在产品中发现其他漏洞。
我们再次感谢uClibc-NG的维护者在此过程中的合作。
gperftools(tcmalloc)
现状:正在进行整合。 激活:默认为关闭。 尽管我们之前提到了Chromium的MaskPtr()功能(自2012年起可用),但该功能并未在tcmalloc的两个公开版本中找到它。因此,我们很幸运能向gperftools的tcmalloc实现提交安全链接。
由于gperftools信息库的状态复杂,现在Google的官方TCMalloc信息库是公开的,因此此过程正在取得进展,但进展缓慢。在2020年1月开始的请求请求中,你可以看到我们为将此功能集成到存储库中所做的努力。最初的反应是我们恐惧的反应:我们的功能“破坏性能”。提醒一下,使用存储库的基准测试套件时,最糟糕的情况是开销为1.5%,平均值仅为0.02%。
最终,我们有些勉强地决定将此功能添加为“默认情况下处于禁用状态”,希望有一天有人能够自行激活此功能。此功能尚未合并,但我们希望它将在不久的将来。
我们仍然要感谢这个项目的唯一维护者,他提供了做所有必要管道的方法,以便允许用户配置选项以启用安全链接。
TCMalloc(tcmalloc)
状态:已拒绝。 激活: N / A。 我们在此pull请求中将补丁提交给了TCMalloc ,但不幸的是,该请求被立即拒绝了。我们再一次听到“此功能的性能成本太高而无法合并”,并且即使作为“默认关闭”可配置功能,他们也不会将其集成:“在受宏保护的同时,它添加了另一种配置需要进行构建并定期进行测试,以确保一切正常。” 我们找不到Google的任何代表来帮助我们解决与存储库维护者的这种冲突,因此我们将其保留。
不幸的是,似乎malloc()在大多数C / C ++项目中使用了Google最常见的实现(如TCMalloc的文档中所述),但并没有集成一个安全功能,该功能将使其在项目中更难以利用漏洞。
0x06 研究总结
安全链接不是万能的灵丹妙药,它可以阻止针对现代堆实现的所有利用尝试。但是,这是朝正确方向迈出的又一步,通过迫使攻击者在甚至没有开始基于堆的攻击之前就存在指针泄漏漏洞,我们逐渐提高了标准。
根据我们过去的经验,这种特殊的缓解措施将阻止我们多年来实施的几项重大攻击,从而将“易受攻击”的软件产品转变为“无法利用”(至少在我们各自研究项目时存在漏洞)。
还需要注意的是,我们的解决方案不仅限于堆实现。它还可以通过位于用户缓冲区附近的单链接列表指针来使濒临灭绝的数据结构获得完整性保护,而不会增加任何内存开销,并且对运行时的影响可以忽略不计。该解决方案可以轻松扩展到具有ASLR的系统中的每个单链列表。
从我们在2019年底首次分析此问题开始,到设计安全缓解措施,最后将其集成为安全功能,这是一段艰难的旅程,默认情况下,该功能可在世界上两个最著名的libc实现中使用:glibc和uClibc-NG。最后,我们在集成此功能方面取得了比最初预期更好的结果,因此,我们再次感谢所有帮助实现此想法的维护者和研究人员。我们逐步地逐步提高了利用水平,并帮助保护全世界的用户。
本文翻译者自:https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/如若转载,请注明原文地址: