利用SPEAR攻击绕过栈溢出保护机制 (上)

匿名者  1029天前

在本文中,我们将为读者详细介绍一种通过劫持预测控制流绕过栈溢出保护机制的漏洞利用方法。

简介

本文演示了通过劫持图像处理服务器的预测控制流来实现本地任意读取的攻击技术。就本文来说,我们假设受害者使用了libpng-1.2.5,该版本存在CVE-2004-0597(见注1)栈溢出漏洞。当创建程序库时,如果启用了栈溢出保护(SSP)机制的话,就能抵御传统的溢出攻击。然而,SSP不仅不能防御预测控制流劫持攻击,而且还提供了便于实现端到端攻击的相关原语。

除了针对SSP预测型控制流的劫持攻击之外,我们的扩展工作还涵盖了一整类的攻击技术,我们称其为基于预测型架构的控制流劫持技术,简称SPEAR。就SPEAR类型来说,SSP攻击属于后向边类别的架构覆盖的预测型控制流程劫持。

相关论文:https://ibm.github.io/system-security-research-updates/2021/06/18/spear-attacks-ssp-usecase

背景知识:绕过栈溢出保护机制的方法

SSP是一种针对传统栈缓冲区溢出攻击的缓解措施。它的设计非常简单:在每个函数的序言中,向栈中写入一个随机生成的值(即canary,下文中我们按字面含义翻译为金丝雀),并使其它位于分配给局部变量的内存区之前。除非攻击者能够在攻击前泄露金丝雀值,否则线性栈缓冲区溢出攻击是无法得手的,因为函数尾声(function epilogue)会检查金丝雀的完整性,如果无法通过该项检查,程序就会崩溃。

然而,即使在金丝雀完整性检查失败的情况下,系统仍然会预测性地运行该exploit(尽管这一预测是错误的,系统只是不取其结果而已,但是还是会预测性地执行的),对于这一点来说,SSP机制是无能为力的。为了解释堆溢出保护机制无法防御Spectre攻击的原因,我们下面给出了由GCC和Clang生成的SSP代码:

func:

    prologue

; Store canary on the stack

    mov rbx,QWORD[fs:0x28]

    movQWORD[stack_canary], rbx

    ...

    body

    ...

; Check for corrupted canary, if yes fail

    mov rbx,QWORD[stack_canary]

    xorQWORD[fs:0x28], rbx

    je exit

    call__stack_chk_fail

exit:

    epilogue

    ret

在上面的代码片段中,xor指令的结果设置了相应的标志,条件跳转指令随后使用这些标志来决定是中止还是返回调用方。当缓冲区溢出确实发生时,对该条件分支的错误预测,致使系统将在被覆盖的返回地址处预测性地执行指令。

背景知识:Spectre攻击的理论和实践

下面,我们开始介绍成功实施Spectre攻击所需的先决条件,并给出在现实环境中针对现实目标实现这些先决条件的方法,与现有的针对Spectre类型漏洞的PoC攻击不同,后者采用人工方法来满足这些条件。要想成功实施这种攻击,需要满足三个前提条件:

  1.     足够大的预测窗口:攻击者通常会通过驱逐确定条件分支结果所需的数据来实现这一点。因此,攻击者必须能够从各级cache中驱逐受害者的所有数据(在PoC中将通过clflush或类似指令实现这一点)。
  2.     存在可用的任意读取原语:它必须是受害者代码的一部分,并且必须在预测窗口内执行(POC通常与受害者代码一起编译该原语)。
  3.     存在可靠的侧信道:最常见的选择是CPU cache,其状态可以通过任意读取原语进行修改,以便攻击者随后可以读取信号(对于POC来说,最常用、噪音最低的选项是攻击者和受害者之间的共享内存区)。

这些先决条件适用于所有Spectre类型的攻击,包括SPEAR。

对于这些先决条件,我们将在下文中设法加以满足。但是,在此之前,我们将先介绍栈缓冲区溢出漏洞,以及如何通过预测型SSP绕过技术来利用这种类型的漏洞。

