第十二章 软件壳(四)(代码抽取型壳)

文章目录

  • 代码抽取型壳
    • 内存重组脱壳法
    • Hook 脱壳法
    • 系统定制脱壳法
      • 脱壳工具

代码抽取型壳

  • 即第二代壳
  • 主要特点
    • 即使 DEX 已加载到内存,仍处于加密状态(所有 DEX 方法都在运行时解密)
  • 比第一代壳难脱

内存重组脱壳法

  • 代码抽取型壳经历多次技术迭代

  • 最初是将 DEX 的 DexCode 提取后填 0,将 DEX 的所有内容保存在 APK 中,APK 运行时会在内存中动态解密,所有解密的方法内容指针位于 DEX 文件结构体外部的内存中,从而有效避免了只知道 DEX 的起始地址即可快速 Dump 的问题

  • 内存重组脱壳法能有效对付此种壳,其通过解析内存中 DEX 的格式,将其重新组合成 DEX,可实现百分百 DEX 代码还原,虽然出现过一些针对内存重组的 Anti,但理论上只要 DEX 在内存中是完整的,即可通过此法脱壳

  • 在内存中加载完成的 DEX 是个 DvmDex 结构体:

    typedef struct DvmDex {
        DexFile*                pDexFile;
        const DexHeader*        pHeader;
        struct StringObject**    pResStrings;
        struct ClassObject**    pResClasses;
        struct Method**            pResMethods;
        struct Field**            pResFields;
        struct AtomicCache*        pInterfaceCache;
        MemMapping                memMap;
        pthread_mutex_t            modLock;
    } DvmDex;
    

  • pDexFile

    • 遍历它的字段即可得到 DEX 的完整内容
  • 首要问题

    • 如何在内存中定位 DvmDex
  • 通用的定位 DvmDex 结构体方法

    • Android 源码中 libdvm.so 导出一个 gDvm 符号,这是 DvmGlobals 结构体类型,其定义位于 Android 源码 dalvik/vm/Globals.h。DvmGlobals 结构体中有个 HashTable 结构体指针类型的 userDexFiles 字段

      typedef struct HashTable {
          int         tableSize;        // must be power of 2
          int            numEntries;        // current #of "live" entries
          int            numDeadEntries;    // current #of tombstone entries
          HashEntry*    pEntries;        /* array on heap */ +0x0c
          HashFreeFunc    freeFunc;
          pthread_mutex_t lock;
      } HashTable;
      

      • numEntries、pEntries
        • 描述了 HashTable 结构体的个数和结构体起始指针,通过它们可定位所有引用的 HashEntry
    • HashEntry 结构体定义

      typedef struct HashEntry {
          u4        hashValue;
          void*    data;    // DexOrJar* pDexOrJar
      };
      

      • data
        • 这是一个 DexOrJar 结构体类型的指针,描述了当前进程的 Dalvik 虚拟机环境引用的所有 DEX 和 jar 包
    • DexOrJar 结构体定义

      typedef struct DexOrJar {
          char*        fileName;
          bool        isDex;
          bool        okayToFree;
          RawDexFile*    pRawDexFile;
          JarFile*    pJarFile;
      } DexOrJar;
      

      • isDex
        • 指定当前结构体描述的是一个 DEX 结构还是 jar 包结构。若其值为 true,则 pRawDexFile 字段有效,指向 RawDexFile 结构体类型的 DEX 数据;若其值为 false,则 pJarFile 字段有效,指向 JarFile 结构体类型的 jar 包。若是 DEX 脱壳,则要关注其值为 true 时指向的 RawDexFile 结构体类型的 DEX 数据
    • RawDexFile 结构体定义

      struct RawDexFile {
          char*        cacheFileName;
          DvmDex*        pDvmDex;
      };
      

      • cacheFileName
        • DEX 的缓存文件名,通过它可初步判断该 DEX 是否为脱壳目标
      • pDvmDex
        • 即前面提到的要定位的 DvmDex 结构体,通过它可定位 DexFile 结构体,为最后的内存重组脱壳提供方便

  • 内存重组脱壳法流程

    • libdvm.so -> gDvm -> userDexFiles -> pEntries.isDex -> pRawDexFile -> pDvmDex -> pDexFile
  • 完整代码流程:dumpDex 脱壳脚本


