Android逆向筆記 —— DEX 文件格式解析

DEX 文件結構思惟導圖及解析源碼見文末。java

往期目錄:android

Class 文件格式詳解c++

Smali 語法解析——Hello Worldgit

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

Smali 語法解析 —— 類算法

Android逆向筆記 —— AndroidManifest.xml 文件格式解析數組

系列第一篇文章就分析過 Class 文件格式,咱們都知道 .java 源文件通過編譯器編譯會生成 JVM 可識別的 .class 文件。在 Android 中,不論是 Dalvik 仍是 Art,和 JVM 的區別仍是很大的。Android 系統並不直接使用 Class 文件,而是將全部的 Class 文件聚合打包成 DEX 文件,DEX 文件相比單個單個的 Class 文件更加緊湊,能夠直接在 Android Runtime 下執行。bash

對於學習熱修復框架,加固和逆向相關知識,瞭解 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 文件格式的定義,dalvik/libdex/DexFile.h,其中詳細定義了 DEX 文件中的各個部分。

第三個是 010 Editor,在以前解析 AndroidManifest.xml 文件格式解析 也介紹過,它提供了豐富的文件模板,支持常見文件格式的解析,能夠很方便的查看文件結構中的各個部分及其對應的十六進制。通常我在代碼解析文件結構的時候都是對照着 010 Editor 來進行分析。下面貼一張 010 Editor 打開以前生成的 Hello.dex 文件的截圖:

咱們能夠一目瞭然的看到 DEX 的文件結構,着實是一個利器。在詳細解析以前,咱們先來大概給 DEX 文件分個層,以下圖所示:

文末我放了一張詳細的思惟導圖,也能夠對着思惟導圖來閱讀文章。

依次解釋一下:

  • header : DEX 文件頭,記錄了一些當前文件的信息以及其餘數據結構在文件中的偏移量
  • string_ids : 字符串的偏移量
  • type_ids : 類型信息的偏移量
  • proto_ids : 方法聲明的偏移量
  • field_ids : 字段信息的偏移量
  • method_ids : 方法信息(所在類,方法聲明以及方法名)的偏移量
  • class_def : 類信息的偏移量
  • data : : 數據區
  • link_data : 靜態連接數據區

headerdata 之間都是偏移量數組,並不存儲真實數據,全部數據都存在 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
複製代碼

字符串格式爲 dex\n035\0,十六進制爲 0x6465780A30333500

checksum 是對去除 magicchecksum 之外的文件部分做 alder32 算法獲得的校驗值,用於判斷 DEX 文件是否被篡改。

signature 是對除去 magicchecksumsignature 之外的文件部分做 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,對照進行分析:

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 79 02 00 00 08 00 00 00
62 00 01 00 62 01 00 00 6E 20 03 00 10 00 0E 00
複製代碼

使用的寄存器個數是 3 個。參數個數是 1 個,就是 main() 方法中的 String[] args。調用外部方法時使用的寄存器個數爲 2 個。指令個數是 8 。

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

第一個指令 62 00 01 00,查詢文檔 62 對應指令爲 sget-object vAA, field@BBBBAA 對應 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, field@BBBB, AA 對應 01BBBB 對應 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}, meth@BBBB6E 後面一個十六位 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
複製代碼

總結

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

Java 版本 DEX 文件格式解析源碼,點我 DexParser

文章首發微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解。

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

相關文章
相關標籤/搜索