Android逆向之旅---解析編譯之後的Dex檔案格式

yangxi_001發表於2016-12-01

一、前言

新的一年又開始了,大家是否還記得去年年末的時候,我們還有一件事沒有做,那就是解析Android中編譯之後的classes.dex檔案格式,我們在去年的時候已經介紹了:

如何解析編譯之後的xml檔案格式:

http://blog.csdn.net/jiangwei0910410003/article/details/50568487

如何解析編譯之後的resource.arsc檔案格式:

http://blog.csdn.net/jiangwei0910410003/article/details/50628894

那麼我們還剩下一個檔案格式就是classes.dex了,那麼今天我們就來看看最後一個檔案格式解析,關於Android中的dex檔案的相關知識這裡就不做太多的解釋了,網上有很多資料可以參考,而且,我們在之前介紹的一篇加固apk的那篇文章中也介紹了一點dex的格式知識點:http://blog.csdn.net/jiangwei0910410003/article/details/48415225,我們按照之前的解析思路來,首先還是來一張神圖:


有了這張神圖,那麼接下來我們就可以來介紹dex的檔案結構了,首先還是來看一張大體的結構圖:



二、準備工作

我們在講解資料結構之前,我們需要先建立一個簡單的例子來幫助我們來解析,我們需要得到一個簡單的dex檔案,這裡我們不借助任何的IDE工具,就可以構造一個dex檔案出來。藉助的工具很簡單:javac,dx命令即可。

建立 java 原始檔 ,內容如下
程式碼:
public class Hello
{
public static void main(String[] argc)
{
System.out.println("Hello, Android!\n");
}
}

在當前工作路徑下 , 編譯方法如下 :
(1) 編譯成 java class 檔案
執行命令 : javac Hello.java
編譯完成後 ,目錄下生成 Hello.class 檔案 。可以使用命令 java Hello 來測試下 ,會輸出程式碼中的 “Hello, Android!” 的字串 。
(2) 編譯成 dex 檔案
編譯工具在 Android SDK 的路徑如下 ,其中 19.0.1 是Android SDK build_tools 的版本 ,請按照在本地安裝的 build_tools 版本來 。建議該路徑載入到 PATH 路徑下 ,否則引用 dx 工具時需要使用絕對路徑 :./build-tools/19.0.1/dx
執行命令 : dx --dex --output=Hello.dex Hello.class
編譯正常會生成 Hello.dex 檔案 。
3. 使用 ADB 執行測試
測試命令和輸出結果如下 :
$ adb root
$ adb push Hello.dex /sdcard/
$ adb shell
root@maguro:/ # dalvikvm -cp /sdcard/Hello.dex Hello
Hello, Android!

4. 重要說明
(1) 測試環境使用真機和 Android 虛擬機器都可以的 。核心的命令是
dalvikvm -cp /sdcard/Hello.dex Hello
-cp 是 class path 的縮寫 ,後面的 Hello 是要執行的 Class 的名稱 。網上有描述說輸入 dalvikvm --help
可以看到 dalvikvm 的幫助文件 ,但是在 Android4.4 的官方模擬器和自己的手機上測試都提示找不到
Class 路徑 ,在Android 老的版本 ( 4.3 ) 上測試還是有輸出的 。
(2) 因為命令在執行時 , dalvikvm 會在 /data/dalvik-cache/ 目錄下建立 .dex 檔案 ,因此要求 ADB 的
執行 Shell 對目錄 /data/dalvik-cache/ 有讀、寫和執行的許可權 ,否則無法達到預期效果 。


三、講解資料結構

下面我們按照這張大體的思路圖來一一講解各個資料結構

第一、頭部資訊Header結構

dex檔案裡的header。除了描述.dex檔案的檔案資訊外,還有檔案裡其它各個區域的索引。header對應成結構體型別,邏輯上的描述我用結構體header_item來理解它。先給出結構體裡面用到的資料型別ubyte和uint的解釋,然後再是結構體的描述,後面對各種結構描述的時候也是用的這種方法。

程式碼定義:

package com.wjdiankong.parsedex.struct;

import com.wjdiankong.parsedex.Utils;

public class HeaderType {
	
	/**
	 * struct header_item
		{
		ubyte[8] magic;
		unit checksum;
		ubyte[20] siganature;
		uint file_size;
		uint header_size;
		unit endian_tag;
		uint link_size;
		uint link_off;
		uint map_off;
		uint string_ids_size;
		uint string_ids_off;
		uint type_ids_size;
		uint type_ids_off;
		uint proto_ids_size;
		uint proto_ids_off;
		uint method_ids_size;
		uint method_ids_off;
		uint class_defs_size;
		uint class_defs_off;
		uint data_size;
		uint data_off;
		}
	 */
	public byte[] magic = new byte[8];
	public int checksum;
	public byte[] siganature = new byte[20];
	public int file_size;
	public int header_size;
	public int endian_tag;
	public int link_size;
	public int link_off;
	public int map_off;
	public int string_ids_size;
	public int string_ids_off;
	public int type_ids_size;
	public int type_ids_off;
	public int proto_ids_size;
	public int proto_ids_off;
	public int field_ids_size;
	public int field_ids_off;
	public int method_ids_size;
	public int method_ids_off;
	public int class_defs_size;
	public int class_defs_off;
	public int data_size;
	public int data_off;
	
	@Override
	public String toString(){
		return "magic:"+Utils.bytesToHexString(magic)+"\n"
				+ "checksum:"+checksum + "\n"
				+ "siganature:"+Utils.bytesToHexString(siganature) + "\n"
				+ "file_size:"+file_size + "\n"
				+ "header_size:"+header_size + "\n"
				+ "endian_tag:"+endian_tag + "\n"
				+ "link_size:"+link_size + "\n"
				+ "link_off:"+Utils.bytesToHexString(Utils.int2Byte(link_off)) + "\n"
				+ "map_off:"+Utils.bytesToHexString(Utils.int2Byte(map_off)) + "\n"
				+ "string_ids_size:"+string_ids_size + "\n"
				+ "string_ids_off:"+Utils.bytesToHexString(Utils.int2Byte(string_ids_off)) + "\n"
				+ "type_ids_size:"+type_ids_size + "\n"
				+ "type_ids_off:"+Utils.bytesToHexString(Utils.int2Byte(type_ids_off)) + "\n"
				+ "proto_ids_size:"+proto_ids_size + "\n"
				+ "proto_ids_off:"+Utils.bytesToHexString(Utils.int2Byte(proto_ids_off)) + "\n"
				+ "field_ids_size:"+field_ids_size + "\n"
				+ "field_ids_off:"+Utils.bytesToHexString(Utils.int2Byte(field_ids_off)) + "\n"
				+ "method_ids_size:"+method_ids_size + "\n"
				+ "method_ids_off:"+Utils.bytesToHexString(Utils.int2Byte(method_ids_off)) + "\n"
				+ "class_defs_size:"+class_defs_size + "\n"
				+ "class_defs_off:"+Utils.bytesToHexString(Utils.int2Byte(class_defs_off)) + "\n"
				+ "data_size:"+data_size + "\n"
				+ "data_off:"+Utils.bytesToHexString(Utils.int2Byte(data_off));
				
				
	}

}

檢視Hex如下:


我們用一張圖來描述各個欄位的長度:


