Android应用学习(三)——Frida入门系列篇II

APP安全 2019-03-04

本篇其实是Frida入门的第二篇,在写本篇的时候其实Frida入门系列篇I都没写,因为今天刚刚用Frida的hook大法在听风者boy的大力帮助下,搞定了owasp里面的一个练手apk,顺便就记录一下整个过程。在这个xposed已老,apk加壳猖狂的年代,只有Frida能给我们一丝温暖,入门篇I打算写一下从零开始安装配置Frida,也会在近期补上,那我们就开始吧!

作者:古月蓝旻

环境介绍:

老规矩介绍一下环境

操作系统:CentOS 6.5
Frida版本:12.2.6
测试机:Google Nexus 5X(已root)
测试机版本:Android 4.4.4
Python版本:Python 3.6.7
Java版本:    1.8.0_60

基础环境已经配置好,手机连接到CentOS上,开启USB调试,手机上也运行了Frida的server端;CentOS也使用adb进行了端口转发,此时我们使用

frida-ps -U

可以列出所连接的USB设备(nexus 5X)上面运行的进程,如下图所示

到这里基本准备工作就完成了,是时候动手了

安装、反编译apk

其实本篇教程,我主要是参考HACKING ANDROID APPS WITH FRIDA II - CRACKME进行的复现

这篇文章是Michael Helwig在2017年3.15的时候创建的,距离现在已经一年多了,带来一个什么问题呢,其实听常见的,链接失效了,比如教程当中提及的项目地址和apk地址早已...

WTF?只能自己动手了,后来看了一下该项目是OWASP官方推出的,其实项目还在,只是地址换了一下,现在是

https://github.com/OWASP/owasp-mstg/tree/master/Crackmes

apk的地址是这个

https://github.com/OWASP/owasp-mstg/blob/master/Crackmes/Android/Level_01/UnCrackable-Level1.apk

但是里面有坑,后面会介绍的

我们根据教程来,首先需要下载上面的apk,然后使用下面的命令去安装一下

adb install UnCrackable-Level1.apk

名字和教程里不太一样,不过没什么影响,安装完打开以后界面是这样的

弹出一个对话框,标题是“Root detected”内容如图,意思说这是不可以访问的,app要退出啦,当我们点击“OK”以后,果然APP就直接退出了,从上图还可以看到最上方有一个“Enter the Secret String”和“Verify”按钮,这个可能是验证我们的输入的,看一下教程,确认一下我们本次的目标,到底达到什么样的效果算破解成功呢

其实分两步:

a. 跳出当前按下“OK”按钮,程序就退出的限制;
b. 输入一段特定的字符串secret通过程序的校验;

成功的效果是这样的

好的,既然知道了目标,咱们就开始行动吧,首先我们的第一步是将该apk反编译出java源码,为什么要这样呢,因为本次学习的Frida是一款基于Python+JavaScript的hook框架,主要使用动态二进制插桩技术[Dynamic Binary Instrumentation(DBI)]:在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久改变。

而我们反编译源码的目的其实就是了解apk目前的代码和逻辑,通过插入我们构造的代码实现破解的效果。

有反编译经验的小伙伴都知道,对于未加壳的apk使用dex2jar反编译是最常见的

以前的反编译无壳apk的套路一般是以下3步

1. 修改apk的后缀名为rar或zip,解压得到classes.dex;
2. 使用dex2jar将第一步的classes.dex反编译成jar文件;
3. 使用jd-gui打开jar包查看java源码,或者使用apktool修改smali源码修改apk;

现在我们既然使用了Frida,那么第三步相应就要调整一下了,不再是通过修改源码文件破解apk了

首先我们从dex2jar官网下载一下,然后解压进入该目录中,使用以下命令

./dex2jar.sh ~/UnCrackable-Level1.apk ~/UnCrackable-Level1.jar

得到了apk对应的jar包,这个时候可以用jd-gui或者文中提到的BytecodeViewer打开,我这里还是用IDEA打开,主要是自己总是忘记IDEA怎么打开jar包看源码,这里给自己留个备忘

Step1: 首先新建一个java的空白项目,然后右键该该项目中新建文件夹

比如我这里新建了一个叫lib的文件夹

Step2: 将上面使用dex2jar反编译得到的jar包拖动到lib文件夹上

Step3: 右键该jar包,选择add as library

稍等片刻该jar包即可解析完毕,jar包下方出现相关文件结构,点击可看源码

不过这种方法得到的jar包无法全局搜索,比较尴尬,下面再介绍一款简易工具jadx,教程如右Jadx简明教程

可以直接将未加壳的apk或者dex直接拖入打开

然后选择全部保存将源码文件保存到指定目录下

此时apk源码会变成一个个源码文件夹,将该文件夹拖入IDEA的项目文件夹中即可查看修改源码,并且支持全局搜索Ctrl+Shift+F

好了,到这里我们的第一步就结束了,下面就是根据源码分析apk的逻辑进而编写相关js使用Frida插入到程序中了

源码分析——跳出退出逻辑

