Android逆向分析工具性能对比分析
2022-10-17 14:18:6 Author: www.4hou.com(查看原文) 阅读量:29 收藏

导语:本次测试对比是为了呈现incinerator与PNFSofteware出品的JEB以及国内出品GDA在Android逆向工程能力的对比,从而让大家更好更直观的了解相关的详细信息

前言

本次测试对比是为了呈现incinerator与PNFSofteware出品的JEB以及国内出品GDA在Android逆向工程能力的对比,从而让大家更好更直观的了解相关的详细信息

本次测试对比的产品信息如下:

Incinerator: 1.0.0

JEB:3.19.1.202005071620

GDA:3.9.0

注:文章编写日期与发布时间有一定时间间隔,所以以下内容并不代表各产品的后续性能指标。

反编译器对循环结构的还原能力测试

Test1

第一个测试中,设计了循环头和锁节点都为二路条件循环结构,为了测试循环结构化分析能力,多嵌套了几个if语句(代码标号为基本块号)。程序简单如下:

publicvoidtest1(int y,int a){
        while(y >0){
            if(a 10){
                    if(a >100){
                        a = a *5;
                        break;
                    }else{
                        y = y / a;
                    }
                }
            }
        }
    }

流程图.png

编写testdemo将代码段生成apk后,并分别使用JEB、GDA、Incinerator来进行反编译操作,从而进行代码可读性和语义准确性上的对比,如下图所示:

test1

通过上述对比可以看出

在语义准确性上,JEB发生了语义错误,在a > 100时,丢失了a *= 5的代码块,Incinerator与GDA保持了语义的准确性。

在代码可读性上,三者相差不大可读性都很好。Incinerator在if-else上做了相应的优化,可读性略有提升。

在代码还原度上,Incinerator做了对应优化,GDA重复声明了a、y变量,其他方面最为接近源码。而JEB存在代码块丢失。

反编译器语义准确性代码可读性代码还原度
Incinerator
JEB×
GDA

Test2

接下来看看他们对双层循环的结构化分析的能力,设计一个双层循环,在内层循环break出外层循环,实际上基本块5即if(a > 10),不仅会是内存循环的锁节点,也会是外层循环的锁节点。并且该锁节点为二路条件节点,其一个分支路径回到内层循环,另外一个分支结构回到外层循环。一般对循环结构算法都是循环头-锁节点一一对应,因此处理过程中可能会复杂化该类结构。代码实现非常简单如下:

publicvoidtest2(int y,int a){
        while(y >0){
            while(a >0){
                if(a 10){
                        break;
                    }
                }
            }
        }
        attachBaseContext(this);
    }

重新编译apk后,再进行反编译后,如下图所示:

test2

通过上述对比可以看出

在语义准确性上,Incinerator、JEB保持了语义的准确性,都识别除了双重循环,GDA仅有函数声明,丢失了整个函数的代码块。

在代码可读性上,Incinerator优化了if-else组合,JEB在if中加入continue省略else语句,两者可读性都很好。

在代码还原度上,Incinerator、JEB除了各自在if-else上的优化,还原度都很高。

反编译器语义准确性代码可读性代码还原度
Incinerator
JEB
GDA×

Test3

这一段代码在退出循环的”if(a>10)”语句中内嵌了另外一个if语句,这会导致内层循环的锁节点发生变化,并且给内层循环添加了一个跟随节点,另外代码做了稍稍的改动。如下:

publicvoidtest3(int y,int a){
        while(y >0){
            while(a >50){
                a = a +1;
                y = y +1;
                if(a >10){
                    if(a >100){
                        a = a *5;
                        break;
                    }else{
                        y = y / a;
                    }
                }
            }
            this.attachBaseContext(this);
        }
    }

继续编译成apk,再进行反编译操作,如下图所示:

test3

通过对比可以看出

在语义准确性上,Incinerator、JEB仍然保持了语义的准确性,GDA重复声明了a、y变量,并且继续丢失函数内部的代码块。

在代码可读性上,Incinerator、JEB保持很好的代码可读性,JEB使用了continue来分割嵌套的if。

在代码还原度上,Incinerator最为接近源码,JEB改为使用continue来分割嵌套的if。

反编译器语义准确性代码可读性代码还原度
Incinerator
JEB
GDA×

Test4

在内层循环的第一个if-else结构上添加一个后随节点,并且最后break出内层循环到外层循环。并且将a=a*5语句后的break改成continue。代码如下:

publicvoidtest4(int y,int a){
        while(y >0){
            y++;
            while(a >50){
                a = a +1;
                y = y +1;
                if(a >10){
                    if(a >100){
                        a = a *5;
                        continue;
                    }else{
                        y = y / a;
                    }
                }
                y = a * y;
                break;
            }
            this.attachBaseContext(this);
        }
    }

