先看结论与判断条件

  • 回归契约的对象是一个明确核心函数和业务资产,不是笼统的页面冒烟或整个应用“运行正常”。
  • 输入需要覆盖正常、边界、空值、错误格式和状态组合,并保存可公开或受控的数据版本与生成方式。
  • 输出比较要先规范化时间、顺序、随机标识和浮点表达,区分业务等价与字节完全相同。
  • 异常类型、错误码、事务结果、文件或数据库变化、日志分类和线程要求都属于函数语义,不能只比较返回值。
  • 性能边界必须在相同候选、设备、进程和迭代条件下测量,由项目风险和调用频率定义,不能套用通用数字。
  • 混淆映射、测试结果和设备证据必须绑定同一 APK 摘要;契约通过也不等于完整兼容矩阵或保护强度得到证明。

先为单个核心函数定义可判定的业务契约

VMP 会改变方法的执行载体和调试形态,但业务方真正关心的是函数语义是否保持。若加固前没有明确契约,团队只能在加固后凭页面是否打开、是否崩溃来判断,细微的错误码、舍入、状态更新或异常变化容易漏掉。契约应以 assetId、方法签名和业务责任为起点。

一个可执行契约至少描述前置状态、输入 schema、测试向量、规范输出、异常、允许副作用、线程条件、资源边界和证据位置。每条用例有稳定 caseId,并引用基线 APK 摘要与保护候选摘要。这样失败能定位到具体输入和语义,而不是把全部问题归为“VMP 不兼容”。

契约不能由实现代码自动复制,因为这样会把当前缺陷固化成预期。业务负责人定义可接受语义,研发提供实现细节,测试负责人把它转换为断言,安全与发布人员确认候选身份。工程判断是让高价值函数优先建立契约,而不是追求所有工具方法都拥有同样深度。

核心函数契约的最小组成
字段回答的问题证据来源失败时动作
assetId 与方法保护哪个业务资产资产清单与编译后符号确认选择器是否漂移
preconditions调用前必须满足什么业务状态和环境夹具区分环境与实现失败
input cases哪些输入必须覆盖受控数据集与生成规则补缺失边界
expected semantics输出、异常和副作用是什么产品规则与基线审查定位语义变化
artifact identity结果对应哪个候选APK、配置与 mapping 摘要拒绝错配报告

输入向量要覆盖边界,不要只保存理想样例

核心函数常在正常输入下表现一致,却在空值、极限长度、编码、状态缺失或并发条件下暴露差异。契约应从输入字段类型、允许域和业务状态推导 case,至少区分有效、边界、无效和依赖状态变化。本文不提供固定用例数量,复杂度应由函数风险和输入空间决定。

测试数据需要可重现。公开安全样例可以直接版本化,涉及客户、账号或专有模型的数据应保存脱敏生成器、内容摘要和受控引用,不能把真实敏感数据放进代码仓库。随机测试必须记录 seed 和生成器版本,否则基线与保护候选可能运行了不同输入而无法比较。

输入序列也可能属于契约。单次纯函数调用与多步状态机不同,授权、缓存、计数器或加密会话可能依赖前序动作。工程判断是把 stateSetup 和 callSequence 明确写入 case,并在每次执行前重置到已知状态。只记录最终请求会遗漏导致结果的历史条件。

输入用例的覆盖维度
维度示例责任常见遗漏契约记录
正常输入主业务路径只保留一条成功样例字段和预期结果
边界输入空值、长度和范围边缘边界由实现猜测业务允许域
错误输入格式、类型和状态不符只断言发生异常异常类型和错误码
状态组合登录、缓存和数据库版本前置状态不重置stateSetup 摘要
输入序列多步协议和重试只测最后一步callSequence 与顺序

输出比较要先定义规范化与等价关系

返回对象可能包含时间戳、随机 ID、无序集合、浮点值或设备信息,直接做字符串比较会产生噪声。契约应列出哪些字段必须完全相同,哪些字段按业务规则规范化,哪些字段只验证格式和关联关系。规范化函数本身需要版本化和测试,不能在失败后临时修改以迎合候选。

业务等价不等于字节相等。序列化字段顺序变化可能不影响业务,金额舍入、权限决策或签名输入变化则可能非常关键。产品与研发应为每个输出字段声明 comparator 类型,例如 exact、ordered-set、format、relation 或 project-tolerance,并写清原因。项目容差不能外推成通用 VMP 结论。

