Usenix 2015的绕过CFI的一篇论文。
简介
CFI是针对控制流劫持攻击的防御机制。最近的研究表明,粗粒度的CFI不足以阻止这种攻击,而细粒度的CFI公认为安全的。
作者认为评估CFI的有效性是很重要的,而普通的评估不能评估CFI的有效性。作者对最精确的静态CFI进行了评估,并且发现细粒度CFI存在一些安全问题。基于一个叫Control-Flow Bending(CFB)的非控制数据攻击,文章展示了一个攻击者如何利用内存破坏类漏洞和标准库的调用来在内存上实现图灵完备的计算。文章使用这种攻击技术来评估六个真实二进制程序上的完全精确的静态CFI防御机制,结果表明其中五个都可以实现攻击。我们的结果表明CFI可能不是一个很可靠的针对内存漏洞的防御机制。
同时,作者还用CFB这种攻击技术对CFI和影子栈相结合的防御机制并进行评估。结果表明,引入了影子栈能够降低攻击者的任意代码执行能力。
背景(软件攻击)
过去的几十年里,最普遍的攻击方式是针对内存不安全语言的内存破坏类漏洞的利用。
详细的可以看13年S&P的SoK:Eternal war in memory
控制流劫持
一个利用内存损坏类漏洞的方法是劫持程序的控制流到应用内部的地址空间。这种方法利用内存损坏漏洞来改变非直接分支指令(ret,jmp,call)的目标。通过这种方式,攻击者可以完全控制下一条指令的执行。
代码复用攻击
DEP防止执行攻击者注入的代码。但是,劫持控制流到一个内存现有的可执行代码是可行的。比如,ret2libc就是复用漏洞进程地址空间的函数。而且,一些运行库(libc)提供了很多很强大的函数,比如libc的system()函数,可以运行攻击者执行shell命令。当攻击者需要的代码在漏洞进程存在的话,代码复用攻击是可行的。
Return Oriented Programming
ROP是代码复用攻击的一种更高级的形式,可以允许攻击者通过复用现有代码来实现任意计算。这种攻击基于以非直接分支指令结尾的指令序列(gadget),这就可以让他们链接起来,所以攻击者通过精心构造一系列gadget来实现任意代码执行。
Non-Control-Data Attacks
非控制数据攻击指的只用内存损坏漏洞来损坏数据,而不是代码指针。这种攻击也是有效的,比如,损坏一个敏感函数的参数(libc的execve())可以允许攻击者执行任意程序。也可以覆盖安全设置的值来关掉某些安全检查。non-control-data 攻击是实际攻击,并且很难防御,因为大部分的防御机制侧重于保护代码指针。
Control-Flow Bending
论文引入了一种通用的non-control-data攻击。由于non-control-data attack不直接修改控制流数据,在CFB里,论文允许修改非直接分支指令的目标,只要不违背CFI的规则。CFB允许攻击者去弯曲控制流,但不违背安全规则。
论文定义了data-only attack为non-control-data attack,当且仅当完整的执行路径和正常执行的路径是一样的。虽然data-only attack可能会修改应用程序的控制流,但踪迹看起来也是合法的,因为观察的执行路径是在正常执行的时候发生的。(这里看得有点疑惑??)
相比之下,CFB更通用,他符合CFG的规则,但是可能和正常执行的路径不同。
通常来说,防御机制实现在一个抽象的机器上,而且只能根据机器的限制来观察到违反安全的情况。比如一个攻击者直接修改execve的参数来实现data-only attack,因为没有改变程序的控制流。一个攻击者通过一次请求修改了is_admin标志位是non-control-data attack,因为覆盖的是非控制数据,并且影响了程序的控制流。一个攻击者修改了一个函数指针来指向一个合法的调用目标叫做CFB攻击。
(这定义看起来和我想象的不一样==)
威胁模型及攻击目标
假设:
- 假设攻击者能够在程序运行时的一个点任意修改内存。
- 进程运行时在数据不可执行,代码不可写的保护下。
攻击目标:
- 任意代码执行
- 限制代码执行
- 信息泄露
对CFI的评估
目前针对CFI的评估都比较粗糙,比如这种Average Indirect target Reduction(AIR)或者Gadget Reduction这种。用这种方式来评估根本就没有回答CFI能不能抵御真实的攻击。这个部分主要就是说AIR和Gadget Reduction这种指标不足以来说明CFI的有效性。
AIR和gadget reduction
AIR metric主要是测量有无CFI时,程序中的非直接分支指令的合法目标的平均数目减少了多少。Gadget reduction也是测量能找到的gadget的减少了多少。这些评估方法测量CFI能够多有效地减少非直接分支指令的合法目标(平均)。
AIR存在两个缺点:
- 不知道每个地址的目标减少的数量(比如一个方案有很高的AIR,但其中的一个分支指令有很多的目标,而其他地址的几乎被优化掉了。)
- 不能确定允许可控跳转的重要性和风险。
gadget reduction缺点在于认为每个代码定位或者gadget都是同等有用的。
CFI的有效性
评估CFI的有效性应该从能够否实现攻击者的目标来进行。
基本的利用测试
提出了一个基本的利用测试(Basic Exploitation Test),通过测试不代表CFI是安全的,但不通过BET,说明CFI是不安全的。
BET由一些带漏洞的小程序组成,允许功能攻击者修改返回地址或者非直接jmp/call指令的目标。并且这些程序应用了一些CFI方案。如果一个CFI方案连小程序都无法保护,更何况大型的程序。因为大程序给攻击者更多的机会去攻击。
针对粗粒度CFI的BET
这一小节用一个小程序,来说明粗粒度的CFI是不安全的。并且AIR和Gadget Reduction的参数不能说明CFI的安全性。
#include <stdio.h>
#include <string.h>
#define STDIN 0
void memLeak () {
char buf [64];
int nr , i;
unsigned int *value;
value = (unsigned int*)buf;
scanf("%d", &nr);
for (i = 0; i < nr; i++)
printf("0x%08xŠ", value[i]);
}
void vulnFunc () {
char buf [1024];
read(STDIN , buf , 2048);
}
int main(int argc , char* argv []) {
setbuf(stdout , NULL );
printf("echo >Š");
memLeak ();
printf("\nread >Š");
vulnFunc ();
printf("\ndone .\n");
return 0;
}
针对细粒度CFI的攻击(重点!)
这章主要是介绍CFB攻击是怎么实现的。
比如下面两个基本块A、C都调用了基本块B,正常来说,基本块A的执行路径是1->2,基本块C的执行路径是3->4。但是攻击者可以实现3->2,1->4这种操作。这也不会影响控制流图,但却改变了程序的执行流。
当出现了以下情况,会使得这种情况更复杂:
- 尾递归调用的优化
- 使用信号处理器
- 程序调用了setjmp/longjmp
作者称这种在CFG有两条出边的函数B,为dispatcher function。通过覆盖这个dispatcher function的返回地址,可以弯曲程序的控制流,又不违反程序的控制流图。
作者整理了几个调度器函数如下:
- memcpy()
- printf():使用%n,可以写任意值到任意位置,所以就可以覆盖printf自己的返回地址。
- strcat()
- fputs
利用这种思想还可以实现一个循环。比如下面这个例子,正常的执行顺序是A调用了函数B(edge1),B返回到A(edge2)。然后执行到后面,A又调用了B(edge3),B又返回到A(edge4)。利用CFB的思想来注入循环的话,与正常不同的是在第二次调用返回的时候,不走edge4,而是走edge2。这样就构成了一个循环。
作者还针对printf()提出了一种图灵完备的计算方式,成为Printf-Oriented Programming,开源在github上:https://github.com/HexHive/printbf
针对Fully-Precise Static CFI
实验结果如表格所示。主要说明了两点:
- 攻击者可以基于已有的漏洞来任意控制内存
- 只要攻击者能够在某个时刻控制内存,就可以实现CFB攻击。
结论
这篇文章强调了影子栈与CFI结合的重要性。同时也指出了无状态CFI的缺陷。(所以后来才有上下文敏感的CFI)。
更高层面来说,本文的工作提出了一个问题:防御要做到多安全才能防止漏洞被触发利用。对于CFI来说,只提供了一部分,但不是完整的防御。评估其他防御机制仍然是未来一个研究热点。