Dalvik模式下基于Android运行时类加载的函数dexFindClass脱壳
阅读原文时间:2021年09月06日阅读:1

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

前段时间在看雪论坛发现了《发现一个安卓万能脱壳方法》这篇文章,文章说的很简略,其实原理很简单也很有意思,说白了还是dalvik虚拟机模式下基于Android运行时的内存dex文件的dump,对一些免费版本的加固壳还是有效果的,dalvik模式下二代之后的加固壳就不行了。文章脱壳的原理涉及到dalvik模式下dex文件的类查找和加载的过程,下图是dalvik模式下dex文件的类查找和加载的流程示意图(native层的实现):

Dalvik虚拟机模式下Android普通apk应用的类方法的调用过程:

先通过类方法所在类的类签名字符串查找到指定的目标类的 ClassObject描述对象,然后通过查找到的目标类对象 ClassObject查找获取到该类方法的描述结构体Method,再通过类方法描述结构体Method进行来方法的调用。dalvik模式下基于 dexFindClass 函数脱壳的原理就是在指定类签名字符串目标类的查找和加载过程中寻找脱壳点,
Dalvik_dalvik_system_DexFile_defineClassNative函数-->dvmDefineClass函数-->findClassNoInit函数-->dexFindClass函数 这整个流程是Android普通应用类查找和加载的流程,dexFindClass函数用于查找类的 DexClassDef 结构,Android普通应用目标类的查找和加载实现主要是在函数findClassNoInit里实现,http://androidxref.com/4.4.4_r1/xref/dalvik/vm/oo/Class.cpp#1473

Android普通Apk应用类方法的查找流程梳理:

1.Android普通Apk应用类方法的查找和加载实现主要是从 函数findClassNoInit 开始的,很明显函数findClassNoInit调用完成之后返回是ClassObject类型的指针,ClassObject对象就是dex文件中类加载到内存之后的表现形式,类方法的调用也是先获取到类方法所在类的描述结构体ClassObject。

2 .dalvik模式下Android类的查找和加载过程中,先调用 函数dvmLookupClass 进行类查找,如果查找不到指定类签名字符串的目标类,则进行目标类加载处理,意思就是说Android普通apk应用的在进行目标类的第一次查找时,目标类的ClassObject描述对象肯定是不存在的,需要先进行目标类的加载才会生成目标类的ClassObject描述对象,下次再查找该目标类就不用再进行类加载了。在进行目标类的加载时先调用
函数dexFindClass 获取到目标类的描述结构体 DexClassDef。

3. 将指定目标类的 DexClassDef传给函数loadClassFromDex进行目标类的加载,函数loadClassFromDex返回之后得到就是内存加载后的类 ClassObject。

函数loadClassFromDex先通过目标类的DexClassDef描述结构体,获取目标类的DexClassData信息结构体,接着根据DexClassData信息结构体获取到目标类的DexClassDataHeader描述结构体,然后将目标类的DexClassDef、DexClassData、DexClassDataHeader等类的描述结构体信息传给函数loadClassFromDex0,最终由函数loadClassFromDex0进行目标类的内存加载。

/*
 * Try to load the indicated class from the specified DEX file.
 *
 * This is effectively loadClass()+defineClass() for a DexClassDef.  The
 * loading was largely done when we crunched through the DEX.
 *
 * Returns NULL on failure.  If we locate the class but encounter an error
 * while processing it, an appropriate exception is thrown.
 */
static ClassObject* loadClassFromDex(DvmDex* pDvmDex,
    const DexClassDef* pClassDef, Object* classLoader)
{
    ClassObject* result;
    DexClassDataHeader header;
    const u1* pEncodedData;
    const DexFile* pDexFile;

    assert((pDvmDex != NULL) && (pClassDef != NULL));
    pDexFile = pDvmDex->pDexFile;

    if (gDvm.verboseClass) {
        ALOGV("CLASS: loading '%s'...",
            dexGetClassDescriptor(pDexFile, pClassDef));
    }

    // 通过目标类的DexClassDef获取到目标类的DexClassData
    pEncodedData = dexGetClassData(pDexFile, pClassDef);

    if (pEncodedData != NULL) {
        // 获取到目标类的DexClassDataHeader
        dexReadClassDataHeader(&pEncodedData, &header);
    } else {
        // Provide an all-zeroes header for the rest of the loading.
        memset(&header, 0, sizeof(header));
    }

    // 根据传入的目标类的DexClassDef、DexClassData以及DexClassDataHeader
    // 对目标类进行内存加载得到目标类内存加载之后的类描述结构体ClassObject
    result = loadClassFromDex0(pDvmDex, pClassDef, &header, pEncodedData,
            classLoader);

    if (gDvm.verboseClass && (result != NULL)) {
        ALOGI("[Loaded %s from DEX %p (cl=%p)]",
            result->descriptor, pDvmDex, classLoader);
    }

    return result;
}

