控制流完整性简介

控制流完整性概述

  • 0x00. 基础知识
  • 0x01. 控制流完整性发展历程
  • 0x02. CFI 机制的比较
  • 0x03. CFI 的应用范围、发展前景
  • 0x04. 个人想法
  • 0x05. 参考文献

0x00. 基础知识

控制流完整性 (Control-Flow Integrity) 是一种针对控制流劫持攻击的防御方法。控制流的转移是以跳转指令为基础的,因此在这一节先介绍跳转指令的相关知识。

在汇编语言中,根据寻址方式的差异可以分为间接和直接两种跳转指令。
直接跳转指令的形式一般如下:

CALL 0x1060000F

在程序执行到这条语句时,会将指令寄存器的值替换为 0x1060000F 。类似于这种在指令中直接给出跳转地址的寻址方式就叫做直接转移。在高级语言中,像if-else、静态函数调用这种跳转目标相对固定的语句就会被编译为直接跳转指令。

间接跳转指令则是使用数据寻址方式间接的指出转移地址,比如:

JMP EBX

执行完这条指令之后, 指令寄存器的值就被替换为EBX寄存器的值。与直接转移不同,间接转移的跳转地址是在执行过程中动态决定的。高级语言中可能被转换的语句有函数指针等等。

以上是通过寻址方式进行分类,在控制流完整性中还有一个比较特殊的分类方式——前向和后向转移。
前向转移指的是将控制权定向到程序中一个新位置的转移方式,比如使用 CALL 指令调用函数。
后向转移是将控制权返回到先前位置,最常见的就是RET指令。

控制流完整性简介_第1张图片
对于控制流完整性策略而言,直接跳转指令的地址在编译时就已固定,难以被攻击者更改,无需消耗大量资源对其进行检查。而间接跳转指令的地址存在被攻击者恶意篡改的可能,因此是检测的重点。间接跳转又分为前向间接跳转 (如通过指针的函数调用) 和后向间接跳转 (如RET指令) ,几乎所有的控制流完整性策略都是针对这两者进行检验。

0x01. 控制流完整性发展历程

