Dex文件内容解析APK相关信息
2017-12-07 16:01
369 查看
Dex文件格式
我们都知道Android项目在构建的时候,会将class文件的jar包通过dx工具将其转化成dex文件,目的是将所有的class文件整合到一个dex文件中,这样的目的是降低冗余,因为每个class的数据格式都相同,dex通过将相同的内容方法一起,使文件结构更加紧凑。通过上图可以看出,dex文件将方法信息、字段信息、类型信息等都相同的信息都放到了一起,从而省下很多空间。
Dex文件组成(这里只介绍了重要部分):
官网查看全部结构
名称 | 说明 |
---|---|
header | dex文件头部,记录了整个dex文件相关信息的起点,很重要 |
string_ids | 存储dex信息相关内容的字符串在数据区的索引位置 |
type_ids | 存储的是类型(各种数据类型,比如类的类型,方法返回值类型等) |
proto_ids | 方法原型的描述,记录了方法的返回值类型,参数列表等 |
field_ids | 字段数据,记录了字段所属类型,类型等 |
method_ids | 记录方法信息,包括方法名,所属类名,方法原型等 |
class_defs | 存储的所有类的索引位置 |
data | 数据区,上面的所有数据都是里面获取的,以及存储dex的数据 |
数据类型
上面的数据类型是dex中的数据存储的相关数据的所有的数据类型,主要是其对应的字节长度,因为读取相关dex中对应的相关信息是通过其内容的数据类型,进入读取指定的长度的数据,也就是我们要的内容了。上面唯一没有特殊的数据类型是LEB128,它没有固定的长度,他是1~5个字节可变的数据类型,主要是针对存储的数据是不定长度的。这种数据类型原理:首先判断第一个字节的最高位是否是1,如果是1,说明当前数据包括下一个字节;继续监测第二个字节的最高位是否为1,以此类推直到下一个字节的最高位是0,表示当前数据完毕。
官方文档
这种数据类型的数据读取方式,Android系统读取方式
源码路径: /dalvik/libdex/Leb128.h
DEX_INLINE int readUnsignedLeb128(const u1** pStream) { const u1* ptr = *pStream; int result = *(ptr++); //取第一个字节 if (result > 0x7f) { //如果第1个字节大于0x7f,表示第一个字节最高位为1 int cur = *(ptr++); //第2个字节 result = (result & 0x7f) | ((cur & 0x7f) << 7); // &0x7f表示去掉这个这个字节的第8位。 合并第一位和第二位的数据 //下面依次读取第3,4,5位的数据 if (cur > 0x7f) { cur = *(ptr++); result |= (cur & 0x7f) << 14; if (cur > 0x7f) { cur = *(ptr++); result |= (cur & 0x7f) << 21; if (cur > 0x7f) { /* * Note: We don't check to see if cur is out of * range here, meaning we tolerate garbage in the * high four-order bits. */ cur = *(ptr++); result |= cur << 28; } } } } *pStream = ptr; return result; }
while循环读取,js代码:
jDataView.prototype.readStringUtf = function () { var len = this.readUnsignedLeb128(); return this.getString(len); // 默认采用unicode读取每个字节的数据(单个字节就和ASCII编码的方式相同了,ASCII最早出现,也是为英文出现的) }; // ulb128 是dex特有的类型,长度是1-5的可变长度,如果当前字节最高位为1,那么下一个字节也在当前数据中;下面的读取方式:不会有5此限制 // 获取去除leb128类型的数据, jDataView.prototype.readUnsignedLeb128 = function () { var result = 0; do { var b = this.getUint8(); // 读取一个字节,return unsigned 8-bit integer; 转换成数字的时候会当成有符号数来转 result = (result << 7) | (b & 0x7f); // 将每个字节最高位去掉,合并出最终的数据 } while (b < 0); // 第一位如果是1(也就是负数,最高位为1),那么高位还有数据 return result; };
Dex header的结构
字段 | 长度(byte) | 说明 |
---|---|---|
magic | 8 | 魔法值。dex文件的唯一标识, 值为dex\n035\0,现在android8已经支持dex\n039\0 |
checksum | 4 | 除去magic和checksum剩余内容的adler32校验和;用于检测当前文件损坏情况 |
signature | 20 | 除了上面三个字段文件剩余内容的SHA-1值,文件的唯一标识 |
file_size | 4 | dex文件的大小,单位:byte |
header_size | 4 | header的大小 |
endian_tag | 4 | 字节序标记,两种,关于这个可以自行查阅 |
link_size | 4 | 链接区大小 |
link_off | 4 | 从文件开头到链接区的偏移量 |
map_off | 4 | 从文件开头到映射项的偏移量 |
string_ids_size | 4 | string_ids的数组长度 |
string_ids_off | 4 | string_ids的偏移量 |
type_ids_size | 4 | type_ids的长度 |
type_ids_off | 4 | type_ids的偏移量 |
proto_ids_size | 4 | proto_ids的长度 |
proto_ids_off | 4 | proto_ids的偏移量 |
field_ids_size | 4 | field_ids的长度 |
field_ids_off | 4 | field_ids的偏移量 |
method_ids_size | 4 | method_ids的长度 |
method_ids_off | 4 | method_ids的偏移量 |
class_defs_size | 4 | class_defs的长度 |
class_defs_off | 4 | class_defs的偏移量 |
data_size | 4 | data数据区的大小,单位:byte |
data_off | 4 | data的偏移量 |
通过上面结构解析数据(method信息为例)
首先从header中解析出,读取所有string_ids的字符串信息DexFileReader.prototype.loadStrings = function (dv) { var curStrings = []; var offsets = []; // 字节流移动到string_ids的偏移位置 dv.seek(this.header.stringIdsOff);// header中的数据是通过上面header结构从dex字节流中读取的 for (var i = 0; i < this.header.stringIdsSize; i++) { offsets[i] = dv.getInt32(); // 通过上面得知string_ids每个item的长度是4字节 } // 从dex数据中读取string内容 dv.seek(offsets[0]); for (var i = 0; i < this.header.stringIdsSize; i++) { dv.seek(offsets[i]); curStrings[i] = dv.readStringUtf(); // 这里从string_ids中读取数据是通过上面定义的读取leb128数据类型方式读取的 } this.strings = this.strings.concat(curStrings); };
然后是读取所有的method_ids中的数据
DexFileReader.prototype.loadMethods = function (dv) { var curMethods = []; dv.seek(this.header.methodIdsOff); for (var i = 0; i < this.header.methodIdsSize; i++) { var classIdx = dv.getInt16() & 0xffff; // 对应的class_defs的索引位置和type_ids的索引位置 var protoIdx = dv.getInt16() & 0xffff; // 对应的proto_ids中的索引位置 var nameIdx = dv.getInt32(); // 对应的string_ids中索引位置 curMethods[i] = new DexFormat.MethodDef(this, classIdx, protoIdx, nameIdx); } this.methods = this.methods.concat(curMethods); };
上面读取的数据的根据是method_ids的格式:
读取class_defs对应的信息
var curClasses = []; dv.seek(this.header.classDefsOff); for (var i = 0; i < this.header.classDefsSize; i++) { var classIdx = dv.getInt32(); // 对应的type_ids的索引 var accessFlags = dv.getInt32(); var superclassIdx = dv.getInt32(); var interfacesOff = dv.getInt32(); var sourceFileIdx = dv.getInt32(); var annotationsOff = dv.getInt32(); var classDataOff = dv.getInt32(); var staticValuesOff = dv.getInt32(); curClasses[i] = new DexFormat.ClassDef(this, classIdx, accessFlags, superclassIdx, interfacesOff, sourceFileIdx, annotationsOff, classDataOff, staticValuesOff); }
class_defs每隔class_def的格式如下:
读取type_ids中的数据
var curTypes = []; dv.seek(this.header.typeIdsOff); for (var i = 0; i < this.header.typeIdsSize; i++) { var descriptorIdx = dv.getInt32(); // 只存取了类的描述符对应的string_idx中的索引位置,并且该字符串存储数据类型是TypeDescriptor,详细查看官方文档 curTypes[i] = new DexFormat.TypeDef(this, descriptorIdx); }
type_id的数据格式中:只含有一个descriptionIdx存储的当前类型的字符串描述对应的索引位置。
读取到的description,对应的格式如下:
上面的含义是:TypeDescription可以 ‘V’ 或者 (‘[’ (0~255个)) NonArrayFieldTypeDescriptor; 其中[ 表示当前类型是几维数组,在字段的类型或者方法的返回值类型等会用到;NonArrayFieldTypeDescriptor就是下面的集中数据类型,前面的字母都是代表基本数据类型,L代表是引用类型;FullClassName的格式是 类的全类名的点替换成/.
上面大写字母对应的数据类型如下:
下面给出集中例子:
[Lcom/meizu/cloud/pushsdk/notification/model/styleenum/InnerStyleLayout; 表示:com.meizu.cloud.pushsdk.notification.model.styleenum.InnerStyleLayout
Z 表示 boolean
[[D 表示 double[][]
读取methods相关的报名信息,解析如下:
var classData = methodRef.getClassData(); var classType = methodRef.getClassType(); //通过上面存储的classIds获取到对应的Type信息 // 通过type获取对应的描述信息 var classDescriptor = classType.getDescriptor(); // 通过描述信息解析出包名,通过上面给出Desciption可以解析出对应的包名 var packageName = DexFileReader.getPackageNameOnly(classDescriptor); // 详细过程如下 DexFileReader.getPackageNameOnly = function (typeName) { var dotted = DexFileReader.descriptorToDot(typeName); // 将dex中存储的类型描述符的字符串 转化成 java代码中数据类型格式 var end = dotted.lastIndexOf("."); if (end < 0) { return ""; } else { return dotted.substring(0, end); } }; // 将dex中存储的类型描述符的字符串 转化成 java代码中数据类型格式 DexFileReader.descriptorToDot = function (descr) { var targetLen = descr.length; var offset = 0; var arrayDepth = 0; // 用于记录数组的维度 // 格式: ('[' 0~255个)'L'FullClassName';' (其中[ : 表示数组的维度, L:表示是类, FullClassName:类的全名,格式:全名称但是用'/'分隔) /* strip leading [s; will be added to end */ // 先取出去除表示数组维度的[ while (targetLen > 1 && descr.charAt(offset) == '[') { offset++; targetLen--; } arrayDepth = offset; if (targetLen == 1) { // 后面的字符串长度只有1,那么就是指包含类的类型标识符,所以是基本数据类型 descr = DexFileReader.getPrimitiveType(descr.charAt(offset)); // 获取基本数据类型对应的全称字符串 offset = 0; targetLen = descr.length; } else { /* account for leading 'L' and trailing ';' */ if (targetLen >= 2 && descr.charAt(offset) == 'L' && descr.charAt(offset + targetLen - 1) == ';') { // L表示自定义的类 targetLen -= 2; /* two fewer chars to copy */ offset++; /* skip the 'L' */ } } var buf = []; /* copy class name over */ var i; // 将 / 替换成 . for (i = 0; i < targetLen; i++) { var ch = descr.charAt(offset + i); buf[i] = (ch == '/') ? '.' : ch; } /* add the appopriate number of brackets for arrays */ // 根据数组的维度,添加指定个数的[] 字符串 while (arrayDepth-- > 0) { buf[i++] = '['; buf[i++] = ']'; } return buf.join(""); };
总结
我们想得到当前apk的相关信息,比如方法数,就可以通过分析dex文件的结构来获取到,市面上的开源软件都是这样做的。关于一些其他信息的获取可以自行通过其结构信息来获取。参考文献
Dex文件格式(官网)
相关文章推荐
- 纯Java环境解析apk文件信息
- CDays-3 习题二 (字典及文件读取练习)及相关内容解析。Python 基础教程
- java 解析apk、ipa文件 获得app信息
- Java环境解析apk文件信息
- POI:支持xls/xlsx文件格式按cell类型解析相关内容(exls 2003/2007 兼容)
- 通过上传的APK文件,解析APK文件内容,获取应用权限包名等
- java通过解析文件获取apk版本等信息
- PC端解析APK文件中的信息(图标、权限、包名等)
- c# 借助cmd命令解析apk文件信息
- android-获得".apk"文件的相关信息。包名、版本号等等
- roid-获得".apk"文件的相关信息。包名、版本号等等
- Java环境解析apk文件信息
- android-获得".apk"文件的相关信息。包名、版本号等等
- 获取已安装的程序和APK文件的信息
- CDays–4 习题六(修改文本)及相关内容解析。
- Android逆向之旅---解析编译之后的Dex文件格式
- [转]C# 解析配置文件内容 System.Configuration
- 纯JAVA读取android应用程序apk包的相关信息
- APK反编译修改包名及相应的基础文件信息
- 获取手机中已安装apk文件信息(PackageInfo、ResolveInfo)(应用...