输出摘要适合归档大结果,但必须保留规范化算法和原始结果的受控引用。只保存 SHA-256 能确认结果是否与某次执行相同,却无法解释哪个字段变化。工程判断是门禁先给出字段级差异,再用摘要绑定完整记录。公开报告只展示安全字段,不泄露核心算法输入输出。

不同输出字段的比较策略
字段类型比较方式需要固定错误做法
业务决策exact输入、状态和策略版本允许任意变化
有序结果逐项和顺序比较排序规则与 locale先排序掩盖顺序语义
无序集合规范化成员后比较去重与身份字段比较原始序列
随机或时间字段格式与关联关系时钟、seed 或生成规则要求字节一致
浮点或模型输出项目容差与数据集指标、基线和后果使用通用阈值

异常、副作用和线程条件也是函数语义

很多回归只比较成功返回值,却忽略失败行为。VMP 前后若异常类型、错误码、抛出时机或包装层次变化,上层可能走入不同恢复路径。契约要为预期失败输入记录 exceptionClass、errorCode、messageCategory 和状态结果,并区分业务拒绝与系统故障。错误消息全文通常不适合作稳定断言。

副作用包括数据库写入、文件变化、缓存更新、计数器、网络请求和审计事件。契约应列出允许新增、必须保持和明确禁止的副作用,并在调用前后读取受控状态。事务失败时要检查是否回滚,而不是看到异常就结束。对于外部服务,使用可复现测试环境或记录请求契约,不发送真实客户数据。

线程和重入条件也可能改变语义。某些核心函数要求主线程,另一些必须离开主线程;并发调用可能要求幂等或串行。工程判断是把 dispatcher、thread expectation、timeout ownership 和 reentrancy 写入契约,但不在文章中给出通用超时数字。实际边界应由业务频率、设备和恢复能力决定。

非返回值语义的回归字段
语义基线记录保护候选检查失败影响
异常类型类、错误码和分类同输入触发相同业务分支上层恢复改变
数据库状态前后快照或查询摘要写入、回滚和约束一致数据损坏或重复
文件与缓存允许路径和内容摘要变化范围符合契约状态泄露或陈旧
外部调用请求 schema 和次数不新增未授权副作用费用或重复动作
线程与重入调用线程和并发规则满足同一执行契约死锁、ANR 或竞态

依赖 Android 的语义要放到设备端验证

Android instrumented tests 说明设备或模拟器上的测试可以访问真实 Android 框架能力。核心函数若依赖 Context、Keystore、Binder、SQLite、文件权限、组件或系统 API,JVM 测试只能覆盖部分逻辑。契约应标记 executionClass,把纯逻辑留在快速测试,把平台语义放进 instrumented test。

设备测试必须安装精确候选,并记录 APK、VMP 配置和测试包摘要。测试运行前建立前置状态,运行后收集断言、日志分类和系统信息。若测试 APK 与应用 APK 使用不同 build variant 或依赖版本,结果不能直接作为发布候选证据。测试通过只覆盖声明的 case 和设备。

单一设备不能代表全部 API、ABI 与厂商矩阵。本文只建立核心函数契约,不替代整机兼容矩阵、冷启动测量或异常专项。项目应根据最低系统、主要 ABI、硬件能力和用户分布分配契约用例;未执行的组合明确标记未验证,不用一个绿色总状态遮盖。

契约用例的执行层选择
依赖首选执行层必须记录边界
纯计算本地或 JVM 测试输入、输出和实现版本不证明设备集成
Android frameworkinstrumented test设备、系统与候选摘要单设备不可外推
组件生命周期真实组件启动路径Manifest、进程和状态单元测试不可替代
硬件或 Keystore目标能力设备能力、密钥状态和结果模拟器能力不同
跨进程与 Binder多进程设备用例进程、调用顺序和错误需单独超时诊断

性能边界要用可重复基准,不预设加固结论

Android Macrobenchmark 适合在独立测试进程和可重复设备条件下比较启动或关键路径,并记录迭代。核心函数性能契约应先说明调用频率、用户路径和资源预算,再选择微观计时或端到端基准。基线和保护候选要在同一设备状态、构建身份和测试流程下运行。

基准框架不会提供加固前后的预设结论。缓存、编译模式、温度、后台负载和数据规模都会影响结果。文章不提供统一百分比或毫秒阈值;项目应根据业务后果、调用频率和设备分布定义 performanceBoundaryRef,并保留原始迭代数据,而不是只保存平均值。

