一、什么是odex文件1.1 定义odex文件是Android上的可执行文件。odex文件是由apk中的dex文件优化生成的,存放在cache/dalvik-cache目录下或者与apk同一目录下,最后删除apk文件中的classes.dex文件。
注意!!!不要被文件后缀名骗了! .dex后缀的文件可以是DEX文件或者OAT文件,.odex不再是ODEX文件(应该是Android 5之后),而是OAT文件,.oat是OAT文件
1.2 优势
加快应用的运行速度,因为有了odex文件包含了加载所需要的依赖库文件列表,DVM只需要检测并加载所需依赖库即可执行odex文件,大大缩短了读取dex文件所需的时间。
减少空间占用。因为如果没有odex,系统会自动提取dex,这时不仅apk内有dex,/data/dalvik-cache目录下也有dex。
反编译APK文件,只能拿到资源文件,起到了一定的保护作用。
二、odex文件结构2.1 整体结构
odex文件在dex文件头部添加了一个odex文件头,然后在dex末尾添加了dex文件的依赖库以及一些辅助数据。
由于odex文件在Android源码中并没有定义很多数据结构,仅有寥寥几个,所以在本文中会自定义一些数据结构,方便理解知识点。
在解析dex文件中,整个文件结构在源码中的定义如下:
123456789101112131415161718192021222324252627282930struct DexFile { /* directly-mapped "opt" header */ const DexOptHeader* pOptHeader; /* pointers to directly-mapped structs and arrays in base DEX */ const DexHeader* pHeader; const DexStringId* pStringIds; const DexTypeId* pTypeIds; const DexFieldId* pFieldIds; const DexMethodId* pMethodIds; const DexProtoId* pProtoIds; const DexClassDef* pClassDefs; const DexLink* pLinkData; /* * These are mapped out of the "auxillary" section, and may not be * included in the file. */ const DexClassLookup* pClassLookup; const void* pRegisterMapPool; // RegisterMapClassPool /* points to start of DEX file data */ const u1* baseAddr; /* track memory overhead for auxillary structures */ int overhead; /* additional app-specific data structures associated with the DEX */ //void* auxData;};
是的,在解析dex文件时,有一部分是没有提及到的。其中DexOptHeader就是odex头,DexLink以下的部分被称为auxillary section,即辅助数据段,它记录了dex文件被优化后添加的一些信息。
2.2 odex文件头odex文件头的数据结构定义在源码目录/dalvik/libdex/DexFile.h中,对应DexOptHeader结构体,定义如下:
123456789101112131415struct DexOptHeader { u1 magic[8]; /* odex标识(dey)+版本号 */ u4 dexOffset; /* dex文件头的文件偏移量 */ u4 dexLength; /* dex文件的总长度 */ u4 depsOffset; /* odex依赖库列表的文件偏移量 */ u4 depsLength; /* 依赖库列表的总长度 */ u4 optOffset; /* odex辅助数据的文件偏移量 */ u4 optLength; /* 辅助数据的总长度 */ u4 flags; /* 标识DVM加载odex时的优化与验证选项 */ u4 checksum; /* 依赖库与辅助数据的校验和(算法adler32) */ /* pad for 64-bit alignment if necessary */};
odex标识具体值为dey!
2.3 dex文件在上一篇文章中讲过,详见dex文件格式解析 - gla2xy’s blog (gal2xy.github.io)。
然而,在优化成odex的过程中,该部分有些内容会被修改。
2.4 依赖库依赖库对应的数据结构在Android源码中没有明确定义,借助《Android软件安全与逆向分析》中的定义:
在Android源码中没有定义的结构体,均来自《Android软件安全与逆向分析》
123456789101112struct Dependences{ u4 modWhen; /* 优化前dex文件的时间戳 */ u4 crc; /* 优化前dex文件的crc校验值 */ u4 DALVIK_VM_BUILD; /* Dalvik虚拟机版本号 */ u4 numDeps; /* 依赖库个数 */ struct{/* 依赖库结构体 */ u4 len; /* name字符串的长度 */ u1 name[len]; /* 依赖库的完整路径名 */ u1 signature[kSHA1DigestLen]; /* sha-1哈希值 */ }tabel[numDeps]; }
依赖库结构的具体操作函数在Android 2.x~4.x版本的源码才有(应该,反正5及以上没见过),目录为dalvik\vm\analysis\DexPrepare.cpp,这里手打一下:
123456789101112131415161718192021222324static int writeDependencies(int fd, u4 modWhen, u4 crc){ ...... buf = (u1*)malloc(bufLen); //分配依赖库的空间 set4LE(buf + 0, modWhen); //写入crc校验 set4LE(buf + 4, crc); //写入crc检验 set4LE(buf + 8, DALVIK_VM_BUILD); //写入Dalvik虚拟机版本号 set4LE(buf + 12, numDeps); //写入依赖库个数 ...... u1* ptr = buf + 16; //跳过前四个字段 for(cpe = gDvm.bootClassPath; cpe->ptr != NULL; cpe++){//循环写入依赖库 ...... const u1* signature = getSignature(cpe);//计算SHA-1哈希值 int len = strlen(cacheFileName) + 1;//'\0'占一字节 ...... set4LE(ptr, len); ptr += 4; memcpy(ptr, cacheFileName, len); //写入依赖库文件名 ptr += len; memcpy(ptr, signature, KSHA1DigestLen); //写入SHA-1哈希值 ptr += KSHA1DigestLen; ...... } ...}
2.5 辅助数据该部分有三个Chunk块,它们被Dalvik虚拟机加载到一个称为auxillart的段中。这三个Chunk块,都以一个header联合体开头,定义如下:
1234567union{ char raw[8]; struct{ u4 type; u4 size; }ts;}header;
union是联合体,里面的变量共用同一空间,所以大小为8字节。其中type字段为枚举常量,具体如下:
12345eum{ kDexChunkClassLookup = 0x434c4b50, /* 对应字符串CLKP */ kDexChunkRegisterMaps = 0x524d4150, /* 对应字符串RMAP */ kDexChunkEnd = 0x41454e44, /* 对应字符串AEND */}
size字段表示需要填充的数据的字节数。
在这三个Chunk块中,首先是ChunkClassLookup结构,具体如下:
123456789101112131415struct ChunkClassLookup{ header联合体; struct DexClassLookup{ int size; /* DexClassLookup结构使用的字节数,包括size字段在内 */ int numEntries; /* 接下来的tabel结构的个数 */ struct { /* 描述类的信息的结构体 */ u4 classDescriptorHash; /* 类的哈希值 */ int classDescriptorOffset; /* 类的描述,值为文件偏移量 */ int classDefOffset; /* 指向DexClassDef结构,值为文件偏移量 */ } table[1]; };};
Dalvik虚拟机通过DexClassLookup结构来检索dex文件中所有的类。
然后是ChunkRegisterMapPool结构体:
123456789101112131415struct ChunkRegisterMapPool { header联合体; struct{ struct RegisterMapClassPool{ u4 numClasses; /* 类的个数 */ u4 classDataOffset[1]; /* 指向类的映射信息的偏移量 */ }classpool; struct RegisterMapMethodPool{ u2 methodCount; /* 方法的个数 */ u4 methodData[1]; /* 方法的映射信息(连续存储) */ }; }MapPool;};
其中methodData对应RegisterMap结构体,该结构体如下:
123456struct RegisterMap { u1 format; u1 regWidth; u1 numEntries[2]; u1 data[1];};
format字段用来指明需要用多少字节来表示方法内的指令地址。
regWidth字段用来指明需要用多少字节来表示方法内各个寄存器的状态。
numEntries字段用来指明保存了多少条寄存器状态的记录。
data字段用来指向保存实际数据的地方。
最后是ChunkEnd结构体:
123struct{ header联合体;};
2.6 整个odex文件结构总览图参考其他人的图,并根据自己的理解画了个总览图,其中有误的地方烦请告知!感谢!
参考:
https://lief-project.github.io/doc/latest/tutorials/10_android_formats.html
https://newandroidbook.com/files/ArtOfDalvik.pdf
Android系统ODEX文件格式解析-CSDN博客
Dalvik虚拟机中RegisterMap结构解析-CSDN博客
https://baike.baidu.com/item/ODEX
https://www.wuyifei.cc/dex-vdex-odex-art/
《Android软件安全与逆向分析》