2018-2019-2 20189206 《密码与安全新技术专题》 论文总结

20189206 2018-2019-2 《密码与安全新技术专题》 论文总结

课程:《密码与安全新技术专题》

班级: 1892

姓名: 王子榛

学号:20189206

上课教师:王志强

论文总结

  • 论文名称:SafeInit: Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities

  • 会议名称:ndss2017

  • 作者:Alyssa Milburn、Herbert Bos、Cristiano Giffrida


SafeInit: 全面而实用的未初始化读取漏洞缓解

背景知识

未初始化漏洞

未初始化值的使用仍然是C / C ++代码中的常见错误。这不仅导致未定义的和通常不期望的行为,而且还导致信息泄露和其他安全漏洞。

我们都知道C/C++中的局部变量,在未初始化的情况下,初值为随机值。

以C++中局部变量的初始化和未初始化为例:(int x;和int x = 0;)

编译器在编译的时候针对这两种情况会产生两种符号放在目标文件的符号表中,对于初始化的,叫强符号,未初始化的,叫弱符号。连接器在连接目标文件的时候,如果遇到两个重名符号,会有以下处理规 则:

1、如果有多个重名的强符号,则报错。

2、如果有一个强符号,多个弱符号,则以强符号为准。

3、如果没有强符号,但有多个重名的弱符号,则任选一个弱符号。

未定义行为

简单地说,未定义行为是指C语言标准未做规定的行为。编译器可能不会报错,但是这些行为编译器会自行处理,所以不同的编译器会出现不同的结果,什么都有可能发生,这是一个极大的隐患,所以我们应该尽量避免这种情况的发生。

介绍

由于C/C++不会像C#或JAVA语言,确保变量的有限分配,要求在所有可能执行的路径上对它们进行初始化。所以,C/C++代码可能容易受到未初始化的攻击读取。同时C/C++编译器可以在利用读取未初始化的内存是“未定义行为”时引入新的漏洞。

在本文中,提出了一种全面而实用的解决方案,通过调整工具链(什么是工具链)来确保所有栈和堆分配始终初始化,从而减轻通用程序中的这些错误。 SafeInit在编译器级别实现。

本文实现了:

  • 提出了safeinit,一种基于编译器的解决方案,结合强化分配器,确保堆和栈上的初始化来自动减轻未初始化值读取。
  • 提出的优化可以将解决方案的开销降到最低水平(< 5%)并可以在现代编译器中实现
  • 基于clang和LLVM的SafeInit原型实现,并表明它可以应用于大多数真实的C / C ++应用程序而无需任何额外的手动工作。
  • CPU-intensive 、 I/O intensive (server) applications 和 Linux kernel测试验证了现实世界的漏洞确实被缓解

背景

在几乎所有应用程序中,内存不断被重新分配,因此被重用。

  • 在栈中,函数激活帧包含来自先前函数调用的数据
  • 在堆上,分配包含来自先前释放的分配数据

如果在使用之前不覆盖这些数据,就会出现未初始化数据的问题,从而将旧数据的生命周期延长到新分配点之外。

内存也可能只是部分初始化; C中的结构和联合类型通常是故意不完全初始化的,并且出于简单性或性能原因,通常为数组分配比存储其内容所需的(最初)更大的大小。实际上,重用内存不仅常见,而且出于性能原因也是可取的。 当不清楚变量是否在使用之前被初始化时,唯一实用且安全的方法是在所有情况下初始化它。

存在的四点威胁

敏感数据泄露

由于未初始化数据而导致信息泄露的最明显危险是直接敏感数据的泄露,例如加密密钥,口令,配置信息和保密文件的内容。

  • 数据生命周期持续时间长于预期,可能会产生许多无意的数据副本。
  • 不是所有情况下编译器可以提供memset优化调用,如果数据不再有效并且因此在该点之后不再使用,编译器可以通过调用memset来优化这些调用。但是如果之后的数据还有效,禁止编译器优化的替代函数(例如memset_s和explicit_bzero)并不是普遍可用的。
  • 未初始化数据的使用受到不可信输入的影响,必须考虑各种潜在的攻击媒介,这种不同的攻击面意味着应该认真对待所有未初始化的数据漏洞。
绕过安全防御

现代软件防御依赖于敏感元数据的保密性,同时,未初始化的值提供了指针公开的丰富资源。

例如:地址空间布局随机化(ASLR)之类的防御一般取决于指针的保密性,并且由于这通常仅通过随机化一个基地址来完成,因此攻击者仅需要获取单个指针以完全抵消保护。 这样的指针可以是代码,堆栈或堆指针,并且这些指针通常存储在栈和堆上,因此未初始化的值错误提供了阻止这种信息隐藏所需的指针公开的丰富源。

