在安卓中可以动态加载 dex 来进行类的调用,而对于市面上常见的 dex 加固来说,也同样采取了加密 dex, 并且在解密之后动态调用的方式来进行 dex 的保护,所以我觉得我有必要去学习一下 dex 的动态加载过程,从而对 apk 的加固有更加深刻的了解和体会.

# Android studio 生成 dex

既然我们想要让我们的程序动态加载 dex, 那么我们肯定需要先生成一个 dex 才可以咯

首先我们点击 File->New->New Module 新建一个 Module

image-20230925143653687

选中此处的 Android Library , 来创建一个 Library

image-20230925143750405

然后把 dex 的核心代码写一下,这个函数的作用是显示一个字符串

image-20230925143945822

随后我们选中 dexlib1 这个 Library , 并点击 Build->Make Module 'apkprotect.dexlib1'

image-20230925144009844

之后我们便可以在 Dexlib1/.transforms 目录中找到生成的 dex 文件

image-20230925150407568

# Dex 从文件中动态加载

为了让 apk 可以顺利找到我们刚刚生成的 Dex, 我们可以将这个 dex_class.dex 移入到 assets 目录中

image-20230925175843816

随后便可以加载该 dex 并通过反射机制进行函数调用

反射机制是指在运行的状态中,对于任意一个类,都能够调用这个类的所有属性和方法;对于任意一个对象,都能够调用任何方法和属性;像这种动态获取信息以及动态调用对象方法的功能称为 Java 的反射机制。

需要用到 DexClassLoader 函数说明如下

/*
DexClassLoader 类参数含义
@param dexPath: 待加载的 dex 文件路径,如果是外存路径,一定要加上读外存文件的权限
@param optimizedDirectory: 解压后的.dex 文件存储路径,不可为空,此位置一定要是可读写且仅该应用可读写
@param librarySearchPath: 指向包含本地库 (so) 的文件夹路径,可以设为 null
@param parent: 父级类加载器,一般可以通过 Context.getClassLoader 获取到,也可通过 ClassLoader.getSystemClassLoader () 获取到
*/
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)

具体代码如下

// 将 dex 文件从 assets 目录复制到应用缓存目录
AssetManager assetManager = getAssets();
InputStream inputStream = assetManager.open("dex_class.dex");
File dexfile = new File(getCacheDir(), "dex_class.dex");
OutputStream outputStream = new FileOutputStream(dexfile);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
    outputStream.write(buffer, 0, length);
}
inputStream.close();
outputStream.close();
// 使用 DexClassLoader 加载 dex
DexClassLoader classLoader = new DexClassLoader(dexfile.getAbsolutePath(),getCacheDir().getAbsolutePath(),null,getClassLoader());
// 加载插件的类 (插件的包名。类名)
Class<?> clazz = classLoader.loadClass("com.oacia.dexlib1.dex_class");
// 获取类的实例
Object dexlib_obj = clazz.newInstance();
// 通过反射获取对应的方法
Method method = clazz.getDeclaredMethod("say_hello", Context.class);
// 关闭安全检查达到提升反射速度的目的
method.setAccessible(true);
// 调用反射方法
method.invoke(dexlib_obj,MainActivity.this);

good! 成功调用方法!

image-20230925183351444

# Dex 从内存中动态加载

事实上,dex 是可以在内存中被动态加载的,而不一定非要使用 dex 文件的方式,这一种加载方式被称为 Dex 不落地加载

通过 InMemoryDexClassLoader 可以实现 dex 在内存的加载

// 加载 dex
    jobject classLoader = CallObjectMethod(g_context, "getClassLoader", "()Ljava/lang/ClassLoader;").l;
	if (g_sdk_int < 26) {
        ndk_dlclose(art_handle);
    } else {
        jclass ElementClass = env->FindClass("java/nio/ByteBuffer");
        jobjectArray dexBufferArr = env->NewObjectArray(dexBuffers.size(), ElementClass, NULL);
        for (int i = 0; i < dexBuffers.size(); i++) {
            env->SetObjectArrayElement(dexBufferArr, i, dexBuffers[i]);
        }
        jclass InMemoryDexClassLoaderClass = env->FindClass("dalvik/system/InMemoryDexClassLoader");
        jmethodID InMemoryDexClassLoaderInit = env->GetMethodID(InMemoryDexClassLoaderClass, "<init>",
                                                                "([Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V");
        jobject InMemoryObj = env->NewObject(InMemoryDexClassLoaderClass, InMemoryDexClassLoaderInit, dexBufferArr, classLoader);
        jobject pathListObj = GetField(InMemoryObj, "pathList", "Ldalvik/system/DexPathList;").l;
        jobjectArray dexElements;
        if (g_sdk_int >= 29) {
            jclass list_jcs = env->FindClass("java/util/ArrayList");
            jmethodID list_init = env->GetMethodID(list_jcs, "<init>", "()V");
            jobject list_obj = env->NewObject(list_jcs, list_init);
            dexElements = static_cast<jobjectArray>(CallStaticMethod("dalvik/system/DexPathList", "makeInMemoryDexElements", "([Ljava/nio/ByteBuffer;Ljava/util/List;)[Ldalvik/system/DexPathList$Element;", dexBufferArr, list_obj).l);
        } else {
            dexElements = static_cast<jobjectArray>(GetField(pathListObj, "dexElements", "[Ldalvik/system/DexPathList$Element;").l);
        }
        for (int i = 0; i < env->GetArrayLength(dexElements); i++) {
            dexobjs.push_back(env->GetObjectArrayElement(dexElements, i));
        }
        env->DeleteLocalRef(ElementClass);
        env->DeleteLocalRef(InMemoryDexClassLoaderClass);
        env->DeleteLocalRef(InMemoryObj);
        env->DeleteLocalRef(pathListObj);
    }

