一种残膜回收机防缠绕挑膜装置的制 一种秧草收获机用电力驱动行走机构

一种针对Dalvik程序的反编译方法与流程

2022-03-23 03:15:41 来源:中国专利 TAG:

一种针对dalvik程序的反编译方法
技术领域
1.本发明涉及一种针对dalvik程序的反编译方法。


背景技术:

2.自2008年android问世以来,其市场占有率一直在稳步提升,并直接导致了symbian、windows phone等移动操作系统退出历史舞台。然而,这样一个主流移动操作系统,多年来一直没有一个较为好用的针对android程序的反编译工具。
3.早期的反编译,采用的是dex2jar和jd-gui这套组合,先将android程序转换为基于java字节码的jar格式,然后再走java的反编译。之后,随着android的流行,市面上依次出现了jeb、jadx、ghidra等直接针对android程序的反编译工具。对于dex2jar/jd-gui,由于进行了两轮处理,且dex和jar不完全对等,其反编译结果不仅较差,而且与实际语义有一定差异。此后面世的多种工具也在各方面存在问题。闭源商业软件jeb反编译成功率高,但其反编译结果粗糙,含有大量goto语句导致难以阅读。jadx在能成功反编译的前提下,其反编译代码可读性较好。然而现实是,它的语句识别存在较为严重的问题。当函数内部存在较为复杂的跳转,源码中语句不容易被推算的情况下,它动辄报错罢工,甚至直接导致程序进入死循环。出自nsa的ghidra虽然名气较大,但它并不只针对android程序,多而不精,在android程序的反编译上表现惨淡,乏善可陈。
4.因此,寻求一种表现优于市面上各种产品,且能为软件安全行业提供一个有效的分析理解android程序的反编方法很有必要。


技术实现要素:

5.有鉴于此,本发明目的是提供一种能够有效避免单轮处理逻辑压力太大而导致的出错甚至崩溃,同时也因为存在多轮处理,使得最终得到的反编译结果质量高,并有着良好的可读性针对dalvik程序的反编译方法。
6.为了解决上述技术问题,本发明的技术方案是:
7.一种针对dalvik程序的反编译方法,包括如下步骤:
8.1)构造控制流图表;
9.得到程序的函数信息后,针对函数内的控制流构造控制流图表,控制流图表是一个带方向的树,书中的每一个节点表示一段控制流,一旦程序运行到一个节点的入口,那么在没有抛出异常的情况,这整个节点的每一行指令都将会被执行,不会遗漏,函数内执行流程的改变主要有两种情况,一是遇到跳转,二是捕获异常,前者在源码中主要体现在if和switch两种语句,其跳转指令的目的地即为节点入口,如果函数内注册了异常处理,那么这些异常处理流程的入口即为节点入口,得到了所有的节点入口之后,就从这些入口开始,进行模拟执行,一旦执行到下一个节点入口,就表示当前节点结束,重复此步骤,直到函数内所有的指令都被整理划分到各自的节点中,对函数整理划分出各个节点后,就要将这些节点按执行流程的方向连接起来,把节点和节点之间的指向关系组合在一起,控制流图表就
完整了;
10.2)生成中间码;
11.构建好控制流图表后,就要将函数内的dalvik控制流翻译成中间码,dalvik控制流和源码相比差异巨大,如果对每种指令都进行独立处理的话,后续诸如变量收缩等步骤将因为没有一个统一的编码而导致难以进行,引入中间码的目的,一方面是对同类的dalvik字节码进行整合从而降低复杂度,另一方面为后期处理提供了一个统一的接口,中间码的类型主要有赋值、调用(invoke系列指令)、算术(如加减乘除)、位运算等;
12.3)语句识别;
13.源码中各种条件语句会带来跳转指令,要实现反编译,就必须依据dalvik控制流中的各种跳转,逆推出源码中出现的各种语句,根据表述的不同,dalvik控制流在事实上提供了足够的信息,使得推断源码中的语句成为可能,根据控制流图表,从函数的入口节点出发,会遍历所有的节点,并在遍历的过程中感知特定的语句表述,将数个节点统合成一个语句,每个节点不再是扁平地存在于函数中,而是根据语句的嵌套而出现了层级关系,并可以据此重现出源码中的代码缩进效果;
14.4)变量分配;
15.要想在反编译结果中正确地表示变量,就要借助静态单赋值这一形式,在静态单赋值中,源码变量和ssa变量将是一对多的映射关系,而这种关系将通过变量下标表示,对于dalvik而言,变量的载体是寄存器,因此某函数中使用了多少种寄存器,其ssa形式中就有多少种变量,每当一个寄存器被重新赋值,在ssa形式中通过给下标作增量以标记新的变量,反编译结果中所使用变量可以直接采用ssa形式中的变量;
16.5)变量收缩
17.要表达源码中的一个表达式,往往需要多个dalvik指令才能完成,依据以上步骤中的ssa信息,遍历函数的变量并尝试进行变量收缩,直到不能再进行收缩为止;
18.6)生成反编译结果
19.将中间码正式翻译成某个编程语言,实现完整的反编译,对于dalvik而言,反编译的目标语言一般是java,需要根据java的语法构造出对应的表达式,比如,"const-string v0,'hello,world!

