Android加壳与脱壳(二)加壳和运行时的机制
zsk Lv4

Android App启动流程

这里就先以一张图来说明这个过程
image

(1)点击桌面APP图标时,Launcher的startActivity()方法,通过Binder通信,调用system_server进程中AMS服务的startActivity方法,发起启动请求
(2)system_server进程接收到请求后,向Zygote进程发送创建进程的请求
(3)Zygote进程fork出App进程,并执行ActivityThread的main方法,创建ActivityThread线程,初始化MainLooper,主线程Handler,同时初始化ApplicationThread用于和AMS通信交互
(4)App进程,通过Binder向sytem_server进程发起attachApplication请求,这里实际上就是APP进程通过Binder调用sytem_server进程中AMS的attachApplication方法,AMS的attachApplication方法的作用是将ApplicationThread对象与AMS绑定
(5)system_server进程在收到attachApplication的请求,进行一些准备工作后,再通过binder IPC向App进程发送handleBindApplication请求(初始化Application并调用onCreate方法)和scheduleLaunchActivity请求(创建启动Activity)
(6)App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送BIND_APPLICATION和LAUNCH_ACTIVITY消息,这里注意的是AMS和主线程并不直接通信,而是AMS和主线程的内部类ApplicationThread通过Binder通信,ApplicationThread再和主线程通过Handler消息交互。
(7)主线程在收到Message后,创建Application并调用onCreate方法,再通过反射机制创建目标Activity,并回调Activity.onCreate()等方法
(8)到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染后显示APP主界面

寒冰大佬在FART:ART环境下基于主动调用的自动化脱壳方案一文中讲述了ActivityThread.main()是进入App世界的大门,并由此展开了对加壳原理的讲述
开始进行ActivityThread源码分析,了解ActivityThread的具体操作:
以下代码均出自安卓8.1,链接:http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/app/ActivityThread.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
6459    public static void main(String[] args) {
6460 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
6461
6462 // CloseGuard defaults to true and can be quite spammy. We
6463 // disable it here, but selectively enable it later (via
6464 // StrictMode) on debug builds, but using DropBox, not logs.
6465 CloseGuard.setEnabled(false);
6466
6467 Environment.initForCurrentUser();
6468
6469 // Set the reporter for event logging in libcore
6470 EventLogger.setReporter(new EventLoggingReporter());
6471
6472 // Make sure TrustedCertificateStore looks in the right place for CA certificates
6473 final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
6474 TrustedCertificateStore.setDefaultUserDirectory(configDir);
6475
6476 Process.setArgV0("<pre-initialized>");
6477
6478 Looper.prepareMainLooper();
6479
6480 ActivityThread thread = new ActivityThread();
6481 thread.attach(false);
6482
6483 if (sMainThreadHandler == null) {
6484 sMainThreadHandler = thread.getHandler();
6485 }
6486
6487 if (false) {
6488 Looper.myLooper().setMessageLogging(new
6489 LogPrinter(Log.DEBUG, "ActivityThread"));
6490 }
6491
6492 // End of event ActivityThreadMain.
6493 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
6494 Looper.loop();
6495
6496 throw new RuntimeException("Main thread loop unexpectedly exited");
6497 }

根据寒冰大佬描述,在ActivityThread完成实例化操作,调用thread.attach(false)完成一系列初始化准备工作,最后主线程进入消息循环,等待接收来自系统的消息。当收到系统发送来的bindapplication的进程间调用时,调用函数handlebindapplication来处理该请求。
可以查看handleMessage方法,查看参数为BIND_APPLICATION,即上面步骤第五步通过binder IPC向App进程发送handleBindApplication请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1580        public void handleMessage(Message msg) {
1581 if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
1582 switch (msg.what) {
......
1653 case BIND_APPLICATION:
1654 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
1655 AppBindData data = (AppBindData)msg.obj;
1656 handleBindApplication(data);
1657 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
1658 break;
......
1846 }
1847 Object obj = msg.obj;
1848 if (obj instanceof SomeArgs) {
1849 ((SomeArgs) obj).recycle();
1850 }
1851 if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
1852 }
1853 }

