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

往期目錄:

Class 文件格式詳解

Smali 語法解析——Hello World

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

Smali 語法解析 —— 類

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

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

Android 逆向筆記 —— 一個簡單 CrackMe 的逆向總結

概述

我們在解壓縮 APK 文件之後,會看到一個叫做 resources.arsc 的文件,它的格式稱之爲 ARSC 文件格式 。那麼它的作用是什麼呢?大家對 R 文件肯定都十分熟悉,它存儲了資源的 ID。在打包過程中,但凡使用到資源的地方都是使用這個 ID 來代替的。ARSC 文件就是一個資源索引表,它可以幫助系統根據資源 ID 快速找到資源。

當我們使用 ApkTool 反編譯的時候,會在 res/value 目錄下生成一個 public.xml 文件,裏面就記錄了資源項及其對應的 ID,如下圖所示:

雖然沒有細看過 ApkTool 的源碼,但我猜測這應該就是根據 ARSC 文件解析出來的。關於 ARSC 的文件結構,網上有一張很好的圖片,拿過來給大家看一下:

如果覺得有點繞,可以對照我畫的思維導圖來閱讀後面的文章:

ARSC 文件格式的數據結構在 AOSP 中也有相應的定義,位於 ResourceType.h 文件中 。整體上可以分爲下面三大塊:

  • ResTableHeader : 文件頭
  • ResStringPool : 資源項值字符串池
  • ResTablePackage : 數據塊

其中 ResTablePackage 項最爲複雜,包含了 ARSC 文件的數據塊內容。其他兩塊內容較爲簡單。下面就來一一解析。

ResTableHeader

struct ResTable_header
{
    struct ResChunk_header header;

    // The number of ResTable_package structures.
    uint32_t packageCount;
};

這裏的 header 是 ResChunk_header 類型,我們先來看一下這個類,它在 ARSC 文件的其他部分也會出現很多次。其實 ARSC 文件和 AndroidManifest.xml 文件有一些類似,也是由一個一個 Chunk 組成的。每一個 Chunk 都有固定的 ResTable_header,具體格式如下:

struct ResChunk_header
{
    // Type identifier for this chunk.  The meaning of this value depends
    // on the containing chunk.
    uint16_t type;

    // Size of the chunk header (in bytes).  Adding this value to
    // the address of the chunk allows you to find its associated data
    // (if any).
    uint16_t headerSize;

    // Total size of this chunk (in bytes).  This is the chunkSize plus
    // the size of any data associated with the chunk.  Adding this value
    // to the chunk allows you to completely skip its contents (including
    // any child chunks).  If this value is the same as chunkSize, there is
    // no data associated with the chunk.
    uint32_t size;
};

type 是該 Chunk 的標識符,不同的 Chunk 都有自己的標識符。headerSize 表示當前 Chunk Header 的大小。size 表示當前 Chunk 的大小。

ResChunkHeader 的結構還是很簡單的,我們再回到 ResTableHeader。它除了 header 字段之外,還有一個 packageCount 字段,表示 ARSC 文件 ResTablePackage 的個數,即數據塊的個數,通常是 1。

ResStringPool

ResTableHeader 後面緊接着的就是 ResStringPool,存放了 APK 中所有資源項值的字符串內容。我這裏就不貼 ResStringPool 的結構圖了,建議閱讀的時候直接對照着我上面給的思維導圖,或者對照着 010 Editor 的解析結果。

ResStringPool 也有一個頭,叫做 ResStringPoolHeader,其格式如下:

struct ResStringPool_header
{
    struct ResChunk_header header;

    // Number of strings in this pool (number of uint32_t indices that follow
    // in the data).
    uint32_t stringCount;

    // Number of style span arrays in the pool (number of uint32_t indices
    // follow the string indices).
    uint32_t styleCount;

    // Flags.
    enum {
        // If set, the string index is sorted by the string values (based
        // on strcmp16()).
        SORTED_FLAG = 1<<0,

        // String pool is encoded in UTF-8
        UTF8_FLAG = 1<<8
    };
    uint32_t flags;

    // Index from header of the string data.
    uint32_t stringsStart;

    // Index from header of the style data.
    uint32_t stylesStart;
};

有六個字段,來分別看一下:

  • header : ResChunkHeader,其 type 是 RES_STRING_POOL_TYPE
  • stringCount : 字符串個數
  • styleCount : 字符串樣式個數
  • flags : 字符串的屬性,可取值包括0x000(UTF-16),0x001(字符串經過排序)、0X100(UTF-8)和他們的組合值
  • stringsStart : 字符串內容偏移量
  • stylesStart : 字符串樣式內容偏移量

