前言
Apktool命令大全一文中提过,Android中的apk程序本质上是一个包含资源和汇编Java代码的压缩(.zip)包,其中最核心的三个文件 分别是AndroidManifest.xml、classes.dex、resources.arsc
做过逆向相关的工作的人都知道,开源工具Apktool 工作原理就是解析这三个文件格式。apk解压后AndroidManifest.xml(清单文件)用普通文本格式打开的话是乱码的。
这是因为在打包的过程中,清单文件被编译成二进制数据,存储在apk安装包中。所以我们需要了解清单文件的二进制结构,这样才能读取我们需要的原始信息。如何解析清单文件呢?让我们带着这个问题往下看。
1.清单文件格式解析
AndroidManifest.xml文件官方文档并没有找到公开格式描述,在网上找到前辈大佬们一些学习资料,最有分析价值的还是看雪论坛大佬MindMac的结构图,请看下图, 原图链接点击直达
MindMac清单文件结构图.png虽然是14年的结构图的放在现在但是依旧经典,除了这个结构图,还给大家推荐一款工具十六进制编辑器
010 editor(v10.0版本) 这对于我们分析清单文件结构非常有用。
对于上面的清单文件结构图 没有过这方面知识和基础看的肯定一脸懵逼,完全不知道什么意思。我第一次看的时候我也如此 ,但是一点点看下来,分析一下结构还是比较清晰明朗的。结合实践, 带大家一步步解惑。本文中涉及的清单文件均来源网络。
用010 editor 编译器,清单文件整体结构非常清晰,如图所示,不理解什么意思耐心往下看
header.png结合上面的两个结构图,可以大体分为四大部分:
-
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 模板分析出的头文件结构
header_file.png头文件有固定的格式头部信息,根据010 editor分析得知其中包含的字段如下:
-
magicnumber :魔数, 固定值 0x0008003(16进制),四个字节 ,(524291这个值是10进制 )
-
filesize :文件总字节数 ,四个字节
这里涉及到一个知识点,大小端模式,清单文件用的是小端模式表示的,filesize从010 editor上正常看是 7c180000,如果是用小端模式表示十六进制应该是 0000187c, 转换成十进制 结果就是 6268字节。进一步转换下文件大小就是6.12kb (6268/1024),我们用代码做下解析即可验证结果。
header_parser.png /**
* 解析头文件
* @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对应上,我会对字符串信息做下说明,让大家知道什么意思。
String Pool.png-
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。
ResourceldChunk.png-
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点击可以直接看源码
public.xml1.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 分析一下结构 ,属性如下图所示:
Start Namespace Chunk.png-
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更好的理解了
namespace_attribute.png stringChunk_attribute.png说明:这一部分也没什么好说,都是封装好的方法,需要注意的是Unknown 四个字节为FFFF需要过滤,命名空间的后缀和 uri 的对应关系保存在了 map 中,供后面解析的时候使用。
1.4.2 Start Tag Chunk
这个TagChunk主要存放清单文件中的标签信息,最核心也是最麻烦的内容。老规矩010 edtor 分析一下结构 ,属性如下图所示:
Tag Chunk.png-
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 文件格式解析
姜维:安卓应用安全防护和逆向分析
结语
记录下自己的学习和工作经验,分享给有需要的人。如果有那里写的不对,说的不理解,欢迎大家的指正。
网友评论