在处理消息过程,就进入了handlebindapplication函数
这里源代码有点长,我再用寒冰大佬文章的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void handleBindApplication(AppBindData data) {
//step 1: 创建LoadedApk对象
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
...
//step 2: 创建ContextImpl对象;
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);

//step 3: 创建Instrumentation
mInstrumentation = new Instrumentation();

//step 4: 创建Application对象;在makeApplication函数中调用了newApplication,
//在该函数中又调用了app.attach(context),在attach函数中调用了Application.attachBaseContext函数
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;

//step 5: 安装providers
List<ProviderInfo> providers = data.providers;
installContentProviders(app, providers);

//step 6: 执行Application.Create回调
mInstrumentation.callApplicationOnCreate(app);
...
}

定位第四步,Application进行实例化,然后进入makeApplication

image
进入983行newApplication函数

image

这里调用的时1084行的newApplication,但它又调用了1099行的newApplication
这里可以看到做了两件事:
(1)完成了Application的实例化
(2)并调用Application.attach()函数
继续进入Application.attach()函数

image

调用了Application.attachBaseContext函数,回到handleBindApplication,这里就执行完第四步
跟进第六步,进入callApplicationOnCreate函数
image

执行了Application.onCreate()方法
从上面可以看到大概的流程:
初始化—>Application的构造函数—>Application.attachBaseContext()—>Application.onCreate()
最后才会进入MainActivity中的attachBaseContext函数、onCreate函数


可以得出结论,app最先获得执行权限的是app中声明的Application类中的attachBaseContext和onCreate函数。因此,壳要想完成应用中加固代码的解密以及应用执行权的交付就都是在这两个函数上做文章。
image

下面这张图大致讲了加壳应用的运行流程。
image

当壳在函数attachBaseContext和onCreate中执行完加密的dex文件的解密后,通过自定义的Classloader在内存中加载解密后的dex文件。为了解决后续应用在加载执行解密后的dex文件中的Class和Method的问题,接下来就是通过利用java的反射修复一系列的变量。其中最为重要的一个变量就是应用运行中的Classloader,只有Classloader被修正后,应用才能够正常的加载并调用dex中的类和方法,否则的话由于Classloader的双亲委派机制,最终会报ClassNotFound异常,应用崩溃退出,这是加固厂商不愿意看到的。由此可见Classloader是一个至关重要的变量,所有的应用中加载的dex文件最终都在应用的Classloader中。

整体加壳原理

Dex整体加壳可以理解为在加密的源Apk程序外面有套上了一层外壳,简单过程为:
image

简要概括:壳dex读取源dex文件,通过加密后写进一个新的dex文件,就成为新的APK

如何对App进行加一层外壳呢,这里就需要应用动态加载的原理,关于动态加载和类加载器,可以看下上一篇文章Android加壳与脱壳(一)ClassLoader和动态加载
大概的流程就是

(1)BootClassLoader加载系统核心库
(2)PathClassLoader加载APP自身dex
(3)进入APP自身组件,解析AndroidManifest.xml,然后查找Application代理
(4)调用声明Application的attachBaseContext()对源程序进行动态加载或解密
(5)调用声明Application的onCreate()对源程序进行动态加载或解密
(6)进入MainActivity中的attachBaseContext(),然后进入onCreate()函数,执行源程序代码

上面文章也说到:

