Frida--Android逆向之动态加载dex Hook(下)

admin 2022年1月6日01:12:25评论1,175 views字数 16965阅读56分33秒阅读模式

​ 上篇主要是跟着师傅学习了Robust的原理,并以做题的思路去求解了这个示例ctf,其实这是一种思路的启示,当我们在不知道怎么hook动态加载的dex,jar时候,找找是否存在能够操作动态加载出来的类的方法。

​ 这一篇我们一起来学习如何用Frida来hook DexclassLoader,如何用反射直接调用类的方法,达到跟hook一般类一样的效果。最终在虚拟机、测试机和frida中发现多种问题,之后更换测试机进行测试的时候仍然存在问题,留下了一部分搞不懂的地方,当个坑未,来找到解决问题在进行填补。

文章涉及内容以及使用到的工具

使用到的工具

ADT(Android Developer Tools)
Jadx-gui
JEB
frida
apktool
android源码包
Nexus 6p(genymotion,实体机等亦可)

涉及知识点

Java 泛型
Java 反射机制
DexClassLoader 动态加载机制
Frida 基本操作
Frida 创建任意类型数组(Java.array)
Frida 类型转换(Java.cast)
Frida 方法重载(overload)
Frida Spawn

代码分析与构造

Frida Spawn的使用

​ 通过上篇Robust的原理学习和对app的分析,我们知道Robust的其实就是在正常的使用DexClassLoader去动态加载文件,随后通过反射的方式去调用类方法或成员变量。

​ 同时在上篇文章中,我们也知道Robust调用DexClassLoader的类是在PatchExecutor中,而调用PatchExecutor类是在一个叫runRobust的方法中,这个方法就在MainActivity中,并且在onCreate方法中调用。

image-20210813160714545

现在我们明白了一点是,app动态加载dex的地方是在onCreate中,也就是说app一启动就执行了动态加载,并不是在我们点击按钮的时候。所以这个地方,我们要执行py脚本的话,需要在app执行onCreate方法之前。frida有一个功能可以为我们生成一个进程而不是将它注入到运行中的进程中,它注入到Zygote中,生成我们的进程并且等待输入。

我们可以通过-f参数选项来实现。

frida -U -f cn.chaitin.geektan.crackme   //app完整名
╰─$ frida -U -f cn.chaitin.geektan.crackme                                                       1 ↵
____
/ _ | Frida 15.1.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
Spawned `cn.chaitin.geektan.crackme`. Use %resume to let the main thread start executing!
[Android Emulator 5554::cn.chaitin.geektan.crackme]-> %resume
[Android Emulator 5554::cn.chaitin.geektan.crackme]->

image-20211108183528543

运行一遍我们就可以从从上面可以看到,通过-f参数,frida会Spawned这个应用,在这个时候启动python脚本,再执行%resume命令,我们就可以在app执行onCreate方法前完成脚本的启动,这时候就能hook住onCreate中执行的一些方法。

DexClassLoader 动态加载机制

现在我们已经知道hook住onCreate中执行的方法了,现在我们就来试试,第一个目标是能获取到动态加载的dex中的类。在这直接我们来试一下直接去hook动态加载的类,会出现什么情况。

JS代码如下:

Java.perform(function(){
console.log("test");
Java.use("cn.chaitin.geektan.crackme.MainActivityPatch");
console.log("test over");

});

完整代码:

# -*- coding: UTF-8 -*-
import frida,sys

def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)


js_code = '''
Java.perform(function(){
console.log("test");
Java.use("cn.chaitin.geektan.crackme.MainActivityPatch");
console.log("test over");

});
'''


session = frida.get_usb_device().attach("cn.chaitin.geektan.crackme")
script = session.create_script(js_code)
script.on('message',on_message)
script.load()
sys.stdin.read()

我们首先启动通过frida启动,然后运行我们的脚本,在oncreate方法未调用前,运行我们的脚本,在使用%resume启动APP

