天天看點

android dex檔案格式(一)_Android逆向筆記 —— DEX 檔案格式解析

DEX 檔案結構思維導圖及解析源碼見文末。

往期目錄:

Class 檔案格式詳解

Smali 文法解析——Hello World

Smali —— 數學運算,條件判斷,循環

Smali 文法解析 —— 類

Android逆向筆記 —— AndroidManifest.xml 檔案格式解析

系列第一篇文章就分析過 Class 檔案格式,我們都知道

.java

源檔案經過編譯器編譯會生成 JVM 可識别的

.class

檔案。在 Android 中,不管是 Dalvik 還是 Art,和 JVM 的差別還是很大的。Android 系統并不直接使用 Class 檔案,而是将所有的 Class 檔案聚合打包成

DEX

檔案,DEX 檔案相比單個單個的 Class 檔案更加緊湊,可以直接在 Android Runtime 下執行。

對于學習熱修複架構,加強和逆向相關知識,了解 DEX 檔案結構是很有必要的。再之前解析過 Class 檔案和 AndroidManifest.xml 檔案結構之後,發現看二進制檔案看上瘾了。。後面會繼續對 Apk 檔案中的其他檔案結構進行分析,例如 so 檔案,resources.arsc 檔案等。

DEX 檔案的生成

在解析 DEX 檔案結構之前,先來看看如何生成 DEX 檔案。為了友善解析,本篇文章中就不從市場上的 App 裡拿 DEX 檔案過來解析了,而是手動生成一個最簡單的 DEX 檔案。還是以 Class 檔案解析時候用的例子:

public class Hello {

    private static String HELLO_WORLD = "Hello World!";

    public static void main(String[] args) {
        System.out.println(HELLO_WORLD);
    }
}
           

首先

javac

編譯成

Hello.class

檔案,然後利用 Sdk 自帶的

dx

工具生成 DEX 檔案:

dx --dex --output=Hello.dex  Hello.class
           

dx 工具位于 Sdk 的 build-tools 目錄下,可添加至環境變量友善調用。dx 也支援多 Class 檔案生成 dex。

DEX 檔案結構

概覽

關于 DEX 檔案結構的學習,給大家推薦兩個資料。

第一個是看雪神圖,出自非蟲,

android dex檔案格式(一)_Android逆向筆記 —— DEX 檔案格式解析

第二個是 Android 源碼中對 DEX 檔案格式的定義,dalvik/libdex/DexFile.h,其中詳細定義了 DEX 檔案中的各個部分。

第三個是

010 Editor

,在之前解析 AndroidManifest.xml 檔案格式解析 也介紹過,它提供了豐富的檔案模闆,支援常見檔案格式的解析,可以很友善的檢視檔案結構中的各個部分及其對應的十六進制。一般我在代碼解析檔案結構的時候都是對照着 010 Editor 來進行分析。下面貼一張 010 Editor 打開之前生成的 Hello.dex 檔案的截圖:

android dex檔案格式(一)_Android逆向筆記 —— DEX 檔案格式解析

我們可以一目了然的看到 DEX 的檔案結構,着實是一個利器。在詳細解析之前,我們先來大概給 DEX 檔案分個層,如下圖所示:

android dex檔案格式(一)_Android逆向筆記 —— DEX 檔案格式解析
文末我放了一張詳細的思維導圖,也可以對着思維導圖來閱讀文章。

依次解釋一下:

  • header :

    DEX 檔案頭,記錄了一些目前檔案的資訊以及其他資料結構在檔案中的偏移量
  • string_ids :

    字元串的偏移量
  • type_ids :

    類型資訊的偏移量
  • proto_ids :

    方法聲明的偏移量
  • field_ids :

    字段資訊的偏移量
  • method_ids :

    方法資訊(所在類,方法聲明以及方法名)的偏移量
  • class_def :

    類資訊的偏移量
  • data :

    : 資料區
  • link_data :

    靜态連結資料區

header

data

之間都是偏移量數組,并不存儲真實資料,所有資料都存在

data

資料區,根據其偏移量區查找。對 DEX 檔案有了一個大概的認識之後,我們就來詳細分析一下各個部分。

header

DEX 檔案頭部分的具體格式可以參考 DexFile.h 中的定義:

