二进制代码块: 面向二进制程序的细粒度控制流完整性校验方法

2016-02-20 03:23王明华AbhishekVasishtBhaskar苏璞睿冯登国
信息安全学报 2016年2期
关键词:控制流二进制结点

王明华, 尹 恒, Abhishek Vasisht Bhaskar, 苏璞睿, 冯登国



二进制代码块: 面向二进制程序的细粒度控制流完整性校验方法

王明华1,2, 尹 恒2, Abhishek Vasisht Bhaskar2, 苏璞睿3,4, 冯登国4

1百度X-Lab2雪城大学3中国科学院软件研究所计算机科学国家重点实验室4中国科学院软件研究所可信计算与信息保障实验室

控制流完整性(CFI)是一种在程序中通过保护间接转移有效减少代码注入和代码重用攻击等威胁的技术。由于二进制程序缺少源代码级别的语义, CFI策略的设定需要很谨慎。现有的面向二进制的 CFI解决方案, 如BinCFI和CCFIR, 虽然能够提供对二进制程序的防护能力, 但它们应用的策略过于宽松, 依然会受到复杂的代码重用攻击。本文提出一种新的面向二进制的CFI保护方案, 称为BinCC。它可以通过静态二进制重写为x86下的二进制程序提供细粒度保护。通过代码复制和静态分析, 我们把二进制代码分成几个互斥代码块。再进一步将代码中的每个间接转移块归类为块间转移或块内转移, 并分别应用严格CFI策略来限制这些转移。为了评估BinCC, 我们引入新的指标来评估每种间接转移中合法目标的平均数量, 以及利用call-preceded gadgets产生ROP漏洞利用的难度。实验结果表明与BinCFI比较, BinCC显著地将合法转移目标降低了81.34%, 并显著增加了攻击者绕过CFI限制实施复杂的ROP攻击的难度。另外, 与BinCC可以降低大约14%的空间开销, 而只提升了4%的运行开销。

控制流完整性

1 前言

ASLR[22]和DEP[2]能够缓解传统漏洞威胁。然而, 即使ASLR和DEP开启, 攻击者仍然能够通过代码重用实施攻击[4,21], 例如ROP[20]就是这样一种代码重用技术。近年来, 一些研究工作[3][6-8][13][19][24]已经提出针对代码重用攻击的解决方案, 并且取得了一定的成果。

控制流完整性[1]在缓解控制流劫持攻击中扮演着重要的角色, 其主要思想是依据控制流图(CFG)来限制程序中的控制流转移。对于具有源代码的程序, CFG图的构造趋于完整, 能够依次提出严格的CFI约束策略。但是, 对于二进制程序而言, 由于缺乏源代码或调试信息, 无法构造完整的CFG信息, 所以提出的CFI策略仅能是粗粒度的。尽管诸如CCFIR和BinCFI等CFI解决方案可以阻止绝大多数控制流劫持威胁, 但是它们仍然可能受到利用call-preceded实现的复杂ROP的攻击[5][10]。

本文提出一个新的面向二进制程序的CFI保护方法——BinCC, 旨在为二进制程序提供更细粒度的保护。具体的方法是: 通过复制少量的代码, 并执行静态分析, 把二进制代码分成几个互斥代码块, 并将每个间接转移分为块间转移或块内转移。接下来提出严格的CFI策略来约束这两种控制流转移, 其中, 块内转移只允许到达当前块内的目标, 块间转移只允许到达代码块中特定类型的目标, 从而显著地缩减了控制流转移合法目标的数量。

为了评估BinCC, 我们引入新的指标来评估每种间接转移中合法目标的平均数量, 以及利用call- preceded gadgets实现ROP利用攻击的难度。与BinCFI相比较, 实验结果表明BinCC在这两方面均有显著的提高。其中, BinCC将合法转移目标降低了81.34%。尤其, BinCC对returns指令(ret)提供细粒度的保护, 将合法目标数量平均降低了87%, 显著增加了通过call-preceded gadget实现的复杂ROP攻击的难度。除此之外, BinCC还具有合理的性能。相比BinCFI, BinCC运行开销仅增加4%, 而空间开销降低14%。

总言之, BinCC做出了如下贡献:

• BinCC通过代码复制和代码块构造, 将程序中的间接控制流转移分类为块内转移和块间转移, 并分别实施了细粒度的CFI策略。

• 与BinCFI和CCFIR相比, BinCC显著缩减了间接控制流转移(尤其是returns指令)的合法目标数量。

• BinCC不仅可以缓解常见的控制流劫持威胁, 而且能够显著增加了利用call-preceded gadget实施复杂ROP利用攻击的难度。

• BinCC加固的程序具有合理的性能。比起BinCFI, BinCC能够将加固的程序大小减少约14%, 运行时间增加约4%。

内容组织如下。在第二章中讨论背景和相关技术, 接着在第三章中介绍代码块的概念和CFI策略。在第四章中介绍代码块结构, 在第五章中介绍CFI的实施方法。第六章为实验评估。第七章探讨本文方法的局限性。第八章对本文进行总结。

2 背景和相关技术

CFI解决方案包括基于源代码和基于二进制两类。由于在实践中我们获得的二进制程序大部分都是没有源码的, 所以我们将侧重点放在基于二进制的解决方案上。为了说明问题, 我们对CCFIR和BinCFI的实现方式, 以及针对它们的攻击方式进行讨论。

2.1 基于源代码的CFI