1.frida -U -f cn.chaitin.geektan.crackme
2.python jscode.py
3.%resume

这里可以明显的看到输出了对应的错误信息

test
{'type': 'error', 'description': 'Error: java.lang.ClassNotFoundException: Didn\'t find class "cn.chaitin.geektan.crackme.MainActivityPatch" on path: DexPathList[[zip file "/data/app/cn.chaitin.geektan.crackme-1/base.apk"],nativeLibraryDirectories=[/data/app/cn.chaitin.geektan.crackme-1/lib/x86, /vendor/lib, /system/lib]]', 'stack': 'Error: java.lang.ClassNotFoundException: Didn\'t find class "cn.chaitin.geektan.crackme.MainActivityPatch" on path: DexPathList[[zip file "/data/app/cn.chaitin.geektan.crackme-1/base.apk"],nativeLibraryDirectories=[/data/app/cn.chaitin.geektan.crackme-1/lib/x86, /vendor/lib, /system/lib]]\n at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:124)\n at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:443)\n at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:812)\n at _make (frida/node_modules/frida-java-bridge/lib/class-factory.js:112)\n at use (frida/node_modules/frida-java-bridge/lib/class-factory.js:63)\n at use (frida/node_modules/frida-java-bridge/index.js:246)\n at <anonymous> (/script1.js:4)\n at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:16)\n at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:238)\n at <anonymous> (frida/node_modules/frida-java-bridge/index.js:230)\n at apply (native)\n at ne (frida/node_modules/frida-java-bridge/lib/class-factory.js:613)\n at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:592)', 'fileName': 'frida/node_modules/frida-java-bridge/lib/env.js', 'lineNumber': 124, 'columnNumber': 1}

果不其然,这样去直接hook类肯定是不行的,但我们知道只要是从外部资源文件中动态加载dex,一般都是采用DexClassLoader动态加载的。学习过Java的同学应该知道,DexClassLoader动态加载的主要方法就是loadClass()。我们从Java源码上去分析一下,这里给大家一个java开发文档的查看地址:访问

我们看到DexClassLoader的构造函数有4个参数,这里没有loadClass(),我们继续查看它的父类BaseDexClassLoader。

public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}

同样BaseDexClassLoader也没有loadClass(),最终在它的父类ClassLoader中找到了loadClass()方法。

可以看到DexClassLoader加载的逻辑其实就是ClassLoader中的loadClass(),它的机制简单的了解到这里,现在我们来试试,通过这样的方式能不能hook我们想要的类。

我们先hook DexClassLoader的构造函数,看看传递进的参数值是什么。

Java.perform(function(){
//创建一个DexClassLoader的wapper
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
//hook 它的构造函数$init,我们将它的四个参数打印出来看看。
dexclassLoader.$init.implementation = function(dexPath,optimizedDirectory,librarySearchPath,parent){
console.log("dexPath:"+dexPath);
console.log("optimizedDirectory:"+optimizedDirectory);
console.log("librarySearchPath:"+librarySearchPath);
console.log("parent:"+parent);
//不破换它原本的逻辑,我们调用它原本的构造函数。
this.$init(dexPath,optimizedDirectory,librarySearchPath,parent);
}
console.log("down!");

});

执行情况如下:

我们获取到了构造函数的参数

down!
dexPath:/data/user/0/cn.chaitin.geektan.crackme/cache/GeekTan/patch_temp.jar
optimizedDirectory:/data/user/0/cn.chaitin.geektan.crackme/cache
librarySearchPath:null
parent:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/cn.chaitin.geektan.crackme-1/base.apk"],nativeLibraryDirectories=[/data/app/cn.chaitin.geektan.crackme-1/lib/arm64, /vendor/lib64, /system/lib64]]]

Frida 方法重载(overload)

我们来尝试获取一下动态加载的类

