前言 resources.arsc 是 Android APK 中最核心的资源索引文件。如果说 AndroidManifest.xml 声明了 APK 的身份,那么 resources.arsc 就是APK 的资源数据库 ——它将文本形式的资源引用编译为紧凑的二进制格式,使运行时能以 O(1) 复杂度根据资源 ID 和当前设备配置找到对应的资源值。
本文基于 AOSP 15 的 androidfw 源码,从二进制层面完整解析 resources.arsc 的结构,并提供生产可用的 Python 解析器。
参考 : frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h (AOSP 15+ 头文件路径), frameworks/base/libs/androidfw/ResourceTypes.cpp (ResTable 实现), frameworks/base/tools/aapt2/ (编译/链接工具)
一、resources.arsc 是什么? 1.1 定位 resources.arsc 是编译后的资源表(Resource Table) ,记录了 APK 中所有非 assets 资源的索引信息。它本身不包含资源的实际数据(图片在 res/drawable/ 中,布局在 res/layout/ 中),而是提供资源的逻辑组织信息 ——名称、类型、配置、值。
1.2 解决的问题 开发者写的代码: val title = resources.getString(R.string.app_name) 编译后的代码: const-string v0, 0x7F020001 // app:string/app_name 的资源ID invoke-virtual {v0}, getString() 运行时: 1. 从资源ID 0x7F020001解析: PP=0x7F, TT=0x02, EEEE=0x0001 2. 在 resources.arsc 的 Package[PP] → Type[TT] → Entry[EEEE] 中查找 3. 根据当前设备配置(语言、屏幕密度、方向…)选择最优匹配的值
resources.arsc 的核心价值在于将人类可读的资源名称映射为整数 ID,并支持多配置适配 的高效查找。
二、整体架构 2.1 Chunk 层级 resources.arsc 使用与 AXML 相同的 ResChunk_header 体系。其顶层为嵌套的 chunk 树:
resources.arsc └── RES_TABLE_TYPE (0x0002) ← 根 chunk ├── StringPool (0x0001) ← 全局资源值字符串池 ├── Package (0x0200) ← 包 "com.example.app" (packageId=0x7F) │ ├── TypeStringPool (内嵌) ← Type 名称 ("string","drawable","layout"…) │ ├── KeyStringPool (内嵌) ← 资源 Key 名称 ("app_name","icon"…) │ ├── TypeSpec (0x0202) ← Type #01 (attr) │ │ ↑ 声明该 type 有哪些配置 │ ├── Type (0x0201) [config=默认] ← Type #01, config=默认 │ ├── Type (0x0201) [config=zh] ← Type #01, config=中文 │ ├── TypeSpec (0x0202) ← Type #02 (string) │ ├── Type (0x0201) [config=默认] ← Type #02, config=默认 │ ├── Type (0x0201) [config=en] ← Type #02, config=英文 │ ├── TypeSpec (0x0202) ← Type #03 (color) │ └── Type (0x0201) [config=默认] └── Package (0x0200) ← 包 "android" (packageId=0x01) └── ...
2.2 所有相关 Chunk 类型
常量
type 值
说明
RES_TABLE_TYPE
0x0002
资源表根节点
RES_STRING_POOL_TYPE
0x0001
字符串常量池
RES_TABLE_PACKAGE_TYPE
0x0200
包节点
RES_TABLE_TYPE_SPEC_TYPE
0x0202
Type 配置声明
RES_TABLE_TYPE_TYPE
0x0201
Type 具体配置的数据
RES_TABLE_LIBRARY_TYPE
0x0203
共享库声明
三、ResourceTable — 根节点 struct ResTable_header { ResChunk_header header; uint32_t packageCount; };
packageCount 定义了紧跟其后的 Package chunk 数量。通常为 1(应用自有资源)或 2(应用 + android 系统资源引用)。
四、Package Chunk 4.1 结构 struct ResTable_package { ResChunk_header header; uint32_t id; char16_t name[128 ]; uint32_t typeStrings; uint32_t lastPublicType; uint32_t keyStrings; uint32_t lastPublicKey; };
4.2 typeStrings — Type 名称表 一个内嵌的 StringPool,将 type ID 映射到名称:
typeStrings[1] = "attr" typeStrings[2] = "string" typeStrings[3] = "color" typeStrings[4] = "layout" typeStrings[5] = "drawable" typeStrings[6] = "mipmap" typeStrings[7] = "id" typeStrings[8] = "style" typeStrings[9] = "dimen" typeStrings[10] = "integer" typeStrings[11] = "bool" typeStrings[12] = "anim" typeStrings[13] = "xml" ...
4.3 keyStrings — 资源名称表 另一个内嵌 StringPool,将 entry ID 映射到名称:
keyStrings[1] = "app_name" keyStrings[2] = "ic_launcher" keyStrings[3] = "main_activity" keyStrings[4] = "primary_color" ...
五、TypeSpec — 配置声明 struct ResTable_typeSpec { ResChunk_header header; uint8_t id; uint8_t res0; uint16_t res1; uint32_t entryCount; };
TypeSpec 后面紧跟一个 uint32_t[entryCount] 数组,每个 uint32 是位掩码,声明该 entry 存在哪些配置变体 。
配置位掩码 AOSP 在 ResourceTypes.h 中定义了配置标志位,每个位代表一种配置维度。这些位值直接取自 ACONFIGURATION_* 系列常量(定义于 <androidfw/Configuration.h>):
位
常量 (Hex)
含义
0
ACONFIGURATION_MCC (0x0001)
移动国家码
1
ACONFIGURATION_MNC (0x0002)
移动网络码
2
ACONFIGURATION_LOCALE (0x0004)
语言和地区
3
ACONFIGURATION_TOUCHSCREEN (0x0008)
触摸屏类型
4
ACONFIGURATION_KEYBOARD (0x0010)
键盘类型
5
ACONFIGURATION_KEYBOARD_HIDDEN (0x0020)
键盘隐藏状态(硬键盘是否可见)
6
ACONFIGURATION_NAVIGATION (0x0040)
导航类型(方向键 / 触控板 / 轨迹球)
7
ACONFIGURATION_ORIENTATION (0x0080)
屏幕方向(竖屏 / 横屏 / 方屏)
8
ACONFIGURATION_DENSITY (0x0100)
屏幕密度(ldpi / mdpi / hdpi / xhdpi …)
9
ACONFIGURATION_SCREEN_SIZE (0x0200)
屏幕尺寸(small / normal / large / xlarge)
10
ACONFIGURATION_SMALLEST_SCREEN_SIZE (0x0400)
最小宽度(swdp)
11
ACONFIGURATION_SCREEN_LAYOUT (0x0800)
屏幕布局(长屏 / 宽屏 / 圆屏标志位)
12
ACONFIGURATION_UI_MODE (0x1000)
UI 模式(普通 / 车载 / 桌面 / 电视 / 手表 / VR)
13
ACONFIGURATION_LAYOUTDIR (0x2000)
布局方向(LTR / RTL)
14
ACONFIGURATION_COLOR_MODE (0x4000)
色彩模式(广色域 / HDR)
15
ACONFIGURATION_GRAMMATICAL_GENDER (0x8000)
语法性别(AOSP 15 新增)
16+
更高位
留给未来扩展
源码参考 : frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h 中定义 ResTable_config 的 CONFIG_* 枚举别名(如 CONFIG_MCC = ACONFIGURATION_MCC);frameworks/native/include/android/configuration.h 定义原始 ACONFIGURATION_* 值。
示例:如果一个 entry 有默认配置和 values-zh 两种变体,它的配置掩码会在 CONFIG_LOCALE 位(bit 2)被置位。如果有 values-zh-rCN-hdpi 变体,则同时置位 bit 2(LOCALE)和 bit 8(DENSITY)。
六、Type Chunk — 实际资源数据 这是 resources.arsc 中最核心、体积最大 的部分。每个 Type chunk 对应一种 type 的一个具体配置。
6.1 结构 struct ResTable_type { ResChunk_header header; uint8_t id; uint8_t flags; uint16_t reserved; uint32_t entryCount; uint32_t entriesStart; ResTable_config config; };
6.2 ResTable_config struct ResTable_config { uint32_t size; uint16_t mcc; uint16_t mnc; char language[2 ]; char country[2 ]; uint8_t orientation; uint8_t touchscreen; uint16_t density; uint8_t keyboard; uint8_t navigation; uint8_t inputFlags; uint8_t inputPad0; uint16_t screenWidth; uint16_t screenHeight; uint16_t sdkVersion; uint16_t minorVersion; uint8_t screenLayout; uint8_t uiMode; uint16_t smallestScreenWidthDp; uint16_t screenWidthDp; uint16_t screenHeightDp; char localeScript[4 ]; char localeVariant[8 ]; uint8_t screenLayout2; uint8_t colorMode; uint16_t screenConfig; ... };
6.3 配置匹配规则 当运行时请求资源时,Android 遍历同 type 的多个 Type chunk(每种配置一个),按以下规则评分:
精确匹配优先 :如果 language 和 country 完全匹配,得分最高
无语言指定次之 :如果 Type chunk 的 language 为 \0\0(即 values/ 目录,未指定语言),则是兜底配置
屏幕密度降级 :如果请求 xxhdpi 但只有 xhdpi,系统自动缩放
方向匹配 :横屏/竖屏
评分算法在 ResourceTypes.cpp 的 ResTable_config::match() 和 isBetterThan() 中实现。
6.4 entriesStart 与 offset 数组 紧接 ResTable_config 之后是 uint32_t[entryCount] 偏移量数组:
如果 offset[i] == NO_ENTRY (0xFFFFFFFF),该 entry 在此配置下不存在
否则 offset[i] 指向从 type chunk 头部算起的 ResTable_entry 位置
七、ResTable_entry 与 Res_value 7.1 简单的 ResTable_entry struct ResTable_entry { uint16_t size; uint16_t flags; uint32_t key; };
7.2 Res_value struct Res_value { uint16_t size; uint8_t res0; uint8_t dataType; uint32_t data; };
dataType 的含义与 AXML 中的 Res_value 一致。对于 TYPE_STRING (0x03),data 是全局 StringPool 中的索引;对于 TYPE_REFERENCE (0x01),data 是资源 ID。
7.3 复杂类型:ResTable_map_entry 对于 style、array、attr 等含子元素的复杂资源,使用 FLAG_COMPLEX 标记的扩展 entry:
struct ResTable_map_entry : public ResTable_entry { uint32_t parent; uint32_t count; }; struct ResTable_map { uint32_t name; Res_value value; };
例如一个 style 定义:
<style name ="AppTheme" parent ="Theme.AppCompat" > <item name ="colorPrimary" > #FF0000</item > <item name ="colorAccent" > #00FF00</item > </style >
编码为:
ResTable_map_entry: key = keyStrings["AppTheme"] = 3 flags = FLAG_COMPLEX parent = 0x7F080001 (style/Theme_AppCompat) count = 2 ResTable_map[0]: { name=0x01010001 (attr/colorPrimary), value={TYPE_INT_COLOR_ARGB8, 0xFFFF0000} } ResTable_map[1]: { name=0x01010002 (attr/colorAccent), value={TYPE_INT_COLOR_ARGB8, 0xFF00FF00} }
八、资源 ID 编码(0xPPTTEEEE) Android 资源 ID 使用 32 位紧凑编码,将资源所属的包、类型、索引信息压缩为一个整数:
31 24 23 16 15 0 ┌────────────┬────────────┬────────────┐ │ PP │ TT │ EEEE │ │ Package ID │ Type ID │ Entry ID │ └────────────┴────────────┴────────────┘ PP (8 bits): 0x01 = android.R, 0x7F = app.R, 0x00-0x0F = reserved TT (8 bits): 0x01=attr, 0x02=string, 0x03=color, 0x04=layout, 0x05=drawable... EEEE (16 bits): 条目序号(0x0000-0xFFFF,最多 65536 个条目/type)
示例解码:
0x7F040012: PP = 0x7F (app) TT = 0x04 (layout) EEEE = 0x0012 (第18个 layout) → R.layout.activity_main
def decode_resource_id (resid ): pp = (resid >> 24 ) & 0xFF tt = (resid >> 16 ) & 0xFF eeee = resid & 0xFFFF return pp, tt, eeee
包 ID 分配规则:
PP
用途
0x01
android 系统资源
0x02-0x6F
保留(未来扩展)
0x7F
应用主包
0x80-0xFE
动态资源 / 共享库
九、配置系统详解 resources.arsc 的配置系统是 Android 资源适配的核心。
9.1 多 Type chunk 组织 同一个 Type 可以存在多个 Type chunk,每个对应不同的设备配置。例如:
res/values/strings.xml → Type[0x02], config={language='\0', density=0} res/values-en/strings.xml → Type[0x02], config={language='en', density=0} res/values-zh-rCN/strings.xml → Type[0x02], config={language='zh', country='CN'} res/values-hdpi/strings.xml → Type[0x02], config={density=240}
同一个资源 app_name 可能在这些 Type chunk 中有不同的值(或者只在部分 chunk 中存在)。
9.2 资源查找流程 给定: resourceId=0x7F020001, 当前设备 config={lang=zh, density=480} 1. 解析 resourceId → PP=0x7F, TT=0x02, EEEE=0x0001 2. 找到 Package[0x7F] 3. 遍历所有 id=0x02 的 Type chunk 4. 对每个 Type chunk: a. 计算其 ResTable_config 与设备 config 的匹配分数 b. 检查 entry[1] 是否存在(offset != NO_ENTRY) c. 记录最高分匹配 5. 返回最高分解码出的 Res_value
9.3 配置标志组合 TypeSpec 中的配置掩码提供了快速判断:如果某个配置位不匹配,可直接跳过该 Type chunk 而非逐个 entry 检查:
TypeSpec[0x02].configMask[entry=1] = 0x0104 → bit 2 (LOCALE=语言/地区), bit 8 (DENSITY=屏幕密度) → 该 entry 有两种配置维度,可能对应以下 Type chunk: Type[config=默认] (language='\0', density=0) Type[config=zh] (language='zh', density=0) Type[config=en] (language='en', density=0) Type[config=hdpi] (language='\0', density=240) → 总共 4 个 Type chunk 包含该 entry 的值
十、完整 Python ARSC 解析器 """resources.arsc 解析器 — dump 所有资源定义""" import structimport sysTYPE_NULL = 0 ; TYPE_REFERENCE = 1 ; TYPE_STRING = 3 TYPE_INT_DEC = 0x10 ; TYPE_INT_HEX = 0x11 TYPE_INT_BOOLEAN = 0x12 ; TYPE_INT_COLOR_ARGB8 = 0x1C CHUNK_TABLE = 0x0002 CHUNK_STRING_POOL = 0x0001 CHUNK_PACKAGE = 0x0200 CHUNK_TYPE_SPEC = 0x0202 CHUNK_TYPE = 0x0201 class ARSCParser : def __init__ (self, path ): with open (path, 'rb' ) as f: self .data = f.read() self .pos = 0 self .string_pool = [] self .packages = {} def parse_header (self ): typ, hdr_size, size = struct.unpack_from('<HHI' , self .data, self .pos) return typ, hdr_size, size def parse_stringpool (self, start ): """解析 StringPool,返回字符串列表""" hdr = self .data[start:start+28 ] _, hdr_sz, chunk_sz, str_cnt, style_cnt, flags, str_start, style_start = \ struct.unpack_from('<HHIIIII' , hdr, 0 ) utf8 = (flags >> 8 ) & 1 strings = [] off_table = start + hdr_sz off_data = start + str_start for i in range (str_cnt): str_off = struct.unpack_from('<I' , self .data, off_table + i * 4 )[0 ] if utf8: pos = off_data + str_off result = 0 ; shift = 0 while True : b = self .data[pos]; pos += 1 result |= (b & 0x7F ) << shift if b & 0x80 == 0 : break shift += 7 char_count = result result = 0 ; shift = 0 while True : b = self .data[pos]; pos += 1 result |= (b & 0x7F ) << shift if b & 0x80 == 0 : break shift += 7 byte_len = result s = self .data[pos:pos+byte_len-1 ].decode('utf-8' , errors='replace' ) strings.append(s) else : pos = off_data + str_off result = 0 ; shift = 0 while True : b = self .data[pos]; pos += 1 result |= (b & 0x7F ) << shift if b & 0x80 == 0 : break shift += 7 char_count = result raw = self .data[pos:pos + char_count * 2 ] s = raw.decode('utf-16-le' , errors='replace' ) strings.append(s) return strings, start + chunk_sz def parse_config (self, offset ): """解析 ResTable_config 并返回 (config_dict, next_offset) ResTable_config 字段布局 (from AOSP ResourceTypes.h): size(4) mcc(2) mnc(2) language[2] country[2] orientation(1) touchscreen(1) density(2) keyboard(1) navigation(1) inputFlags(1) inputPad0(1) screenWidth(2) screenHeight(2) sdkVersion(2) minorVersion(2) screenLayout(1) uiMode(1) smallestScreenWidthDp(2) screenWidthDp(2) screenHeightDp(2) // 扩展字段通过 size 判断是否存在 """ sz = struct.unpack_from('<I' , self .data, offset)[0 ] base_fmt = '<IHH2s2sBBHBBBBHHHHBBHHH' base_len = struct.calcsize(base_fmt) if sz >= base_len: fields = struct.unpack_from(base_fmt, self .data, offset) size, mcc, mnc, lang_raw, country_raw, orient, touch, density, \ kbd, nav, inp_flags, pad0, sw, sh, sdk, minor, scr_layout, ui_mode, \ sw_dp, w_dp, h_dp = fields lang = lang_raw.decode('ascii' , errors='replace' ).rstrip('\0' ) country = country_raw.decode('ascii' , errors='replace' ).rstrip('\0' ) config = {} if mcc: config['mcc' ] = mcc if mnc: config['mnc' ] = mnc if lang: config['language' ] = lang if country: config['country' ] = country if density: config['density' ] = density if sdk: config['sdk' ] = sdk if orient: config['orientation' ] = orient if ui_mode: config['uiMode' ] = ui_mode return config, offset + size def parse_value (self, offset ): """解析 Res_value""" sz, res0, dtype, data = struct.unpack_from('<H B B I' , self .data, offset) dtype = dtype & 0xFF return {'type' : dtype, 'data' : data}, offset + 8 def parse (self ): resources = [] while self .pos < len (self .data): typ, hdr_size, chunk_size = self .parse_header() chunk_end = self .pos + chunk_size if typ == CHUNK_TABLE: pkg_cnt = struct.unpack_from('<I' , self .data, self .pos + 8 )[0 ] print (f"ResourceTable: {pkg_cnt} packages" ) self .pos += hdr_size elif typ == CHUNK_STRING_POOL: self .string_pool, self .pos = self .parse_stringpool(self .pos) elif typ == CHUNK_PACKAGE: pkg_id = struct.unpack_from('<I' , self .data, self .pos + 8 )[0 ] pkg_name_raw = self .data[self .pos+12 : self .pos+12 +256 ] pkg_name = pkg_name_raw.decode('utf-16-le' , errors='replace' ).rstrip('\0' ) type_str_start = struct.unpack_from('<I' , self .data, self .pos + 12 +256 )[0 ] key_str_start = struct.unpack_from('<I' , self .data, self .pos + 12 +256 +8 )[0 ] pkg_start = self .pos type_strings, _ = self .parse_stringpool(pkg_start + type_str_start) key_strings, _ = self .parse_stringpool(pkg_start + key_str_start) print (f"\nPackage: {pkg_name} (id=0x{pkg_id:02X} )" ) print (f" Types: {type_strings[1 :11 ]} ..." ) self .pos = pkg_start + hdr_size elif typ == CHUNK_TYPE_SPEC: tid = self .data[self .pos + 8 ] entry_cnt = struct.unpack_from('<I' , self .data, self .pos + 12 )[0 ] self .pos = chunk_end elif typ == CHUNK_TYPE: tid = self .data[self .pos + 8 ] entry_cnt = struct.unpack_from('<I' , self .data, self .pos + 12 )[0 ] entries_start = struct.unpack_from('<I' , self .data, self .pos + 16 )[0 ] config, _ = self .parse_config(self .pos + 20 ) off_base = self .pos + entries_start for i in range (entry_cnt): ent_off = struct.unpack_from('<I' , self .data, off_base + i * 4 )[0 ] if ent_off == 0xFFFFFFFF : continue ent_pos = self .pos + ent_off esize, eflags, ekey = struct.unpack_from('<HHI' , self .data, ent_pos) key_name = key_strings[ekey] if ekey < len (key_strings) else f"key[{ekey} ]" if eflags & 0x0001 : resources.append({ 'type' : type_strings[tid] if tid < len (type_strings) else f'type{tid} ' , 'key' : key_name, 'config' : config, 'complex' : True , 'resid' : (0x7F << 24 ) | (tid << 16 ) | i, }) else : val, _ = self .parse_value(ent_pos + 8 ) resources.append({ 'type' : type_strings[tid] if tid < len (type_strings) else f'type{tid} ' , 'key' : key_name, 'config' : config, 'value' : val, 'resid' : (0x7F << 24 ) | (tid << 16 ) | i, }) self .pos = chunk_end else : self .pos = chunk_end return resources if __name__ == '__main__' : parser = ARSCParser(sys.argv[1 ] if len (sys.argv) > 1 else 'resources.arsc' ) results = parser.parse() for r in results[:50 ]: cfg_str = ',' .join(f'{k} ={v} ' for k, v in r.get('config' , {}).items()) if r.get('complex' ): print (f" 0x{r['resid' ]:08X} {r['type' ]} /{r['key' ]} [{cfg_str} ] (complex)" ) else : v = r['value' ] print (f" 0x{r['resid' ]:08X} {r['type' ]} /{r['key' ]} = [{v['type' ]} ]{v['data' ]} [{cfg_str} ]" ) print (f"\nTotal: {len (results)} resources" )
十一、逆向实战应用 11.1 提取应用所有字符串资源 aapt2 dump resources app.apk | grep "string/" python3 arsc_parser.py resources.arsc | grep "type=string"
11.2 资源混淆检测 商业加固工具(如 AndResGuard)会混淆资源 ID,将 R.string.app_name 重命名为短的随机名称(如 R.string.a)。通过对比原始和混淆后的 resources.arsc 的 keyStrings,可以检测是否被混淆。
11.3 资源修复 反编译后修改资源值(如替换图片引用、修改颜色、更改字符串),可以通过操作 ResTable_entry 的 Res_value.data 实现,无需完全反编译 APK。
11.4 反编译对抗 某些 APP 会检测 resources.arsc 的完整性(checksum/hash),在混淆后加入资源校验。绕过方式:重新计算并修补校验值,或 Hook 校验函数。
十二、AAPT2 编译流程详解 12.1 AAPT2 的两阶段架构 AAPT2(Android Asset Packaging Tool 2)从 Android Gradle Plugin 3.0.0 开始成为默认资源编译器。它采用分离的编译-链接 两阶段设计,替代了 AAPT1 的单次全量处理:
源文件 (res/values/strings.xml, res/layout/activity.xml ...) │ ▼ 阶段 1: 编译 (compile) ┌───────────────────────────────────────┐ │ aapt2 compile │ │ 每个资源文件 → 独立的 *.flat 文件 │ │ - XML 文本 → 二进制 ResXML* 结构 │ │ - 字符串 → Value 结构 │ │ - 图片资源 → 记录路径引用 │ │ 输出: build/intermediates/compiled_res/ │ │ ├── values_strings.arsc.flat │ │ ├── layout_activity.xml.flat │ │ └── drawable_icon.png.flat │ └───────────────────────────────────────┘ │ ▼ 阶段 2: 链接 (link) ┌───────────────────────────────────────┐ │ aapt2 link │ │ 1. 收集所有 *.flat 文件 │ │ 2. 汇总所有资源定义 │ │ 3. 分配资源ID (0xPPTTEEEE 编号体系) │ │ 4. 构建 StringPool:去重、排序、编码 │ │ 5. 构建层次结构: │ │ ResourceTable → Package → │ │ TypeSpec → Type (×N 配置) │ │ 6. 输出: resources.arsc + │ │ 压缩后的各类资源文件 │ └───────────────────────────────────────┘
12.2 编译阶段:XML → flat 在编译阶段,每个资源 XML 文件被独立编译为中间二进制格式 .flat(Android 11+ 改为 .asrc.flat 扩展名,但本质相同)。编译过程:
XML 解析 :将文本 XML 解析为 DOM 树
字符串提取 :提取所有 android:name 和文本内容,分配临时的局部索引
类型推断 :根据元素标签推断资源值类型(<string> → TYPE_STRING, <color> → TYPE_INT_COLOR_ARGB8, <dimen> → TYPE_DIMENSION 等)
二进制编码 :
普通值:直接编码为 Res_value 结构(dataType + data)
复杂值(style/array):编码为 ResTable_map_entry + ResTable_map[] 结构
引用:保持为 @type/name 字符串形式,链接阶段才解析为资源 ID
输出 :ResTable 格式的扁平文件(类似一个小型 resources.arsc),含局部 StringPool + 资源项
<resources > <string name ="app_name" > My App</string > <string name ="welcome" > Hello, %1$s!</string > </resources >
编译为 flat 后(伪代码表示):
FlatFile { StringPool: ["app_name", "welcome", "My App", "Hello, %1$s!"] Entry[0]: key=0("app_name"), value={TYPE_STRING, data=2("My App")} Entry[1]: key=1("welcome"), value={TYPE_STRING, data=3("Hello, %1$s!")} }
关键:编译阶段不分配最终的资源 ID ,entry 仅以 key 名称索引。
12.3 链接阶段:flat → resources.arsc 链接阶段将所有 .flat 文件合并为最终的 resources.arsc:
步骤 1: 收集与去重 aapt2 link 扫描所有 .flat 文件 → - 合并同名资源(不同配置的同一资源视为同一 entry 的不同 config) - 检测冲突:同配置下同名的资源定义会触发编译错误 - 合并 StringPool:所有字符串去重并重新编码
步骤 2: 资源 ID 分配 分配策略(自动递增): ┌──────────┬──────────┬──────────────────────┐ │ Package │ Type ID │ Entry ID 分配范围 │ ├──────────┼──────────┼──────────────────────┤ │ 0x01 │ (系统) │ 由 framework-res 定义 │ │ 0x7F │ 0x01 │ attr: 0x0000..0xFFFF │ │ 0x7F │ 0x02 │ string: 0x0000..0xFFFF│ │ 0x7F │ 0x03 │ color: 0x0000..0xFFFF │ │ ... │ ... │ ... │ └──────────┴──────────┴──────────────────────┘ 分配规则: 1. 从 0x00 开始按文件中出现顺序递增分配 entry ID 2. 如果指定了 public.xml,优先分配其中声明的固定 ID 3. Type ID 按首次遇到的资源类型递增分配 4. 保留 0x01 到 0x0F 给 AOSP 系统类型(attr, string, color, layout, drawable, mipmap, id, style, dimen, integer, bool, anim, xml, raw, interpolator)
步骤 3: 构建 StringPool 全局资源值 StringPool(位于 ResourceTable 根级别): StringPool[0] = ""(保留) StringPool[1] = "My App" StringPool[2] = "Hello, %1$s!" ... 类型名称 StringPool(位于 Package 内部): StringPool[1] = "attr" StringPool[2] = "string" StringPool[3] = "color" ... 资源 Key StringPool(位于 Package 内部): StringPool[1] = "app_name" StringPool[2] = "welcome" ...
StringPool 使用增量编码 :所有字符串排序后,后面的字符串可以引用前面字符串的子串作为前缀,通过字节偏移量来节省空间。
步骤 4: 构建 TypeSpec + Type 层次 对于每个 (Package, Type) 组合: 1. 创建 TypeSpec chunk: - 声明 entryCount - 为每个 entry 计算配置掩码(位图表示存在哪些配置变体) 2. 为每种存在的配置创建 Type chunk: - 写入 ResTable_config - 构建 entriesStart + offset 数组 - 写入各 entry 的 Res_value 或 ResTable_map_entry
步骤 5: 生成最终文件 最终 resources.arsc 二进制布局: ResTable_header (packageCount=N) StringPool(全局) Package[0]: TypeStringsPool KeyStringsPool TypeSpec[type=attr] → Type[attr][默认] → Type[attr][zh] → ... TypeSpec[type=string] → Type[string][默认] → Type[string][en] → ... ... Package[1]: ...
12.4 AAPT2 vs AAPT1 对比
维度
AAPT1
AAPT2
编译模型
单次全量处理
分离编译 + 链接
增量编译
不支持
支持(只编译变更文件)
中间格式
无
*.flat 可缓存
资源 ID 固定
编译时立即分配
链接时统一分配
产物
APK 直接产出
可输出未签名的中间 APK
性能
大项目慢
多核并行编译,显著更快
AOSP 源码参考 : frameworks/base/tools/aapt2/compile/ — 编译阶段;frameworks/base/tools/aapt2/link/ — 链接阶段;frameworks/base/tools/aapt2/ResourceTable.cpp — 资源表构建核心逻辑。
十三、配置匹配算法深入 13.1 算法概览 resources.arsc 的核心价值之一是自动配置匹配 :当同一资源有多个配置变体时,Android 运行时选择一个”最优”匹配。匹配算法在 ResourceTypes.cpp 的 ResTable_config::match() 和 isBetterThan() 中实现。
输入: 设备当前配置 (requestedConfig) 资源表中有 N 个同 type 的 Type chunk,每个对应一种目标配置 算法: for each Type_chunk in Type[requested_type]: score = ResTable_config::match(targetConfig, requestedConfig) if score != NO_MATCH: 记录当前最佳匹配 = isBetterThan(current, best) ? current : best 返回最佳 Type chunk 中 entry[entryIndex] 的值
13.2 匹配优先级顺序 配置字段的匹配按以下顺序依次比较,先比较的字段优先级更高 :
1. MCC (移动国家码) 2. MNC (移动网络码) 3. Locale (语言+地区) 4. LayoutDirection (布局方向) 5. SmallestScreenWidthDp (最小宽度) 6. AvailableWidthDp (可用宽度) // Android 3.2+ 7. AvailableHeightDp (可用高度) // Android 3.2+ 8. ScreenSize (屏幕尺寸) 9. Density (屏幕密度) 10. Orientation (屏幕方向) 11. UIMode (UI 模式) 12. NightMode (夜间模式)
核心原则 : 更”具体”的配置优先级高于更”通用”的配置。例如 values-zh-rCN 比 values-zh 更具体,values-zh 比 values(默认)更具体。
13.3 ResTable_config::match() 评分细则 ssize_t ResTable_config::match (const ResTable_config& settings) const { if (mcc != 0 ) { if (mcc == settings.mcc) score += MCC_SCORE; else return NO_MATCH; } if (mnc != 0 ) { if (mnc == settings.mnc) score += MNC_SCORE; else return NO_MATCH; } if (locale != "" ) { if (language == settings.language) { if (country == settings.country) score += LOCALE_EXACT_SCORE + LOCALE_REGION_SCORE; else score += LOCALE_EXACT_SCORE; } else return NO_MATCH; } if (density != 0 ) { if (density == settings.density) score += DENSITY_SCORE; else if (density < settings.density) score += DENSITY_LOWER_SCORE; else score += DENSITY_HIGHER_SCORE; } }
13.4 isBetterThan() 比较逻辑 当两个配置都能匹配时,isBetterThan() 决定哪个”更好”:
bool ResTable_config::isBetterThan (const ResTable_config& o, const ResTable_config* requested) const { if (isNullConfig ()) return false ; if (o.isNullConfig ()) return true ; if (mcc != o.mcc) { return (mcc != 0 ) && (mcc == requested->mcc); } if (mnc != o.mnc) { return (mnc != 0 ) && (mnc == requested->mnc); } }
13.5 密度匹配与缩放 密度匹配有特殊规则:
设备请求 480 dpi (xxhdpi): 1. 有 xxhdpi 资源 → 直接使用(得分最高) 2. 有 xxxhdpi (640) → 缩小显示(得分次高,图片质量好但浪费内存) 3. 有 xhdpi (320) → 放大显示(得分第三,可能模糊) 4. 有 hdpi (240) → 大幅放大(得分最低) 5. 有 nodpi 资源 → 直接使用(不缩放) scaling_ratio = target_density / resource_density 例如: 480/320 = 1.5x 放大 480/640 = 0.75x 缩小
13.6 Locale 回退链 Locale 匹配的三级回退: 1. language + country 完全匹配: zh-rCN == zh-rCN → 最佳 2. 仅 language 匹配: zh-rCN vs zh → 次佳 3. 无 language 指定: zh-rCN vs (空) → 兜底 特殊规则: - zh-rCN 可以回退到 zh,但不能回退到 zh-rTW(不同地区) - en 是特殊语言:en-rUS 的回退链:en-rUS → en → (空) - 脚本(script)匹配:Latn, Hans, Hant 等也参与匹配(AOSP 21+ 扩展字段)
13.7 完整匹配示例 设备: zh-rCN, xxhdpi (480dpi), 竖屏, sw360dp 资源表中有: Type[config=默认] → app_name = "Default" Type[config=zh] → app_name = "你好" Type[config=zh-rCN] → app_name = "你好中国" Type[config=zh-rCN-hdpi] → app_name = "你好中国 hdpi" 匹配过程: vs 默认: score = 完全通用, 最低 vs zh: score = locale 语言匹配 vs zh-rCN: score = locale 完全匹配 ★ 赢 vs zh-rCN-hdpi: locale 匹配但 density 不匹配 (240 vs 480), 需要缩放,得分不如 zh-rCN 的精确 locale 匹配 最终选择: "你好中国" (Type[config=zh-rCN])
源码参考 : frameworks/base/libs/androidfw/ResourceTypes.cpp 中 ResTable_config::match() 和 isBetterThan() 实现,约 300 行的完整评分逻辑。
十四、Overlay 与 RRO (Runtime Resource Overlay) 14.1 什么是 RRO RRO(Runtime Resource Overlay)是 Android 8.0(API 26)引入的运行时资源替换 机制。它允许独立的 APK 包 在运行时覆盖目标 APK 的资源值,无需修改原始 APK。RRO 广泛用于:
系统主题 (Android 深色主题通过 overlay 实现)
OEM 定制 (厂商替换系统 UI 资源)
Carrier 定制 (运营商特定配置)
动态换肤 (第三方主题引擎)
14.2 RRO APK 结构 RRO APK 是一个轻量级 APK ,通常只包含:
overlay.apk ├── AndroidManifest.xml ← 声明 overlay 目标包和优先级 ├── resources.arsc ← 仅包含需要覆盖的资源条目 └── res/ └── values/ └── strings.xml ← 仅定义要覆盖的字符串
AndroidManifest.xml 关键声明:
<manifest package ="com.example.overlay" > <overlay android:targetPackage ="com.example.target" android:targetName ="MyOverlay" android:priority ="10" android:isStatic ="false" /> <application > </application > </manifest >
14.3 Overlay 优先级与包 ID 映射 RRO 的核心机制是包 ID 映射 :overlay 的 resources.arsc 使用自己的 package ID(通常是 0x7F),但运行时 AssetManager 将其映射到目标包的 ID :
Overlay APK 编译时: resources.arsc 使用 Package ID = 0x7F R.string.app_name → 0x7F020001 运行时 AssetManager 加载: 1. 读取 overlay 的 AndroidManifest.xml 2. 确认 targetPackage = "com.example.target" 3. 建立 ID 映射: overlay 0x7F → target 0x7F 4. 当请求 target 0x7F020001 时,优先返回 overlay 中映射后的 0x7F020001
优先级规则:
overlay priority 值越大 → 优先级越高 同优先级 → 按安装顺序,后安装的优先 static overlay (isStatic=true) → 不可被禁用,始终生效 runtime overlay → 可通过 OverlayManager API 动态启用/禁用
14.4 AssetManager 合并逻辑 AssetManager.java 在加载资源时按以下顺序查找:
1. Runtime Overlays(动态启用,优先级从高到低) 2. Static Overlays(编译时声明,优先级从高到低) 3. 目标 APK 自身的 resources.arsc(最后回退) 4. 系统 framework-res.apk(兜底) 查找流程: getString(0x7F020001) → AssetManager 遍历 APK 列表 → 每个 overlay: 检查是否存在映射为 0x7F020001 的资源 → 第一个找到的值即为最终结果 → 如果所有 overlay 都不包含该 entry,使用目标 APK 自身值
14.5 Overlay 的 resources.arsc 特点 Overlay APK 的 resources.arsc 与普通 APK 有显著区别:
正常 APK 的 resources.arsc: TypeSpec[string] → entryCount = 200(声明所有 200 个字符串) Type[string][默认] → 200 个 entry Overlay APK 的 resources.arsc: TypeSpec[string] → entryCount = 3(只声明需要覆盖的 3 个字符串) Type[string][默认] → 仅 3 个 entry(app_name, welcome, goodbye)
Overlay 的 entry ID 必须与目标 APK 中相同资源的 ID 完全一致
未被覆盖的 entry 在 overlay ARSC 中标记为 NO_ENTRY (0xFFFFFFFF)
Overlay 可能只覆盖特定配置(如仅覆盖 values-zh 的语言资源)
14.6 Static vs Runtime Overlay
特性
Static Overlay
Runtime Overlay
声明方式
android:isStatic="true"
android:isStatic="false"
生效时机
编译时 / 系统启动时
运行时动态切换
能否禁用
否(始终生效)
可动态启用/禁用
是否需要签名
需要平台签名
需要系统权限 (CHANGE_OVERLAY_PACKAGES)
典型用途
厂商硬件定制
主题引擎 / 动态换肤
IDMAP 文件
需要(/data/resource-cache/)
需要
AOSP 支持
Android 8.0+
Android 10+ 完善
idmap 文件是编译 overlay 时生成的二进制映射表,记录 overlay 资源 ID 到目标资源 ID 的映射关系,由 idmap2 工具生成。
源码参考 : frameworks/base/core/java/android/content/res/AssetManager.java — 资源加载与 overlay 合并;frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java — Overlay 包管理;frameworks/base/cmds/idmap2/ — idmap 生成工具源码。
十五、资源混淆 (AndResGuard) 原理 15.1 为什么需要资源混淆 APK 中的资源名称(如 R.string.app_name、R.drawable.ic_launcher_background)在 resources.arsc 的 keyStrings 中以明文存储。这些名称泄露了:
应用功能模块(activity_login, fragment_payment…)
第三方 SDK(com_facebook_*, google_api_*…)
内部命名规范
资源混淆通过缩短这些名称来减小 APK 体积 并增加逆向难度。
15.2 AndResGuard 工作流 AndResGuard 是最主流的开源 Android 资源混淆工具,其核心流程:
原始 APK │ ▼ 1. 解压 APK → 获取 resources.arsc + res/ 文件 │ ▼ 2. 解析 resources.arsc │ ├── 读取全局 StringPool │ ├── 读取 Package → TypeStringsPool, KeyStringsPool │ └── 记录所有资源引用关系 │ ▼ 3. 混淆字符串 │ ├── 对 KeyStringsPool: "app_name" → "a"(最短可用名称) │ ├── 对 TypeStringsPool: 通常保留("string","drawable"... 不变) │ ├── 对全局 StringPool: 文件路径字符串缩短 │ │ "res/drawable/ic_launcher.png" → "r/d/a.png" │ └── 白名单保留:不混淆 proguard-android.txt 中引用的资源 │ ▼ 4. 重写 resources.arsc │ ├── 重新编码 StringPool(UTF-8/UTF-16) │ ├── 更新所有偏移量引用 │ ├── 更新 entriesStart(entry 数组偏移可能因 pool 变大/变小而改变) │ └── 重新计算所有 chunk size │ ▼ 5. 重命名 res/ 目录下的文件 │ res/layout/activity_main.xml → res/layout/b.xml │ res/drawable/ic_launcher.png → res/drawable/c.png │ ▼ 6. 重新打包 APK + 签名
15.3 AndResGuard 修改 resources.arsc 的关键步骤 (a) 修改 StringPool def obfuscate_key_pool (key_pool ): mapping = {} used_names = set () for i, name in enumerate (key_pool): if is_whitelisted(name): new_name = name else : new_name = generate_short_name(i, used_names) mapping[name] = new_name return rebuild_pool(mapping.values()), mapping
(b) 同步更新资源 ID 引用 虽然资源的 Package ID 和 Type ID 保持不变,但 Entry ID 可能因排序变化 :
原始: string/app_name → 0x7F020000 string/welcome → 0x7F020001 string/goodbye → 0x7F020002 混淆后(按字母序重排 entry): string/b → 0x7F020000 (原 goodbye, 现最短名) string/a → 0x7F020001 (原 app_name) string/c → 0x7F020002 (原 welcome)
关键一致性维护 :
resources.arsc 中的 entry 顺序和 entry ID 必须一致
classes.dex 中硬编码的资源 ID 常量也必须同步更新
AndroidManifest.xml 中引用的资源 ID 同步更新
其他二进制 XML(layout, menu…)中的 @type/name 引用需重新映射
(c) 更新 entriesStart 偏移 混淆后 StringPool 的大小可能改变(通常因为字符串缩短而变小),导致 Type chunk 中的 entriesStart 偏移量需要重新计算:
修改前: Type[type=string]: header: 8 bytes id/flags/reserved: 4 bytes entryCount/entriesStart: 8 bytes config: ~40 bytes entriesStart = 60 ← offset 数组从 chunk 头偏移 60 开始 修改后 (StringPool 缩小): entriesStart = 56 ← 新偏移(config 小了 4 字节) 修改后 (StringPool 增大): entriesStart = 64 ← 新偏移(config 大了 4 字节)
15.4 检测 APK 是否被资源混淆 以下特征可以检测 APK 的资源是否经过混淆:
1. 检查 keyStrings 中的名称长度: 正常: "app_name", "ic_launcher_background", "activity_main" 混淆: "a", "b", "c", "aa", "ab"(1-3 个字符的无意义名称) 2. 检查 TypeStrings: 正常: ["", "attr", "string", "color", "layout", "drawable", "mipmap", "id", "style", "dimen", "integer", "bool"] 混淆(部分工具): ["", "a", "b", "c", "d", ...](连类型名也混淆) 3. 检查资源文件路径: 正常: res/drawable-xxhdpi/ic_launcher.png 混淆: r/d/a.png(无意义的短路径) 4. 检查 resourceID 规律: 正常: 0x7F020000 → 0x7F020001 → 0x7F020002(连续递增) 混淆后: 可能打乱顺序,entry ID 不再连续递增 5. 比对 apktool 反编译结果: apktool 反编译后的 res/values/public.xml 会展示混淆后的资源名称
15.5 对抗与绕过 对于逆向工程师: - 混淆后的资源命名对自动化分析无实质影响(资源 ID 仍然有效) - 可以通过语义分析推断资源用途(字符串内容、引用关系) - 部分工具提供白名单还原(如果知道原始 APK 的映射关系) 对于加固开发者: - 结合资源混淆 + DEX 混淆 + 字符串加密 形成多层防护 - 但需注意:混淆后仍需保证资源引用一致性,否则运行时崩溃 - public.xml 中声明的资源不能混淆(保持 ID 不变)
源码参考 : github.com/shwenzhang/AndResGuard — 开源资源混淆工具,包含 ResourceProguard.cpp (核心混淆逻辑)、AxmlEdit.cpp (AXML 编辑)、SevenZipWrapper.cpp (APK 解压/重打包)。
十六、实战: 提取与对比多版本 APK 的资源变化 16.1 需求背景 在逆向分析中,经常需要对比同一应用两个版本的资源变化:
恶意软件分析 :新版本是否添加了钓鱼字符串?是否修改了权限描述?
SDK 审计 :第三方 SDK 升级后新增了哪些资源?
国际化审查 :某个语言的翻译是否被篡改?
竞品分析 :追踪功能迭代对应的资源变化
16.2 资源 Diff 脚本 以下 Python 脚本基于前文的 ARSC 解析器,实现两个 resources.arsc 的差异对比:
"""resources.arsc Diff Tool — 对比两个 APK 版本的资源变化""" import sysimport jsonfrom collections import defaultdictdef build_resource_index (parser_results ): """构建 资源ID → {config: value} 的索引""" index = defaultdict(dict ) for r in parser_results: resid = r['resid' ] cfg_key = tuple (sorted (r.get('config' , {}).items())) index[resid][cfg_key] = r return index def diff_resources (old_index, new_index ): """找出新增、删除、变更的资源""" old_ids = set (old_index.keys()) new_ids = set (new_index.keys()) print ("=" * 60 ) print ("资源差异报告" ) print ("=" * 60 ) added = new_ids - old_ids if added: print (f"\n[+] 新增资源: {len (added)} 项" ) for rid in sorted (added): entry = list (new_index[rid].values())[0 ] print (f" 0x{rid:08X} ({entry['type' ]} /{entry['key' ]} )" ) removed = old_ids - new_ids if removed: print (f"\n[-] 移除资源: {len (removed)} 项" ) for rid in sorted (removed): entry = list (old_index[rid].values())[0 ] print (f" 0x{rid:08X} ({entry['type' ]} /{entry['key' ]} )" ) common = old_ids & new_ids changed = [] for rid in common: old_entry = old_index[rid] new_entry = new_index[rid] for cfg in old_entry: if cfg in new_entry: old_val = old_entry[cfg].get('value' , {}) new_val = new_entry[cfg].get('value' , {}) if old_val != new_val: changed.append((rid, cfg, old_val, new_val)) if changed: print (f"\n[*] 值变更: {len (changed)} 项" ) for rid, cfg, old_val, new_val in changed: r = old_index[rid][cfg] cfg_str = ',' .join(f'{k} ={v} ' for k, v in dict (cfg).items()) print (f" 0x{rid:08X} ({r['type' ]} /{r['key' ]} ) [{cfg_str} ]" ) print (f" 旧值: {old_val} " ) print (f" 新值: {new_val} " ) print (f"\n[#] 配置变体变化:" ) for rid in sorted (common): old_cfgs = set (old_index[rid].keys()) new_cfgs = set (new_index[rid].keys()) added_cfgs = new_cfgs - old_cfgs removed_cfgs = old_cfgs - new_cfgs if added_cfgs or removed_cfgs: r = list (new_index[rid].values())[0 ] if new_index[rid] else list (old_index[rid].values())[0 ] if added_cfgs: print (f" + 0x{rid:08X} ({r['type' ]} /{r['key' ]} ): 新增 {len (added_cfgs)} 种配置" ) if removed_cfgs: print (f" - 0x{rid:08X} ({r['type' ]} /{r['key' ]} ): 移除 {len (removed_cfgs)} 种配置" ) def compare_languages (old_parser, new_parser, resource_name, type_name="string" ): """对比特定资源在多语言下的值变化""" print (f"\n[语言对比] {type_name} /{resource_name} :" ) if __name__ == '__main__' : if len (sys.argv) < 3 : print ("Usage: python arsc_diff.py old.apk new.apk" ) sys.exit(1 ) parser1 = ARSCParser(sys.argv[1 ]) parser2 = ARSCParser(sys.argv[2 ]) results1 = parser1.parse() results2 = parser2.parse() idx1 = build_resource_index(results1) idx2 = build_resource_index(results2) diff_resources(idx1, idx2)
16.3 扩展用法 检测新增字符串 python3 arsc_parser.py app_v1.apk > v1_strings.txt python3 arsc_parser.py app_v2.apk > v2_strings.txt diff v1_strings.txt v2_strings.txt | grep "^>" | head -20
提取特定语言的字符串 def extract_zh_strings (parser_results ): zh_strings = {} for r in parser_results: if r.get('type' ) == 'string' and 'value' in r: cfg = r.get('config' , {}) lang = cfg.get('language' , '' ) if lang == 'zh' : resid = r['resid' ] str_val = parser.string_pool[r['value' ]['data' ]] if r['value' ]['type' ] == 3 else r['value' ]['data' ] zh_strings[r['key' ]] = str_val return zh_strings
十七、Stale/loose 资源条目 17.1 什么是 Stale 资源 resources.arsc 中可能包含没有对应实际文件的资源条目 ,即 ARSC 中有引用但 res/ 目录下不存在对应文件。这种情况称为 stale resource entry 或 loose resource 。
17.2 产生原因 (a) 编译残留 场景:删除 res/drawable/old_icon.png 后未 clean 构建 结果: resources.arsc: Type[drawable][默认]: entry[5]: key="old_icon", value={TYPE_REFERENCE → res/drawable/old_icon.png} res/drawable/: old_icon.png → 已删除,不存在 运行时: getDrawable(R.drawable.old_icon) → Resources.NotFoundException
(b) aapt2 –no-auto-version AAPT2 的 --no-auto-version 标志会抑制自动版本化资源(如 values-v21/ 的自动选择),可能留下只存在于 ARSC 中但无对应配置文件的条目。
(c) 手动 ARSC 注入 攻击者或加固工具可以手动修改 resources.arsc,插入:
虚假资源引用 :指向不存在的文件,用于反逆向诱饵
隐藏字符串 :在 ARSC StringPool 中插入但在正常反编译中不可见的字符串(反编译工具可能忽略未引用的池条目)
后门配置 :仅在特定极端配置下生效的恶意值
(d) 增量构建不一致 多模块项目中,如果资源表在不同模块间不同步,可能出现:
模块 A 的 flat 文件引用了 res/drawable/module_a_icon.png 模块 B 的编译产物中不包含该文件 链接后 ARSC 中保留了引用,但最终 APK 中无对应文件
17.3 检测方法 def detect_stale_resources (arsc_path, apk_res_dir ): """检测 ARSC 中引用但 res/ 目录下未找到的文件""" parser = ARSCParser(arsc_path) results = parser.parse() for r in results: if r.get('type' ) in ('drawable' , 'layout' , 'xml' , 'anim' , 'raw' ): key = r['key' ] expected_paths = find_files(apk_res_dir, key) if not expected_paths: print (f"[STALE] {r['type' ]} /{r['key' ]} (0x{r['resid' ]:08X} ) " f"— ARSC 中引用但未找到对应文件" )
17.4 逆向工程意义 对于逆向分析师: 1. stale 条目可能是已废弃功能的残留线索 2. 隐藏的 StringPool 条目可能包含未使用的调试/后门字符串 3. 分析 entry 与文件的对齐关系可判断 APK 是否被二次打包修改 对于安全工程师: 1. 检测 ARSC 中的可疑条目(指向非标准路径的资源) 2. 验证 ARSC 中声明资源与实际文件的完整性 3. 甄别 RRO overlay 注入的额外资源
17.5 利用与缓解 利用: - 在 ARSC StringPool 中隐藏反逆向字符串(不通过 entry 引用) - 创建仅特定配置下才激活的隐藏资源 - 伪造资源引用干扰自动化分析工具 检测: - 扫描 StringPool 中未被任何 entry 引用的字符串 - 交叉验证 ARSC 中的文件引用与 APK 内的实际文件列表 - 使用 androguard 的 arsc.py 模块进行完整性分析
面试常考问题 Q1: resources.arsc 和 R.java 的关系? A: R.java 是编译时生成的资源 ID 常量类,供 Java/Kotlin 代码引用(如 R.string.app_name → 0x7F020001)。resources.arsc 是运行时资源查找表,根据 ID 找到实际值和配置适配。两者都必须存在且一致:R.java 在编译时链接到代码中,resources.arsc 在 APK 运行时被 AssetManager 加载。
Q2: TypeSpec 和 Type 的关系是什么? A: TypeSpec 是元数据层,声明一个 type 有多少个 entry、每个 entry 有哪些配置变体(通过配置位掩码)。Type 是数据层,每个 Type 实例对应一种具体配置,包含该配置下所有 entry 的实际值。例如 values-zh 对应一个 Type 实例,values-en 对应另一个 Type 实例,两者共享同一个 TypeSpec。
Q3: 为什么不同的资源类型使用不同的 type ID? A: type ID 是资源查找的第一级索引。AssetManager 在获取资源时,通过 (resourceId >> 16) & 0xFF 快速定位 type ID。不同资源类型的行为不同:string 需要字符串值,layout 需要 XML 文件路径,drawable 需要图片 bitmap。分离 type ID 使得每个类型有独立的处理逻辑和解析器。
Q4: TYPE_DYNAMIC_REFERENCE (0x07) 和 TYPE_REFERENCE (0x01) 有什么区别? A: TYPE_DYNAMIC_REFERENCE 用于运行时动态生成的资源 ID(如 Resources.getIdentifier() 获取的资源或通过 Overlay 注入的资源)。与编译时静态确定的 TYPE_REFERENCE 不同,动态引用需要在运行时验证目标资源是否存在,且不保证在所有配置下都有一个有效值。
Q5: Android 如何处理资源缺失? A: 当某个配置下资源不存在时:(1) 首选最接近匹配的配置(如请求 values-zh-rCN 但只有 values-zh,则使用 values-zh);(2) 退回默认配置(无语言/密度/方向指定的配置);(3) 如果完全不存在,抛出 Resources.NotFoundException。TypeSpec 中的配置掩码可以快速判断是否需要遍历某个 Type chunk。
参考
AOSP: frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h — 所有结构体定义(AOSP 10+)
AOSP: frameworks/base/libs/androidfw/ResourceTypes.cpp — ResTable 完整解析与配置匹配实现(match(), isBetterThan())
AOSP: frameworks/base/tools/aapt2/ — AAPT2 编译/链接工具源码
AOSP: frameworks/base/tools/aapt2/compile/ — 资源编译(XML → .flat 中间格式)
AOSP: frameworks/base/tools/aapt2/link/ — 资源链接(.flat → resources.arsc)
AOSP: frameworks/base/tools/aapt2/ResourceTable.cpp — 资源表构建核心逻辑
AOSP: frameworks/base/core/java/android/content/res/AssetManager.java — Java 层资源加载与 Overlay 合并
AOSP: frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java — RRO 包管理
AOSP: frameworks/base/cmds/idmap2/ — idmap 生成工具(Overlay ID 映射)
AOSP: frameworks/native/include/android/configuration.h — AConfiguration 常量定义
AOSP: frameworks/base/core/jni/android_util_AssetManager.cpp — JNI 桥接层
AOSP: frameworks/native/libs/arect/include/android/asset_manager.h — Native AssetManager API
第三方: AndResGuard — 开源 Android 资源混淆工具
第三方: apktool — APK 反编译工具(含 ARSC 解析实现)
第三方: androguard — Python Android 逆向框架(androguard/core/resource/arsc.py)