控制流完整性 (以下简称CFI) 是一个随着控制流劫持攻击发展而不断演进的策略。

  • 20世纪80年代 出现溢出攻击
    在20世纪80年代,溢出攻击首次进入大众视野。1988年的Morris蠕虫利用了 Unix 系统的 finger 服务的缓冲区溢出漏洞,使得溢出攻击被更多人知晓并研究。

  • 1996年 第一篇具有学术价值的缓冲区溢出论文发表
    随着对溢出类漏洞的利用逐渐增多,1996年 Aleph One 在 Phrack 杂志上发表了一篇名为《Smashing the Stack for Fun and Profit》的文章,详细描述了Linux系统的栈结构以及如何利用栈溢出进行攻击。

  • 1998年 出现StackGuard[8]
    1998年,Cowan、C.Pu等人在 USENIX 上提出了一种侦测和防止缓冲区溢出的自适应技术——StackGuard。StackGuard 首次使用 Canary 探测进行堆栈保护实现,它于1997年作为GCC的一个扩展发布。

    StackGuard简要介绍 ( 参考文献[8] ):
    StackGuard是一个编译器扩展,用于检测并阻止对堆栈的缓冲区溢出攻击。其主要有两种工作模式,一种是在函数返回之前检测返回地址的变化(Canary),另一种则拒绝写入返回地址来阻止对地址的动态修改 (MemGuard)。
    第一种方法 在栈中存放的返回地址附近放置一个"Canary"值,在函数返回之前检测Canary是否一致再跳转。函数执行过程中的栈结构如下图所示:
    控制流完整性简介_第2张图片
    下面两段汇编代码展示了 Canary 模式的具体操作。第一段代码被添加在函数执行前,%gs:0x14中存储的是一个随机数,这个随机数被赋给 EAX 寄存器,然后将 EAX 的值压栈。第二段代码是函数返回前的操作,其将该随机数弹栈,并与原来的数异或比较,若相同则跳转,否则执行异常。(这两段代码属于 Canary 的改进版本 SSP)
    SSP代码SSP代码
    但是这种方法存在限制,因为其假设 Canary 不改变的情况下返回地址就不会被改变,也就是攻击者只会线性、顺序的写入数据,但实际上由于函数指针等问题这个假设不一定成立。文章中还提出了两种可能的攻击,一种是构造满足对齐要求的数组,使得Canary所在的位为空,这样可以避免覆盖Canary,另一种是模拟Canary,比如猜测和暴力破解的方式。
    第二种方法是阻止对函数返回地址的写入。它基于 MemGuard,一种允许将内存中的特定字设置为只读,只能用特定的API写入的方法保护重要数据。但在实现时 MemGuard 通常将重要数据所在的整个虚页设置为只读,在对于其他不受保护的数据进行写入时,它采用模拟写入(开辟一块区域把这些写入存起来,等保护结束后再一并写入)的方式。这种方式造成的性能开销极大,尤其当该字位于栈顶这类写入频繁的区域时。文章中提出一种优化方法,即使用调试寄存器来缓存最近受到保护的return address,避免栈顶所在页被设置为只读。
    第一种方法更加简洁、效率更高,第二种方法安全性更高,但开销也大。StackGuard 在运行时会自适应的选择当前运行环境下更优的方法。

  • 2001年 ASLR (地址空间布局随机化)的提出
    2001年,ASLR 作为 Linux 内核的一个补丁提出。它通过对堆、栈、共享库映射等线性区布局的随机化增加攻击者预测目的地址的难度。绕过ASLR的攻击有堆喷射,攻击未启用ASLR的模块等等。

  • 2004年 Windows XP Service Pack 2 实现DEP (数据执行保护)
    DEP 是 Windows 实现的数据执行保护,还存在其他比 Windwos 实现更早的系统。它通过将内存页设置为写或者执行,使攻击者无法向缓冲区等数据区域注入可执行代码。绕过 DEP 的典型攻击有 ROP 等利用程序中原有的代码段组合进行攻击的方法。

  • 2005年 控制流完整性机制的首次提出
    在DEP、ASLR、Canary 等技术陆续提出以后,用于绕过这些防御机制的攻击手段也随之而来。控制流完整性 (CFI) 的提出初衷是为彻底杜绝控制流劫持攻击。2005年 CCS 发表了一篇名为《Control-Flow Integrity》的文章,正式提出了 CFI 的概念。

    CFI防御机制的核心思想是限制程序运行中的控制流转移,使其始终处于原有的控制流图所限定的范围内 (如下图) 。主要分为两个阶段,一是通过二进制或者源代码程序分析得到控制流图 (CFG),获取转移指令目标地址的列表;二是运行时检验转移指令的目标地址是否与列表中的相对应。控制流劫持往往会违背原有的控制流图,CFI 则可以检验并阻止这种行为。
    控制流完整性简介_第3张图片

  • 2010年 CFI机制的发展与改进 ( 参考文献[4] )
    原始的 CFI 机制是对所有的间接转移指令进行检查,确保其只能跳转到预定的目标地址,但这样开销过大。因此又提出了对 CFI 机制的改进,接下来简单讲解一段 CFI 分析的过程。
    首先是 CFG 的构建,控制流图 (Control-Flow Graph) 是基于静态分析的用图的方式来表达程序的执行路径。下图展示了几个普通的 CFG ,分支指令作为边,圆圈则表示普通指令。CFI 中的 CFG 构建只考虑将可能受到攻击的间接调用、间接跳转和 RET 指令作为边。
    控制流完整性简介_第4张图片
    构建完CFG后就是动态检测过程,CFI 通过二进制代码重写技术在间接调用前和返回前插入标识符 ID 和 ID_check,通过比对两者的值是否一致判断控制流是否被劫持。
    下图左侧展示了一段C语言程序,sort2() 函数调用两次 sort() 函数,sort() 函数又分别以函数指针的方式调用了 lt() 与 gt()。右图以方框代表基本块,箭头代表边,展示了该代码片段的 CFG。其中细虚线表示直接调用,不需要检查。实线代表间接调用,粗虚线代表返回指令,这两者的目标地址需要检查。
    lt() 和 gt() 两个函数的地址是不同的,但它们具有相同的标识符17,这就是为了性能优化而将相似的目标地址放在同一个集合里,在检查时如果目标地址属于这个集合即可通过。 而这两个函数的 RET 地址相同,标识符都为23。控制流完整性简介_第5张图片
    以上就是一种粗粒度的CFI,它将多个不同的目标地址放在一个集合,减少需要分配和检查的标签数量。但降低性能开销的同时,也牺牲了检查的精确性。

  • 2013年 CCFIR的提出 ( 参考文献[5] )
    CFI 被提出后因其开销太大并没有被广泛应用。于是在2013年又提出了CFI 的低精确度版本 CCFIR,在同一年提出的还有binCFI,ModularCFI等等。CCFIR 将目标集合划分为三类,间接调用的目标地址被归为一类,RET 指令的目标地址被归为两类,一类是敏感库函数 (比如libc中的system函数) ,另一类是普通函数。下面以图中的例子来说明CCFIR的具体原理:
    控制流完整性简介_第6张图片
    图左是原始的控制流,右边是 CCFIR 机制下的控制流。CCFIR 提出了通过 Springboard 段 (特殊内存段 右下方灰色部分) 存放间接转移目标地址的集合。这段控制流中,5 和 3 节点分别是 CALL EAX 和 RET这两个间接转移指令的目标地址,都被存储于 Springboard 段中。在程序执行到节点 2’ 时,会检测接下来的跳转地址是否位于 Springboard 段,是则跳转,否则报错,从 6 跳回 3 时同理。
    Springboard 段的内存布局如下图所示,通过将某一位设置成 0/1 来区分普通段和Springboard段。在检测时检查某一个目标是否在Springboard段,只要检测某一位的值即可。
    控制流完整性简介_第7张图片
    进一步地,CCFIR 将目标地址分为三类,其又通过将地址中的不同位设置为 0/1 来区分。第 27bit 为 0 表示是Springboard段,第 3 位为 1 则属于函数指针,为 0 属于ret地址,并通过26位区分是敏感函数地址还是普通函数地址。
    控制流完整性简介_第8张图片
    CCFIR 的主要贡献在于以将 CFI 机制投入生产实际为目的对其进行了改进,效率有了很大的提升。

  • 2014年 Google 的 CFI 实践 ( 参考文献[9] )
    随着对堆栈的保护越来越完善,出现了很多针对非堆栈的前向转移攻击,尤其是针对 CALL 指令等。例如利用 UAF 漏洞覆盖 vtable 指针等等。这篇文章是 Google 将 CFI 机制应用到编译器中的实践。
    Vtable Verification (VTV) 主要是对 vtable 调用进行检测,VTV在每个调用点验证用于虚拟调用的 vtable 指针的合法性。
    Indirect Function Call Checker (IFCC) 基于 LLVM。它通过为间接调用目标生成跳转表并在间接调用点添加代码来转换函数指针来保护间接调用,从而确保它们指向跳转表条目。任何未指向相应表的函数指针都被视为CFI违规。
    Indirect Function Call Sanitizer (FSan) 也基于 LLVM,是一个可选的间接调用检查器。

  • 2014-2015年 其他CFI
    基于前述方案的缺陷,又提出了上下文敏感的 CFI(Context sensitive CFI)机制。它依赖于上下文敏感的静态分析,将 CFI 不变量和 CFG 中的控制流路径联系到一起,运行时在执行路径上强制执行这些不变量。2014 年的论文《Complete Control-Flow Integrity for Commodity Operating System Kernels》在操作系统的内核上实现了 CFI,使之免受控制流劫持等攻击,这个系统被称为 KCoFI。他们在基于标签的控制流间接转移保护的基础上,加入一个运行时监控的软件层,负责保护一些关键的操作系统数据结构和监控操作系统进行的所有底层状态操作。(这个系统加入了实时监控系统底层状态操作,如果是高 IO 的情况下,性能表现比较差) 2015 年论文:《CCFI: Cryptographically Enforced Control Flow Integrity》, 提出了一种通过对代码指针加密的方法来增强 CFI 的保护。这个观点出发点是好的,但是在大部分硬件效率跟不上的情况下,很难在现实中运用。

  • 针对前面几种粗粒度CFI提出的攻击方式
    当然,攻防是相对应的,粗粒度的CFI不够安全一直是公认的。2014 年的论文《Out of Control: Overcoming Control-Flow Integrity》中就针对粗粒度CFI提到了一种攻击手段。他们利用了两种特殊的 Gadget:entry point(EP) gadget 和 call site(CS) gadget,来绕开粗粒度 CFI 机制的防御。
    2015 年的论文《Losing Control: On the Effectiveness of Control-Flow Integrity under Stack Attacks》,也提到了对 CFI 保护下的栈的攻击手段。在此论文发表前,通过影子堆栈(Shadow Stack)来检测函数返回目标,再加上 DEP 和 ASLR 的保护,栈应该会变得非常安全,但是事实并非如此。这篇论文中提到了三种攻击手段,一是利用堆上的漏洞来破坏栈上的 calleesaved 寄 存 器 保 存 区 域, 使得 calleesaved 寄存器被劫持;二是利用用户空间和内核之间进行上下文切换的问题,来劫持 sysenter 指令,使控制跳转到攻击者想跳转的位置;