同样编译成apk后再反编译,如下图所示:

test4

通过上述对比可以看出

在语义准确性上,Incinerator在a *= 5; 后面丢失了continue,在y *= a; 后面丢失了退出循环的break;JEB保持了语义的正确性;GDA重复声明变量,也丢失了函数内的代码块。

在代码可读性上,Incinerator、JEB可读性都很好。

在代码还原度上,Incinerator与源码最为相似,但是丢失了continue、break;JEB使用continue分开了if-else,将else后面的y /= a,与y *= a合并为新的 y = y / a * a,并加入break,还原度上有了一定的改变。

反编译器语义准确性代码可读性代码还原度
Incinerator×
JEB
GDA×

Test5

这次在“if(a>10)”内部加入switch,在a为11、12时,执行“a = a * 5”,并continue返回内循环while,a为13时,执行“a = a * 6”,继续往下执行,并不退出,a为14时,执行“a = a * 7” 退出switch, 与default中加入if-else,代码如下:

publicvoidtest5(int y,int a){
        while(y >0){
            y++;
            while(a >50){
                a = a +1;
                y = y +1;
                if(a >10){
                    switch(a){
                        case11:
                        case12:
                            a = a *5;
                            continue;
                        case13:
                            a = a *6;
                        case14:
                            a = a *7;
                            break;
                        default:
                            if(a >100){
                                a = a *5;
                                continue;
                            }else{
                                y = y / a;
                            }
                    }
                }
                y = a * y;
                break;
            }
            this.attachBaseContext(this);
        }
    }

最后编译成apk后反编译,如下图所示:

test5

通过上述对比可以看出

在语义准确性上,Incinerator在switch将a为11、12、default中的continue错误表达为break,丢失了y *= a后面退出内循环的break;JEB保持了语义的正确性在,但在label_18的break之后,多了两句无用的代码a *=8;continue;GDA没有识别出内循环,使用if与goto做处理,switch中a为11、12时多了break,没有识别出a=14,且在default中,执行完y=y/a后继续执行"a = a * 7"。

在代码可读性上,Incinerator识别出双循环、switch-case可读性上最好,JEB、GDA多次出现goto,在代码可读性上存在一定的影响。

在代码还原度上,Incinerator与源码最为相似,但在节点的退出上存在一定的问题,JEB、GDA在代码的还原度上膨胀比较大。

反编译器语义准确性代码可读性代码还原度
Incinerator×
JEB
GDA×

调试能力测试

逆向工程工具针对Apk可调试,对于研究人员来说有着极大的帮助,而对于已经发布后的应用再进行调试的话,可调试的前提条件会比较苛刻,如:设备是否root、调试属性是否开启、能否重打包等,这些因素都会影响着是否能够调试,而影响调试功能的好坏、支持与否,取决于:能否stepover、stepinto、breakpoint,能否获取/修改变量值等,这些因素都体现着调试器是否好用。所以我们从上述多个维度,对Incinerator、JEB的调试做下简单对比,但因GDA不支持调试,所以下面的内容无法针对GDA进行测试对比。

这里直接使用Android Studio自带的example:Login Activity进行对比,如图1-2所示:

图1

图2

在登录验证的位置做细微修改,让它基本不可能登录成功,然后再分别使用JEB、Incinerator进行分析登录过程,并绕过登录限制。修改的代码与登录失败,如图3-4所示:

图3

图4

调试设备:Nexus 5X

系统版本:7.1.2

root状态:已root

主要测试功能:

  1. 下断点

  2. 步进

  3. 步过

  4. 跑至光标

  5. 显示与修改变量值

  6. 免debugger属性调试

  7. smali调试

  8. 伪代码调试

JEB调试

首先手动安装编译好的apk,然后使用JEB反编译对应apk,点击JEB上的start. 如图5所示:

图5

出来Attach界面,因为应用还没启动,所以并没有看到Processes中有进程列表,如图6所示:

图6

通过命令(adb shell am start -D -n com.testdemo3/.ui.login.LoginActivity)启动进程,JEB点击(Refresh Machines List)刷新列表,看到已经跑起的测试案例,如图7所示:

图7

点击Attach后,发现无法debug,按提示指app没有开启debuggable属性或者设备没有root,建议使用模拟器、root设备或者重打包app。(实际上设备已root),如图8所示:

图8

我们在AndroidManifest中加入android:debuggable="true"并重新编译(如果是第三方的app只能尝试用apktool等工具进行重打包,有签名、完整性等校验的话再想办法将其绕过)

现在可以成功Attach上去。

我们使用JEB反编译登录界面的activity:LoginActivity,在按钮的点击触发代码中加入断点,如图9所示:

图9

因不支持在伪代码中加入断点,我们切换回smali(快捷键Q),在onClick的第一行按Ctrl+B加入断点,如图10所示:

图10

