DexHunter在Dalvik虚拟机模式下的脱壳原理分析
阅读原文时间:2021年09月06日阅读:1

本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78494671

在前面的博客《DexHunter的原理分析和使用说明(一)》、《DexHunter的原理分析和使用说明(二) 》中已经将DexHunter工具的原理和使用需要注意的地方已经学习了一下,前面的博客中只讨论了DexHunter脱壳工具在Dalvik虚拟机模式下的脱壳原理和使用,一直想分析和研究一下DexHunter脱壳工具在ART虚拟机模式下的脱壳原理,终于有时间进行知识的消化了,特此整理一下基于Android运行时的脱壳工具DexHunter的脱壳原理,又将DexHunter工具的代码再看了几遍,对DexHunter脱壳工具的脱壳原理又有啦更进一步的理解和认识,并且也将DexHunter脱壳工具在ART虚拟机模式下的脱壳原理理解清楚啦。在DexHunter脱壳工具公布之后,类似变型的脱壳工具 Xdex 也出来了-详细的信息可以参考看雪文章《Xdex(百度版)脱壳工具基本原理》,基于类方法抽取的Android加固同样可以通过修改DexHunter脱壳工具进行脱壳,只是需要自己选择Android Dex文件的类方法加载点和类方法实现数据的dump点,最后进行dex文件的重组和还原。

1.  Dalvik虚拟机模式下,DexHunter脱壳工具dump出来的文件是odex文件

DexHunter脱壳工具是完全基于Android运行时进行dex文件的分步dump脱壳的,此时原始的Android Dex文件已经被Android系统优化为了odex文件进行执行。因此,DexHunter脱壳工具dump出来的dex文件是Android系统优化后的odex文件,并且DexHunter工具是分步进行odex文件的dump操作的,然后将dump出来odex文件的各部分进行重组得到一个能进行反编译分析的odex文件。

2. Dalvik虚拟机模式下Android Odex文件格式

在进行Dexhunter脱壳工具的原理分析之前,先了解一下Dalvik虚拟机模式下odex文件的格式。有关odex文件格式的详细信息,可以参考作者roland_sun的博文《Android系统ODEX文件格式解析》,写的比较详细也很不错,借用一下作者画的odex文件格式的示意图。

注意:flags域说明是用的大端字节序还是小端字节序,一般是小端,所以是0;最后是校验和的值,注意这个校验和不是算整个ODEX文件的,而是只算依赖库列表段和优化数据段的。

3. Dalvik虚拟机模式下Android Dex文件格式

Dalvik虚拟机模式下dex文件格式示意图来自于大牛Jonathan Levin的paper《Andevcon-DEX.pdf》。

下面Android dex文件格式示的意图来自paper《A_deep_dive_into_dex_file_format》。

4. DexHunter脱壳工具的odex文件dump图解

DexHunter脱壳工具对被脱壳的Android应用的odex文件进行dump操作的原理图解(需要看懂DexHunter脱壳工具的代码,才能深刻的理解下面这幅图)

Dalvik虚拟机模式下,在目标被脱壳的Android进程内存中找到了需要dump的dex文件(实际是优化后的odex文件)所在的内存区域之后,将内存中dex文件分成3部分进行内存dump出来,然后进行dex文件的重组,最终得到能够进行逆向分析的dex文件。将目标被脱壳的Android进程内存中 odex文件开头到class_defs开始之前这段内存区域的数据 内存dump保存到 part1文件中;将从 class_defs的结尾开始到整个odex文件结尾之间的这段内存数据 内存dump操作保存到 data 文件中;dex文件中最关键的类信息描述结构体数据,以 Android运行时 的类填充数据为准,从内存中进行收集和整理,类定义的描述结构体DexClassDef的数据收集之后内存dump保存到classdef文件中,类实际的填充数据DexClassData和类方法的实际数据DexCode则按照偏移的格式保存到extra文件中,最后进行odex文件的重组。