"的中间码是"v0='hello,world!'",将其翻译成java语言的话,在v0不被收缩的情况下,结果将是“string str='hello,world'",其中str是一个依据ssa所创建出来的对应此处v0的变量,当这些语言特定的翻译完成后,就可以得到最终的反编译结果。
20.本发明技术效果主要体现在以下方面:本方法在设计之初就已经考虑到反编译成功率和反编译效果,在反编译流程中引入了多轮处理,逐步修正完善反编译结果,使得每一轮的处理清晰而不过于复杂,有效地避免了单轮处理逻辑压力太大而导致的出错甚至崩溃,同时,也因为存在多轮处理,使得最终得到的反编译结果质量得到了保证,并有着良好的可读性,其表现优于市面上各种产品处理,能为软件安全行业提供一个有效的分析理解android程序的利器。
附图说明
21.图1为本发明一种针对dalvik程序的反编译方法的流程图。
具体实施方式
22.以下结合附图1,对本发明的具体实施方式作进一步详述,以使本发明技术方案更易于理解和掌握。
23.实施例
24.一种针对dalvik程序的反编译方法,如图1所示,包括如下步骤:
25.1)构造控制流图表;
26.得到程序的函数信息后,就可以针对函数内的控制流构造控制流图表。控制流图表是一个带方向的树,书中的每一个节点表示一段控制流。一旦程序运行到一个节点的入口,那么在没有抛出异常的情况,这整个节点的每一行指令都将会被执行,不会遗漏。换句话说,除了节点中最后一条指令,节点内的所有指令都不是跳转指令。因为节点是构成语句的最大允许粒度,构建控制流图表是为后续的语句识别做准备。比如一个没有复杂嵌套的if语句,他的条件判断存在于一个节点,条件满足后执行的代码存在于另一节点。
27.函数内执行流程的改变主要有两种情况,一是遇到跳转,二是捕获异常。前者在源码中主要体现在if和switch两种语句,其跳转指令的目的地即为节点入口。如果函数内注册了异常处理,那么这些异常处理流程的入口即为节点入口。得到了所有的节点入口之后,就从这些入口开始,进行模拟执行。一旦执行到下一个节点入口,就表示当前节点结束。重复此步骤,直到函数内所有的指令都被整理划分到各自的节点中。
28.对函数整理划分出各个节点后,就要将这些节点按执行流程的方向连接起来。比如,节点a跳转到节点b,那么在控制流图表中,节点a将会指向节点b。同理,如果节点a注册了异常处理,而异常处理的代码在节点b中,那么节点a也会指向节点b。把节点和节点之间的指向关系组合在一起,控制流图表就完整了。
29.2)生成中间码;
30.构建好控制流图表后,就要将函数内的dalvik控制流翻译成中间码。dalvik控制流和源码相比差异巨大。对dalvik控制流直接进行反编译不仅难度大,而且不利于对反编译结果的修正。比如,dalvik中的move指令就有十多种,然而这十多种指令,最终表现出来的就都是对寄存器赋值。如果对每种指令都进行独立处理的话,后续诸如变量收缩等步骤将因为没有一个统一的编码而导致难以进行。引入中间码(intermediate representation,ir)的目的,一方面是对同类的dalvik字节码进行整合从而降低复杂度,另一方面为后期处理提供了一个统一的接口。中间码的类型主要有赋值、调用(invoke系列指令)、算术(如加减乘除)、位运算等。
31.3)语句识别;
32.源码中各种条件语句会带来跳转指令。如要实现反编译,就必须依据dalvik控制流中的各种跳转,逆推出源码中出现的各种语句。这是整个反编译过程中最复杂的部分。
33.每一种语句,在图表的角度上都有其各自的表现。比如,if语句在不带else的情况下,在控制流图表中可以表示为"a-》b,b-》n,a-》n“,a为if条件判断所在节点,b为条件成立时所执行的分支,n为与if语句同级的下一个语句。如果存在else分支,那么语句的图表表述将变成”a-》b,b-》n,a-》c,c-》n"。此时,c为条件未成立时所执行的分支,其余节点意义不变。从这个例子可以看出,虽然在编译过程中发生了信息丢失,扁平化的dalvik控制流不带有源码中的语句信息,但不论是源码还是dalvik控制流,各种语句在图表角度上的各自表
述总是存在。根据表述的不同,dalvik控制流在事实上提供了足够的信息,使得推断源码中的语句成为可能。
34.根据控制流图表,从函数的入口节点出发,本方案会遍历所有的节点,并在遍历的过程中感知特定的语句表述,将数个节点统合成一个语句。经过这一流程,此时已经可以生成基于中间码的反编译结果,反编译的雏形就出现了。每个节点不再是扁平地存在于函数中,而是根据语句的嵌套而出现了层级关系,并可以据此重现出源码中的代码缩进效果。
35.4)变量分配;
36.寄存器的数量是有限的,而源码中所定义的变量可以是无穷多个。另外,即使是同一个寄存器,在函数执行过程中,不同的阶段可以带有不同的数据类型。由此可见,反编译的时候不可以将寄存器和变量简单粗暴地画上等号。变量在其生命周期中寄宿于寄存器中,当它的生命周期结束之时,将会有其他变量进入这个寄存器。要想在反编译结果中正确地表示变量,就要借助静态单赋值(staticsingle assignment,ssa)这一形式。
37.一般而言,变量之所以被称为变量,是因为它的内容是可以改变的。换句话说,变量是可以被重新赋值的。然而,在静态单赋值中,“单赋值”意味着每个变量只会被赋值一次。这样一来,源码变量和ssa变量将是一对多的映射关系,而这种关系将通过变量下标表示。在ssa中,每个变量都有各自的下标,而这些下标就是它们的版本号。对于dalvik而言,变量的载体是寄存器,因此某函数中使用了多少种寄存器,其ssa形式中就有多少种变量。每当一个寄存器被重新赋值,在ssa形式中通过给下标作增量以标记新的变量。比如,根据dalvik调用约定,p1就是第一个参数。如果一个函数有至少一个参数,那么这个参数在ssa形式中的初始变量就是p1[1],即第一个版本的p1。假如此函数对这个参数进行了重新赋值,并且值为v,那么从中间码的角度看,这个赋值语句将会是”p1[2]=v"。从这一行开始,后续对p1的引用将会是p1[2]而不再是p1[1]。
[0038]
要构建一个dalvik函数的ssa形式,需要区分写和读两种情况。对于写操作,也就是赋值,只需要在中间码这一层面上遍历函数中的每个节点,在每一次赋值的时候创建新的寄存器下标,并将其分配给此处新创建的寄存器变量。对于函数内的本地寄存器,由于最初它们都处于未初始化的状态,只有当函数内的指令开始对他们进行赋值,他们才会得到第一个版本号。另一方面,对于传入到函数的参数寄存器,由于它们已经在函数外被初始化,因此在遍历函数节点之前要先给它们分配一个初始版本号。
[0039]
相比之下,针对读操作的ssa构建就困难很多。如果对一个寄存器进行读操作,而对于这个寄存器的定义能够在该节点中从当前行往前回溯得到,那么这个读操作所对应的寄存器版本号显而易见。当一个函数存在分支,并且同一种变量在多个分支中被赋值,那么当分支合流时,合流处的变量版本号就无法确定。比如,有这样一段代码,”if(condition){v0=1;}else{v0=2;}returnv0;”。尝试将其转换成ssa形式,则得到"if(condition){v0[1]=1;}else{v0[2]=2;}returnv0[?]"。此时可以发现,在return语句中所使用的v0变量,其版本号无法得到确认。
[0040]
为了表示这种情况,ssa引入了一种名为phi的函数。它的形式为v[x]=φ(v[a],v[b],...),其中[x,a,b]等皆为变量v的下标。phi函数可以定义为“变量选择器”。根据程序上下文,它能够从其他节点中定义的各个变量(即它的各个参数)中选择并返回合适版本的变量。由于phi函数的返回值也要存放到变量中,因此这里也存在赋值,因此使用了新版本
变量v[x]。有了phi函数,合流节点的变量版本号就可以得到表示。
[0041]
得到了函数的ssa形式,变量分配问题就迎刃而解。ssa变量相比源码变量,在生命周期的角度上它的粒度更小,因此ssa变量可以直接反映源码中的变量而不影响函数语义。比如,源码中某个被重复赋值的变量,在它被重复赋值的瞬间,之前的变量内容在客观事实上已经不存在了,它的生命周期结束了。既然如此,这个变量在源码级别上其实也可以拆分成多个变量而不会对程序的效果带来副作用,进而推断出ssa形式中的多变量表示也是合法的。因此,反编译结果中所使用变量可以直接采用ssa形式中的变量。
[0042]
5)变量收缩;
[0043]
一般而言,单一dalvik指令和源码中的单一表达式相比,前者的操作层次更低,实现的功能更为简单。因此,要表达源码中的一个表达式,往往需要多个dalvik指令才能完成。而这些dalvik指令,如果一对一直接翻译成源码中的表达式,即使能翻译也会显得啰嗦。以带返回值的函数调用为例,在源码中形式为“objectobj=function(parameter,...);",待了dalvik则成为了两条指令,分别是invoke和move-result。把这些指令翻译成中间码,将得到"result=function(register,...);vn=result",其中result是dalvik中隐含的存放调用返回值的寄存器,vn是此时存放obj变量的寄存器。从反编译的角度看,这个名为result的中间变量是多余的。
[0044]
依据前述步骤中的ssa信息,遍历函数的变量并尝试进行变量收缩。此处的result变量,其的定义来源于invoke指令,在move-result中被使用。由于result的使用只有一处,即不存在其他对result的引用,那么在中间码的层面上就可以将result变量的定义直接收缩到使用处。经过收缩处理,对应中间码将变成更为贴近真实源码的”vn=function(register,...)”。
[0045]
除了这个例子,还有链式调用等场合也需要进行变量收缩。这种遍历处理需要进行多轮,并维护一个状态,记录此轮是否进行过收缩。如果此轮有进行收缩操作,那么将继续执行同样的遍历处理,直到不能再进行收缩为止。
[0046]
6)生成反编译结果;
[0047]
上面讲述的步骤,除去控制流图表的构建,其他步骤都是基于中间码进行操作。最后的一步,便是将这些中间码正式翻译成某个编程语言,实现完整的反编译。对于dalvik而言,反编译的目标语言一般是java,需要根据java的语法构造出对应的表达式。比如,"const-string v0,'hello,world!

"的中间码是"v0='hello,world!'"。将其翻译成java语言的话,在v0不被收缩的情况下,结果将是“string str='hello,world'",其中str是一个依据ssa所创建出来的对应此处v0的变量。当这些语言特定的翻译完成后,就可以得到最终的反编译结果。
[0048]
对于本领域技术人员而言,显然本发明不限于上述示范性实施例的细节,而且在不背离本发明的精神或基本特征的情况下,能够以其他的具体形式实现本发明。因此,无论从哪一点来看,均应将实施例看作是示范性的,而且是非限制性的,本发明的范围由所附权利要求而不是上述说明限定,因此旨在将落在权利要求的等同要件的含义和范围内的所有变化囊括在本发明内。
[0049]
此外,应当理解,虽然本说明书按照实施方式加以描述,但并非每个实施方式仅包含一个独立的技术方案,说明书的这种叙述方式仅仅是为清楚起见,本领域技术人员应当
将说明书作为一个整体,各实施例中的技术方案也可以经适当组合,形成本领域技术人员可以理解的其他实施方式。
再多了解一些

本文用于企业家、创业者技术爱好者查询,结果仅供参考。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表

相关文献