Java.perform(function(){
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var hookClass = undefined;
//hook loadClass方法
dexclassLoader.loadClass.implementation = function(name){
/*因为loadClass可能会加载很多类,所以我们得定义个hookname变量,
这样有针对的获取我们想要的类*/
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);
if(name === hookname){
hookClass = result;
console.log(hookClass);
return result;
}
return result;
}
});

执行看看结果

{'type': 'error', 'description': "Error: loadClass(): has more than one overload, use
.overload(<signature>) to choose from:.overload('java.lang.String')
.overload('java.lang.String', 'boolean')"....}

发现frida报错了,从报错信息我们可以发现loadClass()有2个重载方法,我们这里需要通过overload指定我们要HOOK的重载方法才行,如果不知道重载哪一个然后可以先让报错在进行操作

ClassLoader类中的两个loadClass重载方法

loadClass(String name);

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

loadClass(String name, boolean resolve);

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}

可以看到真正执行loadClass的方法是loadClass(String name, boolean resolve);, 那么我们可以hook第一个,然后在其调用第二个重载方法。

  Java.perform(function(){
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");

dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
//定义一个String变量,指定我们需要的类
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
//直接调用第二个重载方法,跟原本的逻辑相同。
var result = this.loadClass(name,false);
//如果loadClass的name参数和我们想要hook的类名相同
if(name === hookname){
//则拿到它的值
hookClass = result;
//打印hookClass变量的值
console.log(hookClass);
send(hookClass);
return result;
}
return result;
}
});

执行情况:

class cn.chaitin.geektan.crackme.MainActivityPatch
[*] <instance: java.lang.Class>

可以看到我们已经通过hook loadClass拿到了MainActivityPatch类

Frida 类型转换(Java.cast)

这里我们是不能直接通过(.)运算符直接调用方法的,可以看到loadClass()返回类型的是一个泛型,其中?代表任何类型,因为loadClass()也不知道要加载的类的类型,所以返回类型就采用Class<?>代表所有类型的类,所以最后返回的是一个类型为指向MainActivityPatch的Class对象

protected Class<?> loadClass(String name, boolean resolve);

理论是这样的,但实际上却不是,我们还需要进行类型转换。这里Frida提供的一个方法处理泛型的方法Java.cast
构造代码如下:

 Java.perform(function(){
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var constructorclass = Java.use("java.lang.reflect.Constructor");
var objectclass= Java.use("java.lang.Object");
dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);

if(name == hookname){
var hookClass = result;
console.log("------------------------------CAST--------------------------------")
//类型转换
var hookClassCast = Java.cast(hookClass,ClassUse);
//调用getMethods()获取类下的所有方法
var methods = hookClassCast.getMethods();
console.log(methods);
console.log("-----------------------------NOT CAST-----------------------------")
//未进行类型转换,看看能否调用getMethods()方法
var methodtest = hookClass.getMethods();
console.log(methodtest);
console.log("---------------------OVER------------------------")
return result;

}
return result;
}


});

运行结果:

image-20211221120607031

我们可以看到cast后才能调用getMethods(),没有cast则会报为定义不能调用的错误。

Frida创建任意类型数组(Java.array)

现在我们得到了一个类型为MainActivityPatch的Class对象,我们接下来就来看看怎么调用Joseph方法。在这之前,你需要对反射的用法有一定的了解。至于怎么用,就针对实际情况选取你认为最好的办法就行了。
当然在我多次踩坑之后,比如:

  • 在Class的getMethod方法中,怎么用js构造int.class,float.class,以及构造Integer.TYPE数组出现莫名错误。
  • 怎么利用Frida函数构造任意类型的数组。
  • 无参构造函数调用newInstance(),跟有参构造函数调用newInstance()的问题。
  • ….

我认为还是有必要给大家提供一种比较通用的方法。
1.利用getDeclaredConstructor()获取具有指定参数列表构造函数的Constructor。

public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
return getConstructor0(parameterTypes, Member.DECLARED);
}