ResStringPoolHeader 之後跟着的是兩個偏移量數組 stringOffsetsstyleOffsets,分別是字符串內容偏移量數組和字符串樣式內容偏移量數組。上面提到的偏移量都是相對整個 ResStringPool 的。根據起始偏移量和每個字符串的偏移量數組,我們就可以獲取到所有字符串了。注意這裏的字符串並不是純粹的字符串,它也是有結構的。 u16lenu8len,分別代表 UTF-8UTF-16 下的字符串長度。那麼如何區分呢?之前的 ResStringPoolHeader 中的 flags 屬性就標記了編碼格式。如果是 utf-8,則字符串以 0x00 結尾,開頭前兩個字節分別表示 u8len 和 u16len。如果是 utf-16,則字符串以 0x0000 結尾,開頭前兩個字節表示 u16len,沒有 u8len 字段。

下面簡單看一下解析代碼:

private ResStringPoolHeader parseStringPoolType(List<String> stringPoolList) {
    int currentPosition = reader.getCurrentPosition();
    ResStringPoolHeader stringPoolHeader = new ResStringPoolHeader();
    try {

        stringPoolHeader.parse(reader);
        List<Integer> stringOffsets = new ArrayList<>(stringPoolHeader.stringCount);
        for (int i = 0; i < stringPoolHeader.stringCount; i++) {
            int offset = reader.readInt();
            stringOffsets.add(offset);
        }

        List<Integer> styleOffsets = new ArrayList<>();
        for (int i = 0; i < stringPoolHeader.styleCount; i++) {
            styleOffsets.add(reader.readInt());
        }

        int position = reader.getCurrentPosition();
            for (int i = 0; i < stringPoolHeader.stringCount; i++) {
                int length = 0;
                int skipLength = 0;
                if (stringPoolHeader.flags == ResStringPoolHeader.UTF8_FLAG) {
                    int u16len = reader.read(position + stringOffsets.get(i), 1)[0];
                    int u8len = reader.read(position + stringOffsets.get(i), 1)[0];
                    length = u8len;
                    skipLength = 1; // 如果是 utf-8,則字符串以 0x00結尾
                } else {
                    int u16len =reader.readUnsignedShort();
                    length = u16len;
                    skipLength = 2; // 如果是 utf-16,則字符串以 0x0000結尾
                }
                String string = "";
                try {
                    string = new String(reader.read(position + stringOffsets.get(i) + 2, skipLength*length));
                    reader.skip(skipLength);
                } catch (Exception e) {
                    log("   parse string[%d] error!", i);
                }

                stringPoolList.add(string);
                log("   stringPool[%d]: %s", i, string);
            }

            for (int i = 0; i < stringPoolHeader.styleCount; i++) {
                int index = reader.readInt();
                int firstChar = reader.readInt();
                int lastChar = reader.readInt();
                ResSpanStyle resSpanStyle = new ResSpanStyle(index, firstChar, lastChar);
                log(resSpanStyle.toString());
                reader.skip(4); // 0xffff
            }
            reader.moveTo(currentPosition + stringPoolHeader.resChunkHeader.size);
            return stringPoolHeader;
    } catch (IOException e) {
        log("   parse string pool type error!");
    }
    return null;
}

我拿我自己的 Wanandroid 安裝包解壓得到的 ARSC 文件做測試,一共打印了 2411 個字符串,如下圖所示:

看完了字符串池,接下來就是最最重要的資源數據塊 ResTablePackage 了。

ResTablePackage

從文章開頭給出的思維導圖就可以看出來,ResTablePackage 佔據了 ARSC 文件內容的大半壁江山。ResTablePackage 又可以分爲五小塊,如下所示:

  • ResTablePackageHeader : 頭信息
  • typeStrings : 資源類型字符串池
  • keyStrings : 資源項名稱字符串池
  • ResTableTypeSpec : 資源表規範
  • ResTableType : 資源表類型配置

下面來一一進行解析。

ResTablePackageHeader

其實我只是爲了保持名稱統一才取了個名字叫 ResTablePackageHeader,但是在 AOSP 中是叫做 ResTable_package,其內容如下所示:

struct ResTable_package
{
    struct ResChunk_header header;