软件开发

未初始化数据导致的其他漏洞允许攻击者直接劫持控制流。常见的软件开发的错误是:无法在遇到错误时在执行路径上初始化变量或缓冲区。

两个例子分别是:

  • Microsoft描述了由于2008年Microsoft Excel中未初始化的堆栈变量导致的任意写入漏洞
  • Microsoft的XML解析器中的一个错误使用存储在局部变量中的指针进行虚函数调用,该局部变量未在所有执行路径上初始化。
检测工具

有些工具试图在开发过程中检测未初始化变量,而不是试图减轻未初始化的值错误,允许它们由程序员手动校正。有些工具试图在开发过程中检测它们,而不是试图减轻未初始化的值错误,允许它们由程序员手动校正。更重要的是,编译器警告和检测工具只报告问题,而不是解决问题。 这可能会导致错误和危险的错误。

堆栈变量

函数堆栈帧:在堆栈中为当前正在运行的函数分配的区域、传入的参数。返回地址以及函数所用的内部存储单元都存储在堆栈帧中。

函数堆栈帧包含局部变量的副本,或具有被忽略的局部变量,同时还包含其他局部变量和编译器生成的临时变量的溢出副本,以及函数参数,帧指针和返回地址。 鉴于堆栈内存的不断重用,这些帧提供了丰富的敏感数据源。

现代编译器使用复杂的算法进行寄存器和堆栈帧分配,这种方式减少了内存使用并改善了缓存局部性,但意味着即使在函数调用之前/之后清除寄存器和堆栈帧也不足以避免所有潜在的未初始化变量。

下面的例子说明:内存重用了循环中的局部变量,doSomthing将在所有循环迭代中传递秘密值,由于未初始化的局部变量,但是如果初始化了局部变量,只会在第一次doSomthing传递秘密值。

未定义行为

当C / C ++程序无法遵循该语言强加的规则时,会发生未定义的行为。在我们讨论的环境中,未定义行为是指在代码读取未初始化的堆栈变量或者是未初始化的堆分配。

为了实现最大数量的优化,特别是在可能从模板和宏扩展的代码中,并最终被大部分丢弃为无法访问,现代编译器转换利用了大规模的未定义的行为。这样的转换可以将未定义的值(以及因此也未初始化的值)解释为使得优化更方便的任何值,即使这使得程序逻辑不一致。

概述

SafeInit通过强制初始化堆分配(在分配之后)和所有栈变量(无论何时进入范围)来减轻未初始化的值问题。这是通过修改编译器直接在所有点插入初始化调用来完成的。为了提供实用和全面的安全性,此工具必须在编译器本身内完成。 只需在编译过程中传递额外的加固标记即可启用SafeInit。

上图是利用额外的编译器传递,从而增加了必要的初始化

种简单的初始化方法会导致过多的运行开销,而我们系统的一个重要元素是专门的强化分配器。 在许多情况下,通过利用额外的信息并结合我们的编译器工具,可以避免初始化问题。

可以看到编译器在获得C/C++文件后,编译器前端将源文件转换为中间语言(IR),通过初始化、代码优化结合现存编译器的优化器,之后通过无效数据消除、强化分配器最后获得二进制文件。Safeinit在整个过程中所添加的就是 初始化全部变量、优化以及强化分配器,来避免或缓解未初始化值。

缓解未定义变量

初始化

SafeInit在首次使用之前初始化所有局部变量,作为新分配变量的作用域处理。SafeInit通过修改编译器编译代码的中间表示(IR),在每个变量进入作用域后进行初始化(例如内置memset)。

强化分配器

SafeInit的强化分配器可确保在返回应用程序之前将所有新分配的内存清零。我们通过修改现代高性能堆分配器tcmalloc来实现我们的强化分配器。同时还修改了LLVM,以便在启用SafeInit时将来自新分配的内存的读取视为返回零而不是undef。 同时,我们在安全分配器中执行覆盖所有堆分配函数以确保始终使用强化的分配器函数(对初始化堆分配是在分配之后进行强制初始化)。编译器知道我们的强化分配器正在使用中; 任何已分配内存的代码都不再使用未定义行为,并且编译器无法修改或删除

优化器

目的:可在提高效率和非侵入性的同时提高SafeInit的性能。优化器的主要目标是更改现有编译器中可用的其他标准优化,以消除任何不必要的初始化。

  • 存储下沉:存储到本地的变量应尽可能接近它的用途。

