@inproceedings{qian2019razor,
title={$\{$RAZOR$\}$: A framework for post-deployment software debloating},
author={Qian, Chenxiong and Hu, Hong and Alharthi, Mansour and Chung, Pak Ho and Kim, Taesoo and Lee, Wenke},
booktitle={28th USENIX security symposium (USENIX Security 19)},
pages={1733--1750},
year={2019}
}
Razor: A Framework for Post-deployment
0 摘要
- 最近得debloating技术需要源码,限制了其实际中的应用
- 本文提出了一个实际的debloating框架:RAZOR,在部署的二进制代码上进行代码简化
- 基于用户的期望,生成一个最小的满足功能的程序
- 同时不只是根据给定的测试用例,RAZOR使用了几种控制流的启发式算法来推断出必要的代码,以支持用户期望的功能
- 我们再常见的基准上进行测试,同时也在真实的应用上进行测试,包括 火狐浏览器,闭源PDF阅读器FoxitReader,结果显式RAZOR可以在二进制上减少超过70%的代码
- RAZOR简化了代码,并且没有引入新的安全隐患,因此RAZOR是一个实际的框架,可以在对实际的程序使用
1 介绍
-
要使这种部署后使用的方法(即面向部署二进制文件的方法)对用户有用,有两个挑战
-
- 如何使得对软件了解较少的用户,表达他们对软件特点的需求,即哪个功能需要保留和移除
-
- 如何修改软件二进制文件来移除不需要的功能和保留需要的
-
-
为了解决第一个挑战,可以让用户提供一组样例输入集合来解释他们将如何使用这个软件,这样其实输入到输出构成一个映射即可,然而实际中,即使处理相同的输入,也会有不同的执行路径,因此这种方法不可行
-
为了基于用户提供的输入能够实际地简化程序,我们必须找到支持完整功能但处理样例输入时为执行的代码,称为相关代码
- 然而,相关代码地识别很困难,特别地,让用户(甚至开发者)提供一个能够执行某一功能的所有必要代码的输入语料库是一项具有挑战的任务
- 此外,即使用户提供了所有可能输入的一些描述(例如样式),仍然很难确定这些输入所有可达的代码
- 因此,我们坚信,所有在部署后情况下的去膨胀机制,都应该基于 尽力而为 的启发式机制.
- 启发式方法会尽可能地标识相关代码,同时保留最少的不相关代码
- 而死代码消除,增量调试法不适用于本问题,因为它们只关注移除静态死代码 或 保留程序在几个特定输入上的行为
-
我们设计了一种启发式方法,基于这样的假设:代码路径差异越大,相关功能越少. 具体地,对于给定的与运行路径
,我们要找到一条不同的路径 ,满足 - 1.
没有不同的指令 - 2.
没有调用新的函数 - 3.
不需要额外地库函数 - 4.
不依赖于具有不同功能地库函数
- 1.
-
然后,我们相信
是与 相关的,将 中的代码视为相关代码 -
从(1)到(4),启发式方法在包括地代码越来愈多,对于给定程序,我们会逐渐提升启发式等级直到生成的程序稳定.然而实际上,在我们的评估中,即使是最激进的启发式方法,也只会使得引入的最终代码少量增加
-
当所有相关代码被确定,我们开发了一个二进制重写平台来删除不必要的代码生成一个简化的程序.
- 由于简化程序的特性,我们的平台不会面临一般二进制重写工具的符号化问题[51,52,53,5]
TODO
- 具体来说,通用二进制重写工具必须保留所有程序功能,但在没有可靠反汇编技术和完成的控制流图(control-flow graph,CFG)下很困难
- 对于去膨胀,我们只保留样本输入的功能,其中反汇编和CFG可以通过观察程序运行获得
- 由于简化程序的特性,我们的平台不会面临一般二进制重写工具的符号化问题[51,52,53,5]
-
我们设计了RAZOR框架来实现部署后的去膨胀,框架由三个部分组成
- Tracer:监视程序在样例输入上的执行,记录执行代码
- PathFinder:使用启发式方法从已经执行的代码中推断出更多的相关代码
- Generator:基于Tracer和PathFinder的输出产生新的二进制文件
-
在RAZOR框架中,我们实现了三个追踪器(两个基于动态二进制插装,一个基于硬件追踪功能),四个启发式路径查找算法,一个二进制生成器
-
为了理解RAZOR的效果,我们再三个基准集进行了测试(1)SPEC CPU2006 基准 (2)ChiselBenchmark 10个程序 (3) 两个真实程序,火狐浏览器和闭源PDF阅读器FoxitReader
-
在我们的评估中,我们基于一组训练输入执行跟踪和去膨胀,并用一组和输入相似但不同的输入进行测试,结果显示
- RAZOR可以减少70-80%的程序大小,同时只引入了1.7%的新二进制文件开销
- 我们将RAZOR和CHISEL进行比较,在ChiselBenchmark上,我们发现Chisel简化程度更好,但在测试用例中失败了几个,同时Chisel引入了新的漏洞在简化程序中,例如 由于删除边界检查而导致的缓冲区溢出.而RAZOR不会引入新的问题
- 我们还分析了我们的路径查找器识别出的相关代码,并发现不同的启发式方法可以很好的提升程序的鲁棒性
-
综上所述,我们的论文创新如下
- 新方法:RAZOR,在二进制文件上进行使用,需要给定输入
- 开源:Razor仓库地址
- 贴合实际,即开即用:在现实程序中进行了测试,结果显示简化后的程序有显著的减少,更好的安全性
2 问题
2.1 Motivating Example
- 上图展示了一个膨胀的程序,用于解析不同种类的图片
- 基于用户的选择调用不同的函数
- 在
parsePNG
中- 代码首先分配内存来保存图像
- 然后将内存地址保存在
img
中 - 然后通过使用宏
ALIGN(v,a)
,确保img
按照16字节对齐. - 最后调用函数
readToMem
将图像内容从文件加载到内存中进一步处理
parseJPEG
和parseGIF
代码类似
- 然而当用户只需要解析PNG格式的图片,该程序是膨胀的,例如 iPhone设备总是PNG格式的图片,因此不需要解析JPEG的代码,这些代码中可能还存在一些安全问题.
- 在现实中,我们可以轻易找到支持过时格式(如PCX,Sun Raster,TGA)的文档阅读器(如MacOS 上的 Preview),我们可以对这些程序进行去膨胀来减小他们的大小和攻击面
2.2 Program Debloating
-
问题定义:给定一个程序
, 其有一组功能 ,用户指定的功能 , 我们的目标是生成一个程序 保留在 中的函数,并且拒接对 中函数的请求 -
本文关注于在软件二进制代码中简化功能:我们从运行中构建一个控制流图CFG来指导基于二进制的简化.
-
下图展示了之前例子在二进制下的状态
-
- 黑色箭头表示过程内跳转
-
- 虚线箭头表示过程间跳转
-
-
在CFG中去除不可达的节点,我们可以得到简化后的CFG
2.3 挑战和措施
- 功能->函数->二进制代码的转换是有挑战的,为此引出两个问题
- C1: 如何表达不必要的功能
- C2: 如何将功能对应到代码
- 以上问题的可能解决方法是根据用户提供的一组测试用例来观测程序的运行以此识别出相关代码.
- 我们的问题可以描述为:给定二进制程序
和一组测试集 ,每个测试用例 触发 的一些函数,我们会创建一个最小的程序 仅支持 中的测试用例触发的函数
- 我们的问题可以描述为:给定二进制程序
- 测试用例可以帮我们解决C1,C2,但测试用例不能完全覆盖所需功能的全部相关代码,即有一些相关代码不会被触发.如果我们仅删除未运行的代码,程序功能将会被破坏
- 例如:在上面解析图片的代码中,第11行的判断可能在测试用例中一直未实现,导致第12行被认为是未执行代码,这将引发问题.
- 在本文后续$5.2 的评估中,展示了如果只是简单的移除未运行的代码,会导致很多问题,甚至程序的崩溃.并且所有基于测试用例的方法都会有这个问题
- C3: 如何从有限的测试用例中找到相关代码
- 针对此问题,我们提出了基于CFG的启发式方法来推理出更多的相关代码,来支持我们需要的功能
- 问题定义:
- 假设
中的测试用例触发了$ \mathcal{I} = {i_0,i_1,i_2,…} T$所覆盖功能的相关代码. - 具体地,我们定义一个超集
并保留在 中的所有指令,同时删除其它指令使得代码最小化.
- 假设
- 在上面解析照片的例子中,如果测试用例中没有经过12行,我们的启发式搜索将会包括这一相关代码
- 在$5.3 中 我们将会展示我们的启发式方法在查找相关代码上是有效的,并且只会引发少量的代码增加
3 系统设计
- 上图展示了RAZOR的架构:给定一个膨胀的二进制程序和一组可以触发所需程序功能的测试集,RAZOR移除不需要的代码生成 支持所需功能的最小二进制代码
- 为了实现上述目标
- RAZOR首先使用
Tracer
收集测试用例上的运行路径 - 随后编码路径构造程序的CFG,其中包含所有的运行指令
- 接着,RAZOR使用
PathFinder
来识别出相关代码,即使用启发式方法扩展CFG - 最后基于扩展的CFG
Generator
重新生成二进制代码来生成满足功能的最小二进制程序
- RAZOR首先使用
3.1 运行路径收集
Tracer
收集三种信息记录控制流信息
- 已执行指令,包括内存地址和原始字节
- 条件分支的跳转或不跳转,如 je指令
- 间接跳转或调用 的目标地址,如 jmpq *%rax
-
Tracer记录已执行指令的原始字节以处理动态生成/修改的代码,然而,指令级别的记录效率低下,同时大多数实际程序只包含静态代码
-
因此Tracer从基本块级别的记录开始,仅记录每个已执行基本块的地址;
- 在执行过程中,它检测所有动态代码行为,如可写和可执行的内存区域(例如即时编译[13]),或重叠的基本块(例如合法的代码重用[26]),并切换到指令级别的记录以避免遗漏指令。
- 一个条件分支可能会被多次执行,最终覆盖一个或两个目标(即fall-through目标和跳转目标)。对于间接跳转/调用指令,我们记录所有已执行的目标并计算它们的频率
-
下图为一个收集的运行路径的例子![]
- 记录了分支跳转的情况
- 间接跳转的次数
(/images/论文/software%20debloating%20论文/(4)_4.png)
-
我们发现使用多种工具收集执行跟踪是值得的
- 首先,没有一种机制可以完全高效地记录跟踪信息
- 基于软件的插桩工具:可以忠实地记录所有信息,但引入了显著的开销[7, 25, 6]
- 基于硬件的日志:可以有效的记录信息,但需要特殊的硬件,并且不能保证完备性(例如,Intel PT中的数据丢失)
- 其次,程序在不同跟踪环境下执行的程序将显示不同的路径
Dynamorio
总是将文件名扩展为绝对路径,导致一些程序中不同的执行路径(如 vim)
- 首先,没有一种机制可以完全高效地记录跟踪信息
-
因此,我们提供了三种不同的软件和硬件实现,用户可以根据自己的需求选择最好的,甚至可以将多个工具的跟踪信息合并来获得更好的代码覆盖率
-
CFG的构造
- RAZOR使用收集到的执行路径来反汇编二进制文件,并以此可靠地构造CFG. 与先前使用启发式方法确定函数边界的工作[52, 51, 3, 4, 45]不同,RAZOR从执行跟踪中获取准确的指令地址和函数边界信息
- 从这种可靠地信息开始,我们可以识别更多地代码指令,随后我们的启发式方法可以将他们是否是相关代码进行考量
- 对于条件分支指令,两个目标都是已知的,都可以有效地进行反汇编
- 对于间接跳转/调用指令,我们可以识别出潜在地跳转表
3.2 基于启发式的路径推断 Heuristic-based Path Inference
我们使用最佳尝试的启发式方法以包括更多的相关代码.以下我们将按照保留代码的程度依次介绍
- 下图为使用不同启发式方法产生的不同效果
- 左侧为原始CFG,右侧为扩展后的CFG
- 代码设计为计算
log(sqrt(max(rax,rbx,rcx)))
- 虚线表示追踪过程中未执行的分支和块,原始的执行路径是
(1) 零代码启发式 Zero-code heuristic (zCode)
- 该启发式方法引入新边(例如基本块之间的跳转)在CFG中
- 对于只有一个目标的条件跳转指令,
PathFinder
会检查非选择目标是否已经在CFG中,如果在,PathFinder
也允许从该分支指令跳到非选择目标.- 这里需要注意到,这是将未运行的代码向与运行的代码中调价,即相关代码,所以总的程序仍然是简化的
- 在上图中,
zCode
启发式方法将会引入边,因为 是 的非选择目标,但之后依然存在在CFG中
(2) 零调用启发式 Zero-call heuristic (zCall)
- 该启发式包括不执行任何函数调用的替代执行路径
- 使用该启发式方法,
PathFinder
从条件语句中未被采取的目标出发,并跟着控制流找到最终和 已执行路径合并的新路径. 如果新路径中没有call
指令,则将其添加到CFG中 - 当
PathFinder
走到未运行指令时,我们没有准确的信息来反汇编或构建CFG,相反,我们依赖现有的方法[53,3]进行二进制分析 - 应用该启发式方法zcall,在上图中,
PathFinder
将会引入块和边 ,因为该路径在 出进行了合并,且不包含调用指令
(3) 零库调用启发式 Zero-libcall heuristic (zLib)
- 与zCall先死,但
PathFinder
在包括可选路径时更加激进- 新路径可以包含同二进制文件中的函数或已执行的外部函数的调用指令
- 但zlib不运行调用未执行的外部函数
- 在上图中,使用该启发式,
PathFinder
将会引入块和边 ,因为该路径没有对未执行的外部函数的调用
(4) Zero-functionality heuristic (zFunc)
- 该启发式方法运行包括为执行的外部函数调用只要其不触发新的高级功能
- 为了将库函数和功能相关联,我们检查它们的描述并将其手工分组.
- 例如,log和sqrt术语指数和算数字部份,因此认为其基于相似的功能
- 使用该启发式方法,
PathFinder
将会引入块和边 ,因为该路径没有对未执行的外部函数的调用,且不触发新的高级功能
(5) 算法
- 下图展示了
PathFinder
寻找相关代码的步骤
- 对于CFG中的每一个条件语句
- 使用
get_non_taken_branch
得到未执行的分支 - 如果两个分支都执行了,继续遍历;否则按照启发式等级进行判断
- 如果为执行的分支属于当前CFG且当前启发式等级大于等于
,
5.将未执行的分支加入到CFG中 - 如果启发式等级比
高, PathFinder
将会首先得到从未选择分支出发的可选路径并且最终合并到已执行路径中. - 随后迭代所有的可选路径,在迭代中根据启发式等级调用相应的验证函数
- 最后在13和14行,确定该路径是否需要被包括到CFG中
3.3 去膨胀二进制合成 Debloated Binary Synthesization
-
通过原始的膨胀二进制代码和扩展的CFG,
Generator
生成一个简化的二进制文件并包括所有的功能-
- 其将根据CFG反汇编原本的二进制代码,并生成包含所有必要指令的伪汇编文件
-
Generator
修改伪汇编代码来创建合法的汇编代码. 这些修改包括 基本块的表示,具体化简介跳转,插入故障处理代码
-
- 将汇编代码编译成目标文件,其中包括所需指令的机器码
-
Generator
将机器码从目标文件中复制到原始二进制文件的新代码中
-
Generator
修改新代码段来修复原始代码和数据的引用
-
- 最后,
Generator
将原始代码段设置为不可执行以减小代码大小.(我们将原始代码段留在去膨胀程序中来支持可能的读取,如switch语句的跳转表[11],该设计将于后续讨论)
- 最后,
-
-
该部分需要底层知识,很费时间,如之后需要再细看
3.3.1 基本块的表示
3.3.2 跳转的具体化
3.3.3 故障处理代码
4 实现
- 我们使用1085行C代码、514行C++代码和4034行Python代码实现了RAZOR的原型,如下图所示
- 该原型只支持x86-64 ELF二进制文件
4.1 Tracer Implementations
- 我们为用户提供了三种不同的跟踪器,用户可以根据自己的需求选择最好的,甚至可以将多个工具的跟踪信息合并来获得更好的代码覆盖率.
- 在我们的评估中,我们使用基于软件的插桩技术来收集简单程序的完整跟踪信息,并使用基于硬件的方法来高效地获取大型程序的跟踪信息。
(1) 采用软件插装进行跟踪
- 使用动态插装工具Dynamorio [7]和Pin [25]来监测程序. 这两个工具都提供了在函数级别、基本块级别和指令级别的插装接口
- 我们通过三个插装步骤来收集控制流信息
-
- 在每个基本块的开始处记录起始地址
-
- 在每个条件分支指令,在指令和其两个目标之间插入两段代码来监听跳转的信息
-
- 在每个间接调用和跳转指令前,记录每次调用的具体目标
-
- 当基本块和条件分支指令运行后即可移除插装,保留间接跳转和调用指令的插装,以此减少不必要的开销
(2) 使用硬件特性进行追踪
- 考虑到软件插装的开销,我们提供了一个基于Intel处理器跟踪(Intel PT)的高效Tracer。
- Intel PT以高度压缩的方式记录流信息的变化:TNT数据包描述了一个条件分支是否被执行;TIP数据包记录了间接分支的目标,例如间接调用和返回。
- 由于Intel PT直接将跟踪信息写入物理内存,而无需触及页表或内存缓存,因此它实现了最高效的跟踪。
- 可以使用不同平台上其它硬件功能实现更高效的Tracer,如Intel CPU上的分支跟踪存储(BTS)或在ARM CPU上的程序流跟踪(PTM)。
4.2 更新ELF异常处理 Update ELF Exception Handler
emmm… 看得不是很懂呢,且跟汇编有关,略
5 评估
-
从以下方面进行评估
- Code reduction:代码简化程度5.1
- Functionality:
- 功能有效性 5.2
- 路径寻找的效率 5.3
- 安全性 5.4
- 性能开销 5.5
- 现实中的去膨胀 5.6
-
实验设置
- 在以下基准下进行评估
- 29 SPEC CPU2006 基准:包括12个C,10个C++,10个Fortran程序
- ChiselBench
- 两个现实程序:火狐浏览器和闭源PDF阅读器FoxitReader
- 使用基于软件的追踪器
Dynamorio
和Pin
来收集执行跟踪 - 对于FoxitReader和FireFox,我们使用基于硬件的追踪器
Intel PT
来收集执行跟踪,以此保证执行速度并避免异常行为 - 实验在64位ubuntu 16.04 系统上进行,使用Intel Core i7-6700K CPU, 32GB内存
- 在以下基准下进行评估
5.1 代码简化程度
- 因为RAZOR基于二进制,所以我们比较运行时的内存
- 下面对RAZOR和CHISEL进行对比:图a为在SPEC上,图b为在ChiselBench上
-
- 在SPEC中,RAZOR可以平均减少68.19%代码,CHISEL78.8%
-
- 在ChiselBench中,Chisel减少83.4%.
- 对于7个程序,chisel减少成都优于razor,但是在3个程序中,razor减少更多
-
- 需要注意的是Chisel更倾向于移除代码,只要满足输入即可
5.2 功能有效性
-
使用chiselBenchmark,运行原始二进制文件,使用chisel后的二进制文件,使用razor后的二进制文件进行测试,以确定所需功能是否保留,结果如下图示
-
RAZOR在使用保留更多代码的启发式时,最终都会通过功能验证. 而哈人的,Chisel自己只通过了3个,其主要有以下四个问题
-
- 错误的操作:因过拟合输入导致,如bzip2
-
- 无限循环:因为删除了循环的边界检查,如gzip
-
- 崩溃:删除了参数为空检查,如bzip2,date
-
- 遗漏输出:CHISEL删除了用于在stdout和stderr上打印输出的代码,导致结果丢失。如grep
-
5.3 路径寻找的效率
-
使用两组实验评估
PathFinder
在寻找所需功能的相关代码时的有效性-
- 使用从低到多不同启发式等级的RAZOR来去膨胀程序,找到对每个程序最小的启发式等级
-
- 使用N折交叉验证启发式方法的鲁棒性(在5.6中)
-
-
按照以下方法在ChiselBench上测试RAZOR
-
- 设计覆盖相同功能集的训练输入和测试输入
-
- 使用训练输入跟踪程序,并用不同等级的启发式进行去膨胀 none, zCode, zCall, zLib, zFunc
-
- 在测试输入上使用去膨胀后的程序,记录失败情况
-
-
PathFinder
配置见附录Table7,我们使用相同的选项构造训练集和测试集,但二者间具体的参数不同
-
上图展示了结果,可以明显看出,随着启发式等级的增加,失败的测试用例显著下降,而简化率的下降却不明显.
-
请注意,zCode启发式方法稍微增加了代码的减小幅度,因为它使得条件跳转的分支更多,从而减少了失败分支的插装。
-
四种启发式作用的例子:略,见论文
-
结论:
PathFinder
可以有效识别与训练输入线管的代码,并完备由训练输入出发的功能. 它强化了去膨胀二进制文件的同时保持了简化的效率
5.4 在安全方面带来的好处
-
通过统计减少的bug数来评估去膨胀带来的安全性
-
在ChiselBenchmark中统计其历史bug和如今bug
- 对于历史bug,我们探究其补丁是否在去膨胀程序中
- 对于如今bug,我们探究其问题代码是否仍存在
-
下图展示了评估结果,当前版本有13个bug,有10个在Chisel中被评估,3个bug存在在老版本
- 图中bzip, date, gzip,mkdir, rm, and tar 的六个漏洞仍然存在,因为测试用例执行了相关的有漏洞代码
- 另外六个漏洞并非由二进制文件本身引起。例如,CVE-2011-4089是由bash脚本bzexe的竞态条件引起的,而不是由bzip2二进制文件引起的。因此,RAZOR不会禁用此类漏洞。
-
Chisel与RAZOR相比
- Chisel比较激进,在移除了更多漏洞的同时也引入了旧的.
- Razor比较保守,阻碍移除漏洞,但有助于避免新的漏洞
-
同时测量了ROP(Return-Oriented Programming)gadgets的减少情况. 一旦攻击者能够改变控制流,可重用的ROP gadgets的数量将使得程序更易受控制流劫持的攻击
- RAZOR平均减少了 61.9%的ROP gadgets,而Chisel减少了 85.1%
- 结果在预期内,我们潜在的有意防止 forward-edge control-flow attacks,即攻击者通过破坏函数指针而不是返回地址来改变控制流
- 同时使用新技术可以进一步实现完整的控制流完整性
5.5 性能开销
- 时间
- RAZOR
- ChiselBenchmark上平均1.78s
- Firefox 8.51s
- FoxitReader 50.42
- Chisel:在ChiselBenchmark上要11hours+
- RAZOR
- 运行时开销
- 与SPEC基准测试进行比较,平均而言,RAZOR为去膨胀程序引入1.7%的运行时开销,表明其在现实世界不是是高效的.
- 间接调用是开销的主要来源,这里可以进一步优化
5.6 在现实程序上的去膨胀
-
对于Firefox浏览器,我们使用RAZOR加载了排名前50的Alexa网站[28]。我们随机选择了25个网站作为训练输入,并将另外25个网站作为测试输入。
-
对于Foxit Reader,我们使用RAZOR打开并滚动了包含表格、图形和JavaScript代码的55个不同的PDF文件。我们随机选择了其中的15个文件作为训练输入,并将另外40个文件作为测试输入
代码缩减和功能性
如上图示,当启发式等级达到zLib时,可以通过所有的测试用例
性能消耗
- 在几个基准测试上与运行了去膨胀的Firefox浏览器(启发式等级zLib)并发现RAZOR对Octane [33]、SunSpider [34]、Dromaeo-JS [30]和Dromaeo-DOM [29]基准测试引入了-2.1%、1.6%、0%和2.1%的开销
- 对于Foxit Reader未找到基准测试,但打开和滚动pdf未发现明显减速
应用场景:每个网站的浏览器隔离
- 在传统的浏览器环境中,所有打开的标签页和窗口都在同一个进程和内存空间中运行。这意味着一个恶意网站可以通过注入脚本或其他攻击手段来访问和修改同一进程中的其他站点的内容。这可能导致用户数据泄露、会话劫持和其他安全问题。
- 每个网站的浏览器隔离,作为浏览器去除臃肿的一个应用,我们可以创建仅支持特定网站的最小化版本
- 例如,银行可以为其客户提供一个只支持其网站所需功能的最小化浏览器,同时暴露最少的攻击面。
- 们在三个受欢迎且安全敏感的网站集合上应用了RAZOR:银行网站、电子商务网站和社交媒体网站。可以看出可以带来一定的收益
启发式方法的N折交叉检验
- 首先,我们将Alexa的前50个网站随机分成五组,每组10个网站。我们从中挑选了两组(共20个网站)进行训练,剩下的30个网站用于测试。我们进行了10次这样的评估. 结果略
- 其次,我们将Alexa的前50个网站随机分成10组,每组五个网站。我们从中随机挑选了五组(共25个网站)进行训练,剩下的25个网站用于测试。我们进行了10次这样的评估。结果略
- 结果表明,我们的启发式方法对于推断具有类似训练输入功能的未执行代码是有效的
6 讨论
尽最大可能的路径推断 Best-effort path inference
- 将高等级的功能对应到低等级的代码是具有挑战性的,尤其是在代码未开源时
- RAZOR采用基于控制流的启发式方法来推断更多的相关代码,但是这些方法并不完美,都是尽最大你眼里
- 目前,启发式的方法在二进制分析和重写中被广泛使用[53,52].通过执行跟踪,RAZOR能够减轻这些工作的局限性,例如找到间接调用目标.
- 评估显式,我们的基于控制流的启发式方法在实践中是有效的
CFI and deblaoting
- 控制流完整性Control-flow integrity (CFI)强制每个间接控制流转移(即间接调用/跳转和返回)都前往合法目标[1], 它防止了开发人员无法预料的恶意行为
- 而软件去膨胀需要根据用户的需求删除良性但不必要的代码
- 例如,如果函数A被设计为间接调用i的合法目标,CFI将允许从i到A的转移。但是,如果用户不需要A中的功能,软件去膨胀将禁用转移并完全删除函数代码
- 一方面,去膨胀实现了一种粗粒度的CFI,攻击者只能将控制流转移到剩余的代码。同时简化了一些CFI工作所需的分析,因为代码基础更小
- 另一方面,现有的CFI为实施去膨胀提供了基本平台。
- 例如,RAZOR利用了在binCFI [53]中开发的几种二进制分析技术进行优化。
基于库的去膨胀
- 我们尝试使用RAZOR对每个程序的库进行去膨胀,又成功的也有失败的
- 例如,在去膨胀libc.so库时,使用zFunc启发式方法最激进的保留代码,还是会触发不同的执行路径. 检查原因时发现其执行路径对环境变化非常敏感
-
- 其包含大量针对内存或字符串的高度优化代码,这戏代码会根据参数值选择最有效的实现方式,如strncmp有16种实现方式
-
- 其会根据进程状态进行不同的擦欧总,如每次内存分配,malloc会选择第一个可用块,输入的不同可能会导致malloc遍历完全不同的路径
-
- 为此我们计划开发针对库的启发式方法来应对环境敏感的运行
- 例如,我们可以在函数级别而不是块级别上进行去膨胀
- 例如,在去膨胀libc.so库时,使用zFunc启发式方法最激进的保留代码,还是会触发不同的执行路径. 检查原因时发现其执行路径对环境变化非常敏感
- 目前我们还将探索基于源码的方法,并尝试将其移植到二进制上
删除原始代码
-
RAZOR当前的设计是将原始代码 保留在去膨胀程序中,并将其权限更改为已读,以减少攻击面.
-
这种设计简化了对代码中潜在数据的处理,程序可能因为一些特殊目的来读取这些数据
-
为了进一步减小程序的大小,我们完全可以删除原始代码部分,具体操作如下
- 在执行跟踪期间,我们将原始代码部分设置为仅执行[11],以便任何从代码部分读取的操作都会触发异常并被追踪器记录;
- 我们执行向后的数据流分析,以确定每个记录的内存访问所使用的数据指针的来源;
- 在二进制合成过程中,我们将数据从原始代码部分重定位到新的数据部分,并更新新代码以访问新位置。
-
通过这种方法,我们可以解决二进制重写过程中输入的重定位这一挑战性问题.
- 实际上,我们进行了研究来了解这个问题的普遍性,我们发现在我们文章中测试用的程序,给定我们的测试用例,没有一个程序会从代码部分读取数据,此时只需要简单的删除原始代码即可,以减小文件大小和内存占用
未来工作
- 我们将开源RAZOR
- 我们计划扩展该平台以支持更多格式和架构的二进制文件,包括共享库、32位二进制文件、Windows PE程序、MacOS March-O程序和ARM二进制文件。
- 同时,我们将设计更多与安全性相关的启发式方法,使RAZOR能够支持各种真实世界的情况。
7 相关工作
Library debloating:基于库的去膨胀
-
库旨在未不同的用户提供大量的功能支持,对库去膨胀可以对每个程序定制不同的代码基础,并显著减少代码
-
Mulliner :CodeFreeze:用于从Windows共享库中删除不必要的代码 [36]
-
Quach :piece-wise:使用分块编译和加载进行去膨胀 [40]
-
Jiang : 提出从Android应用程序,Java运行时环境和SDK中删除死代码的方法 [23,22]
-
我们的系统与上述方法有两个不同
- 之前的方法在每次过程开始都要进行二进制重写,而RAZOR通过静态二进制重写生成简化的二进制程序,一次重写永久使用
- 库去膨胀使用静态分析找到不用的代码,并且保守地保留了所有可能有用的代码;而我们地系统通过动态执行来跟踪和定位 执行的代码 或通过启发式找到代码,并移除其它地代码
Delta debugging 增量调试法
- DD被提出来 最小化触发错误的输出,例如
- Regehr,C-Reduce[42]:高效生成更小的测试用例
- Sun,Perses[49]:使用形式化语法 很快地 生成更小却功能相同的程序
- Heo,Chisel[15]:使用强化学习来加速DD过程
- 然而,由DD产生的程序仅支持测试用例,尽管现实中对确定的功能有近乎无限的测试用例.
- 相反,RAZOR使用基于控制流的启发式方法,可以推断出更多的必要的相关代码来完备所需的功能
基于源代码的去膨胀
- 最近很多方法使用程序分析来简化代码
- Bu [8] : 提出了一种 bloat-ware 设计范式,该范式分析Java源代码来优化对象分配,避免运行时内存使用膨胀
- Sharif [44] : 提出 Trimmer,将用户提供的被指传播到程序代码中,然后使用编译器优化来减小代码大小
- 这些系统,以及[42,49,15] 依赖于对源代码的复杂分析,而这些分析不总是能用于部署的系统. 相反,RAZOR仅需要二进制程序,使其更适合实际部署
基于容器的去膨胀
- 容器变得越来越受欢迎,但其代码基础越来越膨胀
- Guo[14] 提出了一个监视程序运行的方法,以识别必要的资源为被追踪的程序创建一个最小的容器
- Rastogi[41] 开发了 Cimplifier , 使用动态分析来收集不同程序的资源使用,然后根据用户定义的策略将原始容器划分为一组较小的容器
- RAZOR也适用于容器或其它系统的去膨胀,例如 Inter PT 支持操作系统的追踪,而这时RAZOR中使用的
基于硬件的去膨胀
- 近来,硬件设备也是膨胀的,例如通用处理器被过度设计于应用于特定应用,如 嵌入式设备,可穿戴设备和互联网设备
- Cherupalli[10] 提出了一个方法可以自动的从 通用处理器中 移除未使用的门,以生成为特定应用的定制处理器
- 目前,软件的膨胀和硬件的膨胀是分开进行的,一个有趣的方向是同时考虑硬件和软件来找到更多的去膨胀空间
8 结论
- 本文提出RAZOR,一个用于实际程序去膨胀的框架,它利用一组测试用例和基于控制流的启发式方法,来收集支持用户所需功能的必要代码
- 去膨胀的二进制程序有较小的攻击面,改进的安全性保证,鲁棒性的功能和高效的执行
- 我们的评估证明,RAZOR是一个实用的,可用于对现实程序简化的去膨胀方法
9 阅读总结
- 本文问题驱动很好,对于老旧的程序,肯定会有一些过时的功能,这些功能的代码可能会带来安全性的问题,为此需要程序去膨胀
- 相关代码的定义很好,但这应该是出现在基于输入测试用例的方法上,因为测试用例可能不能包括所有情况,所以有的相关代码没有被运行,如果轻易删除会引发问题.
- 但是如果是非基于输入的方法呢
- 需要坚实的底层基础,如了解汇编和反汇编等,这样才能更好理解本文方法
- 去膨胀需要在不同级别进行
-
本文因是在二进制上,所有可以从块级别出发构造CFG,然后进行去膨胀
-
对于基于源代码的,可以基于函数级别,基于库级别,而对于面向对象的语言,可以对于包级别等
-
问题
-
- 第四种启发式方法,即保留最大程度,怎么还要手工标注库的功能区分
-
- 方法基于二进制,可以无视软件是否开源直接使用,但相应的需要对应的底层代码知识,不方便开发人员进行二次改造(大概),并且这涉及用户的信任度,真敢用吗
-
10 复现
- 使用docker进行复现
复现ChiselBenchmark
按照 github网站指示 即可