控制流完整性(CFI)发展简述

控制流完整性(CFI)发展简述

0x00 从 shellcode 说起

在二进制安全中,大部分的漏洞利用方式是劫持控制流,接着使程序按照攻击者的攻击思路运行下去,使得攻击者获得目标程序的控制权,甚至还可以进行提权来全面控制目标机器。

在软件安全保护技术还不是很发达的年代,我们如果想劫持程序的控制流,一般的思路是通过栈溢出控制程序中某个函数的返回地址,然后在该地址上布置 shellcode,将控制流劫持至 shellcode。为了阻止此类攻击, 在硬件的支持下,现代操作系统都增加了 DEP(Data-Execution-Prevention, NX or W⊕X) 机制,通过限制内存页不能同时具备执行权限和写权限,即隔离数据与代码来阻止程序运行攻击者的恶意数据。

为了绕过 DEP 的保护,一种可行的攻击手段是代码重用攻击,即利用程序中代码段中的代码片段,拼接成恶意代码进行攻击,代码重用攻击手段包括 Ret2Libc、ROP、JOP 等。当受害者程序代码量足够大时,我们是可以找到足够的 Gadgets 来进行 ROP 攻击,绕过 DEP 保护,最后达到攻击者的目的。

0x01 CFI?

为了抵御劫持控制流的攻击,2005 年 CCS 一篇论文:Control-Flow-Integrity(自行下载)提出了 CFI 的概念。CFI 防御机制,其核心思想是限制程序运行中的控制转移,使程序始终处于原有的控制流图所限定的范围内。具体做法是是通过分析程序的控制流图 (CFG),获取间接转移指令(包括间接跳转、间接调用和函数返回指令)目标的白名单,并在运行过程中,核对间接转移指令的目标是否在白名单中。控制流劫持攻击往往会违背原有的控制流图,CFI 使得这种攻击行为难以实现。

从实现角度上看,CFI 也有粗粒度和细粒度上两种。细粒度 CFI 严格控制每一个间接转移指令的转移目标,这种精细的检查,在现有的系统环境中,通常会引入很大的开销。而粗粒度 CFI 则是将一组类似或相近类型的目标整理到一起进行检查,以降低开销,但这种方法会导致安全性的下降。

0x02 基于 PIN 的 CFI

在上一节提到的论文中,程序在其执行过程中,应当遵循预先定义好的控制流图,以确保程序控制流不被劫持或非法篡改,背离程序编制时所设计的控制流转移关系。CFI 在运行时检测程序的控制转移是否在控制流图中,以识别是否遭遇了攻击。具体作法是在控制流转移指令前插入检验代码,来判断目标地址的合法性。这种做法能够对控制流劫持攻击起到防御作用,但是也存在一些问题。

严格意义上的 CFI,需要做到对每条间接控制转移都做检查,并确保每条转移指令只能转移到它自身的目标集合。但是,CFI 未被广泛应用的原因是插桩引入的开销过大,需要额外的信息(比如间接控制转移可能的目标集合)的支持,且不能进行增量式的部署。

因此,新的策略是对间接调用指令和函数返回指令的目标进行区分,阻止未经验证的返回指令跳转到敏感函数的行为。这就是 CCFIR(Compact Control Flow Integrity and Randomization)。CCFIR 是一个纯粹的二进制转换程序,不依赖源码或调试信息,所依赖的仅是重定位表中的信息。受保护的代码可以被独立地验证,也可以进行增量式的部署。

此外,还有一种 CFI 机制是 binCFI,将间接转移指令的操作数分为代码指针、异常处理程序入口、导出符号地址和返回地址等类型,通过精细的静态分析,对不同类型的间接控制转移,收集它们的合法目标集合,如返回指令的合法目标集合就是所有函数调用指令的下一条指令的地址,导出符号地址集合就是 ELF 文件中 .dynamic 段中存储的地址。利用这种方法,binCFI 可以得到与已有 CFI 方案相当的安全性。通过新增代码段的方式,不改变原有的代码,达到完全透明的目的。

严格意义上,CFI、CCFIR、binCFI 都属于粗粒度的 CFI 机制,粗粒度的 CFI 可以降低开销,但是会带来安全上的问题。

