安卓安全基础——android类加载的分析与实现
北海 发表于 广东 移动安全 1084浏览 · 2024-09-28 15:10

1.前言

在android系统中,通过使用动态加载类可以实现加载插件和第一代整体加壳。本文目标在于分析动态加载dex的原理以及实现

2.基础知识

说起安卓类的动态加载,绕不过的两个内容就是ClassLoader和双亲委派机制

(1)ClassLoader

Android 中的 ClassLoader 是一个用于加载类文件的对象。它的主要作用是将 Java 类的字节码加载到内存中,并创建对应的 Class 对象。在 Android 系统中,不同的 ClassLoader 负责加载不同来源的类文件。

下面借用随风大佬的图:

上图是Android类加载器层级关系及分析,而最基本的类加载器有三种:

1.BootClassLoader (启动类加载器)
负责加载 Android 系统启动时所需的核心类库
2.PathClassLoader (路径类加载器)
主要用于加载 Android 应用程序的本地类和库文件,而且只能加载已经安装到设备上的类文件,不能加载外部存储中的类文件
3.DexClassLoader (动态类加载器)
可以加载外部存储中的 dex 文件、jar 文件或包含 dex 文件的 zip 压缩包,相比 PathClassLoader,DexClassLoader 更加灵活,可以加载来自不同来源的类文件

下面我们通过代码来加深对这三个基本的类加载器的理解
实验环境android-8.0.0

第一段代码
public void testClassLoader(){

        ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
        Log.d("xz_aliyun",pathClassLoader.toString());

        ClassLoader bootClassLoader = String.class.getClassLoader();
        Log.d("xz_aliyun",bootClassLoader.toString());

    }

getClassLoader方法会获取当前类的加载器
运行结果

通过运行结果,我们可以知道MainActvity的加载器是PathClassLoader,String类的加载器是BootClassLoader
这说明PathClassLoader加载已安装的APK类,而BootClassLoader加载系统核心的类

第二段代码

遍历父加载类

public void testClassLoader(){
        ClassLoader classLoader = MainActivity.class.getClassLoader();
        Log.d("xz_aliyun",classLoader.toString());
        ClassLoader parent = classLoader.getParent();

        while(parent != null){
            Log.d("xz_aliyun","parent->"+parent.toString());
            parent = parent.getParent();
        }
    }

这个循环会不断地获取父类加载器,并打印其信息,直到没有父类加载器为止。在每次循环中,先打印当前的父类加载器信息,然后继续获取它的父类加载器,从而实现沿着类加载器层次结构向上遍历。
运行结果

第三段代码

这段代码我们要实现DexClassLoader类对象的创建
具体要怎么创建,我们要追溯到源码,以android-8.0.0为例

由DexClassLoader的构造函数可以知道,要实例化DexClassLoader类需要四个参数分别为dexPath,optimizedDirectory,librarySearchPath,parent

  • String dexPath 指定了要加载的类所在的 dex 文件的位置
  • String optimizedDirectory 用于指定优化后的 dex 文件的输出目录路径
  • String librarySearchPath 指定要搜索的本地库的路径
  • ClassLoader parent 指定 DexClassLoader 的父类加载器

接下类我们用具体代码实现对DexClassLoader对象的创建方法

public void testClassLoader(Context context,String dexfilepath){
        File optfile = context.getDir("opt_dex", Context.MODE_PRIVATE);
        File libfile = context.getDir("lib_path", Context.MODE_PRIVATE);

        DexClassLoader dexClassLoader = new DexClassLoader(dexfilepath, optfile.getAbsolutePath(), libfile.getAbsolutePath(), MainActivity.class.getClassLoader());

    }

File optfile = context.getDir("opt_dex", Context.MODE_PRIVATE);:

使用上下文对象创建一个名为 “opt_dex” 的私有目录,用于存储优化后的 dex 文件。这个目录只能被当前应用程序访问,确保了一定的安全性。

File libfile = context.getDir("lib_path", Context.MODE_PRIVATE);:

类似地,创建一个名为 “lib_path” 的私有目录,用于存储本地库文件(如果有需要加载的本地库)

接下来我们简单讲一下双亲委派机制

(2)双亲委派机制

简单来说,在安卓中,双亲委派机制就是当要加载一个类时,先让父类加载器去尝试加载。如果父类加载器找不到这个类,子类加载器才会去加载。就像你找东西,先问家长有没有,家长没有你再自己去找。这样可以保证核心类优先被系统的类加载器加载,避免重复加载和混乱,让安卓系统更稳定安全。

