课程:《密码与安全新技术专题》html
班级: 1892前端
姓名: 王子榛linux
学号:20189206nginx
上课教师:王志强程序员
论文名称:SafeInit: Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities算法
会议名称:ndss2017apache
做者:Alyssa Milburn、Herbert Bos、Cristiano Giffrida编程
未初始化值的使用仍然是C / C ++代码中的常见错误。这不只致使未定义的和一般不指望的行为,并且还致使信息泄露和其余安全漏洞。数组
咱们都知道C/C++中的局部变量,在未初始化的状况下,初值为随机值。缓存
以C++中局部变量的初始化和未初始化为例:(int x;和int x = 0;)
编译器在编译的时候针对这两种状况会产生两种符号放在目标文件的符号表中,对于初始化的,叫强符号,未初始化的,叫弱符号。链接器在链接目标文件的时候,若是遇到两个重名符号,会有如下处理规 则:
一、若是有多个重名的强符号,则报错。
二、若是有一个强符号,多个弱符号,则以强符号为准。
三、若是没有强符号,但有多个重名的弱符号,则任选一个弱符号。
简单地说,未定义行为是指C语言标准未作规定的行为。编译器可能不会报错,可是这些行为编译器会自行处理,因此不一样的编译器会出现不一样的结果,什么都有可能发生,这是一个极大的隐患,因此咱们应该尽可能避免这种状况的发生。
因为C/C++不会像C#或JAVA语言,确保变量的有限分配,要求在全部可能执行的路径上对它们进行初始化。因此,C/C++代码可能容易受到未初始化的攻击读取。同时C/C++编译器能够在利用读取未初始化的内存是“未定义行为”时引入新的漏洞。
在本文中,提出了一种全面而实用的解决方案,经过调整工具链(什么是工具链)来确保全部栈和堆分配始终初始化,从而减轻通用程序中的这些错误。 SafeInit在编译器级别实现。
本文实现了:
在几乎全部应用程序中,内存不断被从新分配,所以被重用。
若是在使用以前不覆盖这些数据,就会出现未初始化数据的问题,从而将旧数据的生命周期延长到新分配点以外。
内存也可能只是部分初始化; C中的结构和联合类型一般是故意不彻底初始化的,而且出于简单性或性能缘由,一般为数组分配比存储其内容所需的(最初)更大的大小。实际上,重用内存不只常见,并且出于性能缘由也是可取的。 当不清楚变量是否在使用以前被初始化时,惟一实用且安全的方法是在全部状况下初始化它。
因为未初始化数据而致使信息泄露的最明显危险是直接敏感数据的泄露,例如加密密钥,口令,配置信息和保密文件的内容。
现代软件防护依赖于敏感元数据的保密性,同时,未初始化的值提供了指针公开的丰富资源。
例如:地址空间布局随机化(ASLR)之类的防护通常取决于指针的保密性,而且因为这一般仅经过随机化一个基地址来完成,所以攻击者仅须要获取单个指针以彻底抵消保护。 这样的指针能够是代码,堆栈或堆指针,而且这些指针一般存储在栈和堆上,所以未初始化的值错误提供了阻止这种信息隐藏所需的指针公开的丰富源。
未初始化数据致使的其余漏洞容许攻击者直接劫持控制流。常见的软件开发的错误是:没法在遇到错误时在执行路径上初始化变量或缓冲区。
两个例子分别是:
有些工具试图在开发过程当中检测未初始化变量,而不是试图减轻未初始化的值错误,容许它们由程序员手动校订。有些工具试图在开发过程当中检测它们,而不是试图减轻未初始化的值错误,容许它们由程序员手动校订。更重要的是,编译器警告和检测工具只报告问题,而不是解决问题。 这可能会致使错误和危险的错误。
函数堆栈帧:在堆栈中为当前正在运行的函数分配的区域、传入的参数。返回地址以及函数所用的内部存储单元都存储在堆栈帧中。
函数堆栈帧包含局部变量的副本,或具备被忽略的局部变量,同时还包含其余局部变量和编译器生成的临时变量的溢出副本,以及函数参数,帧指针和返回地址。 鉴于堆栈内存的不断重用,这些帧提供了丰富的敏感数据源。
现代编译器使用复杂的算法进行寄存器和堆栈帧分配,这种方式减小了内存使用并改善了缓存局部性,但意味着即便在函数调用以前/以后清除寄存器和堆栈帧也不足以免全部潜在的未初始化变量。
下面的例子说明:内存重用了循环中的局部变量,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)优化,它能够删除老是被另外一个存储覆盖而不被读取的存储。
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进行比较以外,咱们还提供了简单方法的结果,它简单地应用了咱们的初始化过程而没有包含任何咱们提出的优化,使用一个简单地将全部分配归零的强化分配器。
咱们使用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次运行中每秒请求中位数的开销; 咱们没有看到大量的差别。
使用咱们的工具链构建了最新的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能够进一步优化代码的同时,保证全部须要初始化的变量进行初始化,删除多余初始化代码进行优化,这样既保证缓解了未定义变量漏洞的威胁,同时与其余现有方法相比,提高了性能。
选到这篇文章一开始没有具体了解是涉及较为底层的编译器的内容,可是看完后,以为其实有时候越是较为底层的东西学习能够帮助咱们更好地理解咱们利用编程语言实现的一些应用,能够在之后编写代码时注意到一些以往可能注意不到的点,了解程序运行逻辑,因此选择这篇文章也促使我了解了一些计算机系统和编译原理中的内容,获益匪浅。可是遗憾的是,文章中的编译器并没能复现出来,来对一些测试代码进行编译以更好了解其运行机制,我也会在之后继续学习,争取读懂理解这篇文章的内容。