深入浅出的理解Android resources.arsc文件

准备工作

先附图一张

在阅读参考资料的内容时,看了这个图一脸懵逼(what are you about),阅读完之后也是似懂非懂的,还是觉得没有贯通任督二脉,于是决定从一个简单apk文件的resources.arsc文件一个字节一个字节的分析,阅读了参考资料1的github工程的源码,刻意的寻找了下,还真找到了google提供的一个优秀项目android-arscblamer,其代码的优秀程度,我也只能跪拜了。本文结合我对android-arscblamer修改后的工程arscblamer-gradle源码和上图已经关键的ResourceTypes.h文件,让我们一起深入浅出的理解Android resources.arsc文件

arscblamer-gradle工程中的src/main/resources目录中包含了本文分析的资源文件(apk,resources.arsc,以及详细的二进制文件解析的注释文件resources.md和工程生产的excel文件),本文主要起一个引子的作用,教会大家如何分析resources.arsc这个二进制文件,好了,废话完毕,让我们进入主题吧

从源码中读懂二进制文件resources.arsc

如果什么都不看,直接去看文件resources.arsc的16进制码很容易陷进去,我们就站在巨人的肩膀上抄近路吧

理解清楚android-arscblamer的代码结构

如上工程类图已经能反应出很多信息:

1.所有内容都是序列化资源(实现了SerializableResource接口)
2.看到了很多Chunk(没错,resources.arsc从面向对象的角度讲,就是由一个个Chunk构成的)
3.Chunk有之间有点类似树型结构(个人理解这个和Android中ViewGroup和View的关系类似,用到了设计模式中的组合模式)

拿几个Chunk开开刀

如上文所述,resources.arsc就是由多个的Chunk来构成的,那么我们就来几个示例分析下这些Chunk吧(首先你得要能查看resources.arsc文件的16进制编码)

注:resources.arsc文件的二进制是以小端序的方式存储的(最低位字节存储在最低的内存地址处)

  • RES_TABLE Chunk(对应的类文件是ResourceTableChunk.java,没错它就是一个包含多个Chunk的ChunkWithChunks)
1
2
3
4
5
02 00       // RES_TABLE_TYPE                    = 0x0002 0ffset = 0    start table head

0C 00 // ResTable_header headerSize = 12
E0 07 00 00 // ResTable_header size = 2016
01 00 00 00 // ResTable_header packageCount = 1 end table head

如上可以看到Chunk RES_TABLE的头信息,是不是可以和文章最开始附图能对应的上,如果只和图对应上就太简单了,那么让我们来看看android源码中ResourceTypes.h对应的RES_TABLE对应的数据结构吧
如下是所有Chunk公用的数据结构Chunk header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/** ********************************************************************
* Base Types
*
* These are standard types that are shared between multiple specific
* resource types.
*
*********************************************************************** */

/**
* Header that appears at the front of every data chunk in a resource.
*/
struct ResChunk_header
{
// Type identifier for this chunk. The meaning of this value depends
// on the containing chunk.
uint16_t type;

// Size of the chunk header (in bytes). Adding this value to
// the address of the chunk allows you to find its associated data
// (if any).
uint16_t headerSize;

// Total size of this chunk (in bytes). This is the chunkSize plus
// the size of any data associated with the chunk. Adding this value
// to the chunk allows you to completely skip its contents (including
// any child chunks). If this value is the same as chunkSize, there is
// no data associated with the chunk.
uint32_t size;
};

而我们分析的RES_TABLE的header结构体如下,是不是和16进制对应上了,基础的3个头部信息(类型,头大小,块大小)和一个资源包的数量packageCount

1
2
3
4
5
6
7
struct ResTable_header
{
struct ResChunk_header header;

// The number of ResTable_package structures.
uint32_t packageCount;
};