Android中的ClassLoader类型分为系统ClassLoader和自定义ClassLoader。其中系统ClassLoader包括3种是BootClassLoader、DexClassLoader、PathClassLoader
(1)BootClassLoader:Android平台上所有Android系统启动时会使用BootClassLoader来预加载常用的类
(2)BaseDexClassLoader:实际应用层类文件的加载,而真正的加载委托给pathList来完成
(3)DexClassLoader:可以加载dex文件以及包含dex的压缩文件(apk,dex,jar,zip),可以安装一个未安装的apk文件,一般为自定义类加载器
(4)PathClassLoader:可以加载系统类和应用程序的类,通常用来加载已安装的apk的dex文件
补充:
Android 提供的原生加载器叫做基础类加载器,包括:BootClassLoader,PathClassLoader,DexClassLoader,InMemoryDexClassLoader(Android 8.0 引入),DelegateLastClassLoader(Android 8.1 引入)

DexClassLoader加载的类是没有组件生命周期的,例如Activity。Android 生命周期中的方法(如 onCreate(), onStart(), onResume(), 等)由系统在特定情况下自动调用。直接通过类加载器加载这些类并调用这些方法不会触发正确的生命周期行为,也不会被系统管理。
两种解决方法:

  1. 替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoader的parent为系统组件类加载器。
  2. 打破原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入我们自己的DexClassLoader即可。

第一种:类加载器替换

怎么去替换系统的类加载器,这就和上面分析的ActivityThread中LoadedApk有关了,LoadedApk主要负责加载一个Apk程序,我们进一步分析源码
image

在LoadedApk里面就定义了ClassLoader即mClassLoader,可以通过反射获取mclassLoader,然后使用我们的DexClassLoader进行替换。

总结:
(1)获取ActivityThread实例
(2)通过反射获取类加载器
(3)获取LoadedApk
(4)获取mClassLoader系统类加载器
(5)替换自定义类加载器为系统类加载器

大概的图如下,Android 应用的组件mclassLoader是由PathClassLoader加载的,要把mclassLoader替换成DexClassLoader就可以加载自定义的dex文件
image

先新建项目loaddextest编写一个activity类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.loaddextest;

import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;

public class TestActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i("zskkk", "i am from com.example.loaddextest.TestActivity.onCreate");
}
}

设置让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下命名为2.dex

在新项目下AndroidMainfest.xml添加读写sdcard的权限,并在application下添加TestActivity

1
2
3
4
5
6
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<application
...
<activity android:name="com.example.loaddextest.TestActivity">
</application>

在MainActivity下编写如下代码:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package com.example.loaddex;

import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import java.io.File;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
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"); // 加载普通类
staticTestActivityFirstMethod(appContext, "/sdcard/2.dex"); // 加载组件类
Log.i("zskkk", String.valueOf(MainActivity.class.getClassLoader()));
}

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.loaddextest.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();
}
}
}

public void replaceClassLoader(Context context, ClassLoader dexClassLoader) {
ClassLoader classLoader = MainActivity.class.getClassLoader();
try {
// 1. 获取ActivityThread的实例对象
Class<?> ActivityThreadClazz = classLoader.loadClass("android.app.ActivityThread");
Method currentActivityThread = ActivityThreadClazz.getDeclaredMethod("currentActivityThread");
Object activityThreadObj = currentActivityThread.invoke(null);
// 2. 通过反射获得类加载器
// final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
Field mPackagesField = ActivityThreadClazz.getDeclaredField("mPackages");
mPackagesField.setAccessible(true); // 设置可访问
// 3. 拿到LoaderApk
ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj); // 获取了 ActivityThread 类中名为 mPackages 的字段的值,该字段是一个 ArrayMap 类型的对象,用于存储应用程序包名与 LoadedApk 对象的映射关系
String packageName = context.getPackageName(); // 获取当前应用的包名。
WeakReference wr = (WeakReference) mPackagesObj.get(packageName);
Object LoadApkObj = wr.get();
// 4.拿到mclassLoader
Class LoadedApkClass = classLoader.loadClass("android.app.LoadedApk");
// private ClassLoader mClassLoader;
Field mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
// Object mClassLoader = mClassLoaderField.get(LoadApkObj); // 获取 LoadedApk 对象 LoadApkObj 中的 mClassLoader 字段值:
// Log.e("mClassLoader",mClassLoader.toString());
// 5.将系统组件ClassLoader给替换
mClassLoaderField.set(LoadApkObj, dexClassLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}

public void staticTestActivityFirstMethod(Context context, String dexFilePath){
File optFile = context.getDir("opt_dex", 0);
File libFile = context.getDir("lib_path", 0);
DexClassLoader dexClassLoader = new DexClassLoader(dexFilePath, optFile.getAbsolutePath(), libFile.getAbsolutePath(), MainActivity.class.getClassLoader()); // 创建类加载器
replaceClassLoader(context, dexClassLoader);
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.loaddextest.TestActivity"); // 加载类
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
context.startActivity(new Intent(context, clazz));
}
}
// com.example.loaddex I/zskkk: dalvik.system.PathClassLoader
// com.example.loaddex I/zskkk: i am from com.example.loaddextest.TestActivity.onCreate

