天天看點

Android應用資源檔案格式解析與保護對抗研究

原文位址:http://www.freebuf.com/articles/terminal/75944.html

apktool是Android分析中會用到的一個重要的開源工具,但是apktool的使用者的增加,相應的對抗apktool的手段也開始日益增加。 anti apktool的方法有很多,不過一般都是針對.dex的反編譯做處理,這次在分析某樣本時遇到了比較有意思的針對.arsc檔案做處理,造成apktool解包失敗的手段。本着學習研究的目的,進行了本次anti apktool的研究。

首先我們需要對arsc檔案格式有一定的了解,因為我以前也并未研究過*.arsc檔案格式,google後發現相關的資料也比較少,是以參考老羅的部落格以及Apktool的解析代碼研究整理了下面這段arsc的檔案格式,如有錯漏請盡量指正。

arsc檔案格式

Resources.arsc檔案格式是由一系列的chunk構成,每一個chunk均包含如下結構的ResChunk_header,用來描述這個chunk的基本資訊。

struct ResChunk_header
 {
     enum 
     {
         RES_NULL_TYPE               = 0x0000,
         RES_STRING_POOL_TYPE        = 0x0001,
         RES_TABLE_TYPE              = 0x0002,
         RES_XML_TYPE                = 0x0003,
         RES_XML_FIRST_CHUNK_TYPE    = 0x0100,
         RES_XML_START_NAMESPACE_TYPE= 0x0100,
         RES_XML_END_NAMESPACE_TYPE  = 0x0101,
         RES_XML_START_ELEMENT_TYPE  = 0x0102,
         RES_XML_END_ELEMENT_TYPE    = 0x0103,
         RES_XML_CDATA_TYPE          = 0x0104,
         RES_XML_LAST_CHUNK_TYPE     = 0x017f,
         RES_XML_RESOURCE_MAP_TYPE   = 0x0180,
         RES_TABLE_PACKAGE_TYPE      = 0x0200,
         RES_TABLE_TYPE_TYPE         = 0x0201,
         RES_TABLE_TYPE_SPEC_TYPE    = 0x0202
     };
     //目前這個chunk的類型
     uint16_t type;
     //目前這個chunk的頭部大小
     uint16_t headerSize;
     //目前這個chunk的大小
     uint32_t size;
 };
           

資源索引表頭部

Resources.arsc檔案的第一個結構是資源索引表頭部。其結構如下,描述了Resources.arsc檔案的大小和資源包數量。

struct ResTable_header
  {
      struct ResChunk_header header;
     //被編譯的資源包的個數
      uint32_t packageCount;
  };
           

示例:

Android應用資源檔案格式解析與保護對抗研究

圖中藍色高亮的部分就是資源索引表頭部。通過解析,我們可以得到如下資訊,這個chunk的類型為RES_TABLE_TYPE,頭部大小為0XC,整個chunk的大小為1400252byte,有一個編譯好的資源包。

資源項的值字元串資源池

緊跟着資源索引表頭部的是資源項的值字元串資源池,這個字元串資源池包含了所有的在資源包裡面所定義的資源項的值字元串,字元串資源池頭部的結構如下。

struct ResStringPool_header
 {
     struct ResChunk_header header;
     //字元串的數量
     uint32_t stringCount;
     //字元串樣式的數量
     uint32_t styleCount;
     //字元串的屬性,可取值包括0x000(UTF-16),0x001(字元串經過排序)、0X100(UTF-8)和他們的組合值
     uint32_t flags;
     //字元串内容塊相對于其頭部的距離
     uint32_t stringsStart;
     //字元串樣式塊相對于其頭部的距離
     uint32_t stylesStart;
 };
           

示例:

Android應用資源檔案格式解析與保護對抗研究

圖中綠色高亮的部分就是字元串資源池頭部,通過解析,我們可以得到如下資訊,這個chunk的類型為RES_STRING_POOL_TYPE,即字元串資源池。頭部大小為0X1C,整個chunk的大小為369524byte,有8073條字元串,72個字元串樣式,為UTF-8編碼,無排序,字元串内容塊相對于此chunk頭部的偏移為0X7F60,字元串樣式塊相對于此chunk頭部的偏移為0X5A054。

緊接着頭部的的是兩個偏移數組,分别是字元串偏移數組和字元串樣式偏移數組。這兩個偏移數組的大小分别等于stringCount和styleCount的值,而每一個元素的類型都是無符号整型。整個字元中資源池結構如下。

Android應用資源檔案格式解析與保護對抗研究