4. 指定类签名字符串的目标类加载到内存以后,将其添加到普通apk应用进程的类Hash表中,方便下次该类的查找,以后查找该类的时候就不用再加载了,直接查找就能查找到。

/*
 * Add a new class to the hash table.
 *
 * The class is considered "new" if it doesn't match on both the class
 * descriptor and the defining class loader.
 *
 * TODO: we should probably have separate hash tables for each
 * ClassLoader. This could speed up dvmLookupClass and
 * other common operations. It does imply a VM-visible data structure
 * for each ClassLoader object with loaded classes, which we don't
 * have yet.
 */
bool dvmAddClassToHash(ClassObject* clazz)
{
    void* found;
    u4 hash;

    // 对指定签名字符串的类做Hash处理
    hash = dvmComputeUtf8Hash(clazz->descriptor);
    // 锁
    dvmHashTableLock(gDvm.loadedClasses);
    // 指定类的签名字符串hash和类加载后描述结构体ClassObject
    // 添加的进程的loadedClasses的哈希表中
    found = dvmHashTableLookup(gDvm.loadedClasses, hash, clazz,
                hashcmpClassByClass, true);
    dvmHashTableUnlock(gDvm.loadedClasses);

    ALOGV("+++ dvmAddClassToHash '%s' %p (isnew=%d) --> %p",
        clazz->descriptor, clazz->classLoader,
        (found == (void*) clazz), clazz);

    //dvmCheckClassTablePerf();

    /* can happen if two threads load the same class simultaneously */
    return (found == (void*) clazz);
}

5.指定签名字符串的目标类的ClassObject查找函数dvmLookupClass的实现。

/*
 * Search through the hash table to find an entry with a matching descriptor
 * and an initiating class loader that matches "loader".
 *
 * The table entries are hashed on descriptor only, because they're unique
 * on *defining* class loader, not *initiating* class loader.  This isn't
 * great, because it guarantees we will have to probe when multiple
 * class loaders are used.
 *
 * Note this does NOT try to load a class; it just finds a class that
 * has already been loaded.
 *
 * If "unprepOkay" is set, this will return classes that have been added
 * to the hash table but are not yet fully loaded and linked.  Otherwise,
 * such classes are ignored.  (The only place that should set "unprepOkay"
 * is findClassNoInit(), which will wait for the prep to finish.)
 *
 * Returns NULL if not found.
 */
ClassObject* dvmLookupClass(const char* descriptor, Object* loader,
    bool unprepOkay)
{
    ClassMatchCriteria crit;
    void* found;
    u4 hash;

    crit.descriptor = descriptor;
    crit.loader = loader;
    // 对指定类的签名字符串做hash处理
    hash = dvmComputeUtf8Hash(descriptor);

    LOGVV("threadid=%d: dvmLookupClass searching for '%s' %p",
        dvmThreadSelf()->threadId, descriptor, loader);

    dvmHashTableLock(gDvm.loadedClasses);
    // 根据指定类的签名hash值查找该目标类的ClassObject
    found = dvmHashTableLookup(gDvm.loadedClasses, hash, &crit,
                hashcmpClassByCrit, false);
    dvmHashTableUnlock(gDvm.loadedClasses);

    /*
     * The class has been added to the hash table but isn't ready for use.
     * We're going to act like we didn't see it, so that the caller will
     * go through the full "find class" path, which includes locking the
     * object and waiting until it's ready.  We could do that lock/wait
     * here, but this is an extremely rare case, and it's simpler to have
     * the wait-for-class code centralized.
     */
    if (found && !unprepOkay && !dvmIsClassLinked((ClassObject*)found)) {
        ALOGV("Ignoring not-yet-ready %s, using slow path",
            ((ClassObject*)found)->descriptor);
        found = NULL;
    }

    return (ClassObject*) found;
}

