学习目标
今天带大家来做一道简单的Frida Hook Java
层的题目,总共有七个小关卡,每个关卡都有一个小的考察点,来考察我们Frida
的基础知识,如果已经会的同学也可以作为一个资料来翻阅
APP:Frida
测试题
下载地址:看文章结束
第一关
先把APP安装跑起来,每一关都有一个要求和考察点
考察点:方法参数的修改
分析:大概的看一下逻辑,也就是点击下一关,会调用onClick
方法,onClick
方法中会调用check
方法,但是参数的是一个false
,在check
方法中,根据这个参数来选择是进入到下一关还是提示失败,如果我们不进行任何的修改,y永远也无法进入到下一关,所以呢我们要使用Frida,修改check
方法的参数,使其变为true
,来帮助我们进入下一关。
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
就可以了
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的方法来取,来看代码
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
对象也是该类的一个实例,有了这个实例就可以进行主动调用实例方法了。来看代码
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
,来看代码
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
类型的结果,通过这个结果来选择是否进入下一关或提示失败
其实上面的分析过程也是比较好理解,就相当于我们需要一个类,如下
package com.dta.test.frida.activity;
public class RegisterClass{
public RegisterClass{
}
public boolean next(){
return true;
}
}
题目也就是调用了这个RegisterClass
类中的next
方法,那么我们根据这个题目来通过Frida
提供的Java.registerClass
API来构建这样一个类
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
console.log(RegisterClass.$new().next())
// true
那么就说明我们自定义的Class
是创建成功了的,且能够正常使用。那问题就出现在第一个,Class.forName
加载这个类成功了吗?其实是没有成功的。当我们点击下一关按钮的时候,这里抛出的异常其实是ClassNotFoundException
,找不到我们使用Frida
自定义的类。那原因出在哪里呢?我们接着往下看
首先我们来看一下Class.forName
的流程
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
return forName(className, true, VMStack.getCallingClassLoader());
}
内部又调用了forName
的重载,注意第三个参数是VMStack.getCallingClassLoader
/**
* 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
对象
-
- 显式加载:
-
-
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
来表示的,我们直接修改这个值就可以了,看下面第一种实现
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
可以正确加载
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)
第七关
下面贴一下第七关的源代码
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[1024];
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
就可以了,来看代码
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的内容,使我们透过问题来了解本质,才能有所提高。
题目下载地址