libpng-1.2.5:栈缓冲区溢出漏洞

我们挑选的栈缓冲区溢出漏洞示例代码如下所示。

void /* PRIVATE */

png_handle_tRNS(png_structp png_ptr, png_infopinfo_ptr, png_uint_32 length)

{

  ...

  png_bytereadbuf[PNG_MAX_PALETTE_LENGTH];

  ...

  if(png_ptr->color_type == PNG_COLOR_TYPE_PALETTE) {

    if(!(png_ptr->mode & PNG_HAVE_PLTE))

    {

     /* Shouldbe an error, but we can cope with it */

    png_warning(png_ptr, "Missing PLTE before tRNS");

    }

    ...

   png_crc_read(png_ptr, readbuf, (png_size_t)length);

   png_ptr->num_trans = (png_uint_16)length;

  }

  ...

}

其中,png_handle_tRNS函数(用于处理透明度)分配了一个栈缓冲区,并用攻击者控制的数据填充它。另外,拷贝堆栈缓冲区的字节数由参数length决定,该参数实际上也是处于攻击者控制之下的。如代码所示,在调用png_handle_tRNS前没有找到PLTE(用于调色板)的孤立情况下,由于缺乏对最大栈缓冲区长度的检查,因此,攻击者可以随意写入payload,从而导致缓冲区溢出(>2000B)。

使用SSP选项编译库时,则不会出现该漏洞,这是因为如果金丝雀不匹配将导致程序中止,从而挫败相应的攻击。

漏洞利用策略

我们可以利用金丝雀完整性检查的错误预测和程序中止之间的预测窗口,预测性地运行攻击者控制的payload,从而绕过SSP提供的内存保护机制。

我们针对png_handle_tRNS中受SSP保护的栈缓冲区溢出实现了一个端到端的exploit。对于传统的漏洞利用方法来说,攻击者需要精心构造合适的payload并将其发送给受害者;与之不同的是,我们的漏洞利用方法无需满足下面的前提条件:

  1.    CPU必须对栈金丝雀的完整性检查进行错误预测。
  2.     较长的预测窗口(在png_handle_tRNS进行金丝雀检查之前从所有级别的cache中驱逐出栈金丝雀)。

对于第一个前提,我们的绕过方法是使用简单而有效的方法来完成PHT训练:在攻击之前,我们反复向受害者发送正确的PNG文件进行解析,这样的话,CPU的预测行为就会采用“正确的金丝雀”路径,而不是“劫持的金丝雀”路径。

对于第二个前提,我们利用了内核页帧分配机制,这样就可以为受害者提供一个以前由攻击者控制的页帧来放置它的金丝雀。在攻击之前,我们使用Flush+Reload为该内存页找出正确的逐出组(eviction set)。这样的话,攻击者就能使用预计算的逐出组,成功将栈金丝雀逐出。

构建任意读取原语

假设我们已经有了一个足够长的预测窗口来运行攻击性payload,那么,下一个挑战就是找到一个任意读取原语。为此,我们将通过cache的侧信道漏洞来泄露受害者数据。为了将受害者的数据放到cache中(通过侧信道漏洞发送数据),攻击者需要设法让受害者预测性地运行一个Spectre v1型的gadget。这样的话,攻击者就可以通过探测共享内存区域的缓存数据,从缓存中获取受害者数据(通过侧信道接收数据)。

不过,Spectre v1型gadget非常稀有,所以,我们选择通过将ROP gadget链接起来构建一个。概括来讲,Spectre v1型gadget的任务,就是完成数组访问。而数组的长度通常是256,这样它就涵盖了所有的字节值。每个数组元素的大小为256B(见注2)。下面是一个Spectre v1型gadget的示例代码:

; Place array and index in registers

mov rax, [ba se_loc]

mov rdx, [index_loc]

; Multiply with 256

shl rdx, 8

; Compute final address

add rax, rdx

; Array element access

mov rsi, [rax]