第二种:类加载器插入

还有一种方案,动态加载中我们讲述了类加载器的双亲委派机制,就是说我们的类加载器刚拿到类,并不会直接进行加载,而是先判断自己是否加载,如果没有加载则给自己的父类,父类再给父类,所以我们让DexClassLoader成为PathClassLoader的父类,这样就可以解决DexClassLoader生命周期的问题
大概的图如下:
image

总结:
(1)将DexClassloader父节点设置为BootClassLoader
(2)将PathClassLoader父节点设置为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 void staticTestActivitySecondMethod(Context context, String dexFilePath){
File optFile = context.getDir("opt_dex", 0);
File libFile = context.getDir("lib_path", 0);
ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
ClassLoader bootClassLoader = MainActivity.class.getClassLoader().getParent();
DexClassLoader dexClassLoader = new DexClassLoader(dexFilePath, optFile.getAbsolutePath(), libFile.getAbsolutePath(), bootClassLoader); // 创建类加载器
try {
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(pathClassLoader, dexClassLoader);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.loaddextest.TestActivity"); // 加载类
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
context.startActivity(new Intent(context, clazz));
}

加壳案例实现

1. 编写源程序
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.loaddextest;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i("zskkk", "i am a source dex");
}
}

这就是我们的源程序,源程序运行,我们会在日志中看见我们打印的信息,然后我们生成dex文件 。
先打包成apk解压得到dex文件,push到sdcard。

2. 编写壳程序

编写代理类模仿上面的加壳,在attachBaseContext或onCreate中对我们的dex进行动态加载和类加载器修正即可,因为这里我们源dex并未进行加密,所以也无需解密的过程

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
package com.example.loaddex;

import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.util.ArrayMap;
import android.util.Log;
import java.io.File;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;

public class StubApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
String dexFilePath = "/sdcard/3.dex";
File optFile = base.getDir("opt_dex", 0);
File libFile = base.getDir("lib_path", 0);
ClassLoader parentClassLoader = StubApplication.class.getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(dexFilePath, optFile.getAbsolutePath(), libFile.getAbsolutePath(), StubApplication.class.getClassLoader()); // 创建类加载器
// 使用上面的一种方法进行类加载器修正
replaceClassLoader(this, dexClassLoader);
try {
Class<?> activityClass = dexClassLoader.loadClass("com.example.loaddextest.MainActivity");
startActivity(new Intent(StubApplication.this, activityClass));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public void replaceClassLoader(Context context, ClassLoader dexClassLoader){
...
}
@Override
public void onCreate() {
super.onCreate();
}
}

在新建项目的AndroidMainfest.xml添加读写sdcard的权限,还有在application下代理类别,加入导入类的Activity

1
2
3
4
5
6
7
8
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
...
<application
android:name=".StubApplication"
...
<activity android:name="com.example.loaddextest.MainActivity"></activity>
</application>

image

运行成功,说明我们的整体加壳成功

 评论