5. DexHunter脱壳工具odex文件dump点的选择

Dalvik虚拟机模式下Android dex文件的dump点主要有4个地方:1.  打开dex文件的时候,判断dex文件是否优化为odex文件,如果已经优化为odex文件,则打开odex文件加载到内存中;2. 类Class加载的时候,dex文件加载到内存后的最终表现形式为ClassObject,在类方法被调用之前需要先加载该类Class并解析;3. 创建类Class对象初始化的时候,调用实例对象相关的操作时,需要先对类Class进行数据的初始化; 4. 类方法Method被调用的时候,类方法被调用的时需要获取Method方法的实际执行代码指令,这些代码指令从哪儿来,需要从加载到内存的dex文件中来获取。

Davik虚拟机模式下,为什么DexHunter脱壳工具选择类Class加载的时候进行dex文件的dump操作呢?

Dalvik虚拟机模式下,Android 类Class加载操作的步骤。

Dalvik虚拟机模式下,Android加载类Class的两种方法,调用类反射函数Class.forName进行类加载以及调用函数ClassLoader.loadClass进行类的主动加载。

Dalvik虚拟机模式下,java层类加载函数Class.forName和函数ClassLoader.loadClass对应的Native函数调用如下所示,在类对象创建时调用的dvmResolveClass函数获取ClassObject时也会涉及到Android 类的加载操作。

Dalvik虚拟机模式下,Android类的加载最终是通过调用Native层的函数 Dalvik_dalvik_system_DexFile_defineClassNative 实现类的加载,因此我们在进行dex文件的内存dump时紧紧的卡在这个点的位置。

Dalvik虚拟机模式下,dex文件描述的类加载到内存后的ClassObject描述结构体示意图:

6.Dalvik虚拟机模式下,DexHunter脱壳工具对Android源码的修改位置(以Android 4.4.4 r1的源码为例):对 Android 4.4.4 r1 源码的文件路径 /dalvik/vm/native/dalvik_system_DexFile.cpp 中 函数Dalvik_dalvik_system_DexFile_defineClassNative  的实现代码进行修改。

http://androidxref.com/4.4.4_r1/xref/dalvik/vm/native/dalvik_system_DexFile.cpp#349

Dalvik虚拟机模式下,DexHunter脱壳工具内存dump被脱壳Android应用的odex文件的步骤详细分析。

7. Android系统进程的uid一般为0,普通应用的uid是大于0的,因此通过getuid获取当前Android进程的uid,使用uid进行简单的Android系统进程的过滤。创建原生线程,读取DexHunter脱壳工具需要的配置文件 /data/dexname 中的数据信息,顺便提示下:DexHunter脱壳工具出来以后,一些Android加固增加了对DexHunter脱壳工具的检测对抗,检测原理也比较简单--通过检测是否存在 /data/dexname 文件来检测DexHunter脱壳工具的存在, 因此在进行DexHunter脱壳工具的使用过程,最好修改一下DexHunter脱壳工具的配置文件路径。

DexHunter脱壳工具的配置文件 /data/dexname 中,有2行数据内容;第1行数据是Android加固对应的特征字符串 dexname--Android加固对应的被保护dex文件的加载路径字符串,不同Android加固的被保护dex文件的加载路径是不同的,同一种加固的被保护dex文件的加载路径也不是一直固定,脱壳操作之前需要自己先确定;第2行数据是DexHunter脱壳工具存放内存dump的odex文件数据的工作目录dumppath(要保证DexHunter脱壳工具在这个工作目录下有文件的读写权限),一般情况下这个工作目录设置为被脱壳Android应用的数据目录 /data/data/pakegname(被脱壳的Android应用的包名) 。创建并初始化定时器,主要是为了设置内存dump被脱壳Android应用的odex文件的等待时间,建议将定时器的等待时间设置的稍微长一点,因为现在的Android应用的dex文件都比较大,从内存中收集和dump被脱壳Android应用的odex文件还是比较耗费时间的;如果定时器的等待时间设置过短,会导致DexHunter脱壳失败的。