性能失败需要定位而不是自动取消保护。方法可能调用过于频繁、输入过大、被放在启动主路径或包含外部 I/O。工程判断是先分解调用次数、函数时间和端到端时间,再选择减少频率、移动时机、缓存或缩小保护范围。任何优化都要重新绑定候选并重复语义契约。

性能契约的可复核上下文
维度固定内容原因输出
候选身份APK 与配置摘要防止比较不同构建基线和保护映射
设备状态型号、系统和运行条件控制环境差异设备记录
数据与调用路径输入版本和频率解释业务负载caseId 与路径
迭代数据每轮原始结果避免平均值掩盖离散受控结果集
边界引用项目批准的判定规则不使用通用数字通过、失败或待评审

R8、retrace 与证据身份必须一起保存

Enable app optimization with R8 说明 R8 会执行缩减、优化和名称混淆,并需要保留规则与输出。VMP 契约中的源码方法可能在最终候选中被内联、重命名或裁剪,因此测试记录要保存 R8 规则、mapping 摘要、VMP 配置和编译后选择器。只有 assetId 到最终方法的映射闭合,失败才能定位。

R8 retrace 要使用同一构建产生的 mapping 还原混淆后的 Java 或 Kotlin 崩溃。拿错 mapping 会产生误导堆栈,retrace 也不处理 Native 符号,更不能修复候选包身份错配。契约失败若伴随崩溃,报告应先核对 APK 摘要和 mapping 摘要,再进行堆栈还原。

NIST SP 800-218 SSDF 要求保留来源、构建、验证和变更证据。契约台账应连接需求、测试实现、基线结果、保护结果、候选身份、mapping、设备和批准结论。变更测试代码或 comparator 时产生新版本,不能覆盖旧回执。SSDF 不定义 VMP 产品功能。

  • assetId 连接源码与编译后方法
  • R8 规则和 mapping 摘要绑定候选
  • 崩溃只用同构建 mapping retrace
  • VMP 配置与 APK 摘要进入结果
  • 测试和 comparator 版本不可覆盖
  • 失败证据保留原始输入与状态

用结果比较器执行公开安全的契约门禁

下面的 Python 示例读取契约、基线结果和保护候选结果。契约定义 caseId、允许状态、输出摘要、异常类型、允许副作用与 performanceBoundaryRef。比较器检查两份结果都绑定同一 case,候选结果满足契约,并输出字段级差异。它不运行攻击、不读取私钥,也不包含客户数据。

性能字段只要求引用已批准证据,并把基线与候选的 evidenceId 交给独立评审,不在脚本中硬编码通用阈值。大输出使用 canonicalOutputSha256 绑定规范化结果,原始结果保留在受控位置。若契约字段缺失、摘要格式错误或出现未允许副作用,脚本返回非零状态。

准备核心函数的软件加固评估时,可整理资产清单、契约版本、公开或受控输入、基线与保护候选、R8 mapping、设备结果和性能边界,再通过御盾中央平台提交申请。契约通过只能证明已覆盖语义在指定范围一致,不证明完整兼容或保护强度。

  • 契约由业务语义而非实现自动复制
  • 输入数据和生成器具有版本
  • 输出规范化算法固定
  • 异常与副作用都有显式断言
  • 性能使用项目边界引用
  • 结果绑定 APK、配置与 mapping 摘要
  • 契约通过不冒充完整兼容
比较基线与保护候选函数契约结果的只读脚本
from pathlib import Path
import json
import re
import sys

if len(sys.argv) != 4:
    raise SystemExit(2)
contract_path = Path(sys.argv[1])
baseline_path = Path(sys.argv[2])
candidate_path = Path(sys.argv[3])
if not contract_path.is_file() or not baseline_path.is_file() or not candidate_path.is_file():
    raise SystemExit(2)
contract = json.loads(contract_path.read_text(encoding="utf-8"))
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
candidate = json.loads(candidate_path.read_text(encoding="utf-8"))
required = ["caseId", "allowedStatus", "expectedOutputSha256", "expectedException", "allowedSideEffects", "performanceBoundaryRef"]
if any(field not in contract for field in required):
    raise SystemExit(2)
if baseline.get("caseId") != contract["caseId"] or candidate.get("caseId") != contract["caseId"]:
    raise SystemExit(2)
