Android加壳与脱壳(一)ClassLoader和动态加载
zsk Lv4

三层类加载器

1. 启动类加载器 Bootstrap ClassLoader

负责加载存在放<JAVA_HOME>\lib目录或者-Xbootclasspath指定的路径中存放的类库。比如java.lang、java.uti等这些系统类。
启动类加载器无法被java程序直接引用,但可以通过在自定义类加载器时将getClassLoader的返回值设为null 将加载请求委派给启动类加载器处理。

2. 扩展类加载器 Extensions ClassLoader

它负责加载<JAVA_HOME>\lib\ext目录中或者java.ext.dirs指定的路径下的类库。Java中实现类为ExtClassLoader,提供了除了系统类之外的额外功能,可以在java里获取。

3.应用程序类加载器 Application ClassLoader

负责加载用户类路径上的所有类库。该加载器可以通过ClassLoader的静态方法getSystemLoader获得。java中的实现类为AppClassLoader,与我们接触最多的类加载器,也可以通过ClassLoader.getSystemClassLoader返回。

自定义类加载器

需要通过继承java.lang.ClassLoader类的方式来实现自己的类加载器即可。
加载顺序Bootstrap ClassLoader、Extention ClassLoader、AppClassLoader。

双亲委派模式

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//1.先检查是否已经加载过--findLoaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//2.如果自己没加载过,存在父类,则委托父类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}

if (c == null) {
//3.如果父类也没加载过,则尝试本级classLoader加载
c = findClass(name);
}
}
return c;
}

双亲委派模式的工作原理是如果一个类加载器收到了累加器的请求,他并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

我们要加载一个class文件,我们定义了一个CustomerClassLoader类加载器:
(1)首先会判断自己的CustomerClassLoader否加载过,如果加载过直接返回,
(2)如果没有加载过则会调用父类PathClassLoader去加载,该父类同样会判断自己是否加载过,如果没有加载过则委托给父类BootClassLoader去加载,
(3)这个BootClassLoader是顶级classLoader,同样会去判断自己有没有加载过,如果也没有加载过则会调用自己的findClass(name)去加载,
(4)如果顶级BootClassLoader加载失败了,则会把加载这个动作向下交还给PathClassLoader,
(5)这个PathClassLoader也会尝试去调用findClass(name);去加载,如果加载失败了,则会继续向下交还给CustomClassLoader来完成加载,这整个过程感觉是一个递归的过程,逐渐往上然后有逐渐往下,直到加载成功
其实这个String.class在系统启动的时候已经被加载了,我们自己定义一个CustomerClassLoader去加载,其实也是父类加载的

好处:
1.避免重复加载,如果已经加载过一次class,可以直接读取已经加载的class。
2.更加安全,无法自定义类来替代系统的类,可以防止核心api库被随意篡改。

这种机制天生就给java类划分了带优先级的层次关系,bootstrap classloader优先级最高。假设用户自己写了一个java.lang.object类,在加载时会加载请求会委派给bootstrap classloader,它会去\lib目录下加载jdk自带的java.lang.object类,这样不管用哪个类加载器去加载java.lang.object都会是同一个类。如果没有双亲委派的话,出现不同的java.lang.object类型,那么java类型体系中最基础的行为都无法保证。

Android系统中的ClassLoader的继承关系

image

Android系统中与ClassLoader相关的一共有8个:
ClassLoader为抽象类。
BootClassLoader预加载常用类继承自ClassLoader,单例模式。与Java中的BootClassLoader不同,并不是由C/C++代码实现,而是由Java实现的。
BaseDexClassLoaderPathCLassLoaderDexCLassLoaderInMemoryCLassLoader的父类,类加载器的主要逻辑都是在BaseDexClassLoader完成的。
SecureClassLoader继承了抽象类CLassLoader,扩展了ClassLoader类加入了权限方面的功能,加强了安全性,其子类URLClassLOader是用URl路径和jar文件中加载类和资源。
其中重点关注PathCLassLoaderDexCLassLoader
PathClassLoader是Android默认使用的类加载器,一个apk中的Activity等类便是在其中加载。
DexCLassLoader可以加载任意目录下的dex/jar/apl/zip文件,比PathClassLoader更灵活,是实现插件化,热修复,以及dex加壳的重点。
Android8.0新引入InMemoryDexClassloader,从名字看出是直接从内存中加载dex

通过查看Android8.0的源码,ClassLoader是一个抽象类,里面还有个parent变量,这是实现双亲委派机制的关键。对于每个继承的ClassLoader的类,都有parent来表示父节点。还定义了final,只能进行一次赋值。对于实现插件化,热修复parent是一个关键的变量。
不带参的默认ClassLoader构造函数会将parent指定为systemClassLoader,也就是说在不指定 <父>classloader的情况下,而是每个加载器内部都有一个parent,代表它的父classloader。只有bootstrap classloader的parent为null。

image

看下面PathCLassLoader、DexCLassLoader、InMemoryCLassLoader的源码都很少,主要逻辑都是在父类BaseDexClassLoader里。
DexClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}