裡面一對一對以_size和_off為字尾的描述:data_size是以Byte為單位描述data區的大小,其餘的
_size都是描述該區裡元素的個數;_off描述相對與檔案起始位置的偏移量。其餘的6個是描述.dex檔案信
息的,各項說明如下:
(1) magic value
這 8 個 位元組一般是常量 ,為了使 .dex 檔案能夠被識別出來 ,它必須出現在 .dex 檔案的最開頭的
位置 。陣列的值可以轉換為一個字串如下 :
{ 0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00 } = "dex\n035\0"
中間是一個 ‘\n' 符號 ,後面 035 是 Dex 檔案格式的版本 。
(2) checksum 和 signature
檔案校驗碼 ,使用alder32 演算法校驗檔案除去 maigc ,checksum 外餘下的所有檔案區域 ,用於檢
查檔案錯誤 。
signature , 使用 SHA-1 演算法 hash 除去 magic ,checksum 和 signature 外餘下的所有檔案區域 ,
用於唯一識別本檔案 。
(3) file_size
Dex 檔案的大小 。
(4) header_size
header 區域的大小 ,單位 Byte ,一般固定為 0x70 常量 。
(5) endian_tag
大小端標籤 ,標準 .dex 檔案格式為 小端 ,此項一般固定為 0x1234 5678 常量 。

(6) link_size和link_off

這個兩個欄位是表示連結資料的大小和偏移值

(7) map_off
map item 的偏移地址 ,該 item 屬於 data 區裡的內容 ,值要大於等於 data_off 的大小 。結構如
map_list 描述 :

package com.wjdiankong.parsedex.struct;

import java.util.ArrayList;
import java.util.List;

public class MapList {
	
	/**
	 * struct maplist
		{
		uint size;
		map_item list [size];
		}
	 */
	
	public int size;
	public List<MapItem> map_item = new ArrayList<MapItem>();

}
定義位置 : data區
引用位置 :header 區 。
map_list 裡先用一個 uint 描述後面有 size 個 map_item , 後續就是對應的 size 個 map_item 描述 。
map_item 結構有 4 個元素 : type 表示該 map_item 的型別 ,本節能用到的描述如下 ,詳細Dalvik
Executable Format 裡 Type Code 的定義 ;size 表示再細分此 item , 該型別的個數 ;offset 是第一個元
素的針對檔案初始位置的偏移量 ; unuse 是用對齊位元組的 ,無實際用處 。結構定義如下:

package com.wjdiankong.parsedex.struct;

public class MapItem {
	
	/**
	 * struct map_item
		{
		ushort type;
		ushort unuse;
		uint size;
		uint offset;
		}
	 */
	
	public short type;
	public short unuse;
	public int size;
	public int offset;
	
	public static int getSize(){
		return 2 + 2 + 4 + 4;
	}
	
	@Override
	public String toString(){
		return "type:"+type+",unuse:"+unuse+",size:"+size+",offset:"+offset;
	}

}
header->map_off = 0x0244 , 偏移為 0244 的位置值為 0x 000d 。

每個 map_item 描述佔用 12 Byte , 整個 map_list 佔用 12 * size + 4 個位元組 。所以整個 map_list 佔用空
間為 12 * 13 + 4 = 160 = 0x00a0 , 佔用空間為 0x 0244 ~ 0x 02E3 。從檔案內容上看 ,也是從 0x 0244
到檔案結束的位置 。


地址 0x0244 的一個 uinit 的值為 0x0000 000d ,map_list - > size = 0x0d = 13 ,說明後續有 13 個
map_item 。根據 map_item 的結構描述在0x0248 ~ 0x02e3 裡的值 ,整理出這段二進位制所表示的 13 個
map_item 內容 ,匯成表格如下 :
map_list - > map_item 裡的內容 ,有部分 item 跟 header 裡面相應 item 的 offset 地址描述相同 。但
map_list 描述的更為全面些 ,又包括了 HEADER_ITEM , TYPE_LIST , STRING_DATA_ITEM 等 ,
最後還有它自己 TYPE_MAP_LIST 。
至此 , header 部分描述完畢 ,它包括描述 .dex 檔案的資訊 ,其餘各索引區和 data 區的偏移資訊 , 一個
map_list 結構 。map_list 裡除了對索引區和資料區的偏移地址又一次描述 ,也有其它諸如 HEAD_ITEM ,
DEBUG_INFO_ITEM 等資訊 。

(8) string_ids_size和string_ids_off

這兩個欄位表示dex中用到的所有的字串內容的大小和偏移值,我們需要解析完這部分,然後用一個字串池存起來,後面有其他的資料結構會用索引值來訪問字串,這個池子也是非常重要的。後面會詳細介紹string_ids的資料結構

(9) type_ids_size和type_ids_off

這兩個欄位表示dex中的型別資料結構的大小和偏移值,比如類型別,基本型別等資訊,後面會詳細介紹type_ids的資料結構

(10) proto_ids_size和type_ids_off

這兩個欄位表示dex中的後設資料資訊資料結構的大小和偏移值,描述方法的後設資料資訊,比如方法的返回型別,引數型別等資訊,後面會詳細介紹proto_ids的資料結構

(11) field_ids_size和field_ids_off

這兩個欄位表示dex中的欄位資訊資料結構的大小和偏移值,後面會詳細介紹field_ids的資料結構

(12) method_ids_size和method_ids_off

這兩個欄位表示dex中的方法資訊資料結構的大小和偏移值,後面會詳細介紹method_ids的資料結構

(13) class_defs_size和class_defs_off

這兩個欄位表示dex中的類資訊資料結構的大小和偏移值,這個資料結構是整個dex中最複雜的資料結構,他內部層次很深,包含了很多其他的資料結構,所以解析起來也很麻煩,所以後面會著重講解這個資料結構

(14) data_size和data_off

這兩個欄位表示dex中資料區域的結構資訊的大小和偏移值,這個結構中存放的是資料區域,比如我們定義的常量值等資訊。

到這裡我們就看完了dex的頭部資訊,頭部包含的資訊還是很多的,主要就兩個個部分:

1) 魔數+簽名+檔案大小等資訊

2) 後面的各個資料結構的大小和偏移值,都是成對出現的

下面我們就來開始介紹各個資料結構的資訊


第二、string_ids資料結構

string_ids 區索引了 .dex 檔案所有的字串 。 本區裡的元素格式為 string_ids_item , 可以使用結
構體如下描述 。

package com.wjdiankong.parsedex.struct;

import com.wjdiankong.parsedex.Utils;

public class StringIdsItem {
	
	/**
	 * struct string_ids_item
		{
		uint string_data_off;
		}
	 */
	
	public int string_data_off;
	
	public static int getSize(){
		return 4;
	}
	
	@Override
	public String toString(){
		return Utils.bytesToHexString(Utils.int2Byte(string_data_off));
	}

}
以 _ids 結尾的各個 section 裡放置的都是對應資料的偏移地址 ,只是一個索引 ,所以才會在 dex檔案佈局裡把這些區歸類為 “索引區” 。
string_data_off 只是一個偏移地址 ,它指向的資料結構為 string_data_item

package com.wjdiankong.parsedex.struct;

import java.util.ArrayList;
import java.util.List;

public class StringDataItem {
	
	/**
	 * struct string_data_item
		{
		uleb128 utf16_size;
		ubyte data;
		}
	 */
	
	/**
	 *  上述描述裡提到了 LEB128 ( little endian base 128 ) 格式 ,是基於 1 個 Byte 的一種不定長度的
		編碼方式 。若第一個 Byte 的最高位為 1 ,則表示還需要下一個 Byte 來描述 ,直至最後一個 Byte 的最高
		位為 0 。每個 Byte 的其餘 Bit 用來表示資料
	 */
	
	public List<Byte> utf16_size = new ArrayList<Byte>();
	public byte data;

}

延展

上述描述裡提到了 LEB128 ( little endian base 128 ) 格式 ,是基於 1 個 Byte 的一種不定長度的編碼方式 。若第一個 Byte 的最高位為 1 ,則表示還需要下一個 Byte 來描述 ,直至最後一個 Byte 的最高位為 0 。每個 Byte 的其餘 Bit 用來表示資料 。這裡既然介紹了uleb128這種資料型別,就在這裡解釋一下,因為後面會經常用到這個資料型別,這個資料型別的出現其實就是為了解決一個問題,那就是減少記憶體的浪費,他就是表示int型別的數值,但是int型別四個位元組有時候在使用的時候有點浪費,所以就應運而生了,他的原理也很簡單:


圖只是指示性的用兩個位元組表示。編碼的每個位元組有效部分只有低7bits,每個位元組的最高bit用來指示是否是最後一個位元組。
非最高位元組的bit7為0
最高位元組的bit7為1
將leb128編碼的數字轉換為可讀數字的規則是:除去每個位元組的bit7,將每個位元組剩餘的7個bits拼接在一起,即為數字。
比如:
LEB128編碼的0x02b0 ---> 轉換後的數字0x0130
轉換過程:
0x02b0 => 0000 0010 1011 0000 =>去除最高位=> 000 0010 011 0000 =>按4bits重排 => 00 0001 0011 0000 => 0x130

底層程式碼位於:android/dalvik/libdex/leb128.h

Java中也寫了一個工具類:

/**
 * 讀取C語言中的uleb型別
 * 目的是解決整型數值浪費問題
 * 長度不固定,在1~5個位元組中浮動
 * @param srcByte
 * @param offset
 * @return
 */
public static byte[] readUnsignedLeb128(byte[] srcByte, int offset){
	List<Byte> byteAryList = new ArrayList<Byte>();
	byte bytes = Utils.copyByte(srcByte, offset, 1)[0];
	byte highBit = (byte)(bytes & 0x80);
	byteAryList.add(bytes);
	offset ++;
	while(highBit != 0){
		bytes = Utils.copyByte(srcByte, offset, 1)[0];
		highBit = (byte)(bytes & 0x80);
		offset ++;
		byteAryList.add(bytes);
	}
	byte[] byteAry = new byte[byteAryList.size()];
	for(int j=0;j<byteAryList.size();j++){
		byteAry[j] = byteAryList.get(j);
	}
	return byteAry;
}
這個方法是讀取dex中uleb128型別的資料,遇到一個位元組最高位=0就停止讀下個位元組的原理來實現即可

還有一個方法就是解碼uleb128型別的資料:

/**
 * 解碼leb128資料
 * 每個位元組去除最高位,然後進行拼接,重新構造一個int型別數值,從低位開始
 * @param byteAry
 * @return
 */
public static int decodeUleb128(byte[] byteAry) {
	int index = 0, cur;
	int result = byteAry[index];
	index++;

	if(byteAry.length == 1){
		return result;
	}

	if(byteAry.length == 2){
		cur = byteAry[index];
		index++;
		result = (result & 0x7f) | ((cur & 0x7f) << 7);
		return result;
	}

	if(byteAry.length == 3){
		cur = byteAry[index];
		index++;
		result |= (cur & 0x7f) << 14;
		return result;
	}

	if(byteAry.length == 4){
		cur = byteAry[index];
		index++;
		result |= (cur & 0x7f) << 21;
		return result;
	}

	if(byteAry.length == 5){
		cur = byteAry[index];
		index++;
		result |= cur << 28;
		return result;
	}

	return result;

}
這個原理很簡單,就是去除每個位元組的最高位,然後拼接剩下的7位,然後從新構造一個int型別的資料,位不夠就從低位開始左移。

我們通過上面的uleb128的解釋來看,其實uleb128型別就是1~5個位元組來回浮動,為什麼是5呢?因為他要表示一個4個位元組的int型別,但是每個位元組要去除最高位,那麼肯定最多隻需要5個位元組就可以表示4個位元組的int型別資料了。這裡就解釋了uleb128資料型別,下面我們迴歸正題,繼續來看string_ids資料結構

根據 string_ids_item 和 string_data_item 的描述 ,加上 header 裡提供的入口位置 string_ids_size
= 0x0e , string_ids_off = 0x70 ,我們可以整理出 string_ids 及其對應的資料如下 :



string_ids_item 和 string_data_item 裡提取出的對應資料表格 :