下图是网络上一张对 CFI 机制发展历史的总结,作为参考。
控制流完整性简介_第9张图片

0x02. CFI 机制的比较

  • 2017年 《 Control-flow integrity: Precision, security, and performance》对现有CFI机制的安全性和开销作出了系统的评价 (参考文献[6])
  1. 性能比较
    目前大多数的CFI机制对性能的评估都是基于SPEC CPU2006的程序测试的,评价标准一般是百分比的性能开销。在这篇文章中指出,调查发现,只有在CFI机制的开销低于 5% 的时候,这项机制才是受到行业从业者欢迎并且真正能投入使用的。从下图中可以看出,大多数 CFI 机制的平均开销符合要求,但也存在Lockdown,CCFI这种开销过高的机制。
    控制流完整性简介_第10张图片

  2. 安全性能评估
    对 CFI 机制安全性能的评估标准此前都是 AIR (平均间接目标缩减量),也就是经过CFI机制的保护后减少的可攻击目标数量。但这个机制参考意义不大,因为 CFI机制 通常都能降低99%以上的目标,无法体现不同CFI机制的区别。
    本文提出了一种定性的安全评价方法: 它将安全性能的比较分为四个方面,一是支持的控制流传输种类,比如前向后向、间接返回等等,用CF表示。二是性能数值,用1-10来区别,10为最高分,用RP表示。SAP.F是对前向控制流的静态分析精度,SAP.B是对后向控制流的分析精度。文中给出了很多种CFI机制的结果,这里前面提到的来举例:
    左一是原始CFI,可以看到它的性能较差但精度较高。左二是CCFIR,它的性能(RP)值很高,但无论是前向还是后向的精度都非常差,因为它只将目标集合分为了三类。左三是Google文中提到的VTV(对虚表调用的保护),如前所述,它只保护了前向转移,所以没有SAP.B这一项,但它在其他三个方面的表现都比较好。左四是提到过内核CFI。有一点比较特别,因为现在对堆栈的保护往往比较严格,SAP.B也就是对 RET ADDRESS 的检验相较以前减弱,因为可以依赖其他机制来保护。
    控制流完整性简介_第11张图片