ResourceTypes.h中的Chunk类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum {
RES_NULL_TYPE = 0x0000,
RES_STRING_POOL_TYPE = 0x0001,
RES_TABLE_TYPE = 0x0002,
RES_XML_TYPE = 0x0003,

// Chunk types in RES_XML_TYPE
RES_XML_FIRST_CHUNK_TYPE = 0x0100,
RES_XML_START_NAMESPACE_TYPE= 0x0100,
RES_XML_END_NAMESPACE_TYPE = 0x0101,
RES_XML_START_ELEMENT_TYPE = 0x0102,
RES_XML_END_ELEMENT_TYPE = 0x0103,
RES_XML_CDATA_TYPE = 0x0104,
RES_XML_LAST_CHUNK_TYPE = 0x017f,
// This contains a uint32_t array mapping strings in the string
// pool back to resource identifiers. It is optional.
RES_XML_RESOURCE_MAP_TYPE = 0x0180,

// Chunk types in RES_TABLE_TYPE
RES_TABLE_PACKAGE_TYPE = 0x0200,
RES_TABLE_TYPE_TYPE = 0x0201,
RES_TABLE_TYPE_SPEC_TYPE = 0x0202,
RES_TABLE_LIBRARY_TYPE = 0x0203
};

Chunk.java中的Type枚举和上边是一一对应的

  • RES_STRING_POOL Chunk(StringPoolChunk.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
01 00       // RES_STRING_POOL_TYPE              = 0x0001 0ffset = 12   start sting pool head

1C 00 // ResStringPool_header headerSize = 28
08 01 00 00 // ResStringPool_header size = 264
07 00 00 00 // ResStringPool_header stringCount = 7
01 00 00 00 // ResStringPool_header styleCount = 1
00 01 00 00 // ResStringPool_header flags = 256
3C 00 00 00 // ResStringPool_header stringsStart = 60
F0 00 00 00 // ResStringPool_header stylesStart = 240 end sting pool head

00 00 00 00 // 第1个字符串的 stringOffset 60+12+0=72 0ffset = 40
10 00 00 00 // 第2个字符串的 stringOffset 60+12+16=88 下面6个字符串一样就不一一解释了
...
00 00 00 00 // 第1个style的 styleOffset 240+12+0=252
09 // 第1个字符串的字符数 9 0ffset = 72
0D // 第1个字符串的编码数 13 UTF-8字符串有两种长度:字符数量和编码长度;但是,UTF-16字符串只有1个长度:字符数。
E4 BD A0 E5 A5 BD 41 6E 64 72 6F 69 64 00 // 你好Android
...
02 00 00 00 // 第1个1style的 ResStringPool_ref nameIndex = 2 0ffset = 252 注意:一个字符串可以对应多个ResStringPool_span和一个ResStringPool_ref。ResStringPool_span在前描述字符串的样式,ResStringPool_ref在后固定值为0XFFFFFFFF作为占位符
02 00 00 00 // 第1个1style的 start
08 00 00 00 // 第1个1style的 end
FF FF FF FF // 循环判断第1个style的ResStringPool_span
FF FF FF FF FF FF FF FF //样式块最后会以两个值为0XFFFFFFFF的ResStringPool_ref作为结束

如上是RES_STRING_POOL Chunk的部分内容,头部信息我们就略过了,结合resources.arsc文件结构图,我们可以看到RES_STRING_POOL Chunk包含了基本信息(header字符串数,style数,标记flags,字符串的起始位置和style的其实位置)和一个字符串偏移数组,一个style偏移数组以及字符串数组和style数组;如下是RES_STRING_POOL Chunk的header,后边分析的Chunk header 都与我们之前分析的RES_TABLE Chunk和RES_STRING_POOL Chunk类似,后边就不一一说明了,关于RES_STRING_POOL Chunk的内容,在上述的二级制分析中,注释已经很详细了,请大家结合arscblamer-gradle的源码和其中的resource分析查看,另外微信的资源混淆就是在这里做了处理,混淆字符串并修改相应资源的目录。

RES_STRING_POOL Chunk header
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct ResStringPool_header
{
struct ResChunk_header header;

// Number of strings in this pool (number of uint32_t indices that follow
// in the data).
uint32_t stringCount;

// Number of style span arrays in the pool (number of uint32_t indices
// follow the string indices).
uint32_t styleCount;

// Flags.
enum {
// If set, the string index is sorted by the string values (based
// on strcmp16()).
SORTED_FLAG = 1<<0,

// String pool is encoded in UTF-8
UTF8_FLAG = 1<<8
};
uint32_t flags;

// Index from header of the string data.
uint32_t stringsStart;

// Index from header of the style data.
uint32_t stylesStart;
};
  • RES_TABLE_PACKAGE Chunk(PackageChunk.java,也是一个ChunkWithChunks)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
00 02       // RES_TABLE_PACKAGE_TYPE            = 0x0200 0ffset = 276   start package chunk head

20 01 // ResTable_package headerSize = 288
CC 06 00 00 // ResTable_package size = 1740
7F 00 00 00 // ResTable_package id = 0x7F //包的ID,等于Package Id,一般用户包的值Package Id为0X7F,系统资源包的Package Id为0X01
63 00 6F 00 6D 00 2E 00 65 00 78 00 61 00 6D 00 // ResTable_package name = com.example.borney.helloresource
70 00 6C 00 65 00 2E 00 62 00 6F 00 72 00 6E 00
65 00 79 00 2E 00 68 00 65 00 6C 00 6C 00 6F 00
72 00 65 00 73 00 6F 00 75 00 72 00 63 00 65 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20 01 00 00 // ResTable_package typeStrings = 288 //类型字符串资源池相对头部的偏移
00 00 00 00 // ResTable_package lastPublicType = 0 //最后一个导出的Public类型字符串在类型字符串资源池中的索引,目前这个值设置为类型字符串资源池的元素个数。
C8 01 00 00 // ResTable_package keyStrings = 456 //定义资源键符号表的ResStringPool_header的偏移量。 如果为零,则此包将从另一个基本包继承(重写其中的特定值)。
00 00 00 00 // ResTable_package lastPublicKey = 0 //最后一个由其他人公开使用的keyStrings索引。
00 00 00 00 // ResTable_package typeIdOffset = 0

和之前分析的两个Chunk类似,RES_TABLE_PACKAGE Chunk的头部信息如上分析

  • RES_TABLE_PACKAGE Chunk 的类型字符串池(StringPoolChunk.java)

如下是RES_TABLE_PACKAGE Chunk中的类型RES_STRING_POOL Chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
01 00       // RES_STRING_POOL_TYPE              = 0x0001   offset = 564    start sting pool head

1C 00 // ResStringPool_header headerSize = 28
A8 00 00 00 // ResStringPool_header size = 168
07 00 00 00 // ResStringPool_header stringCount = 7
00 00 00 00 // ResStringPool_header styleCount = 0
00 00 00 00 // ResStringPool_header flags = 0
38 00 00 00 // ResStringPool_header stringsStart = 56
00 00 00 00 // ResStringPool_header stylesStart = 0 end sting pool head
00 00 00 00 // 第1个字符串的 stringOffset 564+56+0 = 620 0ffset = 592
0E 00 00 00 // 第2个字符串的 stringOffset 564+56+14=634 下面6个字符串一样就不一一解释了
1C 00 00 00
...
05 // 第1个字符串的字符数 5
00 // UTF-16 offset 占位符
63 00 6F 00 6C 00 6F 00 72 00 00 00 // color
...
  • RES_TABLE_PACKAGE Chunk 的关键字字符串池(StringPoolChunk.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
01 00       // RES_STRING_POOL_TYPE              = 0x0001   offset = 732    start sting pool head

1C 00 // ResStringPool_header headerSize = 28
D0 00 00 00 // ResStringPool_header size = 208
0A 00 00 00 // ResStringPool_header stringCount = 10
00 00 00 00 // ResStringPool_header styleCount = 0
00 01 00 00 // ResStringPool_header flags = 256
44 00 00 00 // ResStringPool_header stringsStart = 68
00 00 00 00 // ResStringPool_header stylesStart = 0
00 00 00 00 // 第1个字符串的 stringOffset 732+68+0 = 800 0ffset = 760
...
83 00 00 00 // 第10个字符串的 stringOffset 732+68+131 = 931
0B
0B
63 6F 6C 6F 72 41 63 63 65 6E 74 00 // colorAccent
...
  • RES_TABLE_TYPE_SPEC Chunk(TypeSpecChunk.java)
1
2
3
4
5
6
7
8
9
10
02 02       // RES_TABLE_TYPE_SPEC_TYPE         = 0x0202   offset = 940

10 00 // ResTable_typeSpec headerSize = 16
1C 00 00 00 // ResTable_typeSpec size = 28
01 // ResTable_typeSpec id = 0x01
00 00 00 // ResTable_typeSpec res0 res1 占位符
03 00 00 00 // ResTable_typeSpec entryCount = 3
00 00 00 00 // 数组resources[0]的大小为0
00 00 00 00 // 数组resources[1]的大小为0
00 00 00 00 // 数组resources[2]的大小为0

关于RES_TABLE_TYPE_SPEC的详细介绍可以参考ResourceTypes.h中的注释和参考资料中的解释,这里我就不在赘述了(其实这个具体是什么我也没搞清楚)

  • RES_TABLE_TYPE Chunk(TypeChunk.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
01 02       // RES_TABLE_TYPE_TYPE              = 0x0201   offset = 968

4C 00 // ResTable_type headerSize = 76
88 00 00 00 // ResTable_type size = 136
01 // ResTable_type id = 0x01
00 00 00 // ResTable_type res0 res1 占位符
03 00 00 00 // ResTable_type entryCount = 3
58 00 00 00 // ResTable_type entriesStart = 88
38 00 00 00 // ResTable_config size = 56
00 00 // ResTable_config mcc = 0
00 00 // ResTable_config mnc = 0
00 00 // ResTable_config language{} = {0,0}
00 00 // ResTable_config country{} = {0,0}
00 // ResTable_config orientation = 0
00 // ResTable_config touchscreen = 0
00 00 // ResTable_config density = 0
00 // ResTable_config keyboard = 0
00 // ResTable_config navigation = 0
00 // ResTable_config inputFlags = 0
00 // ResTable_config inputPad0 = 0 对齐位
00 00 // ResTable_config screenWidth = 0
00 00 // ResTable_config screenHeight = 0
00 00 // ResTable_config sdkVersion = 0
00 00 // ResTable_config minorVersion = 0
00 // ResTable_config screenLayout = 0
00 // ResTable_config uiMode = 0
00 00 // ResTable_config smallestScreenWidthDp = 0
00 00 // ResTable_config screenWidthDp = 0
00 00 // ResTable_config screenHeightDp = 0
00 00 00 00 // ResTable_config localeScript{} = {0,0,0,0}
00 00 00 00 00 00 00 00 // ResTable_config localeVariant{} = {0,0,0,0,0,0,0,0}
00 // ResTable_config screenLayout2 = 0
00 // ResTable_config screenConfigPad1 = 0 占位符
00 00 // ResTable_config screenConfigPad2 = 0 占位符 bytesRead = 52
00 00 00 00 // ResTable_config unknown{} = {0,0,0,0} //size = cofigsize - 52
00 00 00 00 // ResTable_type 第1个ResTable_entry(Entry)的offset
10 00 00 00 // ResTable_type 第2个ResTable_entry(Entry)的offset
20 00 00 00 // ResTable_type 第3个ResTable_entry(Entry)的offset
08 00 // ResTable_type 第1个ResTable_entry headerSize = 8
00 00 // ResTable_type 第1个ResTable_entry flags = 0
00 00 00 00 // ResTable_type 第1个ResTable_entry keyIndex = 0 //引用标识此条目的ResTable_package :: keyStrings
08 00 // ResTable_type 第1个ResTable_entry的Res_value size = 8
00 // ResTable_type 第1个ResTable_entry的Res_value res0 = 0 //占位符
1D // ResTable_type 第1个ResTable_entry的Res_value dataType = 0x1D
81 40 FF FF // ResTable_type 第1个ResTable_entry的Res_value data = 0xFFFF4081
...
FF // Entry 添加结束

如上就是Type Chunk,这个就是我们apk不同config资源的详细信息,PackageChunk之后有着多个TypeSpecChunk和TypeChunk,这里我们也不在一一解释,详细可查看我的工程中的resource目录下的resources.md

修改的arscblamer-gradle

到这里,resources.arsc就分析的差不多了,arscblamer-gradle我将输出信息可以输出到excel中,类似于studio查看apk文件的方式

在工程下运行如下命令

1
java -jar libs/arsc-library-1.0-SNAPSHOT.jar --apk src/main/resources/app-debug.apk

可以看到如下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|--StringPoolChunk
index[0]=你好Android
index[1]=HelloResource
index[2]=b
index[3]=res/drawable/drawable.xml
index[4]=res/layout/activity_main.xml
index[5]=res/mipmap-xxhdpi-v4/ic_launcher.png
index[6]=res/mipmap-xxxhdpi-v4/ic_launcher_round.png
index[0]=StringPoolStyle{spans=[StringPoolSpan{b, start=2, stop=8}]}
|--PackageChunk
id=127
packageName=com.example.borney.helloresource
|--typeStringPool
index[0]=color
index[1]=dimen
index[2]=drawable
index[3]=layout
index[4]=mipmap
index[5]=string
index[6]=style
|--keyStringPool
index[0]=colorAccent
index[1]=colorPrimary
index[2]=colorPrimaryDark
index[3]=textsize
index[4]=drawable
index[5]=activity_main
index[6]=ic_launcher
index[7]=ic_launcher_round
index[8]=app_name
index[9]=Text
|--specChunks
specChunkId:1 resourceCount:3
specChunkId:2 resourceCount:1
specChunkId:3 resourceCount:1
specChunkId:4 resourceCount:1
specChunkId:5 resourceCount:2
specChunkId:6 resourceCount:1
specChunkId:7 resourceCount:1
|--typeChunks
typeChunkId:1
configuration:default
key:colorAccent flags:0 value:rgb8(ffff4081)
key:colorPrimary flags:0 value:rgb8(ff3f51b5)
key:colorPrimaryDark flags:0 value:rgb8(ff303f9f)
typeChunkId:2
configuration:default
key:textsize flags:0 value:dimension(3074)
typeChunkId:3
configuration:default
key:drawable flags:0 value:res/drawable/drawable.xml
typeChunkId:4
configuration:default
key:activity_main flags:0 value:res/layout/activity_main.xml
typeChunkId:5
configuration:xxxhdpi
key:ic_launcher_round flags:0 value:res/mipmap-xxxhdpi-v4/ic_launcher_round.png
typeChunkId:5
configuration:xxhdpi
key:ic_launcher flags:0 value:res/mipmap-xxhdpi-v4/ic_launcher.png
typeChunkId:6
configuration:default
key:app_name flags:0 value:HelloResource
typeChunkId:6
configuration:zh-rCN
key:app_name flags:0 value:你好Android
typeChunkId:7
configuration:default
key:Text flags:1 value:@ref/0x7f020000

是不是已经很清晰了,各个Chunk之间的关系也一目了然,至此,本文已经差不多了,我们做个简短的总结,1、一个resources.arsc文件的结构图;2、了解android源码中的ResourceTypes.h中数据结构和android-arscblamer工程对应的关系;3、结合具体apk的resources.arsc文件详细介绍了关键的Chunk,其他的我们就可以照猫画虎的去做了,4、通过android-arscblamer的完整输出我们进一步了解了resources.arsc中Chunk的组织关系。


参考资料
1、Android逆向之旅—解析编译之后的Resource.arsc文件格式
2、一个HelloWord的resources.arsc分析
3、Android资源管理框架(Asset Manager)简要介绍和学习计划