类加载器

类加载的时机

1
2
3
4
隐式加载:当访问类的静态变量、为静态变量赋值、调用类的静态方法的时候会加载类
此外,创建类的实例、某个类的子类的时候,父类也会被加载

显式加载:使用Class.forName加载、使用loadClass加载

类加载器

1
2
3
4
5
6
7
8
9
BootClassLoader:单例模式,用来加载系统类

BaseDexClassLoader:是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父类,类加载的主要逻辑都是在BaseDexClassLoader完成的

PathClassLoader:是默认使用的类加载器,用于加载app自身的dex

DexClassLoader:用于实现插件化、热修复、dex加固等

InMemoryDexClassLoader:Android 8.0之后才有,用于内存加载dex

dex加载过程中,主要逻辑都是在BaseDexClassLoader中完成的

PathClassLoaderDexClassLoaderInMemoryDexClassLoader继承自BaseDexClassLoader

对于一些app加固,其实用不到这么上层的函数,可能会找更底层的系统调用函数,或者自己实现相关的dex加载函数

demo

这里以DexClassLoader为例,DexClassLoader的实现如下

1
2
3
4
5
6
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}

可以看到DexClassLoaderBaseDexClassLoader的子类,而且调用DexClassLoader的时候需要在最后一个参数指定父类的ClassLoader

MainActivity.java中测试MainAcvtivityclassLoader

1
2
Log.d("this is a test","String: " + String.class.getClassLoader());
Log.d("this is a test","String: " + MainActivity.class.getClassLoader());

得到MainAcvtivityclassLoaderdalvik.system.PathClassLoader ,String系统类的classLoaderjava.lang.BootClassLoader

可以使用getParent()方法获取父classLoader

1
Log.d("this is a test","String: " + MainActivity.class.getClassLoader().getParent());

得到PathClassLoader的父classLoaderjava.lang.BootClassLoader

DexClassLoader的第四个参数指定父classLoader,如果父classLoader不同,加载出来的也类不一样,最终可能会导致DexClassLoader.loadClass(...)加载出的类不一样

一定要搞清楚的一点是,不同的classLoader里的class不一样,有的class是在子类加载器中,不在父类加载器中

类的双亲委派机制

主要解决的是hook的时候找不到类的情况,如果确认类路径和类名正确,此时会枚举所有classLoader,然后在每个classLoader中加载对应的类

找不到类的情况一般就是:类是动态加载的,只有app里某个功能被触发之后才加载;类加载器不对,需要枚举所有classLoader

双亲委派机制的工作原理

如果一个类加载器收到了类加载请求,会先把这个请求委托给父类的加载器去执行,比如上面的DexClassLoader,在收到加载类的请求的时候会交给第四个参数的classLoader去加载

如果父类加载器还存在其他父类加载器,则进一步往上委托,依次类推,最终到达顶层的启动类加载器

如果父类加载器可以完成类的加载任务,就成功返回,而如果父类加载器无法完成加载,子类加载器才会尝试自己去加载

为什么要有双亲委派机制

避免重复加载,已经加载的class,可以直接读取

更加安全,无法自定义类来替代系统的类,可以防止核心API库被篡改,比如classLoader a1里自定义了一个java.lang.String类,然后要使用a1去加载String类的时候,不会先加载a1里自定义的string类,而是加载父加载器里的系统string

fridaHook中找不到类的情况

demo

MainActivity.java中部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public void loadDex() {
String DexName = "JohnDemo.dex";
String DexPath = getDir("shell", MODE_PRIVATE).getAbsolutePath() + File.separator + DexName;
Context context = getApplicationContext();
try {
InputStream ins = context.getAssets().open(DexName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int index;
while ((index = ins.read(bytes)) != -1)
baos.write(bytes, 0, index);
byte[] decDex = decrypt(baos.toByteArray());
ins.close();
baos.close();

FileOutputStream fos = new FileOutputStream(new File(DexPath));
fos.write(decDex);
fos.close();

this.dynamicDex = new DexClassLoader(
DexPath,
context.getCacheDir().getAbsolutePath(),
getApplicationInfo().nativeLibraryDir,
MainActivity.class.getClassLoader());
new File(DexPath).delete();
} catch (Exception e) {
e.printStackTrace();
}
}

public void dynamic() {
if (this.dynamicDex == null) {
loadDex();
}
try {
Class<?> dynamic = this.dynamicDex.loadClass("com.JohnDemo.app.Dynamic");
Object obj = dynamic.newInstance();
Method method = dynamic.getDeclaredMethod("sayHello");
String retval = (String) method.invoke(obj);
Toast.makeText(this, retval, Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
}
}

如果这个时候想使用frida去找com.JohnDemo.app.Dynamic类,直接找是找不到的

1
2
3
Java.perform(function () {
var dynamic = Java.use("com.JohnDemo.app.Dynamic");
});

为什么找不到,因为frida中默认的classLoader是PathClassLoader,PathClassLoader的父classLoader是BootClassLoader,这两个classLoader中都没有这个类,因为代码中是用DexClassLoader去加载的

frida 通过 Java.use() 查找类时,默认使用 Java.availableClasses 列表,而这个列表通常只包含 PathClassLoader 能访问的类,即app在 classes.dex 里默认打包的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function () {

Java.enumerateClassLoaders({
onMatch: function (loader) {
try {
// 修改classloader
Java.classFactory.loader = loader;
var dynamic = Java.use("com.JohnDemo.app.Dynamic");
console.log("dynamic: ", dynamic, loader);
} catch (e) {
console.log(loader);
}
},
onComplete: function () {}
});

});

加固对hook的影响

普通app运行流程

1
2
3
BootClassLoader加载系统核心库

PathClassLoader加载app自身的dex

加固app运行流程

1
2
3
4
5
BootClassLoader加载系统核心库

PathClassLoader加载壳的dex

壳的dex/so加载原先app自身的dex

PathClassLoader加载dex以后,PathClassLoader的值会记录在LoadedApkmClassLoader属性中,默认使用这个ClassLoader去寻找类,因此加固app需要修正ClassLoader

举例

对于没有加固的app来说,比如class.dex就是真正的dex,然后这个dex里有一个类test,此时默认使用PathClassLoader去加载完class.dex之后,mClassLoader的值被置为PathClassLoader,这里使用frida是可以找到test类的

但是对于加固的app来说,PathClassLoader是加载壳的dex,mClassLoader的值也是加载壳的这个PathClassLoader,而真正的dex是由DexClassLoader去加载的,此时使用frida找不到test类

而且对于加固之后app,Android系统会修正ClassLoader,为什么?因为如果不修正的化,mClassLoader就是加载壳的PathClassLoader,那么连系统自己都找不到test类,所以需要修正classLoader

常见的修正方式

1
2
3
插入ClassLoader,可以理解为把mClassLoader的值给设置成DexClassLoader,而DexClassLoader的父加载器是PathClassLoader,所以可以正常运行,对于这种情况,frida其实不需要做任何处理包括枚举所有loader,正常hook就行

替换ClassLoader,也就是把DexClassLoader的父加载器设置为BootClassLoader,然后把PathClassLoader的父加载器设置为DexClassLoader,根据双亲委派机制,仍然可以正常运行