roysue 发表于 2021-8-9 14:26

大数据安全入门安卓Frida习题讲解

本帖最后由 roysue 于 2021-8-9 16:38 编辑

### 学习目标

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

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

### 第一关

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



> 考察点:方法参数的修改

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

```javascript
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`就可以了

```javascript
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的方法来取,来看代码

```javascript
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`对象也是该类的一个实例,有了这个实例就可以进行主动调用实例方法了。来看代码

```javascript
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`,来看代码

```javascript
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`类型的结果,通过这个结果来选择是否进入下一关或提示失败

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

```java
package com.dta.test.frida.activity;

public class RegisterClass{

    public RegisterClass{

    }

    public boolean next(){
      return true;
    }

}
```

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

```javascript
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`方法找到了吗?我们可以通过使用一下我们自定义的`RegisterClass`,`new`一个对象来调用next方法,发现是可以打印的,且结果为`true`

```javascript
console.log(RegisterClass.$new().next())
// true
```

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

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

```java
@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    return forName(className, true, VMStack.getCallingClassLoader());
}
```
内部又调用了`forName`的重载,注意第三个参数是`VMStack.getCallingClassLoader`

```java
/**
* 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`,该`ClassLoader`在`Java`里面无法获取,只负责加载`/lib`下的类。

- - `Extensions ClassLoader`(拓展类加载器):`Java`中的实现类为`ExtClassLoader`,提供了除了系统类之外的功能,可以在`Java`里面获取,负责加载`/lib/ext`下的类。

- - `Application ClassLoader`(应用程序类加载器):`Java`中的实现类为`AppClassLoader`,是与我们接触最多的类加载器,开发人员写的代码默认就是由它来加载,`ClassLoader.getSystemClassLoader`返回的就是它。
- - 自定义类加载器,只需要通过继承`java.lang.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`代码实现的
- `BaseDexClassLoader`是`PathClassLoader`、`DexClassLoader`、`InMemoryDexClassLoader`的父类,类加载的主要逻辑都是在`BaseDexClassLoader`完成的。
- `SecureClassLoader`继承类抽象类`ClassLoader`,拓展类`ClassLoader`类,加入类权限方面的功能,加强了安全性,其子类`URLClassLoader`是用`URL`路径从网络资源来加载类
- `PathClassLoader`是`Android`默认使用的类加载器,一个`apk`中的`Activity`等类就是由它来加载的
- `DexClassLoader`可以加载任意目录下的`dex`、`jar`、`apk`、`zip`文件,比`PathClassLoader`更加灵活,是实现插件化、热修复以及`dex`加壳的重点
- `InMemoryDexClassLoader`是从内存中直接加载`dex`,它是在`Android8.0`以后引入的

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

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

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

既然我们使用默认的`PathClassLoader`加载不了,那么我们知道,它的加载过程是先通过父加载器进行加载的,也就是会调用`BootClassLoader`来加载我们的目标类,当然`BootClassLoader`也是无法成功加载的,所以我们可以在`PathClassLoader`跟`BootClassLoader`中间插入我们`RegisterClass`的`ClassLoader`,就可以由`PathClassLoader`->`BootClassLoader`变为`PathClassLoader`->`RegisterClass`的`ClassLoader`->`BootClassLoader`,这样原本是使用`PathClassLoader`来加载我们的类,`PathClassLoader`会先委托父亲加载,`BootClassLoader`会加载失败,最终交由我们`RegisterClass`的`ClassLoader`来加载,就可以完成本次加载了。插入也是非常简单的,因为`ClassLoader`中标志一个`ClassLoader`是另外一个`ClassLoader`的父亲,就是使用`parent`来表示的,我们直接修改这个值就可以了,看下面第一种实现

```javascript
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
                  }
                }
            }
      })
      var targetClassLoader = RegisterClass.class.getClassLoader()
      Java.enumerateClassLoaders({
            onMatch: function(loader){
                try{
                  if(loader.findClass("com.dta.test.frida.activity.SixthActivity")){
                         // PathClassLoader
                         var PathClassLoader = loader
                         var BootClassLoader = PathClassLoader.parent.value
                         PathClassLoader.parent.value = targetClassLoader
                         targetClassLoader.parent.value = BootClassLoader
                  }
                  
                }catch(e){

                }
               
            },onComplete: function(){
                console.log("Completed!")
            }
      })
    })
}
setImmediate(main)
```
- 第二种思路:`Hook` `Class`类的`forName`方法,可以修改第三个参数为我们`RegisterClass`的`ClassLoader`,从而使`Class.forName`可以正确加载


```javascript
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
                  }
                }
            }
      })
      var targetClassLoader = RegisterClass.class.getClassLoader()
      var Class = Java.use('java.lang.Class')
      Class.forName.overload('java.lang.String', 'boolean', 'java.lang.ClassLoader').implementation = function (str, init, loader) {
            console.log('loader', loader)
            console.log('className', str)
            console.log('iniit', init)
            return this.forName(str, init, targetClassLoader)
      }
    })
}
setImmediate(main)
```

### 第七关

下面贴一下第七关的源代码
```java
package com.dta.test.frida.activity;

import android.view.View;
import com.dta.test.frida.R;
import com.dta.test.frida.base.BaseActivity;
import com.dta.test.frida.base.Level;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import dalvik.system.InMemoryDexClassLoader;

public class SeventhActivity extends BaseActivity {
    private Class<?> MyCheckClass;

    @Override
    protected String getDescription() {
      return getString(R.string.seventh);
    }

    @Override
    public void onClick(View v) {
      super.onClick(v);
      try {
            loadDex();
            Method check = MyCheckClass.getDeclaredMethod("check");
            Object handle = MyCheckClass.newInstance();
            boolean flag = (boolean) check.invoke(handle);
            if (flag){
                gotoNext(Level.Eighth);
            }else {
                failTip();
            }
      } catch (Exception e) {
            failTip();
      }
    }

    private void loadDex() {
      if (MyCheckClass != null){
            return;
      }
      if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            try {
                InputStream is_dex = getAssets().open("mycheck.dex");
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                byte[] bytes = new byte;
                int len;
                while ( ( len = is_dex.read(bytes,0,bytes.length) ) != -1 ){
                  bos.write(bytes,0,len);
                }
                bos.flush();

                ByteBuffer buffer = ByteBuffer.wrap(bos.toByteArray());

                InMemoryDexClassLoader classLoader = new InMemoryDexClassLoader(buffer, getClassLoader());

                MyCheckClass = classLoader.loadClass("com.dta.frida.MyCheck");

            }catch (Exception e){
                e.printStackTrace();
                failTip();
            }
      }
    }

    @Override
    protected String getActivityTitle() {
      return "第七关";
    }
}
```

> 考察点:`Frida`切换`ClassLoader`

> 分析:这道题目是从外部加载了一个`dex`文件,然后反射加载`com.dta.frida.MyCheck`类,并调用其中的`check`方法。而我们想要在`Frida`中`Hook`这个MyCheck类,直接`use`是不行的,也是因为`ClassLoader`的问题,既然我们已经了解了`ClassLoader`,这个题目就很简单了,只需要学会在`Frida`中如何来切换`ClassLoader`就可以了,使用`Java.enumerateClassLoaders`来枚举`ClassLoader`,然后判断哪个`ClassLoader`是我们想要的,再将`Java.classFactory.loader`赋值为我们需要的`loader`就可以了,来看代码

```javascript
function main(){
    Java.perform(function(){
       //Seven
      Java.enumerateClassLoaders({
            onMatch: function(loader){
                try{
                  if(loader.findClass("com.dta.frida.MyCheck")){
                        console.log("Found!")
                        Java.classFactory.loader = loader
                  }
                }catch(e){
                  
                }
            },onComplete: function(){
                console.log("Completed!")
            }
      })

      Java.use("com.dta.frida.MyCheck").check.implementation = function(){
            return true
      }
    })
}
setImmediate(main)
```

### 总结

本篇文章,主要介绍了Frida Hook Java层的用法,API都是死的,但是需要我们灵活掌握,根据不同的目的使用不同的API组合来实现我们的目的,这七个题目也都是比较基础的内容,因为安卓的Java本来就是很简单的。当然知其然必要知其所以然,所以在第六题中讲解了ClassLoader的内容,使我们透过问题来了解本质,才能有所提高。

题目下载地址

huaishion 发表于 2021-8-9 15:55

没看到下载地址啊??

占心 发表于 2022-1-13 14:52

第六期去掉main开头 直接蹦第五题{:1_907:}


Java.perform(function () {
      //Sixth
      var RegisterClass = Java.registerClass({
            name: "com.dta.test.frida.activity.RegisterClass",
            methods: {
                next: {
                  returnType: "boolean",
                  argumentTypes: [],
                  implementation: function () {
                        return true
                  }
                }
            }
      })
      var targetClassLoader = RegisterClass.class.getClassLoader()
      Java.enumerateClassLoaders({
            onMatch: function (loader) {
                try {
                  if (loader.findClass("com.dta.test.frida.activity.SixthActivity")) {
                        // PathClassLoader
                        var PathClassLoader = loader
                        var BootClassLoader = PathClassLoader.parent.value
                        PathClassLoader.parent.value = targetClassLoader
                        targetClassLoader.parent.value = BootClassLoader
                  }
                } catch (e) {
                }
            }, onComplete: function () {
                console.log("Completed!")
            }
      })
    })

laos 发表于 2021-8-9 15:35

666 给大佬打电话

Days0708 发表于 2021-8-9 16:12

好厉害,膜拜大佬

yingsummery 发表于 2021-8-9 16:46

收藏了,谢谢楼主

lies2014 发表于 2021-8-9 17:36

不注册不能下载?能不能给个直接能下的链接

goda 发表于 2021-8-9 23:25

谢谢分享

慕容语嫣 发表于 2021-8-10 03:12

收藏 学习了

Talrity 发表于 2021-8-10 08:37

学习了 不错

bingshen 发表于 2021-8-10 09:27

步骤清晰明了,一步步学习思路。
页: [1] 2 3 4 5 6
查看完整版本: 大数据安全入门安卓Frida习题讲解