【翻译】代码指针完整性——Code Pointer Integrity

本文提出了一种名为代码指针完整性(CPI)的方法,旨在防止控制流劫持攻击,确保所有代码指针的完整性。CPI通过静态分析识别敏感指针,并在运行时将它们存储在安全区域,进行内存安全性检查。此外,还介绍了一个更宽松的版本,代码指针分离(CPS),在性能和安全性之间取得平衡。CPI和CPS已在FreeBSD系统和多个软件包上实现并评估,展示了其在实际应用中的可行性。
摘要由CSDN通过智能技术生成

代码指针完整性

摘要

系统代码通常用C/C++之类的低级语言编写,这提供了很多好处,但也将内存管理委托给程序员。这会引发内存安全漏洞,攻击者可以利用这些漏洞转移控制流并破坏系统。 部署的防御机制(例如ASLR,DEP)是不完整的,而更强大的防御机制(例如CFI)通常具有较高的开销和有限的保证[19、15、9]。

我们引入了代码指针完整性(CPI),这是一个新的设计点,可以保证程序中所有代码指针(例如,函数指针,保存的返回地址)的完整性,从而防止所有控制流劫持攻击,包括面向返回的编程 。 我们还引入了代码指针分隔(CPS),这是CPI的放松,具有更好的性能。 CPI和CPS提供的安全性与开销的比率比现有技术要好得多,它们具有:

  1. 实用,我们保护完整的FreeBSD系统以及超过100个软件包,如apache和postgresql;

  2. 有效,防止RIPE基准测试中的所有攻击;

  3. 高效:在SPEC CPU2006上,CPS的C开销平均为C,C/C++的开销为1.9%,而CPI的C开销为2.9%,C/C++则为8.4%。

CPI和CPS的原型实现可从http://levee.epfl.ch获得。

SEC-1 介绍

系统代码通常以内存不安全的语言编写; 这使得它容易产生内存错误,而内存错误是破坏系统的主要攻击手段。攻击者利用诸如缓冲区溢出之类的错误以及Use-After-Free,导致内存损坏,从而使他们能够窃取敏感数据或执行代码控制远程系统[44、37、12、8]。

我们的目标是保护系统代码免受所有控制流劫持攻击的侵害。诸如C/C++之类的低级语言为系统程序员提供了许多好处,我们希望使这些语言安全使用,同时保留其好处,其中至少包括性能。在期望系统提供任何安全保证之前,我们必须首先保护其构造块。

存在一些保护机制,可以降低控制流劫持攻击的风险而又不会造成不必要的开销。

数据执行保护(DEP)[48]使用内存页保护来防止将新的可执行代码引入正在运行的应用程序中。 不幸的是,DEP被代码重用攻击所击败,例如return-to-libc[37]和面向返回的编程(ROP)[44,8],它们可以通过将程序中现有代码片段(gadget)拼接在一起进而构成攻击代码对程序控制流劫持。

地址空间布局随机化(ASLR)[40]将代码和数据段放置在随机地址上,这使攻击者更难以重用现有代码来执行。不幸的是,ASLR被指针泄漏,边通道攻击[22]和即时代码重用攻击[45]击败。

最后,堆栈cookie [14]保护堆栈上的返回地址,但仅防止连续的缓冲区溢出。

此外还有许多防御措施可以改善这些缺点,但由于它们带来的间接开销,并未得到广泛采用。 根据最近的一项调查[46],这些解决方案是不完整的,无法通过复杂的攻击绕过它们,并且/或者需要对源代码进行修改和/或产生高性能开销。这些方法通常采用语言修改[25、36],编译器修改[13、3、17、34、43]或重写机器代码二进制文件[38、54、53]。 控制流完整性保护(CFI)[1、29、53、54、39]是一种针对防控制流劫持攻击的实用保护措施,目前得到广泛研究,但效果不佳[19、15、9]。

