Android应用逆向分析技术综述Dex文件结构Android 程序静态分析一、Dex文件结构1. 文件头DEX文件头主要包括校验和以及其他结构的偏移地址和长度信息。
字段名称偏移值长度描述magic 0x0 8 'Magic'值,即魔数字段,格式如”dex/n035/0”,其中的035表示结构的版本。
checksum 0x8 4 校验码。
signature 0xC 20 SHA-1签名。
file_size 0x20 4 Dex文件的总长度。
header_size 0x24 4 文件头长度,009版本=0x5C,035版本=0x70。
endian_tag 0x28 4 标识字节顺序的常量,根据这个常量可以判断文件是否交换了字节顺序,缺省情况下=0x78563412。
link_size 0x2C 4 连接段的大小,如果为0就表示是静态连接。
link_off 0x30 4 连接段的开始位置,从本文件头开始算起。
如果连接段的大小为0,这里也是0。
map_off 0x34 4 map数据基地址。
string_ids_size 0x38 4 字符串列表的字符串个数。
string_ids_off 0x3C 4 字符串列表表基地址。
type_ids_size 0x40 4 类型列表里类型个数。
type_ids_off 0x44 4 类型列表基地址。
proto_ids_size 0x48 4 原型列表里原型个数。
proto_ids_off 0x4C 4 原型列表基地址。
field_ids_size 0x50 4 字段列表里字段个数。
field_ids_off 0x54 4 字段列表基地址。
method_ids_size 0x58 4 方法列表里方法个数。
method_ids_off 0x5C 4 方法列表基地址。
class_defs_size 0x60 4 类定义类表中类的个数。
class_defs_off 0x64 4 类定义列表基地址。
data_size 0x68 4 数据段的大小,必须以4字节对齐。
data_off 0x6C 4 数据段基地址2. 魔数字段魔数字段,主要就是Dex文件的标识符,它占用4个字节,在目前的源码里是“dex\n”,它的作用主要是用来标识dex文件的,比如有一个文件也以dex为后缀名,仅此并不会被认为是Davlik虚拟机运行的文件,还要判断这四个字节。
另外Davlik虚拟机也有优化的Dex,也是通过个字段来区分的,当它是优化的Dex文件时,它的值就变成”dey\n”了。
根据这四个字节,就可以识别不同类型的Dex文件了。
3. 检验码字段主要用来检查从这个字段开始到文件结尾,这段数据是否完整,有没有人修改过,或者传送过程中是否有出错等等。
通常用来检查数据是否完整的算法,有CRC32、有SHA128等,但这里采用并不是这两类,而采用一个比较特别的算法,叫做adler32,这是在开源zlib 里常用的算法,用来检查文件是否完整性。
该算法由MarkAdler发明,其可靠程度跟CRC32差不多,不过还是弱一点点,但它有一个很好的优点,就是使用软件来计算检验码时比较CRC32要快很多。
可见Android系统,就算法上就已经为移动设备进行优化了。
4.SHA-1签名字段dex文件头里,前面已经有了面有一个4字节的检验字段码了,为什么还会有SHA-1签名字段呢?不是重复了吗?可是仔细考虑一下,这样设计自有道理。
因为dex文件一般都不是很小,简单的应用程序都有几十K,这么多数据使用一个4字节的检验码,重复的机率还是有的,也就是说当文件里的数据修改了,还是很有可能检验不出来的。
这时检验码就失去了作用,需要使用更加强大的检验码,这就是SHA-1。
SHA-1校验码有20个字节,比前面的检验码多了16个字节,几乎不会不同的文件计算出来的检验是一样的。
设计两个检验码的目的,就是先使用第一个检验码进行快速检查,这样可以先把简单出错的dex文件丢掉了,接着再使用第二个复杂的检验码进行复杂计算,验证文件是否完整,这样确保执行的文件完整和安全。
SHA(Secure Hash Algorithm, 安全散列算法)是美国国家安全局设计,美国国家标准与技术研究院发布的一系列密码散列函数。
SHA-1看起来和MD5算法很像,也许是Ron Rivest在SHA-1的设计中起了一定的作用。
SHA-1的内部比MD5更强,其摘要比MD5的16字节长4个字节,这个算法成功经受了密码分析专家的攻击,也因而受到密码学界的广泛推崇。
这个算法在目前网络上的签名,BT软件里就有大量使用,比如在BT里要计算是否同一个种子时,就是利用文件的签名来判断的。
同一份8G的电影从几千BT用户那里下载,也不会出现错误的数据,导致电影不播放。
5.map_off字段这个字段主要保存map开始位置,就是从文件头开始到map数据的长度,通过这个索引就可以找到map数据。
map的数据结构如下:名称大小说明size 4字节map里项的个数list 变长每一项定义为12字节,项的个数由上面项大小决定。
map数据排列结构定义如下:/**Direct-mapped "map_list".*/typedef struct DexMapList {u4 size; /* #of entries inlist */DexMapItem list[1]; /* entries */}DexMapList;每一个map项的结构定义如下:/**Direct-mapped "map_item".*/typedef struct DexMapItem {u2 type; /* type code (seekDexType* above) */u2 unused;u4 size; /* count of items ofthe indicated type */u4 offset; /* file offset tothe start of data */}DexMapItem;DexMapItem结构定义每一项的数据意义:类型、类型个数、类型开始位置。
其中的类型定义如下:/*map item type codes */enum{kDexTypeHeaderItem = 0x0000,kDexTypeStringIdItem = 0x0001,kDexTypeTypeIdItem = 0x0002,kDexTypeProtoIdItem = 0x0003,kDexTypeFieldIdItem = 0x0004,kDexTypeMethodIdItem = 0x0005,kDexTypeClassDefItem = 0x0006,kDexTypeMapList = 0x1000,kDexTypeTypeList = 0x1001,kDexTypeAnnotationSetRefList = 0x1002,kDexTypeAnnotationSetItem = 0x1003,kDexTypeClassDataItem = 0x2000,kDexTypeCodeItem = 0x2001,kDexTypeStringDataItem = 0x2002,kDexTypeDebugInfoItem = 0x2003,kDexTypeAnnotationItem = 0x2004,kDexTypeEncodedArrayItem = 0x2005,kDexTypeAnnotationsDirectoryItem = 0x2006,};从上面的类型可知,它包括了在dex文件里可能出现的所有类型。
可以看出这里的类型与文件头里定义的类型有很多是一样的,这里的类型其实就是文件头里定义的类型。
其实这个map的数据,就是头里类型的重复,完全是为了检验作用而存在的。
当Android系统加载dex文件时,如果比较文件头类型个数与map里类型不一致时,就会停止使用这个dex 文件。
6.string_ids_size/off字段这两个字段主要用来标识字符串资源。
源程序编译后,程序里用到的字符串都保存在这个数据段里,以便解释执行这个dex文件使用。
其中包括调用库函数里的类名称描述,用于输出显示的字符串等。
string_ids_size标识了有多少个字符串,string_ids_off标识字符串数据区的开始位置。
字符串的存储结构如下:/** Direct-mapped "string_id_item".*/typedef struct DexStringId {u4 stringDataOff; /* file offset to string_data_item */} DexStringId;可以看出这个数据区保存的只是字符串表的地址索引。
如果要找到字符串的实际数据,还需要通过个地址索引找到文件的相应开始位置,然后才能得到字符串数据。
每一个字符串项的索引占用4个字节,因此这个数据区的大小就为4*string_ids_size。
实际数据区中的字符串采用UTF8格式保存。
例如,如果dex文件使用16进制显示出来内容如下:063c 696e 6974 3e00 其实际数据则是”<init>\0”另外这段数据中不仅包括字符串的字符串的内容和结束标志,在最开头的位置还标明了字符串的长度。
上例中第一个字节06就是表示这个字符串有6个字符。
关于字符串的长度有两点需要注意的地方:1、关于长度的编码格式dex文件里采用了变长方式表示字符串长度。
一个字符串的长度可能是一个字节(小于256)或者4个字节(1G大小以上)。
字符串的长度大多数都是小于256个字节,因此需要使用一种编码,既可以表示一个字节的长度,也可以表示4个字节的长度,并且1个字节的长度占绝大多数。
能满足这种表示的编码方式有很多,但dex文件里采用的是uleb128方式。
leb128编码是一种变长编码,每个字节采用7位来表达原来的数据,最高位用来表示是否有后继字节。
它的编码算法如下:/** Writes a 32-bit value in unsigned ULEB128 format.* Returns the updated pointer.*/DEX_INLINE u1* writeUnsignedLeb128(u1* ptr, u4 data){while (true) {u1 out = data & 0x7f;if (out != data) {*ptr++ = out | 0x80;data >>= 7;} else {*ptr++ = out;break;}}return ptr;}它的解码算法如下:/** Reads an unsigned LEB128 value, updating the given pointer to point* just past the end of the read value. This function tolerates* non-zero high-order bits in the fifth encoded byte.*/DEX_INLINE int readUnsignedLeb128(const u1** pStream) {const u1* ptr = *pStream;int result = *(ptr++);if (result > 0x7f) {int cur = *(ptr++);result = (result & 0x7f) | ((cur & 0x7f) << 7);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;}根据上面的算法分析上面例子字符串,取得第一个字节是06,最高位为0,因此没有后继字节,那么取出这个字节里7位有效数据,就是6,也就是说这个字符串是6个字节,但不包括结束字符“\0”。