随后将 dex 添加到 DexPathList 中,这样我们就可以利用 FindClass 获取到这一个类了

static void make_dex_elements(JNIEnv *env, jobject classLoader, std::vector<jobject> dexFileobjs)
{
    jclass PathClassLoader = env->GetObjectClass(classLoader);
    jclass BaseDexClassLoader = env->GetSuperclass(PathClassLoader);
    // get pathList fieldid
    jfieldID pathListid = env->GetFieldID(BaseDexClassLoader, "pathList", "Ldalvik/system/DexPathList;");
    jobject pathList = env->GetObjectField(classLoader, pathListid);
    // get DexPathList Class
    jclass DexPathListClass = env->GetObjectClass(pathList);
    // get dexElements fieldid
    jfieldID dexElementsid = env->GetFieldID(DexPathListClass, "dexElements", "[Ldalvik/system/DexPathList$Element;");
    jobjectArray dexElement = static_cast<jobjectArray>(env->GetObjectField(pathList, dexElementsid));
    jint len = env->GetArrayLength(dexElement);
    LOGD("[+]Elements size:%d, dex File size: %d", len, dexFileobjs.size());
    // Get dexElement all values and add  add each value to the new array
    jclass ElementClass = env->FindClass(
            "dalvik/system/DexPathList$Element"); // dalvik/system/DexPathList$Element
    jobjectArray new_dexElement = env->NewObjectArray(len + dexFileobjs.size(), ElementClass, NULL);
    for (int i = 0; i < len; i++)
    {
        env->SetObjectArrayElement(new_dexElement, i, env->GetObjectArrayElement(dexElement, i));
    }
    if (g_sdk_int < 26) {
        jmethodID ElementInit = env->GetMethodID(ElementClass, "<init>",
                                                 "(Ljava/io/File;ZLjava/io/File;Ldalvik/system/DexFile;)V");
        jboolean isDirectory = JNI_FALSE;
        for (int i = 0; i < dexFileobjs.size(); i++) {
            jobject element_obj = env->NewObject(ElementClass, ElementInit, NULL, isDirectory, NULL,
                                                 dexFileobjs[i]);
            env->SetObjectArrayElement(new_dexElement, len + i, element_obj);
        }
    } else {
        for (int i = 0; i < dexFileobjs.size(); i++) {
            env->SetObjectArrayElement(new_dexElement, len + i, dexFileobjs[i]);
        }
    }
    env->SetObjectField(pathList, dexElementsid, new_dexElement);
    env->DeleteLocalRef(ElementClass);
    env->DeleteLocalRef(dexElement);
    env->DeleteLocalRef(DexPathListClass);
    env->DeleteLocalRef(pathList);
    env->DeleteLocalRef(BaseDexClassLoader);
    env->DeleteLocalRef(PathClassLoader);
}

当 dex 被加载到内存中之后,使用下列的语句即可实现相关函数的反射调用

jclass clazz = env->FindClass("com/oacia/dexlib1/dex_class");
// 创建类 com/oacia/dexlib1/dex_class 的实例
jobject obj = env->AllocObject(clazz);
// 获取 Java 方法 "say_hello" 的 JNI 引用
jmethodID methodID = env->GetMethodID(clazz, "say_hello", "(Landroid/content/Context;)V");
// 调用 Java 方法
env->CallVoidMethod(obj, methodID,cnt);

点击 LOAD MEMORY DEX , 函数调用成功~

image-20240202181233262

# 参考资料

  • Android 加壳与脱壳(11)—— 不落地加载壳的对抗研究
  • https://github.com/Frezrik/Jiagu
更新于 阅读次数