首先先来看一下 Classes.dex 的文件结构,如下图所示:
大概是这么样的一个结构,主要分成了七个部分:
Dex Header # Dex 文件头部,记录整个 Dex 文件的相关属性 String Table # 字符串数据索引,记录每个字符串在数据区的偏移量 Type Table # 类型数据索引,记录每个类型的 Proto Table # 原型数据索引,记录了方法生命的字符串、返回类型和参数列表 Field Table # 字段数据索引,记录了所属类,类名及方法名 Method Table # 类方法索引,记录方法所属类名,方法声明以及方法名称等信息 Class Def Table # 类定义数据索引,记录指定类各类信息,包括借口,超类,类数据偏移量 Data Section # 数据区,保存了各个类的真实数据
而这七个部分,在解析 Classes.dex 的安卓源码中,每个部分是由一个或多个结构体( struct ) 组成的,具体哪部分是由哪些结构体组成的不细说了,对现阶段安卓逆向来说用处不大,不过可以自己看看具体是什么样子的。
这里推荐一下一个二进制查看工具:010 Editor,可以打开二进制文件并且以多种格式显示,Classes.dex 也可以利用这个工具查看。值得一提的是,它支持解析很多种不同的二进制文件格式,非常方便。
Dalvik 虚拟机运行 Classes.dex 时有一个很奇妙的特性:不会马上生成所有 Class 里的 Method,而是在上文种提到的 Class Def Table 内有一个特殊的 DexMethod(struct) 里面有一个 codeOff(u4) 元素,保存的就是对应 Method 的 DexCode(struct) 的偏移地址。每次需要运行某个 Method 时,就根据各种索引查找到对应的 DexCode。在 DexCode 结构体里有很多个 insns[n](u2) 元素,而最终 Dalvik 执行的字节码就保存在 insns 里面。
现在有很多是通过 .so 通过 HOOK libdvm.so 里的 ClassResolver 实现代码还原,不过也有像娜迦这样,利用 static Method 提前初始化的特性进行代码还原。
因为上面这个特性,Classes.dex 变得非常灵活,可以通过 Native Library 在运行状态中对 insns 重新赋值,达到 Dalvik 执行过程中自篡改的目的;也可以通过修改 CodeOff 来切换指向的内存空间,达到重定义 DexCode 的目的;等等。
很多加固频繁使用的 Dex 类抽取其实就是用的这个特性,这种情况如果没有在调用后擦除 insns 那么则可以在加载成功后进行静态dump,否则,DexHunter 是个不错的选择。