Androidni逆向 —— AndroidManifest.xml 解析
做过 Android 开发的同学对 AndroidManifest.xml
文件肯定很熟悉,我们也叫它 清单文件 ,之所以称之为清单文件,因为它的确是应用的 “清单”。它包含了应用的包名,版本号,权限信息,所有的四大组件等信息。在逆向的过程中,通过 apk 的清单文件,我们可以了解应用的一些基本信息,程序的入口 Activity,注册的服务,广播,内容提供者等等。如果你尝试查看过 apk 中的 AndroidManifest.xml
文件,你会发现你看到的是一堆乱码,已经不是我们开发过程中编写的清单文件了。因为在打包过程中,清单文件被编译成了二进制数据存储在安装包中。这就需要我们了解 AndroidManifest.xml
的二进制文件结构,才可以读取到我们需要的信息。当然,已经有一些不错的开源工具可以读取编译后的清单文件,像 AXmlPrinter
, apktool
等等。当然,正是由于这些工具都是开源的,一些开发者会利用其中的漏洞对清单文件进行特定的处理,使得无法通过这些工具反编译清单文件。如果我们了解其二进制文件结构的话,就可以对症下药了。
和之前解析 Class 文件结构一样,仍然手写代码进行解析,这样才能真正的了解其文件结构。通过前辈们的资料和 010 editor
的使用,其实已经大大降低了解析的难度。首先上一张看雪大神 MindMac 的神图(原图链接):
这张图真的很经典,不妨可以打印出来对照着进行分析。
这篇文章以 QQ 的清单文件为例进行分析,下载 QQ 的安装包解压即可拿到清单文件。解析文件格式的惯例,首先用 010 editor 打开,基本结构如下图所示:
运行的 Template 是 AndroidManifest.bt
。结合上面的结构图,对 AndroidManifest.xml
的总体结构应该有了大概的了解。总体上按顺序分为四大部分:
Header
: 包括文件魔数和文件大小String Chunk
: 字符串资源池ResourceId Chunk
: 系统资源 id 信息XmlContent Chunk
: 清单文件中的具体信息,其中包含了五个部分,Start Namespace Chunk
、End Namespace Chunk
、Start Tag Chunk
、End Tag Chunk
、Text Chunk
二进制 AndroidManifest.xml
大致上就是按照这几部分顺序排列组成的,下面就逐一部分详细解析。在这之前还需要知道的一点是,清单文件是小端表示的,ARM 平台下大多数都是小端表示的。
Header
头部由 Magic Number
和 File Size
组成,各自都是 4 字节。
Magic Number
始终为0x0008003
。File Size
表示文件总字节数,
对应的解析代码:
private void parseHeader() {
try {
Xml.nameSpaceMap.clear();
String magicNumber = reader.readHexString(4);
log("magic number: %s", magicNumber);
int fileSize = reader.readInt();
log("file size: %d", fileSize);
} catch (IOException e) {
e.printStackTrace();
log("parse header error!");
}
}
复制代码
解析结果:
magic number: 0x00080003
file size: 273444
复制代码
String Chunk
先来看一下 010 editor 中这一块的内容:
对应看雪神图的 StringChunk
模块:
String Chunk
主要存储了清单文件中的所有字符串信息。结构还是很清晰的。结合上图逐条解释一下:
Chunk Type
: 4 bytes,始终为0x001c0001
,标记这是 String ChunkChunk Size
: 4 bytes,表示 String Chunk 的大小String Count
: 4 bytes,表示字符串的数量Style Count
: 4 bytes,表示样式的数量Unkown
: 4 bytes,固定值,0x00000000
String Pool Offset
: 字符串池的偏移量,注意不是相对于文件开始处,而是相对于 String Chunk 的开始处Style Pool Offset
: 样式池的偏移量,同上,也是相对于 String Chunk 而言String Offsets
: int数组,大小为 String Count,存储每个字符串在字符串池中的相对偏移量Style Offets
: 同上,也是 int 数组。总大小为Style Count * 4
bytesString Pool
: 字符串池,存储了所有的字符串Style Pool
: 样式池,存储了所有的样式
字符串池中的字符串存储也有特定的格式,以 versionName
为例:
前两个字节表示字符串的字符数,注意一个字符是两个字节。如上图所示,字符数为 11
,则后面 22
个字节表示字符串内容,最后以 0000
结尾。如此循环。
样式池在解析过程中一般都为空,样式数量也为 0。
了解了 String Chunk
的结构之后,解析就很简单了。直接上代码:
private void parseStringChunk() {
try {
String chunkType = reader.readHexString(4);
log("chunk type: %s", chunkType);
int chunkSize = reader.readInt();
log("chunk size: %d", chunkSize);
int stringCount = reader.readInt();
log("string count: %d", stringCount);
int styleCount = reader.readInt();
log("style count: %d", styleCount);
reader.skip(4); // unknown
int stringPoolOffset = reader.readInt();
log("string pool offset: %d", stringPoolOffset);
int stylePoolOffset = reader.readInt();
log("style pool offset: %d", stylePoolOffset);
// 每个 string 的偏移量
List<Integer> stringPoolOffsets = new ArrayList<>(stringCount)