下面来验证一下双亲委派机制

代码如下

public void testParentDelegationMechanism(){
        ClassLoader classloader = MainActivity.class.getClassLoader();
        try {
            Class StringClass = classloader.loadClass("java.lang.String");
            Log.d("xz_aliyun","load StringClass success!"+classloader.toString());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            Log.d("xz_aliyun","load StringClass fail!"+classloader.toString());
        }
    }

我尝试使用MainActivity的类加载器(即PathClassLoader)去加载java.lang.String类,如果加载成功则打印成功信息及类加载器信息,否则打印失败信息及类加载器信息,以此测试双亲委派机制下该类加载器对核心类的加载情况。

运行结果

所以,我们使用PathClassLoader去加载String.class类,还是可以加载成功,这是因为在双亲委派机制下,当类加载器收到一个类加载请求时,它首先会把这个请求委派给它的父类加载器去尝试加载。而PathClassLoader会交给BootClassLoader去加载

3.动态加载类

动态加载类是一种强大的技术,可以在运行时加载和使用未预先编译到应用中的类

动态加载非组件类

下面我参考寒冰大佬的案例编写demo

首先来写插件类

具体代码如下

public class TestClass {
    public void testClassLoader(){
        Log.d("xz_aliyun","==========testClassLoader is called===========");
    }
}

此处为方便观察testClassLoader是否被调用,使用了日志打印

接着我们编译打包

打开编译好的apk包

找到TestClass对应的dex

然后上传到设备的sdcard中

上传后就可以删除TestClass类,在MainActivity中写方法加载dex
代码如下

public void testClassLoader(Context context, String dex_file_path){
        File opt_file = context.getDir("opt_dex", Context.MODE_PRIVATE);
        File lib_file = context.getDir("lib_path", Context.MODE_PRIVATE);

        //实例化一个DexClassLoader加载器
        DexClassLoader dexClassLoader = new DexClassLoader(dex_file_path, opt_file.getAbsolutePath(), lib_file.getAbsolutePath(), MainActivity.class.getClassLoader());

        try {
            Class<?> clazz = dexClassLoader.loadClass("com.demo.testdex.TestClass");
            Object obj = clazz.newInstance();
            Method testClassLoader = clazz.getDeclaredMethod("testClassLoader");
            testClassLoader.invoke(obj);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }


简单来说,上面的方法就是通过一个特定的类加载器(dexClassLoader)去加载一个指定名称("com.demo.testdex.TestClass")的类,得到这个类的引用(clazz)。接着,用这个引用创建这个类的一个实例(obj)。然后,找到这个类中的一个特定方法(testClassLoader)。最后,调用testClassLoader

但是有一点要注意的是android6.0以上访问sdcard需要在AndroidManifest.xml中添加权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

而且还要进行申请操作
checkExternalStoragePermission方法:
检查是否有外部存储读取权限。如果是 Android M 及以上版本,检查权限是否已授予。如果未授予,则请求权限。
onRequestPermissionsResult方法:
处理权限请求结果。如果请求的是外部存储读取权限且被授予,则调用testClassLoader方法;如果被拒绝,则记录错误信息。

最后整理代码如下:

package com.demo.testdex;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private static final int REQUEST_READ_EXTERNAL_STORAGE = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Context applicationContext = this.getApplicationContext();
        if (checkExternalStoragePermission()) {
            testClassLoader(applicationContext, "/sdcard/class.dex");
        }
    }


    public void testClassLoader(Context context, String dex_file_path){
        File opt_file = context.getDir("opt_dex", Context.MODE_PRIVATE);
        File lib_file = context.getDir("lib_path", Context.MODE_PRIVATE);

        //实例化一个DexClassLoader加载器
        DexClassLoader dexClassLoader = new DexClassLoader(dex_file_path, opt_file.getAbsolutePath(), lib_file.getAbsolutePath(), MainActivity.class.getClassLoader());

        try {
            Class<?> clazz = dexClassLoader.loadClass("com.demo.testdex.TestClass");
            Object obj = clazz.newInstance();
            Method testClassLoader = clazz.getDeclaredMethod("testClassLoader");
            testClassLoader.invoke(obj);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private boolean checkExternalStoragePermission() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            if (checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_READ_EXTERNAL_STORAGE);
                return false;
            }
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_READ_EXTERNAL_STORAGE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Log.d(TAG, "Permission granted.");
                Context applicationContext = getApplicationContext();
                testClassLoader(applicationContext, "/sdcard/class.dex");
            } else {
                Log.e(TAG, "Permission denied.");
            }
        }
    }
}