许多研究工作[3][11][12][16][23]依赖于源代码来提出CFI的防护策略。研究工作[11][23]主要侧重于保护虚函数的调用。主要思路是利用类继承结构分析来识别合法目标, 并插入检查代码来实现类方法和vtable检查。这两种解决方案都可以为虚函数调用操作提供细粒度保护, 但无法对返回操作进行保护。CFL[3]为在每个间接转移前加一个锁操作, 并仅在合法地址处进行相应的解锁操作。这个方法与我们的方法在相关调用函数返回上类似, 但是它依赖于源码, 而源码在实际应用中并不容易得到, 而且这个方法缺乏对模块化支持。MCFI[15]是一个支持模块化的CFI解决方案。它使用几个地址表来存储间接转移的合法目标, 并在模块动态加载时使用辅助类型信息来更新目标。在运行时, MCFI通过执行特定的校验例程, 对间接控制流转移的合法性进行校验。

RockJIT[16]是MCFI[15]工作的扩展, 它可以缓解与JIT代码相关的控制流攻击, 可以阻止由JITed代码引起的控制流攻击。通过使用JIT编译器的源代码来计算程序的CFG, 并通过运行时产生的动态代码来更新CFI策略。CPI[12]提出了指针代码完整性和指针代码分离的思想。通过有选择地保护易受控制流劫持攻击的指针访问, 该方法可以保障程序控制流安全。

2.2 基于二进制的CFI

研究工作[14][24][25]是基于面向二进制的解决方案。O-CFI利用一个查询表来存放合法控制流转移目标地址, 辅助O-CFI在运行时对转移目标的校验。O-CFI额外使用了随机化技术对程序中所有代码块进行了随机化处理, 减低了程序遭受信息泄露的风险。SFI[24]是一种沙箱技术, 用来实施控制流完整性保护。其基本思想是使不可信的模块在相同的程序地址空间内执行, 不允许其访问其他程序的数据和代码。PittSFIled[13]和NaCl[25]是基于SFI实现的, 用于保护局部代码安全, 而且其限制间接转移的目标也需要满足内存对齐的要求。

Lockdown[17]使用影子栈对ret指令进行约束。然而, 影子栈可能引入高的运行开销, 同时为了维护call/ret对, 需要更多的内存读和写操作。此外, 影子栈需要存储在安全内存域中, 这需要由硬件或类似SFI的分离技术来提供支持。安全内存域也可能存在信息泄露的风险, 能够被攻击者利用, 如文献[9]中一个真实的攻击实例。

CCFIR[26]是一个面向Windows x86 PE二进制程序的CFI解决方案。它将间接转移的目标安排在一个称为“springboard”的新节中。不同类型的跳转目标在springboard按照不同的粒度对齐。在间接控制流转移时, 首先跳转至springboard中, 检测转移目标地址是否按照某种粒度对齐。如果对齐, 控制流会由springboard中对应stub函数接管, 继续执行; 否则, 转移目标被认为不合法。BinCFI[27]是另一种面向Linux x86 ELF二进制程序的CFI方案。BinCFI通过反汇编二进制码, 改写间接控制流转移指令, 再将最终代码放入新代码段。对于每个指令, 它都维护一个原位置与新位置的映射表。当间接转移发生时, BinCFI会使其跳转至相应的地址转换例程, 在地址转换例程中完成对目标地址的查找和转换。除此之外, BinCFI还对模块化具有很好的支持。

CFG依赖二进制程序自身, 因此无法精确表示, 所以提出的CFI策略都是粗粒度的。虽然CFI策略能够减少大量普通控制流攻击, 但允许的策略仍然可能被攻击者绕过。图1显示了针对CCFIR和BinCFI可能的攻击方式。实线表示运行时的执行流, 虚线表示可能被攻击的路径。对CCFIR来说, 如图1(a)所示, springboard中的目标, 只要满足对齐到常量M_R, 任何调用地址都将被认为是合法的。同样, 对于BinCFI, 一个ret能够返回到二进制程序中的任何调用点, 如图1(b)所示。这些返回都是不受保护的, 因此攻击者能够利用call-preceded gadget实施ROP利用攻击。工作[10]已经证实了这种攻击的可能。

3 方法框架

本文提出一种细粒度的CFI策略, 可以显著的改善间接控制流转移中的合法目标。通过复制一些必要代码和执行静态分析将二进制程序划分为一些独立代码部分, 并将每个间接转移定义为代码块内转移和代码块间转移。因此, 我们可以执行独立、严格的策略来达到细粒度保护。下面, 我们将首先通过一个例子介绍代码块相关概念, 然后介绍CFI策略。

3.1 代码块

代码块是由函数的Super-CFG(超级控制流图)构成的。对于一个函数, 其Super-CFG是由其CFG(控制流图)构成的(Super-CFG构造算法将在第四章讨论)。通过对函数的Super-CFGs进行合并, 构建每个代码块的有向图, 而这些代码块之间两两互斥。

我们通过图2的示例进行说明。示例中二进制代码原始函数包含:,,,和。是二进制程序的入口点。图中的节点代表代码中相应的指令。在图中,CC表示由函数的Super- CFG生成的代码块。由于函数在指令3处被直接调用, 所以, 由指令5、6和7组成的函数也被包含在CC中。CC表示由函数的Super-CFG生成的代码块, 包含有四个结点。

我们将包含一个代码块中的结点分为三类: 根结点, 边缘结点和内部结点。根结点表示间接调用函数的入口, 在图中用灰色表示。例如, 1和5是CC的根结点。边缘结点代表间接转移指令, 其目标无法在计算Super-CFG时识别, 在图中用条纹表示。例如, 2、4和6都是CC的边缘结点。那些既不是根结点也不是边缘结点的结点称为内部结点, 在图中用白色表示。这些内部结点所表示的指令是那些非控制流转移指令或在Super-CFG中具有确定目标的控制流转移指令, 例如3和7。

