转载

Android逆向分析(3) Android可执行文件之谜 - DEX与ODEX, OAT与ELF

WORKING - 未完

前言

米娜桑,是时候揭开DEX的面纱了!我们都知道multidex,都知道65535方法数超标,那DEX到底是个什么东西呢?或许又有些同学知道DEX会优化为ODEX,那ODEX又是什么鬼,优化了什么呢?为什么ClassLoader热补丁方案插入构造函数导致CLASS_ISPREVERIFIED为false后,会对性能造成影响,和ODEX又有什么关系呢?

我们又知道5.0以上Android虚拟机变成了Art,那DEX在art上变成了什么呢?为什么安装特别耗时间?有时候我看着我的Nexus6安装一个应用在那进度条读啊读的好像卡住了,有一种想砸了它的想法,所以当我拿到Nexus 5测试机的时候,第一件事就是刷到4.4,不然每次安装的效率实在不能忍(捂脸)。

DEX是什么

直接把apk当成zip打开后,第一级目录你就会看见有classes.dex,这就是我们要揭开面纱的东西了。

Why DEX

为什么需要DEX,class不行吗?相应地,为什么需要Dalvik虚拟机,JVM不行吗?

Dalvik虚拟机是专门为了Android移动平台设计的。目标系统的RAM有限,数据存储在缓慢的内部闪存上,而且性能和上个世纪的周免系统相当。它们运行Linux,来提供虚拟内存,进程和线程,以及基于UID的安全机制。

这些特征和限制使我们聚焦在这些目标上:

  • Class数据,尤其是字节码,必须被多个进程共享,以最小化系统内存使用。
  • 启动一个新app的开销必须最小化,来保证设备的可响应。
  • 在独立的文件存储class数据可能导致很多冗余,尤其是字符串。为了保证磁盘空间,我们需要把这些因子提出来。
  • 解析class数据的fields在class读取的时候增加了很多不必要的开销。把数据值直接当成C类型(比如整数或字符串)使用会更好。
  • 字节码验证是必要的,却也是缓慢的。所以我们想在app执行外尽量验证更多,以便不要影响app本身体验。
  • 字节码优化(加速指令,精简方法)对速度和电池生命很重要。
  • 为了安全原因,进程不能编辑共享代码。

典型的虚拟机执行从压缩文件解压独立的类,然后把它们存到heap上。这就导致了每个class可能在每个进程有独立的拷贝,从而使得应用启动变慢,因为代码必须被解压(或者至少需要从磁盘的很多小片段去读取)。另一方面,在本地heap放置字节码简化了首次使用时的指令重写,从而可能导致一些不同的优化。

这些目标指引了一些基本决定:

  • 多个class被聚集到一个单个的DEX文件。
  • DEX文件被映射为只读,并且在进程间共享。
  • 针对本地系统调整字节码顺序和词对齐。
  • 字节码验证对所有class都是强制的,但我们想要对一切可能的进行”预验证(pre-verify)”。
  • 需要重写字节码的优化必须提前执行。

而Dalvik虚拟机和DEX也就应运而生。

Hello DEX

让我们手动来生成一个java,编译成javac,然后转换为dex看看:

echo 'class Foo {'/
'public static void main(String[] args) {'/
'System.out.println("Hello, world"); }}' > Foo.java
javac Foo.java
dx --dex --output=foo.jar Foo.class
adb push foo.jar /sdcard/
adb shell dalvikvm -cp /sdcard/foo.jar Foo

当我们在dx命令的output中指定输出文件后缀为.jar,.zip,或者.apk,名为classes.dex的文件就会被创建并保存在压缩包内。解开Foo.jar你就会看到classes.dex和META-INF文件夹(里面只有一个MANIFEST.MF文件)。

我们创建完该jar后直接push到设备上,并通过shell直接让dalvik虚拟机去运行它,如果操作无误,会看到命令行的反馈 - Hello, world。

DEX in file system

这次我打算多画点图,所以看图说话吧:

Android逆向分析(3) Android可执行文件之谜 - DEX与ODEX, OAT与ELF

DEX in memory

为什么DEX不能被内存映射,或者说,不能直接从zip去执行呢?因为数据是压缩的,文件头也不保证是词对齐的。这些问题可以通过不压缩直接保存为classes.dex和填充zip文件来解决,但会导致数据网络间传输的包体积变大。

我们需要在使用前把zip包里的classes.dex解压。当我们拿到文件的时候,我们可能还会做些之前提到的其他操作(对齐、优化、验证)。这又引出了另一个问题:谁去负责做这些,我们又该把输出放在哪儿?

ODEX是什么