string 裡的各種標誌符號 ,諸如 L , V , VL , [ 等在 .dex 檔案裡有特殊的意思 。
string_ids 的終極奧義就是找到這些字串 。其實使用二進位制編輯器開啟 .dex 檔案時 ,一般工具預設翻譯成 ASCII 碼 ,總會一大片熟悉的字元白生生地很是親切, 也很是晃眼 。剛才走過的一路子分析流程 ,就是順藤摸瓜找到它們是怎麼來的。以後的一些 type-ids , method_ids 也會引用到這一片熟悉的字串。

注意:我們後面的解析程式碼會看到,其實我們沒必要用那麼複雜的去解析uleb128型別,因為我們會看到這個字串和我們之前解析xml和resource.arsc格式一樣,每個字串的第一個位元組表示字串的長度,那麼我們只要知道每個字串的偏移地址就可以解析出字串的內容了,而每個字串的偏移地址是存放在string_ids_item中的。

到這裡我們就解析完了dex中所有的字串內容,我們用一個字串池來進行儲存即可。下面我們來繼續看type_ids資料結構


第三、type_ids資料結構

這個資料結構中存放的資料主要是描述dex中所有的型別,比如類型別,基本型別等資訊。type_ids 區索引了 dex 檔案裡的所有資料型別 ,包括 class 型別 ,陣列型別(array types)和基本型別(primitive types) 。 本區域裡的元素格式為 type_ids_item , 結構描述如下 :

package com.wjdiankong.parsedex.struct;

import com.wjdiankong.parsedex.Utils;

public class TypeIdsItem {
	
	/**
	 * struct type_ids_item
		{
		uint descriptor_idx;
		}
	 */
	
	public int descriptor_idx;
	
	public static int getSize(){
		return 4;
	}
	
	@Override
	public String toString(){
		return Utils.bytesToHexString(Utils.int2Byte(descriptor_idx));
	}

}
type_ids_item 裡面 descriptor_idx 的值的意思 ,是 string_ids 裡的 index 序號 ,是用來描述此type 的字串 。

根據 header 裡 type_ids_size = 0x07 , type_ids_off = 0xa8 , 找到對應的二進位制描述區 。00000a0: 1a02


根據 type_id_item 的描述 ,整理出表格如下 。因為 type_id_item - > descriptor_idx 裡存放的是指向 string_ids 的 index 號 ,所以我們也能得到該 type 的字串描述 。這裡出現了 3 個 type descriptor :
L 表示 class 的詳細描述 ,一般以分號表示 class 描述結束 ;
V 表示 void 返回型別 ,只有在返回值的時候有效 ;
[ 表示陣列 ,[Ljava/lang/String; 可以對應到 java 語言裡的 java.lang.String[] 型別 。


我們後面的其他資料結構也會使用到type_ids型別,所以我們這裡解析完type_ids也是需要用一個池子來存放的,後面直接用索引index來訪問即可。


第四、proto_ids資料結構

proto 的意思是 method prototype 代表 java 語言裡的一個 method 的原型 。proto_ids 裡的元素為 proto_id_item , 結構如下 。

package com.wjdiankong.parsedex.struct;

import java.util.ArrayList;
import java.util.List;

public class ProtoIdsItem {
	
	/**
	 * struct proto_id_item
		{
		uint shorty_idx;
		uint return_type_idx;
		uint parameters_off;
		}
	 */
	
	public int shorty_idx;
	public int return_type_idx;
	public int parameters_off;
	
	//這個不是公共欄位,而是為了儲存方法原型中的引數型別名和引數個數
	public List<String> parametersList = new ArrayList<String>();
	public int parameterCount;
	
	public static int getSize(){
		return 4 + 4 + 4;
	}
	
	@Override
	public String toString(){
		return "shorty_idx:"+shorty_idx+",return_type_idx:"+return_type_idx+",parameters_off:"+parameters_off;
	}

}
shorty_idx :跟 type_ids 一樣 ,它的值是一個 string_ids 的 index 號 ,最終是一個簡短的字串描述 ,用來說明該 method 原型 
return_type_idx :它的值是一個 type_ids 的 index 號 ,表示該 method 原型的返回值型別 。
parameters_off :字尾 off 是 offset , 指向 method 原型的引數列表 type_list ; 若 method 沒有引數 ,值為0 。引數列表的格式是 type_list ,結構從邏輯上如下描述 。size 表示引數的個數 ;type_idx 是對應引數的型別 ,它的值是一個 type_ids 的 index 號 ,跟 return_type_idx 是同一個品種的東西 。

package com.wjdiankong.parsedex.struct;

import java.util.ArrayList;
import java.util.List;

public class TypeList {
	
	/**
	 * struct type_list
		{
		uint size;
		ushort type_idx[size];
		}
	 */
	
	public int size;//引數的個數
	public List<Short> type_idx = new ArrayList<Short>();//引數的型別
	
}

header 裡 proto_ids_size = 0x03 , proto_ids_off = 0xc4 , 它的二進位制描述區如下 :


根據 proto_id_item 和 type_list 的格式 ,對照這它們的二進位制部分 ,整理出表格如下 :


可以看出 ,有 3 個 method 原型 ,返回值都為 void ,index = 0 的沒有引數傳入 ,index = 1 的傳入一個
String 引數 ,index=2 的傳入一個 String[] 型別的引數 。

注意:我們在這裡會看到很多idx結尾的欄位,這個一般都是索引值,所以我們要注意的是,區分這個索引值到底是對應的哪張表格,是字串池,還是型別池等資訊,這個如果弄混淆的話,那麼解析就會出現混亂了。這個後面其他資料結構都是需要注意的。


第五、field_ids資料結構

filed_ids 區裡面存放的是dex 檔案引用的所有的 field 。本區的元素格式是 field_id_item ,邏輯結構描述如

package com.wjdiankong.parsedex.struct;

public class FieldIdsItem {
	
	/**
	 * struct filed_id_item
		{
		ushort class_idx;
		ushort type_idx;
		uint name_idx;
		}
	 */
	
	public short class_idx;
	public short type_idx;
	public int name_idx;
	
	public static int getSize(){
		return 2 + 2 + 4;
	}

	@Override
	public String toString(){
		return "class_idx:"+class_idx+",type_idx:"+type_idx+",name_idx:"+name_idx;
	}
	
}
class_idx :表示本 field 所屬的 class 型別 , class_idx 的值是 type_ids 的一個 index , 並且必須指向一個class 型別 。
type_idx :表示本 field 的型別 ,它的值也是 type_ids 的一個 index 。
name_idx : 表示本 field 的名稱 ,它的值是 string_ids 的一個 index 。

header 裡 field_ids_size = 1 , field_ids_off = 0xe8 。說明本 .dex 只有一個 field ,這部分的二進位制描述如下 :


注意:這裡的欄位都是索引值,一定要區分是哪個池子的索引值,還有就是,這個資料結構我們後面也要使用到,所以需要用一個池子來儲存。


第六、 method_ids資料結構

method_ids 是索引區的最後一個條目 ,它索引了 dex 檔案裡的所有的 method.
method_ids 的元素格式是 method_id_item , 結構跟 fields_ids 很相似:

package com.wjdiankong.parsedex.struct;

public class MethodIdsItem {
	
	/**
	 * struct filed_id_item
		{
		ushort class_idx;
		ushort proto_idx;
		uint name_idx;
		}
	 */
	
	public short class_idx;
	public short proto_idx;
	public int name_idx;
	
	public static int getSize(){
		return 2 + 2 + 4;
	}
	
	@Override
	public String toString(){
		return "class_idx:"+class_idx+",proto_idx:"+proto_idx+",name_idx:"+name_idx;
	}

}
class_idx :表示本 method 所屬的 class 型別 , class_idx 的值是 type_ids 的一個 index , 並且必須指向一個 class 型別 。
name_idx :表示本 method 的名稱 ,它的值是 string_ids 的一個 index 。
proto_idx :描述該 method 的原型 ,指向 proto_ids 的一個 index 。
header 裡 method_ids_size = 0x04 , method_ids_off = 0xf0 。本部分的二進位制描述如下 :


對 dex 反彙編的時候 ,常用的 method 表示方法是這種形式 :
Lpackage/name/ObjectName;->MethodName(III)Z
將上述表格裡的字串再次整理下 ,method 的描述分別為 :
0:Lhello; -> <init>()V
1:LHello; -> main([Ljava/lang/String;)V
2:Ljava/io/PrintStream; -> println(Ljava/lang/String;)V
3: Ljava/lang/Object; -> <init>()V
至此 ,索引區的內容描述完畢 ,包括 string_ids , type_ids,proto_ids , field_ids , method_ids 。每個索引區域裡存放著指向具體資料的偏移地址 (如 string_ids ) , 或者存放的資料是其它索引區域裡面的 index 號。

注意:這裡的欄位都是索引值,一定要區分是哪個池子的索引值,還有就是,這個資料結構我們後面也要使用到,所以需要用一個池子來儲存。


第八、class_defs資料結構

上面我們介紹了所有的索引區域,終於到了最後一個資料結構了,但是我們現在還不能開心,因為這個資料結構是最複雜的,所以解析下來還是很費勁的。因為他的層次太深了。

1、class_def_item
從字面意思解釋 ,class_defs 區域裡存放著 class definitions , class 的定義 。它的結構較 dex 區都要複雜些 ,因為有些資料都直接指向了data 區裡面 。
class_defs 的資料格式為 class_def_item , 結構描述如下 :

package com.wjdiankong.parsedex.struct;

public class ClassDefItem {
	
	/**
	 * struct class_def_item
		{
		uint class_idx;
		uint access_flags;
		uint superclass_idx;
		uint interfaces_off;
		uint source_file_idx;
		uint annotations_off;
		uint class_data_off;
		uint static_value_off;
		}
	 */
	
	public int class_idx;
	public int access_flags;
	public int superclass_idx;
	public int iterfaces_off;
	public int source_file_idx;
	public int annotations_off;
	public int class_data_off;
	public int static_value_off;
	
	public final static int 
			ACC_PUBLIC       = 0x00000001,       // class, field, method, ic
			ACC_PRIVATE      = 0x00000002,       // field, method, ic
			ACC_PROTECTED    = 0x00000004,       // field, method, ic
			ACC_STATIC       = 0x00000008,       // field, method, ic
			ACC_FINAL        = 0x00000010,       // class, field, method, ic
			ACC_SYNCHRONIZED = 0x00000020,       // method (only allowed on natives)
			ACC_SUPER        = 0x00000020,       // class (not used in Dalvik)
			ACC_VOLATILE     = 0x00000040,       // field
			ACC_BRIDGE       = 0x00000040,       // method (1.5)
			ACC_TRANSIENT    = 0x00000080,       // field
			ACC_VARARGS      = 0x00000080,       // method (1.5)
			ACC_NATIVE       = 0x00000100,       // method
			ACC_INTERFACE    = 0x00000200,       // class, ic
			ACC_ABSTRACT     = 0x00000400,       // class, method, ic
			ACC_STRICT       = 0x00000800,       // method
			ACC_SYNTHETIC    = 0x00001000,       // field, method, ic
			ACC_ANNOTATION   = 0x00002000,       // class, ic (1.5)
			ACC_ENUM         = 0x00004000,       // class, field, ic (1.5)
			ACC_CONSTRUCTOR  = 0x00010000,       // method (Dalvik only)
			ACC_DECLARED_SYNCHRONIZED = 0x00020000,       // method (Dalvik only)
			ACC_CLASS_MASK =
			(ACC_PUBLIC | ACC_FINAL | ACC_INTERFACE | ACC_ABSTRACT
					| ACC_SYNTHETIC | ACC_ANNOTATION | ACC_ENUM),
					ACC_INNER_CLASS_MASK =
					(ACC_CLASS_MASK | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC),
					ACC_FIELD_MASK =
					(ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC | ACC_FINAL
							| ACC_VOLATILE | ACC_TRANSIENT | ACC_SYNTHETIC | ACC_ENUM),
							ACC_METHOD_MASK =
							(ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC | ACC_FINAL
									| ACC_SYNCHRONIZED | ACC_BRIDGE | ACC_VARARGS | ACC_NATIVE
									| ACC_ABSTRACT | ACC_STRICT | ACC_SYNTHETIC | ACC_CONSTRUCTOR
									| ACC_DECLARED_SYNCHRONIZED);
	
	public static int getSize(){
		return 4 * 8;
	}
	
	@Override
	public String toString(){
		return "class_idx:"+class_idx+",access_flags:"+access_flags+",superclass_idx:"+superclass_idx+",iterfaces_off:"+iterfaces_off
				+",source_file_idx:"+source_file_idx+",annotations_off:"+annotations_off+",class_data_off:"+class_data_off
				+",static_value_off:"+static_value_off;
	}

}
(1) class_idx:描述具體的 class 型別 ,值是 type_ids 的一個 index 。值必須是一個 class 型別 ,不能是陣列型別或者基本型別 。
(2) access_flags: 描述 class 的訪問型別 ,諸如 public , final , static 等 。在 dex-format.html 裡 “access_flagsDefinitions” 有具體的描述 。
(3) superclass_idx:描述 supperclass 的型別 ,值的形式跟 class_idx 一樣 。
(4) interfaces_off:值為偏移地址 ,指向 class 的 interfaces , 被指向的資料結構為 type_list 。class 若沒有interfaces ,值為 0。
(5) source_file_idx:表示原始碼檔案的資訊 ,值是 string_ids 的一個 index 。若此項資訊缺失 ,此項值賦值為NO_INDEX=0xffff ffff 
(6) annotions_off:值是一個偏移地址 ,指向的內容是該 class 的註釋 ,位置在 data 區,格式為annotations_direcotry_item 。若沒有此項內容 ,值為 0 。
(7) class_data_off:值是一個偏移地址 ,指向的內容是該 class 的使用到的資料 ,位置在 data 區,格式為class_data_item 。若沒有此項內容 ,值為 0 。該結構裡有很多內容 ,詳細描述該 class 的 field ,method, method 裡的執行程式碼等資訊 ,後面有一個比較大的篇幅來講述 class_data_item 。
(8) static_value_off:值是一個偏移地址 ,指向 data 區裡的一個列表 ( list ) ,格式為 encoded_array_item。若沒有此項內容 ,值為 0 。

header 裡 class_defs_size = 0x01 , class_defs_off = 0x 0110 。則此段二進位制描述為 :



其實最初被編譯的原始碼只有幾行 ,和 class_def_item 的表格對照下 ,一目瞭然 。
source file : Hello.java
public class Hello
{
element value associated strinigs
class_idx 0x00 LHello;
access_flags 0x01 ACC_PUBLIC
superclass_idx 0x02 Ljava/lang/Object;
interface_off 0x00
source_file_idx 0x02 Hello.java
annotations_off 0x00
class_data_off 0x0234
static_value_off 0x00

public static void main(String[] argc)
{
System.out.println("Hello, Android!\n");
}
}


2、 class_def_item => class_data_item
class_data_off 指向 data 區裡的 class_data_item 結構 ,class_data_item 裡存放著本 class 使用到的各種資料 ,下面是 class_data_item 的邏輯結構 :

package com.wjdiankong.parsedex.struct;

public class ClassDataItem {
	
	/**
	 *  uleb128 unsigned little-endian base 128
		struct class_data_item
		{
			uleb128 static_fields_size;
			uleb128 instance_fields_size;
			uleb128 direct_methods_size;
			uleb128 virtual_methods_size;
			encoded_field static_fields [ static_fields_size ];
			encoded_field instance_fields [ instance_fields_size ];
			encoded_method direct_methods [ direct_method_size ];
			encoded_method virtual_methods [ virtual_methods_size ];
		}
	 */
	
	//uleb128只用來編碼32位的整型數
	public int static_fields_size;
	public int instance_fields_size;
	public int direct_methods_size;
	public int virtual_methods_size;
	
	public EncodedField[] static_fields;
	public EncodedField[] instance_fields;
	public EncodedMethod[] direct_methods;
	public EncodedMethod[] virtual_methods;
	
	@Override
	public String toString(){
		return "static_fields_size:"+static_fields_size+",instance_fields_size:"
				+instance_fields_size+",direct_methods_size:"+direct_methods_size+",virtual_methods_size:"+virtual_methods_size
				+"\n"+getFieldsAndMethods();
	}
	
	private String getFieldsAndMethods(){
		StringBuilder sb = new StringBuilder();
		sb.append("static_fields:\n");
		for(int i=0;i<static_fields.length;i++){
			sb.append(static_fields[i]+"\n");
		}
		sb.append("instance_fields:\n");
		for(int i=0;i<instance_fields.length;i++){
			sb.append(instance_fields[i]+"\n");
		}
		sb.append("direct_methods:\n");
		for(int i=0;i<direct_methods.length;i++){
			sb.append(direct_methods[i]+"\n");
		}
		sb.append("virtual_methods:\n");
		for(int i=0;i<virtual_methods.length;i++){
			sb.append(virtual_methods[i]+"\n");
		}
		return sb.toString();
	}

}
關於元素的格式 uleb128 在 string_ids 裡有講述過 ,不贅述 。
encoded_field 的結構如下 :

package com.wjdiankong.parsedex.struct;

import com.wjdiankong.parsedex.Utils;

public class EncodedField {
	
	/**
	 * struct encoded_field
		{
			uleb128 filed_idx_diff; // index into filed_ids for ID of this filed
			uleb128 access_flags; // access flags like public, static etc.
		}
	 */
	public byte[] filed_idx_diff;
	public byte[] access_flags;
	
	@Override
	public String toString(){
		return "field_idx_diff:"+Utils.bytesToHexString(filed_idx_diff) + ",access_flags:"+Utils.bytesToHexString(filed_idx_diff);
	}

}

encoded_method 的結構如下 :

package com.wjdiankong.parsedex.struct;

import com.wjdiankong.parsedex.Utils;

public class EncodedMethod {
	
	/**
	 * struct encoded_method
		{
			uleb128 method_idx_diff;
			uleb128 access_flags;
			uleb128 code_off;
		}
	 */
	
	public byte[] method_idx_diff;
	public byte[] access_flags;
	public byte[] code_off;
	
	@Override
	public String toString(){
		return "method_idx_diff:"+Utils.bytesToHexString(method_idx_diff)+","+Utils.bytesToHexString(Utils.int2Byte(Utils.decodeUleb128(method_idx_diff)))
				+",access_flags:"+Utils.bytesToHexString(access_flags)+","+Utils.bytesToHexString(Utils.int2Byte(Utils.decodeUleb128(access_flags)))
				+",code_off:"+Utils.bytesToHexString(code_off)+","+Utils.bytesToHexString(Utils.int2Byte(Utils.decodeUleb128(code_off)));
	}

}

(1) method_idx_diff:字首 methd_idx 表示它的值是 method_ids 的一個 index ,字尾 _diff 表示它是於另外一個 method_idx 的一個差值 ,就是相對於 encodeed_method [] 陣列裡上一個元素的 method_idx 的差值 。其實 encoded_filed - > field_idx_diff 表示的也是相同的意思 ,只是編譯出來的 Hello.dex 檔案裡沒有使用到class filed 所以沒有仔細講 ,詳細的參考 dex_format.html 的官網文件 
(2) access_flags:訪問許可權 , 比如 public、private、static、final 等 。
(3) code_off:一個指向 data 區的偏移地址 ,目標是本 method 的程式碼實現 。被指向的結構是
code_item ,有近 10 項元素 ,後面再詳細解釋 。
class_def_item -- > class_data_off = 0x 0234 。


名稱為 LHello; 的 class 裡只有 2 個 directive methods 。 directive_methods 裡的值都是 uleb128 的原始二
進位制值 。按照 directive_methods 的格式 encoded_method 再整理一次這 2 個 method 描述 ,得到結果如下
表格所描述 :


method 一個是 <init> , 一個是 main , 這裡我們需要用我們在string_ids那塊介紹到的一個方法就是解碼uleb125型別的方法得到正確的value值。


3、class_def_item => class_data_item => code_item
到這裡 ,邏輯的描述有點深入了 。我自己都有點分析不過來 ,先理一下是怎麼走到這一步的 ,code_item
在 dex 裡處於一個什麼位置 。
(1) 一個 .dex 檔案被分成了 9 個區 ,詳細見 “1. dex 整個檔案的佈局 ” 。其中有一個索引區叫做
class_defs , 索引了 .dex 裡面用到的 class ,以及對這個 class 的描述 。
(2) class_defs 區 , 這裡面其實是class_def_item 結構 。這個結構裡描述了 LHello; 的各種資訊 ,諸如名稱 ,superclass , access flag, interface 等 。class_def_item 裡有一個元素 class_data_off , 指向data 區裡的一個 class_data_item 結構 ,用來描述 class 使用到的各種資料 。自此以後的結構都歸於 data區了 。
(3) class_data_item 結構 ,裡描述值著 class 裡使用到的 static field , instance field , direct_method ,和 virtual_method 的數目和描述 。例子 Hello.dex 裡 ,只有 2 個 direct_method , 其餘的 field 和method 的數目都為 0 。描述 direct_method 的結構叫做 encoded_method ,是用來詳細描述某個 method的 。
(4) encoded_method 結構 ,描述某個 method 的 method 型別 , access flags 和一個指向 code_item的偏移地址 ,裡面存放的是該 method 的具體實現 。
(5) code_item , 一層又一層 ,盜夢空間啊!簡要的說 ,code_item 結構裡描述著某個 method 的具體實現 。它的結構如下描述 :

package com.wjdiankong.parsedex.struct;

import com.wjdiankong.parsedex.Utils;

public class CodeItem {
	
	/**
	 * struct code_item
		{
			ushort registers_size;
			ushort ins_size;
			ushort outs_size;
			ushort tries_size;
			uint debug_info_off;
			uint insns_size;
			ushort insns [ insns_size ];
			ushort paddding; // optional
			try_item tries [ tyies_size ]; // optional
			encoded_catch_handler_list handlers; // optional
		}
	 */
	
	public short registers_size;
	public short ins_size;
	public short outs_size;
	public short tries_size;
	public int debug_info_off;
	public int insns_size;
	public short[] insns;
	
	@Override
	public String toString(){
		return "regsize:"+registers_size+",ins_size:"+ins_size
				+",outs_size:"+outs_size+",tries_size:"+tries_size+",debug_info_off:"+debug_info_off
				+",insns_size:"+insns_size + "\ninsns:"+getInsnsStr();
	}
	
	private String getInsnsStr(){
		StringBuilder sb = new StringBuilder();
		for(int i=0;i<insns.length;i++){
			sb.append(Utils.bytesToHexString(Utils.short2Byte(insns[i]))+",");
		}
		return sb.toString();
	}
	
}
末尾的 3 項標誌為 optional , 表示可能有 ,也可能沒有 ,根據具體的程式碼來 。
(1) registers_size:本段程式碼使用到的暫存器數目。
(2) ins_size:method傳入引數的數目 。
(3) outs_size: 本段程式碼呼叫其它method 時需要的引數個數 。
(4) tries_size: try_item 結構的個數 。
(5) debug_off:偏移地址 ,指向本段程式碼的 debug 資訊存放位置 ,是一個 debug_info_item 結構。
(6) insns_size:指令列表的大小 ,以 16-bit 為單位 。 insns 是 instructions 的縮寫 。
(7) padding:值為 0 ,用於對齊位元組 。
(8) tries 和 handlers:用於處理 java 中的 exception , 常見的語法有 try catch 。

4、 分析 main method 的執行程式碼並與 smali 反編譯的結果比較
在 8.2 節裡有 2 個 method , 因為 main 裡的執行程式碼是自己寫的 ,分析它會熟悉很多 。偏移地址是
directive_method [1] -> code_off = 0x0148 ,二進位制描述如下 :


insns 陣列裡的 8 個二進位制原始資料 , 對這些資料的解析 ,需要對照官網的文件 《Dalvik VM Instruction
Format》和《Bytecode for Dalvik VM》。
分析思路整理如下
(1) 《Dalvik VM Instruction Format》 裡操作符 op 都是位於首個 16bit 資料的低 8 bit ,起始的是 op =0x62。
(2) 在 《Bytecode for Dalvik VM》 裡找到對應的 Syntax 和 format 。
syntax = sget_object
format = 0x21c 。
(3) 在《Dalvik VM Instruction Format》裡查詢 21c , 得知 op = 0x62 的指令佔據 2 個 16 bit 資料 ,格式是 AA|op BBBB ,解釋為 op vAA, type@BBBB 。因此這 8 組 16 bit 資料裡 ,前 2 個是一組 。對比資料得 AA=0x00, BBBB = 0x0000。
(4)返回《Bytecode for Dalvik VM》裡查閱對 sget_object 的解釋, AA 的值表示 Value Register ,即0 號暫存器; BBBB 表示 static field 的 index ,就是之前分析的field_ids 區裡 Index = 0 指向的那個東西 ,當時的 fields_ids 的分析結果如下 :


對 field 常用的表述是
包含 field 的型別 -> field 名稱 :field 型別 。
此次指向的就是 Ljava/lang/System; -> out:Ljava/io/printStream;
(5) 綜上 ,前 2 個 16 bit 資料 0x 0062 0000 , 解釋為
sget_object v0, Ljava/lang/System; -> out:Ljava/io/printStream;
其餘的 6 個 16 bit 資料分析思路跟這個一樣 ,依次整理如下 :
0x011a 0x0001: const-string v1, “Hello, Android!”

0x206e 0x0002 0x0010:

invoke-virtual {v0, v1}, Ljava/io/PrintStream; -> println(Ljava/lang/String;)V

0x000e: return-void
(6) 最後再整理下 main method , 用容易理解的方式表示出來就是 。
ACC_PUBLIC ACC_STATIC LHello;->main([Ljava/lang/String;)V
{
sget_object v0, Ljava/lang/System; -> out:Ljava/io/printStream;
const-string v1,Hello, Android!
invoke-virtual {v0, v1}, Ljava/io/PrintStream; -> println(Ljava/lang/String;)V
return-void
}
看起來很像 smali 格式語言 ,不妨使用 smali 反編譯下 Hello.dex , 看看 smali 生成的程式碼跟方才推匯出
來的有什麼差異 。
.method public static main([Ljava/lang/String;)V
.registers 3
.prologue
.line 5
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello, Android!\n"
index 0
class_idx 0x04
type_idx 0x01
name_idx 0x0c
class string Ljava/lang/System;
type string Ljava/io/PrintStream;
name string out
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
.line 6
return-void
從內容上看 ,二者形式上有些差異 ,但表述的是同一個 method 。這說明剛才的分析走的路子是沒有跑偏
的 。另外一個 method 是 <init> , 若是分析的話 ,思路和流程跟 main 一樣 。走到這裡,心裡很踏實了。


四、解析程式碼

上面我們解析完了所有的資料結構區域,下面就來看看具體的解析程式碼,由於篇幅的原因,這裡就不貼出全部的程式碼了,只貼出核心的程式碼:

1、解析頭部資訊:

public static void praseDexHeader(byte[] byteSrc){
	HeaderType headerType = new HeaderType();
	//解析魔數
	byte[] magic = Utils.copyByte(byteSrc, 0, 8);
	headerType.magic = magic;

	//解析checksum
	byte[] checksumByte = Utils.copyByte(byteSrc, 8, 4);
	headerType.checksum = Utils.byte2int(checksumByte);

	//解析siganature
	byte[] siganature = Utils.copyByte(byteSrc, 12, 20);
	headerType.siganature = siganature;

	//解析file_size
	byte[] fileSizeByte = Utils.copyByte(byteSrc, 32, 4);
	headerType.file_size = Utils.byte2int(fileSizeByte);

	//解析header_size
	byte[] headerSizeByte = Utils.copyByte(byteSrc, 36, 4);
	headerType.header_size = Utils.byte2int(headerSizeByte);

	//解析endian_tag
	byte[] endianTagByte = Utils.copyByte(byteSrc, 40, 4);
	headerType.endian_tag = Utils.byte2int(endianTagByte);

	//解析link_size
	byte[] linkSizeByte = Utils.copyByte(byteSrc, 44, 4);
	headerType.link_size = Utils.byte2int(linkSizeByte);

	//解析link_off
	byte[] linkOffByte = Utils.copyByte(byteSrc, 48, 4);
	headerType.link_off = Utils.byte2int(linkOffByte);

	//解析map_off
	byte[] mapOffByte = Utils.copyByte(byteSrc, 52, 4);
	headerType.map_off = Utils.byte2int(mapOffByte);

	//解析string_ids_size
	byte[] stringIdsSizeByte = Utils.copyByte(byteSrc, 56, 4);
	headerType.string_ids_size = Utils.byte2int(stringIdsSizeByte);

	//解析string_ids_off
	byte[] stringIdsOffByte = Utils.copyByte(byteSrc, 60, 4);
	headerType.string_ids_off = Utils.byte2int(stringIdsOffByte);

	//解析type_ids_size
	byte[] typeIdsSizeByte = Utils.copyByte(byteSrc, 64, 4);
	headerType.type_ids_size = Utils.byte2int(typeIdsSizeByte);

	//解析type_ids_off
	byte[] typeIdsOffByte = Utils.copyByte(byteSrc, 68, 4);
	headerType.type_ids_off = Utils.byte2int(typeIdsOffByte);

	//解析proto_ids_size
	byte[] protoIdsSizeByte = Utils.copyByte(byteSrc, 72, 4);
	headerType.proto_ids_size = Utils.byte2int(protoIdsSizeByte);

	//解析proto_ids_off
	byte[] protoIdsOffByte = Utils.copyByte(byteSrc, 76, 4);
	headerType.proto_ids_off = Utils.byte2int(protoIdsOffByte);

	//解析field_ids_size
	byte[] fieldIdsSizeByte = Utils.copyByte(byteSrc, 80, 4);
	headerType.field_ids_size = Utils.byte2int(fieldIdsSizeByte);

	//解析field_ids_off
	byte[] fieldIdsOffByte = Utils.copyByte(byteSrc, 84, 4);
	headerType.field_ids_off = Utils.byte2int(fieldIdsOffByte);

	//解析method_ids_size
	byte[] methodIdsSizeByte = Utils.copyByte(byteSrc, 88, 4);
	headerType.method_ids_size = Utils.byte2int(methodIdsSizeByte);

	//解析method_ids_off
	byte[] methodIdsOffByte = Utils.copyByte(byteSrc, 92, 4);
	headerType.method_ids_off = Utils.byte2int(methodIdsOffByte);

	//解析class_defs_size
	byte[] classDefsSizeByte = Utils.copyByte(byteSrc, 96, 4);
	headerType.class_defs_size = Utils.byte2int(classDefsSizeByte);

	//解析class_defs_off
	byte[] classDefsOffByte = Utils.copyByte(byteSrc, 100, 4);
	headerType.class_defs_off = Utils.byte2int(classDefsOffByte);

	//解析data_size
	byte[] dataSizeByte = Utils.copyByte(byteSrc, 104, 4);
	headerType.data_size = Utils.byte2int(dataSizeByte);

	//解析data_off
	byte[] dataOffByte = Utils.copyByte(byteSrc, 108, 4);
	headerType.data_off = Utils.byte2int(dataOffByte);

	System.out.println("header:"+headerType);

	stringIdOffset = headerType.header_size;//header之後就是string ids

	stringIdsSize = headerType.string_ids_size;
	stringIdsOffset = headerType.string_ids_off;
	typeIdsSize = headerType.type_ids_size;
	typeIdsOffset = headerType.type_ids_off;
	fieldIdsSize = headerType.field_ids_size;
	fieldIdsOffset = headerType.field_ids_off;
	protoIdsSize = headerType.proto_ids_size;
	protoIdsOffset = headerType.proto_ids_off;
	methodIdsSize = headerType.method_ids_size;
	methodIdsOffset = headerType.method_ids_off;
	classIdsSize = headerType.class_defs_size;
	classIdsOffset = headerType.class_defs_off;

	mapListOffset = headerType.map_off;

}
這裡沒啥說的,就是記錄幾個索引區的偏移值和大小資訊。

解析結果:



2、解析string_ids索引區

/************************解析字串********************************/
public static void parseStringIds(byte[] srcByte){
	int idSize = StringIdsItem.getSize();
	int countIds = stringIdsSize;
	for(int i=0;i<countIds;i++){
		stringIdsList.add(parseStringIdsItem(Utils.copyByte(srcByte, stringIdsOffset+i*idSize, idSize)));
	}
	System.out.println("string size:"+stringIdsList.size());
}

public static void parseStringList(byte[] srcByte){
	//第一個位元組還是字串的長度
	for(StringIdsItem item : stringIdsList){
		String str = getString(srcByte, item.string_data_off);
		System.out.println("str:"+str);
		stringList.add(str);
	}
}

解析結果:



3、解析type_ids索引區

/***************************解析型別******************************/
public static void parseTypeIds(byte[] srcByte){
	int idSize = TypeIdsItem.getSize();
	int countIds = typeIdsSize;
	for(int i=0;i<countIds;i++){
		typeIdsList.add(parseTypeIdsItem(Utils.copyByte(srcByte, typeIdsOffset+i*idSize, idSize)));
	}

	//這裡的descriptor_idx就是解析之後的字串中的索引值
	for(TypeIdsItem item : typeIdsList){
		System.out.println("typeStr:"+stringList.get(item.descriptor_idx));
	}
}

解析結果:



4、解析proto_ids索引區

/***************************解析Proto***************************/
public static void parseProtoIds(byte[] srcByte){
	int idSize = ProtoIdsItem.getSize();
	int countIds = protoIdsSize;
	for(int i=0;i<countIds;i++){
		protoIdsList.add(parseProtoIdsItem(Utils.copyByte(srcByte, protoIdsOffset+i*idSize, idSize)));
	}

	for(ProtoIdsItem item : protoIdsList){
		System.out.println("proto:"+stringList.get(item.shorty_idx)+","+stringList.get(item.return_type_idx));
		//有的方法沒有引數,這個值就是0
		if(item.parameters_off != 0){
			item = parseParameterTypeList(srcByte, item.parameters_off, item);
		}
	}
}

//解析方法的所有引數型別
private static ProtoIdsItem parseParameterTypeList(byte[] srcByte, int startOff, ProtoIdsItem item){
	//解析size和size大小的List中的內容
	byte[] sizeByte = Utils.copyByte(srcByte, startOff, 4);
	int size = Utils.byte2int(sizeByte);
	List<String> parametersList = new ArrayList<String>();
	List<Short> typeList = new ArrayList<Short>(size);
	for(int i=0;i<size;i++){
		byte[] typeByte = Utils.copyByte(srcByte, startOff+4+2*i, 2);
		typeList.add(Utils.byte2Short(typeByte));
	}
	System.out.println("param count:"+size);
	for(int i=0;i<typeList.size();i++){
		System.out.println("type:"+stringList.get(typeList.get(i)));
		int index = typeIdsList.get(typeList.get(i)).descriptor_idx;
		parametersList.add(stringList.get(index));
	}

	item.parameterCount = size;
	item.parametersList = parametersList;

	return item;
}

解析結果:



5、解析field_ids索引區

/***************************解析欄位****************************/
public static void parseFieldIds(byte[] srcByte){
	int idSize = FieldIdsItem.getSize();
	int countIds = fieldIdsSize;
	for(int i=0;i<countIds;i++){
		fieldIdsList.add(parseFieldIdsItem(Utils.copyByte(srcByte, fieldIdsOffset+i*idSize, idSize)));
	}

	for(FieldIdsItem item : fieldIdsList){
		int classIndex = typeIdsList.get(item.class_idx).descriptor_idx;
		int typeIndex = typeIdsList.get(item.type_idx).descriptor_idx;
		System.out.println("class:"+stringList.get(classIndex)+",name:"+stringList.get(item.name_idx)+",type:"+stringList.get(typeIndex));
	}
}

解析結果:



6、解析method_ids索引區

/***************************解析方法*****************************/
public static void parseMethodIds(byte[] srcByte){
	int idSize = MethodIdsItem.getSize();
	int countIds = methodIdsSize;
	for(int i=0;i<countIds;i++){
		methodIdsList.add(parseMethodIdsItem(Utils.copyByte(srcByte, methodIdsOffset+i*idSize, idSize)));
	}

	for(MethodIdsItem item : methodIdsList){
		int classIndex = typeIdsList.get(item.class_idx).descriptor_idx;
		int returnIndex = protoIdsList.get(item.proto_idx).return_type_idx;
		String returnTypeStr = stringList.get(typeIdsList.get(returnIndex).descriptor_idx);
		int shortIndex = protoIdsList.get(item.proto_idx).shorty_idx;
		String shortStr = stringList.get(shortIndex);
		List<String> paramList = protoIdsList.get(item.proto_idx).parametersList;
		StringBuilder parameters = new StringBuilder();
		parameters.append(returnTypeStr+"(");
		for(String str : paramList){
			parameters.append(str+",");
		}
		parameters.append(")"+shortStr);
		System.out.println("class:"+stringList.get(classIndex)+",name:"+stringList.get(item.name_idx)+",proto:"+parameters);
	}

}



7、解析class_def區域

/****************************解析類*****************************/
public static void parseClassIds(byte[] srcByte){
	System.out.println("classIdsOffset:"+Utils.bytesToHexString(Utils.int2Byte(classIdsOffset)));
	System.out.println("classIds:"+classIdsSize);
	int idSize = ClassDefItem.getSize();
	int countIds = classIdsSize;
	for(int i=0;i<countIds;i++){
		classIdsList.add(parseClassDefItem(Utils.copyByte(srcByte, classIdsOffset+i*idSize, idSize)));
	}
	for(ClassDefItem item : classIdsList){
		System.out.println("item:"+item);
		int classIdx = item.class_idx;
		TypeIdsItem typeItem = typeIdsList.get(classIdx);
		System.out.println("classIdx:"+stringList.get(typeItem.descriptor_idx));
		int superClassIdx = item.superclass_idx;
		TypeIdsItem superTypeItem = typeIdsList.get(superClassIdx);
		System.out.println("superitem:"+stringList.get(superTypeItem.descriptor_idx));
		int sourceIdx = item.source_file_idx;
		String sourceFile = stringList.get(sourceIdx);
		System.out.println("sourceFile:"+sourceFile);
		classDataMap.put(sourceFile, item);
	}
}
解析結果:


這裡我們看到解析結果我們可能有點看不懂,其實這裡我是沒有在繼續解讀下去了,為什麼,因為我們通過class_def的資料結構解析可以看到,我們需要藉助《Bytecode for Dalvik VM》這個來進行查閱具體的指令,然後翻譯成具體的指令程式碼,關於這個指令表可以參考這裡:http://www.netmite.com/android/mydroid/dalvik/docs/dalvik-bytecode.html,所以具體解析並不複雜,所以這裡就不在詳細解析了,具體的解析思路,可以參考class_def的資料結構解析那一塊的內容,上面又說道。


專案下載地址:http://download.csdn.net/detail/jiangwei0910410003/9432263


注意:

我們到這裡算是解析完了dex檔案了,但是我現在要告訴大家,其實Android中有一個工具可以為我們做這個事,不知道大家還記得我們之前介紹解析AndroidManifest.xml和resource.arsc檔案格式的時候,也是一樣的,直接用aapt命令就可以檢視了,這裡也是一樣的,只是這個工具是:dexdump

這個命令也是在AndroidSdk目錄下的build-tools下面,這裡我們可以將列印的結果重定向到demo.txt檔案中


那麼我們上面做的解析工作是不是就沒有用了呢?當然不是,我們在後面會說道我們解析dex格式有很多用途的。


五、技術總結和概述

到這裡我們就解析完了dex檔案的所有東東,講解的內容有點多,在這裡就來總結一下:

第一、學習到的技術

1、我們學習到了如何不是用任何的IDE工具,就可以構造一個dex檔案出來,主要藉助於java和dx命令。同時,我們也學會了一個可以執行dex檔案的命令:dalvikvm;不過這個命令需要root許可權。

2、我們瞭解到了Android中的DVM指令,如何翻譯指令程式碼。

3、學習了一個資料型別:uleb128,如何將uleb128型別和int型別進行轉化。

第二、未解決的問題

我們在整個解析的過程中會發現,我們這裡只是用一個非常簡單的dex來做案例解析,所以解析起來也很容易,但是我們實際的過程中,不會這麼簡單的,一個類可能實現多個介面,內部類,註解等資訊的時候,解析起來肯定還要複雜,那麼我們這篇文章主要的目的是介紹一下dex的檔案格式,目的不是說去解決實際中專案的問題,所以後面在解析複雜的dex的時候,我們也只能遇到什麼問題就去解決一下。


第三、我們解析dex的目的是啥?

我們開始的時候,並沒有介紹說解析dex幹啥?那麼現在可以說,解析完dex之後我們有很多事都可以做了。

1、我們可以檢測一個apk中是否包含了指定系統的api(當然這些api沒有被混淆),同樣也可以檢測這個apk是否包含了廣告,以前我們可以通過解析AndroidManifest.xml檔案中的service,activity,receiver,meta等資訊來判斷,因為現在的廣告sdk都需要新增這些東西,如果我們可以解析dex的話,那麼我們可以得到他的所有字串內容,就是string_ids池,這樣就可以判斷呼叫了哪些api。那麼就可以判斷這個apk的一些行為了,當然這裡還有一個問題,假如dex加密了我們就蛋疼了。好吧,那就牽涉出第二件事了。

2、我們在之前說過如何對apk進行加固,其實就是加密apk/dex檔案內容,那麼這時候我們必須要了解dex的檔案結構資訊,因為我們先加密dex,然後在動態載入dex進行解密即可。

3、我們可以更好的逆向工作,其實說到這裡,我們看看apktool原始碼也知道,他內部的反編譯原理就是這些,只是他會將指令翻譯成smail程式碼,這個網上是有相對應的jar包api的,所以我們知道了dex的資料結構,那麼原理肯定就知道了,同樣還有一個dex2jar工具原理也是類似的。


六、總結

到這裡我們就介紹完了dex檔案格式的解析工作,至此我們也解析完了Android中編譯之後的所有檔案格式,我之所以介紹這幾篇文章,一來是更好的瞭解Android中生成apk的流程,其次是我們能更好的的解決反編譯過程中遇到的問題,我們需要去解決。這篇文章解析起來還是很費勁的,累死了,也是2016年第一篇文章,謝謝大家的支援~~。記得點贊呀~~

相關文章