运行

点击运行,就可以看见日志打印显示testClassLoader被调用

加载组件类

我们很容易想到使用DexClassLoader来加载Activity获取到class对象,在使用Intent启动。但是实际上并不是这么简单。因为Android中的四大组件都有一个特点就是他们有自己的启动流程和生命周期,我们使用DexClassLoader加载进来的Activity是不会涉及到任何启动流程和生命周期的概念,所以启动会出错。

问题的关键就是要让加载进来的Activity有启动流程和生命周期

思路一 替换LoadedApk中的mClassLoader

首先要知道的就是
在 Android 系统中,当应用程序启动时,系统会找到应用的入口点,而这个入口点通常就是ActivityThread.main()方法。

具体的apk加载又与LoadedApk类有关
我们进一步来看一下LoadedApk类


可以看到里面定义了一个类加载器mClassLoader,我们可以通过反射用dexClassLoader替换mClassLoader

要想通过反射替换mClassLoader这个属性,就需要获取到LoadedApk类对象,那么该如何获取到该对象呢,我们回到ActivityThread.java

通过搜索可以在ActivityThread类中找到一个存放Apk包名和LoadedApk映射关系的ArrayMap数据结构mPackages,于是我们就可以从中获取LoadedApk类对象了

剩下的问题就是获取ActivityThread类对象,
继续观察源码


可以通过currentActivityThread方法直接返回一个ActivityThread类对象

于是整条链完整了

下面是代码实现

public void replaceClassLoader(ClassLoader classLoader){
        try {
            //加载ActivityThread类
            Class<?> activityThreadClazz = classLoader.loadClass("android.app.ActivityThread");
            //获取currentActivityThread方法
            Method currentActivityThread = activityThreadClazz.getDeclaredMethod("currentActivityThread");
            currentActivityThread.setAccessible(true);
            //调用currentActivityThread方法获取ActivityThread对象
            Object activityThreadObj= currentActivityThread.invoke(null);
            //获取mpackage
            Field mPackages = activityThreadClazz.getDeclaredField("mPackages");
            mPackages.setAccessible(true);
            ArrayMap mPobj =(ArrayMap)mPackages.get(activityThreadObj);
            WeakReference wr = (WeakReference)mPobj.get(this.getPackageName());
            Object loadedApkobj = wr.get();
            //获取LoadedApk类
            Class<?> loadedApkClazz= classLoader.loadClass("android.app.LoadedApk");
            Field mClassLoader = loadedApkClazz.getDeclaredField("mClassLoader");
            mClassLoader.setAccessible(true);
            //替换
            mClassLoader.set(loadedApkobj,classLoader);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

整体代码

package com.demo.testdex;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import android.view.View;
import android.widget.Button;

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 {
    private static final String TAG = "MainActivity";
    private static final int REQUEST_READ_EXTERNAL_STORAGE = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Context applicationContext = this.getApplicationContext();
        Button btnJump = findViewById(R.id.btn_jump);
        btnJump.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (checkExternalStoragePermission()) {
                    testClassLoader(applicationContext, "/sdcard/class.dex");
                }
            }
        });
    }


    public void testClassLoader(Context context, String dex_file_path){
        File opt_file = context.getDir("opt_dex", Context.MODE_PRIVATE);
        File lib_file = context.getDir("lib_path", Context.MODE_PRIVATE);

        //实例化一个DexClassLoader加载器
        DexClassLoader dexClassLoader = new DexClassLoader(dex_file_path, opt_file.getAbsolutePath(), lib_file.getAbsolutePath(), MainActivity.class.getClassLoader());
        replaceClassLoader(dexClassLoader);
        Class<?> secondActivityclazz = null;
        try {
            secondActivityclazz = dexClassLoader.loadClass("com.demo.testdex.SecondActivity");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        Intent intent = new Intent(MainActivity.this, secondActivityclazz);
        startActivity(intent);

    }

    public void replaceClassLoader(ClassLoader classLoader){
        try {
            //加载ActivityThread类
            Class<?> activityThreadClazz = classLoader.loadClass("android.app.ActivityThread");
            //获取currentActivityThread方法
            Method currentActivityThread = activityThreadClazz.getDeclaredMethod("currentActivityThread");
            currentActivityThread.setAccessible(true);
            //调用currentActivityThread方法获取ActivityThread对象
            Object activityThreadObj= currentActivityThread.invoke(null);
            //获取mpackage
            Field mPackages = activityThreadClazz.getDeclaredField("mPackages");
            mPackages.setAccessible(true);
            ArrayMap mPobj =(ArrayMap)mPackages.get(activityThreadObj);
            WeakReference wr = (WeakReference)mPobj.get(this.getPackageName());
            Object loadedApkobj = wr.get();
            //获取LoadedApk类
            Class<?> loadedApkClazz= classLoader.loadClass("android.app.LoadedApk");
            Field mClassLoader = loadedApkClazz.getDeclaredField("mClassLoader");
            mClassLoader.setAccessible(true);
            //替换
            mClassLoader.set(loadedApkobj,classLoader);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

    private boolean checkExternalStoragePermission() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            if (checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_READ_EXTERNAL_STORAGE);
                return false;
            }
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_READ_EXTERNAL_STORAGE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Log.d(TAG, "Permission granted.");
                Context applicationContext = getApplicationContext();
                testClassLoader(applicationContext, "/sdcard/class.dex");
            } else {
                Log.e(TAG, "Permission denied.");
            }
        }
    }
}