通过划分结点, 我们可以将间接转移分为两类, 即块内转移和块间转移。块内转移是由内部结点发起的间接转移, 块间转移则是由边缘结点发起的间接转移。值得注意的是, 在本文方法中, 我们确保每个间接转移仅为块内转移或块间转移的一种, 从而实施不同粒度的约束策略。为了达到该目标, 我们在代码块构造前, 首先需要完成对一部分代码的复制。

通常二进制代码中的函数分为两类, 即间接被调函数(ICF, Indirect Called Function)和直接被调函数(DCF, Direct Called Function)。但可能存在一些函数既是ICF又是DCF。我们将两类交集部分的函数进行复制, 并将这部分新函数视为ICF。这样, 所有函数最终被归进两个互斥的集合中, 如图3所示。当程序运行时, 在一个间接调用位置, 当原始函输被调用时, 我们会实时调度来执行复制的函数。这样, 在程序运行过程中, 一个函数只会按照一种特定的方式被调用, 即间接或直接。因此, ret仅返回到一个特定类型的调用点。ret也被分成两类: 直接ret和间接ret。其中直接ret返回到直接调用点; 间接ret返回到间接调用点。

在图2示例代码中, ICF由、、、组成, 而DCF由组成。只有函数被两种方式同时调用。假设从开始执行,函数首先在9处被间接调用, 接着在3处被直接调用, 因此返回指令7应该分别返回到这两个调用点。在BinCC中,是一个通过复制函数生成的新函数, 它在指令9处被间接调用。这使得指令7变成块内转移, 指令7’变成块间转移, 也就是说7作为直接ret应该返回到3处, 而作为间接ret, 则返回到9处, 如两个虚箭头所示。

3.2 CFI策略

本小节, 我们提出块内策略和块间策略来分别约束块内和块间的间接转移。

块内策略

这类策略是用来限制内部节点描述间接转移的, 其转移目标是由静态分析确定的。这些目标通常存在于代码块的内部, 并且在构成代码块的Super-CFG中确定。在一个代码块中, 我们只关心两种间接控制流转移。一种是直接ret, 另一种是与switch-case跳转表相关的间接jmp。对于每个直接ret, 合法的目标是处于当前代码块内的对应目标调用点。对于每个间接跳转而言, 相应跳转表中所有条件分支都是合法的目标。跳转表中的条件分支可以通过静态分析确定。我们在构造Super-CFG时, 将间接跳转与它的条件分支相关联来确定其目标。

如示例所示, 这些代码块中有一个块内间接转移, 即CC中7处的ret, 它只允许返回至调用点3。

块间策略

这类策略用于限制边界结点, 这些结点的转移目标不能从Super-CFG中静态确定。根据不同的转移类型, 我们提出如下策略。

1. 间接call结点只能到达表示ICF入口点的根结点。

2. 间接ret结点只能回到表示间接call的边界结点。

3. 间接跳转结点, 不能通过静态分析得出其目标, 但可以达到根结点或表示间接call的边界结点。

这些策略的合理性在于: 在函数分成两个互斥集合之后, ICF只能被间接调用指令调用, 因此, ICF的间接返回只能回到间接调用点。间接jmp通常与跳转表相关, 这些跳转的目标可以通过静态分析得到。但是也有一些间接jmp无法通过静态分析得到, 它们的目标可能是ICF的入口点或调用点。

图2示例中, 根结点是ICF(1,5,5’,8和12)的入口点, 边界结点包含间接call结点(2,6和9)和间接ret结点(4,11,13和7’)。在BinCC中, 间接call结点(2,6和9)可以调用ICF(1,5,5’,8和13), 间接ret(4,11,13和7’)可以返回到间接call(2,6和9)。

4 代码块构造

为了构建代码块, 我们首先在二进制程序中识别所有的DCF和ICF, 然后通过控制流分析来获得这些函数的CFG, 进而计算Super-CFG, 并基于Super-CFG构建代码块。下面我们将进行深入讨论。

4.1 DCF识别

每个DCF是通过一个相关的call调用。函数的入口地址可以由call指令的地址和操作数计算得出。按照这种思路, 我们可以获得所有DCF。

4.2 ICF识别

程序中ICF地址与程序中存在的常量有关, 函数地址是常量自身, 或由常量计算而来。根据这一特点, 我们在程序中找出所有与函数地址相关的常量。

如果二进制程序中包含重定位表, 则所有被间接调用函数的地址信息都将在重定位表中。在这种情况下, 我们计算重定位表中的每个常量与代码基地址的和, 如果结果在此代码区中, 那么这个和即是一个ICF的地址。

对于不包含重定位表的二进制程序, 可能有non-PIC或PIC两种情况。在这些情况下, 我们采用同样的方法, 在二进制程序的常量中筛选ICF。

对于non-PIC模块, 我们使用一个窗口值(例如: 在32-bit的系统中为4字节)在data、.rodata、.init_array、exported symbol sections和其他可能存在表示合法代码地址的程序段中扫描常量。同时, 我们也在代码域中收集常量。结合反汇编的结果, 如果此常量, 或常量与代码基址的和是有效代码地址, 且满足一个合法指令的边界, 我们就认为它是一个候选的ICF。

对于一个PIC模块, 函数可以被PC thunks调用。因此, 除了与non-PIC执行相同的方法来收集常量之外, 我们还要识别出所有的PC thunks, 并查看是否被用作函数调用。如果是, 那么也将这些计算出来的函数地址, 加入到候选ICF中。

需要注意的是, 最终得到的常量并不都是ICF。一些常量实际上是switch-case跳转表中条件分支的地址或函数调用点的地址。这些显然不是函数入口点的地址, 所以我们去掉这两种候选常量, 剩下的则是最终的ICF。

4.3 控制流分析

