先看结论与判断条件
- 目标 APK 与测试 APK 都要记录摘要、包名、版本和签名,安装成功不等于指向正确候选。
- 黑盒测试只使用公开组件、系统 UI 和用户可观察结果,不注入内部代码。
- 不得为测试新增 exported 组件、调试后门或宽权限,否则验收对象已经改变。
- 允许路径与拒绝路径成对设计,拒绝要核对错误类型和业务状态未变化。
- 首次安装、覆盖安装、清数据、进程重建和权限撤销属于不同前置状态。
- 外部结果证明公开契约可观察,不证明内部状态、保护强度或全部代码分支。
锁定目标 APK 与外部测试 APK 的双重身份
先把目标包、测试包、版本、摘要与用途写成可复核对象,而不是凭页面印象判断。两份 APK SHA-256、packageName、versionCode、证书摘要和来源应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,测试是否指向指定最终候选和指定测试工具才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。
安装前后读取包信息并复算摘要,不按文件名选择目标。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。
任一 APK 摘要、包名或证书缺失时先标记 blocked,并把剩余未知写进报告。黑盒测试不能确认目标 APK 内部实现或代码覆盖率。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 目标包、测试包、版本、摘要与用途身份 | 两份 APK SHA-256、packageName、versionCode、证书摘要和来源 | 测试是否指向指定最终候选和指定测试工具 | 任一 APK 摘要、包名或证书缺失则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 安装前后读取包信息并复算摘要,不按文件名选择目标 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 黑盒测试不能确认目标 APK 内部实现或代码覆盖率 | 限制结论范围 | 不得扩大为全局结论 |
确认签名、权限和安装顺序
签名证书、权限授予和安装会话要从状态变化而不是最终页面开始核对。把安装顺序、证书、声明权限、授权状态、会话 ID 和结果按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断失败源于签名权限、安装状态还是应用行为发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。
按生产真实权限运行,授权变化单独建立用例。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。
验收不能只看操作完成或命令退出码。测试签名获得了生产中不存在的特殊权限说明链路仍缺少可归因证据;签名级权限与 sharedUser 等关系必须按真实生产机制处理。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 签名证书、权限授予和安装会话身份 | 安装顺序、证书、声明权限、授权状态、会话 ID 和结果 | 失败源于签名权限、安装状态还是应用行为 | 测试签名获得了生产中不存在的特殊权限则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 按生产真实权限运行,授权变化单独建立用例 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 签名级权限与 sharedUser 等关系必须按真实生产机制处理 | 限制结论范围 | 不得扩大为全局结论 |
从 Manifest 建立合法公开交互面
处理Activity、Service、Receiver、Provider 与 Intent时,先固定应用身份、候选摘要、平台版本和可变条件。exported、permission、intent-filter、authority 与目标包必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论哪些交互是生产契约允许的测试入口,否则重试成功也只能说明环境变了,不能支持技术结论。
从最终 APK 声明只读导出组件清单并由 owner 审核。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。
出现用例依赖未导出或未授权组件,应回到上一份同源输入,比较首次分叉点并保留两侧回执。不得修改 Manifest 或扩大导出面来方便测试。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| Activity、Service、Receiver、Provider 与 Intent身份 | exported、permission、intent-filter、authority 与目标包 | 哪些交互是生产契约允许的测试入口 | 用例依赖未导出或未授权组件则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 从最终 APK 声明只读导出组件清单并由 owner 审核 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 不得修改 Manifest 或扩大导出面来方便测试 | 限制结论范围 | 不得扩大为全局结论 |
为每个业务契约设计允许和拒绝路径
先把合法调用、未授权调用、畸形输入和取消写成可复核对象,而不是凭页面印象判断。输入摘要、预期、观察、错误、状态前后值和证据引用应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,每个公开入口是否正确接受合法输入并拒绝越界输入才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。
每条允许用例配一条权限拒绝、畸形输入或取消用例。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。
预期拒绝却改变了业务状态时先标记 blocked,并把剩余未知写进报告。拒绝结果需要无副作用验证,不能通过破坏数据制造证据。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。
固定设备、系统状态和用户前置条件
API、型号、ABI、账户、权限和进程状态要从状态变化而不是最终页面开始核对。把系统构建号、安装状态、网络、账号、locale 与重复次数按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断结果差异是否来自设备环境而非候选变化发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。
每轮恢复明确前置状态,只改变一个设备或权限变量。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。
验收不能只看操作完成或命令退出码。不同设备用例混用账号或安装状态说明链路仍缺少可归因证据;设备矩阵按支持范围确定,未测环境保持未验证。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| API、型号、ABI、账户、权限和进程状态身份 | 系统构建号、安装状态、网络、账号、locale 与重复次数 | 结果差异是否来自设备环境而非候选变化 | 不同设备用例混用账号或安装状态则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 每轮恢复明确前置状态,只改变一个设备或权限变量 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 设备矩阵按支持范围确定,未测环境保持未验证 | 限制结论范围 | 不得扩大为全局结论 |
只用可观察结果形成业务断言
处理系统 UI、返回码、持久化结果和后续页面时,先固定应用身份、候选摘要、平台版本和可变条件。页面状态、系统对话框、结果码、文件变化和业务确认必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论断言是否可由用户或系统回执独立观察,否则重试成功也只能说明环境变了,不能支持技术结论。
使用稳定公开观察点,不依赖内部日志或隐藏字段。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。
出现断言只能从测试代码自述而无法外部观察,应回到上一份同源输入,比较首次分叉点并保留两侧回执。截图可辅助但不能替代机器状态与业务回执。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。
区分安装器回执与应用运行结果
先把PackageInstaller 会话、安装状态与启动状态写成可复核对象,而不是凭页面印象判断。session 状态、安装错误、包信息、首次启动和进程重建应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,安装成功后应用是否仍能按预期启动和交互才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。
分别保存安装器和应用回执,避免把两类成功混为一谈。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。
安装返回成功但目标包版本或签名不符时先标记 blocked,并把剩余未知写进报告。PackageInstaller 只说明安装会话,不证明应用业务正确。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| PackageInstaller 会话、安装状态与启动状态身份 | session 状态、安装错误、包信息、首次启动和进程重建 | 安装成功后应用是否仍能按预期启动和交互 | 安装返回成功但目标包版本或签名不符则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 分别保存安装器和应用回执,避免把两类成功混为一谈 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | PackageInstaller 只说明安装会话,不证明应用业务正确 | 限制结论范围 | 不得扩大为全局结论 |
用代码审计外部测试清单
脱敏测试用例中的包、设备和断言要从状态变化而不是最终页面开始核对。把caseId、目标摘要、测试摘要、预期、观察、状态和证据按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断用例是否字段闭合且错误通过被拒绝发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。
把公开清单写成 JSON,让校验器拒绝候选错配和断言不一致。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。
验收不能只看操作完成或命令退出码。预期和观察不一致却标记通过说明链路仍缺少可归因证据;代码不安装 APK、不调用隐藏组件,也不包含攻击或绕过脚本。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。
import hashlib
import json
import sys
from pathlib import Path
REQUIRED = set([
'caseId',
'targetSha256',
'testApkSha256',
'deviceApi',
'expectedOutcome',
'observedOutcome',
'result',
'evidenceRef'
])
ALLOWED_RESULTS = set([
'pass',
'blocked',
'mismatch'
])
FORBIDDEN = {'token', 'password', 'privateKey', 'secret', 'customerData'}
def stop(message):
raise SystemExit(message)
def digest(path):
hasher = hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
hasher.update(chunk)
return hasher.hexdigest()
def text(record, field, index):
value = str(record[field]).strip()
if not value:
stop(f'record {index} has empty {field}')
return value
def validate(record, index):
if not isinstance(record, dict):
stop(f'record {index} is not an object')
if FORBIDDEN.intersection(record):
stop(f'record {index} contains forbidden fields')
missing = sorted(REQUIRED - record.keys())
if missing:
raise SystemExit(f'record {index} lacks fields: {missing}')
for field in REQUIRED:
if field != 'deviceApi':
text(record, field, index)
result = text(record, 'result', index)
if result not in ALLOWED_RESULTS:
stop(f'record {index} has an unsupported result')
expected = text(record, 'expectedOutcome', index)
observed = text(record, 'observedOutcome', index)
evidence = text(record, 'evidenceRef', index)
if expected != observed and result == 'pass':
stop(f'record {index} passes despite a mismatch')
if expected != observed and not evidence:
stop(f'record {index} has a mismatch without evidence')
return text(record, 'caseId', index)
if len(sys.argv) != 2:
stop('usage: python validate_black_box.py public-checks.json')
source = Path(sys.argv[1]).expanduser().resolve()
if not source.is_file():
stop('public check file is missing')
raw = source.read_bytes()
data = json.loads(raw.decode('utf-8'))
records = data.get('blackBoxCases') if isinstance(data, dict) else None
if not isinstance(records, list) or not records:
raise SystemExit('blackBoxCases must be a non-empty list')
identifiers = [validate(record, index) for index, record in enumerate(records)]
if len(identifiers) != len(set(identifiers)):
raise SystemExit('record identifiers are duplicated')
summary = {
'inputSha256': hashlib.sha256(raw).hexdigest(),
'records': len(records),
'results': {state: sum(1 for item in records if item['result'] == state) for state in sorted(ALLOWED_RESULTS)},
'status': 'pass',
}
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))按公开契约关闭黑盒验收
处理最终候选、公开入口、设备覆盖和回执时,先固定应用身份、候选摘要、平台版本和可变条件。通过、阻塞、未覆盖、限制、owner 和复核时间必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论黑盒证据足以接受哪些契约、哪些仍需白盒验证,否则重试成功也只能说明环境变了,不能支持技术结论。
由业务 owner 与兼容负责人逐项复核并登记限制。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。
出现把少量外部用例宣告为全部功能通过,应回到上一份同源输入,比较首次分叉点并保留两侧回执。结论仅覆盖列明候选、设备、入口和可观察契约。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 依赖真实 Android 运行时、组件和系统 API 的语义应通过设备端 instrumented test 验证。 | Android instrumented tests:依赖真实 Android 运行时、组件和系统 API 的语义应通过设备端 instrumented test 验证。 | 单一设备通过不能代表完整 API、ABI 和厂商矩阵。 |
| Manifest 定义组件、权限、intent filter、SDK 约束和应用元数据。 | Android app manifest:Manifest 定义组件、权限、intent filter、SDK 约束和应用元数据。 | 静态清单不能证明运行时访问控制没有被业务代码放宽。 |
| 导出组件和 intent filter 会扩大跨应用调用面,需显式权限和输入校验。 | Android exported component risk:导出组件和 intent filter 会扩大跨应用调用面,需显式权限和输入校验。 | exported 值只是入口条件之一,不等于完整威胁分析。 |
| split 安装要求 base、packageName、versionCode 与签名证书一致。 | Android PackageInstaller:split 安装要求 base、packageName、versionCode 与签名证书一致。 | API 约束不证明 Play 服务器最终生成的所有 split 已被抽样。 |
| 发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。 | Sign your Android app:发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。 | 文档不能确认某个实际包使用了正确生产证书。 |
| CTS 用于验证设备实现与 Android 兼容性定义的一致性。 | Android CTS overview:CTS 用于验证设备实现与 Android 兼容性定义的一致性。 | 设备通过 CTS 不代表第三方 App 加固后的业务兼容通过。 |
| 外部测试 APK 不得要求目标 App 扩大生产公开面。 | 工程判断:改变 exported 或权限会改变验收对象并引入额外攻击面。 | 确需测试专用构建时应与生产候选分开管理,不能冒充最终 APK。 |
| 黑盒通过只证明列明公开契约在指定环境可观察。 | 工程判断:外部工具无法看到内部断言和未暴露状态,只能依据系统与业务回执。 | 代码覆盖、保护强度和内部状态仍需其他证据。 |
工程常见问题
外部测试 APK 必须和目标 APK 同签名吗?
不一定。应按真实生产交互所需权限选择;为了获得签名权限而共用生产密钥会扩大风险,必须单独授权和审计。
可以临时导出 Activity 方便测试吗?
不能用修改后的目标包验收原最终候选。测试只能走原有公开面,专用测试构建必须独立标识,不能替代生产验收。
安装成功是否说明加固 APK 正常?
不说明。安装器回执只覆盖会话;还要验证首次启动、权限、公开组件、业务结果、进程重建和失败恢复。
黑盒测试怎样检查拒绝路径?
使用未授权调用、畸形但安全的参数、用户取消或权限撤销,核对明确拒绝且业务状态未发生不应有的改变。
只有截图可以当证据吗?
截图可辅助,但应同时保留包信息、设备、操作时间、系统结果码和可观察业务状态,避免只凭页面判断。
提交御盾诊断要准备什么?
准备目标与测试 APK 摘要、包名、证书、设备、安装顺序、权限、公开组件清单、允许和拒绝用例及真实回执。