Group of Software Security In Progress

GoSSIP @ LoCCS.Shanghai Jiao Tong University

Debloating Software Through Piece-Wise Compilation and Loading

作者:Anh Quach,Aravind Prakash

单位:Lok Yan Air Force Research Laboratory

出处:USENIX Security Symposium 2018

原文:https://www.usenix.org/system/files/conference/usenixsecurity18/sec18-quach.pdf


现代操作系统极大依赖于动态链接共享库来节省物理内存,当可执行文件加载某个动态库进内存时,无论用到了这个共享库中的多少函数,整个共享库都会被加载进内存。这是一个巨大的安全隐患,没有被使用到的部分通常被用来实施复用攻击,如ret2libc,rop等。这篇论文里,作者提出了一个从编译到装载时的框架来帮助程序将没有用到的共享库部分清零,从而减少安全隐患。 #### 程序膨胀的原因

  1. 一个共享库通常会包含功能不同的函数实现,如Libc里面既包含了如malloc,free这种内存管理函数,同样包含如fopen,fread这种文件操作函数。而对于一个可执行程序来说,并非共享库中提供的所有功能都会用到,所以当整个共享库加载进内存时,产生了许多不必要的函数。
  2. 对了后向兼容,新版本的共享库会保留旧版本的某个函数。
  3. 由于c语言static关键字带来的函数复制。

消除共享库中多余部分的挑战

为了保证消除多余部分的程序依然能正确的运行,一个要点在于该程序所依赖所有函数,包含这个函数所有依赖的函数统统保留。在这在于确切地分析出函数之间的依赖关系。

  1. 共享库之间的依赖,如a.exe依赖于b.dll而b.dll又依赖于c.dll
  2. 延迟绑定,可执行文件依赖的某些函数直到运行时才知道调用的是哪个函数。
  3. 函数指针的使用使得无法得出确切的依赖关系
  4. 手写汇编
  5. 运行时动态加载的共享库,如使用dlopen,dlsym调用的函数。

设计Overview

首先,将一些常见的共享库使用作者基于llvm开发的编译框架进行编译,生成得到的共享库中会多出来一个section,这个section里面包含了这个共享库中所有函数的位置和大小,同时包含了该共享库中导出函数与其他函数的依赖关系。如编译出的Libc包含了一个.dep section,这个section里包含了printf这个函数的大小以及位置,以及printf所依赖的所有函数。随后在程序装载时,作者设计的装载器会提前将可执行文件所依赖的所有共享库载进内存,然后解析可执行文件所依赖的所有外部函数,作者将这些外部函数及其所依赖的函数以外的部分全部零。

函数指针的处理

收集函数所依赖的函数是要解决的关键问题,对直接函数调用,可以很简单的得到调用关系,对于间接函数调用如函数指针则需要特殊的处理。为了解决间接函数调用,作者提出了三个办法来解决这个问题。

  1. 全局搜索,作者遍历共享库中所有的指令,如果有指令引用了某个函数的地址,被引用地址的函数将永远不会被清零,作者默认所有函数都会引用到他们。
  2. 细粒度的搜索,比全局搜索更进一步,并不会将所有被引用到的都加入依赖中,而是针对每个函数所依赖的函数指针单独分析。如某个共享库中有三条指令分别引用了三个函数a,b,c的地址,对于全局搜索,这三个函数都会被保留,而对于细粒度的搜索,只有间接调用了这三个函数的函数被可执行程序所依赖时才不会被清零。
  3. Point-to analysis,详见隋老师的论文SVF
汇编代码的处理

作者认为所有的汇编代码都应该被保护,原因在于:1. 汇编代码体积很小;2,很难找到对汇编代码的引用。而对于汇编代码所引用的函数,作者直接遍历所有的汇编代码,查看是否引用了其他函数。

C++虚函数的处理

对于每个一个函数,作者查看在该函数内存实例化了哪些对象,对于这些对象,作者将他们的所有虚函数全部加入到该函数依赖的函数中。

共享库文件的生成

随后函数依赖关系,函数位置,函数大小等会被写进.dep section中,由于在编译时具体的地址不能确定,作者自己设计的装载器会在共享库加载进内存时将这部分内容填充进去。

可执行文件的函数依赖关系处理

由于每个共享库中所有导出函数的依赖关系都被写进了.dep section中,装载器会遍历可执行程序中的每个导入函数,对于每个导入函数,作者通过.dep section找到他们依赖的函数,然后将除些之后的所有函数全部清零。

由于.dep section并非一个保留段,因此该共享库也可以被正常的加载器所加载。其他正常的共享库由于没有.dep section而被作者的加载器所忽略不处理,所有作者的设计是后向兼容的。

评估

环境

Intel Core i7-4790 @ 3.60GHz and 32GB RAM running Ubuntu Desktop 16.04 LTS.

是否正确运行与减少的体积比率

作者选取了ubuntu 16.04的400个并重新编译他们然后使用dpkg安装进系统中,然后用自己设计的装载器替换系统中的装载器,这是通过修改可执行文件中的.interp段达到的。

Debloating coreutils,SPEC CPU2006 benchmark programs

109个coreutils程序都能正确运行,下表中reduction比率代表的是清零的代码比率。

SPEC CPU2006 benchmark中的所有程序也能正确运行。

coreutils中减少的代码体积大部分位于60%和80%之间。SPEC CPU2006 benchmark程序中最多可以减少86%的代码而最少可以减少60%的代码。

下表如作者debloat curl可执行程序中,各个共享库所减少的体积。

可以发现依然是point to analysis和细粒度的搜索所减少的体积最多,全局搜索减少的体积最小。这也是反映了通过时间换取性能的作法。

对于c++程序,作者只选取了一个程序Audacity,减少的程序体积如下

CVE验证是否可以抵抗攻击

作者选取了CVE-2016-9842,CVE-2016-7167,CVE-2014-3707,CVE-2016-9586然后发现被漏洞所在的函数都被清零了。

时间性能

作者这篇论文里面系统的系统开销主要来源于编译时以及装载时的时间开销。但是编译时的开销是一次性能的,而装载时的开销如下图所示

表中的单位为milliseconds,所以装载时的开销作者认为可以忽略不计。

评价

这篇论文提出了一个编译时到装载时的debloating方案,其中的核心部分为解析函数之间的依赖关系,而解析函数之间的依赖关系的难点在于间接函数调用(虚函数也是一种间接调用)。而分析间接函数调用是一个已经被广泛分析过的领域,因此,对于这篇论文的创新性还有待考察。