接下来我们通过静态控制流分析来计算所有函数的CFG。我们尝试识别每个间接分支的所有可能的目标。这部分的主要困难在于如何分辨出间接跳转中所有可能的目标。在大多数常见的情况下, 间接jmp是用于跳转表的调度执行, 这个跳转的所有合法目标是对应的条件分支, 这些可以通过之前发现的常量识别出来。因此, 对间接jmp来说, 检查它是否与跳转表相关, 如果相关则将它与对应条件分支联系起来。

4.4 代码复制

如上所述, 为了实现本文提出的CFI策略, 我们需要复制ICF和DCF的交集。然而, 某些情况下, 由于编译器的优化, 一些ICF可能与DCF拥有相同的ret, 并且这些ret无法明确地定义其转移类型(块内或块间)。为了解决这个问题, 如果ICF与DCF这两个函数拥有同样的ret, 我们复制ICF。这个复制的函数在二进制程序中会成为一个新的ICF。

接下来我们复制函数CFG中所有指令。在添加CFI校验策略(在第五章讨论)之后, 所有代码被放置于新增的代码段中。源代码段被标记为不可执行, 而所有数据段都保持不变。被复制指令中的常量也没有变动, 所以这些指令在执行时, 仍然能够保证对数据段的正常访问。

4.5 代码块构造

代码块由函数的Super-CFG构成。算法1说明了如何计算一个函数的Super-CFG。首先在函数中定位所有的直接调用, 然后通过在直接调用处加上被调用函数的Super-CFG。这个函数引入了两条新的边, 一条由直接调用点(如, 结点i)指向被调用点的入口, 另一条由被调用点返回到直接调用点。

算法1.SuperCFG(CFGfunc): 基于CFG计算func的Super-CFG

输入: CFGfunc: 计算func函数的CFG

输出: sg: func的super-graph

1: sg ← CFGfunc2: FOReachi∈Nodes(CFGfunc) DO3: IFiis a direct call to fTHEN4: sg = AddSuperCFG(sg,i,SuperCFG(f))5: END IF6: END FOR7: RETURN sg

算法2显示了如何构造代码块和在代码块中划分结点。此算法的输入为所有ICF。首先为每个ICF计算Super-CFG, 然后基于共同边(例如, 共同的被调用点)合并这些Super-CFG。这一过程通过实现。这个算法最终得到互斥的代码块集合, 每一个代码块包含了划分好的根结点、边界结点和内部结点。从此算法中可以看到, 根结点来自当前块中ICF的入口点。边界结点来自间接调用()、间接返回()和无法静态确定目标的间接跳转(ijmp)的结点。内部结点则由表示当前块中非控制流指令和目标已确定的控制流转移结点组成。

我们还考虑了一种特殊的情况, 即可能存在无法进行静态控制流分析的孤立的代码块。例如, 一个间接跳转未的目标分支或是dead code等。考虑到这部分代码也可能执行, 所以也需要制定CFI约束。为此, 我们为每个孤立代码块生成了一个代码块。孤立代码块由Super-CFG组成, 此Super-CFG由算法1生成。其中, 代码块中的指令被看作“CFG”(记作CFGfunc)。并且, Super-CFG中的(如), 是为边界结点, 它由、和ijmp组成。Super-CFG的(如)为内部结点。考虑到孤立块不能作为函数, 因此不能被间接call调用。我们将孤立块中的入口当作边界结点, 而不是根结点。

算法2.ConstructCC(ICFs): 将ICFs作为输入生成出代码块

输入: 所有ICFs: ICFs

输出: 所有代码块: CC

3:=(CFG);=

5: IFis aicall or aijmpor aTHEN

6:={}

7: ELSE

8:={}

9: END IF

10: ENDFOR

12: ENDFOR

14:cc=;SG=SG{}

15:cc.border=cc.border{}

16:cc.inner=cc.inner{}

17:cc.root=cc.root{}

19: IF(,) THEN

20:cc=(cc,)

21:cc.border=cc.border{}

22:cc.inner=cc.inner{}

23:cc.root=cc.root{}

24:SG=SG{}

25: END IF

26: ENDFOR

28: ENDFOR

29: RETURN

5 CFI实施

在构造代码块之后, 利用二进制重写方法来写入CFI约束规则。此部分主要通过对BinCFI进行扩展来实现。下面, 我们首先简要阐述BinCFI的基本架构, 然后再详细讨论为实施本文CFI策略所做的扩展和修改。

5.1 基础架构

BinCFI基于反汇编结果对间接指令进行改写, 将最终的代码插入到新生成的代码段中, 并修改原代码段使其不可执行。对于原代码段中的每一条指令, BinCFI使用形如的地址对, 将这条指令在原代码段中的地址关联到新代码段的地址。这些地址对用来进行从原代码段到新代码段中的地址转换。BinCFI对所有间接转移目标产生地址对, 并用两个不同地址转换哈希表存放, 。其中, 一个哈希表用于维护ret指令的合法目标, 另一个用于维护间接jmp和call的合法目标。所有地址转换表都是只读属性。

BinCFI对间接call/jmp和ret进行改写。对于与跳转表相关联的间接jmp, 它们的操作数被改写为*(CE+Ind)+CE这种形式, 其中CECE为常量,CE表示跳转表相关,*(CE+Ind)表示所有可能的条件分支。另外, BinCFI基于每个CE引入一个新的跳转表, 用来存放转换后的条件分支地址。对于剩下的间接转移, 改写过程如图4所示, 首先将运行目标(例如, %eax)存到一个线程局部变量中(例如, %gs:0x40), 接着执行地址转换例程。

图5显示的处理过程。首先检查转移是否违反CFI规则。如果没有, 则进行地址转换。%gs:0x40中存放的是原代码段的地址, 即orig_addr。BinCFI通过相关地址转换表寻找对应的转换地址, 即new_addr。如果找到, 则跳转到new_addr执行。否则, 调用全局地址转换例程, 来完成不同模块间的地址转换。

