首页
论坛
课程
招聘
[原创]大数据安全入门安卓Frida习题讲解
2021-9-3 14:25 9780

[原创]大数据安全入门安卓Frida习题讲解

2021-9-3 14:25
9780

目录

学习目标

今天带大家来做一道简单的Frida Hook Java层的题目,总共有七个小关卡,每个关卡都有一个小的考察点,来考察我们Frida的基础知识,如果已经会的同学也可以作为一个资料来翻阅

 

APP:Frida测试题
下载地址:看文章结束

第一关

先把APP安装跑起来,每一关都有一个要求和考察点

 

考察点:方法参数的修改

分析:大概的看一下逻辑,也就是点击下一关,会调用onClick方法,onClick方法中会调用check方法,但是参数的是一个false,在check方法中,根据这个参数来选择是进入到下一关还是提示失败,如果我们不进行任何的修改,y永远也无法进入到下一关,所以呢我们要使用Frida,修改check方法的参数,使其变为true,来帮助我们进入下一关。

1
2
3
4
5
6
7
8
9
10
11
function main(){
    Java.perform(function(){
        // Frist
        Java.use("com.dta.test.frida.activity.FirstActivity").check.implementation = function(z){
            z = true
            this.check(z)
        }
 
    })
}
setImmediate(main)

第二关

考察点:方法返回值的修改

分析:调用的流程都是一样的,还是调用onClick方法,然后再来分析一下逻辑。也是非常简单,调用check方法,根据check方法的返回值来选择是进入下一关还是提示失败。所以呢我们直接修改check方法的返回值为true就可以了

1
2
3
4
5
6
7
8
9
10
function main(){
    Java.perform(function(){
        //Second
        Java.use("com.dta.test.frida.activity.SecondActivity").check.implementation = function(){
            return true
        }
 
    })
}
setImmediate(main)

第三关

考察点:类成员变量的修改、枚举类的取值

分析:这道题目呢是判断了成员变量unkown的值是否等于Level.Fouth,所以呢我们需要修改unkown的值为Level.Fouth,默认值为Level.Unkown对吧,也是比较简单的。
考察的第一个点呢就是这个成员变量的值如何来修改,分为两种情况:
1.静态成员变量,修改静态成员变量的值直接使用Java.use拿到一个类的wraper,直接.变量名.value就可以直接修改它的值。
2.实例成员变量,需要我们先通过Java.choose获取到这个实例,再通过.的方式来修改。
考察的第二个点就是枚举类的取值,因为我们的unkown这个成员变量想给它赋值为Level.Fouth,所以我们要拿到这个Level.Fouth,而这个Level为一个枚举类,从名字也可以看出,枚举类无非就是一个特殊的类,其取值呢也是直接可以用.name.value的方法来取,来看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function main(){
    Java.perform(function(){
        //Third
        Java.choose("com.dta.test.frida.activity.ThirdActivity",{
            onMatch: function(ins){
                console.log(ins)
                ins.unknown.value = Java.use("com.dta.test.frida.base.Level").Fourth.value
            },onComplete: function(){
                console.log("Search Completed!")
            }
        })
 
    })
}
setImmediate(main)

这个题目有个同学提出一个问题,他并不是修改类成员变量的值来进入下一关的,这个我们第一个题目就说过了,不要使用任何非考察点外的方法来达到下一关,不然一个题目的解法会有很多,还达不到我们考察的目的

第四关

考察点:方法的主动调用

分析:这道题的目的是为了让我们学会Frida如何去主动调用一个方法,同样主动调用也是分为了两种情况
1.静态方法的主动调用,直接通过Java.use拿到类的wraper,可以直接调用。
2.实例方法的主动调用,需要先获得一个该类的实例,在Frida中拿到一个类的实例有很多种方法,比如$new()new一个对象,Java.choose来拿到一个内存中现有的该类的实例,方法Hook的时候,this对象也是该类的一个实例,有了这个实例就可以进行主动调用实例方法了。来看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function main(){
    Java.perform(function(){
        //Fourth
        Java.choose("com.dta.test.frida.activity.FourthActivity",{
            onMatch: function(ins){
                console.log(ins)
                ins.next()
            },onComplete: function(){
                console.log("Search Completed!")
            }
        })
 
    })
}
setImmediate(main)

第五关

考察点: Frida数组的构造

