ODEX文件格式解析

一、什么是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 优势

  1. 加快应用的运行速度,因为有了odex文件包含了加载所需要的依赖库文件列表,DVM只需要检测并加载所需依赖库即可执行odex文件,大大缩短了读取dex文件所需的时间。
  2. 减少空间占用。因为如果没有odex,系统会自动提取dex,这时不仅apk内有dex,/data/dalvik-cache目录下也有dex。
  3. 反编译APK文件,只能拿到资源文件,起到了一定的保护作用。

二、odex文件结构

2.1 整体结构

odex文件在dex文件头部添加了一个odex文件头,然后在dex末尾添加了dex文件的依赖库以及一些辅助数据。

由于odex文件在Android源码中并没有定义很多数据结构,仅有寥寥几个,所以在本文中会自定义一些数据结构,方便理解知识点。

在解析dex文件中,整个文件结构在源码中的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct 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结构体,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct 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软件安全与逆向分析》

1
2
3
4
5
6
7
8
9
10
11
12
struct 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,这里手打一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static 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联合体开头,定义如下:

1
2
3
4
5
6
7
union{
char raw[8];
struct{
u4 type;
u4 size;
}ts;
}header;

union是联合体,里面的变量共用同一空间,所以大小为8字节。其中type字段为枚举常量,具体如下:

1
2
3
4
5
eum{
kDexChunkClassLookup = 0x434c4b50, /* 对应字符串CLKP */
kDexChunkRegisterMaps = 0x524d4150, /* 对应字符串RMAP */
kDexChunkEnd = 0x41454e44, /* 对应字符串AEND */
}

size字段表示需要填充的数据的字节数。

在这三个Chunk块中,首先是ChunkClassLookup结构,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct 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结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ChunkRegisterMapPool {

header联合体;

struct{
struct RegisterMapClassPool{
u4 numClasses; /* 类的个数 */
u4 classDataOffset[1]; /* 指向类的映射信息的偏移量 */
}classpool;
struct RegisterMapMethodPool{
u2 methodCount; /* 方法的个数 */
u4 methodData[1]; /* 方法的映射信息(连续存储) */
};
}MapPool;
};

其中methodData对应RegisterMap结构体,该结构体如下:

1
2
3
4
5
6
struct RegisterMap {
u1 format;
u1 regWidth;
u1 numEntries[2];
u1 data[1];
};

format字段用来指明需要用多少字节来表示方法内的指令地址。

regWidth字段用来指明需要用多少字节来表示方法内各个寄存器的状态。

numEntries字段用来指明保存了多少条寄存器状态的记录。

data字段用来指向保存实际数据的地方。

最后是ChunkEnd结构体:

1
2
3
struct{
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软件安全与逆向分析》


ODEX文件格式解析
http://example.com/2023/11/17/Android安全/ODEX文件格式解析/
作者
gla2xy
发布于
2023年11月17日
许可协议