基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译

**

TIE: Principled Reverse Engineering of Types in Binary Programs

**

JongHyup Lee, Thanassis Avgerinos, and David Brumley
Carnegie Mellon University

软件工程中的逆向工程(reverse engineering)就是根据已经存在应用程序,反向推出软件设计时的数据和理念的过程,其最终目的是使软件得以维护。
高级编程语言都含有像结构和类这样的数据抽象,而在编译期间这些数据抽象都被转换成寄存器上的操作或者是全局的内存地址。从二进制代码中恢复出高级语言层次的数据抽象和数据类型一直是逆向工程中的难题。如下图,从Source code对应的二进制文件出发,要求找出buf、out、c这三个变量的类型。其结果应该是即保守又精确的。
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第1张图片
TIE(Type Inference on Executables)就是一个基于二进制代码分析的类型重构系统。下图是TIE方法的整体流程。
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第2张图片
恢复数据抽象的过程主要分为两个部分:1.变量恢复 2.类型重构(有时也称类型推断)

  1. 变量恢复
    (1) 用Binary Analysis Platform (BAP)将二进制代码提升为Static Single Assignment (SSA)的形式。SSA中的变量是不会变化的,他们只会被指定一次。给一个变量赋一个新值,都会导致一个新的绑定。这样的好处就是我们容易知道正在使用的变量是在哪一个分支的或者值是在哪里改变的。并且,第三步使用DVSA算法也需要使用SSA的形式。
    (2) 利用BAP生成的二进制中间语言叫Binary Intermediate Language (BIL),用BIL作为DVSA算法的输入。
    (3) 用DVSA算法来推断有哪些变量和变量的位置。
  2. 类型重构
    (1) 标记DVSA算法推出的每一个类型变量,如rt表示term t的类型
    (2) 根据上下文和这些变量的用法产出相应的类型约束
    (3) 约束求解,给出结果

SSA:

SSA即静态单赋值,Static Single-Assignment,这是一种中间表示形式。 之所以称之为单赋值,是因为每个名字在SSA中仅被赋值一次。
如下图a中的一段程序的控制流图。从这张图中可以看到,最后一个基本块中y值的定义或者来自左侧的分支,或者来自右侧的分支。将每个赋值语句中的变量赋予一个唯一的名称后,一般新名称采用原变量+版本号的形式。 对于上面这段控制流图,就变成图2的形式。但图2中有个问题,有分支时,若分支中有对变量的操作,就无法确定使用了哪个版本的变量。 因此,引入了PHI节点。如下图4所示,PHI将分支中的y1和y2连接,并生成一个新的定义y3。有了PHI节点后,最后一个基本块中y3的定义来自之前的PHI节点,PHI节点中的两个操作数y1和y2分别来自左右两个分支。
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第3张图片
在SSA中间表示中,可以保证每个被使用的变量都有唯一的定义,即SSA能带来精确的使用–定义关系

TIE实例:

下图就是一个完整的实例,图a是二进制中间语言BIL,利用DVSA算法可以找出到底有哪些变量,图b给出的就是变量的地址,在同一个内存块中具有相同偏移的就是同一个变量。图c是为这些变量生成的约束。图d就是约束求解后的最终结果,给出上界和下界,变量的类型就介于上界和下界之间。
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第4张图片

类型重构:

一. 首先要给出类型系统的描述。
变量能被赋予那些类型?
这些类型是否足够,是否足够丰富?
这些类型的子类型关系?

下图给出了类型和约束的形式,以及基础类型的lattice
格中箭头联通的即存在子类型关系
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第5张图片
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第6张图片
在BIL中每一个term都会被赋予一个初始的低水平的类型regi_t,例如r(reg32_t)就是在内存地址中占了32位。

二. 给出最后输出的结果:
转换为C语言的类型
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第7张图片
因为C语言的类型更容易被大众接受,而且每一个类型都能对应到C语言的类型,说明TIE的类型系统足够丰富。
最后输出给用户的结果是一个上界和下界。
这里写图片描述
如果你给出的上界和下界正好是TOP和BOTTOM,那么肯定是对的,而且也不是用户想要的。那么就需要一个评价标准。

三.评价标准
保守的:变量的真是类型必然在所给范围之内。
精确地:上界和下界的距离尽可能的小。
距离的计算方法如下:
如果上界和下界具有子类型关系,那么距离就是他们在lattice中的距离,否则为|TOP-BOTTOM|。
如果是结构类型
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第8张图片