鉴于这个小工具的复杂性有所降低,我们发现Spectrev1型的ROP Gadget构建模块在不同的设置中具有不同的成功率。另外,这些代码的来源包括不同的系统库(libc、libpthread、libpng)。

Exploit总结

1.png

上图概述了执行端到端攻击所需的步骤。其中,步骤1和2为攻击的准备阶段,其任务是识别逐出组(以确保存在适当长度的预测窗口),并刷新侧通道使用的内存,并在指令高速缓存中启动ROP gadget。然后,攻击者将在步骤3中向受害者提交payload,这是为了利用栈缓冲区溢出漏洞而精心编制的。虽然SSP机制可以防御针对传统的内存安全漏洞的攻击,但是,攻击者仍然可以利用预测执行特性发动攻击,从而绕过传统的防御机制,覆盖返回地址,从而劫持预测型控制流(步骤5)。因此,在步骤6中,受害者被诱骗,从攻击者选择的内存的侧信道来发送数据:这一步是通过ROP组件实现的,该组件重用受害者程序的代码片段;实际上,这些代码片段在初始化阶段就被选中并作好了相应的准备。然后,攻击者可以在步骤7中通过侧信道完成数据的接收。我们可以通过先前为栈金丝雀计算的逐出组来并发执行一个驱逐循环,以延长预测窗口(步骤4),进而提高攻击的成功率。

栈金丝雀的逐出

攻击的第一步是为受害者找出正确的逐出组,以便逐出栈金丝雀。这些组将在步骤4的驱逐循环中用到,从而为步骤5和步骤6执行的SPEAR + ROP攻击阶段做好铺垫。在执行到金丝雀完整性检查时,由于受害者的栈金丝雀已经从cache中逐出,导致金丝雀值加载过程中缓存未命中,从而给攻击者提供了一个足够大的预测窗口来完成SPEAR(SPEculative ARchitectural control flowhijacking,基于预测架构的控制流劫持)攻击。

为了解决金丝雀驱逐问题,我们想出了一种新颖的驱逐受害者数据的方法,因为现有的方法会对ROP执行(步骤6)的成功产生高额惩罚。与最先进的cache驱逐解决方案相比,我们的方法具有非侵入性和无噪音的优势。我们优化了步骤4中接触到的cache组的数量,即每个cache片中只保存一个cache组(共8个cache组,见注3),从而降低了意外驱逐攻击关键数据(如ROP Gadget)的机会。

栈金丝雀的驱逐由三个阶段组成。第一阶段对应于步骤1,为金丝雀找到相应的逐出组,这是在攻击的准备阶段实现的(我们的目标受害者实例还没有启动)。我们使用两个不同的进程完成攻击,分别称为进程A1和A2,并且它们可以通过一些信道(我们选择共享内存)进行通信。需要注意的是,进程A2是用SSP选项进行编译的。A2的金丝雀逐出组检测循环将在A1和A2中同步运行。前者按顺序加载每个可能的LLC逐出组,同时后者对其金丝雀执行FLUSH+RELOAD。对于其中的逐出组,A2会遇到缓存未命中情况,并将其报告给A1。结果,A1就获得了与A2的金丝雀地址所对应的LLC逐出组(见注4)。

这种金丝雀逐出方法的其余部分基于这样的观察,即用于存储A2的金丝雀的页帧(TLS页,见注5)随后可以供系统中的其他进程使用,在某些情况下,甚至可以用于相同的用途。

金丝雀逐出的第二阶段,将迫使目标受害者实例重用A2的TLS页。由于Linux内存分配器的行为具有确定性,所以,当攻击者控制的进程通过受害者进程(借助于exec运行时,攻击者就可以利用内存页重用将成功率提高到100%。

小结

在本文中,我们为读者详细介绍了一种通过劫持预测控制流绕过栈溢出保护机制的漏洞利用方法,由于篇幅较长,我们分为两篇发表。更多精彩内容,敬请期待!

 

未完待续

 

原文地址:https://ibm.github.io/system-security-research-updates/2021/06/18/spear-attacks-ssp-usecase

最新评论

昵称
邮箱
提交评论