全局地址转换例程通过查询GTT(全局转换表)来进行地址转换。对于每个加载模块, GTT记录了模块基地址和地址之间的对应关系。在上述例子中, 通过%gs:40可以获知目标模块, 如果GTT中没有找到该模块, 则触发警告; 如果找到, 控制流则转移到目标模块的例程, 接着进行地址检测和转换操作。全局地址转换例程和GTT都被加入载入程序中, 且每次加载到不同的内存地址。另外, 当运行时加载一个新模块, 也会更新GTT, 加入这个模块的相关信息。

5.2 扩展架构

为实现本文CFI策略, 我们对BinCFI进行了扩展和修改。

扩展地址转换表

我们对地址转换表进行两项扩展。首先, 通过代码复制, 将新引入的间接转移目标引入新表项, 其中包含复制的函数和返回调用点。接着, 修改复制函数的有关表项。因此, 如果原函数在运行时被调用, 则实际执行的是对应的新函数。

我们以图2代码中的foo为例进行说明。图6(a)展示了BinCFI记录的foo地址对, 图6(b)显示BinCC的扩展, 在表中加入了函数入口<5’, 5’>与调用点<6’, 6’>。其中, 5_new被修改成5’。当foo在被间接调用时, 将实际执行foo’。

虽然代码复制会引入新的间接转移目标, 但并不会影响保护效果。由于新加入目标的数量很少, 因此仅需复制很小比例(见6.1节)的函数。此外, 新的控制流转移也使用的相同的方法, 实验结果表明, 不会对保护效果造成影响。

块内策略实施

这种策略用来约束代码块中代表间接转移的内部结点, 如直接ret和跳转表相关的间接jmp。它们的目标是确定的, 可以从代码块构成的Super-CFG中获得。

为了约束直接ret, 我们为每个ret都准备了一个独立的地址转换哈希表来存储合法目标。对每个直接ret, 其目标为与Super-CFG相关联的调用点。对直接ret指令的改写过程如图7所示。插在两个指令中的和表明了对应地址转换表的具体位置。我们用另一个线程局部变量%gs:0x50来存储第一个指令地址——。转换例程在运行时使用%gs:0x50来获取和以定位地址转换表, 接着进行地址校验和转换操作。

对与跳转表相关的间接jmp, 也可以使用相似的结构。然而, BinCFI已经实现了对这类jmp转移的约束, 所以, 我们沿用BinCFI对这类jmp的改写方式。

块间策略实施

我们按照3.2节中所述的策略, 对相应执行进行改写。改写方式与图4相似。每种控制流转移都有自己的地址转换表和地址转换例程来实现地址检查与转换。

我们需要对PLT条目中的间接jmp进行特殊处理。这类跳转用于动态符号解析。一般只有两种合法目标, 一种是下一条指令的地址, 也就是在符号解析之前初始化的值; 另一种是在运行时确定的符号解析地址。在这些动态地址解析后, 对这些jmp做出约束, 使其只能跳转至对应的动态解析符号地址。此外, 我们把间接跳转的所有目标安排在一个地址转换表中, 并将表放在新引入的只读数据域中。另外, 通过修改加载器使其能将数据域的属性改为可写, 然后用符号解析后的解析地址更新对应表项, 最后将可写属性改回。

除此之外, , 我们将孤立块中的间接ret定义为孤立ret。由于不能通过静态分析识别孤立块的调用方, 因此, 允许孤立ret返回至任何函数调用点。

C++异常

我们还需要考虑C++异常。在C++程序中, 异常处理的必要信息存储在域中。当异常被触发, 系统会使用当前执行的上下文来执行栈展开操作, 识别对应的catch分支。由于中没有我们通过代码复制引入新代码的异常元数据, 当一个复制函数包含C++异常逻辑且异常被触发时, 栈展开过程无法找到异常处理函数, 于是程序将会异常运行。为了避免这个问题, 我们不复制包含C++异常逻辑的函数, 允许这些函数中的ret返回至任意调用点。

6 实验评估

我们通过从SPEC CPU 2006测试集编译得到的二进制程序对BinCC进行评估。编译器为GCC 4.6.1, 编译优化级别为-O2。实验环境为1.0GB内存、20G硬盘、单核的Ubuntu 11.10 32位虚拟机。

表1 复制函数统计数据

6.1 代码复制评估

CFI策略实施的一个关键操作是代码复制。我们需要评估一下复制的代码量。

表1列出了ICF和DCF的数量, 以及每个程序的所有函数和复制函数的总量。从中可以看到, 为了实现本文CFI策略, 我们仅需要复制少量的函数(约占所有函数的3.4%)。

另外从表1中, 我们发现C++程序(如omnetpp和soplex)通常比C程序需要更多的函数复制。在这些C++程序中, 大多数的复制函数是C++虚拟函数。函数指针在虚拟表中被识别为ICF, 它们既能被间接调用, 也能被直接调用(例如, 通过同一个类的继承函数), 所以我们对这些函数进行复制。

图8显示了示例中需要复制指令的百分比。平均而言, 只需要复制7%的二进制指令。

6.2 间接转移的度量

BinCFI引入平均间接目标缩减量(AIR)来评估CFI防护粒度。公式定义如下。

在这个式子里,T表示间接转换i的合法目标集。表示二进制编码的长度。对于BinCC, 因为需要对代码进行复制, 因此, 编码的长度、间接转换的数目, 以及合法目标集T有可能会增加。考虑到这些因素, 计算AIR结果表明BinCFI为98.86%, 而BinCC为99.54%。

AIR这个评估指标有失客观, 因为二进制中S要远高于T, 这使得粗粒度的CFI方案都可以获得一个较高的AIR值。所以, 我们提出了一种新的度量方法RAIR(Relative AIR)。RAIR用来说明BinCC与BinCFI比较, 对间接控制流转移合法目标的缩减程度。