操作app,输入帐号密码,点击:SIGN IN OR REGISTER,在JEB中成功触发断点,如图11所示:

JEB此处有个优势,它的布局可以根据个人喜好随意拖拉

图11

当前显示的变量似乎有些异常,我们此时忽略他,鼠标放到 00000040 处,点击JEB的"Run to line",成功跳到指定行,如图12所示:

图12

JEB一开始将所有变量当作int类型处理,我们分析代码,可以知道此处的V1、V2是输入的帐号与密码,类型是String,因此,我们点击V1、V2中的Type将int改为String,如图13所示:

图13

v1、v2成功修正类型,对应的值也成功显示出我们测试输入的帐号密码。

在JEB中点击步进(Step Into)到LoginViewModel的login方法,如图14所示:

图14

成功步进LoginViewModel的login方法,但是在这里可以看到,刚才修改的类型(此处对应的是p1、p2)又重新变回了int。

根据smali可知道,接下来会调用LoginRepository的login方法,随后返回Result

我们再继续点击两次步进(Step Into)进入LoginRepository的login方法,如图15所示:

图15

在此方法中,它会继续将帐号密码传给LoginDataSource的login方法,返回Result

继续步进(Step Into)两次,进入LoginDataSource的login方法,如图16所示:

图16

在这方法中,可以看到密码传进来后,跟通过StringBuilder类组合起来的当前时间戳字符串对比,基本不可能相等,随后抛出"Invalid password"的异常。

为了成功登录,我们得令他们的对比结果一致

我们按步过(Step Over)一直跳到00000032处,将v0的值改为true,如图17-18所示:

图17

图18

继续步过(Step Over)发现成功绕过验证,如图19所示:

图19

之后直接点击Run,让他恢复继续执行,如图20所示:

图20

由上面的测试步骤可以看出,JEB的调试需要依赖debuggable属性的开启,支持Smali调试可以在上面下断点,不支持伪代码调试,支持Step Into、Step Over、Run to line等常用功能,可以查看与修改变量值,但类型有时候需要手动纠正。

Incinerator调试

使用Incinerator反编译apk,反编译完毕后,我们打开“Set Debug Device”确认设备已经连接上之后(如果有多个设备可以点击切换),点击Start debug,如图21所示:

图21

无需过多的操作,apk已经自动安装、启动,并进入调试状态。

使用Incinerator反编译登录界面的activity:LoginActivity,然后,鼠标在LoginActivity的第207行点一下(button点击事件中),按Tab切换到smali模式,随后在onClick的第一行加入断点,如图22-23所示:

图22

图23

操作app,输入帐号密码,点击:SIGN IN OR REGISTER,在Incinerator中成功触发断点,如图24所示:

图24

在Smali中可以看到,接下来会将登录信息传入到LoginViewModel.login(52行)方法进行验证。

在login所在行(52行)鼠标点一下,然后点击F10(Run to cursor),成功跳到光标所在行,如图25所示:

图25

可以看出Incinerator支持smali调试。

Smali代码相对来说不方便阅读,我们按Tab切换到伪代码界面,随后,按步进(Step Into)进入LoginViewModel的login方法

成功步进LoginViewModel的login方法后,在Variables的列表中,可以看到我们输入的帐号,密码,如图26所示:

图26

根据伪代码可知道,接下来会调用LoginRepository的login方法,随后返回Result 点击一次步进(Step Into)进入LoginRepository的login方法,如图27所示:

图27

在此方法中,它会继续将帐号密码传给LoginDataSource的login方法,返回Result 再步进(Step Into)一次,进入LoginDataSource的login方法,如图28所示:

图28

在这方法中,可以看到密码传进来后,跟当前时间戳所组成的字符串对比,基本不可能相等,随后抛出"Invalid password"的异常。

为了成功登录,我们得令密码跟时间戳一致。

我们F8(Step Over) 3次,去到if所在行,如图29所示:

图29

现在可以看到时间戳的具体数值,我们将它复制出来,如图30所示:

图30

随后将password设为跟时间戳一致的字符串,如图31-32所示:

图31

图32

设置完毕之后,点击一次F8(Step Over),可以看到已经成功绕过验证,如图33所示:

图33

直接点击F9,让他恢复继续执行,如图34所示:

图34

由上面的测试步骤可以看出,Incinerator调试更为便捷,基本可以一键调试,同样支持Step Into、Step Over、Run to line、下断点、查看与修改变量值等常用功能,并且同时支持smali、伪代码调试。

反编译器下断点步进步过跑到光标显/改变量值免debugger属性调试smali调试伪代码调试
Incinerator
JEB××
GDA××××××××

源地址:https://liansecurity.com/#/main/news/w6s2z4MBqlWBhJyCO8jt/detail

如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/6VNQ
如有侵权请联系:admin#unsafe.sh