安全链接-新的malloc()缓解关闭了十年前的利用漏洞原语

2020-05-21 18:19:31

我们在Check Point Research工作的每个研究项目的目标之一是深入了解软件是如何工作的:它们包含哪些组件?他们脆弱吗?攻击者如何利用这些漏洞?更重要的是,我们如何防范此类攻击?

在我们最新的研究中,我们创建了一种名为“安全链接”的安全机制,以保护malloc()的单链表不被攻击者篡改。我们成功地向核心开源库的维护者介绍了我们的方法,现在它已经集成到最常见的标准库实现中:glibc(Linux),以及其流行的嵌入式对应产品:uClibc-NG。

需要注意的是,安全链接并不是阻止所有针对现代堆实现的攻击尝试的灵丹妙药。然而,这是朝着正确方向又迈出了一步。根据我们过去的经验,这种特定的缓解将阻止我们多年来演示的几个主要漏洞,从而将“损坏”的软件产品变成“无法利用”的产品。

从二进制攻击的早期开始,堆内部数据结构就一直是攻击者的主要目标。通过了解堆的malloc()和free()的工作方式,攻击者能够利用堆缓冲区中的初始漏洞(如线性缓冲区溢出)进入更强大的利用原语(如任意写入)。

这方面的一个例子在2001年的PhRack文章中有详细介绍:Vudo Malloc Tricks。本文探讨了多个堆实现的内部结构,并描述了现在所说的“不安全-取消链接”。修改双向链表的FD和BK指针(例如,小仓)的攻击者可以使用unlink()操作触发任意写入,从而在目标程序上实现代码执行。

事实上,2005年的glibc版本2.3.6嵌入了一个称为“安全解除链接”的已知漏洞原语的修复。这个优雅的修复在从列表取消链接之前验证双链接节点的完整性,如图1所示:

虽然这个利用原语在15年前就被阻止了,但当时还没有人提出类似的解决方案来保护单链表的指针。利用这一弱点,攻击者将他们的注意力转移到这些未受保护的单链表上,如Fast-bins和TCache(线程缓存)。破坏单个链表使得攻击者能够获得任意Malloc原语,即任意内存地址中的小型受控分配。

在本文中,我们将弥合这一存在了近20年的安全差距,并展示如何创建一种安全机制来保护单链表。

在我们深入到安全链接的设计之前,让我们回顾一个针对易受攻击的Fast-Bins的示例攻击。对这类开发技术有丰富经验的读者可以直接跳到介绍安全链接的部分。

在对智能灯泡黑客攻击的研究中,我们发现了一个基于堆的缓冲区溢出漏洞:CVE-2020-6007。通过对此严重漏洞的攻击过程,我们说明了攻击者如何利用Fast-Bins的无保护单链表将基于堆的线性缓冲区溢出转换为更强大的任意写入。

在我们的测试用例中,堆是一个简单的dlmalloc实现,或者更具体地说,是编译成32位版本的uClibc(MicroLibC)的“malloc标准”实现。图2显示了此堆实现使用的元数据:

分配和使用缓冲区时,前两个字段存储在用户的数据缓冲区之前。

当释放缓冲区并将其放入Fast-Bin时,也会使用第三个字段,并指向Fast-Bin的链表中的下一个节点。此字段位于用户缓冲区的前4个字节。

当缓冲区被释放并且没有放在快速入库中时,第三个和第四个字段都用作双向链表的一部分。这些字段位于用户缓冲区的前8个字节。

快速存储箱是由各种大小的“存储箱”组成的数组,每个存储箱都有一个特定大小的块的单链接列表。最小bin大小包含最大0x10字节的缓冲区,NEXT包含0x11到0x18字节的缓冲区,依此类推。

在不深入细节的情况下,我们的漏洞会给我们带来一个小型的、但可控的、基于堆的缓冲区溢出。我们的总体计划是溢出位于快速回收站中的相邻空闲缓冲区。图3显示了溢出之前的缓冲区外观:

图4:我们的溢出修改了释放的缓冲区的Size和PTR字段(以红色显示)。

使用我们的溢出,我们修改了我们希望是Fast-Bin记录的单链接指针。通过将此指针更改为我们自己的任意地址,我们可以触发堆,使其认为新释放的块现在存储在那里。图5显示了Fast-Bin的损坏的单链表,就像它在堆中显示的那样:

通过触发与相关Fast-Bin匹配的大小的分配序列,我们获得Malloc-WHERE原语。关于构建完整的代码执行漏洞的其余技术细节,以及Lightbulb研究的全部内容,将在未来的博客文章中发布。

注意:你们中的一些人可能会说我们获得的Malloc-where原语是受约束的,因为虚拟的“Free Chunk”应该以与当前Fast-Bin的大小字段匹配的大小字段开始。然而,这个额外的验证检查只在glibc中实现,uClibc-NG中没有。因此,我们对我们的Malloc没有任何限制-在那里是原始的。

在完成灯泡研究后,在36C3开始之前,我还有一些时间,我的计划是解决最近CTF比赛中的一些棋子挑战。相反,我发现自己又在思考最近开发的漏洞。近十年来,我一直在以相同的方式利用基于堆的缓冲区溢出,总是以堆中的单链接列表为目标。即使在CTF挑战中,我仍然将重点放在TCache的易受攻击的单链表上。当然,有一些方法可以减轻这种流行的利用原始漏洞。

这就是安全链接概念的由来。安全链接利用地址空间布局随机化(ASLR)中的随机性(目前在大多数现代操作系统中大量部署)来“签署”列表的指针。当与块对齐完整性检查结合使用时,这种新技术可以保护指针免受劫持尝试。

重要的是要注意,我们的攻击者不知道堆的位置,因为ASLR随机化了堆的基地址和mmap_base(下一节将详细介绍此主题)。

我们的解决方案提高了门槛,阻止了攻击者基于堆的攻击企图。一旦部署,攻击者必须具有堆泄漏/指针泄漏形式的附加功能。我们保护的一个示例场景是位置相关的二进制文件(加载时没有ASLR),它在解析传入的用户输入时会出现堆溢出。这就是我们之前在灯泡研究中展示的例子中的情况。

到目前为止,攻击者能够在没有任何堆泄漏的情况下攻击这些目标,并且仅通过仅依赖于二进制文件的固定地址来对堆的分配进行最小程度的控制。当我们将堆分配重定向到目标二进制文件中的固定地址时,我们可以阻止此类利用尝试,并利用堆的ASLR获得随机性。

rndbit在32位Linux计算机上默认为8,在64位计算机上默认为28。

我们将存储单链表指针的地址表示为L。我们现在定义以下计算:

根据上面所示的ASLR公式,移位将存储器地址的第一个随机位定位在掩码的LSBit处。

这就引出了我们的保护计划。我们用P表示单链表指针,该方案如下所示:

#DEFINE PROTECT_PTR(pos,ptr,type)。#定义PROTECT_PTR(pos,ptr,type)。\r定义PROTECT_PTR(pos,ptr,type)。#DEFINE_PRORECT_PTR(pos,ptr,type)#定义PROTECT_PTR(pos,ptr,type)。#定义PROTECT_PTR(pos,ptr,type)。\r定义PROTECT_PTR(pos,ptr,type)。

这样,来自地址L的随机位被放置在存储的受保护指针的LSB之上,如图6所示:

图6:掩码指针P‘被随机位覆盖,如红色所示。

此保护层可防止攻击者在不知道随机ASLR位(以红色显示)的情况下将指针修改为受控值。

然而,如果你注意了,你就会很容易地看到,我们在安全解除链接机制方面处于劣势。虽然攻击者无法正确劫持指针,但我们也受到限制,因为我们无法检查是否发生了指针修改。这是进行额外检查的地方。

堆中所有分配的块都与已知的固定偏移量对齐,该偏移量在32位机器上通常为8字节,在64位机器上通常为16字节。通过检查每个显示的()指针是否相应地对齐,我们添加了两个重要的层:

在64位计算机上,此统计保护会导致攻击尝试失败,每16次中有15次失败。如果我们回到图6,我们可以看到受保护指针的值以半字节0x3结束,这意味着攻击者必须在他的溢出中使用值0x3,否则他将损坏该值并使对齐检查失败。

即使就其本身而言,这种对齐检查也可以防止已知的利用漏洞原语,例如本文描述如何将Fast-Bin指向malloc()挂钩以立即获得代码执行的利用原语。