其中,T’表示BinCFI中间接转换i的合理目标,T表示BinCC中i的合理目标。图9列出了关于这个指标的一些统计数据。平均来说, BinnCC比BinCFI减少了81.34%的间接控制流转移目标。

相比BinCFI, BinCC缩减了每种间接转换的合理目标。我们用下面的表达式来评估每种间接控制流转移的合法目标的平均数量。

在上式中,i是某个特定种类的间接控制流指令;T表示在CFI执行的二进制代码中i的合法目标集; N表示二进制程序中所有间接控制流转移的数量。我们计算三种间接控制流转移的AVG值, 并计算, 即BinCC与BinCFI相比, 对控制流转移合法目标缩减的百分比。图10, 11, 12分别展示了BinCC对间接call, 间接jmp和间接ret缩减的百分比。

在图10中, 相比BinCFI, BinCC为每一个间接call缩减了40%的合法目标。根据BinCFI实现, 所有可能的常量代码指针是间接调用的潜在目标。然而一些常量实际上是函数返回地址和跳转表中的分支地址, 并不是函数。我们在实现中已经把它们从目标集中移除。

对于间接jmp, 很多是在PLT中, 这些跳转有明确的目标, 即被解析的符号地址。因此, 相比BinCFI, BinCC为间接jmp减少了35%的合法目标集, 如图11所示。

如图12所示, 相比BinCFI, BinCC为ret指令平均减少了87%的合法目标。从图中可以看到, gcc提升幅度最大。这是由于在这个二进制程序中, 直接ret比间接ret多, 且每一个直接ret的合法目标远少于间接ret的合法目标。

6.3 ROP攻击评估

由于ASLP和DEP的引入, ROP已成为攻击者常用的攻击手段。CCFIR和BinCFI等CFI解决方案能够有效缓解传统ROP攻击, 然而, 研究工作[5,10]表明在一些情况下攻击者仍然能够通过call-preceded gadget绕过CFI的防护, 实施ROP攻击。

为了评估BinCC在抵御ROP攻击的能力, 我们提出评估指标——GS(GadgetSurvivability)来说明在CFI防护下, 利用call-preceded gadget实现ROP的难度。定义如下。

这个指标旨在评估: 假设在一个CFI加固的二进制程序中, 一条ret指令被攻击者完全掌控, 那么这个ret能够返回到一个call-preceded gadget的几率。

定义中C表示在CFI策略下ret指令r的合法目标集,表示所有的ret指令构成的集合,表示由所有call-preceded gadget构成的集合。对于ret指令r, 能够达到|C|个call-preceded gadget。所以, 它被控制并且能返回一个call-preceded gadget的概率是。在考虑所有ret指令之后, 平均概率是或。

对于BinCC来说, 对于每一个r, |C|等于||, 所以这个指标值是100%。这符合实际情况, 即: 攻击者能够在CFI下返回到任一call-preceded gadget。

表2 BinCC与BinCFI中可用的gadget比例对比

对于BinCC而言, GS值将很小。因为BinCC对直接ret和间接ret, 都显著缩减了它们的合法目标。当被控制的ret是一个间接ret时, 它可以返回到任一间接调用点; 如果ret是一个直接ret, 则仅允许返回至以特定直接调用点起始的call-preceded gadget。表2展示了BinCC和BinCFI对所有的测试用例求得的GS值。从表中可知, BinCC显著降低了利用 call-preceded gadget 实现ROP攻击的可能。具体而言, 与BinCFI的100%相比, BinCC只有大约0.7%的可能。

6.4 性能开销

我们对空间和时间的开销进行评估。对于空间开销, 主要比较BinCC和BinCFI地址转换表的增长量, 同时对增加的编码长度及所有文件比原始文件的增加量进行评估。对于时间的开销, 我们先执行BinCC和BinCFI, 然后进行比较。

6.4.1 空间开销

由于采用相同的BinCC架构来改写二进制程序, 因此文件大小增加的原因和BinCFI的相同, 即新增的代码段和地址转换哈希表。对于BinCC, 新代码段较原始代码长度增加了1.4倍; 对于BinCFI, 这部分增加了1.2倍。BinCC引入了四种表存储间接call, 间接jmp, 间接ret和直接ret的合法目标, 而BinCFI只使用两种表来存储间接call、jmp和ret。总体而言, BinCC引入的表的大小比BinCFI减小了20%。这是由于哈希表大小是2的幂, 而BinCC显著缩减了间接控制流的合法目标, 使得表的大小小于BinCFI中表的大小。另外, BinCC生成文件的大小比原始的二进制大小增加125%, 比BinCFI低了14%。

6.4.2 时间开销

图13展示了由BinCC和BinCFI加固的二进制程序的实际运行时间。在实验环境中, BinCC加固的程序较原始程序在运行时间开销上增加22%, 较BinCFI加固的程序仅增加4%。这是因为BinCC在对间接控制流指令进行替换时, 增加了额外的指令, 来实现更严格的CFI约束策略。考虑到数据和指令的对齐会对程序执行效率产生影响, 所以, 我们在二进制重写操作前, 对一些数据和指令进行必要的对齐操作, 以此来提升访问数据和指令执行的效率, 减少时间开销。我们将在后续工作中进行完善。

7 讨论