struct DexHeader {
    u1  magic[8];           // 魔數
    u4  checksum;           // adler 校驗值
    u1  signature[kSHA1DigestLen]; // sha1 校驗值
    u4  fileSize;           // DEX 檔案大小
    u4  headerSize;         // DEX 檔案頭大小
    u4  endianTag;          // 位元組序
    u4  linkSize;           // 連結段大小
    u4  linkOff;            // 連結段的偏移量
    u4  mapOff;             // DexMapList 偏移量
    u4  stringIdsSize;      // DexStringId 個數
    u4  stringIdsOff;       // DexStringId 偏移量
    u4  typeIdsSize;        // DexTypeId 個數
    u4  typeIdsOff;         // DexTypeId 偏移量
    u4  protoIdsSize;       // DexProtoId 個數
    u4  protoIdsOff;        // DexProtoId 偏移量
    u4  fieldIdsSize;       // DexFieldId 個數
    u4  fieldIdsOff;        // DexFieldId 偏移量
    u4  methodIdsSize;      // DexMethodId 個數
    u4  methodIdsOff;       // DexMethodId 偏移量
    u4  classDefsSize;      // DexCLassDef 個數
    u4  classDefsOff;       // DexClassDef 偏移量
    u4  dataSize;           // 資料段大小
    u4  dataOff;            // 資料段偏移量
};
           

其中的

u

表示無符号數,

u1

就是 8 位無符号數,

u4

就是 32 位無符号數。

magic

一般是常量,用來标記 DEX 檔案,它可以分解為:

檔案辨別 dex + 換行符 + DEX 版本 + 0
           

字元串格式為

dexn0350

,十六進制為

0x6465780A30333500

checksum

是對去除

magic

checksum

以外的檔案部分作 alder32 算法得到的校驗值,用于判斷 DEX 檔案是否被篡改。

signature

是對除去

magic

checksum

signature

以外的檔案部分作 sha1 得到的檔案哈希值。

endianTag

用于标記 DEX 檔案是大端表示還是小端表示。由于 DEX 檔案是運作在 Android 系統中的,是以一般都是小端表示,這個值也是恒定值

0x12345678

其餘部分分别标記了 DEX 檔案中其他各個資料結構的個數和其在資料區的偏移量。根據偏移量我們就可以輕松的獲得各個資料結構的内容。下面順着上面的 DEX 檔案結構來認識第一個資料結構

string_ids

string_ids

struct DexStringId {
    u4 stringDataOff;
};
           

string_ids

是一個偏移量數組,

stringDataOff

表示每個字元串在 data 區的偏移量。根據偏移量在 data 區拿到的資料中,第一個位元組表示的是字元串長度,後面跟着的才是字元串資料。這塊邏輯比較簡單,直接看一下代碼:

private void parseDexString() {
    log("nparse DexString");
    try {
        int stringIdsSize = dex.getDexHeader().string_ids__size;
        for (int i = 0; i < stringIdsSize; i++) {
            int string_data_off = reader.readInt();
            byte size = dexData[string_data_off]; // 第一個位元組表示該字元串的長度,之後是字元串内容
            String string_data = new String(Utils.copy(dexData, string_data_off + 1, size));
            DexString string = new DexString(string_data_off, string_data);
            dexStrings.add(string);
            log("string[%d] data: %s", i, string.string_data);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
           

列印結果如下:

parse DexString
string[0] data: <clinit>
string[1] data: <init>
string[2] data: HELLO_WORLD
string[3] data: Hello World!
string[4] data: Hello.java
string[5] data: LHello;
string[6] data: Ljava/io/PrintStream;
string[7] data: Ljava/lang/Object;
string[8] data: Ljava/lang/String;
string[9] data: Ljava/lang/System;
string[10] data: V
string[11] data: VL
string[12] data: [Ljava/lang/String;
string[13] data: main
string[14] data: out
string[15] data: println
           

其中包含了變量名,方法名,檔案名等等,這個字元串池在後面其他結構的解析中也會經常遇到。

type_ids

struct DexTypeId {
    u4  descriptorIdx;
};
           

type_ids

表示的是類型資訊,

descriptorIdx

指向

string_ids

中元素。根據索引直接在上一步讀取到的字元串池即可解析對應的類型資訊,代碼如下:

private void parseDexType() {
    log("nparse DexTypeId");
    try {
        int typeIdsSize = dex.getDexHeader().type_ids__size;
        for (int i = 0; i < typeIdsSize; i++) {
            int descriptor_idx = reader.readInt();
            DexTypeId dexTypeId = new DexTypeId(descriptor_idx, dexStringIds.get(descriptor_idx).string_data);
            dexTypeIds.add(dexTypeId);
            log("type[%d] data: %s", i, dexTypeId.string_data);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
           

解析結果:

parse DexType
type[0] data: LHello;
type[1] data: Ljava/io/PrintStream;
type[2] data: Ljava/lang/Object;
type[3] data: Ljava/lang/String;
type[4] data: Ljava/lang/System;
type[5] data: V
type[6] data: [Ljava/lang/String;
           

proto_ids

struct DexProtoId {
    u4  shortyIdx;          /* index into stringIds for shorty descriptor */
    u4  returnTypeIdx;      /* index into typeIds list for return type */
    u4  parametersOff;      /* file offset to type_list for parameter types */
};
           

proto_ids

表示方法聲明資訊,它包含以下三個變量:

  • shortyIdx : 指向 string_ids ,表示方法聲明的字元串
  • returnTypeIdx : 指向 type_ids ,表示方法的傳回類型
  • parametersOff : 方法參數清單的偏移量

方法參數清單的資料結構在 DexFile.h 中用

DexTypeList

來表示:

struct DexTypeList {
    u4  size;               /* #of entries in list */
    DexTypeItem list[1];    /* entries */
};

struct DexTypeItem {
    u2  typeIdx;            /* index into typeIds */
};
           

size

表示方法參數的個數,參數用

DexTypeItem

表示,它隻有一個屬性

typeIdx

,指向

type_ids

中對應項。具體的解析代碼如下:

private void parseDexProto() {
    log("nparse DexProto");
    try {
        int protoIdsSize = dex.getDexHeader().proto_ids__size;
        for (int i = 0; i < protoIdsSize; i++) {
            int shorty_idx = reader.readInt();
            int return_type_idx = reader.readInt();
            int parameters_off = reader.readInt();

            DexProtoId dexProtoId = new DexProtoId(shorty_idx, return_type_idx, parameters_off);
            log("proto[%d]: %s %s %d", i, dexStringIds.get(shorty_idx).string_data,
                    dexTypeIds.get(return_type_idx).string_data, parameters_off);

            if (parameters_off > 0) {
                parseDexProtoParameters(parameters_off);
            }

            dexProtos.add(dexProtoId);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
           

解析結果:

parse DexProto
proto[0]: V V 0
proto[1]: VL V 412
parameters[0]: Ljava/lang/String;
proto[2]: VL V 420
parameters[0]: [Ljava/lang/String;
           

field_ids

struct DexFieldId {
    u2  classIdx;           /* index into typeIds list for defining class */
    u2  typeIdx;            /* index into typeIds for field type */
    u4  nameIdx;            /* index into stringIds for field name */
};
           

field_ids

表示的是字段資訊,指明了字段所在的類,字段的類型以及字段名稱,在

DexFile.h

中定義為

DexFieldId

, 其各個字段含義如下:

  • classIdx : 指向 type_ids ,表示字段所在類的資訊
  • typeIdx : 指向 ype_ids ,表示字段的類型資訊
  • nameIdx : 指向 string_ids ,表示字段名稱

代碼解析很簡單,就不貼出來了,直接看一下解析結果:

parse DexField
field[0]: LHello;->HELLO_WORLD;Ljava/lang/String;
field[1]: Ljava/lang/System;->out;Ljava/io/PrintStream;
           

method_ids

struct DexMethodId {
    u2  classIdx;           /* index into typeIds list for defining class */
    u2  protoIdx;           /* index into protoIds for method prototype */
    u4  nameIdx;            /* index into stringIds for method name */
};
           

method_ids

指明了方法所在的類、方法聲明以及方法名。在 DexFile.h 中用

DexMethodId

表示該項,其屬性含義如下:

  • classIdx : 指向 type_ids ,表示類的類型
  • protoIdx : 指向 type_ids ,表示方法聲明
  • nameIdx : 指向 string_ids ,表示方法名

解析結果:

parse DexMethod
method[0]: LHello; proto[0] <clinit>
method[1]: LHello; proto[0] <init>
method[2]: LHello; proto[2] main
method[3]: Ljava/io/PrintStream; proto[1] println
method[4]: Ljava/lang/Object; proto[0] <init>
           

class_def

struct DexClassDef {
    u4  classIdx;           /* index into typeIds for this class */
    u4  accessFlags;
    u4  superclassIdx;      /* index into typeIds for superclass */
    u4  interfacesOff;      /* file offset to DexTypeList */
    u4  sourceFileIdx;      /* index into stringIds for source file name */
    u4  annotationsOff;     /* file offset to annotations_directory_item */
    u4  classDataOff;       /* file offset to class_data_item */
    u4  staticValuesOff;    /* file offset to DexEncodedArray */
};
           

class_def

是 DEX 檔案結構中最複雜也是最核心的部分,它表示了類的所有資訊,對應

DexFile.h

中的

DexClassDef

:

  • classIdx : 指向 type_ids ,表示類資訊
  • accessFlags : 通路辨別符
  • superclassIdx : 指向 type_ids ,表示父類資訊
  • interfacesOff : 指向 DexTypeList 的偏移量,表示接口資訊
  • sourceFileIdx : 指向 string_ids ,表示源檔案名稱
  • annotationOff : 注解資訊
  • classDataOff : 指向 DexClassData 的偏移量,表示類的資料部分
  • staticValueOff :指向 DexEncodedArray 的偏移量,表示類的靜态資料

DefCLassData

重點是

classDataOff

這個字段,它包含了一個類的核心資料,在 Android 源碼中定義為

DexClassData

,它不在 DexFile.h 中了,而是在 DexClass.h 中:

struct DexClassData {
    DexClassDataHeader header;
    DexField*          staticFields;
    DexField*          instanceFields;
    DexMethod*         directMethods;
    DexMethod*         virtualMethods;
};
           

DexClassDataHeader

定義了類中字段和方法的數目,它也定義在 DexClass.h 中:

struct DexClassDataHeader {
    u4 staticFieldsSize;
    u4 instanceFieldsSize;
    u4 directMethodsSize;
    u4 virtualMethodsSize;
};
           
  • staticFieldsSize : 靜态字段個數
  • instanceFieldsSize : 執行個體字段個數
  • directMethodsSize : 直接方法個數
  • virtualMethodsSize : 虛方法個數

在讀取的時候要注意這裡的資料是 LEB128 類型。它是一種可變長度類型,每個 LEB128 由 1~5 個位元組組成,每個位元組隻有 7 個有效位。如果第一個位元組的最高位為 1,表示需要繼續使用第 2 個位元組,如果第二個位元組最高位為 1,表示需要繼續使用第三個位元組,依此類推,直到最後一個位元組的最高位為 0,至多 5 個位元組。除了 LEB128 以外,還有無符号類型 ULEB128。

那麼為什麼要使用這種資料結構呢?我們都知道 Java 中 int 類型都是 4 位元組,32 位的,但是很多時候根本用不到 4 個位元組,用這種可變長度的結構,可以節省空間。對于運作在 Android 系統上來說,能多省一點空間肯定是好的。下面給出了 Java 讀取 ULEB128 的代碼:

public static int readUnsignedLeb128(byte[] src, int offset) {
    int result = 0;
    int count = 0;
    int cur;
    do {
        cur = copy(src, offset, 1)[0];
        cur &= 0xff;
        result |= (cur & 0x7f) << count * 7;
        count++;
        offset++;
        DexParser.POSITION++;
    } while ((cur & 0x80) == 128 && count < 5);
    return result;
}
           

繼續回到 DexClassData 中來。

header

部分定義了各種字段和方法的個數,後面跟着的分别就是

靜态字段

執行個體字段

直接方法

虛方法

的具體資料了。字段用

DexField

表示,方法用

DexMethod

表示。

DexField

struct DexField {
    u4 fieldIdx;    /* index to a field_id_item */
    u4 accessFlags;
};
           
  • fieldIdx : 指向 field_ids ,表示字段資訊
  • accessFlags :通路辨別符

DexMethod

struct DexMethod {
    u4 methodIdx;    /* index to a method_id_item */
    u4 accessFlags;
    u4 codeOff;      /* file offset to a code_item */
46};
           

method_idx

是指向 method_ids 的索引,表示方法資訊。

accessFlags

是該方法的通路辨別符。

codeOff

是結構體

DexCode

的偏移量。如果你堅持看到了這裡,是不是發現說到現在還沒說到最重要的東西,DEX 包含的代碼,或者說指令,對應的就是 Hello.java 中的 main 方法。沒錯,

DexCode

就是用來存儲方法的詳細資訊以及其中的指令的。

struct DexCode {
    u2  registersSize;  // 寄存器個數
    u2  insSize;        // 參數的個數
    u2  outsSize;       // 調用其他方法時使用的寄存器個數
    u2  triesSize;      // try/catch 語句個數
    u4  debugInfoOff;   // debug 資訊的偏移量
    u4  insnsSize;      // 指令集的個數
    u2  insns[1];       // 指令集
    /* followed by optional u2 padding */  // 2 位元組,用于對齊
    /* followed by try_item[triesSize] */
    /* followed by uleb128 handlersSize */
    /* followed by catch_handler_item[handlersSize] */
};
           

我們打開 010 Editor,定位到 main() 方法對應的

DexCode

,對照進行分析:

android dex檔案格式(一)_Android逆向筆記 —— DEX 檔案格式解析
public class Hello {

    private static String HELLO_WORLD = "Hello World!";

    public static void main(String[] args) {
        System.out.println(HELLO_WORLD);
    }
}
           

main()

方法對應的 DexCode 十六進制表示為 :

03 00 01 00 02 00 00 00 00 00 79 02 00 00 08 00
62 00 01 00 62 01 00 00 6E 20 03 00 01 00 0E 00
           

使用的寄存器個數是 3 個。參數個數是 1 個,就是

main()

方法中的

String[] args

。調用外部方法時使用的寄存器個數為 2 個。指令個數是 8 。

終于說到指令了,main() 函數中有 8 條指令,就是上面十六進制中的第二行。嘗試來解析一下這段指令。Android 官網就有 Dalvik 指令的相關介紹,連結。

第一個指令

62 00 01 00

,查詢文檔

62

對應指令為

sget-object vAA, [email protected]

AA

對應

00

, 表示

v0

寄存器。

BBBB

對應

01 00

,表示

field_ids

中索引為 1 的字段,根據前面的解析結果該字段為

Ljava/lang/System;->out;Ljava/io/PrintStream

,整理一下,

62 00 01 00

表示的就是:

sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
           

接着是

62 01 00 00

。還是

sget-object vAA, [email protected]

,

AA

對應

01

BBBB

對應

0000

, 使用的是

v1

寄存器,field 位 field_ids 中索引為 0 的字段,即

LHello;->HELLO_WORLD;Ljava/lang/String

,該句完整指令為:

sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
           

接着是

6E 20 03 00

, 檢視文檔

6E

指令為

invoke-virtual {vC, vD, vE, vF, vG}, [email protected]

6E

後面一個十六位

2

表示調用方法是兩個參數,那麼

BBBB

就是

03 00

,指向 method_ids 中索引為 3 方法。根據前面的解析結果,該方法就是

Ljava/io/PrintStream;->println(Ljava/lang/String;)V

。完整指令為:

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

最後的

0E

,檢視文檔該指令為

return-void

,到這 main() 方法就結束了。

将上面幾句指令放在一起:

62 00 01 00 : sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
62 01 00 00 : sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
6E 20 03 00 : invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
OE OO : return-void
           

這就是 main() 方法的完整指令了。還記得我之前的一篇文章 Smali 文法解析——Hello World,其實這個解析結果和 Hello.java 對應的 smali 代碼是一緻的:

.method public static main([Ljava/lang/String;)V
    .registers 3

    .prologue
    .line 6
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

    sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;

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

    .line 7
    return-void
.end method
           

總結

這種文章真的是又臭又長,但是耐下心去看,還是會有很大收貨的。最後來一張思維導圖總結一下:

android dex檔案格式(一)_Android逆向筆記 —— DEX 檔案格式解析

Java 版本 DEX 檔案格式解析源碼,點我 DexParser

文章首發微信公衆号:

秉心說

, 專注 Java 、 Android 原創知識分享,LeetCode 題解。

更多 JDK 源碼解析,掃碼關注我吧!

android dex檔案格式(一)_Android逆向筆記 —— DEX 檔案格式解析

繼續閱讀