Android逆向之旅---解析編譯以後的AndroidManifest文件格式

1、前言

今天又是週六了,閒來無事,只能寫文章了呀,今天咱們繼續來看逆向的相關知識,咱們今天來介紹一下Android中的AndroidManifest文件格式的內容,有的同窗可能好奇了,AndroidManifest文件格式有啥好說的呢?不會是介紹那些標籤和屬性是怎麼用的吧?那確定不會,介紹那些知識有點無聊了,並且和咱們的逆向也不要緊,咱們今天要介紹的是Android中編譯以後的AndroidManifest文件的格式,首先來腦補一個知識點,Android中的Apk程序其實就是一個壓縮包,咱們能夠用壓縮軟件進行解壓的:java



2、技術介紹

咱們能夠看到這裏有三個文件咱們後續都會作詳細的解讀的:AndroidManifest.xml,classes.dex,resources.arscandroid

其實說到這裏只要反編譯過apk的同窗都知道一個工具apktool,那麼其實他的工做原理就是解析這三個文件格式,由於自己Android在編譯成apk以後,這個文件有本身的格式,用普通文本格式打開的話是亂碼的,看不懂的,因此須要解析他們成咱們能看懂的東東,因此從這篇文章開始,陸續介紹這三個文件的格式解析,這樣咱們在後面反編譯apk的時候,遇到錯誤可以精確的定位到問題。數組

今天咱們先來看一下AndroidManifest.xml格式:微信


若是咱們這裏顯示全是16進制的內容,因此咱們須要解析,就像我以前解析so文件同樣:app

http://blog.csdn.net/jiangwei0910410003/article/details/49336613
框架

任何一個文件都必定有他本身的格式,既然編譯成apk以後,變成這樣了,那麼google就是給AndroidManifest定義了一種文件格式,咱們只須要知道這種格式的話,就能夠詳細的解析出來文件了:函數


看到此圖是否是又很激動呢?這又是一張神圖,詳細的解析了AndroidManifest.xml文件的格式,可是光看這張圖咱們能夠看不出來啥,因此要結合一個案例來解析一個文件,這樣才能理解透徹,可是這樣圖是根基,下面咱們就用一個案例來解析一下吧:工具

案例處處都是,誰便搞一個簡單的apk,用壓縮文件打開,解壓出AndroidManifest.xml就能夠了,而後就開始讀取內容進行解析:佈局


3、格式解析

第1、頭部信息

任何一個文件格式,都會有頭部信息的,並且頭部信息也很重要,同時,頭部通常都是固定格式的。優化


這裏的頭部信息還有這些字段信息:

一、文件魔數:四個字節

二、文件大小:四個字節


下面就開始解析全部的Chunk內容了,其實每一個Chunk的內容都有一個類似點,就是頭部信息:

ChunkType(四個字節)和ChunkSize(四個字節)


第2、String Chunk內容

這個Chunk主要存放的是AndroidManifest文件中全部的字符串信息


一、ChunkType:StringChunk的類型,固定四個字節:0x001C0001

二、ChunkSize:StringChunk的大小,四個字節

三、StringCount:StringChunk中字符串的個數,四個字節

四、StyleCount:StringChunk中樣式的個數,四個字節,可是在實際解析過程當中,這個值一直是0x00000000

五、Unknown:位置區域,四個字節,在解析的過程當中,這裏須要略過四個字節

六、StringPoolOffset:字符串池的偏移值,四個字節,這個偏移值是相對於StringChunk的頭部位置

七、StylePoolOffset:樣式池的偏移值,四個字節,這裏沒有Style,因此這個字段可忽略

八、StringOffsets:每一個字符串的偏移值,因此他的大小應該是:StringCount*4個字節

九、SytleOffsets:每一個樣式的偏移值,因此他的大小應該是SytleCount*4個字節

後面就開始是字符串內容和樣式內容了。


下面咱們就開始來看代碼了,因爲代碼的篇幅有點長,因此這裏就分段說明,代碼的整個工程,後面我會給出下載地址的,

一、首先咱們須要把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){

	}
}

二、下面咱們就來看看解析頭部信息:

/**
 * 解析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");
}
這裏沒什麼說的,按照上面咱們說的那個格式解析便可



三、解析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);

}
這裏咱們須要解釋的幾個點:

一、在上面的格式說明中,咱們須要注意,有一個Unknow字段,四個字節,因此咱們須要略過

二、在解析字符串內容的時候,字符串內容的結束符是:0x0000

三、每一個字符串開始的前兩個字節是字符串的長度

因此咱們有了每一個字符串的偏移值和大小,那麼解析字符串內容就簡單了:


這裏咱們看到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中全部的字符串內容。這裏咱們須要用一個全局的字符列表,用來存儲這些字符串的值,後面會用索引來獲取這些字符串的值。


第3、解析ResourceIdChunk

這個Chunk主要是存放的是AndroidManifest中用到的系統屬性值對應的資源Id,好比android:versionCode中的versionCode屬性,android是前綴,後面會說道


一、ChunkType:ResourceIdChunk的類型,固定四個字節:0x00080108

二、ChunkSize:ResourceChunk的大小,四個字節

三、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文件格式的時候還會繼續用到。


第4、解析StartNamespaceChunk

這個Chunk主要包含一個AndroidManifest文件中的命令空間的內容,Android中的xml都是採用Schema格式的,因此確定有Prefix和Uri的。

這裏在腦補一個知識點:xml格式有兩種:DTD和Schema,不瞭解的同窗能夠閱讀這篇文章

http://blog.csdn.net/jiangwei0910410003/article/details/19340975

一、ChunkType:Chunk的類型,固定四個字節:0x00100100

二、ChunkSize:Chunk的大小,四個字節

三、LineNumber:在AndroidManifest文件中的行號,四個字節

四、Unknown:未知區域,四個字節

五、Prefix:命名空間的前綴(在字符串中的索引值),好比:android

六、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對應的關係,後面在解析節點內容的時候會用到。


第5、StratTagChunk

這個Chunk主要是存放了AndroidManifest.xml中的標籤信息了,也是最核心的內容,固然也是最複雜的內容


一、ChunkType:Chunk的類型,固定四個字節:0x00100102

二、ChunkSize:Chunk的大小,固定四個字節

三、LineNumber:對應於AndroidManifest中的行號,四個字節

四、Unknown:未知領域,四個字節

五、NamespaceUri:這個標籤用到的命名空間的Uri,好比用到了android這個前綴,那麼就須要用http://schemas.android.com/apk/res/android這個Uri去獲取,四個字節

六、Name:標籤名稱(在字符串中的索引值),四個字節

七、Flags:標籤的類型,四個字節,好比是開始標籤仍是結束標籤等

八、AttributeCount:標籤包含的屬性個數,四個字節

九、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包含的屬性:


這裏有幾個問題須要解釋一下:

一、爲何咱們看到的是三個屬性,可是解析打印的結果是5個?

由於系統在編譯apk的時候,會添加兩個屬性:platformBuildVersionCode和platformBuildVersionName

這個是發佈的到設備的版本號和版本名稱


這個是解析以後的結果

二、當沒有android這樣的前綴的時候,NamespaceUri是null


三、當dataType不一樣,對應的data值也是有不一樣的含義的:


這個方法就是用來轉義的,後面在解析resource.arsc的時候也會用到這個方法。

四、每一個屬性理論上都會含有一個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中的不同,因此這裏須要單獨解析一下:


修改以後就能夠了。


4、技術拓展

在反編譯的時候,有時候咱們只想反編譯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的全部工具,這裏再看一下這些工具的用途:


一、使用Android SDK提供的aapt.exe生成R.java類文件
二、使用Android SDK提供的aidl.exe把.aidl轉成.java文件(若是沒有aidl,則跳過這一步)
三、使用JDK提供的javac.exe編譯.java類文件生成class文件
四、使用Android SDK提供的dx.bat命令行腳本生成classes.dex文件
五、使用Android SDK提供的aapt.exe生成資源包文件(包括res、assets、androidmanifest.xml等)
六、使用Android SDK提供的apkbuilder.bat生成未簽名的apk安裝文件
七、使用jdk的jarsigner.exe對未簽名的包進行apk簽名

看到了吧。咱們原來能夠不借助任何IDE工具,也是能夠出一個apk包的。哈哈~~

繼續看aapt命令的用法,命令很簡單:

aapt l -a apk名稱 > demo.txt

將輸入的結果定向到demo.txt中


看到咱們弄出來的內容,發現就是咱們上面解析的AndroidManifest.xml內容,因此這個也是一個方法,固然aapt命令這裏我爲何最後說呢?以前咱們講解的AndroidManifest.xml格式確定是有用的,aapt命令只是系統提供給咱們一個很好的工具,咱們能夠在反編譯的過程當中藉助這個工具也是不錯的選擇。因此這裏我就想說,之後咱們記得有一個aapt命令就行了,他的用途仍是不少的,能夠單獨編譯成一個resource.arsc文件來,咱們後面會用到這個命令。


項目下載地址:http://download.csdn.net/detail/jiangwei0910410003/9415325


5、爲何要寫這篇文章

那麼如今咱們也能夠不用這個工具了,由於咱們本身也寫了一個工具解析,是否是很吊吊的呢?那麼咱們這篇文章僅僅是爲了解析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文件內容來達到加固的效果,以及如何咱們作修復來破解這種加固。


6、總結

這篇文章到這裏就算結束了,寫的有點累了,解析代碼已經有下載地址了,有不理解的同窗能夠聯繫我,加入公衆號,留言問題,我會在適當的時間給予回覆,謝謝,同時記得關注後面的兩篇解析resource.arsc和classes.dex文件格式的文章。謝謝~~

PS: 關注微信,最新Android技術實時推送

相關文章
相關標籤/搜索