在正常执行期间不执行此代码路径,并且我们不需要初始化缓冲区,直到我们到达它将到达的路径使用。

  • 检测初始化:检测初始化数组(或部分数组)的典型代码

  • 字符串缓冲区:用于存储C风格的以空字符结尾的字符串的缓冲区通常仅以“安全”方式使用,其中永远不会使用超出空终止符的内存中的数据。传递给已知C库字符串函数(例如strcpy和strlen)的缓冲区是“安全的”,优化器检测到该缓冲区始终被初始化,可以删除掉该缓冲区的初始化代码。

无效存储消除

“无效存储消除”(DSE)优化,它可以删除总是被另一个存储覆盖而不被读取的存储。

  • 堆清除:所有堆分配都保证初始化为零,如果有存储到新分配堆内存中的零值都会被删除
  • 非恒定长度存储清除:为了删除动态堆栈分配和堆分配的不必要初始化
  • 交叉块DSE:可以跨多个基本块执行无效存储消除
  • 只写缓冲区:通过指定该缓冲区只用来存储而不是删除,就可以将该缓冲区删除。

实施

初始化

LLVM中的局部变量是使用alloca指令定义的; 我们的pass通过在每条指令之后添加对LLVM memset内部的调用来执行初始化。可以保证清除整个分配,并在适当的时候转换为存储指令。

强化分配器

通过修改现代高性能堆分配器tcmalloc来实现我们的强化分配器。 只需清除在分配器返回指针之前,所有其他堆分配为零。还修改了LLVM,以便在启用SafeInit时将来自新分配的内存的读取视为返回零而不是undef。 如上所述,这对于避免未定义值的不可预测后果至关重要。

优化

通过将插入的memset调用移动到alloca的所有使用的主导点来实现我们提出的用于堆栈初始化的下沉存储优化。 在启用优化的情况下进行编译时,clang将发出'lifetime'标记,指示局部变量进入范围的点; 我们修改了clang以在所有情况下发出适当的生命周期标记,并在这些点之后插入初始化。

通过添加一个新的内部函数“initialized”来实现初始化检测优化,该函数具有与memset相同的存储杀死副作用,但是被代码生成忽略。 通过扩展诸如LLVM的循环习语检测之类的组件来生成这种新的内在函数,其中无法用memset替换代码,我们允许其他现有的优化传递利用这些信息而无需单独修改它们。

无效存储消除

我们通过扩展现有的LLVM代码实现了上述其他优化,尽可能减少我们的更改。 我们对只写缓冲区的实现使用了D18714中的补丁(自合并以来),它为writeonly属性添加了基本框架。
我们还基于D13363中的(拒绝)补丁实现了跨块死区消除。 由于性能回归,我们为小型商店(≤8字节)禁用此交叉块DSE; 我们还扩展了此代码以支持删除memset,并缩短此类存储。

评估

我们的基准测试运行在(4核)Intel i7-3770上,内存为8GB,运行(64位)Ubuntu 14.04.1。 禁用CPU频率缩放,并启用超线程。

基线配置:clang / LLVM的未修改版本,使用未修改的tcmalloc版本。

除了将它与SafeInit进行比较之外,我们还提供了简单方法的结果,它简单地应用了我们的初始化过程而没有包含任何我们提出的优化,使用一个简单地将所有分配归零的强化分配器。

SPEC CPU2006

我们使用LTO和-O3在SPEC CPU2006中构建了所有C / C ++基准测试。 我们使用参考数据集提供3次运行中值的开销图。

在没有我们的优化器的情况下应用时,运行时开销的(几何)平均值为8%。应用我们的优化器可以显着降低剩余基准测试的开销,与我们的基线编译器相比,导致CINT2006的平均开销为3.5%。 CFP2006的结果类似,如图14所示,平均开销为2.2%。

表I提供了每个基准测试的allocas数量(表示局部变量的数量,偶尔的参数副本或动态分配)的详细信息。 该表还提供了(剥离的)二进制大小; 在许多情况下,初始化的影响对最终的二进制大小没有任何影响,并且在最坏的情况下它是最小的。#INITS是现有编译器优化之后剩余的大量初始化数量,并且我们的优化器已经分别运行。

使用我们的优化器作为基线时的平均开销为3.8%

服务

我们通过使用两个现代高性能Web服务器nginx(1.10.1)和lighttpd(1.4.41)来评估SafeInit的开销,以减少计算密集度较低的任务。 我们使用LTO和-O3构建了Web服务器。 由于它们在我们的1gbps网络接口上使用时受到I / O限制,因此我们使用环回接口对它们进行基准测试。