Hook 脱壳法

  • 针对早期的第二代壳

  • 由于 DEX 代码在内存中完整解密,除了上述方法,还可用 Hook 脱壳法,在 DEX 加载后进行内存重组脱壳

  • 不同点

    • 内存重组脱壳法:在 APK 运行后的任意时刻用 kill 命令让程序暂停,然后从内存中将其重组并 Dump
    • Hook 脱壳法:不用暂停程序运行,重点在于查找合适的 Hook 点
  • 一个合适的 Hook点

    • libdvm.so 中的 dvmCallMethodV()

    • 当一个 APK 启动时,首先会执行其 Application 类的 onCreate()

    • Dalvik 虚拟机通过 dvmCallMethodV() 启动 Java 方法,其实现位于 Android 源码 dalvik/vm/interp/Stack.cpp,其函数原型:

      void dvmCallMethodV(Thread* self, const Method* method, Object* obj, bool fromJni, JValue* pResult, va_list args);
      

      • 对第二个 Method 类型的 method 参数,可通过其 name 字段判断当前执行的方法名,确定是 onCreate() 时,可进一步判断方法所在的类的名字,从而确定其是否为脱壳目标。获取 ClassObject 类型的类对象指针后,可通过其 pDvmDex 字段获取内存重组脱壳法所用的 DvmDex 结构体信息,接下来的 DEX 内存重组步骤和前述方法一样

系统定制脱壳法

  • 后期的第二代软件壳,不再一次性在内存中解密所有 DEX 方法,而在执行具体的方法时才解密方法内容
  • 如此一来,若直接内存 Dump 或 Hook 脱壳,只能提取在内存中解密过的 DEX 方法,没启动过的 DEX 方法仍处于加密状态,前述两种方法因此失效
  • 一次性将 DEX 中所有方法在内存中加载并解密是对抗这种壳的有效方法,涉及 DEX 的加载和初始化过程
  • Java 的类加载
    • 显示加载
      • 基于 ClassLoader 的 loadClass() 方式
        • 在 Dalvik 虚拟机中调用了 Dalvik_java_lang_Class_classForName()
      • 基于 Class 的 forName() 方式
    • 隐式加载
      • 调用的是 dvmResolveClass()
  • 它们在底层都会执行 Dalvik_dalvik_system_DexFile_defineClassNative(),因此可修改 Dalvik 虚拟机中该方法的实现代码,通过调用 dvmDefineClass() 手动加载 DEX 中所有的类

脱壳工具

  • DexHunter

  • 针对第二代壳的通用脱壳工具

  • 其脱壳代码的核心是 DumpClass()

    void* DumpClass(void* parament) {
        ...
        const char* header = "Landroid";
        unsigned int num_class_defs = pDexFile->pHeader->classDefsSize;
        uint32_t total_pointer = mem->length - uint32_t(pDexFile->baseAddr - (const u1*)mem->addr);
        uint32_t rec = total_pointer;
     
        while (total_pointer)
            total_pointer++;
     
        int inc = total_pointer - rec;
        uint32_t start = pDexFile->pHeader->classDefsOff + sizeof(DexClassDef) * num_class_defs;
        uint32_t end = (uint32_t)((const u1*)mem->addr + mem->length - pDexFile->baseAddr);
     
        for (size_t i = 0; i < num_class_defs; i++) {
            ...
            const DexClassDef* pClassDef = dexGetClassDef(pDvmDex->pDexFile, i);
            const char* descriptor = dexGetClassDescriptor(pDvmDex->pDexFile, pClassDef);
     
            if (!strncmp(header, descriptor, 8) || !pClassDef->classDataOff) {
                pass = true;
                goto classdef;
            }
     
            clazz = dvmDefineClass(pDvmDex, descriptor, loader);
            ...
            if (!dvmIsClassInitialized(clazz))
                if (dvmInitClass(clazz))
                    ALOGI("GOT IT init: %s", descriptor);
     
            if (pClassDef->classDataOff < start || pClassDef->classDataOff > end)
                need_extra = true;
     
            data = dexGetClassData(pDexFile, pClassDef);
            pData = ReadClassData(&data);
     
            if (!pData)
                continue;
     
            if (pData->directMethods) {
                ...
            }
     
            if (pData->virtualMethods) {
                ...
            }
     
    classdef:
            ...
            if (need_extra) {
                ...
                uint8_t* out = EncodeClassData(pData, class_data_len);
                ...
                ALOGI("GOT IT classdata written");
            }
            else {
                if (pData) {
                    free(pData);
                }
            }
            ...
        }
        ...
     
        time = dvmGetRelativeTimeMesc();
        ALOGI("GOT IT end: %d ms", time);
     
        return NULL;
    }
    

    • num_class_defs
      • 代表所有要加载的类,通过它可遍历 DEX 中的类和方法
    • dexGetClassDef()
      • 用于获取指定序号的 DEX 方法的 DexClassDef 结构体。将该结构体传递给 dexGetClassDescriptor(),可获取类的签名描述信息 descriptor
      • 要想显式加载类的签名描述信息,可调用 dvmDefineClass()
    • 对加载后的类,可遍历其实例方法和虚方法,进而修改其 DexCode
  • DexHunter 的做法是将不需要解密的数据和要解密的数据分别保存,最后合并成完整的 DEX

你可能感兴趣的:(《Android,软件安全权威指南》学习笔记)