先看结论与判断条件

  • 测试清单必须来自最终加固候选的 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 filteraction 与类别隐式解析更新分发用例
process enabled组件属性生命周期可用性补充状态测试

先区分系统是否分发与业务是否接受

跨应用调用至少经过两层判定。Android 系统根据组件存在、exported、权限、Intent 解析、URI grant 和进程状态决定是否分发;组件收到调用后,业务代码再验证调用方、用户会话、参数、数据范围和幂等条件。测试报告要分别记录 deliveryResult 与 businessResult,不能把未收到回调统一写成业务拒绝,也不能把入口到达写成操作成功。

Android exported component risk 指出导出组件和 intent filter 会扩大跨应用调用面,需要显式权限和输入校验。exported 值只是入口条件之一:即使 exported 为 true,也可以用签名级权限、调用方校验和严格数据规则限制能力;即使组件未导出,错误的委托能力或同 UID 交互仍可能产生其他边界。回归只对清单中约定的入口作候选级验证。

观测点应公开安全。记录候选摘要、调用方测试包公开身份、组件、测试用例、Intent 类型、权限集合、参数 schema、系统结果、业务结果和关联号,不记录真实用户数据、令牌或内部 URI。拒绝路径要验证没有副作用,例如没有创建任务、写入数据、启动后台工作或发送后续广播,而不只是检查异常文本。

两层调用结果
系统结果业务结果解释下一步
not-deliverednot-run系统入口拒绝核对声明和权限
deliveredallowed合法调用完成检查副作用回执
delivereddenied业务安全拒绝确认无副作用
deliveredinvalid-input参数校验拒绝核对 schema
deliveredunavailable业务状态不可用验证恢复路径
unknownunknown证据不足补充设备与日志

Activity 要覆盖显式、隐式和任务状态

导出 Activity 的允许用例需要分别覆盖显式组件 Intent 与符合 intent filter 的隐式 Intent,并记录解析出的唯一目标。若多个应用都能处理同一隐式 Intent,测试应控制选择结果,不能把系统选择器行为误判为组件兼容失败。启动后验证目标页面、用户会话、参数 schema、返回结果和任务栈,确保加固没有破坏入口类、路由或生命周期。

拒绝矩阵包含调用方无权限、账号未登录、资源不属于当前用户、必填参数缺失、类型错误、超长值、未知枚举和重复请求。参数测试使用合成数据,不构造可运行攻击链。对于安全拒绝,Activity 可以展示受控错误或直接结束,但必须确认未执行受保护操作。异常崩溃不算合格拒绝,因为它既暴露可用性问题也没有明确业务状态。

进程和任务状态覆盖应用未启动、后台存在、前台活动、目标 Activity 被禁用、用户从最近任务恢复以及配置变更。冷进程能发现初始化顺序和类加载问题,热进程能发现旧状态或重复 Intent 消费问题。本文不展开 Deep Link 解析专题;若 Activity 由网页链接进入,只把最终组件分发和业务校验纳入本矩阵。

Activity 跨应用回归矩阵
维度允许用例拒绝用例回执
目标方式显式正确组件不存在或禁用组件系统分发结果
隐式解析匹配 action 类别缺失必要类别解析目标
调用权限持有批准权限无权限调用方SecurityException 类别
业务会话已登录有权用户未登录或越权业务判定
参数合法 schema缺失类型错误校验结果
进程状态冷启动和热启动组件禁用生命周期事件

Service 要分开 start、bind 与调用方生命周期

导出 Service 的启动与绑定语义不同。start 路径关注系统能否启动服务、Intent 是否正确消费、重复启动是否幂等以及后台限制如何表现;bind 路径还要关注 Binder 接口、连接断开、调用方死亡和重连。测试清单明确每个 Service 支持哪种方式,禁止因一种方式成功就把另一种登记为通过。

权限不仅检查 Manifest 声明,还要验证运行时方法。持有组件启动权限的调用方未必有权执行所有 Binder 操作,接口应按业务身份和数据范围再次校验。拒绝用例覆盖无权限、错误调用身份、无会话、未知操作码、畸形数据和重复事务,并确认没有残留任务、通知、文件或服务端请求。

冷进程测试从目标应用完全未运行开始,验证 Service 所需初始化、依赖注入和加固后类加载;组件禁用、系统停止包或服务依赖不可用时,验证可解释的失败。恢复后使用新关联号再次调用,不复用已失败事务的可变状态。没有设备端和业务端双回执时,只能记录调用尝试。

Service 调用边界
路径系统条件业务检查失败副作用
startexported 与权限命令 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 变化,所有调用方契约都可能失效;若系统分发正常而数据解析失败,才进入模型和保护范围排查。

Provider 访问矩阵
维度允许条件拒绝条件验证结果
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 差异、调用方测试包身份、四类组件允许拒绝矩阵、设备系统范围、系统分发和业务副作用回执。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: App 加固 PoC 如何形成发布结论