字元串資源池中的字元串前兩個位元組為字元串長度,長度計算方法如下。另外如果字元串編碼格式為UTF-8則字元串以0X00作為結束符,UTF-16則以0X0000作為結束符。

字元串與字元串樣式有一一對應的關系,也就是說如果第n個字元串有樣式,則它的樣式描述位于樣式塊的第n個元素。 字元串樣式的結構包括如下兩個結構體,ResStringPool_ref和ResStringPool_span。 一個字元串可以對應多個ResStringPool_span和一個ResStringPool_ref。ResStringPool_span在前描述字元串的樣式,ResStringPool_ref在後固定值為0XFFFFFFFF作為占位符。樣式塊最後會以兩個值為0XFFFFFFFF的ResStringPool_ref作為結束。

struct ResStringPool_ref
 {
     uint32_t index;
 };
 
 struct ResStringPool_span
 {
     enum {
         END = 0xFFFFFFFF
     };
     //指向樣式字元串在字元串池中偏移,例如粗體樣式<b>XXX</b>,則此處指向b
     ResStringPool_ref name;
     //指向應用樣式的第一個字元
     uint32_t firstChar
     //指向應用樣式的最後一個字元
     uin32_t  lastChar;
 };
           

示例:

Android應用資源檔案格式解析與保護對抗研究

圖中藍色高亮的部分就是樣式内容塊,按照格式解析可以得出,第一個字元串和第二字元串無樣式,第三個字元串第4個字元到第7個字元的位置樣式為字元串資源池中0X1F88的字元,以此類推。

Package資料塊

接着資源項的值字元串資源池後面的部分就是Package資料塊,這個資料塊記錄編譯包的中繼資料,頭部結構如下:

struct ResTable_package
 {
     struct ResChunk_header header;
     //包的ID,等于Package Id,一般使用者包的值Package Id為0X7F,系統資源包的Package Id為0X01。
     uint32_t id;
     //包名稱
     char16_t name[128];
     //類型字元串資源池相對頭部的偏移
     uint32_t typeStrings;
     //最後一個導出的Public類型字元串在類型字元串資源池中的索引,目前這個值設定為類型字元串資源池的元素個數。
     uint32_t lastPublicType;
     //資源項名稱字元串相對頭部的偏移
     uint32_t keyStrings;
     //最後一個導出的Public資源項名稱字元串在資源項名稱字元串資源池中的索引,目前這個值設定為資源項名稱字元串資源池的元素個數。
     uint32_t lastPublicKey;
 };
           

示例:

Android應用資源檔案格式解析與保護對抗研究

圖中紫色高亮的部分就是ResTable_package,按照上面的格式解析資料,我們可以得出,此Chunk的Type為RES_TABLE_PACKAGE_TYPE,頭部大小為0X120,整個chunk的大小為1030716byte,Package Id為0X7F,包名稱為co.runner.app,類型字元串資源池距離頭部的偏移是0X120,有15條字元串,資源項名稱字元串資源池0X1EC,有6249條字元串。

Packege資料塊的整體結構,可以用以下的示意圖表示:

Android應用資源檔案格式解析與保護對抗研究

其中Type String Pool和Key String Pool是兩個字元串資源池,結構和資源項的值字元串資源池結構相同,分别對應類型字元串資源池和資源項名稱字元串資源池。

再接下來的結構體可能是類型規範資料塊或者類型資源項資料塊,我們可以通過他們的Type來識别,類型規範資料塊的Type為RES_TABLE_TYPE_SPEC_TYPE,類型資源項資料塊的Type為RES_TABLE_TYPE_TYPE。

類型規範資料塊

類型規範資料塊用來描述資源項的配置差異性。通過這個差異性描述,我們就可以知道每一個資源項的配置狀況。知道了一個資源項的配置狀況之後,Android資源管理架構在檢測到裝置的配置資訊發生變化之後,就可以知道是否需要重新加載該資源項。類型規範資料塊是按照類型來組織的,也就是說,每一種類型都對應有一個類型規範資料塊。其資料塊頭部結構如下。

struct ResTable_typeSpec
 {
     struct ResChunk_header header;
     //辨別資源的Type ID,Type ID是指資源的類型ID。資源的類型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若幹種,每一種都會被賦予一個ID。
     uint8_t id;
     //保留,始終為0
     uint8_t res0;
     //保留,始終為0
     uint16_t res1;
     //等于本類型的資源項個數,指名稱相同的資源項的個數。
     uint32_t entryCount;
 };
           