    // If this is a base package, its ID.  Package IDs start
    // at 1 (corresponding to the value of the package bits in a
    // resource identifier).  0 means this is not a base package.
    uint32_t id;

    // Actual name of this package, \0-terminated.
    uint16_t name[128];

    // Offset to a ResStringPool_header defining the resource
    // type symbol table.  If zero, this package is inheriting from
    // another base package (overriding specific values in it).
    uint32_t typeStrings;

    // Last index into typeStrings that is for public use by others.
    uint32_t lastPublicType;

    // Offset to a ResStringPool_header defining the resource
    // key symbol table.  If zero, this package is inheriting from
    // another base package (overriding specific values in it).
    uint32_t keyStrings;

    // Last index into keyStrings that is for public use by others.
    uint32_t lastPublicKey;

    uint32_t typeIdOffset;
};
  • header : ResChunkHeader , 其 type 是 RES_TABLE_PACKAGE_TYPE
  • id : 包的 ID, 等於 Package Id,一般用戶包的 Package Id 爲 0X7F, 系統資源包的 Package Id 爲 0X01
  • name : 包名
  • typeStrings : 資源類型字符串池在 ResTablePackage 中的偏移量
  • lastPublicType : 一般資源類型字符串資源池的元素個數
  • keyStrings : 資源名稱字符串池在 ResTablePackage 中的偏移量
  • lastPublicKey : 一般指資源項名稱字符串資源池的元素個數。
  • typeIdOffset : 未知,值爲 0

typeStrings

typeStrings 是資源類型字符串池,既然是資源類型,很容易就想到 stringlayoutdrawablemipmap 等等,這些都是資源類型。說直白點,就是通常寫代碼時候的 R. 後面跟的東西。typeStrings 就是一個 ResStringPool,所以它的解析方式和之前是一模一樣的。直接看一下解析結果:

typeStrings: 
   stringPool[0]: a n i m 
   stringPool[1]: a n i m a t o r 
   stringPool[2]: a t t r 
   stringPool[3]: b o o l 
   stringPool[4]: c o l o r 
   stringPool[5]: d i m e n 
   stringPool[6]: d r a w a b l e 
   stringPool[7]: i d 
   stringPool[8]: i n t e g e r 
   stringPool[9]: i n t e r p o l a t o r 
   stringPool[10]: l a y o u t 
   stringPool[11]: m e n u 
   stringPool[12]: m i p m a p 
   stringPool[13]: r a w 
   stringPool[14]: s t r i n g 
   stringPool[15]: s t y l e 

這裏是 utf-16 編碼的。

keyStrings

keyStrings 是資源項名稱字符串池,它也是 ResStringPool,就不再多說了,直接看解析結果:

keyStrings: 
   stringPool[0]: abc_fade_in
   stringPool[1]: abc_fade_out
   stringPool[2]: abc_grow_fade_in_from_bottom
   stringPool[3]: abc_popup_enter
   stringPool[4]: abc_popup_exit
   stringPool[5]: abc_shrink_fade_out_from_bottom
   stringPool[6]: abc_slide_in_bottom
   stringPool[7]: abc_slide_in_top
   stringPool[8]: abc_slide_out_bottom
   stringPool[9]: abc_slide_out_top
   stringPool[10]: abc_tooltip_enter
   stringPool[11]: abc_tooltip_exit
   stringPool[12]: btn_checkbox_to_checked_box_inner_merged_animation
   stringPool[13]: btn_checkbox_to_checked_box_outer_merged_animation
   stringPool[14]: btn_checkbox_to_checked_icon_null_animation
   ...
   ...
   ...
   stringPool[2322]: Widget.Support.CoordinatorLayout
   stringPool[2323]: leak_canary_LeakCanary.Base
   stringPool[2324]: leak_canary_Theme.Transparent

資源項名稱字符串池 keyStrings 之後是 ResTableTypeSpecResTableType ,它們是不定的交叉出現的。我們先來看看 ResTableTypeSpec

ResTableTypeSpec

ResTableTypeSpec 是資源表規範,用來描述資源項的配置差異性。系統根據不同設備的配置差異就可以加載不同的資源項。該部分數據結構對應結構體 ResTable_typeSpec :

struct ResTable_typeSpec
{
    struct ResChunk_header header;

    // The type identifier this chunk is holding.  Type IDs start
    // at 1 (corresponding to the value of the type bits in a
    // resource identifier).  0 is invalid.
    uint8_t id;

