利用SPEAR攻击绕过栈溢出保护机制 (下)
在本文中,我们继续为读者介绍通过劫持预测控制流绕过栈溢出保护机制的新型漏洞利用方法。
(接上文)
滥用Linux的内存页分配器
TLS页重用是攻击的一个关键部分,因为它有利于在攻击过程中进行非侵入性的金丝雀值驱逐。我们是在了解了进程加载过程是如何从操作系统(OS)请求资源,即页帧之后,才想出了这个方法。我们通过实验发现,有时相同的页帧号码(PFN)会出现在不同的进程中。下面,我们通过深入考察进程启动和退出过程中的内存分配和释放过程,来了解操作系统的页帧处理。首先,让我们来讨论页帧的释放过程,这样我们就可以得出关于分配器所管理的空闲页块的结论。随后,我们将考察如何通过内存分配请求从空闲页块中检索页帧。实际上,空闲页块对应于一个内部的内核数据结构——按块大小(2的n次方个内存页)组织的链表数组。每个链表被称为一个n阶空闲页表,其中n是2的指数(见注7)。在这次攻击中,只需操纵0阶空闲页表即可。
当进程删除部分虚拟内存(madvise、munmap)或释放全部虚拟内存(exec、exit)时,就会发生页帧释放操作。对于后一种情况来说,释放的顺序是由unmap_vmas决定的(见注8)。通常来说,页帧是根据其对应的虚拟地址按升序进行释放的。这也适用于前一种情况,不过,内存块释放的顺序会影响空闲页表的布局。
由于TLB(Translation Lookaside Buffer,转译查找缓存)是按照LIFO(后进先出)的顺序完成释放的,但是页帧最终是以降序方式插入空闲页表的(见注9)。
在搞清楚释放器的行为后,我们精心设计了攻击者的虚拟内存空间,使得目标TLS页在攻击者释放内存后落在0阶空闲页表的固定位置。我们这个固定位置的索引称为K。K的选择标准是,使得在受害者对TLS页帧的进程内存请求期间,0阶空闲页表的第K个节点正好是攻击者控制的页帧。在攻击之前,K的值是通过跟踪模拟攻击中受害者的内存分配、在perf下启动受害者二进制文件并记录所有页帧分配和释放过程来确定的。
在TLS页的情况下,我们能够在每次尝试中重复使用攻击者的页面。不过,并不是程序分配的任何页面都是如此:例如,当目标页面是加载器请求的第一批页面之一,或者当目标页面是在受害者内存释放之后请求的,成功率就会下降。
实现金丝雀驱逐
我们接下来演示针对攻击者运行的(执行的)受害者代码的攻击,因为此设置允许完全控制受害者代码的启动,并将攻击者控制的内存页正确地放置到0阶空闲页表的合适位置。
我们使用execv将目标受害者置于攻击者控制的进程A2的运行上下文中,从而启动目标受害者。在此之前,A2会根据K值和内存页的长度分配一定数量的内存空间,以在execv之后实现TLS页重用。之后,该系统调用将放弃正在运行的进程(A2)的完整内存布局,并将其替换为受害者的内存布局,但其方式是以攻击者所需的顺序返回由OS分配的相应物理内存页。我们的方法能够保证包含A2的金丝雀值的TLS页面,同时包含目标受害者的金丝雀值,因此,A1可以通过加载步骤1中计算出的逐出组来驱逐受害者的金丝雀值(见步骤4)。
预测型ROP
金丝雀驱逐确保了足够长的预测窗口,这样的话,我们就有足够的时间来运行攻击性payload(步骤6)。实际上,这些payload是由精心选择的ROP Gadget组成,而这些ROP Gadget是在步骤1中作为初始化的一部分准备好的。
通过Spectre v1型侧信道发送Gadget
我们在背景知识部分简单说过,payload的主要用途是泄露受害者数据。这里的攻击方法所使用的任意读取原语为Spectre v1型gadget,实际上,这是由ROP Gadget链接而成的,因为受害者的内存空间很难找到现成的。
实际上,Spectre v1型gadget的本质作用,就是实现数组访问,其中array_ba se和scale是固定的。变量是array_index,代表一个受害者机密字节的值。因此,ROP链的用途是:
- 计算数组地址
mov array_index, [victim_secret_address]
address = array_ba se + scale x array_index
- 加载地址内容,以通过侧信道发送信号
mov reg, [address]
为了计算Spectre v1数组地址,我们寻找由下面的两条指令:
pop reg; ret;
这些gadget的作用,就是将victim_secret_address和array_ba se放入相应的寄存器中。我们假设这两个地址是已知的(见注10),因此,它们可以从堆栈中弹出并用于后续的计算。下一步是对victim_secret_address进行解引用,以获得array_index,这一步是借助寄存器解引用gadget实现的。
mov reg0, [reg1]; ret;
下一个gadget是一个算术或逻辑运算,用于将array_index与scale相乘。实际上,最常见的变体是:
shl reg0, 8; ret;
最后,我们需要另一个寄存器/地址解引用来实现侧信道的发送操作。
我们在不同的库(包括libc、libpthread和libpng)中发现了不同工作版本的ROP链。至于PoC中使用是哪些ROPGadget,请参考我们的论文。
限制条件
传统上,代码重用攻击可以使用任何映射至内存的可执行内存页中的gadget,并且与该内存页的运行时状态无关。然而,为了让ROP Gadget成功地预测执行,该gadget必须驻留在具有有效TLB映射的虚拟页中。此外,icache未命中会缩短后续ROP Gadget的预测窗口,因此,位于cache中的gadget能够有效提高侧信道发送操作的成功率。
为了克服第一个限制,我们建立了一个基于ROPgadget的预测型ROP gadget搜索工具。该工具的作用,就是确定所有在运行期间被物理映射的代码页,并将ROP Gadget的搜索空间限定为这些页面。为了达到这个目的,我们在gdb中模拟了对受害者的攻击,并收集执行痕迹。这些痕迹有助于找到在攻击发生时具有相应TLB映射的代码页(见注11)。此外,我们还规定:ROPGadget只能在之前找到的代码页中搜索gadget。
为了克服第二个限制,需要将ROP链放入cache,这是由步骤2中ROP初始化阶段处理的。随后,在步骤3(发送payload)之后,攻击者启动了一个进程,与受害者共处一地,循环访问ROP链,以在预测性执行ROP过程中降低icache未命中的可能性。
预测型ROP是预测执行攻击中的一个强大工具。作为证明,我们只用6个gadget就实现了任意读取原语。然而,由于上面提到的限制条件(TLB/icache未命中对预测窗口大小的影响),预测型ROP的设置并不简单。此外,环境中的任何轻微变化都可能影响攻击的成功率(不像传统的ROP,在不同的设置中可以找到类似的gadget)。
侧信道
在上一节中,我们讨论了通过预测型ROP发送侧信道信号的问题。关于信道,该攻击使用了预测执行攻击中的标准选项,即FLUSH+RELOAD缓存侧信道,尽管其他侧信道也是可行的。缓存侧信道可以使用攻击者和受害者之间的任何共享内存区域作为侧信道数组(例如,受害者程序使用的任何动态库)。对共享空间的限制是:大小(至少64KB)和噪声源——攻击者可能更喜欢在距离攻击很近的时间内不被访问的共享库。为了方便起见,在这次攻击中,我们选择了libpthread作为侧通道数组。
攻击者在步骤3之后,需要立即从所有级别的cache中刷新所有的数组元素,从而为侧信道数组打下基础。在步骤7中,攻击者需要对数组进行必要的探测,以读取由预测型ROP发送的侧信道信号。如果一切顺利的话,一个受害者的秘密字节就会被泄露出去。
SPEAR提供的任意读取原语,允许每次从任意的受害者内存地址泄露一个字节。在现实世界中,对手需要一个以上的字节来接管账户或升级权限,因此,我们测量了泄漏2位数大小的秘密字节序列的攻击效果。由于各种噪声源的存在,攻击的成功率为7.19% +-0.62(100次运行的平均值,95%的置信区间),因此,我们对每个秘密字节进行了10到100次采样。在这种条件下,我们实现了每秒0.3字节的端到端泄漏速率。
缓解措施
对于启用所有系统级的Spectre缓解措施的Linux机器来说,仍然无法防御这种新型攻击,这是因为SPEAR类攻击根本不受Spectre v2缓解措施的影响。由于SPEAR利用了测试内存完整性的分支,因此必须在应用程序级别应用缓解措施。为此,我们对两种可能的缓解措施进行了基准测试:一种是基于lfence的,一种是基于敏感元数据(如数组索引、返回地址、前向边)屏蔽的。lfence缓解措施产生了很高的开销(最高记录为100%),尽管它得到了其他研究工作的认可,但我们建议使用其他替代方案。基于敏感元数据屏蔽的缓解措施的开销仍然是不可忽略的,但在我们的基准中,它没有超过13%。本文详细介绍了这些基准,以及基准方法和基准平台。
我们提出的针对SPEAR的缓解措施的一个重要方面是,它们只保护主张内存完整性的应用程序代码。在SSP的情况下,缓解措施针对的是每个函数序言前执行的金丝雀完整性检查。我们的结论是,在不启用针对Spectre v1的全面缓解措施的情况下,仍然可以成功阻止SPEAR攻击,这样的好处是降低了性能损失。
小结
在本文中,我们研究了基于CPU架构的预测执行特性的控制流劫持技术(SPEAR变体),该技术能够绕过针对经典缓冲区溢出的缓解措施,从本地受害者那里泄露信息。此外,我们还展示了针对SSP机制(旨在保护软件免受漏洞利用的机制)的实际攻击,得出的结论是现有的缓解措施在Spectre时代是不完善的。
除了SSP机制之外,我们还研究了内存安全语言(Go、Rust),Clang CFI和GCC VTV机制。除了Clang CFI,所有提到的机制都无法抵御SPEAR,因为它们都可能错误地预测执行完整性检查。我们提出了一种针对SPEAR的低开销缓解措施:在完整性检查之前掩盖敏感的元数据(数组索引、返回地址、前向边),从而在发生错误预测时阻止SPEAR攻击。
关于SPEAR攻击的更多信息,请参考我们的相关论文。
尾注
- 我们之所以选择这个特定的CVE,是因为它能越界写入足够多的内容,以适应攻击性payload。在实践中栈缓冲区溢出必须允许覆盖返回地址,以满足栈跳转的要求。
- 在最初的Spectre论文中,每个元素的大小等于PAGE_SIZE,以避免探测噪声。噪声源来自与请求的行一起提取的相邻缓存行(cacheline)。我们试验了不同的元素大小,直到达到256B,这个值不仅能够实现较低的探测噪声,同时也使探测区域相对较小(16页)。
- 虽然cache的内部结构不是这篇文章的主题,但是有必要描述一下实验机器的缓存配置:LLC(Last Level Cache)的大小为8MB。CPU有4个物理核心,因此,有8个虚拟核心。正如最新的研究报告和我们的实验所证实的那样,相应的LLC片数为8。
- LLC的组织是基于数据的物理内存位置的。
- TLS页面是在加载时由_dl_allocate_tls_storage分配的,这属于SSP初始化的任务。完成内存分配之后,由_dl_setup_stack_chk_guard生成金丝雀值。
- 为了完整起见,我们分析了攻击者控制的和独立的受害者启动场景。除了攻击者执行的受害者代码(攻击者控制的)之外,我们还对以下情况下的内存页重用进行了描述:a)受害者进程是攻击者控制的进程的一个子进程;b)受害者进程是独立的。如果攻击者控制的受害者启动时,攻击者进程可以通过mmap操纵其运行时的虚拟内存空间,则TLS页面重用的成功率为100%。至于能否强制系统中的独立进程中进行页面重用(b)),具体取决于攻击者跟踪受害者内存分配的能力。在Linux系统中,默认允许监控(通过/proc)其他进程的常驻内存大小,粒度为66页,详见注12。
- 关于Linux分配器内部结构的更多信息,请看这里。
- 调用栈取是否调用unmap_vmas,这要决于用户空间进程的初始操作(exit、execv、munmap、madvise)。
- 我们通过使用perf捕捉内存分配器事件(mm_page_alloc, mm_page_free)来获得调用堆栈。
- ASLR(地址空间布局随机化)的绕过方法不在这项工作的范围内,所以我们这里假设ASLR已经失效。关于这个问题的讨论,请参考相关论文。
- TLB受制于替换策略,因此,并非所有工具返回的页面都有一个有效的TLB条目。虽然TLB的长度允许使用足够的ROP Gadget搜索空间来建立Spectre v1型gadget,但最终仍可能会出现假阳性。
- 当有足够的事件(页面故障)发生时,Linux将通过sync_mm_rss异步更新驻留组的大小(RSS)。在这里,我们选择的事件数量是64(TASK_RSS_EVENTS_THRESH),然而,但实验发现RSS监控粒度为66。同时,代码会检查计数器是否大于64,也就是是否已经发生了65个事件。后来,随着事件的出现,计数器会逐渐递增,因为它就是统计当前事件数量的(=>66)。最后,满足上述条件后,就会调用sync_mm_rss函数。
原文地址:https://ibm.github.io/system-security-research-updates/2021/06/18/spear-attacks-ssp-usecase
最新评论