ODEX
- 在 Android 5.0 前,主要使用的虚拟机是 Dalvik。当 APK 首次安装,或系统升级、重新启动时,为提高 DEX 的执行效率,Dalvik 会对 APK 中的 DEX 进行一定程度的优化。具体做法:解析 DEX 并生成一个 ODEX 文件,将其存放在 Android 设备的 /data/dalvikcache 目录下。以后在运行这个程序时,就不会读取 APK 中的 DEX,而是直接加载这个优化过的 ODEX,从而大大节省每次运行程序时在优化上花费的时间
生成 ODEX 文件
- 系统生成 ODEX 的方法是内部调用系统命令 dexopt。此命令不允许直接调用生成 ODEX,但 Android 在 Dalvik 时代的早期版本中,会在系统源码的 build/tools/dexpreopt/dexopt-wrapper 目录下提供 dexopt-wrapper 工具,可用于手动生成 ODEX
- 以 Crackme0201 为例,除了在 /data/dalvik-cache 目录下直接拿到 ODEX,也可执行如下命令生成 ODEX(执行此命令要有设备的 Root 权限)
- 登录设备的 shell:
adb shell
- 提升权限:
su
- 进入指定目录:
cd /data/local/tmp
- 生成 ODEX:
dexopt-wrapper /data/app/com.droider.crackme0201-1.apk crackme0201.odex
- 获取设备中的 ODEX:
adb pull /data/local/tmp/crackme0201.odex .
ODEX 文件格式
- 完整的 ODEX 文件格式图:
- ODEX 比 DEX 多了如下内容:
- DexOptHeader:ODEX 文件头,描述 ODEX 的基本信息
- Dependences:依赖库列表,描述 ODEX 加载时可能使用的依赖库
- ClassLookups:优化数据块的类索引列表信息,用于提高类搜索速度
- RegisterMaps:优化数据块的寄存器图(Register Map)信息,主要用于帮助 Dalvik 虚拟机进行精确的垃圾回收(Garbage Collection)
- 先看 DexOptHeader:
struct DexOptHeader {
u1 magic[8];
u4 dexOffset;
u4 dexLength;
u4 depsOffset;
u4 depsLength;
u4 optOffset;
u4 optLength;
u4 flags;
u4 checksum;
};
-
magic 字段:表示这是一个 ODEX 文件,取值固定为 “dex\n036”
-
dexOffset 字段:ODEX 中包含的 DEX 在 ODEX 中的偏移量
-
dexLength 字段:ODEX 中包含的 DEX 的长度
-
depsOffset 字段:依赖库列表 DependenceLists 的文件偏移
-
depsLength 字段:依赖库列表 DependenceLists 所占的字节数
-
optOffset、optLength 字段:分别表示优化数据块的文件偏移与大小
-
flags 字段:ODEX 的一些验证标志位,描述 ODEX 是否开启了验证、优化,及是否生成了寄存器图信息
-
checksum 字段:包含从依赖库列表到文件结尾的校验和信息
-
DEX 文件结构前面已学过,现在看 Dependences。Dalvik 虚拟机内部调用 writeDependencies() 将依赖(Dependency)库列表写入 ODEX,代码(片段)如下:
static int writeDependencies(int fd, u4 modWhen, u4 crc) {
...
ClassPathEntry* cpe;
...
for (cpe = gDvm.bootClassPath; cpe->ptr != NULL; cpe++) {
const char* cacheFileName =
dvmPathToAbsolutePortion(getCacheFileName(cpe));
assert(cacheFileName != NULL);
numDeps++;
bufLen += strlen(cacheFileName) + 1;
}
...
}
- 这里写入的就是全局变量 gDvm 里的 bootClassPath 变量中存的所有系统库的完整路径
- 依赖库列表信息用结构体 Dependences 表示:
struct Dependences {
u4 modWhen; // 时间戳
u4 crc; // 检验
u4 DALVIK_VM_BUILD; // Dalvik 虚拟机版本号
u4 numDeps; // 依赖库个数
struct {
u4 len; // name 字符串的长度
u1 name[len]; // 依赖库名称
kSHA1DigestLen signature; // SHA-1 散列值
} table[numDeps];
};
-
modWhen 字段:记录优化前 DEX 的时间戳
-
crc 字段:记录优化前 DEX 的 CRC 校验值
-
DALVIK_VM_BUILD 字段:Dalvik 虚拟机版本号
-
numDeps 字段:描述接下来的依赖库个数
-
每个 table 结构体字段都描述一条依赖库信息
-
len 字段:存放依赖库完整路径的长度
-
name 字段:保存依赖库完整路径
-
signature 字段:依赖库的 SHA-1 散列值
-
依赖库之后就是具体的优化数据。虚拟机通常调用 writeOptData() 向 ODEX 写优化信息,代码(片段)如下:
static bool writeOptData(int fd, const DexClassLookup* pClassLookup,
const RegisterMapBuilder* pRegMapBuilder) {
if (!writeChunk(fd, (u4)kDexChunkClassLookup,
pClassLookup, pClassLookup->size)) {
return false;
}
...
if (pRegMapBuilder != NULL) {
if (!writeChunk(fd, (u4)kDexChunkRegisterMaps,
pRegMapBuilder->data, pRegMapBuilder->size)) {
return false;
}
}
...
if (!writeChunk(fd, (u4)kDexChunkEnd, NULL, 0)) {
return false;
}
...
}
- 整个优化数据块中只有三种类型:
enum {
kDexChunkClassLookup = 0x434c4b50,
kDexChunkRegisterMaps = 0x524d4150,
kDexChunkEnd = 0x41454e44,
};
- kDexChunkClassLookup:表示 ClassLookups 数据,作用域 DEX 中的类
- kDexChunkRegisterMaps:表示 RegisterMaps 数据,作用于 DEX 中类的方法
- kDexChunkEnd:表示优化数据块结束。通常位于 ODEX 的最后
- ClassLookups 数据与 RegisterMaps 数据分别由 DexClassLookup 和 RegisterMapPool 结构体表示
将 ODEX 文件转换成 DEX 文件
- 经过优化的 ODEX 中包含与设备相关的依赖库列表 Dependeces 结构信息,不同的 Android 设备的底层 bootClassPath 环境变量中存放的系统加载库列表不尽相同,因此,将 ODEX 转换成 DEX 的过程是设备相关的
- 为将 ODEX 还原成 DEX,要先将 ODEX 反编译成 smali 文件,再将 smali 文件编译成 DEX。这个过程称“deodex”
- 反编译和编译 smali 文件使用的工具都是 smali。在使用 baksmali 命令反编译 ODEX 时,要加入参数 -d,以指定与 ODEX 相关的设备的 framework 目录。因为依赖库都来源于 Android 设备的 /system/framework 目录,所以第一步操作是将设备上的 framework 目录拉(pull)到本地,命令如下:
adb pull /system/framework ~/Program/framework_19
- 执行上述命令要有 Root 权限
- 执行如下命令完成反编译:
baksmali -a 19 -x crackme0201.odex -d ./framework -o ./outdex
- -a 参数指定 Android 设备的版本,“19”表示当前设备是 4.4 版本。-x 参数指定要操作的 ODEX。-d 参数指定刚 pull 下来的 framework 目录。-o 参数指定输出 smali 文件的目录。完成上述操作后,若没出现错误,会在 outdex 目录生成一系列 smali 文件。这时只要执行 smali 命令即可生成 DEX