可以看到,参数是一个Class<?>…,也就是说这是一个[Ljava.lang.Object;类型的数组。我们现在要到MainActivity

Patch构造函数对象,从代码中可以知道参数是Object类型。

public MainActivityPatch(Object obj){
this.orginClass = (MainActivity) obj;
}

我们如何构造并传入这个数组呢

\\利用java.array的标准写法
var objectclass= Java.use("java.lang.Object");
var ConstructorParam =Java.array('Ljava.lang.Object;',[objectclass.class]);
var a = hookClassCast.getDeclaredConstructor(ConstructorParam);

\\偷懒写法
var a = hookClassCast.getDeclaredConstructor([objectclass.class]);

Java.array()的用法格式如下

Java.array('type',[value1,value2,....]);

支持什么type,大家可以参看frida-java源码。在class-factory.js中就可以找到了。基本类型如下:

1. Z -- boolean
2. B -- byte
3. C -- char
4. S -- short
5. I -- int
6. J -- long
7. F -- float
8. D -- double
9. V -- void

我们getDeclaredConstructor()得到了构造函数Constructor,我们现在要将它实例化,再来看看MainActivityPatch的构造函数,传递一个object对象,并将它强制转换成MainActivity类型。
那我们实例化的参数就是MainActivity对象了。

public class MainActivityPatch {
MainActivity originClass;
//构造函数
public MainActivityPatch(Object obj) {
this.originClass = (MainActivity) obj;
}

代码如下:

Java.perform(function(){
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var objectclass= Java.use("java.lang.Object");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var orininclass = Java.use("cn.chaitin.geektan.crackme.MainActivity");
var Integerclass = Java.use("java.lang.Integer");
//实例化MainActivity对象
var mainAc = orininclass.$new();


dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);

if(name == hookname){
var hookClass = result;
var hookClassCast = Java.cast(hookClass,ClassUse);
console.log("-----------------------------BEGIN-------------------------------------");
//获取构造器
var ConstructorParam =Java.array('Ljava.lang.Object;',[objectclass.class]);
var Constructor = hookClassCast.getDeclaredConstructor(ConstructorParam);
console.log("Constructor:"+Constructor);
console.log("orinin:"+mainAc);
//实例化,newInstance的参数也是Ljava.lang.Object;
var instance = Constructor.newInstance([mainAc]);
console.log("patchAc:"+instance);
send(instance);
console.log("--------------------------------------------------------------------");
return result;

}
return result;
}
});

执行结果:

Constructor:public cn.chaitin.geektan.crackme.MainActivityPatch(java.lang.Object)
orinin:cn.chaitin.geektan.crackme.MainActivity@4a3e446
patchAc:cn.chaitin.geektan.crackme.MainActivityPatch@d79b607
message: {'type': 'send', 'payload': '<instance: java.lang.Object, $className: cn.chaitin.geektan.crackme.MainActivityPatch>'} data: None

2.我们得到了实例化的类后,第二步就是利用getDeclaredMethods(),获取本类中的所有方法,包括私有的(private、protected、默认以及public)的方法,并通过数组下标的方式获取我们想要的方法。

public Method[] getDeclaredMethods() throws SecurityException {
Method[] result = getDeclaredMethodsUnchecked(false);
for (Method m : result) {
// Throw NoClassDefFoundError if types cannot be resolved.
m.getReturnType();
m.getParameterTypes();
}
return result;
}

从getDeclareMethods(),我们知道它返回的是一个Method数组

var func = hookClassCast.getDeclaredMethods();
console.log(func);
//直接通过下标获取我们要调用的方法
console.log(func[0]);

看看一个完整的示例,和上面的一样,仅仅调用了getDeclaredMethods()方法。