DexHunter脱壳工具开源公布时,一些常见Android加固的特征字符串即被保护dex文件的加载路径特征字符串的格式,如下表所示:

8. 使用Android加固的特征字符串 dexname 即被保护dex文件的加载路径字符串,进行内存odex文件dump操作的准确过滤。

Android加固对应的特征字符串即被保护dex文件的加载路径字符串,Dalvik虚拟机模式下是根据 cookie值 的描述结构体 DexOrJar 中的成员变量 fileName 来确定的,最终都是由加载dex文件到内存的 函数 Dalvik_dalvik_system_DexFile_openDexFileNative 和 函数Dalvik_dalvik_system_DexFile_openDexFile_bytearray 来决定的。

Dalvik虚拟机模式下,DexOrJar 结构体中的成员变量 filename 描述了被加载的dex文件的路径。

函数 Dalvik_dalvik_system_DexFile_openDexFileNative 加载dex文件到Android进程内存的实现(正常情况下,Android应用加载dex文件调用该函数)。

/*
 * private static int openDexFileNative(String sourceName, String outputName,
 *     int flags) throws IOException
 *
 * Open a DEX file, returning a pointer to our internal data structure.
 *
 * "sourceName" should point to the "source" jar or DEX file.
 *
 * If "outputName" is NULL, the DEX code will automatically find the
 * "optimized" version in the cache directory, creating it if necessary.
 * If it's non-NULL, the specified file will be used instead.
 *
 * TODO: at present we will happily open the same file more than once.
 * To optimize this away we could search for existing entries in the hash
 * table and refCount them.  Requires atomic ops or adding "synchronized"
 * to the non-native code that calls here.
 *
 * TODO: should be using "long" for a pointer.
 */
static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args,
    JValue* pResult)
{
    StringObject* sourceNameObj = (StringObject*) args[0];
    StringObject* outputNameObj = (StringObject*) args[1];
    DexOrJar* pDexOrJar = NULL;
    JarFile* pJarFile;
    RawDexFile* pRawDexFile;
    char* sourceName;
    char* outputName;

    if (sourceNameObj == NULL) {
        dvmThrowNullPointerException("sourceName == null");
        RETURN_VOID();
    }

    sourceName = dvmCreateCstrFromString(sourceNameObj);
    if (outputNameObj != NULL)
        outputName = dvmCreateCstrFromString(outputNameObj);
    else
        outputName = NULL;

    /*
     * We have to deal with the possibility that somebody might try to
     * open one of our bootstrap class DEX files.  The set of dependencies
     * will be different, and hence the results of optimization might be
     * different, which means we'd actually need to have two versions of
     * the optimized DEX: one that only knows about part of the boot class
     * path, and one that knows about everything in it.  The latter might
     * optimize field/method accesses based on a class that appeared later
     * in the class path.
     *
     * We can't let the user-defined class loader open it and start using
     * the classes, since the optimized form of the code skips some of
     * the method and field resolution that we would ordinarily do, and
     * we'd have the wrong semantics.
     *
     * We have to reject attempts to manually open a DEX file from the boot
     * class path.  The easiest way to do this is by filename, which works
     * out because variations in name (e.g. "/system/framework/./ext.jar")
     * result in us hitting a different dalvik-cache entry.  It's also fine
     * if the caller specifies their own output file.
     */
    if (dvmClassPathContains(gDvm.bootClassPath, sourceName)) {
        ALOGW("Refusing to reopen boot DEX '%s'", sourceName);
        dvmThrowIOException(
            "Re-opening BOOTCLASSPATH DEX files is not allowed");
        free(sourceName);
        free(outputName);
        RETURN_VOID();
    }

    /*
     * Try to open it directly as a DEX if the name ends with ".dex".
     * If that fails (or isn't tried in the first place), try it as a
     * Zip with a "classes.dex" inside.
     */
    if (hasDexExtension(sourceName)
            && dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) {
        ALOGV("Opening DEX file '%s' (DEX)", sourceName);

        pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
        pDexOrJar->isDex = true;
        pDexOrJar->pRawDexFile = pRawDexFile;
        pDexOrJar->pDexMemory = NULL;
    } else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) {
        ALOGV("Opening DEX file '%s' (Jar)", sourceName);

        pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
        pDexOrJar->isDex = false;
        pDexOrJar->pJarFile = pJarFile;
        pDexOrJar->pDexMemory = NULL;
    } else {
        ALOGV("Unable to open DEX file '%s'", sourceName);
        dvmThrowIOException("unable to open DEX file");
    }

    if (pDexOrJar != NULL) {

        // 被加载的dex文件的路径描述
        pDexOrJar->fileName = sourceName;
        addToDexFileTable(pDexOrJar);
    } else {
        free(sourceName);
    }

    free(outputName);
    RETURN_PTR(pDexOrJar);
}