现有技术既不能保证针对控制流劫持的保护,又不能带来低开销,同时不改变程序员编写代码的方式。 例如,内存安全语言保证只能基于特定对象使用指针正确访问内存对象,这又使控制流劫持成为不可能,但是这种方法需要运行时检查以验证指针的时间和空间正确性计算,不可避免地会引起不必要的开销,尤其是当改型为不安全的内存语言时。 例如,用于C/C++的最新的内存安全实现会产生≥2倍的开销[35], 我们观察到,其为了使控制流劫持成为不可能,充分保证了那些用于确定间接控制流传输(间接调用,间接跳转或返回)目标指针的代码指针完整性。

本文介绍了代码指针完整性(Code-Pointer Integrity, CPI),这是一种对程序中所有代码指针实施精确的确定性内存安全性的方法。关键思想是将过程内存分为安全区域(safe region)和常规区域(regular region)。 CPI使用静态分析来识别必须保护的一些内存对象,将这些内存对象放置在安全区域,以确保代码指针的内存安全。必须保护的内存对象包括:

  1. 所有包含代码指针的内存对象;

  2. 所有用于间接访问代码指针的数据指针;

通过一些手段(例如,通过硬件保护)将安全区域与其余内存区域进行地址空间隔离。安全区域的内存只能通过在编译时被证明是安全的或在运行时经过安全检查的内存操作来访问。

常规区域就像普通的进程内存一样:无需运行时检查即可访问它,因此没有开销。在典型程序中,对安全区域的访问仅占所有内存访问的一小部分(SPEC CPU2006中所有指针操作的6.5%需要保护)。现有的内存安全技术不能有效地仅对程序中部分内存对象进行保护,而是对所有潜在危险的指针操作进行检查。

CPI能阻断并防御所有利用程序内存错误的控制流劫持攻击,进而在此方面完全地保护程序。 CPI不改变程序员编写代码的方式,它在编译时会自动检查指针访问。 通过选择性地仅检查那些必须并且足以保证所有代码指针完整性的指针访问,CPI来实现了较低的开销。CPI方法也可用于数据,以选择性地保护敏感信息,例如内核中的进程UID。

我们还介绍了代码指针分隔(CPS),这是CPI的一种宽松形式,它更适合于具有大量虚函数指针的代码。 在CPS中,所有代码指针都放置在安全区域中,但是用于间接访问代码指针的指针则保留在常规区域中(例如,指向包含虚拟函数的C++对象的指针)。与CPI不同,CPS可能允许某些控制流劫持攻击,但与CFI相比,它仍然提供更强大的保证,并且产生的开销可忽略不计。

我们的实验评估表明,我们提出的方法施加了足够低的开销,可以在生产中部署。 例如,CPS在SPEC CPU2006中的C程序上平均产生1.2%的开销,而对于所有C/C++程序则产生1.9%的平均开销。 C程序的CPI平均开销为2.9%,而所有C/C++ SPEC CPU2006程序的CPI开销为8.4%。 CPI和CPS有效:它们可以防止RIPE基准测试中的100%攻击以及最近绕过CFI,ASLR,DEP和所有其他Microsoft Windows保护的攻击[19、15、9]。 我们使用CPI/CPS编译并运行了完整的FreeBSD发行版以及≥100个广泛使用的软件包,这表明该方法是可行的。 本文做出了以下贡献:

1.定义了两个新的程序属性,它们提供了优于现有技术的安全性与执行成本的比率:代码指针完整性(CPI)确保控制流不会通过内存错误被劫持,并且代码指针分离( CPS)提供了比控制流完整性(CFI)更强的安全性保证,但执行成本却微不足道。

2.CPI和CPS是基于编译器的高效实现,无需修改的C/C++代码。

3.首次实用且完整的OS发行版(基于FreeBSD),并内置了针对控制流劫持攻击的全面保护。

在本文的其余部分中,我们介绍了威胁模型(SEC-2),描述了CPI和CPS(SEC-3),介绍了我们的实施(SEC-4),评估了我们的方法(SEC-5),讨论了相关工作(SEC-6),并得出了结论(SEC-7)。 我们正式制定了CPI执行机制,并在附录A中提供了其正确性证明的草图。

SEC-2 威胁模型