ODEX,全名Optimized DEX,即优化过的DEX。

有至少3种方法去创建一个“准备好的”DEX文件,即ODEX:

  1. 虚拟机“即时(just in time)”执行。输出会跑到一个特殊的dalvik-cache目录。这只在一些特殊的桌面和工程机的设备上使用(这些机器的build中,dalvik-cache目录的权限不是严格的)。在生产机器上这是不被允许的。
  2. 系统的安装器在程序首次安装时候执行,它有写dalvik-cache的权限。
  3. 构建(build)系统预先执行。相关的 jar / apk 文件还在,但classes.dex被剥离出来了。ODEX和原来的zip包保存在一起,不在dalvik-cache,而是系统镜像的一部分。

dalvik-cache目录更准确地说是$ANDROID_DATA/data/dalvik-cache。里面的文件的名字来源于源DEX的完整路径。在设备上该目录被system所拥有,而system拥有0771权限,保存在那里的ODEX被系统和应用的组所拥有,权限为0644。数字权限保护的应用会使用640权限来防止其他应用去检测它们。底线是你可以读取自己的与其他大部分应用的DEX文件,但你不能创建、修改,或删除它们。

前两种方法的执行分为以下三个步骤:

首先,dalvik-cache文件被创建。这必须在一个有恰当权限的进程进行,所以在“系统安装器”的场景,是在运行为root的installd进程执行的。

接着,classes.dex从zip包中解压出来。文件头部留出一小块空间给ODEX header。

最后,文件被内存映射以便读取,并被调整以使用在当前系统。这包括了字节交换(byte-swapping),结构重新排列(structure realigning),但并没有对DEX文件做有意义的改变。还做了一些其他的基本结构检查,比如确保文件偏移量和数据索引落在有效范围内。

构建系统不在桌面上运行工具,而宁愿去启动模拟器,强制所有相关DEX文件的即时优化,然后从dalvik-cache把结果提取出来。这样做的原因,在解释完优化后会变得更显而易见。

一旦代码被字节替换和对齐,我们就可以继续了。我们添加了一些预计算的数据,在文件头填写ODEX header,然后开始执行。然而,如果我们对验证和优化有兴趣,就需要在初始准备后再插入一个步骤。

dexopt的魔法

在Android 2.3版本以前,系统源码中提供了生成odex的工具dexopt-wrapper,位于Android 2.2系统源码的 build/tools/dexpreopt/dexopt-wrapper/ 目录下,查看 DexOptWrapper.cpp 文件会发现实际调用的是 /system/bin/dexopt 程序。在5.0及以上版本的设备上,你可能已经再也找不到dexopt了,取而代之的是dex2oat。

我们想要验证和优化DEX文件里的所有class。最简单和安全的方法就是把所有class读到虚拟机,然后跑一遍。任何读取失败的就是验证/优化失败的。不幸的是,这可能导致一些资源的分配难以释放(比如native共享库的读取),所以我们不想执行在应用运行的虚拟机里。

解决方案就是起一个叫做dexopt的程序(事实上就是虚拟机的后门)。它会执行一个简短的虚拟机初始化,从引导的类路径载入0个或多个DEX文件,然后开始做一切从目标DEX可以做的验证和优化。结束后,进程退出,释放所有资源。

因为多个虚拟机可能同时需求同一个DEX文件,文件锁被用来确保dexopt仅被执行一次。

Hello ODEX

我们继续玩耍之前生成的dex,来做一个odex:

adb push dexopt-wrapper /sdcard/

adb shell
# 不然没权限去/data/local
su
chmod 777 dexopt-wrapper
# 直接在sdcard执行会提示权限错误
cp dexopt-wrapper /data/local/
cp foo.jar /data/local/
cd /data/local
/dexopt-wrapper foo.jar foo.odex
--- BEGIN 'foo.jar' (bootstrap=0) ---
--- waiting for verify+opt, pid=5220
--- would reduce privs here
--- END 'foo.jar' (success) ---
cp foo.odex /sdcard
exit
exit

adb pull /sdcard/foo.odex .

这样子就拿到了优化后的odex,赶紧把手机还给同事。

oat与elf

下期预告

下一次让我们利用本次讲到的这些知识,来改一改apktool,让它能重返19岁,反编译腾讯的apk。最后代码会丢到GitHub上。

参考资料

  • https://zh.wikipedia.org/wiki/Android_Runtime
  • http://blog.csdn.net/luoshengyang/article/details/18006645
  • AOSP: dalvik/docs
原文  http://blog.zhaiyifan.cn/2016/02/24/android-reverse-3/
正文到此结束
Loading...