较已有的二进制CFI解决方案, BinCC对间接控制流转移进行了更为细粒度的约束, 显著缩减了ret的合法目标, 使得可利用的call-preceded gadget显著减少。然而, 对于indirect call转移目标的约束, 尽管已经将转移目标限制在间接被调函数, 但是仍然存在被攻击者利用的可能。攻击者可以将indirect call操作数篡改为合法目标集合中的任何一个地址, 绕过对indirect call的CFI的检测, 可能造成程序运行崩溃或拒绝服务攻击等。这是针对二进制程序的CFI解决方案共同面临的问题。由于不依赖程序源代码和调试符号信息, 所以无法构造完整CFG, 也无法准确确定indirect call的目标函数。研究工作[18]针对特定一类的间接函数(C++虚函数)的调用提出了约束和检验方法, 具体是对调用虚函数的indirect call进行加固, 仅允许调用目标为它所在类或其他同类族(class hierarchy)中的虚函数。在下一步工作中, 我们可以结合这种方法进一步提升BinCC对控制流指令的约束粒度。

另外, 当前BinCC不支持动态生成代码(例如JIT代码, 或者混淆代码等)。而这些问题也是面向二进制CFI解决方案存在的共同问题。我们将在下一步工作中对这部分内容进行系统的研究。

8 小结

对于面向二进制程序的CFI方法, 由于不依赖于程序源代码和调试符号, 所以已有解决方案采用粗粒度的CFI策略, 虽然能够防范普通控制流劫持攻击, 但是对于复杂ROP攻击防护能力有限。攻击者仍然能够构造利用绕过CFI规则的ROP链, 实现目的。

本部分提出了一种新的CFI解决方案BinCC, 为二进制程序实施细粒度的控制流完整性校验。该方法通过复制部分程序代码和静态分析, 将二进制程序划分为多个互斥的代码块。进一步将二进制程序中所有的控制流指令归类为代码块内转移指令和代码块间的转移指令, 并为这两种类型的控制流指令分别实施不同的转移约束条件。BinCC严格地限定了间接控制流转移的合法目标, 显著提高控制流转移的防护效果。为了检验我们所提出的控制流完整性约束的策略, 我们提出若干新的评估指标, 并将BinCC和BinCFI进行了比较。实验结果表明, BinCC具有较低的时间开销和空间开销。

致谢 我们感谢帮助完成这篇论文的匿名评论者的反馈。这项工作是在Minghua Wang作为美国雪城大学的访问学生时完成的。本研究由国家科学基金会资助# 1054605, 美国空军研究实验室资助# FA8750- 15-2-0106, 中国的国家基础研究发展计划资助# 2012 CB315804, 中国的国家自然科学基金资助# 91418206, 以及中国国家留学基金委(CSC)等机构出资支持。本材料中任何意见、发现和结论都属于作者, 不代表资金会机构的观点。

[1] M. Abadi, M. Budiu, Ú. Erlingsson, and J. Ligatti. Control-flow integrity principles, implementations, and applications., 13(1): 4, 2009.

[2] S. Andersen and V. Abella. Data execution prevention. changes to functionality in microsoft windows xp service pack 2, part 3: Memory protection technologies, 2004.

[3] T. Bletsch, X. Jiang, and V. Freeh. Mitigating code-reuse attacks with control-flow locking.In Proceedings of the 27 Annual Computer Security Applications Conference, pages 353–362. ACM, 2011.

[4] T. Bletsch, X. Jiang, V. W. Freeh, and Z. Liang. Jump-oriented programming: a new class of code-reuse attack., pages 30–40. ACM, 2011.

[5] N. Carlini and D. Wagner. Rop is still dangerous: Breaking modern defenses., 2014.

[6] Y. Cheng, Z. Zhou, M. Yu, X. Ding, and R. H. Deng.Ropecker: A generic and practical approach for defending against rop attacks., 2014.

[7] L. Davi, A.-R. Sadeghi, and M. Winandy. Ropdefender: A detection tool to defend against return-oriented programming attacks., Computer and Communications Security, pages 40–51. ACM, 2011.

[8] U. Erlingsson, M. Abadi, M. Vrable, M. Budiu, and G. C.Necula. Xfi: Software guards for system address spaces., pages 75–88. USENIX Association, 2006.

[9] I. Evans, S. Fingeret, J. González, U. Otgonbaatar, T. Tang, H. Shrobe, S. Sidiroglou-Douskos, M. Rinard, and H. Okhravi. Missing the point (er): On the effectiveness of code pointer integrity1., 2015.

[10] E. Goktas, E. Athanasopoulos, H. Bos, and G. Portokalidis. Out of control: Overcoming control-flow integrity., 2014 IEEE Symposium on, pages 575–589. IEEE, 2014.

[11] D. Jang, Z. Tatlock, and S. Lerner. Safedispatch: Securing c++ virtual calls from memory corruption attacks., 2014.

[12] V. Kuznetsov, L. Szekeres, M. Payer, G. Candea, R. Sekar, and D. Song. Code-pointer integrity., 2014.

[13] S. McCamant and G. Morrisett. Evaluating sfi for a cisc architecture., page 15, 2006.

[14] V. Mohan, P. Larsen, S. Brunthaler, K. Hamlen, and M. Franz. Opaque control-flow integrity., 2015.

[15] B. Niu and G. Tan. Modular control-flow integrity., page 58. ACM, 2014.

[16] B. Niu and G. Tan. Rockjit: Securing just-in-time compilation using modular control-flow integrity., pages 1317–1328. ACM, 2014.

[17] M. Payer, A. Barresi, and T. R. Gross. Fine-grained control-flow integrity through binary hardening., and Vulnerability Assessment, 2015.

[18] A. Prakash, X. Hu, and H. Yin. vfguard: Strict protection for virtual function calls in cots c++ binaries., NDSS, volume 15, 2015.

[19] A. Prakash, H. Yin, and Z. Liang. Enforcing system-wide control flow integrity for exploit detection and diagnosis. In Proceedings of the 8th ACM SIGSAC symposium on Information,, pages 311–322. ACM, 2013.

[20] R. Roemer, E. Buchanan, H. Shacham, and S. Savage. Return-oriented programming: Systems, languages, and applications., 15(1): 2, 2012.