运行发现可以成功加载跳转

思路二 将DexClassLoader插入PathClassLoader加载器和BootClassLoader加载器之间

参考寒冰大佬的案例

下面编写代码

public void startTestActivity(Context context, String dex_file_path){
        File opt_file = context.getDir("opt_dex", 0);
        File lib_file = context.getDir("lib_path", 0);

        ClassLoader pathClassLoader = MainActivity.class.getClassLoader();
        ClassLoader bootClassLoader = MainActivity.class.getClassLoader().getParent();

        //实例化一个DexClassLoader加载器
        DexClassLoader dexClassLoader = new DexClassLoader(dex_file_path, opt_file.getAbsolutePath(), lib_file.getAbsolutePath(), bootClassLoader);

        //将pathClassLoader的父加载器
        try {
            Field parent = ClassLoader.class.getDeclaredField("parent");
            parent.setAccessible(true);
            parent.set(pathClassLoader,dexClassLoader);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        ClassLoader classLoader = MainActivity.class.getClassLoader();
        Log.d("xz_aliyun",classLoader.toString());
        ClassLoader parent = classLoader.getParent();

        while(parent != null){
            Log.d("xz_aliyun","parent->"+parent.toString());
            parent = parent.getParent();
        }
        Class<?> clazz = null;
        try {
            clazz = dexClassLoader.loadClass("com.demo.testdex.SecondActivity");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        startActivity(new Intent(MainActivity.this,clazz));
    }

4.细节解析

1.为什么直接使用DexClassLoader加载组件类会没有生命周期,而替换mClassLoader后又行了

直接用 DexClassLoader 加载组件类时不能拥有正常生命周期,是因为 Android 系统在启动四大组件时有特定的启动流程和机制,这个流程通常依赖系统默认的类加载机制。当直接使用 DexClassLoader 加载组件类时,系统并不知道这个类是通过自定义的类加载器加载的,不会将其纳入正常的启动流程中进行管理。所以,这些通过 DexClassLoader 加载的类就无法触发系统为四大组件设计的启动流程和生命周期管理,导致没有正常的生命周期表现
而用 DexClassLoader 替换 LoadedApk 中的 mClassLoader 后,系统启动流程会使用 DexClassLoader 加载组件类。这使得 DexClassLoader 加载的类能被系统正确找到并触发正常启动流程和生命周期管理机制,如 Activity 会按正常生命周期方法被调用。但这种方式有风险且在不同版本表现可能不同。

2.父加载类并非父类

我们回到这张图可以看到PathClassLoader和DexClassLoader的父类是BaseDexCLassLoader,这不等同于父加载类。
PathClassLoader的父加载类是BootClassLoader,而双亲委派是委派给父加载类

5.参考文章

https://bbs.kanxue.com/thread-271538.htm
https://blog.csdn.net/hzwailll/article/details/85339714
https://blog.csdn.net/suyimin2010/article/details/80958712

0 条评论
某人
表情
可输入 255