6. Dalvik模式下基于Android运行时类加载的函数dexFindClass脱壳的要点: 由于一些免费版的apk加固基本都是dex文件的整体加载,粒度比较粗还没有细分到dex文件类方法的加固处理,在dalvik模式下Android运行时的类加载需要使用到内存加载的dex文件(此时一代的加固apk一般都已经在内存里解密完成,因此在解密后dex文件的类加载时,我们可以获取到解密后的dex文件的内存地址,这里选择在类加载的相关函数dexFindClass中进行内存dex文件的获取和dump处理),很多的普通apk应用都会调用dexFindClass函数,那么该怎么设置dex文件内存dump的过滤条件呢?被加固的dex虽然dex文件整体被加固了,类被隐藏了但是碍于加固的处理方法被加固dex文件的主Activity 还是暴露给我们了,故将被加固dex文件的主Activity类的签名字符串作为过滤条件。

7.  以Android 4.4.4版本的系统为例,在dalvik虚拟机动态库文件libdvm.so 中,dvmFindClassNoInit、dvmFindDirectMethodByDescriptor、dvmFindVirtualMethodByDescriptor、dvmFindLoadedClass、dvmFindLoadedClass、dvmFindClass、dexFindClass 等函数是导出函数,可以被Hook掉。

8. Dalvik模式下基于Android运行时类加载的函数dexFindClass脱壳,对Android源码的修改(以Android 4.4.4 r1的源码为例),主要代码修改在源码文件 /dalvik/libdex/DexFile.cpp 中,修改如下:

// *******添加脱壳的代码********************************************************
// http://androidxref.com/4.4.4_r1/xref/dalvik/libdex/DexFile.cpp

//#include <asm/siginfo.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

// 需要脱壳的apk的主activity的名称字符串
static char mainActivityName[128] = {0};
// 内存dex文件dump后文件夹
// /data/data/apk应用的包名/
static char dexDumpName[128] = {0};
// 记录脱壳配置文件是否读取的标记
static bool readable = true;
// 信号互斥量
static pthread_mutex_t read_mutex;

// 线程回调函数
void* ReadThread(void *arg)
{
    FILE *fp = NULL;
    while (mainActivityName[0] == 0 || dexDumpName[0] == 0)
    {
        // 读取脱壳的配置文件/data/local/tmp/dex_dump_ok的信息
        fp = fopen("/data/local/tmp/dex_dump_ok", "r");
        if (fp==NULL)
        {
            sleep(1);
            continue;
        }

        // 读取需要脱壳的apk的主activity的名称字符串(第1行)
        fgets(mainActivityName, 128, fp);
        mainActivityName[strlen(mainActivityName)-1] = 0;

        // 读取脱壳apk的内存dex文件dump后的文件名称字符串(第2行)
        fgets(dexDumpName, 128, fp);
        dexDumpName[strlen(dexDumpName)-1] = 0;

        fclose(fp);
        fp = NULL;
    }

    // 释放信号量
    pthread_mutex_lock(&read_mutex);
    return NULL;
}
// *******添加脱壳的代码********************************************************

/*
 * Look up a class definition entry by descriptor.
 *
 * "descriptor" should look like "Landroid/debug/Stuff;".
 */
