目标
这里的dex文件,就是android中dalvik虚拟机运行的程序格式文件;art虚拟机,也是基于dex格式再创作的。
我们的目标,就是直观的了解下,这个dex到底是什么东东。
知识点
在开始之前,先熟悉python的两个知识点:
1. mmap
2. struct
这两个是python中的内置模块,分别是映射文件和结构化操作。
mmap
了解linux的是不是感觉很亲切?是的,功能是类似的,把一个文件映射到一段内存,然后操作内存就像直接读写文件一样。
创建的格式:
mmap函数原型
例如:
mmap调用示例
使用更简单,可以直接操作数组:
直接读写
更详细介绍参考:
对linux的mmap感兴趣的可以参考:
http://ju.outofmemory.cn/entry/224106
struct
struct是对二进制流进行操作的一个模块,直译就是数据结构,很像c语言中的结构体struct声明。它提供了从二进制流读取数据(unpack),和把数据写入二进制流的方法(pack)。
使用示例:
静态方法调用
其中'
格式对应表如下:
struct格式对应表
还可以指定字节顺序:
struct字节序
除此外,还可以指定缓冲区,这里不介绍,参考:
https://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
DEX文件格式
言归正传,我们的目标是dex文件。
文件结构:
dex文件结构
文件头结构
文件头固定0x70长度,也就是112个字节。
其中signature计算的范围是整个文件除去magic, checksum和signature三部分的其余所有部分。
checksum计算的范围是除去magic和checksum两部分的其余所有部分。
magic是dex文件标识,固定为'dex\n035'
其余大部分字段,都是在划分区域,通过(size, off)的方式,就是圈定文件的[off, off+size]部分为相关段的数据;后续的索引,也是建立在这个基础上的。
dex文件头
索引区结构
索引区有五大区域,分别是字符串区域,类名区域,函数原型区域,类属性区域,方法区域。其中每个区域的范围,可以通过头部的信息获取到,[off, size]。
这五个区域,是有相互关系的,一个区域记录的往往是另一个区域的索引。
字符串区域
从下图看,就是每四个字节是一项,这四个字节代表的是真实字符串的偏移值。
字符串区域结构
如果把这个数值作为文件的偏移值,相关的内存是这样的:
其中size是字符串的长度,str是size大小的一串字符。
字符串真实值
类名区域
这个区域存储的是所有的类名索引,结构如下,每一项用4个字节存储,表示字符串区域的索引;这里就已经有两重索引了,一重似乎类索引包含的字符串索引,一重是字符串本身的索引。
类名索引结构
函数原型区域
这个区域存储的是所有函数的原型汇总,每个原型由三部分组成,(名称,返回类型,参数类型),当然它们存储的都是索引数值。
其中名称的索引是字符串区域的索引,返回类型是类名区域的索引,而参数类型则导向另一个地方,它存储的是一个偏移数值。
函数原型结构
这个偏移数值最终导向的结构,是一个数组列表结构,由[size, itemlist]格式组成,其中size是参数的总个数,itemlist就是size大小的参数列表,列表的每一项又是一个索引数,指向类名区域的索引。
也就是说,这个结构其实就是size大小的一个类名数组,也就是我们要找寻的参数类型。
函数原型导向结构
方法区域
方法结构相比原型要简单很多,也是三部分组成,(类名,函数原型,方法名),当然也都是存的索引数值。
其中类名是指向类名区域的索引,函数原型是指向函数原型区域的索引,方法名是指向字符串区域的索引。
方法结构
类属性区域
类的属性和类的方法是相似的,由三部分组成(类名,字段类型,名称),也都是存储的索引数值;
其中类名是指向类名区域的索引,字段类型是指向类名区域的索引,名称则是指向字符串区域的索引。
类属性结构
结构先分析到这里为止,接下来我要把这些结构对应的数据都打印出来。
DEX解析库
首先分享一个现有的python库:
https://github.com/bunseokbot/dexparser
这个库是两年前写的,非常简洁明了。
其实关于dex解析的库还是很多的,我比较喜欢这个库的直观性,说到底是解析Dex的文件格式,这种c风格的写法,非常浅显易懂,直接触摸到文件结构。也因为它的简单,在它的基础之上,我们可以继续做扩充。
这个dexparser库利用了mmap和struct模块,对dex文件做解析。
画风是这样的:
初始化和解析文件头
这样的:
文件头数据赋值
还有这样的:
读取字符串列表
这里用struct,直接从dex文件的映射区域self.mmap中,通过索引读出数据,然后根据结构说明继续转索引或者直接使用。
上面的string_list代码,首先锁定了string索引的区域,[string_ids_size, string_ids_off],也就是索引的总数是string_ids_size大小;
然后根据这个大小遍历,分别从区域相关位置中取出某一个字符串的索引off;
接着,通过这个偏移数值定位到字符串真实数据的区域self.mmap[off],并根据格式[size, char]进行读取;
最后就形成了一个字符串的队列。
dexparser模块中的其它函数也是类似的。
解析和打印
现在用这个库,我要把前面介绍的文件头和索引区域的五大区域都打印出来。
构建一个Dexparser对象
(自己解压一个Apk,拿出其中的dex作为测试用)
初始化
打印文件头
打印文件头(代码)
画风是这样的:
打印文件头(结果)
打印字符串列表
打印字符串(代码)
画风是这样的:
打印字符串(结果)
打印类名列表
打印类名列表(代码)
画风是这样的:
打印类名列表(结果)
打印函数原型列表
如下图所示,Dexparser中提供的列表其实只有索引,我在这里进一步做了补充,根据函数原型的结构,将参数原型的描述也解析了出来。
打印函数原型(代码)
画风是这样的:
其中,函数名的部分我用了'%s'来替代,是为了后面解析方法区域时用的。
打印函数原型(结果)
打印方法列表
下图中就用到了上面函数原型列表中的'%s',把函数名称填写进去。
打印方法列表(代码)
画风是这样的:
打印方法列表(结果)
打印类属性列表
打印类属性列表(代码)
画风是这样的:
打印类属性列表(结果)
保存信息
最后,把打印出来的五大索引区域的列表信息,都保存到文件中。
保存到文件
【未完待续】
微信公众号: 一叶谷
领取专属 10元无门槛券
私享最新 技术干货