編輯:關於android開發
今天又是周六了,閒來無事,只能寫文章了呀,今天我們繼續來看逆向的相關知識,我們今天來介紹一下Android中的AndroidManifest文件格式的內容,有的同學可能好奇了,AndroidManifest文件格式有啥好說的呢?不會是介紹那些標簽和屬性是怎麼用的吧?那肯定不會,介紹那些知識有點無聊了,而且和我們的逆向也沒關系,我們今天要介紹的是Android中編譯之後的AndroidManifest文件的格式,首先來腦補一個知識點,Android中的Apk程序其實就是一個壓縮包,我們可以用壓縮軟件進行解壓的:
我們可以看到這裡有三個文件我們後續都會做詳細的解讀的:AndroidManifest.xml,classes.dex,resources.arsc
其實說到這裡只要反編譯過apk的同學都知道一個工具apktool,那麼其實他的工作原理就是解析這三個文件格式,因為本身Android在編譯成apk之後,這個文件有自己的格式,用普通文本格式打開的話是亂碼的,看不懂的,所以需要解析他們成我們能看懂的東東,所以從這篇文章開始,陸續介紹這三個文件的格式解析,這樣我們在後面反編譯apk的時候,遇到錯誤能夠精確的定位到問題。
今天我們先來看一下AndroidManifest.xml格式:
如果我們這裡顯示全是16進制的內容,所以我們需要解析,就像我之前解析so文件一樣:
任何一個文件都一定有他自己的格式,既然編譯成apk之後,變成這樣了,那麼google就是給AndroidManifest定義了一種文件格式,我們只需要知道這種格式的話,就可以詳細的解析出來文件了:
看到此圖是不是又很激動呢?這又是一張神圖,詳細的解析了AndroidManifest.xml文件的格式,但是光看這張圖我們可以看不出來啥,所以要結合一個案例來解析一個文件,這樣才能理解透徹,但是這樣圖是根基,下面我們就用一個案例來解析一下吧:
案例到處都是,誰便搞一個簡單的apk,用壓縮文件打開,解壓出AndroidManifest.xml就可以了,然後就開始讀取內容進行解析:
任何一個文件格式,都會有頭部信息的,而且頭部信息也很重要,同時,頭部一般都是固定格式的。
這裡的頭部信息還有這些字段信息:
1、文件魔數:四個字節
2、文件大小:四個字節
下面就開始解析所有的Chunk內容了,其實每個Chunk的內容都有一個相似點,就是頭部信息:
ChunkType(四個字節)和ChunkSize(四個字節)
這個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(" "); 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這裡我們需要解釋的幾個點:(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(); ArrayListnewByte = new ArrayList (); for(int i=0;i 其實邏輯很簡單,就是過濾空字符串:在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); ArrayListresourceIdList = new ArrayList (resourceIdByte.length/4); for(int i=0;i 解析結果:
我們看到這裡解析出來的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,不了解的同學可以閱讀這篇文章
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 && prefixIndexattrList = new ArrayList(attrCount); for(int i=0;i> 24); attrData.type = value; break; case 4: attrData.data = value; break; } values[j] = value; } attrList.add(attrData); } for(int i=0;i代碼有點長,我們來分析一下: 解析屬性:
//解析屬性 //這裡需要注意的是每個屬性單元都是由五個元素組成,每個元素占用四個字節:namespaceuri, name, valuestring, type, data //在獲取到type值的時候需要右移24位 ArrayList attrList = new ArrayList(attrCount); for(int i=0;i> 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
但是我們在解析的時候,我們需要做一個循環操作:
因為我們知道,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
從項目結構我們可以發現,他用的是Android中自帶的Pull解析xml的,主函數是:
五、為什麼要寫這篇文章
那麼現在我們也可以不用這個工具了,因為我們自己也寫了一個工具解析,是不是很吊吊的呢?那麼我們這篇文章僅僅是為了解析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文件格式的文章。謝謝~~
PS: 關注微信,最新Android技術實時推送
android編譯系統學習,android編譯學習近日接手了後續android新平台項目搭建的任務。 本文內容基於sprd公司提供的android5.1源碼。 一、一般的
Android 內存洩漏的幾種可能總結 Java是垃圾回收語言的一種,其優點是開發者無需特意管理內存分配,降低了應用由於局部故障(segmentation
谷歌電子市場3--應用,谷歌電子市場3-- public class AppFragment extends BaseFragment { Arr
[android] 安卓自定義樣式和主題,android安卓簡單練習自定義樣式和主題,樣式是加在View上,主題是加在Application或者Activity上 sty