分析:在我们想主动调用一个方法的时候,最重要的就是这个方法的参数该如何去构造。比如这个题目,也是一个check方法,它的参数就是一个数组,内部进行了判断,判断该数组的长度为5就可以通过,否则提示失败。所以我们需要Hook check方法,修改其参数为一个长度为5的String数组就可以了。数组的构造Frida也提供了API:Java.array,第一个参数为要构造的数据类型,基本数据类型可以直接写,如int char,而复杂数据类型需要填写全限定类名,如java.lang.String,来看代码

1
2
3
4
5
6
7
8
9
10
11
12
function main(){
    Java.perform(function(){
        //Fifth
        var strarr = Java.array("java.lang.String",["d","t","a","b","c"])
        Java.use("com.dta.test.frida.activity.FifthActivity").check.implementation = function(arr){
            arr = strarr
            this.check(arr)
        }
 
    })
}
setImmediate(main)

第六关

考察点:Frida自定义类

分析:这道题目是比较有意思的一道题目,当时出题的时候没有考虑到ClassLoader的问题,自己在做的时候才发现这个题目是有坑的。我们先按正常的思路来解决这道题目:代码就几行,先通过Class.forName来加载一个类的,拿到com.dta.test.frida.activity.RegisterClass这个Class对象,然后调用getDeclaredMethod方法来拿到这个类中的next方法,最后再来调用这个next方法,拿到返回值后调用booleanValue方法来拿到一个boolean类型的结果,通过这个结果来选择是否进入下一关或提示失败

 

其实上面的分析过程也是比较好理解,就相当于我们需要一个类,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.dta.test.frida.activity;
 
public class RegisterClass{
 
    public RegisterClass{
 
    }
 
    public boolean next(){
        return true;
    }
 
}

题目也就是调用了这个RegisterClass类中的next方法,那么我们根据这个题目来通过Frida提供的Java.registerClass API来构建这样一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function main(){
    Java.perform(function(){
        //Sixth
        var RegisterClass = Java.registerClass({
            name: "com.dta.test.frida.activity.RegisterClass",
            methods: {
                next: {
                    returnType: "boolean",
                    argumentTypes:[],
                    implementation: function(){
                        return true
                    }
                }
            }
        })
    })
}
setImmediate(main)

我们通过Frida来帮我们构造出来了我们需要的类,而且也会自动加载到内存中去,但是我们发现题目还是过不了的。这个时候我们就要思考问题出在哪里?从上至下来思考的话,第一行的Class.forName执行成功了吗?第二行的getDeclaredMethod方法找到我们需要的next方法了吗?返回值获取成功了吗?

 

带着这些问题,我们来排查。因为题目中catch块中的异常没有进行print,我们通过其他的方式来排查。首先排查简单的项,getDeclaredMethod方法找到了吗?我们可以通过使用一下我们自定义的RegisterClassnew一个对象来调用next方法,发现是可以打印的,且结果为true

1
2
console.log(RegisterClass.$new().next())
// true

那么就说明我们自定义的Class是创建成功了的,且能够正常使用。那问题就出现在第一个,Class.forName加载这个类成功了吗?其实是没有成功的。当我们点击下一关按钮的时候,这里抛出的异常其实是ClassNotFoundException,找不到我们使用Frida自定义的类。那原因出在哪里呢?我们接着往下看

 

首先我们来看一下Class.forName的流程

1
2
3
4
5
@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    return forName(className, true, VMStack.getCallingClassLoader());
}

内部又调用了forName的重载,注意第三个参数是VMStack.getCallingClassLoader

1
2
3
4
5
6
7
8
/**
* Returns the defining class loader of the caller's caller.
*
* @return the requested class loader, or {@code null} if this is the
*         bootstrap class loader.
*/
@FastNative
native public static ClassLoader getCallingClassLoader();

是一个native方法,注释翻译一下就是返回请求类的loader,如果为null则返回bootstrap class loader,那我们这里调用这个方法拿到的就是跟SixthActivity是同一个ClassLoader

 

这个方法只有三个参数,第一个为className,我们的className是正确的,所以我们要从ClassLoader来入手解决这个问题。下面来补充下ClassLoader的知识