Java.perform(function(){
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var objectclass= Java.use("java.lang.Object");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var orininclass = Java.use("cn.chaitin.geektan.crackme.MainActivity");
var Integerclass = Java.use("java.lang.Integer");
//实例化MainActivity对象
var mainAc = orininclass.$new();


dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);

if(name == hookname){
var hookClass = result;
var hookClassCast = Java.cast(hookClass,ClassUse);
console.log("-----------------------------BEGIN-------------------------------------");
//获取构造器
var ConstructorParam =Java.array('Ljava.lang.Object;',[objectclass.class]);
var Constructor = hookClassCast.getDeclaredConstructor(ConstructorParam);
console.log("Constructor:"+Constructor);
console.log("orinin:"+mainAc);
//实例化,newInstance的参数也是Ljava.lang.Object;
var instance = Constructor.newInstance([mainAc]);
console.log("MainActivityPatchInstance:"+instance);
send(instance);
console.log("----------------------------Methods---------------------------------");
var func = hookClassCast.getDeclaredMethods();
console.log(func);
console.log("--------------------------Need Method---------------------------------");
console.log(func[0]);
var f = func[0];
console.log("---------------------------- OVER---------------------------------");
return result;

}
return result;
}
});

执行结果:

image-20211221163603106

3.接下来就是调用Method.invoke()去执行对应的方法了,invoke方法的第一个参数是执行这个方法的对象实例,第二个参数是带入的实际值数组,返回值是Object,也既是该方法执行后的返回值。

public native Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;

那现在就有一个问题,第二个值是一个数据,我们需要带入实际值的数组,那这么来构造数组呢,很简单刚刚我们已经学习了Frida 中Java.array的用法。现在我们就来构造2个实际值的Integer数组。

var Integerclass = Java.use("java.lang.Integer");
var num1 = Integerclass.$new(5);
var num2 = Integerclass.$new(6);
var numArr1 = Java.array('Ljava.lang.Object;',[num1,num2]);
var num3 = Integerclass.$new(7);
var num4 = Integerclass.$new(8);
var numArr2 = Java.array('Ljava.lang.Object;',[num3,num4]);

接下来我们就可以愉快的调用Joseph方法了。

最终代码

Java.perform(function(){
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var objectclass= Java.use("java.lang.Object");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var orininclass = Java.use("cn.chaitin.geektan.crackme.MainActivity");
var Integerclass = Java.use("java.lang.Integer");
var mainAc = orininclass.$new();


dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);
if(name == hookname){
var hookClass = result;
var hookClassCast = Java.cast(hookClass,ClassUse);
console.log("-----------------------------GET Constructor-------------------------------------");
var ConstructorParam =Java.array('Ljava.lang.Object;',[objectclass.class]);
var Constructor = hookClassCast.getDeclaredConstructor(ConstructorParam);
console.log("Constructor:"+Constructor);
console.log("orinin:"+mainAc);
var instance = Constructor.newInstance([mainAc]);
console.log("patchAc:"+instance);
send(instance);

console.log("-----------------------------GET Methods----------------------------");
var func = hookClassCast.getDeclaredMethods();
console.log(func);
console.log("--------------------------GET Joseph Function---------------------------");
console.log(func[0]);
var f = func[0];
var num1 = Integerclass.$new(5);
var num2 = Integerclass.$new(6);
var numArr1 = Java.array('Ljava.lang.Object;',[num1,num2]);
var num3 = Integerclass.$new(7);
var num4 = Integerclass.$new(8);
var numArr2 = Java.array('Ljava.lang.Object;',[num3,num4]);
console.log("-----------------------------GET Array------------------------------");
console.log(numArr1);
console.log(numArr2);
var rtn1 = f.invoke(instance,numArr1);
var rtn2 = f.invoke(instance,numArr2);
console.log("--------------------------------FLAG---------------------------------");
console.log("DDCTF{"+rtn1+rtn2+"}");
console.log("--------------------------------OVER--------------------------------");
return result;

}
return result;
}
});

得到最终答案:

image-20211221182844509

参考链接

https://bbs.pediy.com/thread-229657.htm

FROM :ol4three.com | Author:ol4three

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:12:25
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Frida--Android逆向之动态加载dex Hook(下)https://cn-sec.com/archives/721400.html

发表评论

匿名网友 填写信息