在安卓中可以动态加载 dex 来进行类的调用,而对于市面上常见的 dex 加固来说,也同样采取了加密 dex, 并且在解密之后动态调用的方式来进行 dex 的保护,所以我觉得我有必要去学习一下 dex 的动态加载过程,从而对 apk 的加固有更加深刻的了解和体会.
# Android studio 生成 dex
既然我们想要让我们的程序动态加载 dex, 那么我们肯定需要先生成一个 dex 才可以咯
首先我们点击 File->New->New Module
新建一个 Module
选中此处的 Android Library
, 来创建一个 Library
然后把 dex 的核心代码写一下,这个函数的作用是显示一个字符串
随后我们选中 dexlib1
这个 Library
, 并点击 Build->Make Module 'apkprotect.dexlib1'
之后我们便可以在 Dexlib1/.transforms
目录中找到生成的 dex
文件
# Dex 从文件中动态加载
为了让 apk 可以顺利找到我们刚刚生成的 Dex, 我们可以将这个 dex_class.dex
移入到 assets
目录中
随后便可以加载该 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! 成功调用方法!
# 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
, 函数调用成功~
# 参考资料
- Android 加壳与脱壳(11)—— 不落地加载壳的对抗研究
- https://github.com/Frezrik/Jiagu