附注:在英特尔CPU上,glibc仍然在32位和64位架构上使用0x10字节对齐,这与我们刚才描述的常见情况不同。这意味着,对于glibc,我们在32位上提供了增强的保护,并且可以从统计上阻止16次攻击尝试中的15次。

图7显示了我们提交给glibc的初始补丁的片段:

图7:发送到glibc的补丁初始版本的示例代码片段。

虽然补丁已经清除,但我们仍然可以看到,保护glibc的TCache所需的代码修改很小,而且很简单。这就把我们带到了下一个部分,基准测试。

基准测试显示,对于free(),添加的代码总计为2-3条ASM指令,对于malloc(),添加的代码总计为3-4条ASM指令。在glibc上,这一变化可以忽略不计,即使将10亿(!)。GCP中单个vCPU上的malloc()/free()操作。测试在64位版本的库上运行。以下是在同一GCP服务器上对大小为128(0x80)字节的缓冲区运行glibc的基准测试malloc-Simple后的结果:

每次测试的较快结果以粗体标记。正如我们所看到的,结果几乎是一样的,而且在一半的测试中,打补丁的版本速度更快,这真的没有什么意义。这通常意味着服务器上的噪音级别远远高于添加的功能对整体结果的实际影响。或者简而言之,这意味着我们的新功能增加的开销可以忽略不计,这是个好消息。

再举一个例子,对tcmalloc(Gpertools)基准测试影响最大的是1.5%的开销,而平均只有0.02%。

该保护仅使用L和P,它们在指针需要被保护()或显示()时都存在。

需要注意的是,Fast-bin和TCache都使用单链表来保存数据,这一点在glibc的文档中有详细介绍。它们只支持PUT/GET API,没有公共功能遍历整个列表并保持其完好无损。虽然确实存在这样的功能,但它仅用于收集malloc统计信息(Mallinfo),因此访问单链接指针所增加的开销可以忽略不计。

对齐检查减少了攻击面,并要求Fast-Bin或TCache块指向对齐的内存地址。如上所述,这将直接阻止已知的利用漏洞变体。

就像安全解除链接(对于双向链表)一样,我们的保护依赖于这样一个事实,即攻击者不知道合法的堆指针是什么样子。在双向链表方案中,攻击者可以伪造内存结构,并且知道有效的堆指针是什么样子,可以成功地伪造有效的FD/BK指针对,该指针对不会触发任意写入原语,但允许在攻击者控制的地址处使用块。

在单链表方案中,由于保护层依赖于从部署的ASLR继承的随机性,没有指针泄漏的攻击者将无法完全控制被覆盖的指针。建议的page_shift将随机位直接放在存储指针的第一位上。与对齐检查一起,这将在统计上阻止攻击者更改存储的单链接指针的最低位/字节(小端)。

我们的目的是将安全链接合并到实现各种包含单链接列表的堆的领先开放源码中。一个这样的实现是Google的tcmalloc(Thread-Cache Malloc),当时它只是作为gpertools存储库的一部分开源的。在将我们的补丁提交给gpertools之前,我们决定先看看Chromium的Git库,以防他们使用不同版本的tcmalloc。事实证明,他们确实做到了。

Chrome的tcmalloc看起来像是基于gpertool的2.0版本。

在2020年2月,在我们已经向所有开放源码提交补丁之后,Google发布了官方的TCMalloc GitHub存储库,这与之前的两个实现都不同。

在检查Chromium的版本时,我们看到他们的TCache现在不仅基于双链表(现在称为FL,代表自由列表),而不是单链表(最初称为SLL),他们还添加了一个称为MaskPtr()的特殊函数。仔细查看2012年的问题,会发现以下代码片段:

inline void*MaskPtr(void*p){0//最大化ASLR熵并保证结果为无效地址。(const uintptr_t掩码=~(reinterpret_cast<;uintptr_t>;(TCMalloc_SystemAlloc)(reinterpret_cast<;void*>;(reinterpret_cast<;uintptr_t>;(p)>;>;13);(返回掩码^);}。

该代码与我们的Protected_ptr实现非常相似。此外,该补丁的作者特别提到,“这里的目标是防止利用漏洞的自由列表攻击。”

看起来Chromium的安全团队引入了他们自己版本的安全链接到Chromium的tcmalloc,他们在8年前就这样做了,这是相当令人印象深刻的。

通过检查它们的代码,我们可以看到它们的掩码基于来自代码段(TCMalloc_Systemalloc)的(随机值)指针,而不是我们实现中使用的堆位置。此外,它们将地址移位硬编码值13,并且还反转其掩码的位。因为我们找不到他们设计选择的文档,所以我们可以从代码中读到使用位反转来保证结果是无效地址。

通过阅读他们的日志,我们还了解到,他们估计此功能的性能开销不到2%。

与我们的设计相比,Chromium的实现意味着额外的内存引用(对代码函数)和额外的位翻转ASM指令。不仅如此,它们的指针屏蔽是在没有额外对齐检查的情况下使用的,因此代码无法在不使进程崩溃的情况下提前捕获指针修改。

我们实现并测试了补丁,以成功地将建议的缓解集成到最新版本的glibc(Ptmalloc)、uClibc-NG(Dlmalloc)、gPertools(Tcmalloc)以及后来的Google全新TCMalloc中。此外,我们还向Chromium的开发团队指出了我们提交给gpertools的安全链接版本,希望我们的一些特定于gperftools的性能提升能够应用到Chromium的版本中。

当我们开始研究安全链接时,我们相信将安全链接集成到这3个(现在是4个)占主导地位的库中将导致其他库更广泛地采用,无论是在开放源码社区中还是在行业中的封闭源码软件中。自2012年以来,基本版本的安全链接已经嵌入到Chromium中,这一事实证明了该解决方案的成熟性。

以下是在撰写本文时的集成结果。

状态:已集成。将于2020年8月发布2.32版。激活:默认情况下打开。GNU的glibc项目的维护人员非常合作,反应非常灵敏。主要的障碍是签署法律文件,允许我们作为公司的员工将GPL许可的源代码捐赠给GNU的存储库。一旦我们解决了这个问题,过程就非常顺利,最新版本的补丁已提交到库中,准备包含在即将到来的版本中。

我们要感谢Glibc的维护人员在整个过程中的合作。他们愿意将“默认开启”的安全特性集成到他们的项目中,这令人暖心,特别是与我们最初的期望相比,以及与我们从其他存储库收到的响应相比。

状态:版本v1.0.33发布。激活:默认情况下打开。向uClibc-NG提交我们的特性非常容易,并且它立即集成到这个提交中。我们可以自豪地说,安全链接已经作为uClibc-NG版本v1.0.33的一部分发布了。如果我们回到我们对智能灯泡的研究,这个功能会阻止我们的攻击,并迫使我们在产品中发现一个额外的漏洞。

我们要再次感谢uClibc-NG的维护者在这一过程中的合作。

状态:正在整合中。激活:默认关闭。尽管我们早些时候提到了Chromium的MaskPtr()功能,自2012年开始使用,但这一功能并没有出现在tcmalloc的两个公共版本中。因此,我们碰碰运气,提交了指向gpertools的tcmalloc实现的安全链接。

由于gpertools存储库的复杂状态,既然Google的官方TCMalloc存储库是公开的,这个过程正在取得进展,但进展缓慢。在2020年1月初的拉取请求中,您可以看到我们为将此功能集成到存储库中所做的努力。最初的反应是我们害怕的:我们的功能“毁了性能”。提醒一下,当使用存储库的基准测试套件时,最坏的结果是1.5%的开销,平均只有0.02%。

最后,带着一些不情愿,我们决定将这一功能添加为“默认关闭”,希望有一天有人会自动激活这一功能。这个功能还没有合并,但我们希望在不久的将来会合并。

我们仍然要感谢该项目的唯一维护人员,他提供了所有必要的管道,以便允许用户选择启用安全链接的配置选项。

状态:已拒绝。激活:不适用。在此拉取请求中,我们向TCMalloc提交了我们的补丁,遗憾的是,该请求立即被拒绝。我们再一次听说,“这样做的性能成本太高,无法合并”,他们甚至不会将其作为“默认关闭”的可配置功能进行集成:“虽然是宏保护,但它添加了另一个需要构建和定期测试的配置,以确保一切都能正常工作。”我们找不到任何来自Google的代表来帮助我们解决与存储库维护人员的冲突,所以我们让它保持原样。

不幸的是,它看起来像是Google最常见的malloc()实现,大多数C/C++都使用它

..