編輯:關於android開發
快過年了,先提前祝賀大家新年快樂,這篇文章也是今年最後一篇了。今天我們繼續來看逆向的相關知識,前篇文章中我們介紹了如何解析Android中編譯之後的AndroidManifest.xml文件格式
當時我說到其實後續還要繼續介紹兩個文件一個是resource.arsc和classes.dex,今天我們就來看看resource.arsc文件個格式解析,classes.dex的解析要等年後了。
我們在使用apktool工具進行反編譯的時候,會發現有一個:res/values/public.xml這個文件:
我們查看一下public.xml文件內容:
看到了,這個文件就保存了apk中所有的類型和對應的id值,我們看到這裡面的每個條目內容都是:
type:類型名
name:資源名
id:資源的id
類型的話有這麼幾種:
drawable,menu,layout,string,attr,color,style等
所以我們會在反編譯之後的文件夾中看到這幾個類型的文件xml內容。
上面我們介紹了如何使用apktool反編譯之後的內容,下面我們要做的事情就是如何來解析resource.arsc文件,解析出這些文件。
我們解壓一個apk得到對應的resource.arsc文件。按照國際慣例,每個文件的格式描述都是有對應的數據結構的,resource也不例外:frameworks\base\include\androidfw\ResourceTypes.h,這就是resource中定義的所有數據結構。
下面再來看一張神圖:
每次我們在解析文件的時候都會有一張神圖,我們按照這張圖來進行數據解析工作。
這個是項目工程結構,我們看到定義了很多的數據結構
Resources.arsc文件格式是由一系列的chunk構成,每一個chunk均包含如下結構的ResChunk_header,用來描述這個chunk的基本信息
package com.wjdiankong.parseresource.type; import com.wjdiankong.parseresource.Utils; /** 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; }; * @author i * */ public class ResChunkHeader { public short type; public short headerSize; public int size; public int getHeaderSize(){ return 2+2+4; } @Override public String toString(){ return "type:"+Utils.bytesToHexString(Utils.int2Byte(type))+",headerSize:"+headerSize+",size:"+size; } }type:是當前這個chunk的類型
headerSize:是當前這個chunk的頭部大小
size:是當前這個chunk的大小
Resources.arsc文件的第一個結構是資源索引表頭部。其結構如下,描述了Resources.arsc文件的大小和資源包數量。
package com.wjdiankong.parseresource.type; /** struct ResTable_header { struct ResChunk_header header; // The number of ResTable_package structures. uint32_t packageCount; }; * @author i * */ public class ResTableHeader { public ResChunkHeader header; public int packageCount; public ResTableHeader(){ header = new ResChunkHeader(); } public int getHeaderSize(){ return header.getHeaderSize() + 4; } @Override public String toString(){ return "header:"+header.toString()+"\n" + "packageCount:"+packageCount; } }
header:就是標准的Chunk頭部信息格式
packageCount:被編譯的資源包的個數
Android中一個apk可能包含多個資源包,默認情況下都只有一個就是應用的包名所在的資源包
實例:
圖中藍色高亮的部分就是資源索引表頭部。通過解析,我們可以得到如下信息,這個chunk的類型為RES_TABLE_TYPE,頭部大小為0XC,整個chunk的大小為1400252byte,有一個編譯好的資源包。
緊跟著資源索引表頭部的是資源項的值字符串資源池,這個字符串資源池包含了所有的在資源包裡面所定義的資源項的值字符串,字符串資源池頭部的結構如下。
package com.wjdiankong.parseresource.type; /** 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; }; * @author i * */ public class ResStringPoolHeader { public ResChunkHeader header; public int stringCount; public int styleCount; public final static int SORTED_FLAG = 1; public final static int UTF8_FLAG = (1<<8); public int flags; public int stringsStart; public int stylesStart; public ResStringPoolHeader(){ header = new ResChunkHeader(); } public int getHeaderSize(){ return header.getHeaderSize() + 4 + 4 + 4 + 4 + 4; } @Override public String toString(){ return "header:"+header.toString()+"\n" + "stringCount:"+stringCount+",styleCount:"+styleCount+",flags:"+flags+",stringStart:"+stringsStart+",stylesStart:"+stylesStart; } }
header:標准的Chunk頭部信息結構
stringCount:字符串的個數
styleCount:字符串樣式的個數
flags:字符串的屬性,可取值包括0x000(UTF-16),0x001(字符串經過排序)、0X100(UTF-8)和他們的組合值
stringStart:字符串內容塊相對於其頭部的距離
stylesStart:字符串樣式塊相對於其頭部的距離
實例:
圖中綠色高亮的部分就是字符串資源池頭部,通過解析,我們可以得到如下信息,這個chunk的類型為RES_STRING_POOL_TYPE,即字符串資源池。頭部大小為0X1C,整個chunk的大小為369524byte,有8073條字符串,72個字符串樣式,為UTF-8編碼,無排序,字符串內容塊相對於此chunk頭部的偏移為0X7F60,字符串樣式塊相對於此chunk頭部的偏移為0X5A054。
緊接著頭部的的是兩個偏移數組,分別是字符串偏移數組和字符串樣式偏移數組。這兩個偏移數組的大小分別等於stringCount和styleCount的值,而每一個元素的類型都是無符號整型。整個字符中資源池結構如下。
字符串資源池中的字符串前兩個字節為字符串長度,長度計算方法如下。另外如果字符串編碼格式為UTF-8則字符串以0X00作為結束符,UTF-16則以0X0000作為結束符。
len = (((hbyte & 0x7F) << 8)) | lbyte;
字符串與字符串樣式有一一對應的關系,也就是說如果第n個字符串有樣式,則它的樣式描述位於樣式塊的第n個元素。 字符串樣式的結構包括如下兩個結構體,ResStringPool_ref和ResStringPool_span。 一個字符串可以對應多個ResStringPool_span和一個ResStringPool_ref。ResStringPool_span在前描述字符串的樣式,ResStringPool_ref在後固定值為0XFFFFFFFF作為占位符。樣式塊最後會以兩個值為0XFFFFFFFF的ResStringPool_ref作為結束。
package com.wjdiankong.parseresource.type; /** struct ResStringPool_ref { uint32_t index; }; * @author i * */ public class ResStringPoolRef { public int index; public int getSize(){ return 4; } @Override public String toString(){ return "index:"+index; } }
實例:
圖中藍色高亮的部分就是樣式內容塊,按照格式解析可以得出,第一個字符串和第二字符串無樣式,第三個字符串第4個字符到第7個字符的位置樣式為字符串資源池中0X1F88的字符,以此類推。
接著資源項的值字符串資源池後面的部分就是Package數據塊,這個數據塊記錄編譯包的元數據,頭部結構如下:
package com.wjdiankong.parseresource.type; /** 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. char16_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; }; * @author i * */ public class ResTablePackage { public ResChunkHeader header; public int id; public char[] name = new char[128]; public int typeStrings; public int lastPublicType; public int keyStrings; public int lastPublicKey; public ResTablePackage(){ header = new ResChunkHeader(); } @Override public String toString(){ return "header:"+header.toString()+"\n"+",id="+id+",name:"+name.toString()+",typeStrings:"+typeStrings+",lastPublicType:"+lastPublicType+",keyStrings:"+keyStrings+",lastPublicKey:"+lastPublicKey; } }header:Chunk的頭部信息數據結構
id:包的ID,等於Package Id,一般用戶包的值Package Id為0X7F,系統資源包的Package Id為0X01;這個值很重要的,在後面我們構建前面說到的那個public.xml中的id值的時候需要用到。
name:包名
typeString:類型字符串資源池相對頭部的偏移
lastPublicType:最後一個導出的Public類型字符串在類型字符串資源池中的索引,目前這個值設置為類型字符串資源池的元素個數。在解析的過程中沒發現他的用途
keyStrings:資源項名稱字符串相對頭部的偏移
lastPublicKey:最後一個導出的Public資源項名稱字符串在資源項名稱字符串資源池中的索引,目前這個值設置為資源項名稱字符串資源池的元素個數。在解析的過程中沒發現他的用途
實例:
圖中紫色高亮的部分就是ResTable_package,按照上面的格式解析數據,我們可以得出,此Chunk的Type為RES_TABLE_PACKAGE_TYPE,頭部大小為0X120,整個chunk的大小為1030716byte,Package Id為0X7F,包名稱為co.runner.app,類型字符串資源池距離頭部的偏移是0X120,有15條字符串,資源項名稱字符串資源池0X1EC,有6249條字符串。
Packege數據塊的整體結構,可以用以下的示意圖表示:
其中Type String Pool和Key String Pool是兩個字符串資源池,結構和資源項的值字符串資源池結構相同,分別對應類型字符串資源池和資源項名稱字符串資源池。
再接下來的結構體可能是類型規范數據塊或者類型資源項數據塊,我們可以通過他們的Type來識別,類型規范數據塊的Type為RES_TABLE_TYPE_SPEC_TYPE,類型資源項數據塊的Type為RES_TABLE_TYPE_TYPE。
類型規范數據塊用來描述資源項的配置差異性。通過這個差異性描述,我們就可以知道每一個資源項的配置狀況。知道了一個資源項的配置狀況之後,Android資源管理框架在檢測到設備的配置信息發生變化之後,就可以知道是否需要重新加載該資源項。類型規范數據塊是按照類型來組織的,也就是說,每一種類型都對應有一個類型規范數據塊。其數據塊頭部結構如下。
package com.wjdiankong.parseresource.type; /** 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 }; }; * @author i * */ public class ResTableTypeSpec { public final static int SPEC_PUBLIC = 0x40000000; public ResChunkHeader header; public byte id; public byte res0; public short res1; public int entryCount; public ResTableTypeSpec(){ header = new ResChunkHeader(); } @Override public String toString(){ return "header:"+header.toString()+",id:"+id+",res0:"+res0+",res1:"+res1+",entryCount:"+entryCount; } }header:Chunk的頭部信息結構
id:標識資源的Type ID,Type ID是指資源的類型ID。資源的類型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干種,每一種都會被賦予一個ID。
res0:保留,始終為0
res1:保留,始終為0
entryCount:等於本類型的資源項個數,指名稱相同的資源項的個數。
實例:
圖中綠色高亮的部分就是ResTable_typeSpec,按照上面的格式解析數據,我們可以得出,此Chunk的Type為RES_TABLE_TYPE_SPEC_TYPE,頭部大小為0X10,整個chunk的大小為564byte,資源ID為1,本類型資源項數量為137。
ResTable_typeSpec後面緊跟著的是一個大小為entryCount的uint32_t數組,每一個數組元素都用來描述一個資源項的配置差異性的。
類型資源項數據塊用來描述資源項的具體信息, 這樣我們就可以知道每一個資源項的名稱、值和配置等信息。 類型資源項數據同樣是按照類型和配置來組織的,也就是說,一個具有n個配置的類型一共對應有n個類型資源項數據塊。其數據塊頭部結構如下
package com.wjdiankong.parseresource.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; }; * @author i * */ public class ResTableType { public ResChunkHeader header; public final static int NO_ENTRY = 0xFFFFFFFF; public byte id; public byte res0; public short res1; public int entryCount; public int entriesStart; public ResTableConfig resConfig; public ResTableType(){ header = new ResChunkHeader(); resConfig = new ResTableConfig(); } public int getSize(){ return header.getHeaderSize() + 1 + 1 + 2 + 4 + 4; } @Override public String toString(){ return "header:"+header.toString()+",id:"+id+",res0:"+res0+",res1:"+res1+",entryCount:"+entryCount+",entriesStart:"+entriesStart; } }header:Chunk的頭部信息結構
id:標識資源的Type ID
res0:保留,始終為0
res1:保留,始終為0
entryCount:等於本類型的資源項個數,指名稱相同的資源項的個數。
entriesStart:等於資源項數據塊相對頭部的偏移值。
resConfig:指向一個ResTable_config,用來描述配置信息,地區,語言,分辨率等
實例:
圖中紅色高亮的部分就是ResTable_type,按照上面的格式解析數據,我們可以得出,RES_TABLE_TYPE_TYPE,頭部大小為0X44,整個chunk的大小為4086byte,資源ID為1,本類型資源項數量為137,資源數據塊相對於頭部的偏移為0X268。
ResTable_type後接著是一個大小為entryCount的uint32_t數組,每一個數組元素都用來描述一個資源項數據塊的偏移位置。 緊跟在這個偏移數組後面的是一個大小為entryCount的ResTable_entry數組,每一個數組元素都用來描述一個資源項的具體信息。ResTable_entry的結構如下:
package com.wjdiankong.parseresource.type; import com.wjdiankong.parseresource.ParseResourceUtils; /** 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 }; uint16_t flags; // Reference into ResTable_package::keyStrings identifying this entry. struct ResStringPool_ref key; }; * @author i * */ public class ResTableEntry { public final static int FLAG_COMPLEX = 0x0001; public final static int FLAG_PUBLIC = 0x0002; public short size; public short flags; public ResStringPoolRef key; public ResTableEntry(){ key = new ResStringPoolRef(); } public int getSize(){ return 2+2+key.getSize(); } @Override public String toString(){ return "size:"+size+",flags:"+flags+",key:"+key.toString()+",str:"+ParseResourceUtils.getKeyString(key.index); } }ResTable_entry根據flags的不同,後面跟隨的數據也不相同,如果flags此位為1,則ResTable_entry是ResTable_map_entry,ResTable_map_entry繼承自ResTable_entry,其結構如下。
package com.wjdiankong.parseresource.type; /** struct ResTable_map_entry : public ResTable_entry { //指向父ResTable_map_entry的資源ID,如果沒有父ResTable_map_entry,則等於0。 ResTable_ref parent; //等於後面ResTable_map的數量 uint32_t count; }; * @author i * */ public class ResTableMapEntry extends ResTableEntry{ public ResTableRef parent; public int count; public ResTableMapEntry(){ parent = new ResTableRef(); } @Override public int getSize(){ return super.getSize() + parent.getSize() + 4; } @Override public String toString(){ return super.toString() + ",parent:"+parent.toString()+",count:"+count; } }ResTable_map_entry其後跟隨則count個ResTable_map類型的數組,ResTable_map的結構如下:
package com.wjdiankong.parseresource.type; /** struct ResTable_map { //bag資源項ID ResTable_ref name; //bag資源項值 Res_value value; }; * @author i * */ public class ResTableMap { public ResTableRef name; public ResValue value; public ResTableMap(){ name = new ResTableRef(); value = new ResValue(); } public int getSize(){ return name.getSize() + value.getSize(); } @Override public String toString(){ return name.toString()+",value:"+value.toString(); } }
實例:
圖中顏色由深到淺就是一個完整的flags為1的資源項,現在就一起來解讀這段數據的含義,這個資源項頭部的大小為0X10,flags為1所以後面跟隨的是ResTable_map數組,名稱沒有在資源項引用池中,沒有父map_entry,有一個ResTable_map。
如果flags此位為0,則ResTable_entry其後跟隨的是一個Res_value,描述一個普通資源的值,Res_value結構如下。
package com.wjdiankong.parseresource.type; import com.wjdiankong.parseresource.ParseResourceUtils; /** 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; }; * @author i * */ public class ResValue { //dataType字段使用的常量 public final static int TYPE_NULL = 0x00; public final static int TYPE_REFERENCE = 0x01; public final static int TYPE_ATTRIBUTE = 0x02; public final static int TYPE_STRING = 0x03; public final static int TYPE_FLOAT = 0x04; public final static int TYPE_DIMENSION = 0x05; public final static int TYPE_FRACTION = 0x06; public final static int TYPE_FIRST_INT = 0x10; public final static int TYPE_INT_DEC = 0x10; public final static int TYPE_INT_HEX = 0x11; public final static int TYPE_INT_BOOLEAN = 0x12; public final static int TYPE_FIRST_COLOR_INT = 0x1c; public final static int TYPE_INT_COLOR_ARGB8 = 0x1c; public final static int TYPE_INT_COLOR_RGB8 = 0x1d; public final static int TYPE_INT_COLOR_ARGB4 = 0x1e; public final static int TYPE_INT_COLOR_RGB4 = 0x1f; public final static int TYPE_LAST_COLOR_INT = 0x1f; public final static int TYPE_LAST_INT = 0x1f; public static final int COMPLEX_UNIT_PX =0, COMPLEX_UNIT_DIP =1, COMPLEX_UNIT_SP =2, COMPLEX_UNIT_PT =3, COMPLEX_UNIT_IN =4, COMPLEX_UNIT_MM =5, COMPLEX_UNIT_SHIFT =0, COMPLEX_UNIT_MASK =15, COMPLEX_UNIT_FRACTION =0, COMPLEX_UNIT_FRACTION_PARENT=1, COMPLEX_RADIX_23p0 =0, COMPLEX_RADIX_16p7 =1, COMPLEX_RADIX_8p15 =2, COMPLEX_RADIX_0p23 =3, COMPLEX_RADIX_SHIFT =4, COMPLEX_RADIX_MASK =3, COMPLEX_MANTISSA_SHIFT =8, COMPLEX_MANTISSA_MASK =0xFFFFFF; public short size; public byte res0; public byte dataType; public int data; public int getSize(){ return 2 + 1 + 1 + 4; } public String getTypeStr(){ switch(dataType){ case TYPE_NULL: return "TYPE_NULL"; case TYPE_REFERENCE: return "TYPE_REFERENCE"; case TYPE_ATTRIBUTE: return "TYPE_ATTRIBUTE"; case TYPE_STRING: return "TYPE_STRING"; case TYPE_FLOAT: return "TYPE_FLOAT"; case TYPE_DIMENSION: return "TYPE_DIMENSION"; case TYPE_FRACTION: return "TYPE_FRACTION"; case TYPE_FIRST_INT: return "TYPE_FIRST_INT"; case TYPE_INT_HEX: return "TYPE_INT_HEX"; case TYPE_INT_BOOLEAN: return "TYPE_INT_BOOLEAN"; case TYPE_FIRST_COLOR_INT: return "TYPE_FIRST_COLOR_INT"; case TYPE_INT_COLOR_RGB8: return "TYPE_INT_COLOR_RGB8"; case TYPE_INT_COLOR_ARGB4: return "TYPE_INT_COLOR_ARGB4"; case TYPE_INT_COLOR_RGB4: return "TYPE_INT_COLOR_RGB4"; } return ""; } /*public String getDataStr(){ if(dataType == TYPE_STRING){ return ParseResourceUtils.getResString(data); }else if(dataType == TYPE_FIRST_COLOR_INT){ return Utils.bytesToHexString(Utils.int2Byte(data)); }else if(dataType == TYPE_INT_BOOLEAN){ return data==0 ? "false" : "true"; } return data+""; }*/ public String getDataStr() { if (dataType == TYPE_STRING) { return ParseResourceUtils.getResString(data); } if (dataType == TYPE_ATTRIBUTE) { return String.format("?%s%08X",getPackage(data),data); } if (dataType == TYPE_REFERENCE) { return String.format("@%s%08X",getPackage(data),data); } if (dataType == TYPE_FLOAT) { return String.valueOf(Float.intBitsToFloat(data)); } if (dataType == TYPE_INT_HEX) { return String.format("0x%08X",data); } if (dataType == TYPE_INT_BOOLEAN) { return data!=0?"true":"false"; } if (dataType == TYPE_DIMENSION) { return Float.toString(complexToFloat(data))+ DIMENSION_UNITS[data & COMPLEX_UNIT_MASK]; } if (dataType == TYPE_FRACTION) { return Float.toString(complexToFloat(data))+ FRACTION_UNITS[data & COMPLEX_UNIT_MASK]; } if (dataType >= TYPE_FIRST_COLOR_INT && dataType <= TYPE_LAST_COLOR_INT) { return String.format("#%08X",data); } if (dataType >= TYPE_FIRST_INT && dataType <= TYPE_LAST_INT) { return String.valueOf(data); } return String.format("<0x%X, type 0x%02X>",data, dataType); } private static String getPackage(int id) { if (id>>>24==1) { return "android:"; } return ""; } public static float complexToFloat(int complex) { return (float)(complex & 0xFFFFFF00)*RADIX_MULTS[(complex>>4) & 3]; } private static final float RADIX_MULTS[]={ 0.00390625F,3.051758E-005F,1.192093E-007F,4.656613E-010F }; private static final String DIMENSION_UNITS[]={ "px","dip","sp","pt","in","mm","","" }; private static final String FRACTION_UNITS[]={ "%","%p","","","","","","" }; @Override public String toString(){ return "size:"+size+",res0:"+res0+",dataType:"+getTypeStr()+",data:"+getDataStr(); } }
size:ResValue的頭部大小
res0:保留,始終為0
dataType:數據的類型,可以從上面的枚舉類型中獲取
data:數據對應的索引
這裡我們看到了有一個轉化的方法,這個我們在解析AndroidManifest文件的時候也用到了這個方法。
實例:
圖中畫紅線的部分就是一個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源碼的方法。
因為篇幅的原因,這裡就不把所有的代碼都粘貼出來了,後面會列出來代碼下載地址
package com.wjdiankong.parseresource; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; public class ParseResourceMain { public static void main(String[] args){ byte[] srcByte = null; FileInputStream fis = null; ByteArrayOutputStream bos = null; try{ fis = new FileInputStream("resource/resources_gdt1.arsc"); bos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while((len=fis.read(buffer)) != -1){ bos.write(buffer, 0, len); } srcByte = bos.toByteArray(); }catch(Exception e){ System.out.println("read res file error:"+e.toString()); }finally{ try{ fis.close(); bos.close(); }catch(Exception e){ System.out.println("close file error:"+e.toString()); } } if(srcByte == null){ System.out.println("get src error..."); return; } System.out.println("parse restable header..."); ParseResourceUtils.parseResTableHeaderChunk(srcByte); System.out.println("++++++++++++++++++++++++++++++++++++++"); System.out.println(); System.out.println("parse resstring pool chunk..."); ParseResourceUtils.parseResStringPoolChunk(srcByte); System.out.println("++++++++++++++++++++++++++++++++++++++"); System.out.println(); System.out.println("parse package chunk..."); ParseResourceUtils.parsePackage(srcByte); System.out.println("++++++++++++++++++++++++++++++++++++++"); System.out.println(); System.out.println("parse typestring pool chunk..."); ParseResourceUtils.parseTypeStringPoolChunk(srcByte); System.out.println("++++++++++++++++++++++++++++++++++++++"); System.out.println(); System.out.println("parse keystring pool chunk..."); ParseResourceUtils.parseKeyStringPoolChunk(srcByte); System.out.println("++++++++++++++++++++++++++++++++++++++"); System.out.println(); int resCount = 0; while(!ParseResourceUtils.isEnd(srcByte.length)){ resCount++; boolean isSpec = ParseResourceUtils.isTypeSpec(srcByte); if(isSpec){ System.out.println("parse restype spec chunk..."); ParseResourceUtils.parseResTypeSpec(srcByte); System.out.println("++++++++++++++++++++++++++++++++++++++"); System.out.println(); }else{ System.out.println("parse restype info chunk..."); ParseResourceUtils.parseResTypeInfo(srcByte); System.out.println("++++++++++++++++++++++++++++++++++++++"); System.out.println(); } } System.out.println("res count:"+resCount); } }我們看到代碼,首先我們讀取resource.arsc文件到一個byte數組,然後開始解析。
/** * 解析頭部信息 * @param src */ public static void parseResTableHeaderChunk(byte[] src){ ResTableHeader resTableHeader = new ResTableHeader(); resTableHeader.header = parseResChunkHeader(src, 0); resStringPoolChunkOffset = resTableHeader.header.headerSize; //解析PackageCount個數(一個apk可能包含多個Package資源) byte[] packageCountByte = Utils.copyByte(src, resTableHeader.header.getHeaderSize(), 4); resTableHeader.packageCount = Utils.byte2int(packageCountByte); }解析結果:
/** * 解析Resource.arsc文件中所有字符串內容 * @param src */ public static void parseResStringPoolChunk(byte[] src){ ResStringPoolHeader stringPoolHeader = parseStringPoolChunk(src, resStringList, resStringPoolChunkOffset); packageChunkOffset = resStringPoolChunkOffset + stringPoolHeader.header.size; }這裡有一個核心的方法:parseStringPoolChunk
/** * 統一解析字符串內容 * @param src * @param stringList * @param stringOffset * @return */ public static ResStringPoolHeader parseStringPoolChunk(byte[] src, ArrayListstringList, int stringOffset){ ResStringPoolHeader stringPoolHeader = new ResStringPoolHeader(); //解析頭部信息 stringPoolHeader.header = parseResChunkHeader(src, stringOffset); System.out.println("header size:"+stringPoolHeader.header.headerSize); System.out.println("size:"+stringPoolHeader.header.size); int offset = stringOffset + stringPoolHeader.header.getHeaderSize(); //獲取字符串的個數 byte[] stringCountByte = Utils.copyByte(src, offset, 4); stringPoolHeader.stringCount = Utils.byte2int(stringCountByte); //解析樣式的個數 byte[] styleCountByte = Utils.copyByte(src, offset+4, 4); stringPoolHeader.styleCount = Utils.byte2int(styleCountByte); //這裡表示字符串的格式:UTF-8/UTF-16 byte[] flagByte = Utils.copyByte(src, offset+8, 4); System.out.println("flag:"+Utils.bytesToHexString(flagByte)); stringPoolHeader.flags = Utils.byte2int(flagByte); //字符串內容的開始位置 byte[] stringStartByte = Utils.copyByte(src, offset+12, 4); stringPoolHeader.stringsStart = Utils.byte2int(stringStartByte); System.out.println("string start:"+Utils.bytesToHexString(stringStartByte)); //樣式內容的開始位置 byte[] sytleStartByte = Utils.copyByte(src, offset+16, 4); stringPoolHeader.stylesStart = Utils.byte2int(sytleStartByte); System.out.println("style start:"+Utils.bytesToHexString(sytleStartByte)); //獲取字符串內容的索引數組和樣式內容的索引數組 int[] stringIndexAry = new int[stringPoolHeader.stringCount]; int[] styleIndexAry = new int[stringPoolHeader.styleCount]; System.out.println("string count:"+stringPoolHeader.stringCount); System.out.println("style count:"+stringPoolHeader.styleCount); int stringIndex = offset + 20; for(int i=0;i 看到這裡,我們發現這裡的解析很復雜的,和我們在講解數據結構的時候那裡一樣,他需要解析很多內容:
ResValue,ResTableMap,ResTableMapEntry,ResTableEntry,ResConfig
關於每個數據結構如何解析這裡就不多說了,就是讀取字節即可。這裡有一個核心的代碼:
//這裡需要注意的是,先判斷entry的flag變量是否為1,如果為1的話,那就ResTable_map_entry if(entry.flags == 1){ //這裡是復雜類型的value ResTableMapEntry mapEntry = new ResTableMapEntry(); mapEntry = parseResMapEntry(Utils.copyByte(src, valueOffset, mapEntry.getSize())); System.out.println("map entry:"+mapEntry); ResTableMap resMap = new ResTableMap(); for(int j=0;j判斷flag的值,來進行不同的解析操作。這裡需要注意這點。
解析結果:
看到解析結果,還是挺欣慰的,因為最難的地方我們解析成功了,而且看到結果我們很激動,就是我們想要的結果,但是這裡需要解釋的是,有了這些值我們構建public.xml內容和各個類型的xml內容是很簡單,當然這裡我們去構建了,感興趣的同學可以去嘗試一下。
注意:這裡的ResId的構造方法是:
/** * 獲取資源id * 這裡高位是packid,中位是restypeid,地位是entryid * @param entryid * @return */ public static int getResId(int entryid){ return (((packId)<<24) | (((resTypeId) & 0xFF)<<16) | (entryid & 0xFFFF)); }這裡我們可以看到就是一個int類型的resId,
他的最高兩個字節表示packId,系統資源id是:0x01,普通應用資源id是:0x7F
他的中間的兩個字節表示resTypeId,類型id,這個值從0開始,比如我們例子中第一個類型是attr,那麼他的resTypeId就是00
他的最低四個字節表示這個資源的順序id,從1開始,逐漸累加1
五、技術概述
上面我們就很蛋疼的解析完了所有的resource.arsc文件,當然內容有點多,所以有些地方可能沒介紹清楚或者是有錯誤的地方,請多指正。當然關於Android編譯之後的四個文件格式,我們已經介紹了三個了:
so文件格式、AndroidManifest.xml格式/資源文件.xml、resource.arsc
那麼剩下就只有classes.dex這一個文件格式了,我們就算大功告成了。但是我想在這裡說的是,這篇文章我們主要是介紹解析resource.arsc文件格式,那麼寫這篇文章的目的是什麼呢?
有兩個:
1、我們在使用apktool工具進行反編譯的時候,經常出現一些莫名的一場信息,最多的就是NotFound ResId 0x0000XXX這些內容,那麼這時候我們就可以去修復了,當然我們可以得到apktool的源碼來解決這個問題,還可以就是使用我們自己寫的這套解析代碼也是可以的。
2、我們之前提過,解析resource.arsc文件之後,對resource.arsc文件格式如果有了解了之後,可以對資源文件名進行混淆,從而來減小apk包大小,我在之前的一篇文章
因為META-INF文件夾下的三個文件大小很大,原因就是他們內部保存了每個資源名稱,我們在項目中有時候為了不造成沖突,就把資源名起的很長,那麼這樣就會導致apk的包很大。
同樣resource.arsc文件也會很大,因為資源名都是需要保存的,但是我們知道Android中的混淆是不會對資源文件進行混淆的,所以這時候我們就可以通過這個思路來減小包apk的大小了。這個後續我會繼續講解的。
六、總結
這篇文章篇幅有點長,所以我寫的很蛋疼,但是得耐心的看,因為resource.arsc文件格式比AndroidManifest.xml文件格式復雜得多,所以解析起來很費勁的。也希望你們看完之後能多多支持,後面還有一篇解析classes.dex文件格式,當然這篇文章要等年後來才能動筆了,所以盡請期待,最好注大家新年快樂~~
Android反編譯和二次打包實戰 作為Android開發者,工作中少不了要反編譯別人的apk,當然主要目的還是為了學習到更多,取彼之長,補己之短。今天就來總結一下A
ERROR2003 10060引發的MySql用不了綠色版的MYSQL啟動時的問題:ERROR2003 10060引發的MySql起不來。 具體情形: ERROR 200
Andriod React Native 樣式表中可用樣式屬性 寫了這麼多篇Android React Native的博文,基本上把復雜的東西都搞定了,接下來來看看一
Android基礎入門教程——8.4.3 Android動畫合集之屬性動畫-初見 Android基礎入門教程——8.4.3 Android動畫