函数Dalvik_dalvik_system_DexFile_openDexFile_bytearray 加载dex文件到Android进程内存的实现(Android加固加载被保护dex文件时调用该函数)。

/*
 * private static int openDexFile(byte[] fileContents) throws IOException
 *
 * Open a DEX file represented in a byte[], returning a pointer to our
 * internal data structure.
 *
 * The system will only perform "essential" optimizations on the given file.
 *
 * TODO: should be using "long" for a pointer.
 */
static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args,
    JValue* pResult)
{
    ArrayObject* fileContentsObj = (ArrayObject*) args[0];
    u4 length;
    u1* pBytes;
    RawDexFile* pRawDexFile;
    DexOrJar* pDexOrJar = NULL;

    if (fileContentsObj == NULL) {
        dvmThrowNullPointerException("fileContents == null");
        RETURN_VOID();
    }

    /* TODO: Avoid making a copy of the array. (note array *is* modified) */
    length = fileContentsObj->length;
    pBytes = (u1*) malloc(length);

    if (pBytes == NULL) {
        dvmThrowRuntimeException("unable to allocate DEX memory");
        RETURN_VOID();
    }

    memcpy(pBytes, fileContentsObj->contents, length);

    if (dvmRawDexFileOpenArray(pBytes, length, &pRawDexFile) != 0) {
        ALOGV("Unable to open in-memory DEX file");
        free(pBytes);
        dvmThrowRuntimeException("unable to open in-memory DEX file");
        RETURN_VOID();
    }

    ALOGV("Opening in-memory DEX");
    pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
    pDexOrJar->isDex = true;
    pDexOrJar->pRawDexFile = pRawDexFile;
    pDexOrJar->pDexMemory = pBytes;
    // 被加载的dex文件的路径描述
    pDexOrJar->fileName = strdup("<memory>"); // Needs to be free()able.
    addToDexFileTable(pDexOrJar);

    RETURN_PTR(pDexOrJar);
}

9.通过被脱壳Android应用进程dex文件的内存描述结构体 DvmDex  的成员变量 memMap 获取到该进程的odex文件所在的内存区域,然后将 odex文件开头到class_defs数据起始偏移之间的数据 从内存中dump出来保存到文件 /data/data/pakegname/part1 中,其中pakegname为被脱壳Android应用的包名。

示意图:

10.将被脱壳Android应用的内存odex文件的 class_defs数据结束偏移到odex文件结束的这段内存数据 内存dump出来保存到文件 /data/data/pakegname/data  中,其中pakegname为被脱壳Android应用的包名。

示意图:

