致謝:感謝看雪論壇的MindMac大神提供的資料:http://bbs.pediy.com/thread-194206.htm
一、前言
今天又是週六了,閒來無事,只能寫文章了呀,今天我們繼續來看逆向的相關知識,我們今天來介紹一下Android中的AndroidManifest文件格式的內容,有的同學可能好奇了,AndroidManifest文件格式有啥好說的呢?不會是介紹那些標籤和屬性是怎麼用的吧?那肯定不會,介紹那些知識有點無聊了,而且和我們的逆向也沒關係,我們今天要介紹的是Android中編譯之後的AndroidManifest文件的格式,首先來腦補一個知識點,Android中的Apk程序其實就是一個壓縮包,我們可以用壓縮軟件進行解壓的:
二、技術介紹
我們可以看到這裏有三個文件我們後續都會做詳細的解讀的:AndroidManifest.xml,classes.dex,resources.arsc
其實說到這裏只要反編譯過apk的同學都知道一個工具apktool,那麼其實他的工作原理就是解析這三個文件格式,因爲本身Android在編譯成apk之後,這個文件有自己的格式,用普通文本格式打開的話是亂碼的,看不懂的,所以需要解析他們成我們能看懂的東東,所以從這篇文章開始,陸續介紹這三個文件的格式解析,這樣我們在後面反編譯apk的時候,遇到錯誤能夠精確的定位到問題。
今天我們先來看一下AndroidManifest.xml格式:
如果我們這裏顯示全是16進制的內容,所以我們需要解析,就像我之前解析so文件一樣:
http://blog.csdn.net/jiangwei0910410003/article/details/49336613
任何一個文件都一定有他自己的格式,既然編譯成apk之後,變成這樣了,那麼google就是給AndroidManifest定義了一種文件格式,我們只需要知道這種格式的話,就可以詳細的解析出來文件了:
看到此圖是不是又很激動呢?這又是一張神圖(看雪大神傑作,盜圖),詳細的解析了AndroidManifest.xml文件的格式,但是光看這張圖我們可以看不出來啥,所以要結合一個案例來解析一個文件,這樣才能理解透徹,但是這樣圖是根基,下面我們就用一個案例來解析一下吧:
案例到處都是,誰便搞一個簡單的apk,用壓縮文件打開,解壓出AndroidManifest.xml就可以了,然後就開始讀取內容進行解析:
三、格式解析
第一、頭部信息
任何一個文件格式,都會有頭部信息的,而且頭部信息也很重要,同時,頭部一般都是固定格式的。
這裏的頭部信息還有這些字段信息:
1、文件魔數:四個字節
2、文件大小:四個字節
下面就開始解析所有的Chunk內容了,其實每個Chunk的內容都有一個相似點,就是頭部信息:
ChunkType(四個字節)和ChunkSize(四個字節)
第二、String Chunk內容
這個Chunk主要存放的是AndroidManifest文件中所有的字符串信息
1、ChunkType:StringChunk的類型,固定四個字節:0x001C0001
2、ChunkSize:StringChunk的大小,四個字節
3、StringCount:StringChunk中字符串的個數,四個字節
4、StyleCount:StringChunk中樣式的個數,四個字節,但是在實際解析過程中,這個值一直是0x00000000
5、Unknown:位置區域,四個字節,在解析的過程中,這裏需要略過四個字節
6、StringPoolOffset:字符串池的偏移值,四個字節,這個偏移值是相對於StringChunk的頭部位置
7、StylePoolOffset:樣式池的偏移值,四個字節,這裏沒有Style,所以這個字段可忽略
8、StringOffsets:每個字符串的偏移值,所以他的大小應該是:StringCount*4個字節
9、SytleOffsets:每個樣式的偏移值,所以他的大小應該是SytleCount*4個字節
後面就開始是字符串內容和樣式內容了。
下面我們就開始來看代碼了,由於代碼的篇幅有點長,所以這裏就分段說明,代碼的整個工程,後面我會給出下載地址的,
1、首先我們需要把AndroidManifest.xml文件讀入到一個byte數組中:
- byte[] byteSrc = null;
- FileInputStream fis = null;
- ByteArrayOutputStream bos = null;
- try{
- fis = new FileInputStream("xmltest/AndroidManifest1.xml");
- bos = new ByteArrayOutputStream();
- byte[] buffer = new byte[1024];
- int len = 0;
- while((len=fis.read(buffer)) != -1){
- bos.write(buffer, 0, len);
- }
- byteSrc = bos.toByteArray();
- }catch(Exception e){
- System.out.println("parse xml error:"+e.toString());
- }finally{
- try{
- fis.close();
- bos.close();
- }catch(Exception e){
-
- }
- }
2、下面我們就來看看解析頭部信息:
- /**
- * 解析xml的頭部信息
- * @param byteSrc
- */
- public static void parseXmlHeader(byte[] byteSrc){
- byte[] xmlMagic = Utils.copyByte(byteSrc, 0, 4);
- System.out.println("magic number:"+Utils.bytesToHexString(xmlMagic));
- byte[] xmlSize = Utils.copyByte(byteSrc, 4, 4);
- System.out.println("xml size:"+Utils.bytesToHexString(xmlSize));
-
- xmlSb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
- xmlSb.append("\n");
- }
這裏沒什麼說的,按照上面我們說的那個格式解析即可3、解析StringChunk信息
- /**
- * 解析StringChunk
- * @param byteSrc
- */
- public static void parseStringChunk(byte[] byteSrc){
- //String Chunk的標示
- byte[] chunkTagByte = Utils.copyByte(byteSrc, stringChunkOffset, 4);
- System.out.println("string chunktag:"+Utils.bytesToHexString(chunkTagByte));
- //String Size
- byte[] chunkSizeByte = Utils.copyByte(byteSrc, 12, 4);
- //System.out.println(Utils.bytesToHexString(chunkSizeByte));
- int chunkSize = Utils.byte2int(chunkSizeByte);
- System.out.println("chunk size:"+chunkSize);
- //String Count
- byte[] chunkStringCountByte = Utils.copyByte(byteSrc, 16, 4);
- int chunkStringCount = Utils.byte2int(chunkStringCountByte);
- System.out.println("count:"+chunkStringCount);
-
- stringContentList = new ArrayList<String>(chunkStringCount);
-
- //這裏需要注意的是,後面的四個字節是Style的內容,然後緊接着的四個字節始終是0,所以我們需要直接過濾這8個字節
- //String Offset 相對於String Chunk的起始位置0x00000008
- byte[] chunkStringOffsetByte = Utils.copyByte(byteSrc, 28, 4);
-
- int stringContentStart = 8 + Utils.byte2int(chunkStringOffsetByte);
- System.out.println("start:"+stringContentStart);
-
- //String Content
- byte[] chunkStringContentByte = Utils.copyByte(byteSrc, stringContentStart, chunkSize);
-
- /**
- * 在解析字符串的時候有個問題,就是編碼:UTF-8和UTF-16,如果是UTF-8的話是以00結尾的,如果是UTF-16的話以00 00結尾的
- */
-
- /**
- * 此處代碼是用來解析AndroidManifest.xml文件的
- */
- //這裏的格式是:偏移值開始的兩個字節是字符串的長度,接着是字符串的內容,後面跟着兩個字符串的結束符00
- byte[] firstStringSizeByte = Utils.copyByte(chunkStringContentByte, 0, 2);
- //一個字符對應兩個字節
- int firstStringSize = Utils.byte2Short(firstStringSizeByte)*2;
- System.out.println("size:"+firstStringSize);
- byte[] firstStringContentByte = Utils.copyByte(chunkStringContentByte, 2, firstStringSize+2);
- String firstStringContent = new String(firstStringContentByte);
- stringContentList.add(Utils.filterStringNull(firstStringContent));
- System.out.println("first string:"+Utils.filterStringNull(firstStringContent));
-
- //將字符串都放到ArrayList中
- int endStringIndex = 2+firstStringSize+2;
- while(stringContentList.size() < chunkStringCount){
- //一個字符對應兩個字節,所以要乘以2
- int stringSize = Utils.byte2Short(Utils.copyByte(chunkStringContentByte, endStringIndex, 2))*2;
- String str = new String(Utils.copyByte(chunkStringContentByte, endStringIndex+2, stringSize+2));
- System.out.println("str:"+Utils.filterStringNull(str));
- stringContentList.add(Utils.filterStringNull(str));
- endStringIndex += (2+stringSize+2);
- }
-
- /**
- * 此處的代碼是用來解析資源文件xml的
- */
- /*int stringStart = 0;
- int index = 0;
- while(index < chunkStringCount){
- byte[] stringSizeByte = Utils.copyByte(chunkStringContentByte, stringStart, 2);
- int stringSize = (stringSizeByte[1] & 0x7F);
- System.out.println("string size:"+Utils.bytesToHexString(Utils.int2Byte(stringSize)));
- if(stringSize != 0){
- //這裏注意是UTF-8編碼的
- String val = "";
- try{
- val = new String(Utils.copyByte(chunkStringContentByte, stringStart+2, stringSize), "utf-8");
- }catch(Exception e){
- System.out.println("string encode error:"+e.toString());
- }
- stringContentList.add(val);
- }else{
- stringContentList.add("");
- }
- stringStart += (stringSize+3);
- index++;
- }
- for(String str : stringContentList){
- System.out.println("str:"+str);
- }*/
-
- resourceChunkOffset = stringChunkOffset + Utils.byte2int(chunkSizeByte);
-
- }
這裏我們需要解釋的幾個點:1、在上面的格式說明中,我們需要注意,有一個Unknow字段,四個字節,所以我們需要略過
2、在解析字符串內容的時候,字符串內容的結束符是:0x0000
3、每個字符串開始的前兩個字節是字符串的長度
所以我們有了每個字符串的偏移值和大小,那麼解析字符串內容就簡單了:
這裏我們看到0x000B(高位和低位相反)就是字符串的大小,結尾是0x0000
一個字符對應的是兩個字節,而且這裏有一個方法:Utils.filterStringNull(firstStringContent):
- public static String filterStringNull(String str){
- if(str == null || str.length() == 0){
- return str;
- }
- byte[] strByte = str.getBytes();
- ArrayList<Byte> newByte = new ArrayList<Byte>();
- for(int i=0;i<strByte.length;i++){
- if(strByte[i] != 0){
- newByte.add(strByte[i]);
- }
- }
- byte[] newByteAry = new byte[newByte.size()];
- for(int i=0;i<newByteAry.length;i++){
- newByteAry[i] = newByte.get(i);
- }
- return new String(newByteAry);
- }
其實邏輯很簡單,就是過濾空字符串:在C語言中是NULL,在Java中就是00,如果不過濾的話,會出現下面的這種情況:
每個字符是寬字符,很難看,其實願意就是每個字符後面多了一個00,所以過濾之後就可以了
這樣就好看多了。
上面我們就解析了AndroidManifest.xml中所有的字符串內容。這裏我們需要用一個全局的字符列表,用來存儲這些字符串的值,後面會用索引來獲取這些字符串的值。
第三、解析ResourceIdChunk
這個Chunk主要是存放的是AndroidManifest中用到的系統屬性值對應的資源Id,比如android:versionCode中的versionCode屬性,android是前綴,後面會說道
1、ChunkType:ResourceIdChunk的類型,固定四個字節:0x00080108
2、ChunkSize:ResourceChunk的大小,四個字節
3、ResourceIds:ResourceId的內容,這裏大小是ResourceChunk大小除以4,減去頭部的大小8個字節(ChunkType和ChunkSize)
- /**
- * 解析Resource Chunk
- * @param byteSrc
- */
- public static void parseResourceChunk(byte[] byteSrc){
- byte[] chunkTagByte = Utils.copyByte(byteSrc, resourceChunkOffset, 4);
- System.out.println(Utils.bytesToHexString(chunkTagByte));
- byte[] chunkSizeByte = Utils.copyByte(byteSrc, resourceChunkOffset+4, 4);
- int chunkSize = Utils.byte2int(chunkSizeByte);
- System.out.println("chunk size:"+chunkSize);
- //這裏需要注意的是chunkSize是包含了chunkTag和chunkSize這兩個字節的,所以需要剔除
- byte[] resourceIdByte = Utils.copyByte(byteSrc, resourceChunkOffset+8, chunkSize-8);
- ArrayList<Integer> resourceIdList = new ArrayList<Integer>(resourceIdByte.length/4);
- for(int i=0;i<resourceIdByte.length;i+=4){
- int resId = Utils.byte2int(Utils.copyByte(resourceIdByte, i, 4));
- System.out.println("id:"+resId+",hex:"+Utils.bytesToHexString(Utils.copyByte(resourceIdByte, i, 4)));
- resourceIdList.add(resId);
- }
-
- nextChunkOffset = (resourceChunkOffset+chunkSize);
-
- }
解析結果:
我們看到這裏解析出來的id到底是什麼呢?
這裏需要腦補一個知識點了:
我們在寫Android程序的時候,都會發現有一個R文件,那裏面就是存放着每個資源對應的Id,那麼這些id值是怎麼得到的呢?
Package ID相當於是一個命名空間,限定資源的來源。Android系統當前定義了兩個資源命令空間,其中一個系統資源命令空間,它的Package ID等於0x01,另外一個是應用程序資源命令空間,它的Package ID等於0x7f。所有位於[0x01, 0x7f]之間的Package ID都是合法的,而在這個範圍之外的都是非法的Package ID。前面提到的系統資源包package-export.apk的Package ID就等於0x01,而我們在應用程序中定義的資源的Package ID的值都等於0x7f,這一點可以通過生成的R.java文件來驗證。
Type ID是指資源的類型ID。資源的類型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干種,每一種都會被賦予一個ID。
Entry ID是指每一個資源在其所屬的資源類型中所出現的次序。注意,不同類型的資源的Entry ID有可能是相同的,但是由於它們的類型不同,我們仍然可以通過其資源ID來區別開來。
關於資源ID的更多描述,以及資源的引用關係,可以參考frameworks/base/libs/utils目錄下的README文件
我們可以得知系統資源對應id的xml文件是在哪裏:frameworks\base\core\res\res\values\public.xml
那麼我們用上面解析到的id,去public.xml文件中查詢一下:
查到了,是versionCode,對於這個系統資源id存放文件public.xml還是很重要的,後面在講解resource.arsc文件格式的時候還會繼續用到。
第四、解析StartNamespaceChunk
這個Chunk主要包含一個AndroidManifest文件中的命令空間的內容,Android中的xml都是採用Schema格式的,所以肯定有Prefix和Uri的。
這裏在腦補一個知識點:xml格式有兩種:DTD和Schema,不瞭解的同學可以閱讀這篇文章
http://blog.csdn.net/jiangwei0910410003/article/details/19340975
1、ChunkType:Chunk的類型,固定四個字節:0x00100100
2、ChunkSize:Chunk的大小,四個字節
3、LineNumber:在AndroidManifest文件中的行號,四個字節
4、Unknown:未知區域,四個字節
5、Prefix:命名空間的前綴(在字符串中的索引值),比如:android
6、Uri:命名空間的uri(在字符串中的索引值):比如:http://schemas.android.com/apk/res/android
解析代碼:
- /**
- * 解析StartNamespace Chunk
- * @param byteSrc
- */
- public static void parseStartNamespaceChunk(byte[] byteSrc){
- //獲取ChunkTag
- byte[] chunkTagByte = Utils.copyByte(byteSrc, 0, 4);
- System.out.println(Utils.bytesToHexString(chunkTagByte));
- //獲取ChunkSize
- byte[] chunkSizeByte = Utils.copyByte(byteSrc, 4, 4);
- int chunkSize = Utils.byte2int(chunkSizeByte);
- System.out.println("chunk size:"+chunkSize);
-
- //解析行號
- byte[] lineNumberByte = Utils.copyByte(byteSrc, 8, 4);
- int lineNumber = Utils.byte2int(lineNumberByte);
- System.out.println("line number:"+lineNumber);
-
- //解析prefix(這裏需要注意的是行號後面的四個字節爲FFFF,過濾)
- byte[] prefixByte = Utils.copyByte(byteSrc, 16, 4);
- int prefixIndex = Utils.byte2int(prefixByte);
- String prefix = stringContentList.get(prefixIndex);
- System.out.println("prefix:"+prefixIndex);
- System.out.println("prefix str:"+prefix);
-
- //解析Uri
- byte[] uriByte = Utils.copyByte(byteSrc, 20, 4);
- int uriIndex = Utils.byte2int(uriByte);
- String uri = stringContentList.get(uriIndex);
- System.out.println("uri:"+uriIndex);
- System.out.println("uri str:"+uri);
-
- uriPrefixMap.put(uri, prefix);
- prefixUriMap.put(prefix, uri);
- }
解析的結果如下:
這裏的內容就是上面我們解析完String之後的對應的字符串索引值,這裏我們需要注意的是,一個xml中可能會有多個命名空間,所以這裏我們用Map存儲Prefix和Uri對應的關係,後面在解析節點內容的時候會用到。
第五、StratTagChunk
這個Chunk主要是存放了AndroidManifest.xml中的標籤信息了,也是最核心的內容,當然也是最複雜的內容
1、ChunkType:Chunk的類型,固定四個字節:0x00100102
2、ChunkSize:Chunk的大小,固定四個字節
3、LineNumber:對應於AndroidManifest中的行號,四個字節
4、Unknown:未知領域,四個字節
5、NamespaceUri:這個標籤用到的命名空間的Uri,比如用到了android這個前綴,那麼就需要用http://schemas.android.com/apk/res/android這個Uri去獲取,四個字節
6、Name:標籤名稱(在字符串中的索引值),四個字節
7、Flags:標籤的類型,四個字節,比如是開始標籤還是結束標籤等
8、AttributeCount:標籤包含的屬性個數,四個字節
9、ClassAtrribute:標籤包含的類屬性,四個字節
10,Atrributes:屬性內容,每個屬性算是一個Entry,這個Entry固定大小是大小爲5的字節數組:
[Namespace,Uri,Name,ValueString,Data],我們在解析的時候需要注意第四個值,要做一次處理:需要右移24位。所以這個字段的大小是:屬性個數*5*4個字節
解析代碼:
- /**
- * 解析StartTag Chunk
- * @param byteSrc
- */
- public static void parseStartTagChunk(byte[] byteSrc){
- //解析ChunkTag
- byte[] chunkTagByte = Utils.copyByte(byteSrc, 0, 4);
- System.out.println(Utils.bytesToHexString(chunkTagByte));
-
- //解析ChunkSize
- byte[] chunkSizeByte = Utils.copyByte(byteSrc, 4, 4);
- int chunkSize = Utils.byte2int(chunkSizeByte);
- System.out.println("chunk size:"+chunkSize);
-
- //解析行號
- byte[] lineNumberByte = Utils.copyByte(byteSrc, 8, 4);
- int lineNumber = Utils.byte2int(lineNumberByte);
- System.out.println("line number:"+lineNumber);
-
- //解析prefix
- byte[] prefixByte = Utils.copyByte(byteSrc, 8, 4);
- int prefixIndex = Utils.byte2int(prefixByte);
- //這裏可能會返回-1,如果返回-1的話,那就是說沒有prefix
- if(prefixIndex != -1 && prefixIndex<stringContentList.size()){
- System.out.println("prefix:"+prefixIndex);
- System.out.println("prefix str:"+stringContentList.get(prefixIndex));
- }else{
- System.out.println("prefix null");
- }
-
- //解析Uri
- byte[] uriByte = Utils.copyByte(byteSrc, 16, 4);
- int uriIndex = Utils.byte2int(uriByte);
- if(uriIndex != -1 && prefixIndex<stringContentList.size()){
- System.out.println("uri:"+uriIndex);
- System.out.println("uri str:"+stringContentList.get(uriIndex));
- }else{
- System.out.println("uri null");
- }
-
- //解析TagName
- byte[] tagNameByte = Utils.copyByte(byteSrc, 20, 4);
- System.out.println(Utils.bytesToHexString(tagNameByte));
- int tagNameIndex = Utils.byte2int(tagNameByte);
- String tagName = stringContentList.get(tagNameIndex);
- if(tagNameIndex != -1){
- System.out.println("tag name index:"+tagNameIndex);
- System.out.println("tag name str:"+tagName);
- }else{
- System.out.println("tag name null");
- }
-
- //解析屬性個數(這裏需要過濾四個字節:14001400)
- byte[] attrCountByte = Utils.copyByte(byteSrc, 28, 4);
- int attrCount = Utils.byte2int(attrCountByte);
- System.out.println("attr count:"+attrCount);
-
- //解析屬性
- //這裏需要注意的是每個屬性單元都是由五個元素組成,每個元素佔用四個字節:namespaceuri, name, valuestring, type, data
- //在獲取到type值的時候需要右移24位
- ArrayList<AttributeData> attrList = new ArrayList<AttributeData>(attrCount);
- for(int i=0;i<attrCount;i++){
- Integer[] values = new Integer[5];
- AttributeData attrData = new AttributeData();
- for(int j=0;j<5;j++){
- int value = Utils.byte2int(Utils.copyByte(byteSrc, 36+i*20+j*4, 4));
- switch(j){
- case 0:
- attrData.nameSpaceUri = value;
- break;
- case 1:
- attrData.name = value;
- break;
- case 2:
- attrData.valueString = value;
- break;
- case 3:
- value = (value >> 24);
- attrData.type = value;
- break;
- case 4:
- attrData.data = value;
- break;
- }
- values[j] = value;
- }
- attrList.add(attrData);
- }
-
- for(int i=0;i<attrCount;i++){
- if(attrList.get(i).nameSpaceUri != -1){
- System.out.println("nameSpaceUri:"+stringContentList.get(attrList.get(i).nameSpaceUri));
- }else{
- System.out.println("nameSpaceUri == null");
- }
- if(attrList.get(i).name != -1){
- System.out.println("name:"+stringContentList.get(attrList.get(i).name));
- }else{
- System.out.println("name == null");
- }
- if(attrList.get(i).valueString != -1){
- System.out.println("valueString:"+stringContentList.get(attrList.get(i).valueString));
- }else{
- System.out.println("valueString == null");
- }
- System.out.println("type:"+AttributeType.getAttrType(attrList.get(i).type));
- System.out.println("data:"+AttributeType.getAttributeData(attrList.get(i)));
- }
-
- //這裏開始構造xml結構
- xmlSb.append(createStartTagXml(tagName, attrList));
-
- }
代碼有點長,我們來分析一下:解析屬性:
- //解析屬性
- //這裏需要注意的是每個屬性單元都是由五個元素組成,每個元素佔用四個字節:namespaceuri, name, valuestring, type, data
- //在獲取到type值的時候需要右移24位
- ArrayList<AttributeData> attrList = new ArrayList<AttributeData>(attrCount);
- for(int i=0;i<attrCount;i++){
- Integer[] values = new Integer[5];
- AttributeData attrData = new AttributeData();
- for(int j=0;j<5;j++){
- int value = Utils.byte2int(Utils.copyByte(byteSrc, 36+i*20+j*4, 4));
- switch(j){
- case 0:
- attrData.nameSpaceUri = value;
- break;
- case 1:
- attrData.name = value;
- break;
- case 2:
- attrData.valueString = value;
- break;
- case 3:
- value = (value >> 24);
- attrData.type = value;
- break;
- case 4:
- attrData.data = value;
- break;
- }
- values[j] = value;
- }
- attrList.add(attrData);
- }
看到第四個值的時候,需要額外的處理一下,就是需要右移24位。解析完屬性之後,那麼就可以得到一個標籤的名稱和屬性名稱和屬性值了:
看解析的結果:
標籤manifest包含的屬性:
這裏有幾個問題需要解釋一下:
1、爲什麼我們看到的是三個屬性,但是解析打印的結果是5個?
因爲系統在編譯apk的時候,會添加兩個屬性:platformBuildVersionCode和platformBuildVersionName
這個是發佈的到設備的版本號和版本名稱
這個是解析之後的結果
2、當沒有android這樣的前綴的時候,NamespaceUri是null
3、當dataType不同,對應的data值也是有不同的含義的:
這個方法就是用來轉義的,後面在解析resource.arsc的時候也會用到這個方法。
4、每個屬性理論上都會含有一個NamespaceUri的,這個也決定了屬性的前綴Prefix,默認都是android,但是有時候我們會自定義一個控件的時候,這時候就需要導入NamespaceUri和Prefix了。所以一個xml中可能會有多個Namespace,每個屬性都會包含NamespaceUri的。
其實到這裏我們就算解析完了大部分的工作了,至於還有EndTagChunk,那個和StartTagChunk非常類似,這裏就不在詳解了:
- /**
- * 解析EndTag Chunk
- * @param byteSrc
- */
- public static void parseEndTagChunk(byte[] byteSrc){
- byte[] chunkTagByte = Utils.copyByte(byteSrc, 0, 4);
- System.out.println(Utils.bytesToHexString(chunkTagByte));
- byte[] chunkSizeByte = Utils.copyByte(byteSrc, 4, 4);
- int chunkSize = Utils.byte2int(chunkSizeByte);
- System.out.println("chunk size:"+chunkSize);
-
- //解析行號
- byte[] lineNumberByte = Utils.copyByte(byteSrc, 8, 4);
- int lineNumber = Utils.byte2int(lineNumberByte);
- System.out.println("line number:"+lineNumber);
-
- //解析prefix
- byte[] prefixByte = Utils.copyByte(byteSrc, 8, 4);
- int prefixIndex = Utils.byte2int(prefixByte);
- //這裏可能會返回-1,如果返回-1的話,那就是說沒有prefix
- if(prefixIndex != -1 && prefixIndex<stringContentList.size()){
- System.out.println("prefix:"+prefixIndex);
- System.out.println("prefix str:"+stringContentList.get(prefixIndex));
- }else{
- System.out.println("prefix null");
- }
-
- //解析Uri
- byte[] uriByte = Utils.copyByte(byteSrc, 16, 4);
- int uriIndex = Utils.byte2int(uriByte);
- if(uriIndex != -1 && prefixIndex<stringContentList.size()){
- System.out.println("uri:"+uriIndex);
- System.out.println("uri str:"+stringContentList.get(uriIndex));
- }else{
- System.out.println("uri null");
- }
-
- //解析TagName
- byte[] tagNameByte = Utils.copyByte(byteSrc, 20, 4);
- System.out.println(Utils.bytesToHexString(tagNameByte));
- int tagNameIndex = Utils.byte2int(tagNameByte);
- String tagName = stringContentList.get(tagNameIndex);
- if(tagNameIndex != -1){
- System.out.println("tag name index:"+tagNameIndex);
- System.out.println("tag name str:"+tagName);
- }else{
- System.out.println("tag name null");
- }
-
- xmlSb.append(createEndTagXml(tagName));
- }
但是我們在解析的時候,我們需要做一個循環操作:
因爲我們知道,Android中在解析Xml的時候提供了很多種方式,但是這裏我們沒有用任何一種方式,而是用純代碼編寫的,所以用一個循環,來遍歷解析Tag,其實這種方式類似於SAX解析XML,這時候上面說到的那個Flag字段就大有用途了。
這裏我們還做了一個工作就是將解析之後的xml格式化一下:
難度不大,這裏也就不繼續解釋了,這裏有一個地方需要優化的就是,可以利用LineNumber屬性來,精確到格式化行數,不過這個工作量有點大,這裏就不想做了,有興趣的同學可以考慮一下,格式化完之後的結果:
帥氣不帥氣,把手把手的將之前的16進制的內容解析出來了,吊吊的,成就感爆棚呀~~
這裏有一個問題,就是我們看到這裏還有很多@7F070001這類的東西,這個其實是資源Id,這個需要我們後面解析完resource.arsc文件之後,就可以對應上這個資源了,後面會在提到一下。這裏就知道一下可以了。
這裏其實還有一個問題,就是我們發現這個可以解析AndroidManifest文件了,那麼同樣也可以解析其他的xml文件:
擦,我們發現解析其他xml的時候,發現報錯了,定位代碼發現是在解析StringChunk的地方報錯了,我們修改一下:
因爲其他的xml中的字符串格式和AndroidManifest.xml中的不一樣,所以這裏需要單獨解析一下:
修改之後就可以了。
四、技術拓展
在反編譯的時候,有時候我們只想反編譯AndroidManifest內容,所以ApkTool工具就有點繁瑣了,不過網上有個牛逼的大神已經寫好了這個工具AXMLPrinter.jar,這個工具很好用的:java -jar AXMLPrinter.java xxx.xml >demo.xml
將xxx.xml解析之後輸出到demo.xml中即可
工具下載下載地址:http://download.csdn.net/detail/jiangwei0910410003/9415323
不過這個大神和我一樣有着開源的精神,源代碼下載地址:
http://download.csdn.net/detail/jiangwei0910410003/9415342
從項目結構我們可以發現,他用的是Android中自帶的Pull解析xml的,主函數是:
注意:
到這裏我們還需要告訴一件事,那就是其實我們上面的解析工作,有一個更簡單的方法就可以搞定了?那就是aapt命令?關於這個aapt是幹啥的?網上有很多資料,他其實很簡單就是將Android中的資源文件打包成resource.arsc即可:
只有那些類型爲res/animator、res/anim、res/color、res/drawable(非Bitmap文件,即非.png、.9.png、.jpg、.gif文件)、res/layout、res/menu、res/values和res/xml的資源文件均會從文本格式的XML文件編譯成二進制格式的XML文件
這些XML資源文件之所要從文本格式編譯成二進制格式,是因爲:
1. 二進制格式的XML文件佔用空間更小。這是由於所有XML元素的標籤、屬性名稱、屬性值和內容所涉及到的字符串都會被統一收集到一個字符串資源池中去,並且會去重。有了這個字符串資源池,原來使用字符串的地方就會被替換成一個索引到字符串資源池的整數值,從而可以減少文件的大小。
2. 二進制格式的XML文件解析速度更快。這是由於二進制格式的XML元素裏面不再包含有字符串值,因此就避免了進行字符串解析,從而提高速度。
將XML資源文件從文本格式編譯成二進制格式解決了空間佔用以及解析效率的問題,但是對於Android資源管理框架來說,這只是完成了其中的一部分工作。Android資源管理框架的另外一個重要任務就是要根據資源ID來快速找到對應的資源。
那麼下面我們用aapt命令就可以查看一下?
aapt命令在我們的AndroidSdk目錄中:
看到路徑了:Android-SDK目錄/build-tools/下面
我們也就知道了,這個目錄下全是Android中build成一個apk的所有工具,這裏再看一下這些工具的用途:
1、使用Android SDK提供的aapt.exe生成R.java類文件
2、使用Android SDK提供的aidl.exe把.aidl轉成.java文件(如果沒有aidl,則跳過這一步)
3、使用JDK提供的javac.exe編譯.java類文件生成class文件
4、使用Android SDK提供的dx.bat命令行腳本生成classes.dex文件
5、使用Android SDK提供的aapt.exe生成資源包文件(包括res、assets、androidmanifest.xml等)
6、使用Android SDK提供的apkbuilder.bat生成未簽名的apk安裝文件
7、使用jdk的jarsigner.exe對未簽名的包進行apk簽名
看到了吧。我們原來可以不借助任何IDE工具,也是可以出一個apk包的。哈哈~~
繼續看aapt命令的用法,命令很簡單:
aapt l -a apk名稱 > demo.txt
將輸入的結果定向到demo.txt中
看到我們弄出來的內容,發現就是我們上面解析的AndroidManifest.xml內容,所以這個也是一個方法,當然aapt命令這裏我爲什麼最後說呢?之前我們講解的AndroidManifest.xml格式肯定是有用的,aapt命令只是系統提供給我們一個很好的工具,我們可以在反編譯的過程中藉助這個工具也是不錯的選擇。所以這裏我就想說,以後我們記得有一個aapt命令就好了,他的用途還是很多的,可以單獨編譯成一個resource.arsc文件來,我們後面會用到這個命令。
項目下載地址:https://github.com/fourbrother/parse_androidxml
五、爲什麼要寫這篇文章
那麼現在我們也可以不用這個工具了,因爲我們自己也寫了一個工具解析,是不是很吊吊的呢?那麼我們這篇文章僅僅是爲了解析AndroidManifest嗎?肯定不是,寫這篇文章其實是另有目的的,爲我們後面在反編譯apk做準備,其實現在有很多同學都發現了,在使用apktool來反編譯apk的時候經常報出一些異常信息,其實那些就是加固的人,用來對抗apktool工具的,他們專門找apktool的漏洞,然後進行加固,從而達到反編譯失敗的效果,所以我們有必要了解apktool的源碼和解析原理,這樣才能遇到反編譯失敗的錯誤的時候,能定位到問題,在修復apktool工具即可,那麼apktool的工具解析原理其實很簡單,就是解析AndroidManifest.xml,然後是解析resource.arsc到public.xml(這個文件一般是反編譯之後存放在values文件夾下面的,是整個反編譯之後的工程對應的Id列表),其次就是classes.dex。還有其他的佈局,資源xml等,那麼針對於這幾個問題,我們這篇文章就講解了:解析XML文件的問題。後面還會繼續講解如何解析resource.arsc和classes.dex文件的格式。當然後面我會介紹一篇關於如果通過修改AndroidManifest文件內容來達到加固的效果,以及如何我們做修復來破解這種加固。
六、總結
這篇文章到這裏就算結束了,寫的有點累了,解析代碼已經有下載地址了,有不理解的同學可以聯繫我,加入公衆號,留言問題,我會在適當的時間給予回覆,謝謝,同時記得關注後面的兩篇解析resource.arsc和classes.dex文件格式的文章。謝謝~~
更多內容:點擊這裏
關注微信公衆號,最新技術乾貨實時推送