Android逆向(一) —— AndroidManifest.xml 二進位制解析

秉心說發表於2018-12-25

Androidni逆向 —— AndroidManifest.xml 解析

做過 Android 開發的同學對 AndroidManifest.xml 檔案肯定很熟悉,我們也叫它 清單檔案 ,之所以稱之為清單檔案,因為它的確是應用的 “清單”。它包含了應用的包名,版本號,許可權資訊,所有的四大元件等資訊。在逆向的過程中,通過 apk 的清單檔案,我們可以瞭解應用的一些基本資訊,程式的入口 Activity,註冊的服務,廣播,內容提供者等等。如果你嘗試檢視過 apk 中的 AndroidManifest.xml 檔案,你會發現你看到的是一堆亂碼,已經不是我們開發過程中編寫的清單檔案了。因為在打包過程中,清單檔案被編譯成了二進位制資料儲存在安裝包中。這就需要我們瞭解 AndroidManifest.xml 的二進位制檔案結構,才可以讀取到我們需要的資訊。當然,已經有一些不錯的開源工具可以讀取編譯後的清單檔案,像 AXmlPrinter , apktool 等等。當然,正是由於這些工具都是開源的,一些開發者會利用其中的漏洞對清單檔案進行特定的處理,使得無法通過這些工具反編譯清單檔案。如果我們瞭解其二進位制檔案結構的話,就可以對症下藥了。

和之前解析 Class 檔案結構一樣,仍然手寫程式碼進行解析,這樣才能真正的瞭解其檔案結構。通過前輩們的資料和 010 editor 的使用,其實已經大大降低了解析的難度。首先上一張看雪大神 MindMac 的神圖(原圖連結):

xml_binary.png

這張圖真的很經典,不妨可以列印出來對照著進行分析。

這篇文章以 QQ 的清單檔案為例進行分析,下載 QQ 的安裝包解壓即可拿到清單檔案。解析檔案格式的慣例,首先用 010 editor 開啟,基本結構如下圖所示:

xml_all.png

執行的 Template 是 AndroidManifest.bt。結合上面的結構圖,對 AndroidManifest.xml 的總體結構應該有了大概的瞭解。總體上按順序分為四大部分:

  • Header : 包括檔案魔數和檔案大小
  • String Chunk : 字串資源池
  • ResourceId Chunk : 系統資源 id 資訊
  • XmlContent Chunk : 清單檔案中的具體資訊,其中包含了五個部分,Start Namespace ChunkEnd Namespace ChunkStart Tag ChunkEnd Tag ChunkText Chunk

二進位制 AndroidManifest.xml 大致上就是按照這幾部分順序排列組成的,下面就逐一部分詳細解析。在這之前還需要知道的一點是,清單檔案是小端表示的,ARM 平臺下大多數都是小端表示的。

Header

xml_header.png

頭部由 Magic NumberFile Size 組成,各自都是 4 位元組。

  • Magic Number 始終為 0x0008003
  • File Size 表示檔案總位元組數,

對應的解析程式碼:

private void parseHeader() {
    try {
        Xml.nameSpaceMap.clear();
        String magicNumber = reader.readHexString(4);
        log("magic number: %s", magicNumber);

        int fileSize = reader.readInt();
        log("file size: %d", fileSize);
    } catch (IOException e) {
        e.printStackTrace();
        log("parse header error!");
    }
}
複製程式碼

解析結果:

magic number: 0x00080003
file size: 273444
複製程式碼

String Chunk

先來看一下 010 editor 中這一塊的內容:

xml_string_chunk.png

對應看雪神圖的 StringChunk 模組:

kanxue_string_chunk.png

String Chunk 主要儲存了清單檔案中的所有字串資訊。結構還是很清晰的。結合上圖逐條解釋一下:

  • Chunk Type : 4 bytes,始終為 0x001c0001,標記這是 String Chunk
  • Chunk Size : 4 bytes,表示 String Chunk 的大小
  • String Count : 4 bytes,表示字串的數量
  • Style Count : 4 bytes,表示樣式的數量
  • Unkown : 4 bytes,固定值,0x00000000
  • String Pool Offset : 字串池的偏移量,注意不是相對於檔案開始處,而是相對於 String Chunk 的開始處
  • Style Pool Offset : 樣式池的偏移量,同上,也是相對於 String Chunk 而言
  • String Offsets : int陣列,大小為 String Count,儲存每個字串在字串池中的相對偏移量
  • Style Offets : 同上,也是 int 陣列。總大小為 Style Count * 4 bytes
  • String Pool : 字串池,儲存了所有的字串
  • Style Pool : 樣式池,儲存了所有的樣式