sha256_pattern = re.compile(r"^[0-9a-f]{64}$")
expected_digest = str(contract["expectedOutputSha256"]).lower()
if not sha256_pattern.fullmatch(expected_digest):
    raise SystemExit(2)
if candidate.get("status") not in contract["allowedStatus"]:
    raise SystemExit(2)
if str(candidate.get("canonicalOutputSha256", "")).lower() != expected_digest:
    raise SystemExit(2)
if candidate.get("exception", "") != contract["expectedException"]:
    raise SystemExit(2)
allowed_effects = set(contract["allowedSideEffects"])
candidate_effects = set(candidate.get("sideEffects", []))
if not candidate_effects.issubset(allowed_effects):
    raise SystemExit(2)
if not baseline.get("performanceEvidenceId") or not candidate.get("performanceEvidenceId"):
    raise SystemExit(2)
differences = {}
for field in ["status", "canonicalOutputSha256", "exception", "sideEffects"]:
    if baseline.get(field) != candidate.get(field):
        differences[field] = {"baseline": baseline.get(field), "candidate": candidate.get(field)}
result = {"status": "contract-pass", "caseId": contract["caseId"], "differencesForReview": differences, "performanceBoundaryRef": contract["performanceBoundaryRef"], "performanceEvidence": {"baseline": baseline["performanceEvidenceId"], "candidate": candidate["performanceEvidenceId"]}}
print(json.dumps(result, ensure_ascii=False, indent=2))

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
依赖 Android 运行时、组件和系统 API 的语义应在设备端验证。Android instrumented tests 说明设备测试可以访问真实 Android 框架能力。单一设备通过不能代表完整 API、ABI 和厂商矩阵。
启动和关键路径比较应在独立测试进程与可重复设备条件下执行并记录迭代。Android Macrobenchmark 描述 Android 端到端基准的执行模型。基准框架不提供加固前后的预设性能结论或通用阈值。
混淆后的 Java 或 Kotlin 崩溃需要使用同一构建的 mapping 进行还原。R8 retrace 说明堆栈还原对 mapping 文件的使用。retrace 不处理 Native 符号,也不能修复候选包或 mapping 身份错配。
R8 会执行代码和资源缩减、优化与名称混淆并产生发布输出。Enable app optimization with R8 说明 R8 的发布构建职责。R8 优化不等于 VMP,也不证明抗动态分析或核心函数保护强度。
安全发布应保留来源、构建、验证和变更证据。NIST SP 800-218 SSDF 提供组织级安全软件开发实践。SSDF 不定义具体 VMP 产品、测试阈值或项目验收结论。
移动安全验收需要按不同控制域分别取证。OWASP MASVS overview 组织存储、密码学、认证、平台交互、代码质量与韧性等控制域。控制域不提供御盾或任何具体产品的通过结论。
VMP 前后回归契约必须同时覆盖输入、输出、异常和副作用。工程判断:只比较成功返回值会遗漏上层控制流、事务和外部状态变化。具体字段和用例取决于函数业务语义,本文不提供通用数量。
性能边界必须由项目风险、调用频率和目标设备定义。工程判断:同一执行变化在不同业务路径和设备上的后果不同。本文不提供统一百分比、毫秒阈值或任何项目性能结论。

工程常见问题

VMP 前后页面都能打开,为什么还要函数契约?

页面冒烟会漏掉错误码、舍入、异常、副作用和边界输入变化。函数契约能把失败定位到具体资产、case 和语义字段。

回归契约是否只需要保存输入和返回值?

不够。还要记录前置状态、异常、事务、文件或数据库变化、外部调用、线程条件、候选身份和性能证据。

输出包含时间戳或随机 ID 时怎样比较?

先定义版本化规范化规则,固定必须相等字段,对随机或时间字段验证格式和关联关系,不能在失败后临时改 comparator。

JVM 单元测试能否替代 instrumented test?

纯计算可以优先用 JVM 测试;依赖 Context、组件、Keystore、Binder、文件权限或系统 API 的语义需要设备端验证。

契约通过是否证明 VMP 没有性能影响?

不能。语义契约与性能证据应分开,性能要在相同候选、设备和流程下按项目边界评审,不存在通用预设结论。

申请核心函数 VMP 评估前要准备什么?

准备资产清单、契约、输入数据、基线与保护候选、VMP 配置、R8 mapping、设备和性能证据,再从御盾中央平台提交申请。

想用自己的 App 验证?

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

继续阅读: VMP 加固是什么,如何选择保护范围