先看结论与判断条件
- 测试清单必须来自最终加固候选的 Manifest 合并结果,源码声明或旧基线不能代表实际导出面。
- 每个 Activity、Service、Receiver 和 Provider 都需要正常允许、无权限拒绝、畸形输入拒绝与组件不可用用例。
- 系统分发成功只说明组件入口可达,业务层仍需校验调用方身份、权限、用户状态、数据范围和参数类型。
- 显式调用、隐式 Intent、冷启动进程、后台限制和被禁用组件要分开测试,不能由单一路径代替。
- PendingIntent 是系统代持的调用能力,身份与可变性有独立规则;本文只检查它是否意外扩大当前组件入口。
- 回归结论必须绑定候选摘要、签名身份、设备、系统版本和调用方测试包,单一设备通过不代表完整矩阵。
从最终 Manifest 建立真实导出面基线
Android app manifest 定义应用组件、权限、intent filter、SDK 约束和元数据。加固回归不能只读源码中的 AndroidManifest.xml,因为构建变体、库清单、manifest merger 和后续处理都会影响最终结果。应从实际加固候选解析 Activity、activity-alias、Service、Receiver 和 Provider,记录 exported、permission、intent filter、process、enabled 及 authorities 等字段。
基线同时保留未加固对照候选和加固候选的文件摘要、应用标识、版本、签名身份与 Manifest 摘要。差异报告只关注组件声明与预期处理变化,不把正常的文件字节变化当成错误。若出现新导出入口、入口消失、权限被改写、Provider authorities 变化或组件类名无法解析,先停止跨应用测试并确认候选身份。
静态清单只能说明系统入口条件,不能证明运行时访问控制正确。业务代码仍可能根据 extras、URI、调用方、账号或用户状态放行与拒绝。因此每条导出记录要关联运行时测试计划,至少含正常允许、缺少权限、错误身份、畸形输入和组件不可用场景。没有拒绝用例的组件不能登记边界已验证。
| 字段 | 来源 | 影响 | 漂移处置 |
|---|---|---|---|
| 组件类型 | 最终 Manifest | 系统分发语义 | 重建测试矩阵 |
| 组件名称 | 合并后声明 | 显式目标身份 | 停止错误映射 |
| exported | 最终候选 | 跨应用入口 | 安全评审 |
| permission | 组件声明 | 系统访问控制 | 核对调用方权限 |
| intent filter | action 与类别 | 隐式解析 | 更新分发用例 |
| process enabled | 组件属性 | 生命周期可用性 | 补充状态测试 |
先区分系统是否分发与业务是否接受
跨应用调用至少经过两层判定。Android 系统根据组件存在、exported、权限、Intent 解析、URI grant 和进程状态决定是否分发;组件收到调用后,业务代码再验证调用方、用户会话、参数、数据范围和幂等条件。测试报告要分别记录 deliveryResult 与 businessResult,不能把未收到回调统一写成业务拒绝,也不能把入口到达写成操作成功。
Android exported component risk 指出导出组件和 intent filter 会扩大跨应用调用面,需要显式权限和输入校验。exported 值只是入口条件之一:即使 exported 为 true,也可以用签名级权限、调用方校验和严格数据规则限制能力;即使组件未导出,错误的委托能力或同 UID 交互仍可能产生其他边界。回归只对清单中约定的入口作候选级验证。
观测点应公开安全。记录候选摘要、调用方测试包公开身份、组件、测试用例、Intent 类型、权限集合、参数 schema、系统结果、业务结果和关联号,不记录真实用户数据、令牌或内部 URI。拒绝路径要验证没有副作用,例如没有创建任务、写入数据、启动后台工作或发送后续广播,而不只是检查异常文本。
| 系统结果 | 业务结果 | 解释 | 下一步 |
|---|---|---|---|
| not-delivered | not-run | 系统入口拒绝 | 核对声明和权限 |
| delivered | allowed | 合法调用完成 | 检查副作用回执 |
| delivered | denied | 业务安全拒绝 | 确认无副作用 |
| delivered | invalid-input | 参数校验拒绝 | 核对 schema |
| delivered | unavailable | 业务状态不可用 | 验证恢复路径 |
| unknown | unknown | 证据不足 | 补充设备与日志 |
Activity 要覆盖显式、隐式和任务状态
导出 Activity 的允许用例需要分别覆盖显式组件 Intent 与符合 intent filter 的隐式 Intent,并记录解析出的唯一目标。若多个应用都能处理同一隐式 Intent,测试应控制选择结果,不能把系统选择器行为误判为组件兼容失败。启动后验证目标页面、用户会话、参数 schema、返回结果和任务栈,确保加固没有破坏入口类、路由或生命周期。
拒绝矩阵包含调用方无权限、账号未登录、资源不属于当前用户、必填参数缺失、类型错误、超长值、未知枚举和重复请求。参数测试使用合成数据,不构造可运行攻击链。对于安全拒绝,Activity 可以展示受控错误或直接结束,但必须确认未执行受保护操作。异常崩溃不算合格拒绝,因为它既暴露可用性问题也没有明确业务状态。
进程和任务状态覆盖应用未启动、后台存在、前台活动、目标 Activity 被禁用、用户从最近任务恢复以及配置变更。冷进程能发现初始化顺序和类加载问题,热进程能发现旧状态或重复 Intent 消费问题。本文不展开 Deep Link 解析专题;若 Activity 由网页链接进入,只把最终组件分发和业务校验纳入本矩阵。
| 维度 | 允许用例 | 拒绝用例 | 回执 |
|---|---|---|---|
| 目标方式 | 显式正确组件 | 不存在或禁用组件 | 系统分发结果 |
| 隐式解析 | 匹配 action 类别 | 缺失必要类别 | 解析目标 |
| 调用权限 | 持有批准权限 | 无权限调用方 | SecurityException 类别 |
| 业务会话 | 已登录有权用户 | 未登录或越权 | 业务判定 |
| 参数 | 合法 schema | 缺失类型错误 | 校验结果 |
| 进程状态 | 冷启动和热启动 | 组件禁用 | 生命周期事件 |
Service 要分开 start、bind 与调用方生命周期
导出 Service 的启动与绑定语义不同。start 路径关注系统能否启动服务、Intent 是否正确消费、重复启动是否幂等以及后台限制如何表现;bind 路径还要关注 Binder 接口、连接断开、调用方死亡和重连。测试清单明确每个 Service 支持哪种方式,禁止因一种方式成功就把另一种登记为通过。
权限不仅检查 Manifest 声明,还要验证运行时方法。持有组件启动权限的调用方未必有权执行所有 Binder 操作,接口应按业务身份和数据范围再次校验。拒绝用例覆盖无权限、错误调用身份、无会话、未知操作码、畸形数据和重复事务,并确认没有残留任务、通知、文件或服务端请求。
冷进程测试从目标应用完全未运行开始,验证 Service 所需初始化、依赖注入和加固后类加载;组件禁用、系统停止包或服务依赖不可用时,验证可解释的失败。恢复后使用新关联号再次调用,不复用已失败事务的可变状态。没有设备端和业务端双回执时,只能记录调用尝试。
| 路径 | 系统条件 | 业务检查 | 失败副作用 |
|---|---|---|---|
| start | exported 与权限 | 命令 schema | 不得残留工作 |
| repeat-start | 相同或新 Intent | 幂等键 | 不得重复操作 |
| bind | 服务解析与连接 | 接口级授权 | 不得泄露 Binder |
| caller-death | 连接被回收 | 事务终止策略 | 不得悬挂资源 |
| cold-process | 进程未运行 | 初始化完整 | 不得只在热态成功 |
| disabled | 组件不可用 | 受控恢复 | 不得绕过 enabled |
Receiver 要验证 action、权限与重复广播
导出 Receiver 的矩阵从 Manifest 中的 action、category、permission、enabled 和 process 建立。显式广播验证指定目标,隐式广播验证 action 解析与系统限制。测试调用方必须是独立签名的公开安全测试包,不能在同一应用内发送后宣称跨应用通过。回执记录广播发送、系统分发、onReceive 到达与业务处理四个状态。
输入校验覆盖 extras 缺失、类型错误、未知 action、错误用户、重复事件和过期事件。Receiver 入口应尽快验证并把必要工作交给受控后续组件;拒绝时不创建任务、不写入状态、不触发网络副作用。若加固后 onReceive 到达但后续工作消失,问题位于业务调度,不应改写为系统未分发。
进程未运行与应用被系统停止是不同状态。冷进程测试可以发现组件类、初始化和依赖问题;被停止包的系统行为应按平台语义记录,而不是强行期望每次都接收。设备厂商和系统版本可能影响后台行为,因此报告写清设备矩阵,避免一个环境的广播结果被推广为全部兼容。
Provider 要把 authorities、URI 权限和数据规则连起来
导出 Provider 的身份由组件名称、authorities、exported、readPermission、writePermission、grantUriPermissions 和 path permission 等共同决定。回归先从最终 Manifest 读取这些字段,再验证查询、插入、更新、删除或 call 中实际允许的操作。只测试能打开 URI 不足以证明行级、字段级和用户级边界。
允许用例使用最小合法 projection、selection 和合成数据;拒绝用例覆盖无权限调用方、错误 authority、越界 path、未知列、畸形类型、过大分页、跨用户资源和已撤销 URI grant。测试不发送破坏性真实数据,写操作在隔离夹具中执行并验证回滚。异常不能泄露数据库结构、内部路径或用户记录。
加固可能影响 Provider 类加载、模型反射、Cursor 字段和序列化,但 exported 或权限漂移是另一类问题。先确认系统是否解析到正确 Provider,再确认业务访问控制和数据格式。若 authorities 变化,所有调用方契约都可能失效;若系统分发正常而数据解析失败,才进入模型和保护范围排查。
| 维度 | 允许条件 | 拒绝条件 | 验证结果 |
|---|---|---|---|
| authority | 精确匹配 | 未知 authority | 解析结果 |
| 读权限 | 批准调用方 | 无读权限 | 无数据泄露 |
| 写权限 | 隔离测试身份 | 无写权限 | 无状态改变 |
| path | 允许路径 | 越界路径 | 路径规则 |
| 参数 | 合法字段类型 | 未知列和畸形值 | 受控错误 |
| URI grant | 有效最小授权 | 撤销或过期授权 | grant 状态 |
PendingIntent 只作为委托入口变量,不替代组件专题
PendingIntent security risks 说明可变性、目标组件和一次性标志会改变重放与重定向风险;PendingIntent API 又说明其身份由类型、请求码和底层 Intent 的特定字段共同决定,并由系统在进程外持有。若导出组件可由 PendingIntent 触发,矩阵要记录这条委托入口,验证目标是否明确、可变字段是否受限和重复发送如何处理。
本文不重做 PendingIntent 完整专题,也不把 flags 正确等同业务状态恢复正确。即使委托对象身份符合预期,组件仍要校验 extras、用户状态、资源范围和幂等条件;通知或外部应用触发后还要验证任务栈和进程恢复。测试只使用应用正常公开流程产生的委托对象,不提供拦截、篡改或重定向脚本。
如果加固后只有 PendingIntent 路径失败,而直接显式调用成功,先比较委托对象的创建位置、请求码、目标字段、可变性和进程外恢复;如果两条路径都在系统分发前失败,检查 Manifest 与签名权限;若两条都到达但业务拒绝,检查共同输入 schema。分层对照可以避免把委托身份问题归到导出声明。
用代码核对 Manifest 与测试清单是否相互覆盖
下面的 Python 示例读取公开示例 Manifest 和 JSON 测试清单,枚举显式 exported 的 Activity、Service、Receiver 与 Provider,提取组件权限,并检查每个入口是否同时存在 allow、deny-permission 和 deny-malformed 用例。它还要求畸形输入用例声明 inputValidation,并发现测试清单引用不存在组件或权限预期与 Manifest 漂移。
示例只解析公开安全的静态文件,不启动组件、不构造攻击 Intent,也不包含真实包名、内部地址或用户数据。对 Provider、Service 等更细权限,实际项目可扩展专用字段;教学代码把组件级 permission 作为共同基线。没有显式 exported 值的组件被要求人工确认构建目标语义,而不是由脚本猜测。
校验通过只说明测试计划覆盖了三类基础结果,不证明设备运行通过,也不证明业务拒绝没有副作用。真实执行仍需独立测试调用方、设备端 instrumented test、候选摘要、系统分发和业务日志。若最终 Manifest 变化,必须重新生成基线并让旧测试计划失效。
import json
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
ANDROID = '{http://schemas.android.com/apk/res/android}'
TYPES = {'activity', 'activity-alias', 'service', 'receiver', 'provider'}
REQUIRED_OUTCOMES = {'allow', 'deny-permission', 'deny-malformed'}
FORBIDDEN_FIELDS = {'token', 'password', 'privateKey', 'userData'}
def stop(message):
raise SystemExit(message)
def component_name(element):
return str(element.get(ANDROID + 'name', '')).strip()
def exported_components(manifest_path):
root = ET.parse(manifest_path).getroot()
application = root.find('application')
if application is None:
stop('manifest has no application element')
result = {}
for element in application:
if element.tag not in TYPES:
continue
exported = element.get(ANDROID + 'exported')
if exported is None:
stop(f'{element.tag} has no explicit exported value')
if exported != 'true':
continue
name = component_name(element)
if not name:
stop(f'{element.tag} has no component name')
if name in result:
stop(f'duplicate exported component: {name}')
result[name] = {
'type': element.tag,
'permission': str(element.get(ANDROID + 'permission', '')).strip(),
}
return result
def load_plan(plan_path):
data = json.loads(plan_path.read_text(encoding='utf-8'))
tests = data.get('tests') if isinstance(data, dict) else None
if not isinstance(tests, list) or not tests:
stop('test plan has no tests')
return tests
if len(sys.argv) != 3:
stop('usage: check_exported_matrix.py AndroidManifest.xml tests.json')
manifest_path = Path(sys.argv[1])
plan_path = Path(sys.argv[2])
if not manifest_path.is_file() or not plan_path.is_file():
raise SystemExit('manifest or test plan is missing')
components = exported_components(manifest_path)
tests = load_plan(plan_path)
coverage = {name: set() for name in components}
for index, test in enumerate(tests):
if not isinstance(test, dict):
stop(f'test {index} is not an object')
if FORBIDDEN_FIELDS.intersection(test):
stop(f'test {index} contains forbidden fields')
component = str(test.get('component', '')).strip()
outcome = str(test.get('expected', '')).strip()
if component not in components:
stop(f'test {index} references unknown component')
if outcome not in REQUIRED_OUTCOMES:
stop(f'test {index} has invalid expected outcome')
expected_permission = str(test.get('componentPermission', '')).strip()
if expected_permission != components[component]['permission']:
stop(f'test {index} permission differs from manifest')
if outcome == 'deny-malformed' and test.get('inputValidation') is not True:
stop(f'test {index} lacks input validation evidence')
coverage[component].add(outcome)
missing = {name: sorted(REQUIRED_OUTCOMES - outcomes) for name, outcomes in coverage.items() if outcomes != REQUIRED_OUTCOMES}
if missing:
raise SystemExit(f'exported components lack test outcomes: {missing}')
print(json.dumps({'components': len(components), 'tests': len(tests), 'status': 'pass'}))在真实设备上闭合候选、调用方和业务副作用
Android instrumented tests 适合验证真实运行时、组件和系统 API 语义。测试工程应包含目标应用和独立调用方两个制品,分别记录摘要与签名;在设备上执行显式、隐式、权限、参数、进程和组件状态矩阵。每次调用使用关联号,系统分发日志与业务副作用回执都引用同一关联号。
Android CTS overview 说明 CTS 用于验证设备实现与 Android 兼容性定义一致。设备通过 CTS 不能证明第三方 App 加固后的业务兼容,因此报告仍需列出实际设备、系统、ABI、厂商和用例。单一设备通过只适用于该组合;厂商后台限制或系统版本差异应作为扩展矩阵,不可被静态 Manifest 检查替代。
修复后生成新候选并重新解析 Manifest,重复原失败点、同组件的拒绝用例和其他四类组件冒烟。需要继续排查组件初始化问题,可阅读本站的启动 Provider 顺序回归文章;需要提交项目验证,可携带候选摘要、Manifest 差异、调用方身份、设备矩阵和允许拒绝回执进入御盾中央平台。没有真实 HTTP 或设备结果时不登记线上兼容结论。
| 证据 | 必须绑定 | 可确认 | 不能单独确认 |
|---|---|---|---|
| 最终 Manifest | 目标候选摘要 | 静态导出面 | 业务访问控制 |
| 调用方制品 | 摘要与签名 | 测试身份 | 目标业务状态 |
| 系统分发 | 组件和关联号 | 入口是否到达 | 业务操作完成 |
| 业务回执 | 用例和关联号 | 允许拒绝与副作用 | 未测设备通过 |
| 设备矩阵 | 系统 ABI 与厂商 | 已测组合表现 | 完整生态兼容 |
| 修复候选 | 新摘要和差异 | 原失败点状态 | 其他风险不存在 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Manifest 定义组件、权限、intent filter、SDK 约束和应用元数据。 | Android app manifest 描述 Android 应用清单及组件声明。 | 静态清单不能证明运行时访问控制没有被业务代码放宽,也不代表实际设备行为。 |
| 导出组件和 intent filter 会扩大跨应用调用面,需要显式权限与输入校验。 | Android exported component risk 描述 android:exported 相关安全风险与缓解方向。 | exported 值只是入口条件之一,不等于完整威胁分析或业务授权结论。 |
| PendingIntent 的可变性、目标组件和一次性标志会改变重放与重定向风险。 | PendingIntent security risks 说明 PendingIntent 的安全风险与配置边界。 | 正确 flags 不保证组件 extras、通知返回栈和业务状态恢复正确。 |
| PendingIntent 身份由类型、请求码和底层 Intent 特定字段共同决定,并由系统在进程外持有。 | PendingIntent API 描述 PendingIntent 的身份与系统代持语义。 | API 身份规则不替代应用对参数、用户状态、权限和目标业务的验证。 |
| 依赖真实 Android 运行时、组件和系统 API 的语义应通过设备端测试验证。 | Android instrumented tests 说明 instrumented test 在 Android 设备上的执行环境。 | 单一设备通过不能代表完整 API、ABI 和厂商矩阵,也不能替代业务回执。 |
| CTS 用于验证设备实现与 Android 兼容性定义的一致性。 | Android CTS overview 描述兼容性测试套件的目的。 | 设备通过 CTS 不代表第三方 App 加固后的组件调用和业务兼容通过。 |
| 导出组件回归应同时验证系统分发结果与业务允许拒绝结果。 | 工程判断:系统入口控制和组件内部业务校验发生在不同层,证据不能合并。 | 双层结果只适用于绑定候选、调用方、设备和测试条件,不能推广到未测环境。 |
| 每个导出组件都需要正常允许、无权限拒绝、畸形输入拒绝和不可用状态用例。 | 工程判断:只测成功路径无法发现权限放宽、输入校验丢失和失败副作用。 | 具体参数 schema 与业务拒绝条件由应用设计决定,文章不提供攻击载荷。 |
工程常见问题
加固后某个导出 Activity 能启动,是否代表跨应用回归通过?
不代表。还要验证无权限、未登录、越权资源、畸形参数、冷进程、组件禁用和副作用,且系统分发与业务判定要分别取证。
测试导出组件为什么必须读取最终加固包的 Manifest?
构建变体、库合并和处理步骤可能改变最终声明。源码清单或旧基线不能代表实际候选,测试矩阵必须绑定最终文件摘要与解析结果。
Service 的 start 成功能否代替 bind 回归?
不能。start 与 bind 的系统语义、生命周期和接口授权不同,应分别验证启动、连接、调用方死亡、重连和接口级权限。
Provider 有 readPermission 就不需要业务输入校验吗?
仍然需要。系统权限不能替代 authority、path、projection、selection、用户和数据范围校验,写操作还要验证没有越权状态改变。
PendingIntent 触发组件时只检查 immutable flag 是否足够?
不够。还要核对目标身份、请求码、一次性或重复语义、extras、用户状态、权限和业务幂等;flags 正确不保证组件恢复正确。
向御盾中央平台提交导出组件回归需要准备什么?
准备未加固和加固候选摘要、最终 Manifest 差异、调用方测试包身份、四类组件允许拒绝矩阵、设备系统范围、系统分发和业务副作用回执。