无goto:使用模式无关的控制流结构化和语义保留转换的反编译
No More Gotos:Decompilation Using Pattern-Independent Control-Flow Structuring and Semantics-Preserving Transformation,2015
简介
本文实现了无goto输出的DREAM反编译器,非结构化的AST利用语义保留的控制流转换为结构化的AST,结构化的AST通过模式无关控制流结构化算法产生反编译输出。DREAM反编译器在评估中的结构化程度和紧凑性明显优于当时先进的反编译器Pheonix和Hex-Rays v2.0。
产生goto的原因和避免goto的理由
产生goto :传统反编译器使用模式匹配来确定高级结构,当遇到无法匹配的复杂结构时,会使用goto来进行跳转。
避免goto:因为goto的跳转会增加反编译程序的复杂程度,使得反编译代码不易理解,降低可读性,所以要避免。
模式无关的结构化算法
结构化环区域
- 初始循环节点与后继节点:确定初始循环节点集合 n l n_l nl,如果循环区域包含异常入口,则用语义保留的控制流转换将其转换为单入口区域。
- 后继节点优化与循环成员资格:通过迭代查找所有直接前驱节点都在循环内且被头节点支配的后继节点,扩展当前的循环节点集合。根据优化后的循环节点集合定义精确的循环成员关系,前驱结点不都在 n l n_l nl中的节点为出口。如果存在多个出口的情况,则用语义保留的控制流转换将其转换为单出口区域。
- 循环条件推断:表示循环头节点到后继节点的每个边为一个break语句,计算循环头节点到后继节点的条件取反即为循环条件。
结构化无环区域
- if-else结构:首先检查是否存在if-else结构,即分支节点的到达条件互补。将一个子节点分为真分支候选Vc,假分支候选为剩余节点Vd。如果符合条件,则构造一个if节点替代Vc和Vd。
- switch结构:当if-else结构不能优化AST时,寻找可以表示为switch结构的节点。我们首先寻找一个switch候选节点,其达到条件是一个变量与常量的比较。基于达到条件的类型,将剩余节点分为三组:case候选Vc,default候选Vd,和剩余项Vr。如果找到至少两个case节点,构造一个switch节点Vs替代Vc和Vd在序列中。
- if-else if结构:当if-else结构和switch结构都无法进一步优化AST时,寻找if-else if结构。如果节点N的子节点集合{n1, …, nk},集合中的任意两个节点之间不存在路径,所有达到条件的逻辑或结果为true。如果符合条件,则构造一个if-else if节点替代N和{n1, …, nk}。
计算点间到达条件的方法
点间到达条件即为点到点间每个无环路径到达条件取逻辑OR。
副作用和解决措施
副作用:a-b-c,当计算a和b之间的条件和计算a和c之间的条件时,会重复计算a和b之间的条件两次。
解决措施:当计算高级结构时,会用bool变量记录两个点之间的条件,方便之后的计算。
语义保留的控制流转换
循环作为一种特殊的高级控制结构,当其具有多入口或多后继点时,无法被模式无关的方法化简为单入口与单后继的形式,因此我们提出一种基于语义保留思想将多入口多后继循环转换为等效的单入口单后继结构的方法。
- 多入口时,循环有除了 n h n_h nh以外的入口,即不平凡入口
- 多出口时,循环有除了 n s n_s ns以外的出口,即不平凡出口
容易知道,平凡入口 n h n_h nh指向循环头,平凡后继 n s n_s ns由发出回边的节点指出,其它与循环节点直接相连的外部节点即不平凡入口(出口)
结构化多入口循环
基本思想是使用结构化变量 i 记录进入循环的位置,进入循环时测试 i 的值以定向到不同的进入位置
进入循环时执行的第一个检查时i=0,这最小化了对原程序性能的改变
结构化多后继循环
基本思想是为每个不平凡入口分配唯一的条件,到达不平凡出口时使用break离开循环并测试条件
从所有不平凡出口的最邻近公共支配者(NCD)开始计算条件,这样能够降低获取到条件的复杂程度
结构化后优化
执行了如模式匹配三目表达式和for循环、用函数调用替换函数内联、基于API调用命名变量的一些优化
评估
测试集选取了GNU基准代码和三种恶意代码样本,对本文实现的DREAM、最先进的学术反编译器Pheonix和行业领先的商业反编译器Hex-Rays v2.0一起进行了测试
评估指标:
- 正确性:基于GNU基准测试
- 结构化程度:基于产生goto语句的数量来衡量
- 紧凑性:基于反编译出代码的行数
实验结果表明,DREAM能做到100%的正确性,总是0 goto的反编译代码,以及大幅领先的紧凑性
总结与讨论
创新。本文创造性地提出了0 goto的设想,并创造性地提出了基于模式无关的结构化思想来实现这一设想;卓越。本文完全实现了无goto的设想而不是只完成部分工作,并在评估中与最先进的反编译器进行对比且能取得优势;严谨。在实验设置中本文作者对许多细节的讨论,以及最终实验时采取了四组实验互相对照的模式,充分体现了本文作者严谨的科研作风。综上所述,我认为本文是一篇学术价值非常高的文章,充分展现了作者高水平的学术能力。
当然,本文同时也存在一些不可忽视的局限性,这些局限性大多在后来的研究中被挖掘出来。
-
首先,在结构化多后继的循环时,条件部分容易出现路径重叠,并且这样的结构化会导致循环内的局部变量需要全局定义,造成了变量冗余。这个问题在2020年ren.ng1的研究中得到了部分解决,ren.ng使用全新的结构化算法,实现0 goto的同时优化了DREAM的冗余表达式以及路径重叠问题,提升了可读性
-
其次,强行生成不含goto的代码并不一定利于理解,特别是在源码含有goto的情况下。这个问题是十分显然的以至于作者在本文中也多次强调。最新的2024年的研究2提出要生成接近于源码数量的goto语句,这有可能是未来的研究前景。
-
最后,简化任意布尔表达式仍是NP困难问题。由于DREAM的算法通过增加布尔表达式来简化结构,最终的结果可能是表达式过于冗余以至于无法理解。Sailr2的作者在他的一次演讲中计算了 DREAM 和其他算法输出中布尔运算符的数量。 DREAM 有大约9,600个布尔值。Pheonix有342个。源代码 (Coreutils) 有1,256个。
另外还有一个值得被讨论的问题。无论是DREAM,Pheonix,rev.ng还是Sailr,在进行评估时采取的是近乎截然不同的标准。因此,我们可以期待,未来在反编译器的评估方面能够提出更加科学客观的指标,用以最真实地评估反编译器生成代码的可读性。
相关工作
-
Andrea Gussoni, Alessandro Di Federico, Pietro Fezzardi, and Giovanni Agosta. A comb for decompiled c code. In Proceedings of the 15th ACM Asia Conference on Computer and Communications Security, 2020.
-
Zion Leonahenahe Basque, Ati Priya Bajaj, Wil Gibbs, Jude O’Kain, Derron Miao, Tiffany Bao, Adam Doupé, Yan Shoshitaishvili, Ruoyu Wang, Ahoy SAILR! There is No Need to DREAM of C: A Compiler-Aware Structuring Algorithm for Binary Decompilation, USENIX Security '24, 2024