    // Must be 0.
    uint8_t res0;
    // Must be 0.
    uint16_t res1;

    // Number of uint32_t entry configuration masks that follow.
    uint32_t entryCount;

    enum {
        // Additional flag indicating an entry is public.
        SPEC_PUBLIC = 0x40000000
    };
};
  • header : ResChunkHeader,其 type 是 RES_TABLE_TYPE_SPEC_TYPE
  • id : 標識資源的 Type ID, Type ID 是指資源的類型 ID 。資源的類型有 animator、anim、color、drawable、layout、menu、raw、string 和 xml 等等若干種,每一種都會被賦予一個 ID
  • res0 : must be 0
  • res1 : must be 0
  • entryCount : 等於本類型的資源項個數,指名稱相同的資源項的個數

緊接着後面的是 entryCount 個 uint_32 數組,數組每個元素都是用來描述資源項的配置差異性的。

ResTableType

ResTableType 是資源項的具體信息,包括資源項的名稱,類型,值和配置等等。對應結構體 ResTable_type

struct ResTable_type
{
    struct ResChunk_header header;

    enum {
        NO_ENTRY = 0xFFFFFFFF
    };

    // The type identifier this chunk is holding.  Type IDs start
    // at 1 (corresponding to the value of the type bits in a
    // resource identifier).  0 is invalid.
    uint8_t id;

    // Must be 0.
    uint8_t res0;
    // Must be 0.
    uint16_t res1;

    // Number of uint32_t entry indices that follow.
    uint32_t entryCount;

    // Offset from header where ResTable_entry data starts.
    uint32_t entriesStart;

    // Configuration this collection of entries is designed for.
    ResTable_config config;
};
  • header : ResChunkHeader,其 type 是 RES_TABLE_TYPE_TYPE
  • id : 標識資源的 Type ID, Type ID 是指資源的類型 ID 。資源的類型有 animator、anim、color、drawable、layout、menu、raw、string 和 xml 等等若干種,每一種都會被賦予一個 ID
  • res0 : must be 0
  • res1 : must be 0
  • entryCount : 資源項的個數
  • entryStart :資源項相對於本結構的偏移量
  • config : 資源的配置信息

config 之後是一個大小爲 entryCount 的 uint32_t 數組,用於描述資源項數據庫的偏移量。這個偏移量數組之後是一個 ResTableEntry[],我們再來看一下這塊內容。

ResTableEntry 是資源項數據,對應結構體 ResTable_entry

struct ResTable_entry
{
    // Number of bytes in this structure.
    uint16_t size;

    enum {
        // If set, this is a complex entry, holding a set of name/value
        // mappings.  It is followed by an array of ResTable_map structures.
        FLAG_COMPLEX = 0x0001,
        // If set, this resource has been declared public, so libraries
        // are allowed to reference it.
        FLAG_PUBLIC = 0x0002,
        // If set, this is a weak resource and may be overriden by strong
        // resources of the same name/type. This is only useful during
        // linking with other resource tables.
        FLAG_WEAK = 0x0004
    };
    uint16_t flags;

    // Reference into ResTable_package::keyStrings identifying this entry.
    struct ResStringPool_ref key;
};
  • size : 該結構體大小
  • flags : 標誌位
  • key : 資源項名稱在資源項名稱字符串資源池的索引

根據 flags 的不同,後面的數據結構也有所不同。如果 flags 包含 FLAG_COMPLEX(0x0001),則該數據結構是 ResTableMapEntryResTableMapEntry 是繼承自 ResTableEntry 的,在原有結構上多了兩個 uint32_t 字段 parentcountparent 表示父資源項的 ID。count 表示接下來有多少個 ResTableMapResTableMap 結構如下所示:

struct ResTable_map
{
    ResTable_ref name; // 資源名稱
    Res_value value; // 資源值
}

再來看看 ResValue

struct Res_value {
    uint16_t size;
    uint8_t res0;
    uint8_t dataType;
    data_type data;

以上就是 flags 包含 FLAG_COMPLEX(0x0001)時表示的 ResTableMapEntry 的結構。如果不包含的話,就直接是 Res_value

總結

最後關於 Package 數據塊的內容其實說的比較簡略,因爲個人覺得了解了解就可以了。如果想要深入學習的話,個人十分推薦老羅的一篇文章,Android應用程序資源的編譯和打包過程分析,寫的相當的詳細。

最後還是給出解析代碼地址,ResParser

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章