App 进程的创建流程如下
从上图我们可以看到 ActivityThread.main() 是进入 App 世界的大门,在 Android 源码 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {
@UnsupportedAppUsage
private static volatile ActivityThread sCurrentActivityThread;
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
// Install selective syscall interception
AndroidOs.install();
// 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();
// Make sure TrustedCertificateStore looks in the right place for CA certificates
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);
// Call per-process mainline module initialization.
initializeMainlineModules();
Process.setArgV0("<pre-initialized>");
Looper.prepareMainLooper();
// Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
// It will be in the format "seq=114"
long startSeq = 0;
if (args != null) {
for (int i = args.length - 1; i >= 0; --i) {
if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
startSeq = Long.parseLong(
args[i].substring(PROC_START_SEQ_IDENT.length()));
}
}
}
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
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 这个类,其中的静态变量 sCurrentActivityThread 用于全局保存创建的 ActivityThread 实例,同时还提供了静态方法 currentActivityThread() 用于获取当前虚拟机创建的 ActivityThread 实例。ActivityThread.main() 是 Java 中的入口 main 方法,这里会启动主消息循环,并创建 ActivityThread 实例,之后调用 thread.attach(false) 完成一系列初始化准备工作,在 attach() 方法中会完成全局静态变量 sCurrentActivityThread 的初始化。之后主线程进入消息循环,等待接收来自系统的消息。当收到系统发送来的 BIND_APPLICATION 消息时,调用方法 handleBindApplication() 来处理该请求。
◎ 调用 handleBindApplication()
handleBindApplication() 的功能是启动一个 application,并把系统收集的 apk 组件等相关信息绑定到 application 里,在创建完 application 对象后,接着调用了 application 的 attachBaseContext 方法,之后调用 application 的 onCreate 方法。由此可以发现,app 的 Application 类中的 attachBaseContext 和 onCreate 这两个方法是最先获取执行权进行代码执行的。
◎ APP 运行流程
在前面的内容中我们可以得到结论,app 最先获得执行权限的是 app 中声明的 Application 类中的 attachBaseContext 和 onCreate 方法。因此,壳要想完成应用中加固代码的解密以及应用执行权的交付就都是在这两个方法上做文章。
当壳在方法 attachBaseContext 和 onCreate 中执行完成对加密 dex 文件的解密操作后,通过自定义的 ClassLoader 在内存中加载解密后的 dex 文件。为了解决后续应用在加载执行解密后的 dex 文件中的 Class 和 Method 的问题,接下来就是通过利用 Java 的反射修复一系列的变量。其中最为重要的一个变量就是应用运行中的 ClassLoader,只有 ClassLoader 被修正后,应用才能够正常地加载并调用 dex 中的类和方法,否则由于 ClassLoader 的双亲委派机制,最终会报 ClassNotFound 异常,导致应用崩溃退出。
动态加载就是用到的时候再去加载,也叫懒加载,意味着用不到的时候是不会去加载的。动态加载是 dex 加壳、插件化、热更新的基础。
动态加载的 dex 不具有生命周期特征,app 中的 Activity、Service 等组件无法正常工作,只能完成一般方法的调用。因此需要对 ClassLoader 进行修正,app 才能正常运行。下面我们将通过三种方式进行修正。
新建一个项目,此处包名为 dev.svip.mydex,并新建一个 TestActivity 类。用于模拟生成被加载的 dex 文件。代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package dev.svip.mydex;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
Log.e("blog.svip.dev", "I am from TestActivity.onCreate");
}
}
|
新建一个 TestClass 类,代码如下
1
2
3
4
5
6
7
8
9
|
package dev.svip.mydex;
import android.util.Log;
public class TestClass {
public static void testFunc(){
Log.e("blog.svip.dev", "I am from TestClass.testFunc");
}
}
|
编译项目,解压生成的 apk 文件,将 classes.dex 上传到手机中,此处上传至 /sdcard/svip/my.dex
另外新建一个项目,首先获取文件的读写权限,因为我使用的测试手机为 Android 12,所以使用以下代码获取管理所有文件的权限。
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
|
@RequiresApi(api = Build.VERSION_CODES.R)
public void getPermission() {
if (Environment.isExternalStorageManager()) {
Toast toast = Toast.makeText(this, "有权限", Toast.LENGTH_SHORT);
toast.show();
} else {
Toast toast = Toast.makeText(this, "没有权限", Toast.LENGTH_SHORT);
toast.show();
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + this.getPackageName()));
ActivityResultLauncher<Intent> setPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (Environment.isExternalStorageManager()) {
Toast getPermissionSucceed = Toast.makeText(MainActivity.this, "获取权限成功!", Toast.LENGTH_SHORT);
getPermissionSucceed.show();
} else {
Toast getPermissionSucceed = Toast.makeText(MainActivity.this, "获取失败!", Toast.LENGTH_SHORT);
getPermissionSucceed.show();
}
}
);
setPermissionLauncher.launch(intent);
}
}
|
同时,在 AndroidManifest.xml 中添加需要的权限声明。
1
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
|
另外,在 AndroidManifest.xml 中添加 Activity
1
2
3
4
5
6
7
|
<application
...
<activity
android:name="dev.svip.mydex.TestActivity"
tools:ignore="MissingClass">
</activity>
</application>
|
替换系统组件类加载器为我们的 DexClassLoader,同时设置 DexClassLoader 的 parent 为系统组件类加载器。
Android 源码中相关部分如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//frameworks/base/core/java/android/app/ActivityThread.java
public final class ActivityThread extends ClientTransactionHandler
implements ActivityThreadInternal {
private static volatile ActivityThread sCurrentActivityThread;
final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
public static ActivityThread currentActivityThread() {
return sCurrentActivityThread;
}
}
//frameworks/base/core/java/android/app/LoadedApk.java
public final class LoadedApk {
private ClassLoader mClassLoader;
}
|
参考以上内容,通过反射,将 mClassLoader 的值修改为自己的 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public static void replaceClassLoader(ClassLoader classLoader) {
ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
try {
assert pathClassLoader != null;
@SuppressLint("PrivateApi")
Class<?> ActivityThreadClass = pathClassLoader.loadClass("android.app.ActivityThread");
@SuppressLint("DiscouragedPrivateApi")
Method currentActivityThread = ActivityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThread.setAccessible(true);
Object sCurrentActivityThread = currentActivityThread.invoke(null);
@SuppressLint("DiscouragedPrivateApi")
Field mPackages = ActivityThreadClass.getDeclaredField("mPackages");
mPackages.setAccessible(true);
ArrayMap<?, ?> mPackagesObj = (ArrayMap<?, ?>) mPackages.get(sCurrentActivityThread);
String packageName = Objects.requireNonNull(MainActivity.class.getPackage()).getName();
assert mPackagesObj != null;
WeakReference<?> weakReference = (WeakReference<?>) mPackagesObj.get(packageName);
assert weakReference != null;
Object loadedApk = weakReference.get();
@SuppressLint("PrivateApi")
Class<?> LoadedApkClass = pathClassLoader.loadClass("android.app.LoadedApk");
@SuppressLint("DiscouragedPrivateApi")
Field mClassLoader = LoadedApkClass.getDeclaredField("mClassLoader");
mClassLoader.setAccessible(true);
mClassLoader.set(loadedApk, classLoader);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
}
public static void startActivityFirstMethod(Context context) {
ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
@SuppressLint("SdCardPath")
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/svip/my.dex", context.getApplicationContext().getCacheDir().getAbsolutePath(), null, pathClassLoader);
replaceClassLoader(dexClassLoader);
try {
Class<?> TestActivityClass = dexClassLoader.loadClass("dev.svip.mydex.TestActivity");
Log.e("blog.svip.dev", "class: " + TestActivityClass.toString());
context.startActivity(new Intent(context, TestActivityClass));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
|
◎ 运行结果
可见已成功加载 dex 文件中的 Activity 并执行了 onCreate 方法。
打破原有的双亲关系,在系统组件类加载器和 BootClassLoader 之间插入我们自己的 DexClassLoader。
Android 源码中相关部分如下
1
2
3
4
|
//libcore/ojluni/src/main/java/java/lang/ClassLoader.java
public abstract class ClassLoader {
private final ClassLoader parent;
}
|
参考以上内容,我们修改 parent 属性即可修改 ClassLoader 之间的关系,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@SuppressLint("SdCardPath")
public static void startActivitySecondMethod(Context context) {
ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
assert pathClassLoader != null;
ClassLoader bootClassLoader = pathClassLoader.getParent();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/svip/my.dex", context.getApplicationContext().getCacheDir().getAbsolutePath(), null, bootClassLoader);
try {
@SuppressLint("DiscouragedPrivateApi")
Field parent = ClassLoader.class.getDeclaredField("parent");
parent.setAccessible(true);
parent.set(pathClassLoader, dexClassLoader);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
try {
Class<?> TestActivityClass = dexClassLoader.loadClass("dev.svip.mydex.TestActivity");
Log.e("blog.svip.dev", "class: " + TestActivityClass.toString());
context.startActivity(new Intent(context.getApplicationContext(), TestActivityClass));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
|
该方法理论上应该没有问题,但是我在手机(Android 12)上测试报错未成功,因为疫情居家,另一个测试手机在公司,后续再进行排错。
对 PathClassLoader 中的 dexElements 进行合并。
Android 源码中相关代码
1
2
3
4
5
6
7
8
9
10
11
12
|
// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
}
// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public final class DexPathList {
private Element[] dexElements;
static class Element {
private final DexFile dexFile;
}
}
|
参考以上内容,我们首先获取到 PathClassLoader 和 DexClassLoader 中 dexElements 的值,然后将它们进行合并,赋值给 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
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
|
public static Object getDexElementsInClassLoader(ClassLoader classLoader) {
try {
Class<?> BaseDexClassLoaderClass = classLoader.loadClass("dalvik.system.BaseDexClassLoader");
@SuppressLint("DiscouragedPrivateApi")
Field pathList = BaseDexClassLoaderClass.getDeclaredField("pathList");
pathList.setAccessible(true);
Object pathListObj = pathList.get(classLoader);
Class<?> DexPathListClass = classLoader.loadClass("dalvik.system.DexPathList");
@SuppressLint("DiscouragedPrivateApi")
Field dexElements = DexPathListClass.getDeclaredField("dexElements");
dexElements.setAccessible(true);
return dexElements.get(pathListObj);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
public static Object combineDexElements(Object dexElements1, Object dexElements2) {
int length1 = Array.getLength(dexElements1);
int length2 = Array.getLength(dexElements2);
int length = length1 + length2;
Object result = Array.newInstance(Objects.requireNonNull(dexElements1.getClass().getComponentType()), length);
for (int i = 0; i < length; i++) {
if (i < length1) {
Array.set(result, i, Array.get(dexElements1, i));
} else {
Array.set(result, i, Array.get(dexElements2, i - length1));
}
}
return result;
}
public static void setDexElementsInClassLoader(ClassLoader classLoader, Object newDexElements) {
try {
Class<?> BaseDexClassLoaderClass = classLoader.loadClass("dalvik.system.BaseDexClassLoader");
@SuppressLint("DiscouragedPrivateApi")
Field pathList = BaseDexClassLoaderClass.getDeclaredField("pathList");
pathList.setAccessible(true);
Object pathListObj = pathList.get(classLoader);
Class<?> DexPathListClass = classLoader.loadClass("dalvik.system.DexPathList");
@SuppressLint("DiscouragedPrivateApi")
Field dexElements = DexPathListClass.getDeclaredField("dexElements");
dexElements.setAccessible(true);
dexElements.set(pathListObj, newDexElements);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public static void startActivityThirdMethod(Context context) {
ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
assert pathClassLoader != null;
ClassLoader bootClassLoader = pathClassLoader.getParent();
@SuppressLint("SdCardPath")
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/svip/my.dex", context.getApplicationContext().getCacheDir().getAbsolutePath(), null, bootClassLoader);
Object dexElements1 = getDexElementsInClassLoader(pathClassLoader);
Object dexElements2 = getDexElementsInClassLoader(dexClassLoader);
Object newDexElements = combineDexElements(dexElements1, dexElements2);
setDexElementsInClassLoader(pathClassLoader, newDexElements);
try {
Class<?> TestActivityClass = pathClassLoader.loadClass("dev.svip.mydex.TestActivity");
Log.e("blog.svip.dev", "classname is " + TestActivityClass.toString());
context.startActivity(new Intent(context, TestActivityClass));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
|
◎ 运行结果
前面我们提到,动态加载的 dex 不具有生命周期特征,在不修正 ClassLoader 的情况下,只能正常调用普通的方法,调用代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static void loadDex(Context context) {
ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
@SuppressLint("SdCardPath")
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/svip/my.dex", context.getApplicationContext().getCacheDir().getAbsolutePath(), null, pathClassLoader);
try {
Class<?> TestClass = dexClassLoader.loadClass("dev.svip.mydex.TestClass");
Log.e("blog.svip.dev", "class: " + TestClass.toString());
Method testFuncMethod = TestClass.getDeclaredMethod("testFunc");
testFuncMethod.setAccessible(true);
testFuncMethod.invoke(null);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
|
◎ 运行结果
1、ActivityThread.java
2、ART环境下基于主动调用的自动化脱壳方案