我们先在手机上试验了一下,点击OK按钮以后就直接退出程序了,并没有给我们任何输入的机会,所以我们首先要去hook的就是这段点击OK就退出的函数,到了分析源码的环节要求我们具备一定的java基础和android基础,不过不会的话问题也不是很大,这只是一个简单的小程序,逻辑上不是很复杂。

不需要通读代码逻辑,我们先看一下我们一眼能看见的部分

出来一个对话框,分成3部分:

标题是“Root detected!”
内容是“This is unacceptable. The app is now going to exit.”
还有一个“OK”按钮

看一下源码,其实很显而易见,正是sg.vantagepoint.uncrackable1包的MainActivity类中的前几行代码

简单介绍一下这3个部分:

Part1: 主类中的私有方法a定义了一个AlertDialog对象,将message设置为“This is unacceptable. The app is now going to exit.”

Part2: 设置了一个按钮OK,添加onClick方法,点击就调用System.exit(0);退出程序

Part3: 下面还定义了一个onCreate方法负责根据系统不同状态设置标题栏的内容

其实第三个部分在本次破解中完全可以不用看,因为这涉及到另一个问题:应用程序如何判断当前系统是否被root了。而本程序对于root的系统没有任何反制措施,这个在文章后面会介绍一下,感兴趣的可以自行查看。

我们不难看出,其实操纵程序退出的正是我们的Part2部分,所以我们hook的重点自然是这里,不过坑也来了

按照我一开始参考的教程,里面的js是这样的

让我们使用sg.vantagepoint.uncrackable1.b类,然后hook其中的onClick方法,将函数体改成输出"[*] onClick called"

如果你真的这么做了会发现肯定会hook失败,因为sg.vantagepoint.uncrackable1包里根本就没有叫b的类

而且教程里面apk反编译出的代码虽然看着和我们反编译出来的类似,但是很多细节之处其实根本不一样,比如我们看一下我方退出和敌方退出的代码

我方是直接创建单击事件的动作,直接退出

教程是新建了类b的示例,通过该示例完成退出,教程中的b类,长这样

其中也创建了onClick方法,同样通过调用System.exit(0);的方式完成退出程序,看着这些代码,完全理解了教程中为啥js会写成这样

