代码抽取型壳
- 即第二代壳
- 主要特点
- 即使 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
- numEntries、pEntries
HashEntry 结构体定义
typedef struct HashEntry { u4 hashValue; void* data; // DexOrJar* pDexOrJar };- data
- 这是一个 DexOrJar 结构体类型的指针,描述了当前进程的 Dalvik 虚拟机环境引用的所有 DEX 和 jar 包
- data
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 数据
- isDex
RawDexFile 结构体定义
struct RawDexFile { char* cacheFileName; DvmDex* pDvmDex; };- cacheFileName
- DEX 的缓存文件名,通过它可初步判断该 DEX 是否为脱壳目标
- pDvmDex
- 即前面提到的要定位的 DvmDex 结构体,通过它可定位 DexFile 结构体,为最后的内存重组脱壳提供方便
- cacheFileName
内存重组脱壳法流程
libdvm.so->gDvm->userDexFiles->pEntries.isDex->pRawDexFile->pDvmDex->pDexFile
完整代码流程:dumpDex 脱壳脚本
Hook 脱壳法
针对早期的第二代壳
由于 DEX 代码在内存中完整解密,除了上述方法,还可用 Hook 脱壳法,在 DEX 加载后进行内存重组脱壳
不同点
- 内存重组脱壳法:在 APK 运行后的任意时刻用
kill命令让程序暂停,然后从内存中将其重组并 Dump - Hook 脱壳法:不用暂停程序运行,重点在于查找合适的 Hook 点
- 内存重组脱壳法:在 APK 运行后的任意时刻用
一个合适的 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()
- 在 Dalvik 虚拟机中调用了
- 基于 Class 的 forName() 方式
- 基于 ClassLoader 的 loadClass() 方式
- 隐式加载
- 调用的是 dvmResolveClass()
- 显示加载
- 它们在底层都会执行
Dalvik_dalvik_system_DexFile_defineClassNative(),因此可修改 Dalvik 虚拟机中该方法的实现代码,通过调用 dvmDefineClass() 手动加载 DEX 中所有的类
脱壳工具
针对第二代壳的通用脱壳工具
其脱壳代码的核心是 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