*本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布
html
DEX文件就是Android Dalvik虚拟机运行的程序,关于DEX文件的结构的重要性我就很少说了。下面,开练!java
建议:不要只看,跟着我作。看再多遍不如本身亲自实践一遍来的可靠,别问我为何知道。泪崩ing.....安全
首先,咱们须要本身构造一个dex文件,由于本身构造的比较简单,分析起来比较容易。等你简单的会了,难的天然也就懂了。微信
首先,咱们编写一个简单的Java程序,以下:
数据结构
public class HelloWorld { int a = 0; static String b = "HelloDalvik"; public int getNumber(int i, int j) { int e = 3; return e + i + j; } public static void main(String[] args) { int c = 1; int d = 2; HelloWorld helloWorld = new HelloWorld(); String sayNumber = String.valueOf(helloWorld.getNumber(c, d)); System.out.println("HelloDex!" + sayNumber); } }
javac HelloWorld.java编辑器
接下来会出现一个HelloWorld.class文件,而后继续执行命令:
编码
dx --dex --output=HelloWorld.dex HelloWorld.classspa
就会出现HelloWorld.dex文件了。这时,咱们须要下载一个十六进位文本编辑器,由于用它能够解析二进制文件,咱们用它打开dex文件就会所有以十六进制的数进行展示了。这里推荐010Editor,下载地址:010Editor(收费软件,能够免费试用30天)。
命令行
下载完成以后,咱们能够用它打开dex文件了,打开以后,你的界面应该是这样的:
3d
一会儿看到这些东西,是否是立马懵逼了,正常,我刚开始看的时候也是,这什么玩意儿啊!其实,这就是二进制流文件中的内容,010Editor把它转化成了16进制的内容,以方便咱们阅读的。
不要慌,下面我跟你解释,这些东西咱们虽然看了懵逼,可是Dalvik虚拟机不会,由于它就是解析这些东西的,这些东西虽然看起来头大,可是它是有本身的格式标准的。dex文件的结构以下图所示:
这就是dex的文件格式了,下面咱们从最上面的Header提及,Header中存储了什么内容呢?下面咱们还得来一张图:
先看下就行,不用着急,下面咱们一步一步来,首先点击你的010Editor的这里:
对,就是箭头指的那里,点击以后,你会发现上面的有一片区域成了选中的颜色,这部分里面存储的就是Header中的数据了,下面咱们根据Header的数据图以此来进行分析。
首先,咱们看到DexHeader中每一个数据前面有个u1或者u4,这个是什么意思呢?它们其实就是表明1个或者4个字节的无符号数。下面咱们依次根据Header中的数据段进行解释。
1. 从第一个看起,magic[8];它表明dex中的文件标识,通常被称为魔数。是用来识别dex这种文件的,它能够判断当前的dex文件是否有效,能够看到它用了8个1字节的无符号数来表示,咱们在010Editor中能够看到也就是“64 65 78 0A 30 33 35 00 ”这8个字节,这些字节都是用16进制表示的,用16进制表示的话,两个数表明一个字节(一个字节等于8位,一个16进制的数能表示4位)。这8个字节用ASCII码表转化一下能够转化为:dex.035(点击这里能够进行十六进制转ASCII,你能够试试:其中,'.' 不是转化来的)。目前,dex的魔数固定为dex.035。
2. 第二个是,checksum; 它是dex文件的校验和,经过它能够判断dex文件是否被损坏或者被篡改。它占用4个字节,也就是“5D 9D F9 59”。这里提醒一下,在010Editor中,其实能够分别识别咱们在DexHeader中看到的这些字段的,你能够点一下这里:
你能够看到这个header列表展开了,其实咱们分析下来就和它这个结构是同样的,你能够先看下,咱们如今分析到了checksum中了,你能够看到后面对应的值是“59 F9 9D 5D”。咦?这好像和上面的字节不是一一对应的啊。对的,你能够发现它是反着写的。这是因为dex文件中采用的是小字节序的编码方式,也就是低位上存储的就是低字节内容,因此它们应该要反一下。
3. 第三个到了signature[kSHA1DigestLen]了,signature字段用于检验dex文件,其实就是把整个dex文件用SHA-1签名获得的一个值。这里占用20个字节,你能够本身点010Editor看一看。
4. 第四个fileSize;表示整个文件的大小,占用4个字节。
5. 第五个headerSize;表示DexHeader头结构的大小,占用4个字节。这里能够看到它一共占用了112个字节,112对应的16进制数为70h,你能够选中头文件看看010Editor是否是真的占用了这么多:
6. 第6个是endianTag;表明 字节序标记,用于指定dex运行环境的cpu,预设值为0x12345678,对应在101Editor中为“78 56 34 12”(小字节序)。
7. 接下来两个分别是linkSize;和u4 linkOff;这两个字段,它们分别指定了连接段的大小和文件偏移,一般状况下它们都为0。linkSize为0的话表示静态连接。
8. 再下来就是mapOff字段了,它指定了DexMapList的文件偏移,这里咱们先不过多介绍它,你能够看一下它的值为“14 04 00 00”,它其实对应的16进制数就是414h(别忘了小字节序),咱们能够在414h的位置看一下它在哪里:
其实就是dex文件最后一部份内容。关于这部份内容里面是什么,咱们先不说,继续往下看。
9. stringIdsSize 和 stringIdsOff字段:这两个字段指定了dex文件中全部用到的字符串的个数和位置偏移,咱们先看stringIdsSize,它的值为:“1C 00 00 00”,16进制的1C也就是十进制的28,也就是说咱们这个dex文件中一共有28个字符串,而后stringIdsOff为:“70 00 00 00”,表明字符串的偏移位置为70h,这下咱们找到70h的地方:
这下咱们就要先介绍一下DexStringId这个结构了,图中从70h开始,全部被选中的都是DexStringId这种数据结构的内容,DexStringId表明的是字符串的位置偏移,每一个DexStringId占用4个字节,也就是说它里面存的还不是真正的字符串,它们只是存储了真正字符串的偏移位置。
下面咱们先分析几个看看,
①取第一个“B2 02 00 00”,它表明的位置偏移是2B2h,咱们先找到这个位置:
能够发现我一共选中了10个字节,这10个字节就表示了一个字符串。下面咱们看一下dex文件中的字符串是如何表示的。dex中的字符串采用了一种叫作MUTF-8这样的编码,它是通过传统的UTF-8编码修改的。在MTUF-8中,它的头部存放的是由uleb128编码的字符的个数。(至于uleb128编码是什么编码,这里我不详细展开说,有兴趣的能够搜索看看。)
也就是说在“08 3C 63 6C 69 6E 69 74 3E 00”这些字节中,第一个08指定的是后面须要用到的编码的个数,也就是8个,即“ 3C 63 6C 69 6E 69 74 3E”这8个,可是咱们为何一共选中了10个字节呢,由于最后一个空字符“0”表示的是字符串的结尾,字符个数没有把它算进去。下面咱们来看看“ 3C 63 6C 69 6E 69 74 3E”这8个字符表明了什么字符串:
依旧能够点这里查询ASCII 。(要说明的一点是,这里凑巧这几个uleb128编码的字符都用了1个字节,因此咱们能够这样进行查询,uleb128编码标准用的是1~5个字节, 这里只是刚好都是一个字节)。也就是说上面的70h开始的第一个DexStringId指向的实际上是字符串“<clinit>”(可是貌似咱们的代码中没有用到这个字符串啊,先不用管,咱们接着分析)。再看到这里:
②刚刚咱们分析到“B2 02 00 00”所指向的真实字符串了,下面咱们接着再分析一个,咱们直接分析第三个,不分析第二个了。第三个为“C4 02 00 00”,对应的位置也就是2C4h,咱们找到它:
看这里,这就是2C4h的位置了。咱们首先看第一个字符,它的值为0Bh,也就是十进制的11,也就是说接下来的11个字符表明了它的字符串,咱们依旧是查看接下来11个字符表明的是什么,通过查询整理:
依旧能够在这里查询ASCII。上面就是“HelloDalvik”这个字符串,能够看看咱们的代码,咱们确实用了一个这样的字符串,bingo。
下面剩下的字符串就不分析了。通过整理,能够整理出咱们一共用到的28个字符串为:
ok,字符串这里告一段落,下面咱们继续看DexHeader的下面的字段。头好晕~乎乎
噢,读了,还不能结束呢,你如今能够看一下最开始发的那张dex结构图了:
看到了吧,咱们这半天分析的stringIdsSize 和 stringIdsOff字段指向的位置就是上面那个箭头指向的位置,它们里面存储的是真实字符串的位置偏移,它们都存储在data区域。(先透露一下,后面咱们要分析的几个也和stringIdsSize 与stringIdsOff字段相似,它们里面存储的基本都是位置偏移,并非真正的数据,真正的数据都在data区域)
好,咱们继续。
10. 继续看DexHeader图,咱们如今该typeIdsSize和typeIdsOff了。它们表明什么呢?它们表明的是类的类型的数量和位置偏移,也是都占4个字节,下面咱们看它们的值
能够看到,typeIdsSize的值为9h,也就是咱们dex文件中用到的类的类型一共有9个,位置偏移在E0h位置,下面咱们找到这个位置
看到了吧,我选中的位置就是了。这里咱们又得介绍一种数据结构了,由于这里的数据也是一种数据结构的数据组成的。那就是DexTypeId,也就是说选中的内容都是DexTypeId这种数据,这种数据结构中只有一个变量,以下所示:
struct DexTypeId{ u4 descriptorIdx; /*指向DexStringId列表的索引*/ }看到了吧,这就是DexTypeId数据结构,它里面只有一个数据descriptorIdx,它的值的内容是DexStringId列表的索引。还记得DexStringId是什么吗?在上面咱们分析字符串时,字符串的偏移位置就是由DexStringId这种数据结构描述的,也就是说descriptorIdx指向的是全部的DexStringId组成的列表的索引。上面咱们整理出了全部的字符串,你能够翻上去看看图。而后咱们看这里一共是9个类的类型表明的都是什么。先看第一个“05 00 00 00”,也就是05h,即十进位的5。而后咱们在上面全部整理出的字符串看看5索引的是什么?翻上去能够看到是“I”。接下来咱们依次整理这些类的类型,也能够获得类的类型的列表
看到了吧,这就是咱们dex文件中全部用到的类的类型。好比“I”表明的就是int,LHelloWorld表明的就是HelloWorld,Ljava/io/PrintStream表明的就是java.io.PrintStream。后面的几个先就不说了。咱们接着往下分析。
11. 这下到了protoIdsSize和protoIdsOff了,它们表明的是dex文件中方法原型的个数和位置偏移。咱们先看它们的值
如上图就是它们的值了,protoIdsSize的值为十进制的7,说明有7个方法原型,而后位置偏移为104h,咱们找到这个位置
看到了吧,这里就是了。对,下面又有新的数据结构了。这下一个数据结构不能知足这块的内容了,咱们先看第一个数据结构,DexProtoId
struct DexProtoId{ u4 shortyIdx; /*指向DexStringId列表的索引*/ u4 returnTypeIdx; /*指向DexTypeId列表的索引*/ u4 parametersOff; /*指向DexTypeList的位置偏移*/ }能够看到,这个数据结构由三个变量组成。第一个shortyIdx它指向的是咱们上面分析的DexStringId列表的索引,表明的是方法声明字符串。第二个returnTypeIdx它指向的是 咱们上边分析的DexTypeId列表的索引,表明的是方法返回类型字符串。第三个parametersOff指向的是DexTypeList的位置索引,这又是一个新的数据结构了,先说一下这里面 存储的是方法的参数列表。能够看到这三个参数,有方法声明字符串,有返回类型,有方法的参数列表,这基本上就肯定了咱们一个方法的大致内容。
咱们接着看看DexTypeList这个数据结构,看看参数列表是如何存储的。
struct DexTypeList{ u4 size; /*DexTypeItem的个数*/ DexTypeItem list[1]; /*DexTypeItem结构*/ }看到了嘛,它有两个参数,其中第一个size说的是DexTypeItem的个数,那DexTypeItem又是啥咧?它又是一种数据结构。咱们继续看看
struct DexTypeItem{ u2 typeIdx; /*指向DexTypeId列表的索引*/ }恩,还好,里面就一个参数。也比较简单,就是一个指向DexTypeId列表的索引,也就是表明参数列表中某一个具体的参数的位置。
分析完这几个数据结构了,下面咱们具体地分析一个类吧。别走神,咱们该从上图的104h开始了。
在104h这里,因为 都是DexProtoId这种数据结构的数据,一个DexProtoId一共占用12个字节。因此,咱们取前12个字节进行分析。“06 00 00 00,00 00 00 00,94 02 00 00”,这就是那12个字节了。首先“06 00 00 00”表明的是shortyIdx,它的值是指向DexStringId列表的索引,咱们找到DexStringId列表中第6个对应的值,也就是III,说明这个方法中声明字符串为三个int。接着,“00 00 00 00”表明的是returnTypeIdx,它的值指向的是DexTypeId列表的索引,咱们找到对应的值,也就是I,说明这个方法的返回值是int类型的。最后,咱们看“94 02 00 00”,它表明的是DexTypeList的位置偏移,它的值为294h,咱们找到这个位置
这里是DexTypeList结构,首先看前4个字节,表明的是DexTypeItem的个数,“02 00 00 00 ”也就是2,说明接下来有2个DexTypeItem的数据,每一个DexTypeItem占用2个字节,也就是两个都是“00 00”,它们的值是DexTypeId列表的索引,咱们去找一下,发现0对应的是I,也就是说它的两个参数都是int型的。所以这个方法的声明咱们也就肯定了。也就是int(int,int),能够看看咱们的源代码,getNumber方法确实是这样的。好,第一个方法就这样分析完了,下面咱们依旧是将这些方法的声明整理成列表,后面可能有数据会指向它们的索引。
终于又完了一个。咱们准备继续下面的。累了就先去听听歌吧,歇一歇再看 -_-
12. fieldIdsSize和fieldIdsOff字段。这两个字段指向的是dex文件中字段名的信息。咱们看到这里
能够看到,fieldIdsSize为3h,说明共有3个字段。fieldIdsOff为158h,说明偏移为158h,咱们继续看到158h这里
咳咳,又该新的数据结构了,再忍一忍,接下来的数据结构是DexFieldId,咱们看下
struct DexFieldId{ u2 classIdx; /*类的类型,指向DexTypeId列表的索引*/ u2 typeIdx; /*字段类型,指向DexTypeId列表的索引*/ u4 nameIdx; /*字段名,指向DexStringId列表的索引*/ }能够看到,这三个数据都是指向的索引值,具体的就不说了,看后面的备注就是。咱们依旧是分析一下第一个字段,“01 00 ,00 00,13 00 00 00”,类的类型为DexTypeId列表的索引1,也就是HelloWorld,字段的类型为DexTypeId列表中的索引0,也就是int,字段名为DexStringId列表中的索引13h,即十进制的19,找一下,是a,也就是说咱们这个字段就确认了,即int HelloWorld.a。这不就是咱们在HelloWorld.java文件里定义的变量a嘛。而后咱们依次把咱们全部的3个字段都列出来:
〇int HelloWorld.a , ①java.lang.String HelloWorld.b ,②java.io.PrintStream java.lang.System.out
ok,先告一段落。继续分析下一个
13. methodIdsSize和methodIdsOff字段。这俩字段指明了方法所在的类、方法的声明以及方法名。咱们看看
先是,methodIdsSize,为Ah,即十进制的10,说明共有10个方法。methodIdsOff,为170h,说明它们的位置偏移在170h。咱们看到这里
对对对,又是新的数据结构,不过这个和上个同样简单,请看DexMethodId
struct DexMethodId{ u2 classIdx; /*类的类型,指向DexTypeId列表的索引*/ u2 protoIdx; /*声明类型,指向DexProtoId列表的索引*/ u4 nameIdx; /*方法名,指向DexStringId列表的索引*/ }对吧,这个也简单,三个数据也都是指向对应的结构的索引值。咱们直接分析一下第一个数据,“01 00, 04 00, 00 00 00 00”,首先,classIdx,为1,对应DexTypeId列表的索引1,也就是HelloWorld;其次,protoIdx,为4,对应DexProtoId列表中的索引4,也就是void();最后,nameIdx,为0,对应DexStringId列表中的索引0,也就是<clinit>。所以,第一个数据就出来了,即void HelloWorld.<clinit>() 。后面的不进行分析了,咱们依旧是把其他的9个方法列出来
好了,这个就算分析完了。下面真正开始咱们的重头戏了。先缓一缓再继续吧。
14. classDefsSize和classDefsOff字段。这两个字段指明的是dex文件中类的定义的相关信息。咱们先找到它们的位置。
classDefsSize字段,为1,也就是只有一个类定义,classDefsOff,为1C0h,咱们找到它的偏移位置。
这里就是了,到了这里,你如今应该也知道又有新的数据结构了。对的,接下来的数据结构是DexClassDef,请看
struct DexClassDef{ u4 classIdx; /*类的类型,指向DexTypeId列表的索引*/ u4 accessFlags; /*访问标志*/ u4 superclassIdx; /*父类类型,指向DexTypeId列表的索引*/ u4 interfacesOff; /*接口,指向DexTypeList的偏移*/ u4 sourceFileIdx; /*源文件名,指向DexStringId列表的索引*/ u4 annotationsOff; /*注解,指向DexAnnotationsDirectoryItem结构*/ u4 classDataOff; /*指向DexClassData结构的偏移*/ u4 staticValuesOff; /*指向DexEncodedArray结构的偏移*/ }很少说了,咱们直接根据结构开始分析吧,反正就只有一个类定义。classIdx为1,对应DexTypeId列表的索引1,找到是HelloWorld,确实是咱们源程序中的类的类型。accessFlags为1,它是类的访问标志,对应的值是一个以ACC_开头的枚举值,1对应的是 ACC_PUBLIC,你能够在010Editor中看一下,说明咱们的类是public的。superclassIdx的值为3,找到DexTypeId列表中的索引3,对应的是java.lang.object,说明咱们的类的父类类型是Object的。interfaceOff指向的是DexTypeList结构,咱们这里是0说明没有接口。若是有接口的话直接对应到DexTypeList,就和以前咱们分析的同样了,这里很少解释,有兴趣的能够写一个有接口的类验证下。再下来sourceFileIdx指向的是DexStringId列表的索引,表明源文件名,咱们这里位4,找一下对应到了字符串"HelloWorld.java",说明咱们类程序的源文件名为HelloWorld.java。annotationsOff字段指向注解目录接口,根据类型不一样会有注解类、注解方法、注解字段与注解参数,咱们这里的值为0,说明没有注解,这里也不过多解释,有兴趣能够本身试试。
接下来是classDataOff了,它指向的是DexClassData结构的位置偏移,DexClassData中存储的是类的数据部分,咱们开始详细分析一下它,首先,仍是先找到偏移位置3F8h
接着,咱们看看DexClassData数据结构
struct DexClassData{ DexClassDataHeader header; /*指定字段与方法的个数*/ DexField* staticFields; /*静态字段,DexField结构*/ DexField* instanceFields; /*实例字段,DexField结构*/ DexMethod* directMethods; /*直接方法,DexMethod结构*/ DexMethod* virtualMethods; /*虚方法,DexMethod结构*/ }
struct DexClassDataHeader{ u4 staticFieldsSize; /*静态字段个数*/ u4 instanceFieldsSize; /*实例字段个数*/ u4 directMethodsSize; /*直接方法个数*/ u4 virtualMethodsSize; /*虚方法个数*/ } struct DexField{ u4 fieldIdx; /*指向DexFieldId的索引*/ u4 accessFlags; /*访问标志*/ } struct DexMethod{ u4 methodIdx; /*指向DexMethodId的索引*/ u4 accessFlags; /*访问标志*/ u4 codeOff; /*指向DexCode结构的偏移*/ }代码中的注释写的也都很清楚了,咱们就很少说了。可是请注意,在这些结构中的u4不是指的占用4个字节,而是指它们是uleb128类型(占用1~5个字节)的数据。关于uleb128仍是很少说,想了解的能够本身查查看。
好,接下来开始分析,对于DexClassData,第一个为DexClassDataHeader,咱们找到相应的位置,第一个staticFieldsSize其实只占用了一个字节,即01h就是它的值,也就是说共有一个静态字段,接下来instanceFieldsSize,directMethodsSize,virtualMethodsSize也都是只占用了一个字节,即实例字段的个数为1,直接方法的个数为3,虚方法的个数为1。(这里只是凑巧它们几个都占用一个字节,并不必定是只占用一个字节,这关于到uleb128数据类型,具体能够本身了解下)。
而后接下来就是staticFields了,它对应的数据结构为DexField,能够看到,第一个fieldIdx,是指向DexFieldId的索引,值为1,找到对应的索引值为java.lang.String HelloWorld.b。第二个accessFlags,值为8,对应的ACC_开头的数据为ACC_STATIC(能够在010Editor中对应查看一下),说明咱们这个静态字段为:static java.lang.String HelloWorld.b。能够对应咱们的源代码看一下,咱们确实定义了一个static的b变量。
接着看instanceFields,它和staticFields对应的数据结构是同样的,咱们直接分析,第一个fieldIdx,值为0,对应的DexField的索引值为int HelloWorld.a。第二个accessFlags,值为0,对应的ACC_开头的数据为空,就是什么也没有。说明咱们这个实例字段为:int HelloWorld.a。能够对应咱们的源码 看看,咱们确实定义了一个a实例变量。
再接着,根据directMethodsSize,有3个直接方法,咱们先看第一个,它对应的数据结构是DexMethod,首先methodIdx指向的是DexMethodId的索引,值为0,找到对应的索引值为void HelloWorld.<clinit>()。而后accessFlages为......为......为....个人个天!我觉得就这样能蒙混过关了,没想到还真碰到一个uleb128数据不是占用一个字节的,这个accessFlags对应的值占用了三个字节,“88 80 04”,为何?由于是按照uleb128格式的数据读出来的(仍是本身去查查吧,这个坑先不填了,其实这种数据也不麻烦,就是前面字节上的最高位指定了是否须要下一个字节上的内容)。“88 80 04”对应的ACC_开头的数据为 ACC_STATIC ACC_CONSTRUCTOR,代表这个方法是静态的,而且是构造方法。最后,看看codeOff,它对应了DexCode结构的偏移,DexCode中存放了方法的指令集等信息,也就是真正的代码了。咱们暂且不分析DexCode,就先看看它的偏移位置为“E0 03”,这个等于多少呢?uleb128转化为16进制数结果为:1E0h。也就是DexCode存放在偏移位置1E0h的位置上。
具体的DexCode咱们就先不分析了,由于它里面存放的一些指令局须要根据相关资料一一查找,有兴趣的本身能够找资料看看。剩下的两个直接方法咱们也不分析了。
接下来,咱们看根据virtualMethodsSize,有1个虚方法,咱们直接看。首先methodIdx的值为2,对应的DexMethodId的索引值为int HelloWorld.getNumber(int, int)。而后accessFlags为1,对应的值为ACC_PUBLIC,代表这是一个public类。codeOff为“FC 04”,对应的位置为27Ch,这里就不上图了,本身找找吧。
好了,咱们整个DEX文件的结构就这样从DexHeader开始基本分析完了,好累啊,不过这样分析一遍,对DEX文件的格式会有更深入的认识。老是看别人的真不如本身来一遍来的实在!
参考资料:
《Android软件安全与逆向分析》.非虫