0x03. CFI 的应用范围、发展前景

  • Clang: https://en.wikipedia.org/wiki/Clang
  • Microsoft’s Control Flow Guard:https://en.wikipedia.org/wiki/Control-flow_integrity
  • Return Flow Guard: https://xlab.tencent.com/en/2016/11/02/return-flow-guard/
  • Google’s Indirect Function-Call Checks
  • Reuse Attack Protector: https://grsecurity.net/rap_faq.php

以上都是 CFI机制 的一些现有应用,当然 Canary 机制也可以算作 CFI 机制的一种。

0x04. 个人想法

  • 粗粒度的CFI安全性不够
  • 细粒度的CFI性能开销太大
  • 出现很多CFI无法防护的攻击——Data oriented programming等

以上对于 CFI 机制的介绍偏策略。因为随着软硬件的发展,不同的硬件、操作系统、编译器甚至语言都会有不同的实现方式,而且没有哪一种是完美的。安全和性能在实际使用中还需要用户自行抉择。

0x05. 参考文献

  1. 吴世忠, 郭涛, 董国伟, 张普含, 软件漏洞分析技术 ,科学出版社,2014.
  2. Bryant &O’Hallaron, Computer Systems: A Programmer’s Perspective (2 ed.) , Pearson Education, 2011.中译本: 深入理解计算机系统,机械工业出版社, 2011.
  3. David Brumley and Vyas Sekar, Introduction to Computer Security (18487/15487), 2015
  4. Control-Flow Integrity Principles, Implementations, and Applications[J].ACM transactions on information and system security,2010,13(1):p.4:1-. (B类 TISSEC)
  5. Zhang, Chao,Wei, Tao,Chen, Zhaofeng, et al.Practical Control Flow Integrity and Randomization for Binary Executables[C].//2013 IEEE symposium on security and privacy: SP 2013, Berkeley, California, USA, 19-22 May 2013.%%%,2013:559-573.(A类 S&P)
  6. Burow N, Carr S A, Nash J, et al. Control-flow integrity: Precision, security, and performance[J]. ACM Computing Surveys (CSUR), 2017, 50(1): 16.(1区 CSUR)
  7. Carlini N, Barresi A, Payer M, et al. Control-Flow Bending: On the Effectiveness of Control-Flow Integrity[C]//USENIX Security Symposium. 2015: 161-176.(A类 USENIX)
  8. Cowan C, Pu C, Maier D, et al. Stackguard: Automatic adaptive detection and prevention of buffer-overflow attacks[C]//USENIX Security Symposium. 1998, 98: 63-78.(A类 USENIX)
  9. Tice C, Roeder T, Collingbourne P, et al. Enforcing Forward-Edge Control-Flow Integrity in GCC & LLVM[C]//USENIX Security Symposium. 2014: 941-955.(A类 USENIX)

以上整理并转自我的个人 GitHub 账号,如需转载请注明出处,谢谢 !

你可能感兴趣的:(安全,控制流完整性,CFI,CCFIR)