DVM 类加载原理:
DEX 文件加载到内存中 DvmDex 结构后,还没有完成类的解析工作,我们将 DEX 中的类填充到 ClassObject 结构的过程称为类加载。
ClassObject 用来描述一个完整的类,其中 Method 结构用于描述类的方法:
struct ClassObject : Object {
-- snip --
/* static, private, and <init> methods */
int directMethodCount;
Method* directMethods;
/* virtual methods defined in this class; invoked through vtable */
int virtualMethodCount;
Method* virtualMethods;
-- snip --
};
其包含了指令位置指针:
struct Method {
-- snip --
/* the actual code */
const u2* insns; /* instructions, in memory-mapped .dex */
-- snip --
};
Android DVM 提供了三种类加载方法:
(1)使用 Class.forName
显式加载
(2)使用 ClassLoader.loadClass
显式加载
(3)隐式加载,比如 new 操作符,当相应类未被访问过时,则发生隐式加载
其中,
Class.forName
调用 DVM 的 Dalvik_java_lang_Class_classForName
函数;ClassLoader.loadClass
调用 Dalvik_dalvik_system_DexFile_defineClassNative
函数;
隐式加载调用 dvmResolveClass
函数;
调用关系如下:
DEX dump 时机:
观察 Dalvik_dalvik_system_DexFile_defineClassNative
函数实现:
static void Dalvik_dalvik_system_DexFile_defineClassNative(const u4* args,
JValue* pResult)
{
-- snip --
if (pDexOrJar->isDex)
pDvmDex = dvmGetRawDexFileDex(pDexOrJar->pRawDexFile);
else
pDvmDex = dvmGetJarFileDex(pDexOrJar->pJarFile);
clazz = dvmDefineClass(pDvmDex, descriptor, loader);
-- snip --
}
选取这个函数插入脱壳代码的原因有以下:
-
函数位于关键路径
无论何种类加载方式,必然会执行到 Dalvik_dalvik_system_DexFile_defineClassNative 函数 -
包含内存中 DEX 结构,且可以通过 pDexOrJar->fileName 匹配 APK
通过 pDvmDex = dvmGetJarFileDex(pDexOrJar->pJarFile) 处代码,可以取到内存中 DEX 文件结构信息,这些信息包括:
/*
* Internal struct for managing DexFile.
*/
struct DexOrJar {
char* fileName; // Unique String,可用来命中待脱壳 APP
bool isDex;
bool okayToFree;
RawDexFile* pRawDexFile;
JarFile* pJarFile; // 内存中 zip(APK) 文件结构
u1* pDexMemory; // malloc()ed memory, if any
};
pDvmDex
表示一个打开的 ODEX 文件,DvmDex
结构体有一个 memMap
成员,用来表示 ODEX 文件对应的内存信息:
/*
* Some additional VM data structures that are associated with the DEX file.
*/
struct DvmDex {
-- snip --
/* shared memory region with file contents */
bool isMappedReadOnly;
MemMapping memMap;
-- snip --
};
其中 addr
代表这块内存起始地址,length
代表这块内存大小:
/*
* Use this to keep track of mapped segments.
*/
struct MemMapping {
void* addr; /* start of data */
size_t length; /* length of data */
void* baseAddr; /* page-aligned base address */
size_t baseLength; /* length of mapping */
};
想要 dump 目标 DEX,只需匹配 pDexOrJar->fileName 到相应的 fileName 时,通过 memMap->addr
和 memMap->length
定位到 ODEX 的内存位置, dump 出来即可。
ODEX 文件是为了提高 DVM 运行效率而设计的,它通过将引用到的 framework APIs 替换成预加载 vtable
的索引,提高方法查找和运行效率,因此 ODEX 是与具体设备强相关的,更具体来说是与 /system/framework
目录下的 odex 文件强相关的。
通过 backsmali
与 /system/framework
下 odex,便可以将 ODEX 恢复为 DEX 文件,网上有很多这方面材料,不再赘述。
解决几个问题:
目前,仍然有一个值得关注的问题:一个类加载完,其初始化(如<clinit>
)可能仍未执行。
由于 <clinit>
先于其他任何类方法执行,因此加固程序可以在 <clinit>
中做些手脚,实现对方法指令的动态修改,即初始化前我们 dump 出的 DEX 可能完全是错的。
解决办法是,使用 dvmDefineClass
遍历 DEX 的所有类,通过 dvmIsClassInitialized
判断类是否已经初始化过,并调用 dvmInitClass
主动初始化所有类。这样内存中的类,都是初始化过的,这时便可以 dump 出相对正确的 ODEX 文件。
另外还有一个问题:
图中标志的 code_off
表示一个 direct_method
方法的指令字节码相对于 ODEX 头的偏移,而它的取值范围完全可以在 ODEX 内存区域之外,因此如果单纯根据 memMap
的 addr
和 length
进行 dump, 可能缺失关键的指令数据。解决办法是将不在 ODEX 内存区域的指令单独存储为一个 extra 文件中附在 dump 出的 ODEX 之后,并修复 code_off
等偏移。
Dexhunter 的弱点:
DexHunter 通过在类加载过程中插入代码,主动遍历并初始化所有类,然后进行内存 dump。但在类初始化完成后,DVM 并不保证方法指令正确。
因此对抗 DexHunter 的一个办法是,将指令还原选在 Dalvik_dalvik_system_DexFile_defineClassNative 函数执行完,方法指令执行前的某个位置,比如 Hook dvmDefineClass 函数。
另外一个方法是自己实现 Dalvik_dalvik_system_DexFile_defineClassNative 函数,通过 pDexOrJar->fileName 可以发现,360可能用了类似办法。