示例:

Android應用資源檔案格式解析與保護對抗研究

圖中綠色高亮的部分就是ResTable_typeSpec,按照上面的格式解析資料,我們可以得出,此Chunk的Type為RES_TABLE_TYPE_SPEC_TYPE,頭部大小為0X10,整個chunk的大小為564byte,資源ID為1,本類型資源項數量為137。

ResTable_typeSpec後面緊跟着的是一個大小為entryCount的uint32_t數組,每一個數組元素都用來描述一個資源項的配置差異性的。

類型資源項資料塊

類型資源項資料塊用來描述資源項的具體資訊, 這樣我們就可以知道每一個資源項的名稱、值和配置等資訊。 類型資源項資料同樣是按照類型和配置來組織的,也就是說,一個具有n個配置的類型一共對應有n個類型資源項資料塊。其資料塊頭部結構如下

struct ResTable_type
 {
     struct ResChunk_header header;
 
     enum {
         NO_ENTRY = 0xFFFFFFFF
     };
     //辨別資源的Type ID
     uint8_t id;
     //保留,始終為0
     uint8_t res0;
     //保留,始終為0
     uint16_t res1;
     //等于本類型的資源項個數,指名稱相同的資源項的個數。
     uint32_t entryCount;
     //等于資源項資料塊相對頭部的偏移值。
     uint32_t entriesStart;
     //指向一個ResTable_config,用來描述配置資訊,地區,語言,分辨率等
     ResTable_config config;
 };
           

示例:

Android應用資源檔案格式解析與保護對抗研究

圖中紅色高亮的部分就是ResTable_type,按照上面的格式解析資料,我們可以得出,RES_TABLE_TYPE_TYPE,頭部大小為0X44,整個chunk的大小為4086byte,資源ID為1,本類型資源項數量為137,資源資料塊相對于頭部的偏移為0X268。

ResTable_type後接着是一個大小為entryCount的uint32_t數組,每一個數組元素都用來描述一個資源項資料塊的偏移位置。 緊跟在這個偏移數組後面的是一個大小為entryCount的ResTable_entry數組,每一個數組元素都用來描述一個資源項的具體資訊。ResTable_entry的結構如下:

struct ResTable_entry
 {
     //表示資源項頭部大小。
     uint16_t size;
 
     enum {
         //如果flags此位為1,則ResTable_entry後跟随ResTable_map數組,為0則跟随一個Res_value。
         FLAG_COMPLEX = 0x0001,
         //如果此位為1,這個一個被引用的資源項
         FLAG_PUBLIC = 0x0002
     };
     //資源項标志位
     uint16_t flags;
     //資源項名稱在資源項名稱字元串資源池的索引
     struct ResStringPool_ref key;
 };
           

ResTable_entry根據flags的不同,後面跟随的資料也不相同,如果flags此位為1,則ResTable_entry是ResTable_map_entry,ResTable_map_entry繼承自ResTable_entry,其結構如下。

struct ResTable_map_entry : public ResTable_entry
 {
     //指向父ResTable_map_entry的資源ID,如果沒有父ResTable_map_entry,則等于0。
     ResTable_ref parent;
     //等于後面ResTable_map的數量
     uint32_t count;
 };
           

ResTable_map_entry其後跟随則count個ResTable_map類型的數組,ResTable_map的結構如下:

struct ResTable_map
 {
     //bag資源項ID
     ResTable_ref name;
     //bag資源項值
     Res_value value;
 };
           

示例:

Android應用資源檔案格式解析與保護對抗研究

圖中顔色由深到淺就是一個完整的flags為1的資源項,現在就一起來解讀這段資料的含義,這個資源項頭部的大小為0X10,flags為1是以後面跟随的是ResTable_map數組,名稱沒有在資源項引用池中,沒有父map_entry,有一個ResTable_map。

如果flags此位為0,則ResTable_entry其後跟随的是一個Res_value,描述一個普通資源的值,Res_value結構如下。