本文仅涉及控制流劫持攻击,即攻击者控制指令指针的攻击。 这种攻击的目的是将控制流转移到程序同一上下文中无法到达的位置。 此类攻击的包括强迫程序跳至(i)攻击者注入shellcode程序代码的位置,(ii)跳到面向返回的程序片段(“小工具”)链的开头,或(iii)跳到 在给定上下文中执行不良操作的函数,例如使用攻击者提供的参数调用system( )。 仅数据攻击(即修改或泄漏不受保护的非控制数据)不在范围内。

我们假设攻击者具有强大而现实的功能:完全控制进程内存,但不能修改代码段。 攻击者可以通过利用程序中输入控制的内存损坏错误来执行任意内存读取和写入操作。 它们不能修改代码段,因为代码段相应的页面被标记为可执行和不可写的,并且它们无法控制程序的加载过程。 这些可确保在编译时检查到的原始程序代码的完整性,并使程序加载器能够安全地建立安全内存区域与常规内存区域之间的隔离。

SEC-3 设计

现在,我们介绍用于描述我们的设计的术语,然后定义代码指针完整性属性(SEC-3.1),描述相应的强制机制(SEC-3.2),并定义一个宽松的版本,该版本以牺牲一些性能保证为代价(SEC-3.3)。 我们进一步规范了CPI执行机制,并在附录A中概述了其正确性证明。

当且仅当解引用指针访问的内存位于指针所基于的目标对象之内,我们说指针解引用是安全的。 目标对象可以是内存对象或控制流目标。 通过指针解引用,我们的意思是访问指针所针对的内存,以对其进行读/写(对于数据指针)或将控制流转移到其位置(对于代码指针)。

内存对象是特定语言的内存分配单元,如:全局、局部变量、动态分配的内存块或较大的内存对象的子对象(如,结构中的字段)。内存对象也可以是特定于程序的,例如在使用自定义内存分配器时。 控制流目的地是代码中的位置,如函数的开始或返回位置。 目标对象始终具有明确定义的生存期。 例如,释放一个数组并分配一个具有相同地址的新数组会创建一个不同的对象。

我们说一个指针是基于目标对象X的,当且仅当该指针是在运行时通过以下方式获得的:(i)在堆上分配X,(ii)如果X是静态分配的,则显式采用X的地址,例如局部或全局分配 变量,或者是控制流目标(包括返回位置,其返回地址在调用函数时被隐式获取并存储在堆栈中),(iii)获取X子对象y的地址(例如, X结构),或(iv)指针计算表达式(例如,指针算术,数组索引或简单地复制指针),该表达式涉及本身基于对象X或非指针的操作数。 这是“基于”C99定义的较严格版本:我们确保每个指针最多基于一个对象。

程序的执行是内存安全的,前提是执行中的所有指针解引用都是安全的。 如果程序的所有可能执行(对于所有输入)都是内存安全的,则程序是内存安全的。 此定义与C/C++的最新技术水平一致,例如Soft-Bounds + CETS [34,35]。

精确的内存执行安全[34、36、25]是跟踪程序中每个指针的基础信息,以根据上面的定义检查每个指针解引用的安全性,在检查到不安全的解引用将中止程序。

3.1 代码指针完整性(CPI)的属性

如果程序的所有解引用都是安全的,则程序执行将满足代码指针的完整性属性。敏感指针是代码指针和用于访问敏感指针的指针。注意,敏感指针定义是递归的,如下图1所示。

图1:

CPI保护的指针包括:

  1. 代码指针,3和4;
  2. 2.指针1和2(可以间接访问指针3和4);

注意:类型为void *的指针2可能在不同时间指向不同的对象。int *指针5和非指针数据位置不受保护。

根据上面的基于定义的情况(iv),将指针解引用为指针将相应地传播基于原始指针上的信息;例如,如下表达式:

type * p = &q;

