加壳APP运行流程和ClassLoader修正
本章内容主要基于分析Android 8.0源码,来学习APP的启动流程和APP加壳原理以及运行流程,以及写代码怎么去从sd卡加载其它dex中的Activity,不涉及加密解密的操作。
app启动流程
- 通过Zygote进程最终进入到app进程的世界,ActivityThread.main()是进入app进程世界的大门。只有通过这个函数之后,我们才进入到一个加壳app自己的代码当中。
源码网址:
ActivityThread的源代码
ActivityThread的源代码在frameworks当中
通过Zygote进程最终进入到APP进程的世界,ActivityThread.main()是进入app进程世界的大门。只有通过这个函数之后,我们才进入到一个加壳app自己的代码当中。ActivityThread还有一个静态函数currentActivityThread那我们就可以通过它来获取到进程当中的ActivityThread实例进而获取到Activity当中的一些很重要的变量。
对于ActivityThread这个类,其中的sCurrentActivityThread静态变量用于全局保存创建的ActivityThread实例,
javaprivate static volatile ActivityThread sCurrentActivityThread;
同时还提供了
public static ActivityThread currentActivityThread()
这个静态函数用于获取当前虚拟机创建的ActivityThread实例。ActivityThread.main()函数是java中app启动的入口main()函数,这里会启动主消息循环,并创建ActivityThread实例,之后调用thread.attach(false)完成一系列初始化准备工作,并完成全局静态变量sCurrentActivityThread的初始化。private void attach(boolean system) { sCurrentActivityThread = this; ... } -------------------------------------------------- public static void main(String[] args) { ... // 创建Looper对象,创建MessageQueue对象 Looper.prepareMainLooper(); // 创建自己的ActivityThread对象 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"); }
之后主线进程进入消息循环,等待接收来自系统的消息。当收到系统发送来的bindapplication的进程间调用时,调用函数handlebindapplication来处理该请求
public void handleMessage(Message msg) { ... // 处理bindApplication调用 case BIND_APPLICATION: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication"); AppBindData data = (AppBindData)msg.obj; handleBindApplication(data); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); ... } ------------------------------------------------------------- // 定义不可修改&继承的LoadedApk对象 public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo) { return getPackageInfo(ai, compatInfo, null, false, true, false); } ------------------------------------------------------------- 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);
在handleBindApplication函数中第一次进入了app的代码世界,该函数功能启动一个application,并把系统收集到的apk组件等相关信息绑定到application里,在创建完application对象后,接着调用了application的attachBaseContext方法,之后调用了application的onCreate函数。由此可以发现,app的Application类中的attachBaseContext和onCreate这两个函数是最先获取执行权进行代码执行的。这也是为什么各家的加固工具的主要逻辑都是通过替换app入口Application,并亲自实现这两个函数,在这两个函数中进行代码的脱壳以及执行权交付的原因。
APP加壳原理以及运行流程
从前面的分析可以得出一个结论,app最先获得执行权限的是app中声明的Application类中的attachBaseContext和onCreate函数。因此,壳想要完成应用中加固代码的解密以及应用执行权的交付就都是在这两个函数上做文章。下图大致讲了加壳应用的运行流程。
当壳函数attachBaseContext和onCreate中执行完加密的dex文件的解密后,通过自定义的ClassLoader在内存中加载解密后的dex文件。为了解决后续应用在加载执行解密后的dex文件中的Class和Method的问题,接下来就是通过利用java的反射修复一系列需要用到的变量。其中最为重要的一个变量就是应用运行中的ClassLoader,只有ClassLoader被修正后,应用才能够正常的加载并调用dex中的类和方法,否则的话由于ClassLoader的双亲委派机制,最终会报ClassnotFound异常,应用崩溃退出。ClassLoader是一个至关重要的变量,所有的应用中加载的dex文件最终都在app的ClassLoader中。
因此,只要获取到加固应用最终通过反射设置后的ClassLoader,我们就可以通过一系列反射最终获取到当前应用所加载的解密后的内存中的Dex文件。
随着加壳技术的发展,为了对抗dex整体加固容易被内存dump来得到原始dex的问题,各加固厂商又结合hook技术,通过hook dex文件中类和方法加载执行过程中的关键流程,来实现在函数执行前才进行解密操作的指令抽取的解决方案。此时,就算是对内存中的dex整体进行了dump,但是由于其方法的最为重要的函数体中的指令被加密,导致无法对相关的函数进行脱壳。由此Fupk3诞生了,该脱壳工具通过欺骗壳而主动调用dex中的各个函数,完成调用流程,让壳主动解密对应method的指令区域,从而完成对指令抽取型壳的脱壳。
分析源码并写代码加载其它dex中Activity
先通过反射获取到ActivityThread,它是一个单例模式,可以获取到app进程当中仅有的ActivityThread的实例
接下来可以通过反射获取到mPackages的ArrayMap对象(这里的ArrayMap用于存储已加载的应用程序包的信息)
通过mPackages获取到loadApk:(LoadedApk是Android中表示已加载应用程序包信息的类,它包含了应用程序的资源、类加载器、类信息等,其中就保存有我们加载app组件的mClassLoader)
通过LoadedApk获取到mClassLoader字段(这就是接下来app运行过程中用于加载相关的四大组件的这些类的classLoader)
接下来写代码去加载dex中的Activity
如果继续按照上一节相同的代码去加载Activity的话会出现找不到dex中的方法的问题,无法启动我们的目标Activity
这个问题有两个解决方案:
替换系统组件的类加载器为我们的DexClassloader,同时设置DexClassLoader的parent节点为系统组件类加载器也就是pathClassLoader
打破原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入我们自己的DexClassLoader
这种方法和双亲委派密切相关,不替换原来组件的mClassLoader,保持原来的mClassLoader。但是呢如果再当前app进程的BootClassLoader和PathClassLoader之间插入我们的dexClassLoader,那在接下来启动其它的组件的时候,就会由于双亲委派虽然找不到pathClassLoader但是它的父节点是能找得到的。这种方法不需要通过繁琐的反射过程一步一步找到主键的mClassLoader。
第一种解决方法的代码:load03_java
package com.kanxue.load03_java; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; 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 android.Manifest; import dalvik.system.DexClassLoader; public class MainActivity extends AppCompatActivity { private static final int PERMISSION_REQUEST_CODE = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 检查是否已经获得外部存储权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // 请求权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE); } else { // 已经有权限,执行操作 performAction(); } } } // 处理权限请求结果 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 用户授予了权限,执行操作 performAction(); } else { // 用户拒绝了权限,可以做一些处理,比如显示一个提示信息 Log.d("Permission", "External storage permission denied"); } } } // 在获得权限后执行的操作 private void performAction() { // 在这里执行加载DEX文件并启动Activity的操作 startTestActivity(this, "/sdcard/4.dex"); } // 用于替换类加载器的方法 public void replaceClassLoader(ClassLoader classLoader) { try { // 加载 android.app.ActivityThread 类 Class<?> ActivityThreadClazz = classLoader.loadClass("android.app.ActivityThread"); // 获取 currentActivityThread 静态方法 Method currentActivityThreadMethod = ActivityThreadClazz.getDeclaredMethod("currentActivityThread"); currentActivityThreadMethod.setAccessible(true); // 调用 currentActivityThread 方法,获取 ActivityThread 实例 Object activityThread = currentActivityThreadMethod.invoke(null); // 获取 ActivityThread 类中的 mPackages 字段 // final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>(); Field mPackagesField = ActivityThreadClazz.getDeclaredField("mPackages"); mPackagesField.setAccessible(true); // 获取 mPackages 字段在 activityThread 实例中的值,它是一个 ArrayMap 对象 ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThread); // 从 mPackages 中获取对当前应用程序包的 WeakReference 引用 WeakReference wr = (WeakReference) mPackagesObj.get(this.getPackageName()); // 获取 WeakReference 中的实际对象,这是 LoadedApk 类的一个实例 Object loadApkObj = wr.get(); // 加载 android.app.LoadedApk 类 Class loadedApkClazz = classLoader.loadClass("android.app.LoadedApk"); // 获取 LoadedApk 类中的 mClassLoader 字段 //private ClassLoader mClassLoader; Field mClassLoaderField = loadedApkClazz.getDeclaredField("mClassLoader"); mClassLoaderField.setAccessible(true); // 将 LoadedApk 实例的 mClassLoader 字段设置为传入的类加载器 mClassLoaderField.set(loadApkObj, classLoader); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } public void startTestActivity(Context context, String dexfilepath) { // 获取应用的私有目录,用于加载DEX文件和库文件 File optfile = context.getDir("opt_dex", 0); File libfile = context.getDir("lib_path", 0); // 获取主Activity的类加载器和上下文的类加载器 ClassLoader parentClassloader = MainActivity.class.getClassLoader(); ClassLoader tmpClassloader = context.getClassLoader(); // 创建一个DexClassLoader,用于加载DEX文件中的类 DexClassLoader dexClassLoader = new DexClassLoader( dexfilepath, optfile.getAbsolutePath(), libfile.getAbsolutePath(), MainActivity.class.getClassLoader()); // 替换类加载器,使外部类能够在应用中被访问 replaceClassLoader(dexClassLoader); Class<?> clazz = null; try { // 从DEX文件加载指定的类 clazz = dexClassLoader.loadClass("com.kanxue.test01_java.TestActivity"); } catch (ClassNotFoundException e) { e.printStackTrace(); } // 启动加载的外部Activity context.startActivity(new Intent(context, clazz)); } }
test01_java
package com.kanxue.test01_java; import android.app.Activity; import android.os.Bundle; import android.util.Log; import androidx.appcompat.app.AppCompatActivity; public class TestActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); Log.i("xiaoeryu","i am TestActivity.onCreate"); } }
执行结果
第二种解决方法:load04
package com.kanxue.load04; import androidx.annotation.NonNull; import android.app.Activity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.util.Log; import java.io.File; import java.lang.reflect.Field; import android.Manifest; import dalvik.system.DexClassLoader; public class MainActivity extends Activity { private static final int PERMISSION_REQUEST_CODE = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 检查是否已经获得外部存储权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // 请求权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE); } else { // 已经有权限,执行操作 performAction(); } } } // 处理权限请求结果 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 用户授予了权限,执行操作 performAction(); } else { // 用户拒绝了权限,可以做一些处理,比如显示一个提示信息 Log.d("Permission", "External storage permission denied"); } } } // 在获得权限后执行的操作 private void performAction() { // 在这里执行加载DEX文件并启动Activity的操作 startTestActivitySecondlyMethod(this, "/sdcard/5.dex"); } // 使用第二种方法启动测试Activity public void startTestActivitySecondlyMethod(Context context, String dexFilePath) { // 获取应用程序的私有目录,用于存放优化后的DEX文件和本地库 File optDir = context.getDir("opt_dex", 0); File libDir = context.getDir("lib_path", 0); // 获取系统类加载器和引导类加载器 ClassLoader pathClassLoader = MainActivity.class.getClassLoader(); ClassLoader bootClassLoader = MainActivity.class.getClassLoader().getParent(); // 创建DexClassLoader来加载外部的DEX文件 DexClassLoader dexClassLoader = new DexClassLoader( dexFilePath, // DEX文件路径 optDir.getAbsolutePath(), // 优化后的DEX文件存放目录 libDir.getAbsolutePath(), // 本地库存放目录 bootClassLoader); // 引导类加载器作为父加载器 try { // 使用反射设置系统类加载器的父加载器为DexClassLoader 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 { // 加载外部DEX文件中的类 clazz = dexClassLoader.loadClass("com.xiaoeryu.test04.TestActivity"); } catch (ClassNotFoundException e) { e.printStackTrace(); } // 启动外部加载的Activity context.startActivity(new Intent(context, clazz)); } }
test04
package com.xiaoeryu.test04; import android.app.Activity; import android.os.Bundle; import android.util.Log; public class TestActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); Log.i("xiaoeryu", "i am from TestActivity.onCreate"); } }
这个代码除了要注意前面的几点之外还要把constraintlayout降一下级,不然运行的时候会有库版本冲突的问题
这两种方案都可以解决加载dex中Activity的问题,相对来说第一种再加壳厂商中比较常用,通过替换app组件的ClassLoader: mClassLoader来实现,让插件类具有生命周期。这两种方案都要用大量的反射相关的api来实现。
动态调试
在插入DexClassLoader的代码后面,插入log看一下双亲委派的调用关系
// 看一下双亲委派的调用关系
ClassLoader tmpClassLoader = pathClassloader;
ClassLoader parentClassLoader = pathClassloader.getParent();
while (parentClassLoader != null){
Log.i("xiaoeryu", "当前节点: " + tmpClassLoader + "--父节点: " + parentClassLoader);
tmpClassLoader = parentClassLoader;
parentClassLoader = parentClassLoader.getParent();
}
Log.i("xiaoeryu", "根节点: " + tmpClassLoader);
运行结果
当前节点: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.kanxue.load04-zqQtFmn29CDDYSwO8gpsPQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.kanxue.load04-zqQtFmn29CDDYSwO8gpsPQ==/lib/arm64, /system/lib64, /vendor/lib64]]]--父节点: dalvik.system.DexClassLoader[DexPathList[[dex file "/sdcard/5.dex"],nativeLibraryDirectories=[/data/user/0/com.kanxue.load04/app_lib_path, /system/lib64, /vendor/lib64]]]
当前节点: dalvik.system.DexClassLoader[DexPathList[[dex file "/sdcard/5.dex"],nativeLibraryDirectories=[/data/user/0/com.kanxue.load04/app_lib_path, /system/lib64, /vendor/lib64]]]--父节点: java.lang.BootClassLoader@118830f
根节点: java.lang.BootClassLoader@118830f
题外话:
对于用户提交的一个带加固的app之后呢,加固厂商一般分两种情况去处理:
- 这个app的AndroidManifest.xml当中没有声明这个application的话,处理起来较为简单一些,加固厂商只需要添加一个application就可以了,然后在这个application里面完成一些classloader的替换
- 如果已经有application的话呢,就需要一个代理的application。这种情况下壳的application不仅要完成解密dex以及classloader的一些相关修复还要完成解密dex之后原来application中两个函数(attachBaseContext和onCreate)的调用
- 标题: 加壳APP运行流程和ClassLoader修正
- 作者: xiaoeryu
- 创建于 : 2023-08-24 16:41:02
- 更新于 : 2023-08-26 22:39:52
- 链接: https://github.com/xiaoeryu/2023/08/24/加壳APP运行流程和ClassLoader修正/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。