意义
商用软件 大量功能 for 用户流行度,但个体用户仅需小部分功能
bloated code 缺点:阻碍最优执行;更多的薄弱点,更大的攻击面;浪费内存
RAZOR
基于users‘ specification
,采用几个control-flow heuristics
去推断支持用户期望的功能所需的related-code
,以此给deployed binaries
进行code reduction
。
实现
software debloating最初总依赖 程序源代码可用
(不可行:用户一般无权访问源码;rebuild软件很难;每个用户想去除的功能不一),故在软件在终端部署完成后
对binary进行debloating最好,即post-deployment。
给定一个bloated binary和一组test cases,RAZOR
移除unnecessary code,产生一个debloated binary,它用minimal code size支持所有被要求的功能
大致实现流程:
-
post-deployment approach
的挑战:
1)如何让没有普通end-user表达自己想要哪部分功能。
终端用户提供一组sample input
。2)如何修改software binary只留下需要功能
换个说法 1)如何表达unnecessary功能。2)如何映射高级功能到low-level code。(功能 to 代码) 3)怎么从有限的test cases中发现更多related-code
-
related-code
:对支持用户要求功能是必要的,但在处理sample inputs时未执行过的代码叫相关代码。但是用户很难提供一个齐全的input,能把实现某个功能的所需代码都用到。就算用户能提供得很全面,也很难识别这些input的reachable code。所以需要使用
best-effort heuristics
去尽可能的识别related-code
。使用heuristics去推断related-code基于假设
:具有较大差异的代码路径展示较少的相关功能(不同指令、调用新函数、额外库函数、新的高级功能) -
识别出related-code后,开发一个
binary rewriting platform
移除不必要代码,产生一个debloated program。只保留与sample input相关的功能,需要反汇编跟control-flow graph(CFG),通过观察程序执行得到。
因此,RAZOR有三个组成部分:Tracer
监控并记录执行给定sample inputs时的executed code,还可获得disassembling和CFG;PathFinder
使用heuristics根据executed code识别出related code;Generator
基于Tracer跟PathFinder的输出产生new binary。
具体设计
Execution Trace Collection
Tracer
用test cases执行bloated program并记录control-flow information,主要是三类信息:1)executed instructions,包括指令的内存地址跟原始字节(raw bytes);2)条件分支是否taken(使用?);3)间接jumps和calls的明确目标以及跳转频数。
Tracer只记录每个executed basic block
的地址,当发现动态代码行为才切换到instruction-level recording
(平时不用因为指令级效率低且大多现实程序只有静态代码)
需要多个工具一同收集execution trace。只用软件仪器可能需要significant overhead,只用硬件可能要特殊硬件而且可能不全面。此外不同的跟踪环境下可能paths不同。
作者用software-based instrumentation
(Dynamoria和Pin)收集简单程序完整traces:首先在静态块开始处记录其起始地址,在条件跳转的指令和目标间插入两个代码片收集是否taken,记录每个间接调用和跳转的确切目标。runtime
时,静态块执行一次后马上移除,条件分支被taken后的instrumentation(测量代码?)马上移除避免开销,间接调用不移除;
用hardware-based
(Intel Processor Trace,Intel PT)跟踪大程序:TNT包记录条件分支的taken,TIP包记录间接调用的目标,Intel PT直接把trace写入物理内存(不经过页表和cache),非常高效的获取控制流信息。
构建CFG图:RAZOR根据collected execution traces反汇编bloated binary并构建CFG图。
Heuristic-based Path Inference
Zero-code heuristic(zCode
):如果no-taken分支可通过其他块达到,就直接把该分支加到CFG图中
Zero-call heuristic(zCall
):如果 non-taken分支不包含任何call指令,就加到CFG中(如进到if的某个不包含函数调用的分支里是可以的)
Zero-libcall heuristic(zLib
):分支里的call指令调用的是相同binary下的函数?或者是已经被执行过的外部函数,该分支也可以添加进去
Zero-functionality heuristic(zFunc
):如果分支调用了未被执行过的外部函数,但该外部函数不触发新的高级功能,那就也添加进去
Debloated Binary Synthesization(合成)
现在有了:original bloated binary 跟 expanded CFG
1.Generator对原始二进制按照CFG反汇编,生成一个包含所有必要指令的伪汇编文件
(pseudo-assembly)。
2.将伪汇编文件修改成有效汇编文件
,修改包括符号化基础块、具体化间接调用/跳转、插入错误处理代码。
3.编译汇编文件成为包含必要指令的机器代码的目标文件
(object file)
4.复制机器代码到原始二进制中的new code section
5.修改新代码段来修复对原始代码和数据的所有引用?
6.设置原始代码段不可被执行(为什么还要把源代码保存在debloated program中?为了可能性读取?)
给每个basic block设置唯一标签
(如L1,L2),然后把直接jump跟call的目标地址用标签替代,这样不管我们如何操作汇编文件都能产生正确的机器代码?(只更新显示的直接调用地址)
indirect call/jump具体化
。确保所有possible targets都指向new code section。Generator处理间接调用:同一模块下的local targets,用许多compare-and-call指令替换,compare成功后执行。不同模块下的global targets,先通过全局转换函数找到正确模块,每个模块有一张存着被其他模块调用过的targets的translation table,然后从表中找到正确target。
错误处理
:对于条件跳转到non-taken target或者间接调用指令target不被允许就直接转到fault handler,将退出执行并dumps the call stack。(这样真的好么?)。生成ELF binary后还得修改.gcc_except_table指向这个错误处理程序。
结果
在 常用benchmarks跟Firefox、FoxitReader上都成功从bloated binaries中减少大量代码
并且没有引入任何安全问题
,heuristics也只使code size少量增加,因此对于debloating现实app也是实用的
RAZOR能有效减少70%~80%的original code,只给新binary引入了1.7%的overhead,未引入任何安全问题。
RAZOR采用激进些的启发法zLib、zFunc等,虽然代码减少的稍微少一点,但是可以fail cases会减少很多。例举了一些不同启发法fail的实例:不用zCode就会一直进到if的正确分支;zCall训练集中if总不进,测试集里却可能进,zCall可以把进if的跳转指令加上;zLib可以进一些执行过的函数;zFunc,strcmp,strncmp
RAZOR更保守的移除bloated code,虽然移除的bugs
可能少些,但不会引进新的bug
code reduction:因为RAZOR工作在binaries上面,所以不能测量源代码行的减少,所以测量的是可执行内存区域的大小。
RAZOR debloat CHISEL基准点基本少于一分钟,但是CHISEL要1到11小时。
针对Firefox跟FoxitReader,代码减少率达到60%跟80%以上,并且zLib跟zFunc基本可以做到无crash。对于一些银行网站、电力公司网址等,只支持clients所需的功能可以使攻击面降到最低,较安全。
缺点:
假如程序包含大量间接调用,RAZOR的performance overhead会比较大,因为要用大量比较跟直接调用替换,引入大量if-else。解决:通过frequency-based optimization可以减少开销。
在一些包含大量内存跟string操作的库上debloating就不好用。因为这些库基于参数值、进程状态执行。解决:1.用功能级debloating代替basic block级 2.探索已有的库debloating方法。
CHISEL
只支持given inputs
,跟把输入输出提前对应起来没差。并且多次重复相同输入也会导致不同execution path。too naive。CHISEL实现smaller code size
,但是有fails,还引入了可被利用的vulnerabilities(如buffer边界检查被移除),即CHISEL有很多鲁棒性问题跟安全问题
,如unexpected operation、infinite loop(remove loop condition)、crashes(remove the check)、missed output(stdout\stderr)
dead code elimination
方法和delta debugging
方法只关注移除静态代码或者根据一些特定input阻止程序行为,不能识别related-code。
问题:
sample input的格式是什么?什么时候用户给出这么一个sample input?流式计算是直接默认用户的常用功能,其他功能按需加载还是也会有这么一个sample input?
代码切片跟debloating的关联在哪?