Java源檔案通過Java編譯器生成CLASS檔案,再通過dx工具轉換為classes.dex檔案。
DEX檔案從整體上來看是一個索引的結構,類名、方法名、字段名等資訊都存儲在常量池中,這樣能夠充分減少存儲空間,相較于Java位元組碼檔案更适合手機裝置。
DEX檔案的相關結構聲明定義在/dalvik/libdex/DexFile.h檔案中,下面我們先來看一下DEX檔案中使用到的資料結構。
表1 dex檔案使用到的資料結構
類型 | 含義 |
---|---|
u1 | 等同于uint8_t,表示1位元組的無符号數 |
u2 | 等同于uint16_t,表示2位元組的無符号數 |
u4 | 等同于uint32_t,表示4位元組的無符号數 |
u8 | 等同于uint64_t,表示8位元組的無符号數 |
sleb128 | 有符号LEB128,可變長度1~5位元組 |
uleb128 | 無符号LEB128,可變長度1~5位元組 |
uleb128p1 | 無符号LEB128加1,可變長度1~5位元組 |
DEX檔案的基本結構如下圖所示:
header |
---|
string_ids |
type_ids |
proto_ids |
field_ids |
method_ids |
class_def |
data |
link_data |
圖1 DEX檔案結構
1. header(基本資訊)
header是DEX檔案頭,包含magic字段、alder32校驗值、SHA-1哈希值、string_ids的個數以及偏移位址等。DEX檔案的頭結構很固定,占用0x70個位元組,具體定義代碼如下所示(摘自DexFile.h):
/*
* Direct-mapped "header_item" struct.
*/
struct DexHeader {
u1 magic[]; /* includes version number */
u4 checksum; /* adler32 checksum */
u1 signature[kSHA1DigestLen]; /* SHA- hash */
u4 fileSize; /* length of entire file */
u4 headerSize; /* offset to start of next section */
u4 endianTag;
u4 linkSize;
u4 linkOff;
u4 mapOff;
u4 stringIdsSize;
u4 stringIdsOff;
u4 typeIdsSize;
u4 typeIdsOff;
u4 protoIdsSize;
u4 protoIdsOff;
u4 fieldIdsSize;
u4 fieldIdsOff;
u4 methodIdsSize;
u4 methodIdsOff;
u4 classDefsSize;
u4 classDefsOff;
u4 dataSize;
u4 dataOff;
};
magic[8]:共8個位元組。目前為固定值dex\n035。
checksum:檔案校驗碼,使用alder32算法校驗檔案除去magic、checksum外餘下的所有檔案區域,用于檢查檔案錯誤。
signature:使用 SHA-1算法hash除去magic,checksum和signature外餘下的所有檔案區域 ,用于唯一識别本檔案 。
fileSize:DEX檔案的長度。
headerSize:header大小,一般固定為0x70位元組。
endianTag:指定了DEX運作環境的cpu位元組序,預設值ENDIAN_CONSTANT等于0x12345678,表示預設采用Little-Endian位元組序。
linkSize和linkOff:指定連結段的大小與檔案偏移,大多數情況下它們的值都為0。link_size:LinkSection大小,如果為0則表示該DEX檔案不是靜态連結。link_off用來表示LinkSection距離DEX頭的偏移位址,如果LinkSize為0,此值也會為0。
mapOff:DexMapList結構的檔案偏移。
stringIdsSize和stringIdsOff:DexStringId結構的資料段大小與檔案偏移。
typeIdsSize和typeIdsOff:DexTypeId結構的資料段大小與檔案偏移。
protoIdsSize和protoIdsSize:DexProtoId結構的資料段大小與檔案偏移。
fieldIdsSize和fieldIdsSize:DexFieldId結構的資料段大小與檔案偏移。
methodIdsSize和methodIdsSize:DexMethodId結構的資料段大小與檔案偏移。
classDefsSize和classDefsOff:DexClassDef結構的資料段大小與檔案偏移。
dataSize和dataOff:資料段的大小與檔案偏移。
下面我們來看某apk中classes.dex的解析結果,确實與上面的結構一緻:
2.DexMapList區段(大綱)
Dalvik虛拟機解析DEX檔案的内容,最終将其映射成DexMapList資料結構,它實際上包含所有其他區段的結構大綱。DexHeader中的mapOff字段指明了DexMapList結構在DEX檔案中的偏移。具體定義代碼如下所示:
struct DexMapList {
u4 size; /* DexMapItem的個數 */
DexMapItem list[]; /* DexMapItem的結構 */
};
struct DexMapItem {
u2 type; /* kDexType開頭的類型 */
u2 unused; /* 未使用,用于位元組對齊 */
u4 size; /* type指定類型的個數,它們在dex檔案中連續存放 */
u4 offset; /* 指定類型資料的檔案偏移 */
};
/* type字段為一個枚舉常量,通過類型名稱很容易判斷它的具體類型。 */
/* map item type codes */
enum {
kDexTypeHeaderItem = ,
kDexTypeStringIdItem = ,
kDexTypeTypeIdItem = ,
kDexTypeProtoIdItem = ,
kDexTypeFieldIdItem = ,
kDexTypeMethodIdItem = ,
kDexTypeClassDefItem = ,
kDexTypeMapList = ,
kDexTypeTypeList = ,
kDexTypeAnnotationSetRefList = ,
kDexTypeAnnotationSetItem = ,
kDexTypeClassDataItem = ,
kDexTypeCodeItem = ,
kDexTypeStringDataItem = ,
kDexTypeDebugInfoItem = ,
kDexTypeAnnotationItem = ,
kDexTypeEncodedArrayItem = ,
kDexTypeAnnotationsDirectoryItem = ,
};
下面我們來看一下010Editor對某classes.dex檔案的解析出的DexMapList結構。上面DexMapList結構中的size字段表示list數組的成員個數,即DexMapItem結構的數量:圖中是11h,表示共有17個DexMapItem結構,與圖中的list數組大小相符。
然後我們再來看下DexMapItem的結構。例如對于下圖中的DexMapItem的第一項來說,type等于0說明其是kDexTypeHeaderItem類型的結構;unused一般都為0;size為1代表該結構僅有一個,即隻有一個Dex檔案頭;offset為0代表Dex檔案頭從0h開始。
最後我們将所有DexMapItem結構整理成下表:
類型(type) | 個數(size) | 偏移(offset) |
---|---|---|
kDexTypeHeaderItem(0x0000) | 0x1 | 0x0 |
kDexTypeStringIdItem(0x0001) | 0xA115 | 0x70 |
kDexTypeTypeIdItem(0x0002) | 0x1D38 | 0x284C4 |
kDexTypeProtoIdItem(0x0003) | 0x2505 | 0x2F9A4 |
kDexTypeFieldIdItem(0x0004) | 0x9FB9 | 0x4B5E0 |
kDexTypeMethodIdItem(0x0005) | 0xC344 | 0x9B3A8 |
kDexTypeClassDefItem(0x0006) | 0x189D | 0xFCDC8 |
kDexTypeAnnotationSetItem(0x1003) | 0x10E0 | 0x12E168 |
kDexTypeCodeItem(0x2001) | 0x96DB | 0x138A34 |
kDexTypeAnnotationsDirectoryItem(0x2006) | 0xCE6 | 0x4A3EEC |
kDexTypeTypeList(0x1001) | 0x1620 | 0x4B9894 |
kDexTypeStringDataItem(0x2002) | 0xA115 | 0x4C74CA |
kDexTypeDebugInfoItem(0x2003) | 0x8FCC | 0x5C8544 |
kDexTypeAnnotationItem(0x2004) | 0x101C | 0x63FBC1 |
kDexTypeEncodedArrayItem(0x2005) | 0x10E | 0x653536 |
kDexTypeClassDataItem(0x2000) | 0x184F | 0x65B97A |
kDexTypeMapList(0x1000) | 0x1 | 0x6B8828 |
可以看出,其中區段的offset與header中的off是完全相等的。
3.DexStringId區段(字元串)
struct DexStringId {
u4 stringDataOff; /* 字元串資料偏移 */
}
DexStringId結構隻有一個stringDataOff字段,直接指向字元串資料。這個區段中包含了DEX檔案中用到的所有字元串。
4.DexTypeId區段(類名/類型名稱字元串)
struct DexTypeId {
u4 descriptorIdx; /* 指向 DexStringId清單的索引 */
};
descriptorIdx為指向DexStringId清單的索引,它對應的字元串代表了具體類的類型(DEX檔案中用到的所有基本資料類型和類的名稱)。如下圖中的第一項值為0xAEB,表示其是DexStringId中第0xAEB(2795)項;而第8項值為0x1969,表示其是DexStringId中第0x1969(6505)項。經過我們的驗證,以上分析是正确的。
5.DexProtoId區段(方法聲明=傳回類型 + 參數清單)
struct DexProtoId {
u4 shortyIdx; /* 指向DexStringId清單的索引 */
u4 returnTypeIdx; /* 指向DexTypeId清單的索引 */
u4 parametersOff; /* 指向DexTypeList的偏移 */
}
struct DexTypeList {
u4 size; /* 接下來DexTypeItem的個數 */
DexTypeItem list[]; /* DexTypeItem結構 */
};
struct DexTypeItem {
u2 typeIdx; /* 指向DexTypeId清單的索引 */
};
下面結合執行個體進行分析:
- DexProtoId
- shortyIdx:方法聲明字元串,具體而言是由方法的傳回類型與參數清單組成的一個字元串,并且傳回類型位于參數清單的前面。如“III”“V”“VI”“VL”等。在下圖的三個方法聲明中分别為B、BL、DL。
- returnTypeIdx:方法傳回類型,指向DexTypeId清單。下圖的分别為byte、byte、double。
- parametersOff:指向一個DexTypeList結構體,存放了方法的參數類型。下圖分别為0、0x4BA78C、0x4BA7BC。值為0表示參數為void。
- DexTypeList
- size:DexTypeItem的個數,即參數的數量。下圖分别為?、1、1,表示後兩個方法都隻有一個參數。
- list:指向size個DexTypeItem項,每一項代表方法的一個參數。
- DexTypeItem
- typeIdx:指向DexTypeId清單,最終指向參數類型的字元串。如第三圖61h(97)項在DexTypeId清單中正好指向”Landroid/content/Context;”類型字元串。
6.DexFieldId區段(字段)
DexFieldId結構中的資料全部是索引值,指明了字段所在的類、字段的類型以及字段名。
struct DexFieldId {
u2 classIdx; /* 類的類型,指向DexTypeId清單的索引 */
u2 typeIdx; /* 字段類型,指向DexTypeId清單的索引 */
u4 nameIdx; /* 字段名,指向DexStringId清單的索引 */
};
如下圖,可以看到字段所屬類名為MTT.ThirdAppInfoNew,字段類型為int,字段名為iCoreType。
7.DexMethodId區段(方法)
DexMethodId結構中的資料全部是索引值,指明了方法所在的類、方法的聲明以及方法名。
struct DexMethodId {
u2 classIdx; /* 類的類型,指向DexTypeId清單的索引 */
u2 protoIdx; /* 聲明類型,指向DexProtoId清單的索引 */
u4 nameIdx; /* 方法名,指向DexStringId清單的索引 */
};
如下圖,可以看到方法所屬類為MTT.ThirdAppInfoNew,方法聲明為V,方法名為。
8.DexTypeClassDefItem(類定義)
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結構的偏移 */
};
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結構的偏移 */
};
struct DexCode {
u2 registersSize; /* 使用的寄存器個數 */
u2 insSize; /* 參數個數 */
u2 outsSize; /* 調用其他方法時使用的寄存器個數 */
u2 triesSize; /* Try/Catch個數 */
u4 debugInfoOff; /* 指向調試資訊的偏移 */
u4 insnsSize; /*指令集個數,以2位元組為機關 */
u2 insns[]; /* 指令集 */
- DexClassDef
- classIdx:索引值,表明類的類型。下圖中值為0x6,指向類型MTT.ThirdAppInfoNew。
- accessFlags:類的通路标志,它是以ACC_開頭的一個枚舉值。具體定義可以參考這裡。下圖中值為0x11,表示同時具有ACC_PUBLIC和ACC_FINAL。
- superclassIdx:父類類型索引值。下圖中值為0x1B13,指向java.lang.Object類。
- interfacesOff:如果類中含有接口聲明或實作,interfaceOff會指向一個DexTypeList結構,否則這裡的值為0。圖中值為0x4B9894(4954260),指向的DexTypeList結構為java.lang.Cloneable。
- sourceFileIdx:字元串索引值,表示類所在的源檔案名稱。圖中值為NO_INDEX(0xffffffff),表示該值丢失。
- annotationsOff:指向注解目錄結構,根據類型不同會有注解類、注解方法、注解字段與注解參數,如果類中沒有注解,這裡的值則為0。圖中值為0,表示類中沒有注解。
- classDataOff:指向DexClassData結構,它是類的資料部分。圖中為0x65B97A。
- staticValuesOff:指向DexEncodedArray結構,記錄了類中的靜态資料。圖中為0,表示類中沒有靜态資料。
- DexClassData
- header:一個DexClassDataHeader結構,指定字段與方法的個數。如下圖中的staticFieldsSize為0,表示沒有靜态字段;instanceFieldsSize為0xC,表示有12個執行個體字段;directMethodsSize為0x1,表示有一個直接方法;virtualMethodsSize為0x0,表示沒有虛方法。
- staticFields*:指向一個DexField結構,表示靜态字段的類型與通路标志。由于本例沒有靜态字段,是以該結構無效。
- directMethods*:指向一個DexField結構,表示執行個體字段的類型與通路标志。如下圖本例中有一個執行個體字段。
- directMethods*:指向一個DexMethod結構,表示直接方法的原型、名稱、通路标志、代碼資料塊。。如下圖本例中有12個執行個體字段。
- virtualMethods*:指向一個DexMethod結構,表示虛方法的原型、名稱、通路标志、代碼資料塊。由于本例沒有靜态字段,是以該結構無效。
- DexField
- fieldIdx:指向DexFieldId的索引,表示字段的所屬類、字段類型和字段名。
- accessFlags:通路标志。
- DexMethod
- methodIdx:指向DexMethodId的索引,表示方法的所在類、方法的聲明和方法名。
- accessFlags:通路标志。
- codeOff:指向DexCode結構的偏移,圖中為0x138A34。
- DexCode
- registersSize:該方法使用的寄存器個數。下圖中為3。
- insSize:該方法的參數個數,對應smali文法中的”.register”指令。下圖中為1。
- outsSize:該方法調用其他方法時,對應smali文法中的”.paramter”指令。例如現在有一個方法,使用了5個寄存器,其中有2個為參數,而該方法調用了另一個方法,後者使用了20個寄存器,那麼Dalvik虛拟機在配置設定時,會在配置設定自身方法寄存器空間時加上那20個寄存器空間。下圖中為1。
- triesSize:方法中Try/Catch的個數。
- debugInfoOff:如果dex檔案保留了調試資訊,debugInfoOff字段會指向它。
- insnsSize:指令個數,以2位元組為機關。
- insns[1]:真正的指令部分。
參考文檔
- 《Android軟體安全與逆向分析》 非蟲
- 《.dex — Dalvik Executable Format》 AOSP