Java.perform(function() {

  bClass = Java.use("sg.vantagepoint.uncrackable1.b");
  bClass.onClick.implementation = function(v) {
     console.log("[*] onClick called");
  }

既然按钮是由b类的onClick方法控制的,自然我们hook的思路就是修改b类onClick方法的实现逻辑

但是,我们的apk为啥源码不长教程这样,后来我去github上看了一下,非常不幸,我们的apk在2018年9月被作者重新修改了,我们参考的教程是2017年3月写的,对不上也正常,至于为啥改了,原因大致其实也能猜到:这种退出的逻辑简直太扯,几乎不会有apk是这么退出的,而且难度实在太低了,不具备教学的意义

好吧,到这里,我们的只能自己探索了,看一下退出的逻辑

var2.setButton(-3, "OK", new OnClickListener() {
        public void onClick(DialogInterface var1, int var2) {
            System.exit(0);
        }

看着和教程的区别不是很大,所以一开始我的思路也偏了,也想去改改OnClickListener对应的onClick方法,不过查了一下相关API马上就放弃了这种思路DialogInterface.OnClickListener API(名字叫OnClickListener的接口很多,注意结合源码中的import确认该接口对应的相关package,不要找错API)

onClickListener是接口而非类,我在Frida的官方API里没有找到使用接口的相关APIFrida API

照猫画虎去hook教程中的onClick函数果然是不行的

好了,换一下思路,既然是不想让程序退出,我们直接看看操作退出的
System.exit(0);

一路Ctrl+单击看看exit函数的源头

首先它来自java.lang.System包

然后发现来自Runtime类,用于确保程序的安全退出

就不接着上溯了,看API文档亦可,总之exit(int status)是java.lang.System类中用于控制应用程序退出的一个方法,好了我们现在只会hook类中的方法,现在刚好满足我们的要求,就准备动手吧

setImmediate(function() { //prevent timeout
    console.log("[*] Starting script");

    Java.perform(function() {

      Class = Java.use("java.lang.System");
      Class.exit.implementation = function(v) {
         console.log("[*] onClick called");
      }
      console.log("[*] onClick handler modified")

    })
})

思路就是使用Java.use配合implementation语句修改java.lang.System类中exit方法的实现,让它只输出一段话,而非退出,然后我们试验一下

frida -U -l hook_exit.js  -f owasp.mstg.uncrackable1 --no-pause

程序自动打开,弹窗依旧,当我们点击OK按钮后,程序不会再自动退出

此时查看输出信息,我们预设的console.log语句也是正常输出

到这里,我们目标的第一阶段就完成了,实现了让程序不退出的效果,接下来就是第二部分,得到secret信息

源码分析——得到secret

第二步就是获取secret,首先我们看一下随便在文本框中输入一个字符串的效果,比如这里我们输入“meetsec”

自然是不正确的,返回“That's not it. Try again.”

找该语句对应的代码还是挺方便的

定义了一个verify函数,将文本框(EditText)中输入的字符串赋值给var2,然后将该值作为a类a方法的参数,该方法返回true则显示“This is the correct secret”,反之返回“That's not it. Try again.”

所以我们看一下a类的a方法

这个方法逻辑略微复杂一些,其实就是比较传入的字符串和一段使用AES算法的密文解密后的结果是否相同,其中密文解密的结果在a.a.a()方法的返回值中,这里不深入展开,有兴趣的可以自行手工解密一下这段密码。

分析到这里,其实想要返回“This is the correct secret”就有两种思路:

1. hook a.a()函数的结果,返回一个true即可这样无论输入什么值都会提示正确
2. hook a.a.a()函数的结果,使用程序中a.a.a()函数调用的参数,得到解密后的字符串并输出

第一种思路其实挺简单的

setImmediate(function() { //prevent timeout
            console.log("[*] Starting script");

                Java.perform(function() {

                  Class = Java.use("java.lang.System");
                  Class.exit.implementation = function(v) {
                     console.log("[*] onClick called");
                  }
                  console.log("[*] onClick handler modified")

                //all right
                aClass = Java.use("sg.vantagepoint.uncrackable1.a");
                aClass.a.implementation = function(arg1) {
                return true;
                }
            console.log("[*] sg.vantagepoint.a.a modified");
    });
});

效果其实还挺好,随便输入一个字符串,返回都是正确

Frida的自定义输出也告诉我们a.a方法被修改了

好了,上面这种思路其实是过验证的常用思路,下面介绍第二种

第二种方式相对而言对于初学者同学来说有必要了解一下,直接调用对应函数参数得到返回值的方法其实思路很棒,我们看一下核心的代码

        //secret value print
        aaClass = Java.use("sg.vantagepoint.a.a");
        aaClass.a.implementation = function(arg1, arg2) {
        retval = this.a(arg1, arg2);
        password = ''
        for(i = 0; i < retval.length; i++) {
           password += String.fromCharCode(retval[i]);
        }

        console.log("[*] Decrypted:" + password);
        return retval;
    }
    console.log("[*] sg.vantagepoint.a.a.a modified");
    });

也是hook了a.a.a方法,使用了this.a(arg1,arg2)的方式直接指向sg.vantagepoint.a.a.a(b("8d127684cbc37c17616d806cf50473cc"), var1);,此时参数arg1和arg2则分别对应b("8d127684cbc37c17616d806cf50473cc"),var

这就是hook大法动态插桩调试的魅力所在我们不需要去得到方法参数的值,由于程序在运行时,相关参数的值已经生成,因此我们直接调用即可

完整代码如下:

setImmediate(function() { //prevent timeout
        console.log("[*] Starting script");

        Java.perform(function() {

          Class = Java.use("java.lang.System");
          Class.exit.implementation = function(v) {
             console.log("[*] onClick called");
          }
          console.log("[*] onClick handler modified")
        //secret value print
        aaClass = Java.use("sg.vantagepoint.a.a");
        aaClass.a.implementation = function(arg1, arg2) {
        retval = this.a(arg1, arg2);
        password = ''
        for(i = 0; i < retval.length; i++) {
           password += String.fromCharCode(retval[i]);
        }

        console.log("[*] Decrypted:" + password);
        return retval;
    }
    console.log("[*] sg.vantagepoint.a.a.a modified");
    });
});

我们测试一下,还是随便输入一个字符串,还是错误

此时我们看一下frida的输出

相关secret已经出来了,值为I want to believe,我们输入一下这个字符串看一下,注意空格

果然成功了,到这里我们就完成了对level1级别apk的frida学习

apk检测root思路

嗯,这里补充一下我们从源码中发现的root检测的思路,虽然本例并没有用到,但是很多其它的apk确实是有root检测的,比如它们本身就是root后才能安装的软件,或者限制root手机运行等。

这里的root一共是3个判断条件,有一个命中即认为系统已经root

我们在IDEA使用Ctrl+单击的方式溯源一下

方法1:环境变量大法

调用相关API的getenv方法看看环境变量中是否有包含su的文件

方法2:build tag大法

根据相关资料描述,build tag中如果包含test-keys表明内核编译的签名是通过第三方开发者使用自定义key完成的,这也被认为是root的一大特征。

方法3:root应用特征大法

/system/app/Superuser.apk", 
"/system/xbin/daemonsu",
 "/system/etc/init.d/99SuperSUDaemon", 
"/system/bin/.ext/.su", 
"/system/etc/.has_su_daemon", 
"/system/etc/.installed_su_daemon", 
"/dev/com.koushikdutta.superuser.daemon/"

通过检测常用root应用生成的文件是否在系统中存在判断当前系统是否存在

检测的思路其实挺好,但是现在我们已经会Frida的情况下,这些都可以轻松绕过

好了本期教程就到这里,也写了1w+字了,咱们下期再会~


本文由 古月蓝旻 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论