字串池中的字串儲存也有特定的格式,以 versionName 為例:

xml_versioname.png

前兩個位元組表示字串的字元數,注意一個字元是兩個位元組。如上圖所示,字元數為 11 ,則後面 22 個位元組表示字串內容,最後以 0000 結尾。如此迴圈。

樣式池在解析過程中一般都為空,樣式數量也為 0。

瞭解了 String Chunk 的結構之後,解析就很簡單了。直接上程式碼:

private void parseStringChunk() {
        try {
            String chunkType = reader.readHexString(4);
            log("chunk type: %s", chunkType);

            int chunkSize = reader.readInt();
            log("chunk size: %d", chunkSize);

            int stringCount = reader.readInt();
            log("string count: %d", stringCount);

            int styleCount = reader.readInt();
            log("style count: %d", styleCount);

            reader.skip(4);  // unknown

            int stringPoolOffset = reader.readInt();
            log("string pool offset: %d", stringPoolOffset);

            int stylePoolOffset = reader.readInt();
            log("style pool offset: %d", stylePoolOffset);

            // 每個 string 的偏移量
            List<Integer> stringPoolOffsets = new ArrayList<>(stringCount);
            for (int i = 0; i < stringCount; i++) {
                stringPoolOffsets.add(reader.readInt());
            }

            // 每個 style 的偏移量
            List<Integer> stylePoolOffsets = new ArrayList<>(styleCount);
            for (int i = 0; i < styleCount; i++) {
                stylePoolOffsets.add(reader.readInt());
            }

            log("string pool:");
            for (int i = 1; i <= stringCount; i++) { // 沒有讀最後一個字串
                String string;
                if (i == stringCount) {
                    int lastStringLength = reader.readShort() * 2;
                    string = new String(moveBlank(reader.readOrigin(lastStringLength)));
                    reader.skip(2);
                } else {
                    reader.skip(2); // 字元長度
                    // 根據偏移量讀取字串
                    byte[] content = reader.readOrigin(stringPoolOffsets.get(i) - stringPoolOffsets.get(i - 1) - 4);
                    reader.skip(2); // 跳過結尾的 0000
                    string = new String(moveBlank(content));

                }
                log("   %s", string);
                stringChunkList.add(string);
            }


            log("style pool:");
            for (int i = 1; i < styleCount; i++) {
                reader.skip(2);
                byte[] content = reader.readOrigin(stylePoolOffsets.get(i) - stylePoolOffsets.get(i - 1) - 4);
                reader.skip(2);
                String string = new String(content);
                log("   %s", string);
            }

        } catch (IOException e) {
            e.printStackTrace();
            log("parse StringChunk error!");
        }
    }
複製程式碼

解析結果如下:

chunk type: 0x001C0001
chunk size: 101216
string count: 1163
style count: 0
string pool offset: 4680
style pool offset: 0
string pool:
   installLocation
   versionName
   versionCode
   minSdkVersion
   targetSdkVersion
   largeScreens
   normalScreens
   smallScreens
   anyDensity
   name
   glEsVersion
   required
   protectionLevel
   permissionGroup
   ...
   ...
   ...
複製程式碼

ResourceId Chunk

資源 Id 塊,儲存了清單檔案中用到的系統屬性的資源 Id 值。還是先看一下 010 edtior 中的對應塊:

xml_resourceid_chunk.png

對應到看雪神圖中:

kanxue_resourceid_chunk.png

  • Chunk Type : 4 位元組,固定值,0x00080180,標識 ResourceId Chunk
  • Chunk Size : 4 位元組,標識此 Chunk 的位元組數
  • ResourceIds : int 陣列,大小為 (chunkSize - 8) / 4

解析程式碼:

private void parseResourceIdChunk() {
        try {
            String chunkType = reader.readHexString(4);
            log("chunk type: %s", chunkType);

            int chunkSize = reader.readInt();
            log("chunk size: %d", chunkSize);

            int resourcesIdChunkCount = (chunkSize - 8) / 4;
            for (int i = 0; i < resourcesIdChunkCount; i++) {
                String resourcesId = reader.readHexString(4);
                log("resource id[%d]: %s", i, resourcesId);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
複製程式碼

解析結果:

chunk type: 0x00080180
chunk size: 192
resource id[0]: 0x010102B7
resource id[1]: 0x0101021C
resource id[2]: 0x0101021B
resource id[3]: 0x0101020C
resource id[4]: 0x01010270
resource id[5]: 0x01010286
resource id[6]: 0x01010285
resource id[7]: 0x01010284
resource id[8]: 0x0101026C
resource id[9]: 0x01010003
resource id[10]: 0x01010281
resource id[11]: 0x0101028E
resource id[12]: 0x01010009
複製程式碼

XmlContent Chunk

這一塊程式碼中儲存了清單檔案的詳細資訊。其中包含了五種 Chunk 型別,從下面的解析程式碼中就可以看出來:

private void parseXmlContentChunk() {
        try {
            while (reader.avaliable() > 0) {
                int chunkType = reader.readInt();
                switch (chunkType) {
                    case Xml.START_NAMESPACE_CHUNK_TYPE:
                        parseStartNamespaceChunk();
                        break;
                    case Xml.START_TAG_CHUNK_TYPE:
                        parseStartTagChunk();
                        break;
                    case Xml.END_TAG_CHUNK_TYPE:
                        parseEndTagChunk();
                        break;
                    case Xml.END_NAMESPACE_CHUNK_TYPE:
                        parseEndNamespaceChunk();
                        break;
                    case Xml.TEXT_CHUNK_TYPE:
                        parseTextChunk();
                        break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            log("parse XmlContentChunk error!");
        }
    }
複製程式碼

通過 chunkType 來迴圈讀取不同型別的 chunk 並進行解析。每一種 chunk 都具有類似的資料結構,我定義了一個抽象類 Chunk 作為不同 chunk 的基類:

public abstract class Chunk {

    int chunkType; // 標識不同 chunk 型別
    int chunkSize; // 該 chunk 位元組數
    int lineNumber; // 行號

    Chunk(int chunkType){
        this.chunkType=chunkType;
    }

    public abstract String toXmlString();
}
複製程式碼

這三個屬性再加上 Unkown(0xFFFFFFFF),這前 16 個位元組是這五種 chunk 中都有的,後面不再特別敘述。

下面依次解析這五種 Chunk :

Start Namespace Chunk

Start Namespace Chunk 一般儲存了清單檔案的名稱空間資訊。再回顧一下 Start Namespace Chunk 的結構:

kanxue_start_namespace.png

對應 010 editor 中內容:

xml_start_namespace.png

前面四項不再解釋,我們著重看一下最後兩項 PrefixUriPrefix 是一個索引值,4 位元組,指向字串池中對應的字串,表示名稱空間的字首。Uri 同樣也是指向字串池中對應索引的字串,表示名稱空間的 uri。看上圖 010 editor 截圖中的例子,Prefix 值為 46Uri 值為 47。檢視前面解析過的字串池,發現這兩個字串分別是 androidhttp://schemas.android.com/apk/res/android。看到這裡應該很熟悉了,這的確是我們的 AndroidManifest.xml 檔案的名稱空間。

解析程式碼:

private void parseStartNamespaceChunk() {
        log("\nparse Start NameSpace Chunk");
        log("chunk type: 0x%x", Xml.START_NAMESPACE_CHUNK_TYPE);

        try {
            int chunkSize = reader.readInt();
            log("chunk size: %d", chunkSize);

            int lineNumber = reader.readInt();
            log("line number: %d", lineNumber);

            reader.skip(4); // 0xffffffff

            int prefix = reader.readInt();
            log("prefix: %s", stringChunkList.get(prefix));

            int uri = reader.readInt();
            log("uri: %s", stringChunkList.get(uri));

            StartNameSpaceChunk startNameSpaceChunk = new StartNameSpaceChunk(chunkSize, lineNumber, prefix, uri);
            chunkList.add(startNameSpaceChunk);

            Xml.nameSpaceMap.put(stringChunkList.get(prefix), stringChunkList.get(uri));
        } catch (IOException e) {
            e.printStackTrace();
            log("parse Start NameSpace Chunk error!");
        }
    }
複製程式碼

解析程式碼很簡單,按順序讀取就可以了。需要注意的是我們把名稱空間的字尾和 uri 的對應關係儲存在了 map 中,供後面解析的時候使用。

End Namespace Chunk

此 chunk 與 Start Namespace Chunk 結構完全一致,解析過程也完全一致,不再贅述。

Start Tag Chunk

Start Tag Chunk 是所有 chunk 中結構最複雜的一個,儲存了清單檔案中最重要的標籤資訊。通過這一個 chunk,基本上就可以獲取 AndroidManifest.xml 的所有資訊了。

還是先回顧一下看雪神圖:

kanxue_start_tag.png

對應 010 editor 中的解析結果:

xml_start_tag.png

  • Namespace uri :這個標籤用到的名稱空間 uri 在字串池中的索引。值為 -1 表示沒有用到名稱空間 uri。標籤的一般都沒有使用到名稱空間,此值為 -1

  • Name : 標籤名稱在字串池中的索引

  • Flags : 固定值,0x00140014,暫未發現有何作用

  • Attribute Count : 4 bytes,表示標籤包含的屬性個數

  • Class Attribute : 4 bytes,表示標籤包含的類屬性個數。解析過程中此項常為 0

  • Attributes : 屬性集合,大小為 Attribute Count

標籤中包含了屬性集合,這就是清單檔案的重要組成部分。

屬性也有固定的格式:

xml_attribute.png

每個屬性固定 20 個位元組,包含 5 個欄位,每個欄位都是 4 位元組無符號 int,各個欄位含義如下:

  • NamespaceUri : 屬性的名稱空間 uri 在字串池中的索引。此處很少會等於 -1

  • name : 屬性名稱在字串池中的索引

  • valueStr : 屬性值

  • type : 屬性型別

  • data : 屬性資料

屬性根據 type 的不同,其屬性值的表達形式也是不一樣的。比如表示許可權的 android:name="android.permission.NFC",指向資源id 的 android:theme="@2131624762",表示大小的 android:value="632.0dip" 等等。Android 原始碼中就提供了根據 typedata 獲取屬性值字串的方法,這個方法就是 TypedValue.coerceToString(int type, int data),程式碼如下:

/**
     * Perform type conversion as per {@link #coerceToString()} on an explicitly
     * supplied type and data.
     *
     * @param type
     *            The data type identifier.
     * @param data
     *            The data value.
     *
     * @return String The coerced string value. If the value is null or the type
     *         is not known, null is returned.
     */
    public static final String coerceToString(int type, int data) {
        switch (type) {
            case TYPE_NULL:
                return null;
            case TYPE_REFERENCE:
                return "@" + data;
            case TYPE_ATTRIBUTE:
                return "?" + data;
            case TYPE_FLOAT:
                return Float.toString(Float.intBitsToFloat(data));
            case TYPE_DIMENSION:
                return Float.toString(complexToFloat(data))
                        + DIMENSION_UNIT_STRS[(data >> COMPLEX_UNIT_SHIFT)
                        & COMPLEX_UNIT_MASK];
            case TYPE_FRACTION:
                return Float.toString(complexToFloat(data) * 100)
                        + FRACTION_UNIT_STRS[(data >> COMPLEX_UNIT_SHIFT)
                        & COMPLEX_UNIT_MASK];
            case TYPE_INT_HEX:
                return String.format("0x%08X", data);
            case TYPE_INT_BOOLEAN:
                return data != 0 ? "true" : "false";
        }

        if (type >= TYPE_FIRST_COLOR_INT && type <= TYPE_LAST_COLOR_INT) {
            String res = String.format("%08x", data);
            char[] vals = res.toCharArray();
            switch (type) {
                default:
                case TYPE_INT_COLOR_ARGB8:// #AaRrGgBb
                    break;
                case TYPE_INT_COLOR_RGB8:// #FFRrGgBb->#RrGgBb
                    res = res.substring(2);
                    break;
                case TYPE_INT_COLOR_ARGB4:// #AARRGGBB->#ARGB
                    res = new StringBuffer().append(vals[0]).append(vals[2])
                            .append(vals[4]).append(vals[6]).toString();
                    break;
                case TYPE_INT_COLOR_RGB4:// #FFRRGGBB->#RGB
                    res = new StringBuffer().append(vals[2]).append(vals[4])
                            .append(vals[6]).toString();
                    break;
            }
            return "#" + res;
        } else if (type >= TYPE_FIRST_INT && type <= TYPE_LAST_INT) {
            String res;
            switch (type) {
                default:
                case TYPE_INT_DEC:
                    res = Integer.toString(data);
                    break;
            }
            return res;
        }

        return null;
    }
複製程式碼

我就直接引用這個方法進行屬性的解析。

到這裡,我們已經可以解析標籤和屬性了。對整個 Start Tag Chunk 的解析程式碼如下:

private void parseStartTagChunk() {
        log("\nparse Start Tag Chunk");
        log("chunk type: 0x%x", Xml.START_TAG_CHUNK_TYPE);

        try {
            int chunkSize = reader.readInt();
            log("chunk size: %d", chunkSize);

            int lineNumber = reader.readInt();
            log("line number: %d", lineNumber);

            reader.skip(4); // 0xffffffff

            int namespaceUri = reader.readInt();
            if (namespaceUri == -1)
                log("namespace uri: null");
            else
                log("namespace uri: %s", stringChunkList.get(namespaceUri));

            int name = reader.readInt();
            log("name: %s", stringChunkList.get(name));

            reader.skip(4); // flag 0x00140014

            int attributeCount = reader.readInt();
            log("attributeCount: %d", attributeCount);

            int classAttribute = reader.readInt();
            log("class attribute: %s", classAttribute);

            List<Attribute> attributes = new ArrayList<>();
            // 每個 attribute 五個屬性,每個屬性 4 位元組
            for (int i = 0; i < attributeCount; i++) {

                log("Attribute[%d]", i);

                int namespaceUriAttr = reader.readInt();
                if (namespaceUriAttr == -1)
                    log("   namespace uri: null");
                else
                    log("   namespace uri: %s", stringChunkList.get(namespaceUriAttr));

                int nameAttr = reader.readInt();
                if (nameAttr == -1)
                    log("   name: null");
                else
                    log("   name: %s", stringChunkList.get(nameAttr));

                int valueStr = reader.readInt();
                if (valueStr == -1)
                    log("   valueStr: null");
                else
                    log("   valueStr: %s", stringChunkList.get(valueStr));

                int type = reader.readInt() >> 24;
                log("   type: %d", type);

                int data = reader.readInt();
                String dataString = type == TypedValue.TYPE_STRING ? stringChunkList.get(data) : TypedValue.coerceToString(type, data);
                log("   data: %s", dataString);

                Attribute attribute = new Attribute(namespaceUriAttr == -1 ? null : stringChunkList.get(namespaceUriAttr),
                        stringChunkList.get(nameAttr), valueStr, type, dataString);
                attributes.add(attribute);
            }
            StartTagChunk startTagChunk = new StartTagChunk(namespaceUri, stringChunkList.get(name), attributes);
            chunkList.add(startTagChunk);
        } catch (IOException e) {
            e.printStackTrace();
            log("parse Start NameSpace Chunk error!");
        }
    }
複製程式碼

以 010 editor 解析到的第一個 Start Tag Chunk 為例,看一下解析的結果:

parse Start Tag Chunk
chunk type: 0x100102
chunk size: 116
line number: 2
namespace uri: null
name: manifest
attributeCount: 4
class attribute: 0
Attribute[0]
   namespace uri: http://schemas.android.com/apk/res/android
   name: versionCode
   valueStr: null
   type: 16
   data: 980
Attribute[1]
   namespace uri: http://schemas.android.com/apk/res/android
   name: versionName
   valueStr: 7.9.5
   type: 3
   data: 7.9.5
Attribute[2]
   namespace uri: http://schemas.android.com/apk/res/android
   name: installLocation
   valueStr: null
   type: 16
   data: 0
Attribute[3]
   namespace uri: null
   name: package
   valueStr: com.tencent.mobileqq
   type: 3
   data: com.tencent.mobileqq
複製程式碼

根據解析結果,可以輕鬆的寫出這個標籤的內容:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        android:versionCode="980"
        android:versionName="7.9.5"
        android:installLocation="0"
        package="com.tencent.mobileqq">
複製程式碼

依次解析後面的 chunk,就可以拼接出整個 AndroidManifest.xml 檔案了。

End Tag Chunk

End Tag Chunk 一共有 6 項資料,也就是 Start Tag Chunk 的前 6 項。

該項用來標識一個標籤的結束。在生成 xml 的過程中,遇到此標籤,就可以將當前解析出的標籤結束掉。就像上面的 manifest 標籤,就可以給它加上結束標籤了。

Text Chunk

Text Chunk 在解析過程中暫時還沒遇到過,這裡就不細說了。

到此為止,AndroidManifest.xml 的解析就全部結束了,但是還沒有生成一份可以直接閱讀的清單檔案。具體的生成程式碼可以看我的解析工程 Parser。包括之前的 Class 檔案解析,以及後續的其他解析程式碼都會放在這個目錄中。

文章同步更新於微信公眾號: 秉心說 , 專注 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!

Android逆向(一) —— AndroidManifest.xml 二進位制解析

相關文章