11. 创建原生线程,遍历被脱壳Android应用odex文件的 DexClassDef 数据数组,基于Android运行时进行被脱壳Android应用类的描述数据 DexClassDef 、DexClassData 和类方法的实现数据 DexCode 的收集和内存dump,将类的定义描述数据 DexClassDef内存dump保存到文件/data/data/pakegname/classdef 中,将类的实际描述结构体数据DexClassData  和类方法的实现数据DexCode 内存dump保存到文件/data/data/pakegname/extra 中,然后将内存dump的odex文件的 4部分数据 进行重组,得到能够反编译odex文件,其中pakegname为被脱壳Android应用的包名。

12. 创建保存内存dump的类定义描述结构体 DexClassDef 数据的文件 /data/data/pakegname/classdef 以及创建保存内存dump的类数据 DexClassData 和 类方法的实际实现数据 DexCode 的文件 /data/data/pakegname/extra其中pakegname为被脱壳Android应用的包名 。

被脱壳Android应用的odex文件中的类数据以Android运行时的 DexClassData 的实际填充数据 为基准进行内存dump,意思就是内存dump的类描述结构体数据以dex文件加载到内存后转换为ClassObject *中的类实际填充数据为基准进行内存dump,为什么要这样做呢? 因为,Android加固会对被保护的dex文件进行加固处理,对被保护的dex文件中的类数据偏移 classDataOff  进行修改并将类数据 DexClassData 进行加密处理,在类被执行时先对被加密的类数据 DexClassData 进行解密处理,然后修正类数据的偏移classDataOff  使其指向解密后正确的DexClassData数据(一般情况,被解密的DexClassData数据会存放在odex文件文件头之前的内存区域 或者 在odex文件文件尾之后的内存区域);还有一些Android加固采取的加固粒度更细,对被保护dex文件的 codeOff  进行修改并对类方法实现 DexCode 的数据进行加密处理,在类方法执行时,先解密被加密的类方法实现 DexCode的数据,然后修正指向DexCode数据的偏移 codeOff (一般情况,解密后的DexCode数据会被存放在在odex文件文件头之前的内存区域 或者 在odex文件文件尾之后的内存区域)。由于当dex文件中类被执行时,类相关的描述数据都会被解密后正确填充,因此基于Android运行时进行dex文件类数据的dump。

13. 鉴于Android进程中内存数据 4 字节对齐的要求(目的是为了提高内存数据访问的效率),因此需要对被脱壳Android进程的odex文件的内存数据进行 4 字节对齐处理,不足4字节进行 0 的数据填充,也是为了后面odex文件重组时正确的设置odex文件类数据DexClassData和DexCode的偏移。

14.为了判断被脱壳Android应用dex文件的DexClassData数据是否被Android加固所加固处理,因此需要对DexClassData数据的偏移 classDataOff 进行边界的判断,DexClassData数据保存的起始文件偏移是dex文件存放DexClassDef段数据结束的位置,DexClassData数据保存的结束文件偏移是整个odex文件结束的位置。遍历dex文件的每个类定义描述结构体DexClassDef,基于Android运行时的类数据DexClassData和DexCode的收集;在进行dex文件类数据收集时,排除过滤掉 "Landroid" 开头的Android系统类和空类的类数据DexClassData和DexCode的收集。

对于是Android系统(Landroid开头的)的类和空类,need_extra为 false 即不对 Landroid开头 的Android系统类和空类进行类实现数据DexClassData的收集,并设置这两种情况的类DexClassDef的成员变量 classDataOff 和 annotationsOff 的文件偏移值为 0 ,还有对于这两种情况的类,只保存 类定义的数据DexClassDef 到文件 /data/data/pakegname/classdef 中。这里还需要对标志 need_extra 和 pass 的意思进行说明一下,标志need_extra 的意思是是否保存类的实现数据 DexClassData 到文件/data/data/pakegname/extra 中;标志 pass的意思是 是否设置类定义DexClassDef 的成员变量 classDataOff 和 annotationsOff 的文件偏移值为 0,标志 need_extra 和 pass 的bool值总是相反的,其中pakegname为被脱壳Android应用的包名。

15.重点描述(突出DexHunter脱壳工具的基于运行时的脱壳)

未完待续~

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章