先看结论与判断条件
- 签名校验是资源完整性的前置条件,但仅证明 APK 文件未被第三方重打包,不能证明资源值符合业务预期语义。
- 单独计算 resources.arsc 的摘要可与签名无关,提供对构建流水线内部篡改或意外修改的检测能力。
- 运行时动态资源替换应通过显式允许列表与摘要校验,并严格区分于常规业务配置下发流程。
- 工程判断:跨构建维护允许清单时优先使用资源名称而非数值 ID;ID 是否稳定须由当前 aapt2 版本和两份实际产物对比确认。
- 供应链证明(provenance)将资源摘要绑定到构建上下文,使验证链从文件签名延伸到构件创建过程。
- 校验失败时应按安全策略立即阻断,并记录详细日志以便溯源,避免盲目进入不可控的降级状态。
资源篡改的威胁模型与职责边界
攻击者通过二次打包替换 resources.arsc 内的字符串、图标或数值,可改变界面展示、功能分支或业务逻辑关联的资源值。若业务代码根据资源值做权限判断或价格计算,篡改可导致直接经济损失。这类修改发生在构建发布之后,因此签名校验最先感知,但若攻击者利用自定义证书重新签名,签名校验即失效,需要资源值层面的完整性与语义验证。
构建环境中的恶意脚本或错误合并也可能在 APK 签名前将错误资源植入。此类情形下签名有效,但资源已非预期。开发者必须依赖构建流水线中的资源摘要记录与供应链证明,将资源产物与构建配置绑定。只有这样才能区分签名有效的合法 APK 和签名有效但内容被污染的 APK。
运行时动态资源加载(如通过热修复、网络下发)绕过了 APK 签名与静态资源表的保护。这类场景应将动态资源视作独立安全边界,引入单独签名和允许列表。对于此层面的篡改,静态 resources.arsc 检查无法覆盖,必须在运行时建立验证点,并确保隔离动态资源对核心资源 ID 映射的污染。
明确职责边界有助于避免过度防御或防御缺失。静态资源表的完整性由构建系统和签名机制共同保障,而运行时加载的外部资源则完全依赖应用自身的校验逻辑。混淆两者会导致安全策略错位,例如用签名校验去验证动态下发的配置,或用运行时检查去防御已签名的静态包体篡改。
| 篡改类型 | 注入阶段 | 签名有效? | 内容检查责任 | 运行时责任 |
|---|---|---|---|---|
| resources.arsc 直接替换 | 打包后分发前 | 否(重签名后可变有效) | 静态摘要比对 | 无 |
| 构建中资源注入 | 构建阶段 | 是 | 需摘要比对 + 证明 | 无 |
| 运行时动态替换 | 运行时 | 签名不覆盖 | 不适用 | 运行时上下文校验 |
| 额外资源文件添加(非 arsc) | 二次打包 | 否 | arsc 摘要无法检测 | 可与签名结合 |
- 是否已识别二次打包检测策略
- 构建系统是否产生并存储资源摘要
- 动态资源加载清单是否已独立审核
resources.arsc 结构与篡改可操作面
resources.arsc 是二进制资源表,由一系列 chunk 组成,包含表头、字符串池、包信息、类型定义和条目列表,每个条目指向具体的资源值或引用。攻击者可修改字符串池内容而不改变长度,替换指向的偏移,或在类型条目中插入新的配置限定符,从而在特定设备上加载未授权值。
由于资源的查找依赖 ID(0xPPTTEEEE)和配置限定符,对 entry 顺序的重排或对 type 结构的插入可能会改变业务代码中 R.class 的映射。如果应用使用了反射或 getIdentifier,攻击者还可以通过伪造资源名称映射来重定向资源请求。因此,完整性检查不能只依赖 ID 比对,应当结合资源名称和配置上下文校验。
解析 resources.arsc 时必须严格处理大端字节序、字符串长度编码和 chunk 长度字段,否则可能被特意构造的畸形 chunk 触发缓冲区越界或解析适配器错误。在完整性检查中,首选使用 aapt2 dump 或其底层库来生成标准化摘要,而避免手写解析器在安全工具链中产生新攻击面。
AOSP ResourceTypes 定义了这些二进制结构的布局,包括 chunk 类型与长度字段。从工程实践来看,自行解析底层结构需要跟随格式变化维护。实际项目可优先调用官方构建工具导出稳定的中间结果,再将其作为值级清单的输入,减少自制解析器误读。
- 确认使用的资源解析库来源与完整性
- 检查是否仅依赖资源 ID
- 是否处理畸形 chunk 导致偏移越界
签名校验的覆盖与限制
APK 签名方案 v2/v3 通过在 APK Signing Block 中保存签名数据覆盖 ZIP 文件内除签名块本身之外的几乎所有内容,包括 resources.arsc、manifest 和 DEX 文件。验证工具(apksigner verify)可确认签名是否与证书链匹配以及签名方案是否允许降级。但签名校验成功只表明文件未被修改过,并不能保证其内容原先就符合预期。
如果开发者在签名前生成并验证资源摘要,并将该摘要作为 APK 内容的一部分由签名保护,那么就可以把对内容正确性的信任从构建环境扩展到分发渠道。然而,签名不保护 APK 之外通过应用商店元数据或动态下载的资源,所以对 runtime 资源不可依靠签名。
Android 仍兼容 v1 签名,但资源完整性策略不能只看某一种方案是否存在。验收脚本应输出每个签名方案的验证结果,再按应用的 minSdk 和渠道要求决定最低接受组合;不要让运行时代码从自身 APK 推导预期策略。
apksigner 的结论限于当前文件的签名与证书信息,并不证明私钥保管流程安全。本文不涉及 keystore 管理流程的具体细节;资源表检查还需把提取值与独立维护的允许清单比对,不能把签名通过当成业务配置正确。
关键资源允许列表设计原则
允许列表应明确列出所有在安全决策中引用的静态资源,例如控制支付环境、API 端点、许可开关或调试标志的资源 ID 或名称。不应将整个资源表纳入允许列表,以减少误报并保留业务配置演进所需的弹性。过度严格的列表会增加维护负担,导致正常业务迭代受阻。
对于每个条目,需确定其唯一标识方法:使用资源名称(如 com.example.app:string/api_endpoint)比资源 ID 更稳定,可避免因 R 类重新生成导致 ID 变更。摘要算法应选择抗碰撞的 SHA256,并与资源值和配置限定符一同计算,防止只替换特定配置限定的资源版本。
允许列表本身以及对应的预期摘要应版本化,并存入 APK 的 assets 或 raw 目录,这样签名保护同时覆盖列表。构建系统将该列表与 resources.arsc 的解析输出一并生成,并记入供应链证明中。当列表变动时,需同步更新证明并触发额外的安全评审,确保变更可追溯。
工程建议优先使用资源名称作为标识键,因为 ID 的稳定性依赖于构建输入的严格排序。若必须使用 ID,则需在构建系统中锁定资源文件的处理顺序。这种设计权衡直接影响允许列表的长期可维护性,需在项目初期明确技术选型。
| 决策项 | 选项 A | 选项 B | 工程建议 |
|---|---|---|---|
| 资源标识粒度 | 资源 ID | 资源名称 | 优先使用名称,若必须用 ID 则锁定构建 |
| 摘要包含范围 | 仅值字符串 | 值 + 配置限定符 + 类型 | 包含配置与类型以发现限定符替换 |
| 摘要算法 | SHA256 | BLAKE3 | SHA256 已足够且生态支持广 |
| 列表存放方式 | 硬编码 | assets 动态读取 | 存入 assets 内由签名保护 |
- 清单是否按资源名称标识
- 是否包含对应的配置限定符
- 摘要算法是否记录并保持一致
- 清单是否版本化并与构建证明绑定
摘要清单的生成、嵌入与校验
在构建阶段,通过在 aapt2 link 后立即调用自定义工具解析 resources.arsc,遍历指定资源名称列表,提取实际值,计算各值的摘要并组装为 JSON。该工具应基于 AOSP ResourceTypes 定义或 aapt2 的内部格式,避免引入其他解析器导致的偏差。这一步骤是建立可信基线的关键。
生成的清单 JSON 必须随 APK 打包进 assets 目录,使签名同时覆盖。清单中每个条目不仅包含资源名和摘要,还应包含配置限定符列表和构建生成时的工具版本信息,以便后续审计。构建系统最好将清单的摘要存储于 provenance,记录到构建来源证明以便追溯。
应用冷启动时可从 APK 读取清单,并通过同一套资源解析逻辑获取关键资源的实际值,计算摘要后比对。任何不匹配都要记录资源名称、预期摘要与实际摘要,再由既定策略决定停止功能或使用安全默认值。预期清单不能与待检查 APK 一起由同一不可信输入生成。
代码实现需注意异常处理,避免因资源缺失或格式错误导致应用崩溃。校验过程应在主线程之外异步执行,或在应用初始化早期同步完成,具体取决于业务对启动速度的容忍度。无论何种方式,都必须确保在校验完成前不加载受保护的资源值。
#!/usr/bin/env python3
import sys
import hashlib
import zipfile
def compute_sha256(filepath):
sha256 = hashlib.sha256()
with open(filepath, 'rb') as f:
while True:
data = f.read(65536)
if not data:
break
sha256.update(data)
return sha256.hexdigest()
def main():
if len(sys.argv) != 3:
print("Usage: verify_arsc.py <apk_file> <expected_hash>")
sys.exit(2)
apk_path = sys.argv[1]
expected_hash = sys.argv[2]
try:
with zipfile.ZipFile(apk_path, 'r') as z:
arsc_info = z.getinfo('resources.arsc')
with z.open(arsc_info) as arsc:
sha = hashlib.sha256()
while True:
data = arsc.read(8192)
if not data:
break
sha.update(data)
actual_hash = sha.hexdigest()
if actual_hash != expected_hash:
print("MISMATCH: resources.arsc integrity check failed")
sys.exit(1)
else:
print("resources.arsc integrity verified")
except KeyError:
print("ERROR: resources.arsc not found in APK")
sys.exit(1)
except Exception as e:
print(f"ERROR: {e}")
sys.exit(1)
if __name__ == "__main__":
main()运行时区分篡改、业务配置与缓存
业务配置往往通过远程配置中心、粘性广播或 SharedPreferences 下发,这些值可能影响资源选择或界面行为,但不能直接修改 resources.arsc。运行时校验必须将动态配置与静态资源分开处理:对于静态资源,只接受由签名保护清单认可的值;对于动态配置,应使用独立的完整性保护(如 HMAC 或数字签名)并保持审计日志。
缓存可能保存了上一次资源检查的中间状态或经过允许的替换值,因此需要设计缓存有效期和刷新策略。一旦检测到 resources.arsc 摘要与预期不符,应立即清除与其关联的缓存,防止残留的合法资源被恶意替换后的代码引用。缓存一致性是防止旧数据污染的重要环节。
失败条件包括:清单文件缺失、清单无法解析、资源提取异常、摘要不匹配、签名校验未通过等。每个条件需对应清晰的处理动作,例如阻断启动、限制网络权限、擦除用户数据或仅通知监测系统。必须将处理动作记录在安全事件日志或远程遥测中,以便事件回溯。
区分静态与动态资源的另一个好处是减少误报。业务频繁调整配置时,若未正确隔离,会触发大量静态资源校验告警。通过明确的边界定义,运维团队可以快速定位问题根源,是构建产物被篡改,还是配置下发出现了异常,从而采取正确的响应措施。
- 是否在代码中区分静态资源与动态配置
- 缓存失效策略是否跟随资源完整性状态
- 失败动作是否按安全等级明确定义
供应链证明中的资源摘要绑定
SLSA Provenance v1.1 要求构建证明绑定产物 identity、构建器、构建类型和输入材料。在资源完整性场景中,可以将 resources.arsc 的摘要作为 subject 之一,或作为单个产物的材料列表中的一个条目,确保有人篡改构建输出时证明无法通过验证。这增强了构建过程的可信度。
in-toto Attestation 的 Statement 模型允许将产物摘要与声明负载绑定。资源允许清单的摘要可以与构建信息组合成一个 in-toto 声明,由构建系统签名,然后由部署端或审计系统在启动时交叉验证,进一步闭环比对环节到构建环境的信任链。这种机制能有效防止构建产物被调包。
供应链证明本身并不取代运行时资源校验。证明只能证实构建过程的宣称,若运行时使用了其他由更新服务下发的资源包,则需要另建证明机制。工程建议是始终将静态核心资源的摘要嵌入 APK 并受签名保护,同时将同一摘要纳入 provenance,形成双层校验。
接入供应链证明需要在 CI/CD 中增加摘要采集、声明签名与验证步骤。改造工作量视现有流水线复杂度而定,此处不做具体估算。实施时先为单一 release 变体绑定 resources.arsc 摘要与构建参数,验证回执可复现后再扩展到其他变体。
| 方法 | 检测范围 | 依赖项 | 局限 |
|---|---|---|---|
| APK 签名验证 | 整个 APK 文件 | apksigner / 证书链 | 无法检测签名前污染,不覆盖外部资源 |
| 独立 arsc 文件摘要 | 仅 resources.arsc | 构建时摘要记录 | 不检测其他文件,需额外工具 |
| 关键资源 ID/值比对 | 特定资源值 | 允许清单与解析器 | 需维护清单,漏项不覆盖 |
| 供应链证明绑定 | 从源码到产物 | 可信构建环境与记录 | 只能证实构建过程,非运行时验证 |
端到端验证 checklist 与失败处置
安全团队应建立端到端验证流程,从 CI 签名、资源摘要生成、清单嵌入、APK 分发到应用启动检查逐环节校验。使用自动化脚本对每个发布候选 APK 执行签名验证、摘要比对和允许清单一致性测试。自动化能减少人为疏忽,确保每次发布都经过严格审查。
对于生产环境中检测到的失败,应有分级响应:签名失败、arsc 摘要不匹配、关键资源值异常属于严重事件,需立即阻断上线或强制更新;业务配置摘要变化属于警告,但若不在预期清单内也应及时复核;检查基础设施自身异常应触发安全告警并回退至最近一次可信状态。
应明确记录所有失败的证据,包括 APK 哈希、签名证书指纹、出错的资源名和值、时间戳和环境信息,以便于安全回溯和判定事故边界。所有日志需要防篡改保护(如远程上报并签名)并设定保留期。完整的证据链是事后分析和定责的基础。
定期回顾校验策略的有效性至关重要。随着业务发展和攻击手法演变,允许列表和校验逻辑可能需要调整。建立定期的复盘机制,分析误报和漏报案例,不断优化检测规则,确保持续适应新的安全挑战。
- 各环节是否已自动化执行完整性检查
- 失败分级策略是否涵盖签名、摘要、允许清单异常
- 失败证据日志是否启用并防篡改
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android 二进制资源表由带长度和类型的 chunk、package、type 与 entry 结构组成。 | AOSP ResourceTypes | 固定历史源码用于解释格式结构,实际构建仍应以当前 aapt2 输出为准。 |
| resources.arsc 内字符串池与资源条目通过偏移关联,解析时需严格验证 chunk 长度和标志防止越界。 | AOSP ResourceTypes | 手写解析器易引入偏移计算缺陷,生产环境应优先采用 SDK 工具。 |
| apksigner 可签名、验证方案覆盖与证书,并要求签名后不再修改 APK。 | Android apksigner | 工具验证成功不证明 keystore 管理、渠道流程或候选包归档正确。 |
| APK Signature Scheme v2 通过 APK Signing Block 覆盖文件主体,并包含防止降级到较弱方案的机制。 | APK Signature Scheme v2 | v2 不覆盖独立于 APK 分发的外部资产。 |
| APK 签名方案(v1/v2/v3)允许应用选择性启用 v1 签名兼容性,但验证者应拒绝仅 v1 签名的 APK 以防范降级攻击。 | APK Signature Scheme v2 | 降级保护要求验证方代码实现正确校验所有方案标志。 |
| Manifest 定义组件、权限、intent filter、SDK 约束和应用元数据。 | Android app manifest | 静态清单不能证明运行时访问控制没有被业务代码放宽。 |
| 构建证明应绑定产物主体、构建者、构建类型、外部参数与依赖材料。 | SLSA Provenance v1.1 | provenance 只能证明记录的构建过程,不能单独证明运行时安全性。 |
| 供应链证明应把产物摘要与有类型的声明负载绑定,避免报告与候选包错配。 | in-toto Attestation Statement v1 | 声明格式不保证声明内容真实,仍需可信签名者和门禁核验。 |
工程常见问题
APK 签名已通过,为何还要校验 resources.arsc 摘要?
签名只能证明 APK 未被第三方篡改,但无法防止构建链引入的错误或已在签名环节前植入的异常值。单独校验资源摘要可将完整性前移至构建产物,与签名互补。
业务动态下发配置导致资源变化,如何避免误报?
应对动态配置建立独立的渠道与签名审计,并将变化登记至允许列表的扩展区域,或在运行时上下文标记,确保核心静态资源不允许未登记修改。
资源 ID 在跨版本构建中会漂移,如何维护允许列表?
可以将允许列表锚定在资源名称或文件名上,并在构建时生成摘要与命名映射,由供应链证明锁定具体的资源 ID 产出。只有在 aapt2 输入源完全锁定且顺序稳定时才可使用 ID。
多渠道打包时,渠道号可能写入资源,如何确保摘要一致性?
应将渠道信息作为构建参数记录在 provenance 中,资源摘要覆盖除渠道字段外的确定性部分,或采用按渠道分离的摘要清单。
生成的摘要清单自身需要签名吗?
必须纳入 APK 签名保护范围,即清单应置于 APK 内,使得签名同时绑定清单与资源。独立分发清单则需要独立的可信交付与校验机制。
校验失败后应用应该立即退出还是降级运行?
对于安全级别高的应用,应立即阻断并上报;对于需要高可用的业务,可设立受控降级策略,例如限制功能但记录证据,且降级策略须经安全评审。运行时上下文必须记录事件并防止重放。