[21] H. Shacham. The geometry of innocent flesh on the bone: Return-into-libc without function calls (on the x86)., pages 552–561. ACM, 2007.

[22] P. Team. Pax address space layout randomization, 2003.

[23] C. Tice, T. Roeder, P. Collingbourne, S. Checkoway, Ú. Erlingsson, L. Lozano, and G. Pike. Enforcing forward-edge control-flow integrity in gcc&llvm., 2014.

[24] R. Wahbe, S. Lucco, T. E. Anderson, and S. L. Graham.Efficient software-based fault isolation., volume 27, pages 203–216.ACM, 1994.

[25] B. Yee, D. Sehr, G. Dardyk, J. B. Chen, R. Muth, T. Ormandy, S. Okasaka, N. Narula, and N. Fullagar. Native client: A sandbox for portable, untrusted x86 native code., 2009 30th IEEE Symposium on, pages 79–93. IEEE, 2009.

[26] C. Zhang, T. Wei, Z. Chen, L. Duan, L. Szekeres, S. McCamant, D. Song, and W. Zou. Practical control flow integrity and randomization for binary executables., 2013 IEEE Symposium on, pages 559–573. IEEE, 2013.

[27] M. Zhang and R. Sekar. Control flow integrity for cots binaries., pages 337–352, 2013.

王明华 现为Baidu X-Lab高级研发工程师。2016年1月在中国科学院大学计算机应用技术专业获得博士学位。研究兴趣包括: 程序安全分析、软件漏洞分析等。Email: wmhfzh@163.comHeng Yin 美国雪城大学副教授。研究领域包括恶意代码检测和分析、软件漏洞分析、移动应用安全性分析、虚拟化安全等。Email: heyin@syr.edu Abhishek Vasisht Bhaskar 现为美国雪城大学硕士研究生。研究兴趣包括:程序安全分析、虚拟化安全等。Email: abhaskar@syr.edu苏璞睿 中国科学院研究员、博士生导师, 研究方向为可信计算与信息保障。研究兴趣包括: 恶意代码动态逆向分析、移动终端应用安全、软件漏洞分析与评估等。Email: supurui@tca.iscas.ac.cn 冯登国 中国科学院研究员、博士生导师, 研究方向为可信计算与信息保障。研究兴趣包括: 密码算法设计与分析、安全协议设计与分析、PKI理论与关键技术、可信计算理论与关键技术、信息与网络安全等。Email: feng@tca. iscas.ac.cn

Binary Code Continent: Finer-Grained Control Flow Integrity for Stripped Binaries

Minghua Wang1,2,4, Heng Yin2, Abhishek Vasisht Bhaskar2, Purui Su1,3, Dengguo Feng1

1Trusted Computing and Information Assurance Laboratory, Institute of Software, Chinese Academy ofSciences2Syracuse University3State Key Laboratory of Computer Science, Institute of Software, Chinese Academy of Sciences4University of Chinese Academy of Sciences

Control Flow Integrity (CFI) is an effective technique to mitigate threats such as code-injection and code-reuse attacks in programs by protecting indirect transfers. For stripped binaries, a CFI policy has to be made conservatively due to the lack of source code level semantics. Existing binary-only CFI solutions such as BinCFI and CCFIR demonstrate the ability to protectstripped binaries, but the policies they apply are too permissive, allowing sophisticated code-reuse attacks. In this paper, we propose a new binary-only CFI protection scheme called BinCC, which applies static binary rewriting to provide finer-grained protection for x86 stripped ELF binaries. Through code duplication and static analysis, we divide the binary code into several mutually exclusive code continents. We further classify each indirect transfer within a code continent as either an Intra-Continent transfer or an Inter-Continent transfer, and apply separate, strict CFI polices to constrain these transfers. To evaluate BinCC, we introduce new metrics to estimate the average amount of legitimate targets of each kind of indirect transfer as well as the difficulty to leverage call preceded gadgets to generate ROP exploits. Compared to the state of the art binary-only CFI, BinCFI, the experimental results show that BinCC significantly reduces the legitimate transfer targets by 81.34% and increases the difficulty for adversaries to bypass CFI restriction to launch sophisticated ROP attacks. Also, BinCC achieves a reasonable performance, around 14% of the space overhead decrease and only 4% runtime overhead increase as compared to BinCFI.

control flow integrity

TP309.7

王明华, 于2016年在中国科学院软件研究所可信计算与信息保障实验室获得博士学位。现为Baidu X-Lab高级研发工程师。研究领域为信息安全。研究兴趣包括: 程序安全分析、软件漏洞分析。Email: wmhfzh@163.com。

本课题得到国家科学基金会资助# 1054605, 美国空军研究实验室资助# FA8750-15-2-0106, 中国的国家基础研究发展计划资助# 2012 CB315804, 中国的国家自然科学基金资助# 91418206, 以及中国国家留学基金委(CSC)等机构出资支持。

2015-11-18; 修改日期: 2016-3-15; 定稿日期: 2016-4-23

DOI号 10.19363/j.cnki.cn10-1380/tn.2016.02.006

本文是Binary Code Continent: Finer-Grained Control Flow Integrity for Stripped Binaries(发表于ACSAC 2015)的扩展。

猜你喜欢
控制流二进制结点
LEACH 算法应用于矿井无线通信的路由算法研究
用二进制解一道高中数学联赛数论题
基于八数码问题的搜索算法的研究
抵御控制流分析的Python 程序混淆算法
基于返回地址签名的控制流攻击检测方法
基于控制流的盒图动态建模与测试
有趣的进度
二进制在竞赛题中的应用
基于Petri网数据流约束下的业务流程变化域分析
二进制宽带毫米波合成器设计与分析