const DexClassDef* dexFindClass(const DexFile* pDexFile,
    const char* descriptor)
{
    const DexClassLookup* pLookup = pDexFile->pClassLookup;
    u4 hash;
    int idx, mask;

    // *******添加脱壳的代码****************************************************
    // 1.读取配置文件(获取需要脱壳的apk的主activity类字符串)
    int uid = getuid();
    // 过滤掉系统进程
    if (uid)
    {
        // 打印当前被查找的类名称
        ALOGI("dexFindClass--DexFile addr: 0x%08x, Class descriptor: %s", (int)pDexFile, descriptor);

        if (readable)
        {
            // 创建互斥信号通量
            pthread_mutex_lock(&read_mutex);
            if (readable)
            {
                readable = false;

                // 释放互斥信号通量
                pthread_mutex_unlock(&read_mutex);
                pthread_t read_thread;
                // 创建线程,读取脱壳配置文件/data/local/tmp/dex_dump_ok的信息
                pthread_create(&read_thread, NULL, ReadThread, NULL);
            }
            else
            {
                // 释放互斥信号通量
                pthread_mutex_unlock(&read_mutex);
            }
        }
    }

    // 2,进行需要脱壳的apk的主activity类字符串的匹配
    // 格式:"Landroid/debug/Stuff;"
    if (strcmp(mainActivityName, descriptor) == 0) {

        // 3.匹配成功进行内存dex文件的dump处理
        char szBuffer[128] = {0};
        // 字符串拼接得到内存dex文件的dump路径
        // /data/data/com.example.seventyfour.tencenttest/
        strcat(szBuffer, dexDumpName);
        strcat(szBuffer, "dump_dex_over");
        // 打印dex文件的dump文件路径
        ALOGI("DEX_DUMP_PATH: %s", szBuffer);

        // 创建新文件保存dump的内存dex文件
        FILE* file = fopen(szBuffer, "wb+");
        if (file == NULL) {

            ALOGI("DEX_DUMP_PATH--fopen: %s error !", szBuffer);
        } else {

            // 保存三倍dex文件长度(比较暴力)
            // 暂时不考虑Hook系统函数write或者read反内存dump的情况
            fwrite(pDexFile->baseAddr, (pDexFile->pHeader->fileSize)*3, 1, file);
            // 关闭文件
            fclose(file);
        }
        // 打印内存dex文件的信息
        ALOGI("DEX_DUMP_PATH--addr: 0x%08x, lenth: %d", pDexFile->baseAddr, pDexFile->pHeader->fileSize);

    }
    // *******添加脱壳的代码****************************************************

    hash = classDescriptorHash(descriptor);
    mask = pLookup->numEntries - 1;
    idx = hash & mask;

    /*
     * Search until we find a matching entry or an empty slot.
     */
    while (true) {
        int offset;

        offset = pLookup->table[idx].classDescriptorOffset;
        if (offset == 0)
            return NULL;

        if (pLookup->table[idx].classDescriptorHash == hash) {
            const char* str;

            str = (const char*) (pDexFile->baseAddr + offset);
            if (strcmp(str, descriptor) == 0) {
                return (const DexClassDef*)
                    (pDexFile->baseAddr + pLookup->table[idx].classDefOffset);
            }
        }

        idx = (idx + 1) & mask;
    }
}

9.  将Android 4.4.4 r1的源码文件 /dalvik/libdex/DexFile.cpp 按上面的修改以后,重新编译dalvik虚拟机的源码生成新的动态库文件libdvm.so,make snod 重新生成新的Android系统镜像文件system.img,重启Nexus 5手机进入刷机模式,使用新的Android系统镜像文件system.img进行刷机。加固apk脱壳的时候使用比较简单,先安装需要脱壳的apk应用到手机设备上,按下面的格式构建脱壳配置文件
dex_dump_ok,adb push 脱壳配置文件 dex_dump_ok 到手机设备 /data/local/tmp 文件夹下,运行需要脱壳的加固apk,过一会儿在脱壳apk应用的包名路径下就会生成内存dump的dex文件
dump_dex_over

adb push  dex_dump_ok  /data/local/tmp

10. Dalvik模式下基于Android运行时类加载的函数dexFindClass脱壳,只针对第一代加壳的apk脱壳比较有效,我这里是通过修改Android源码的方式来进行内存dex文件dump脱壳操作,比较麻烦,由于dexFindClass函数 在动态库文件 libdvm.so 中是导出函数,因此也可以使用Hook dexFindClass函数的方式来进行dex文件内存dump的脱壳。整体来说,Dalvik模式下基于Android运行时类加载的函数dexFindClass脱壳的原理比较简单,主要是为了熟悉一下dalvik模式下dex文件类查找和加载的流程。

参考资料:

发现一个安卓万能脱壳方法