-
-
[原创]加壳脱壳知识点总结--Dex文件加载流程及脱壳实战
-
发表于: 2024-5-31 22:41 28899
-
1.简述
总结记录下App启动流程中Dex动态加载流程以及相关知识,加深对App加壳以及脱壳原理的理解。
环境:Android 8.0.0
2.相关知识总结
1. App启动流程
Zygote fork之后会在新进程中调用main()方法,然后 main() 方法会创建 ActivityThread 实例,并调用其 main() 方法,从而启动应用程序的主线程,这里也就是App相关的步骤,所以直接从ActiveThread开始说起。
ActivityThread 是 Android 系统中的一个核心类,负责管理应用程序的主线程以及应用程序中的各种组件(比如 Activity、Service、BroadcastReceiver 等)的生命周期、消息循环、消息处理等。
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
|
public static void main(String[] args) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain" );
SamplingProfilerIntegration.start();
/ / CloseGuard defaults to true and can be quite spammy. We
/ / disable it here, but selectively enable it later (via
/ / StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);
Environment.initForCurrentUser();
/ / Set the reporter for event logging in libcore
EventLogger.setReporter(new EventLoggingReporter());
/ / Make sure TrustedCertificateStore looks in the right place for CA certificates
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);
Process.setArgV0( " );
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler = = null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread" ));
}
/ / End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException( "Main thread loop unexpectedly exited" );
}
|
源码中这两句是比较关键的代码,第一句实例化ActivityThread类,之后调用attach方法,进行应用的初始化工作。
1
2
|
ActivityThread thread = new ActivityThread();
thread.attach(false);
|
一些初始化工作可能依赖于系统的消息或事件的处理结果。例如,创建 Application 实例可能需要获取系统的一些状态或资源,所以在attach方法中初始化工作做好之后会进入消息循环等待,当系统发来消息就开始时调用handlebindApplication开始初始化。
这里分辨一下ActityThread.attach和handlebindApplication初始化的区别ActivityThread.attach() 主要负责初始化应用程序的运行环境和关键对象,而不是直接加载应用程序的组件(如 Activity)。handleBindApplication() 方法则更侧重于实际绑定应用程序,初始化应用程序的 Application、Activity、Service 等组件,并启动应用程序的主 Activity,所以说到了handlebindApplication这一步就App内的代码开始起作用了。
对于了解加壳脱壳来说handlebindApplication方法中的这两行代码比较关键,第一行最终会通过LoadApk类中的newApplication创建Application对象并调用attachBaseContext方法,第二行调用Application的OnCreate方法,这俩个方法也是App代码中最先运行的两个方法。
1
2
|
Application App = data.info.makeApplication(data.restrictedBackupMode, null);
mInstrumentation.callApplicationOnCreate(App);
|
到这里App开始调用第一个Activity的OnCreate方法。
2. 双亲委派和类加载器
先介绍一下安卓中的四个类加载器
- PathClassLoader:
PathClassLoader 是 Android 应用程序的默认类加载器,用于加载应用程序 APK 文件中的类和资源。
它负责加载应用程序的 Dex 文件(即 APK 文件中的 classes.Dex)中的类。 - DexClassLoader:
DexClassLoader 用于从外部 Dex 文件加载类和资源。
它可以加载存储在文件系统上的 Dex 文件,并从中加载类。 - InMemoryDexClassLoader:
InMemoryDexClassLoader 是安卓8.0之后添加的一个类加载器,用于从内存中加载 Dex 字节码数据,并生成对应的 Class 对象。 - BootClassLoader:
BootClassLoader 是 Android 系统的引导类加载器,也是类加载器层次结构的根加载器,负责加载 Android 系统的核心类库。
它负责加载 Android 系统的核心类,如 java.lang 包中的类等。
再说双亲委派,双亲委派是当一个类加载器收到加载类的请求时使用的机制,这里借用一张图说明。
c04K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0L8r3!0#2k6q4)9J5k6i4c8W2L8X3y4W2L8Y4c8Q4x3X3g2U0L8$3#2Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3x3U0t1#2x3e0t1#2x3R3`.`.
总结就是两步,第一步当类加载器加载一个类时如果没有找到就会向上询问父类加载器是否加载过没有就继续向上直到BootClassLoader,这里到了第二步,如果BootClassLoader也没有加载过,BootClassLoader自己如果可以加载,则BootClassLoader会直接加载,如果不可以会一路向下执行这样的判断,直到最开始的子加载器。
这里要注意加载器的父子关心并不是类的继承父子关系。
3. 整体加壳实现方式
整体加壳的实现,原Dex会被加密保存在某个地方,那一定有一个原Dex加解密的过程,那这个过程一定是在App自身逻辑代码运行之前,所以根据对App启动流程的分析原Dex的解密时机在Application的attachBaseContext方法,OnCreate方法调用的时候,壳代码一般会重写这两个方法,这里可以看一个例子,壳代码继承了Application并重写了两个方法。
除了加原Dex解密之外,还有一个重要的步骤就是加载解密后的Dex替换壳Dex。这一步就涉及Dex的加载以及双亲委派机制了。
一个App正常启动后加载Dex文件使用的是默认类加载器PathClassLoader,这个加载其不能用于加载外部Dex,通过之前的介绍,壳通常用的是DexClassLoader和InMemoryDexClassLoader来进行解密后的Dex加载,假设我们的代码是这样的。
1
2
3
4
5
6
7
8
|
dexClassLoader = new DexClassLoader( "/sdcard/4.dex" , context.getApplicationContext().getCacheDir().getAbsolutePath(), null, pathClassloader);
try {
Class TestActivityClass = dexClassLoader.loadClass( "com.kanxue.test02.TestActivity" );
Log.e( "class" , TestActivityClass.toString());
context.startActivity(new Intent(context, TestActivityClass));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
|
这样运行后会报错,ClassNotFound,这里的话引出另一个问题这两个类加载的Dex,仅仅只是加载了一个普通的类进来,并没有生命周期,这样的话运行起来就会报错ClassNotFound,对于这个为什么没有生命周期这个问题,我找了一些startActivity调用流程的文章最终发现了可能的原因,以下这段话来自ChatGPT
1 |
当你调用 startActivity() 启动一个 Activity 时,实际上是向系统发送了一个启动 Activity 的请求,系统会通过 ActivityThread 来处理这个请求。在 ActivityThread 中,当收到启动 Activity 的请求时,会调用 handleLaunchActivity() 方法来处理,而在 handleLaunchActivity() 方法中会最终调用 performLaunchActivity() 方法来执行 Activity 的启动流程 |
也就是说startActivity方法调用后会执行performLaunchActivity方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/ frameworks / base / core / java / android / app / ActivityThread.java:
performLaunchActivity()部分代码
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
... try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state ! = null) {
r.state.setClassLoader(cl);
}
}
...
}
|
这段代码主要调用newActivity创建一个Activity实例,当Activity 实例对象被添加到 Activity 栈中,并由系统管理时,生命周期就会正常触发。
1
2
3
4
5
6
|
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
|
再看一下newActivity的代码,使用了传入的ClassLoader来加载类
这里调用getClassLoader获取系统默认的PathClassLoader,所以传入的是PathClassLoader,PathClassLoader是DexClassLoader和InMemoryDexClassLoader的父加载器,所以按双亲委派的流程是会报错的。
这里解决方法有很多种,以为例DexClassLoader这里列三种(1)替换PathClassLoader(2)改变父子加载器关系把DexClassLoader改为PathClassLoader的父加载器。(3)合并PathClassLoader和DexClassLoader中的dexElements数组。
3.两种动态加载方式源码分析
接下来是DexClassLoader和InMemoryDexClassLoader两种动态加载方式的源码分析这里就和脱壳有关了。
1.DexClassLoader
DexClassLoader
1
2
3
4
|
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super (dexPath, new File (optimizedDirectory), librarySearchPath, parent);
}
|
BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent)
1
2
3
4
5
6
7
8
9
|
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super (parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
if (reporter ! = null) {
reporter.report(this.pathList.getDexPaths());
}
}
|
public DexPathList(ClassLoader definingContext, String dexPath,String librarySearchPath, File optimizedDirectory)
1
2
3
4
5
6
|
public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
... this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext);
... } |
makeDexElements
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
private static Element[] makeDexElements( List < File > files, File optimizedDirectory,
309 List
310 Element[] elements = new Element[files.size()];
311 int elementsPos = 0 ;
312 / *
313 * Open all files and load the (direct or contained) dex files up front.
314 * /
315 for ( File file : files) {
316 if ( file .isDirectory()) {
317 / / We support directories for looking up resources. Looking up resources in
318 / / directories is useful for running libcore tests.
319 elements[elementsPos + + ] = new Element( file );
320 } else if ( file .isFile()) {
321 String name = file .getName();
322 323 if (name.endsWith(DEX_SUFFIX)) {
324 / / Raw dex file ( not inside a zip / jar).
325 try {
326 DexFile dex = loadDexFile( file , optimizedDirectory, loader, elements);
... } |
loadDexFile(File file, File optimizedDirectory, ClassLoader loader,Element[] elements)
1
2
3
4
5
6
7
8
9
|
private static DexFile loadDexFile( File file , File optimizedDirectory, ClassLoader loader,Element[] elements)
throws IOException {
if (optimizedDirectory = = null) {
return new DexFile( file , loader, elements);
} else {
String optimizedPath = optimizedPathFor( file , optimizedDirectory);
return DexFile.loadDex( file .getPath(), optimizedPath, 0 , loader, elements);
}
}
|
DexFile loadDex(String sourcePathName, String outputPathName,int flags, ClassLoader loader, DexPathList.Element[] elements)
1
2
3
4
5
|
static DexFile loadDex(String sourcePathName, String outputPathName, int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
}
|
DexFile(String sourceName, String outputName, int flags, ClassLoader loader,DexPathList.Element[] elements)
1
2
3
4
5
6
7
8
|
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
DexPathList.Element[] elements) throws IOException {
... mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
mInternalCookie = mCookie;
mFileName = sourceName;
/ / System.out.println( "DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}
|