将&q的结果复制到p指向的内存位置,并将基于基础的元数据与该p指向的内存位置相关联。因此,与敏感指针相关联的元数据的完整性,要求用于更新敏感指针的指针也必须敏感(我们在SEC-3.3中讨论放宽此定义的含义)。敏感指针的概念是动态的。例如,图1中的void *指针2在运行时指向另一个敏感指针时,它是敏感的,但是当指向整数时,它是不敏感的。

内存安全程序的执行通常可以满足CPI属性,但是内存安全检查通常具有较高的运行时开销,例如,在《Compiler Enforced Temporal Safety for C》提到的最新的实现中,其运行开销≥2倍[35]。我们的观察结果是,所有指针中只有一小部分负责进行控制流传输,因此,通过仅对控制敏感的数据强制执行内存安全性(因此不会为所有其他数据产生开销),我们获得了重要的安全保证。同时保持较低的执行成本。这类似于网络路由器和现代服务器中控制平面/数据平面的分离[5],CPI确保直接或间接影响控制平面的数据的安全性。

只有运行时才能精确决定敏感指针的集合。 但仍可以在编译时使用静态分析来获得近似的敏感指针集合,来实施CPI属性。

3.2 CPI执行机制

本节我们描述一种使用静态检查和运行时支持的组合将程序P改造为具有CPI属性的方法。 我们的方法包括:静态分析遍历,该遍历识别P中的所有敏感指针以及对其执行的所有指令(SEC-3.2.1);检查过程PASS,重写P以“保护”所有敏感指针,即将它们存储在单独安全的存储区,并关联、传播和检查敏感指针的元数据(SEC-3.2.2);采用指令级隔离机制防止非保护性内存操作访问安全区(SEC-3.2.3)。 出于性能原因,我们使用安全栈(SafeStack)机制(SEC-3.2.4)与其他代码指针分开处理存储在堆栈上的返回地址。

3.2.1 CPI静态分析

我们使用基于类型的静态分析来确定敏感指针的集合:如果指针的类型是敏感的,则它是敏感的。

敏感类型是:

  1. 指向函数的指针;
  2. 指向敏感类型的指针;
  3. 包含一个或多个敏感类型成员的复合类型的指针(例如struct或array);
  4. 通用指针(即void *,char *和不透明的指针指向之前声明的结构或类);
  5. 编译器或运行时隐式创建的所有代码指针(例如返回地址,C++虚拟表指针和setjmp缓冲区)也是敏感的;

如果需要,程序员还可以指示其他类型,例如FreeBSD内核中用于存储进程UID和监狱信息的结构。

确定敏感指针集后,我们将使用静态分析来查找所有可操纵这些指针的程序指令。 这些指令包括:

  1. 指针解引用;
  2. 指针算术;
  3. 内存(取消)分配操作,这些操作调用(i)相应的标准库函数,(ii)C++的new/delete运算符或(iii)手动实现的自定义内存分配器;

派生的敏感指针集合是过度逼近,该集合要大于精确的铭感指针集合(overapproximate):它可能包含通用指针(void *和char *),这些指针永远不会在运行时指向敏感值。 例如,C/C++标准允许char *指针指向任何类型的对象,但此类指针也用于C字符串。 作为一种启发式方法,我们假定传递给标准libc字符串操作函数或分配给指向字符串常量的char *指针不是通用的。 过度逼近和char *启发式方法都不会影响CPI提供的安全性保证:过度逼近仅会引入额外的开销,而启发式错误可能会导致错误的违规报告(尽管实际上我们从未观察到)。

来自libc的内存操作函数(例如memset或memcpy)可能会在CPI中引入大量开销:它们采用void *参数,因此使用libc和CPI编译会检查函数内部的所有访问,而不管它们是否对敏感数据进行操作与否。 CPI的静态分析通过在将参数转换为void *之前的真实类型来检查此类情况,随后的检查过程使用相应的内存操作函数的特定于类型的版本分别处理它们。

我们使用数据流分析增强了基于类型的静态分析,该数据流分析可以处理实际中大多数的不安全指针强制转换以及指针与整数之间的强制转换的情况。在正在分析的函数中,如果将值v转换为敏感指针类型,或者将其作为参数传递或返回到另一个函数&#x

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值