前言
Apktool命令大全一文中提過,Android中的apk程序本質上是一個包含資源和匯編Java代碼的壓縮(.zip)包,其中最核心的三個文件 分別是AndroidManifest.xml、classes.dex、resources.arsc
做過逆向相關的工作的人都知道,開源工具Apktool 工作原理就是解析這三個文件格式。apk解壓后AndroidManifest.xml(清單文件)用普通文本格式打開的話是亂碼的。
這是因為在打包的過程中,清單文件被編譯成二進制數據,存儲在apk安裝包中。所以我們需要了解清單文件的二進制結構,這樣才能讀取我們需要的原始信息。如何解析清單文件呢?讓我們帶著這個問題往下看。
1.清單文件格式解析
AndroidManifest.xml文件官方文檔并沒有找到公開格式描述,在網上找到前輩大佬們一些學習資料,最有分析價值的還是看雪論壇大佬MindMac的結構圖,請看下圖, 原圖鏈接點擊直達
雖然是14年的結構圖的放在現在但是依舊經典,除了這個結構圖,還給大家推薦一款工具十六進制編輯器
010 editor(v10.0版本) 這對于我們分析清單文件結構非常有用。
對于上面的清單文件結構圖 沒有過這方面知識和基礎看的肯定一臉懵逼,完全不知道什么意思。我第一次看的時候我也如此 ,但是一點點看下來,分析一下結構還是比較清晰明朗的。結合實踐, 帶大家一步步解惑。本文中涉及的清單文件均來源網絡。
用010 editor 編譯器,清單文件整體結構非常清晰,如圖所示,不理解什么意思耐心往下看
結合上面的兩個結構圖,可以大體分為四大部分:
Header :頭文件
String Chunk : 存儲字符串資源的程序塊
ResourceId Chunk :存儲資源id的程序塊
XmlContent Chunk : 存儲xml內容程序塊,其中包含了五個部分,Start Namespace Chunk 、End Namespace Chunk 、Start Tag Chunk 、End Tag Chunk 、Text Chunk
1.1解析頭文件
任何一個文件格式都會有頭文件信息,AndroidManifest.xml 是一系列的模塊組合而成,我們可以理解為清單文件整體就是一個模塊,這個整體的模塊包含了多個子模塊。下圖所示就是010 editor 模板分析出的頭文件結構
頭文件有固定的格式頭部信息,根據010 editor分析得知其中包含的字段如下:
magicnumber :魔數, 固定值 0x0008003(16進制),四個字節 ,(524291這個值是10進制 )
filesize :文件總字節數 ,四個字節
這里涉及到一個知識點,大小端模式,清單文件用的是小端模式表示的,filesize從010 editor上正常看是 7c180000,如果是用小端模式表示十六進制應該是 0000187c, 轉換成十進制 結果就是 6268字節。進一步轉換下文件大小就是6.12kb (6268/1024),我們用代碼做下解析即可驗證結果。
/**
* 解析頭文件
* @throws IOException
*/
private void getHeaderChunk() throws IOException {
String magicNumber = byteReader.readHexString(4); //讀取成16進制字符串
System.out.println("ParserSources Header Start=================================");
System.out.println("magin number hex :"+magicNumber);
int fileSize=byteReader.readInt(); //16進制轉換成 10進制字節碼
System.out.println("file Size decimal :"+fileSize);
System.out.println("ParserSources Header End=================================");
}
上面的代碼是經過封裝處理過的,直接調用即可。簡述下代碼邏輯: 將清單文件讀到一個byte數組中解析,因為是小端模式 ,獲取magic number 的時候進行過一次反轉byte數組,將byte數組轉化成16進制的字符串。得到的結果進行拼串得出的結果是0x00080003,fileSize是對byte[]進行了 十進制的轉換 ,得到的值就是6268。
//結果輸出
ParserSources Header Start=================================
magin number hex :0x00080003
file Size decimal :6268
ParserSources Header END=================================
1.1.1大小端模式
舉個例子,直觀的認識下大小端模式,0x12345678在內存中的存儲方式
-
大端模式
低地址 -----> 高地址
0x12 | 0x34 | 0x56 | 0x78
-
小端模式
低地址 -----> 高地址
0x78| 0x56 | 0x34 | 0x12
1.2 解析String Chunk
String Chunk 主要用于存放清單文件所有字符串信息,做解析的時候最終獲取的就是strItem[0]-strItem[n]里的字符串值
如圖所示,010 editor 模板解析出來的字符串結構,正好能和上面的看雪大佬的結構圖String Chunk對應上,我會對字符串信息做下說明,讓大家知道什么意思。
ChunkType :StringChunk類型,4個字節 ,固定值 0x001c0001
ChunkSize :StringChunk大小 ,4個字節
StringCount :StringChunk字符串的個數,4個字節
StyleCount :StringChunk樣式的個數,4個字節,固定值 0x00000000
Unkown : 位置區域,4個字節,固定值 0x00000000解析時候需要略過4個字節
StringPoolOffset :字符串池偏移量,4個字節,偏移量相對StringChunk頭部位置
StylePoolOffset :樣式池偏移量,4個字節,偏移量相對于StringChunk頭部位置
固定值 0x00000000 ,這個字段基本沒用到過,可忽略掉StringOffsets :每個字符串在字符串池中的相對偏移量,int數組,它的大小是StringCount*4個字節
StyleOffsets :每個樣式的偏移量,int數組,它的大小是StyleCount*4個字節
String Pool :字符串池,存儲了所有的字符串
Style Pool :樣式池,存儲了所有的樣式
private void getStringChunk() throws IOException {
System.out.println("");
System.out.println("ParserSources String Chunk Start=================================");
String chunkType = byteReader.readHexString(4); //輸出的值是 用16進制 的 0x001C0001 ,轉換成10進制 就是1835009
System.out.println("String chunkType :"+chunkType);
int chunkSize = byteReader.readInt();
System.out.println("String chunkSize :"+chunkSize);
int stringCount = byteReader.readInt();
System.out.println("String StringCount :"+stringCount);
int styleCount = byteReader.readInt();
System.out.println("String StyleCount :"+styleCount);
byteReader.jump(4);//跳過 unknown的 4個字節
int stringPoolOffset = byteReader.readInt();
System.out.println("String StringPoolOffset :"+stringPoolOffset);
int stylePoolOffset = byteReader.readInt();
System.out.println("String StylePoolOffset :"+stylePoolOffset);
// 每個 string 的偏移量,stringCount*4個字節
List<Integer> stringPoolOffsets = new ArrayList<>(stringCount);
for (int i = 0; i < stringCount; i++) {
stringPoolOffsets.add(byteReader.readInt());
}
// 每個 style 的偏移量 ,StringCount*4個字節
List<Integer> stylePoolOffsets = new ArrayList<>(styleCount);
for (int i = 0; i < styleCount; i++) {
stylePoolOffsets.add(byteReader.readInt());
}
System.out.println("string pool start" );
//偏移值開始的兩個字節是字符串的長度,接著是字符串的內容,后面跟著兩個字符串的結束符00
for (int i = 1; i <= stringCount; i++) { // 沒有讀最后一個字符串
String string;
if (i == stringCount) {
int lastStringLength = byteReader.readShort() * 2; //一個字符對應著2個字節,所以要*2
string = new String(moveBlank(byteReader.readOrigin(lastStringLength)));
byteReader.jump(2);
} else {
byteReader.jump(2); // 字符長度
byte[] content = byteReader.readOrigin(stringPoolOffsets.get(i) - stringPoolOffsets.get(i - 1) - 4); // 根據偏移量讀取字符串
byteReader.jump(2); // 跳過結尾的 0000 2個字節
string = new String(moveBlank(content));
}
System.out.println(string);
stringChunkList.add(string); //把所有字符串都添加到ArrayList中
}
//Style Pool
for (int i = 1; i < styleCount; i++) {
byteReader.jump(2);
byte[] content = byteReader.readOrigin(stylePoolOffsets.get(i) - stylePoolOffsets.get(i - 1) - 4);
byteReader.jump(2);
String string = new String(content);
System.out.println("Style Pool "+string);
}
byteReader.moveTo(chunkSize+8); //ResourceIdChunk 之前可能存在 0000,是為了對齊
System.out.println("ParserSources String Chunk End=================================");
}
/**
* 移除多余的空的字節
*/
public static byte[] moveBlank(byte[] data) {
List<Byte> byteList = new ArrayList<>();
for (Byte b : data) {
if (b != 0) byteList.add(b);
}
byte[] result = new byte[byteList.size()];
for (int i = 0; i < result.length; i++)
result[i] = byteList.get(i);
return result;
}
//解析結果輸出
ParserSources String Chunk Start=================================
String chunkType :0x001C0001
String ChunkSize :3172
String StringCount :69
String StyleCount :0
String StringPoolOffset :304
String StylePoolOffset :0
string pool start
compileSdkVersion
compileSdkVersionCodename
installLocation
versionCode
versionName
minSdkVersion
targetSdkVersion
name
glEsVersion
required
allowBackup
banner
resizeableActivity
icon
isGame
label
usesCleartextTraffic
theme
value
configChanges
hardwareAccelerated
launchMode
screenOrientation
android
http://schemas.android.com/apk/res/android
package
platformBuildVersionCode
platformBuildVersionName
manifest
6.0-2438415
com.xdqbf.cw
23
2.6.1
uses-sdk
uses-permission
com.android.vending.BILLING
android.permission.WRITE_EXTERNAL_STORAGE
uses-feature
android.permission.INTERNET
android.permission.ACCESS_NETWORK_STATE
android.hardware.touchscreen
android.hardware.touchscreen.multitouch
android.hardware.touchscreen.multitouch.distinct
com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE
application
com.hikergames.HikerGamesApplication
meta-data
Y2GAME_CHANNEL
Y2PUBLISHER_CHANNEL
android.max_aspect
activity
com.hikergames.MainActivity
intent-filter
action
android.intent.action.MAIN
category
android.intent.category.LAUNCHER
android.intent.category.LEANBACK_LAUNCHER
unityplayer.UnityActivity
unityplayer.ForwardNativeEventsToDalvik
unity.build-id
93406909-c0b3-40de-b7c5-75971379027e
unity.splash-mode
unity.splash-enable
com.unity.udp.udpsandbox.LoginActivity
com.unity.udp.udpsandbox.UDPPurchasing$LoginHelperActivity
CHANNEL_NAME
UDP
string pool end
ParserSources String Chunk End=================================
這里做個說明: 除了解析字符串內容外,其他的沒什么可說的,封裝好的方法復用。
unknown的這個字段上面解釋過,沒有什么實際意義,在解析中跳過忽略
一個字符串對應的是2個字節,上面有個方法moveBlank(byte[] data),邏輯就是過濾一下空的字符串,在java語言中空字符串就是 00 ,如果不過濾會出現寬字符(例:v e r s i o n),過濾后就正常了展示。
樣式池在解析過程中一般都為空,樣式數量也為 0
解析字符串有個格式,偏移值開始的兩個字節是字符串的長度,接著是字符串的內容,后面跟著兩個字符串的結束符00 (0x0000),下圖中有三個屬性分別是 size, oneChar,end,對應這字符串長度,字符串內容和結束符,字符長度這里是00 11 (十進制 值是17 )。字符內容16進制有點多忽略,結束符的是 00 00
1.2.1 偏移量的概念
把存儲單元的實際地址與其所在段的段地址之間的距離稱為段內偏移。更通俗一點講,內存中存儲數據的方式是:一個存儲數據的“實際地址”=段首地址+偏移量,你也可以這樣理解:就像我們現實中的“家庭地址”=“小區地址”+“門牌號”
1.3ResourceId Chunk
ResourceId Chunk主要 用來存放清單文件中所引用到的系統屬性值和當前應用所對應的資源ID,看一下 010 edtior屬性塊,對應這看雪大神結構圖ResourceId Chunk。
ChunkType :ResourceldChunk的類型,4個字節 ,固定值 0x00080180
ChunkSize :ResourceldChunk的大小 ,4個字節
ResourceIds :int數組,大小為(chunkSize - 8) / 4 ,減 8是減去頭部大小的8個字節(ChunkType和ChunkSize)
/**
*獲取資源清單文件資源id
* @throws IOException
*/
private void getResourceIdChunk() throws IOException {
System.out.println();
System.out.println("ParserSources ResourceId Chunk Start=================================");
String chunkType = byteReader.readHexString(4);
System.out.println("chunkType:"+chunkType);
int chunkSize = byteReader.readInt();
System.out.println("chunkSize:"+chunkSize);
// ResourceIds 大小為(chunkSize - 8) / 4
int resourcesIdChunkCount = (chunkSize - 8) / 4;
for (int i = 0; i < resourcesIdChunkCount; i++) {//遍歷ResourceIds
String resourcesId = byteReader.readHexString(4);
System.out.println("resource id:"+resourcesId);
}
System.out.println("ParserSources ResourceId Chunk End=================================");
}
//解析結果輸出
ParserSources ResourceId Chunk Start=================================
chunkType:0x00080180
chunkSize:100
resource id:0x01010572
resource id:0x01010573
resource id:0x010102B7
resource id:0x0101021B
resource id:0x0101021C
resource id:0x0101020C
resource id:0x01010270
resource id:0x01010003
resource id:0x01010281
resource id:0x0101028E
resource id:0x01010280
resource id:0x010103F2
resource id:0x010104F6
resource id:0x01010002
resource id:0x010103F4
resource id:0x01010001
resource id:0x010104EC
resource id:0x01010000
resource id:0x01010024
resource id:0x0101001F
resource id:0x010102D3
resource id:0x0101001D
resource id:0x0101001E
ParserSources ResourceId Chunk End=================================
說明:這一部分沒什么好說,都是封裝好的方法,輸出的resource Id 是16進制的,010 edtor上顯示的數值是轉換后的10進制。
1.3.1解析出來的ID是什么?
接著上面的說,上一章節1.3 解析ResourceId Chunk結果 會得到一個十進制的或者 16進制的一個Id值,那這個id值是怎么來的呢?
編譯器在編譯安卓資源的時候,至少會涉及兩個包,其中一個是系統資源包,另一個就是當前正在編譯應用的資源包。每一個包都有定義自己的資源,同時它也可以引用其它包的資源,一個包通過什么方式來引用其它包的資源呢?這就是我們熟悉的資源ID 了。資源ID是一個4字節的無符號整數,其中,最高字節表示Package ID,次高字節表示Type ID,最低兩字節表示Entry ID。
Package ID相當于是一個命名空間,限定資源的來源。Android系統當前定義了兩個資源命令空間,其中一個系統資源命令空間,它的Package ID等于0x01,另外一個是應用程序資源命令空間,它的Package ID等于0x7f。所有位于[0x01, 0x7f]之間的Package ID都是合法的,而在這個范圍之外的都是非法的Package ID。這一點可以通過生成的R.java文件來驗證,引用系統資源都是0x01開頭的。
Type ID是指資源的類型ID。資源的類型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干種,每一種都會被賦予一個ID。
Entry ID是指每一個資源在其所屬的資源類型中所出現的次序。注意,不同類型的資源的Entry ID有可能是相同的,但是由于它們的類型不同,我們仍然可以通過其資源ID來區別開來。
關于系統對應的資源ID更多描述,以及系統資源的引用關系,可以參考系統源碼,我這里用的是安卓8.0系統, 存放路徑:frameworks/base/core/res/res/values/public.xml點擊可以直接看源碼
1.4 XmlContentChunk
XmlContentChunk 這部分表示的是,存儲了清單文件的詳細信息,包含的5項,其中Start Namespace Chunk 和End Namespace Chunk ,這兩個可以合并一個來說明, 因為他們的結構是完全一致,解析過程也是一樣的。至于EndTagChunk一共有6個數據,也就是 Start Tag Chunk 的前 6 項,這里不做單獨解析和說明。EndTagChunk這個跟清單文件標簽一樣的,就是給解析出來的標簽加上結束標簽一樣。
Text Chunk這個模塊在010 edtor模板里并沒有用到過,我就不做說明了,如果哪位大佬遇到過這個模塊,可以私聊或者評論,一起研究一下。
1.4.1 Start Namespace Chunk
這個Chunk主要包含是一個清單文件的命令空間內容,老規矩010 edtor 分析一下結構 ,屬性如下圖所示:
ChunkType :Chunk的類型,4個字節 ,固定值 0x00100100
ChunkSize :Chunk的大小 ,4個字節
LineNumber :清單文件中的行號, 4個字節
Unknown :未知區域, 4個字節
Prefix :命名空間的前綴, 4個字節
Uri :命名空間的URI, 4個字節
private void getStartNamespaceChunk() throws IOException{
System.out.println();
System.out.println("ParserSources StartNamespaceChunk Start=================================");
int chunkSize = byteReader.readInt();
System.out.println("NamespaceChunk ChunkType :"+chunkSize);
int lineNumber = byteReader.readInt();
System.out.println("NamespaceChunk LineNumber :"+lineNumber);
byteReader.jump(4); //這里需要注意的是行號后面的四個字節為0xffffffff,過濾
int prefix = byteReader.readInt();
System.out.println("NamespaceChunk Prefix :"+prefix);
int uri = byteReader.readInt();
System.out.println("NamespaceChunk Uri :"+uri);
StartNameSpaceChunk startNameSpaceChunk = new StartNameSpaceChunk(chunkSize, lineNumber, prefix, uri);
chunkList.add(startNameSpaceChunk);
nameSpaceMap.put(stringChunkList.get(prefix), stringChunkList.get(uri));
System.out.println("ParserSources StartNamespaceChunk End=================================");
}
//解析結果輸出
ParserSources StartNamespaceChunk Start=================================
NamespaceChunk ChunkType :24
NamespaceChunk LineNumber :1
NamespaceChunk Prefix :23
NamespaceChunk Uri :24
ParserSources StartNamespaceChunk End=================================
我還是拿個例子圖,讓大家更直觀的了解什么意思,重點是prefix和url 兩個屬性。
首先如下圖所示,Prefix的值是23 ,Uri的值是24,根據這2個數字值,在字符串池能找到對應的字符值,如圖stringChunk_attribute.png,23對應著字符串值android ,24對應著字符串http://schemas.android.com/apk/res/android,也可以把Namespace Chunk 當成字符串索引值,這樣能對這個Namespace Chunk更好的理解了
說明:這一部分也沒什么好說,都是封裝好的方法,需要注意的是Unknown 四個字節為FFFF需要過濾,命名空間的后綴和 uri 的對應關系保存在了 map 中,供后面解析的時候使用。
1.4.2 Start Tag Chunk
這個TagChunk主要存放清單文件中的標簽信息,最核心也是最麻煩的內容。老規矩010 edtor 分析一下結構 ,屬性如下圖所示:
ChunkType :Chunk的類型,4個字節 ,固定值 0x00100102
ChunkSize :Chunk的大小 ,4個字節
LineNumber :清單文件中的行號, 4個字節
Unknown :未知區域, 4個字節
Namespace Uri :命名空間用到的url在字符串的索引,值為 -1 表示沒有用到命名空間 uri。
標簽的一般都沒有使用到命名空間,此值為 -1,4個字節Name :標簽名稱(在字符串中的索引值),4個字節
Flags :標簽類型例如開始標簽還是結束標簽,固定值0x00140014,4個字節,
Attribute Count :標簽包含的屬性個數,4個字節
Class Attribute :標簽包含的類屬性,此項值常為 0,4個字節
-
Attributes :屬性內容集合,每個屬性固定 20 個字節,包含 5 個字段,每個字段都是 4 字節無符號 int,解析的時候需要注意Type這個值需要做一次處理需要又移24位。各個字段含義如下:
NamespaceUri :屬性的命名空間uri 在字符串池中的索引
Name :屬性名稱在字符串池中的索引
ValueStr :屬性值
Type :屬性類型
Data :屬性數據
private void getStartTagChunk()throws IOException{
System.out.println();
System.out.println("ParserSources StartTagChunk Start=================================");
// System.out.println("Chunk Type: "+START_TAG_CHUNK_TYPE);//因為是固定值 ,所以直接寫的靜態常量
int chunkSize = byteReader.readInt();
System.out.println("StartTag ChunkSize :"+chunkSize);
int lineNumber = byteReader.readInt();
System.out.println("StartTag LineNumber :"+lineNumber);
byteReader.jump(4); // Unknown 需要跳過4個字節 0xffffffff
int namespaceUri = byteReader.readInt();
if (namespaceUri == -1)
System.out.println("namespace uri is null");
else
System.out.println("namespace uri :"+stringChunkList.get(namespaceUri));
int name = byteReader.readInt();
System.out.println("Name :"+stringChunkList.get(name));
byteReader.jump(4); // Flag 固定值需要做忽略處理 0x00140014
int attributeCount = byteReader.readInt();
System.out.println("AttributeCount:"+attributeCount);
int classAttribute = byteReader.readInt();
System.out.println("ClassAttribute:"+classAttribute);
List<Attribute> attributes = new ArrayList<>();
for (int i = 0; i < attributeCount; i++) {
System.out.println("Attribute["+i+"]");
int namespaceUriAttr = byteReader.readInt();
if (namespaceUriAttr == -1)
System.out.println("Namespace Uri: null");
else
System.out.println("Namespace Uri:"+stringChunkList.get(namespaceUriAttr));
int nameAttr = byteReader.readInt();
if (nameAttr == -1)
System.out.println("Name: null");
else
System.out.println("Name:"+stringChunkList.get(nameAttr));
int valueStr = byteReader.readInt();
if (valueStr == -1)
System.out.println("ValueStr: null");
else
System.out.println("ValueStr:"+stringChunkList.get(valueStr));
int type = byteReader.readInt() >> 24;
System.out.println("Type:"+type);
int data = byteReader.readInt();
String dataString = type == TypedValue.TYPE_STRING ? stringChunkList.get(data) : TypedValue.coerceToString(type, data);
System.out.println("Data:"+dataString);
Attribute attribute = new Attribute(namespaceUriAttr == -1 ? null : stringChunkList.get(namespaceUriAttr),
stringChunkList.get(nameAttr), valueStr, type, dataString);
attributes.add(attribute);
}
StartTagChunk startTagChunk = new StartTagChunk(namespaceUri, stringChunkList.get(name), attributes);
chunkList.add(startTagChunk);
System.out.println("ParserSources StartTagChunk End=================================");
}
// 解析結果輸出
ParserSources StartTagChunk Start=================================
StartTag ChunkSize :196
StartTag LineNumber :1
namespace uri is null
Name :manifest
AttributeCount:8
ClassAttribute:0
Attribute[0]
Namespace Uri:http://schemas.android.com/apk/res/android
Name:versionCode
ValueStr: null
Type:16
Data:41
Attribute[1]
Namespace Uri:http://schemas.android.com/apk/res/android
Name:versionName
ValueStr:2.6.1
Type:3
Data:2.6.1
Attribute[2]
Namespace Uri:http://schemas.android.com/apk/res/android
Name:installLocation
ValueStr: null
Type:16
Data:2
Attribute[3]
Namespace Uri:http://schemas.android.com/apk/res/android
Name:compileSdkVersion
ValueStr: null
Type:16
Data:23
Attribute[4]
Namespace Uri:http://schemas.android.com/apk/res/android
Name:compileSdkVersionCodename
ValueStr:6.0-2438415
Type:3
Data:6.0-2438415
Attribute[5]
Namespace Uri: null
Name:package
ValueStr:com.xdqbf.cw
Type:3
Data:com.xdqbf.cw
Attribute[6]
Namespace Uri: null
Name:platformBuildVersionCode
ValueStr:23
Type:16
Data:23
Attribute[7]
Namespace Uri: null
Name:platformBuildVersionName
ValueStr:6.0-2438415
Type:3
Data:6.0-2438415
ParserSources StartTagChunk End=================================
/**
*根據上面的輸出結果再做進一步處理一下 ,看著是不是很熟悉了。
*/
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:compileSdkVersion="23"
android:compileSdkVersionCodename="6.0-2438415"
android:installLocation="preferExternal"
package="com.xdqbf.cw"
platformBuildVersionCode="23"
platformBuildVersionName="6.0-2438415">
解析說明:
解析屬性的需要注意的是每個屬性單元都是由5個元素組成(Attributes 字段),每個元素占用4個字節,所以獲取type時候需要做右移24位。
platformBuildVersionCode和platformBuildVersionName 這兩個是系統在編譯的時候自動添加的兩個屬性,在解析中會體現出來。
當沒有android這樣的前綴時,NamespaceUri 是 null
- 當dataTtpe不同,對應的data值也不同,當前代碼引用的系統的工具TypedValue,這個類就是用來進行轉義的。
- 每個屬性理論上都會含有一個NamespaceUri,這個決定了屬性前綴Prefix 默認都是Android,但清單文件也會存在自定義控件的情況,所以就需要導入NamespaceUri和Prefix ,所以一個XML可能有多個Namespace,每個屬性都會包含NamespaceUri
1.4.3 展示出完整的解析結果
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="41"
android:versionName="2.6.1"
android:installLocation="2"
android:compileSdkVersion="23"
android:compileSdkVersionCodename="6.0-2438415"
package="com.xdqbf.cw"
platformBuildVersionCode="23"
platformBuildVersionName="6.0-2438415">
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="29">
</uses-sdk>
<uses-permission
android:name="com.android.vending.BILLING">
</uses-permission>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE">
</uses-permission>
<uses-feature
android:glEsVersion="0x00020000">
</uses-feature>
<uses-permission
android:name="android.permission.INTERNET">
</uses-permission>
<uses-permission
android:name="android.permission.ACCESS_NETWORK_STATE">
</uses-permission>
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false">
</uses-feature>
<uses-feature
android:name="android.hardware.touchscreen.multitouch"
android:required="false">
</uses-feature>
<uses-feature
android:name="android.hardware.touchscreen.multitouch.distinct"
android:required="false">
</uses-feature>
<uses-permission
android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE">
</uses-permission>
<application
android:theme="@2131165186"
android:label="@2131099648"
android:icon="@2130837506"
android:name="com.hikergames.HikerGamesApplication"
android:allowBackup="true"
android:banner="@2130837505"
android:isGame="true"
android:usesCleartextTraffic="true"
android:resizeableActivity="true">
<meta-data
android:name="Y2GAME_CHANNEL"
android:value="0">
</meta-data>
<meta-data
android:name="Y2PUBLISHER_CHANNEL"
android:value="0">
</meta-data>
<meta-data
android:name="android.max_aspect"
android:value="2.1">
</meta-data>
<activity
android:label="@2131099648"
android:icon="@2130837506"
android:name="com.hikergames.MainActivity"
android:launchMode="2"
android:screenOrientation="6"
android:configChanges="0x40002FFF"
android:hardwareAccelerated="false">
<intent-filter>
<action
android:name="android.intent.action.MAIN">
</action>
<category
android:name="android.intent.category.LAUNCHER">
</category>
<category
android:name="android.intent.category.LEANBACK_LAUNCHER">
</category>
</intent-filter>
<meta-data
android:name="unityplayer.UnityActivity"
android:value="true">
</meta-data>
<meta-data
android:name="unityplayer.ForwardNativeEventsToDalvik"
android:value="false">
</meta-data>
</activity>
<meta-data
android:name="unity.build-id"
android:value="93406909-c0b3-40de-b7c5-75971379027e">
</meta-data>
<meta-data
android:name="unity.splash-mode"
android:value="0">
</meta-data>
<meta-data
android:name="unity.splash-enable"
android:value="true">
</meta-data>
<activity
android:name="com.unity.udp.udpsandbox.LoginActivity">
</activity>
<activity
android:theme="@16973841"
android:name="com.unity.udp.udpsandbox.UDPPurchasing$LoginHelperActivity"
android:configChanges="0x40000FFF">
</activity>
<meta-data
android:name="CHANNEL_NAME"
android:value="UDP">
</meta-data>
</application>
</manifest>
做下說明:
其中 android:theme="@2131165186"或者 android:label="@2131099648" 這個數字是不是跟平時寫的不一樣? 這是解析時候做了處理 ,展示的是10進制(android:theme="@2131165186),轉換成16進制 就是0x7f070002。這個在1.3.1章節中說過,引用的是應用程序資源,再往下說就涉及到.arsc文件了,解釋到此為止下次可能會出對應文章針對arsc文件做解讀。
android:screenOrientation="6" 這個值是sensorLandscape,這個sensorLandscape在系統定義的 int值 數字6 。在打包時候會有對應映射關系 ,用apktool解包后自然展示成 android:screenOrientation="sensorLandscape"
技術總結
在逆向應用中,用工具apktool 反編譯應用,尤其是一些大廠的應用,經常出現一些異常信息,不讓我們輕松的反編譯。因為apktool本就是開源的,所以一些大企業會針對這個開源項目去找它的漏洞,通過一些漏洞對自己的apk進行處理,增加反編譯的難度。我覺得非常有必要了解apktool原理和解析源碼,這篇文章就是針對apktool中清單文件的解讀。只要你了解了這個原理,對逆向和反逆向有很好的幫助。
聲明
本文中涉及的代碼均改自秉心說,代碼封裝的很好,我的代碼就不發出來了,有需要去下載大佬的代碼--點擊即可進入
文章參考
秉心說文章鏈接: AndroidManifest.xml 文件格式解析
姜維:安卓應用安全防護和逆向分析
結語
記錄下自己的學習和工作經驗,分享給有需要的人。如果有那里寫的不對,說的不理解,歡迎大家的指正。