struct Res_value
 {
     //Res_value頭部大小
     uint16_t size;
     //保留,始終為0
     uint8_t res0;
 
     enum {
         TYPE_NULL = 0x00,
         TYPE_REFERENCE = 0x01,
         TYPE_ATTRIBUTE = 0x02,
         TYPE_STRING = 0x03,
         TYPE_FLOAT = 0x04,
         TYPE_DIMENSION = 0x05,
         TYPE_FRACTION = 0x06,
         TYPE_FIRST_INT = 0x10,
         TYPE_INT_DEC = 0x10,
         TYPE_INT_HEX = 0x11,
         TYPE_INT_BOOLEAN = 0x12,
         TYPE_FIRST_COLOR_INT = 0x1c,
         TYPE_INT_COLOR_ARGB8 = 0x1c,
         TYPE_INT_COLOR_ARGB8 = 0x1c,
         TYPE_INT_COLOR_RGB8 = 0x1d,
         TYPE_INT_COLOR_ARGB4 = 0x1e,
         TYPE_INT_COLOR_RGB4 = 0x1f,
         TYPE_LAST_COLOR_INT = 0x1f,
         TYPE_LAST_INT = 0x1f
     };
     //資料的類型,可以從上面的枚舉類型中擷取
     uint8_t dataType;
 
     //資料對應的索引
     uint32_t data;
 };
           

示例:

Android應用資源檔案格式解析與保護對抗研究

圖中畫紅線的部分就是一個ResTable_entry其後跟随的是一個Res_value的例子,從中我們可以得出以下資訊,這個頭部大小為8,flags等于0,是以後面跟随的是Res_value,在資源項名稱字元串資源池中的索引為150,對應的值是badge_continue_months,Res_value的大小為8,資料的類型是TYPE_STRING,在資源項的值字元串資源池的索引為1912,對應的值是res/drawable-nodpi-v4/badge_continue_months.png。

當我們對arsc的檔案格式有了了解過後,我們就可以開始我們的探索之旅了,由于在使用Android studio調試Apktool源碼的時候遇到很多障礙,在前輩的指導下才能夠順利進行調試,是以下面簡單介紹下設定Android studio調試Apktool源碼的方法。

通過Android studio調試Apktool源碼

1.拉取apktool源碼到本地

git clone https://github.com/iBotPeaches/Apktool      

2.将apktool源代碼導入android studio,導入後會android studio會自動查找對應Grable并下載下傳,需要比較長的時間等待。

Android應用資源檔案格式解析與保護對抗研究

3.選中Grable中的fatjar進行編譯

Android應用資源檔案格式解析與保護對抗研究

4.增加RunApktool配置

Android應用資源檔案格式解析與保護對抗研究

5.開始調試

Android應用資源檔案格式解析與保護對抗研究

anti apktool分析

首先我們從解包失敗的錯誤異常入手,定位崩潰處的代碼。

Android應用資源檔案格式解析與保護對抗研究

通過錯誤提示可以定位到apktool崩潰處的代碼,源碼位置如下:

\git\Apktool\brut.apktool\apktool-lib\src\main\java\brut\androlib\res\data\ResType.java      
Android應用資源檔案格式解析與保護對抗研究

在崩潰的函數處下斷點,我們開始調試。

通過抛出的異常的說明“Multiple res specs”,以及整個調用堆棧,大體上我們可以看出造成這個異常的原因是在同一個類型的資源裡,有多個同名稱的資源。

我們使用aapt dump一份apk的資源清單進行觀察,在這之前我們需要了解一下appt dump resources的基本格式。

resource <Resource ID> <Package Name>:<Type>/<Name>: t=<DataType> d=<Data> (s=<Size> r=<Res0>)      
Resource ID R.java中的資源ID 
Package Name 資源所在的的包 
Type 資源的類型 
Name 資源名稱 
DataType 資料類型,按照以下枚舉類型取值 
Data 資源的值,根據dataType進行解釋 
Size 一直為0x0008 
Res0 固定為0x00
           
enum {
         // Contains no data.
         TYPE_NULL = 0x00,
         // The 'data' holds a ResTable_ref, a reference to another resource
         // table entry.
         TYPE_REFERENCE = 0x01,
         // The 'data' holds an attribute resource identifier.
         TYPE_ATTRIBUTE = 0x02,
         // The 'data' holds an index into the containing resource table's
         // global value string pool.
         TYPE_STRING = 0x03,
         // The 'data' holds a single-precision floating point number.
         TYPE_FLOAT = 0x04,
         // The 'data' holds a complex number encoding a dimension value,
         // such as "100in".
         TYPE_DIMENSION = 0x05,
         // The 'data' holds a complex number encoding a fraction of a
         // container.
         TYPE_FRACTION = 0x06,
 
         // Beginning of integer flavors...
         TYPE_FIRST_INT = 0x10,
 
         // The 'data' is a raw integer value of the form n..n.
         TYPE_INT_DEC = 0x10,
         // The 'data' is a raw integer value of the form 0xn..n.
         TYPE_INT_HEX = 0x11,
         // The 'data' is either 0 or 1, for input "false" or "true" respectively.
         TYPE_INT_BOOLEAN = 0x12,
 
