第四章 常见的 Android 文件格式(六)(ODEX)

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
发布了9 篇原创文章 · 获赞 6 · 访问量 2372

猜你喜欢

转载自blog.csdn.net/zlmm741/article/details/104758848