我们使用apachebench重复下载4Kb,64Kb和1MB文件,持续30秒。 我们启用了流水线操作,使用了8个并发工作程序,并使用CPU功能为apachebench保留了CPU核心。 我们测量了10次运行中每秒请求中位数的开销; 我们没有看到大量的差异。

Linux

使用我们的工具链构建了最新的LLVM Linux内核树。 我们定制了构建系统,以允许使用LTO,重新启用内置clang函数,并修改gold链接器以解决我们在符号排序时遇到的一些LTO代码生成问题。

由于Linux内核执行自己的内存管理,因此它不会与用户空间强化分配器链接; 我们的自动加固仅保护局部变量。

下表提供了使用内核微基准测试工具LMbench的典型系统调用的延迟和带宽选择。 我们运行了每个基准测试10次,每次运行的预热时间很短,迭代次数很多(100次),并提供中位数结果。 TCP连接是localhost,其他参数是默认LMbench脚本使用的参数。

对于stat和open系统调用,我们会产生大量开销; 虽然我们的优化器提供的性能得到了很大程度的缓解,但这是值得关注的,我们打算进一步研究它,以及fstat和(信号)保护故障,这是我们所看到的开销大于5%的唯一系统调用。
为了评估应用于内核堆栈的SafeInit的实际性能,我们使用SafeInit强化了nginx和内核,并将性能与在非强化内核下运行的非强化nginx进行了比较。 使用我们上面讨论过的发送文件配置,再次使用环回接口提供极端情况,我们分别观察到1M,64kB和4kB情况下的开销分别为2.9%,3%和4.5%。

安全

为了验证SafeInit是否按预期工作,不仅考虑了各种现实漏洞,例如下表中的漏洞,还创建了一套单独的测试用例。 我们手动检查了为相关代码生成的bitcode和机器代码,并使用我们上面描述的检测系统运行我们的测试套件。 我们还用valgrind来验证我们的硬化; 例如,我们确认当使用SafeInit强化OpenSSL 0.8.9a时,来自valgrind的所有未初始化的值警告都会消失。

总的来说,我们的SafeInit原型在LLVM中添加或修改的代码少于2000行,包括一些调试代码和基于第三方补丁的大约400行代码。 虽然我们的修改很复杂,但这是一个相对较少的代码,每个组件都应该是可单独审查的; 为了比较,我们(单独的)帧清除通道单独超过350行代码。
我们的强化不会阻止程序在内部重用内存。例如,堆栈缓冲区可以在同一个函数中重用于不同的目的,或者自定义内部堆分配器可以重用内存而不清除它,例如我们在PHP中看到的。 尽管可能使用启发式方法或通过附加某种注释来捕获其中一些案例,但我们认为编译器支持这种情况并不现实也不合理。将变量清零可确保任何未初始化的指针为空。 尝试取消引用这样的指针将导致错误; 在这种情况下,我们的缓解措施已将更严重的问题减少为拒绝服务漏洞。

结论

未初始化的数据漏洞继续在现代C / C ++软件中造成安全问题,并且确保不使用未初始化值的安全性并不像看起来那么容易。 从简单的信息披露到严重问题(如任意内存写入,静态分析限制以及利用未定义行为的编译器优化)等威胁相结合,使这成为一个难题。
本文提出了一种基于工具链的强化技术SafeInit,它通过确保在使用前初始化所有局部变量和堆栈分配来减轻C / C ++程序中未初始化值的使用。通过使用适当的优化,我们发现许多应用程序的运行时开销可以降低到可以作为标准强化保护应用的水平,并且这可以在现代编译器中实际完成。

总结

本文通过在clang/LLVM编译器架构上,通过修改代码,实现了safeinit原型,在编译C/C++源代码时,传递一个标记即可使用safeinit实现优化编译,缓解未定义变量。使用了强化分配器的safeinit可以进一步优化代码的同时,保证所有需要初始化的变量进行初始化,删除多余初始化代码进行优化,这样既保证缓解了未定义变量漏洞的威胁,同时与其他现有方法相比,提升了性能。

选到这篇文章一开始没有具体了解是涉及较为底层的编译器的内容,但是看完后,觉得其实有时候越是较为底层的东西学习可以帮助我们更好地理解我们利用编程语言实现的一些应用,可以在以后编写代码时注意到一些以往可能注意不到的点,了解程序运行逻辑,所以选择这篇文章也促使我了解了一些计算机系统和编译原理中的内容,获益匪浅。但是遗憾的是,文章中的编译器并没能复现出来,来对一些测试代码进行编译以更好了解其运行机制,我也会在以后继续学习,争取读懂理解这篇文章的内容。

posted @ 2019-06-03 22:16  王子榛20189206  阅读(301)  评论(0编辑  收藏  举报
levels of contents