         // Beginning of color integer flavors...
         TYPE_FIRST_COLOR_INT = 0x1c,
 
         // The 'data' is a raw integer value of the form #aarrggbb.
         TYPE_INT_COLOR_ARGB8 = 0x1c,
         // The 'data' is a raw integer value of the form #rrggbb.
         TYPE_INT_COLOR_RGB8 = 0x1d,
         // The 'data' is a raw integer value of the form #argb.
         TYPE_INT_COLOR_ARGB4 = 0x1e,
         // The 'data' is a raw integer value of the form #rgb.
         TYPE_INT_COLOR_RGB4 = 0x1f,
 
         // ...end of integer flavors.
         TYPE_LAST_COLOR_INT = 0x1f,
 
         // ...end of integer flavors.
         TYPE_LAST_INT = 0x1f
     };
           

下面是dump resources 出來的部分内容:

Android應用資源檔案格式解析與保護對抗研究

從這部分資訊中我們可以看到在這份資源索引表中大部分的資源并沒有名稱,正是因為沒有名稱,apktool在往包結構中添加資料時,兩個同類型的沒有名稱的資源就産生了沖突,造成了崩潰。

知道了崩潰的原因,那我們就需要确定它具體是使用怎樣的方法做到的,知道了方法我們才能想辦法去避開它。

通過前面的arsc檔案格式的學習,我們知道資源項中ResTable_entry.key是在資源項名稱字元串資源池中的索引,那麼我們通過16進制編輯器就可以真實的觀察到資料真實的對應情況。

Android應用資源檔案格式解析與保護對抗研究

圖中紅框内的就是一串連續的ResTable_entry.key,我們可以看到,他們全部都指向了資源項名稱字元串資源池下标為0的字元串。

Android應用資源檔案格式解析與保護對抗研究

上圖就是資源項名稱字元串資源池下标為0的字元串,很不幸,這是一個空字元串,是以所有名稱指向它的資源都變成了空字元,這才造成了我們的解析失敗。

Android應用資源檔案格式解析與保護對抗研究

确定了失敗的原因,我們就可以對我們apktool的源碼進行改造,讓它能處理這種情況,我通過在Apktool.apktool-lib.java 中的 readEntry()函數中增加對ResTable_entry.key的判斷,如果指向0則産生的名稱則使用目前資源的資源ID作為名稱來解決這個問題。

Android應用資源檔案格式解析與保護對抗研究

解包沒有崩潰,好像是解決了問題,但是當我們打開res中檔案檢視時就會發現事情并沒有這麼簡單。

Android應用資源檔案格式解析與保護對抗研究
Android應用資源檔案格式解析與保護對抗研究
Android應用資源檔案格式解析與保護對抗研究

首先解包出的檔案夾中缺少drawables檔案夾,這意味着缺少大量圖檔檔案,第二我們可以看見layout.xml中引用了大量的外部檔案,這部分檔案也沒有解析,全部放到了unknow目錄下的r目錄中,從這些現象我們可以得出現在資源擷取是沒有問題,但是在資源解析的時候又出現了問題。

沒辦法,繼續調試,首先定位到資源解析的位置。

Android應用資源檔案格式解析與保護對抗研究

解析資源的代碼位于Apktool.apktool-lib.java中, 上圖中紅框這種的兩處就是我們解析失敗的原因,包括以下兩點。

第一:隻考慮了解析res目錄下的檔案。

第二:沒有将其他檔案目錄下的檔案歸類到需要解析的list中。 下面針對這兩點我們對apktool繼續進行改造。

Android應用資源檔案格式解析與保護對抗研究

首先修改對是否是檔案的資源的判斷進行修改,支援r目錄,不過這裡隻是粗略的修改,最好的方式還是通過正規表達式來比對。

Android應用資源檔案格式解析與保護對抗研究

然後是修改擷取資源的輸入路徑,增加對r目錄的支援。

Android應用資源檔案格式解析與保護對抗研究

最後是增加對輸入路徑的判斷。

Android應用資源檔案格式解析與保護對抗研究
Android應用資源檔案格式解析與保護對抗研究

解析成功,到這裡,我們關于這個樣本的anti apktool差不多就結束,剩下的就是一些需要調整的小問題,包括名稱的确定,多路徑的支援等,就不再在這裡贅述了,感興趣的讀者可以自行實驗。

繼續閱讀