PathClassLoader

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
public class PathClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code PathClassLoader} that operates on a given list of files
* and directories. This method is equivalent to calling
* {@link #PathClassLoader(String, String, ClassLoader)} with a
* {@code null} value for the second argument (see description there).
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}

/**
* Creates a {@code PathClassLoader} that operates on two given
* lists of files and directories. The entries of the first list
* should be one of the following:
*
* <ul>
* <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
* <li>Raw ".dex" files (not inside a zip file).
* </ul>
*
* The entries of the second list should be directories containing
* native library files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}

InMemoryDexClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class InMemoryDexClassLoader extends BaseDexClassLoader {
/**
* Create an in-memory DEX class loader with the given dex buffers.
*
* @param dexBuffers array of buffers containing DEX files between
* <tt>buffer.position()</tt> and <tt>buffer.limit()</tt>.
* @param parent the parent class loader for delegation.
* @hide
*/
public InMemoryDexClassLoader(ByteBuffer[] dexBuffers, ClassLoader parent) {
super(dexBuffers, parent);
}

/**
* Creates a new in-memory DEX class loader.
*
* @param dexBuffer buffer containing DEX file contents between
* <tt>buffer.position()</tt> and <tt>buffer.limit()</tt>.
* @param parent the parent class loader for delegation.
*/
public InMemoryDexClassLoader(ByteBuffer dexBuffer, ClassLoader parent) {
this(new ByteBuffer[] { dexBuffer }, parent);
}
}

代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void testClassLoader(){
ClassLoader thisClassloader = MainActivity.class.getClassLoader();
Log.i("ClassLoaderTag", "thisClassLoader: " + thisClassloader);
ClassLoader tmpClassloader = null;
ClassLoader parentClassloader = thisClassloader.getParent();
while (parentClassloader != null) {
Log.i("ClassLoaderTag", "this:" + thisClassloader + "---" + parentClassloader);
tmpClassloader = parentClassloader.getParent();
thisClassloader = parentClassloader;
parentClassloader = tmpClassloader;
}
Log.i("ClassLoaderTag", "root:" + thisClassloader);
}
// thisClassLoader: dalvik.system.PathClassLoader
// this:dalvik.system.PathClassLoader---java.lang.BootClassLoader@e2f9b5c
// root:java.lang.BootClassLoader@e2f9b5c

动态加载dex,调用dex的函数

1
2
3
4
5
6
7
import android.util.Log;

public class TestClass {
public void testFunc(){
Log.i("DexTestTag", "i am from com.example.test01.TestClass.testFunc");
}
}

设置让apk解压后只生成一个class.dex文件,在proguard-rules.pro中,可以添加如下配置:

1
2
3
-forceprocessing
-dontshrink
-dontoptimize

在 build.gradle 中添加:

1
2
3
4
5
6
android {
defaultConfig {
...
multiDexEnabled false
}
}

打包解压推送dex到sdcard下命名为1.dex

再新建个项目用于加载上面dex文件里面的testFunc函数,用DexClassLoader类去加载dex文件

1
2
3
4
5
// DexClassLoader方法参数:
// dexPath:目标所在的apk或者jar文件的路径,装载器将从路径中寻找指定的目标类。
// dexOutputDir:由于dex文件在apk或者jar文件中,所以在装载前面前先要从里面解压出dex文件,这个路径就是dex存放的路径,在android系统中,一个应用程序对应一个linux用户id,应用程序只对自己的数据目录有写的权限,所以存放在这个路径。
// libPath:目标类中使用的C/C++库。
// 最后一个参数是该装载器的父装载器,一般为当前类执行的装载器。
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
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Context appContext = this.getApplicationContext();
testDesClassLoader(appContext, "/sdcard/1.dex");
}

public void testDesClassLoader(Context context, String dexFilePath){
File optFile = context.getDir("opt_path", 0); // 用于存放优化的dex文件
File libFile = context.getDir("lib_path", 0); // 用于存放优化的dex文件
// ClassLoader parentClassLoader = MainActivity.class.getClassLoader(); // 获取当前的类加载器
// ClassLoader tmpClassLoader = context.getClassLoader(); // 获取当前的类加载器
// 指定了它的双亲为mainactivety的classloader,这样就可以访问到mainactivety的类
DexClassLoader dexClassLoader = new DexClassLoader(dexFilePath, optFile.getAbsolutePath(), libFile.getAbsolutePath(), MainActivity.class.getClassLoader()); // 创建类加载器
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.test01.TestClass"); // 加载类
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (clazz != null){
try {
Method testFuncMethod = clazz.getDeclaredMethod("testFunc"); // 通过反射获取方法
Object obj = clazz.newInstance(); // 创建对象
testFuncMethod.invoke(obj); // 调用方法
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
// I/DexTestTag: i am from com.example.test01.TestClass.testFunc

还要在AndroidMainfest.xml文件里添加读写sd卡的权限,并在手机设置给应用增加读取文件权限

1
2
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>

这样就实现了dex通过ClassLoader加载进行调用,并指定了MainActivity.class.getClassLoader(),也就是PathClassLoader作为父节点。

 评论