0x03 粗粒度下 CFI 的攻击手段

详细攻击流程读完论文后我再补充

2014 年的论文《Out of Control: Overcoming Control-Flow Integrity》,DOI: 10.1109/SP.2014.43,中提到了一种攻击手段。他们利用了两种特殊的 Gadget:entry point(EP) gadget 和 call site(CS) gadget,来绕开粗粒度 CFI 机制的防御。

2015 年的论文《Losing Control: On the Effectiveness of Control-Flow Integrity under Stack Attacks》,DOI: 10.1145/2810103.2813671,也提到了对 CFI 保护下的栈的攻击手段。在此论文发表前,通过影子栈(Shadow Stack)来检测函数返回目标,再加上 DEP 和 ASLR 的保护,栈应该会变得非常安全,但是事实并非如此。这篇论文中提到了三种攻击手段,他们提出了三种攻击方法:一是利用堆上的漏洞来破坏栈上的 calleesaved 寄 存 器 保 存 区 域, 使得calleesaved 寄存器被劫持;二是利用用户空间和内核之间进行上下文切换的问题,来劫持 sysenter 指令,使控制跳转到攻击者想跳转的位置;三是通过泄露主栈的地址来泄露出 shadow stack 的地址,进而进行攻击。

0x04 上下文敏感的 CFI

以往 CFI 方案的问题是只实施了控制流不敏感策略,粗粒度 CFI 则是将一组类似或相近类型的目标归到一起进行检查,这种检查还是有一些问题的。因此,上下文敏感的 CFI(Context sensitive CFI)应运而生。它依赖于上下文敏感的静态分析,将 CFI 不变量和 CFG 中的控制流路径联系到一起,运行时在执行路径上强制执行这些不变量。

2015 年论文:《CCFI: Cryptographically Enforced Control Flow Integrity》, 提出了一种通过对代码指针加密的方法来增强 CFI 的保护。(个人观点:用脚趾头都想的到这是理论上可行现实中做不到的脑洞,因为开销实在是太大)这个观点出发点是好的,但是在大部分硬件效率跟不上的情况下,几乎不可能在现实中运用,因此 CCFI 被废弃。

2014 年的论文:《Complete Control-Flow Integrity for Commodity Operating System Kernels》,他们在操作系统的内核上实现了 CFI,使之免受控制流劫持等攻击,这个系统被称为 KCoFI。他们在基于标签的控制流间接转移保护的基础上,加入一个运行时监控的软件层,负责保护一些关键的操作系统数据结构和监控操作系统进行的所有底层状态操作。(个人观点:这个系统加入了实时监控系统底层状态操作,如果是高 IO 的情况下,性能可想而知

0x05 总结

有次和马老师聊天提到了 CFI,他的观点是,粗粒度的 CFI 本质上不安全,而细粒度的 CFI 性能太差。

末日が期待し 2:14:01 PM
cfi貌似已经凉了
末日が期待し 2:15:09 PM
都凉了,本质缺陷
末日が期待し 2:16:00 PM
16年就已经基本凉透了
-SkyΞ- 2:17:43 PM
就是因为有control flow bending 然后cfi就凉了?
-SkyΞ- 2:18:37 PM
我觉得也不能这么说,实际用起来的东西和这些理论还是有一点距离的
末日が期待し 2:18:42 PM
也不是,因为粗粒度cfi本质上不安全,细粒度cfi性能太差
末日が期待し 2:19:12 PM
你看完那些用来绕过cfi的rop设计就不会这么想了

我也认同他的观点... 精确的 CFI 需要进行大量的静态、上下文敏感分析,因此带来了巨大的开销,而为了降低开销,粗粒度 CFI 放宽了检查条件,缺给敌手留下了足够的利用空间。

那为什么说 CFI 已经凉了呢?(因为 Data oriented programming

CFI 技术已经提出了 10 多年,虽然说在最新的攻击手段下已经失去了保护应用程序的能力,但会不会有一天在数据导向编程提出的某天之后,会有数据流完整性保护呢?这还是值得我们期待的。 这个地方失误,实际上已经有 DFI 的相关成果了。

留下你的脚步
推荐阅读