JVM类加载器

  • JVM的类加载器包括三种:
    • Bootstrap ClassLoader(引导类加载器):C/C++代码实现的加载器,用于加载指定的JDK核心的类库,比如java.lang.、java.util.等系统类。Java虚拟机的启动就是通过Bootstrap,该ClassLoaderJava里面无法获取,只负责加载/lib下的类。
    • Extensions ClassLoader(拓展类加载器):Java中的实现类为ExtClassLoader,提供了除了系统类之外的功能,可以在Java里面获取,负责加载/lib/ext下的类。
    • Application ClassLoader(应用程序类加载器):Java中的实现类为AppClassLoader,是与我们接触最多的类加载器,开发人员写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
    • 自定义类加载器,只需要通过继承java.lang.ClassLoader类的方式来实现自己的类加载器即可。

ClassLoader继承关系

图中我们几种ClassLoader的继承关系,其中红色箭头所描述的就是双亲委派机制。当我们自定义类加载器想要加载一个类时,它首先不会自己想着去加载这个类,而是要先向上询问Application类加载器,你能帮我加载这个类吗?Application类加载器说,我上面还有你爷爷类加载器,就这样,直到找到Bootstrap类加载器,如果Bootstrap类加载器不能加载此类,那么就会反向让自己的子类去想办法处理加载。如果上层的类加载器能够加载该类,就直接加载成功了。简单点说就是每个儿子都不愿意干活,每次有活就它的父亲去干,直到父亲说这件事我干不了,让儿子想办法去完成,这个就是双亲委派机制。

 

双亲委派机制有什么好处呢?

  • 安全:我们已经知道,Bootstrap类加载器会加载系统的类库,比如它加载了一个String类,我们自定义的类加载器还想加载一个一模一样的类,那肯定是不行的,因为上层已经加载过的类就不能在下层重新加载了,所以可以保护系统核心库不被篡改。
  • 效率:就是这样可以保证一个类只能被加载一次,不会重复加载,可以直接读取已经加载的Class

类加载的时机

  • 隐式加载:开发人员并没有刻意的想去加载一个类,或者都并不清楚类加载的概念
    • 创建类的示例
    • 访问类的静态变量
    • 调用类的静态方法
    • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
    • 初始化某个类的子类
  • 显式加载:
    • 使用LoadClass方法加载
    • 使用forName方法加载

Android系统中的类加载器

Android平台的类加载器并不是Android特有的,而是从Java当中继承过来的,那么Android类加载器跟Java中的又有什么区别呢?下面我们来一一介绍:

  • ClassLoader:一个抽象类,所有的类加载器都直接或间接的继承它
  • BootClassLoader:预加载常用的类加载器,它是单例模式的。与Java中的BootStrapClassLoader不同,它并不是由C实现的,而是由Java代码实现的
  • BaseDexClassLoaderPathClassLoaderDexClassLoaderInMemoryDexClassLoader的父类,类加载的主要逻辑都是在BaseDexClassLoader完成的。
  • SecureClassLoader继承类抽象类ClassLoader,拓展类ClassLoader类,加入类权限方面的功能,加强了安全性,其子类URLClassLoader是用URL路径从网络资源来加载类
  • PathClassLoaderAndroid默认使用的类加载器,一个apk中的Activity等类就是由它来加载的
  • DexClassLoader可以加载任意目录下的dexjarapkzip文件,比PathClassLoader更加灵活,是实现插件化、热修复以及dex加壳的重点
  • InMemoryDexClassLoader是从内存中直接加载dex,它是在Android8.0以后引入的

注意:系统当中常用的Framwork层的类都是由BootClassLoader来加载的,开发一个APP所使用的系统API的类,都是由它加载的,而APP内的类都是由PathClassLoader进行加载的。

 

再回到第六关的问题,其实根本原因就是Frida加载我们自定义类RegisterClass使用的ClassLoaderSisthActivityPathClassLoader并不是同一个ClassLoader,所以导致我们使用PathClassLoader来加载会出现ClassNotFoundException,知道这个问题所在我们就可以解决这个问题了,下面提供两种解决思路,感谢Simp1er大佬提供的解决方案

  • 第一种思路:双亲委派机制

既然我们使用默认的PathClassLoader加载不了,那么我们知道,它的加载过程是先通过父加载器进行加载的,也就是会调用BootClassLoader来加载我们的目标类,当然BootClassLoader也是无法成功加载的,所以我们可以在PathClassLoaderBootClassLoader中间插入我们RegisterClass