四. 产生类型约束
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第9张图片
说明:
表达式中的大部分约束,re表示运算前的类型,r是运算后的类型,若他们应该至少为某个类型T,那么约束为re<:T ∧ r>:T(对于前提是contravariant,对于结论是covariant。)

例如:
一元运算符-e,可以看作一个数字到数字函数,它的类型至少为intn_t⟶intn_t。而它的父类型也接受,即intn_t⟶intn_t<:re⟶r,那么有箭头类型的子类型规则有re<:intn_t ∧ r>:intn_t。
32位运算下的e1+e2分为三种情况,有可能是变量+变量,也有可能是指针+变量或者变量+指针。其中Tr的引入是为了说明r并非是要求r>:num32_t,只需要r>:re。

另外对于结构类型,32位运算下的e1+e2约束也需要稍作变化。(TIE把Structural type、array、prt(a)都看出record type,因为在二进制文件下这三种类型的本质都是record type)
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第10张图片
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第11张图片

五. 约束求解
利用相应的约束求解算法来对下面5个工作集进行更新,最后各个term的上界和下界就是最终结果。
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第12张图片
(C是上一步产生的约束的列表,算法每次处理C中的一条约束,当C为空时,该过程停止。S都初始化为空,B初始化为TOP和bottom)
TIE按照下面的顺序进行约束求解:

Equality constraints:
实质上是一个替换过程,如果有S=T,我们就将C集中的S替换成T
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第13张图片
其思想其实就是unification算法。简单的说unification 就是一个用来解constraint problem 的算法。这个算法的输入是一个constraint set, 输出是一组substitution 。在进行type inference的时候,我们会对各个term产生constraint,通过不断产生这样的constraints,最终unify出我们想要的值。比如有一个函数f 接受参数x,但程序没有写出函数的参数和返回值类型,所以我们先假设参数类型是a,返回类型是b。这样我们就带着这个假设去看函数定义。比如这个函数的函数体只有一个表达式x + 1 而且我们知道(事先给出)只有int 类型的值可以做”+”运算而且结果还是int,假设这个表达式的类型是c,那我们就有了constraint set {a = int, c = int},我们还知道函数体的类型就是函数的返回类型,所以constraint set 更新为{a = int, c = int, b = c}, 经过unify之后得出的substitution 是{ b -> int}。

Subtype relation constraints
算法:首先看S<:T是否可以分解,如果S <: T ᴖU, 那么可以拆分为S <: T和 S <: U,同理如果S ᴗT <: U,那么可以拆分为S <: U 和T <:U。如果有形式ptr(S) <: ptr(T),可以转换为S <: T.
然后将可传递的子类型关系都加到S<:集中,最后再更新上界和下界。
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第14张图片

Conjunctive constraints
如果约束的形式是C1∧C2,那么可以删除这条约束,并把C1、C2加到C集中。

Disjunctive constraints
约束的形式是C1∨C2。
如果C1和C2都不能满足时,TIE会提升一个类型错误。
如果C1和C2至少有一个能满足时,就将可满足的约束都加到C集中(相当于∧,这样得到的结果是保守的)。
Composition Rules说明了如何将可满足的约束合并,例子如下:
基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译_第15张图片

解释:a<:Q或者a<:U, 那么a<:QᴗU肯定也是对的,但如果只是简单地合并成a<:QᴗU,就会造成信息丢失,得到的结果过于保守。同理B:>TᴖV

六. 最后结果
最后C集为空时,约束求解的过程停止,最后B集中对于各个变量的最小上界和最大下界即为所求。

TIE还存在的问题(缺点)

  1. 不能求解递归类型。在求解等号约束时,如果等号两边有相同的变量,那么这条约束会被删除,例如:a= ptr(a)。所以TIE不能用来求解递归类型。
  2. 约束求解∨过于保守。因为对于有∨的约束,需要考虑的情况是指数增长的,TIE将∨替换成∧,这样会造成结果过于保守。
  3. 无法区分Structural type、array、prt(a)这三个类型,将Structural type、array、prt(a)都看出record type,即ptr(a) = {0 ⟶ a} = a[] (array)。这也可能是它们在二进制下的表示是一样的,还原的难度太大。

你可能感兴趣的:(基于二进制程序的类型推导--TIE: Principled Reverse Engineering of Types in Binary Programs-wcventure译)