先看结论与判断条件

  • 依赖声明不是最终输入,必须冻结目标 release configuration 的解析组件、artifact 摘要、variant 属性与选择原因。
  • 方法身份使用 owner、name、descriptor、访问标记、代码摘要和关键调用边,不能只比较源码展示名。
  • 每条 VMP 规则必须声明期望方法和命中数;工具退出码为零但规则零命中仍然是发布失败。
  • 删除、迁移、签名变化、实现替换与新增候选分别处理,自动迁移只能生成待审候选规则。
  • 先解决传递依赖冲突和重复类,再评估 VMP 命中;宽泛通配会掩盖来源与扩大兼容风险。
  • 静态漂移检查不证明 VMP 已执行或运行兼容,仍需最终候选核对、验签、SDK 独立审计与设备测试。

直接答案:同时比较依赖图、最终方法索引、R8 映射和规则命中结果

依赖升级后的 VMP 规则漂移不能靠构建成功判断。门禁应冻结升级前后同一 release 变体的 resolved dependency graph、最终 APK 方法索引、R8 mapping、VMP 规则展开结果和调用路径,再逐条确认旧规则仍命中同一业务语义;零命中、签名变化、owner 替换或命中数异常都阻断发布。

规则文本没有变化不代表保护对象没有变化。依赖可重命名类、迁移包、改变参数或返回类型、把实现改成生成代码,R8 还可能内联、合并或删除方法。若规则只按模糊名称匹配,它可能继续返回成功,却命中另一个 adapter;若工具忽略未命中规则,则关键方法可能完全失去保护。

审计目标不是让命中数量机械保持不变,而是确认受保护的业务责任仍有唯一、稳定且可回归的实现。依赖升级还会带来行为、权限和数据处理变化,VMP 只能处理选定客户端实现的分析成本,不能替代服务端授权、SDK 安全评审或真实设备兼容测试。

依赖升级后的主要漂移信号
信号可能原因默认处置需要证据
旧规则零命中类或方法删除阻断最终方法索引
描述符变化参数或返回类型变更阻断前后签名
owner 变化包迁移或重定位人工确认依赖来源
命中数增加通配扩张阻断展开清单
调用目标变化adapter 或实现替换回归调用边
规则成功但对象消失工具忽略未命中阻断逐规则回执

先冻结同一 release 变体的解析依赖图

Gradle 解析的是直接与传递依赖组成的图,不是 build.gradle 中几行声明。平台、约束、BOM、严格版本、替换和 conflict resolution 都会改变最终选择。漂移比较必须保存 releaseRuntimeClasspath 等目标 configuration 的 component、版本、variant 属性、artifact 摘要和选择原因。

不能拿 debug 依赖图、IDE External Libraries 或版本目录文本代替 release 解析结果。debug 可能多出调试工具、使用不同 flavor 依赖并跳过发布专属插件。门禁需要与最终候选的 build ID、applicationId、variantName、R8 配置和签名流程绑定,避免证据错配。

依赖锁定与 verification metadata 能提高输入稳定性,但不会自动证明 API 或运行语义未变。升级批准应记录为什么变、更改了哪些 artifact、传递图新增和删除什么、是否出现动态版本或仓库来源变化。来源不明或摘要漂移时先停止,而不是直接调整 VMP 通配规则。

依赖证据的粒度
证据最小字段漂移用途局限
声明依赖group/name/request解释意图非最终选择
解析组件selected version/reason发现版本替换不含字节
artifact 清单文件 SHA-256绑定输入不含方法
variant 属性usage/flavor/buildType避免错图不含运行结果
锁文件版本集合可复现不证明兼容
仓库来源repository 与校验供应链审计不证明业务语义

最终方法索引要以 owner、name、descriptor 和代码摘要为身份

VMP 规则通常最终落到 DEX 方法。稳定身份至少包含类描述符 owner、方法名、参数与返回类型 descriptor、访问标记和所在 DEX;为识别同签名实现替换,还可保存规范化指令摘要、调用目标集合和来源 artifact。只保存 Java 展示名会混淆重载、桥接与合成方法。

索引必须从加固前的最终发布候选生成,而不是从源码或单个依赖 JAR 推断。R8 会改名、内联、合并、移除和优化控制流;Kotlin、Compose、serialization 与 desugaring 还会生成额外方法。规则若使用 mapping 前身份,应保存到 mapping 后方法的确定映射和未映射原因。

升级前后比较时区分身份变化与语义变化。owner 搬迁但代码和调用责任一致,可能通过规则迁移解决;签名相同但指令摘要与调用目标大变,仍需重新评审。任何自动迁移只能生成候选规则,不能未经人工确认直接进入生产范围。

方法身份与语义线索
字段回答问题变化含义门禁
owner方法属于哪个类包迁移/重定位确认来源
name规则入口名重命名/桥接结合 mapping
descriptor精确参数返回API 改动阻断
access flags静态/虚拟/桥接调用约定变化回归
code digest实现是否替换语义可能变化人工评审
call targets关键下游是谁责任漂移路径回归

规则必须逐条展开并规定零命中、单命中和多命中语义

精确规则应绑定最终方法身份或可验证的 mapping 前身份,并声明期望命中数量。零命中不能只是 warning;它表示保护对象消失、规则格式失效或工具链忽略。多命中也不能自动接受,可能是重载新增、bridge 生成、通配范围扩大或同名方法被复制。

宽泛包规则会把依赖升级引入的新类自动纳入,表面上保持成功,实际扩大性能和兼容风险。相反,基于不稳定 synthetic 名称的规则会频繁失效。应优先保护显式业务函数,把反射、JNI、生成 adapter 和平台胶水交给精确 keep 与独立回归,避免一条规则承担两种职责。

每条规则回执至少保存规则 ID、期望方法、实际 owner/name/descriptor、命中数、mapping 关系、方法摘要和决策。工具若无法导出展开结果,不应把退出码零视为完成;需从最终 DEX 另行核对标记或保护清单,并明确证据只能覆盖工具可观察部分。

把漂移分成删除、迁移、签名变化、实现替换和新增候选

删除表示旧方法在候选中不存在,可能是功能移除、内联或依赖替换;迁移表示业务责任仍在但 owner 变化;签名变化包括参数、返回类型或静态与实例调用约定改变;实现替换则是身份不变而方法摘要或关键调用目标变化。四类都需要不同处置。

新增候选不是看到新方法就扩大保护。先确认它是否承担新的关键业务责任、是否可被服务端最终裁决、变化是否稳定、能否建立固定向量和设备回归。生成代码、简单 getter、日志、线程调度和 SDK adapter 通常不是高价值候选,盲目纳入只会放大保护面。

规则迁移应和源码或架构变更一起评审。若旧业务责任被拆成多个函数,先确定哪个函数持有不可替代的决策,再把副作用和平台边界剥离。不能为了保留单命中数字,把多个新函数随意包装进一个人工桥接方法而不验证语义。

漂移类型与修复策略
类型识别条件修复禁止做法
删除旧身份不存在确认功能或内联静默忽略
迁移owner 变、责任相同精确迁移规则包通配
签名变化descriptor 改变更新调用与测试只改名称
实现替换摘要/调用边变化重新评审语义沿用旧回执
新增候选出现关键责任单独立项自动全选
命中扩张数量超期望收紧规则接受成功码

传递依赖冲突与重复类要先解决,再讨论 VMP 规则

Android 依赖诊断资料要求从变体对应的解析结果处理重复类与版本冲突。一个 SDK 升级可能带来旧模块与新模块并存、传递版本被强制、shade 不完整或 classpath 与最终 DEX 来源不一致。若类身份本身不唯一,VMP 规则命中结果也没有稳定解释。

排查顺序是确认 selected component 与原因、检查重复 artifact 和类、定位约束或排除规则、重建最终方法索引,再比较 VMP 规则。不要先扩大通配让工具重新命中,这会把冲突的两份实现都纳入保护,并可能把类加载问题变成更难诊断的运行异常。

构建冲突消失不等于运行时语义恢复。依赖可以在二进制兼容的情况下改变默认值、线程、回调和数据处理。应保存升级前后关键调用路径、错误类型和副作用次数,在真实 Android 运行时执行回归,静态方法身份只用于决定需要测什么。

SDK 升级评审还要覆盖权限、可靠性和数据处理

Google Play SDK Index 可为部分 SDK 提供版本采用、可靠性、安全和数据处理线索,但不覆盖全部私有或开源依赖,也不能替代成品清单和代码审计。发现已知问题或政策变化时,应把它们纳入升级理由和回归范围,而不是据此直接认定某个版本安全。

依赖升级可能新增权限、ContentProvider、后台任务、native library、网络端点或数据收集。VMP 规则漂移门禁只回答关键方法是否仍被正确选择;Manifest 差分、SBOM、签名、Native ABI、隐私与服务端契约需要独立 gate。把所有风险压进一个命中报告会产生错误完成感。

若 SDK 闭源,类方法索引与运行回归更重要,但仍不得虚构其内部实现或攻击效果。对无法解释的新增组件、权限或高敏调用保持 blocked,向供应商索取一手说明与版本证据。预算不应降低证据门槛,缺少事实就明确标记未接入。

用基线和候选方法清单检查规则是否失效或扩张

下面的 Python 脚本读取公开 JSON:规则定义包含 ruleId、expectedBaselineMethod 和 expectedHits;基线与候选方法索引使用 owner、name、descriptor、codeSha256 和 callTargets。脚本不修改 APK,也不生成 VMP 规则,只把删除、签名变化、实现变化、调用边变化、零命中和多命中列为阻断差异。

示例采用精确匹配,避免把通配表达式当作生产规则语言。真实流水线可以在受控适配器中展开产品规则,再把展开后的精确方法集合交给同类比较。任何输入结构、摘要或期望命中数量不合法都会非零退出,确保失败来自供应的证据文件。

通过结果只说明这三份输入之间没有定义的漂移,不证明索引来自完整最终 APK、VMP 实际执行、运行语义兼容或 SDK 没有其他风险。回执必须绑定候选哈希、解析依赖图、mapping 与规则工具版本,并在设备上覆盖关键调用路径。

依赖升级前后 VMP 规则命中漂移门禁
import json
import re
import sys
from pathlib import Path

if len(sys.argv) != 4:
    raise SystemExit("usage: check_rule_drift.py rules.json baseline.json candidate.json")

def load_array(name):
    path = Path(name).resolve()
    if not path.is_file():
        raise SystemExit(f"input missing: {path.name}")
    with path.open("r", encoding="utf-8") as stream:
        value = json.load(stream)
    if not isinstance(value, list):
        raise SystemExit(f"{path.name} root must be an array")
    return value

rules = load_array(sys.argv[1])
baseline = load_array(sys.argv[2])
candidate = load_array(sys.argv[3])
digest = re.compile(r"^[0-9a-f]{64}$")

def method_key(item):
    for field in ("owner", "name", "descriptor", "codeSha256", "callTargets"):
        if field not in item:
            raise SystemExit(f"method record missing {field}")
    if not digest.fullmatch(str(item["codeSha256"])):
        raise SystemExit("method codeSha256 is invalid")
    if not isinstance(item["callTargets"], list):
        raise SystemExit("callTargets must be an array")
    return (item["owner"], item["name"], item["descriptor"])

base_by_key = {method_key(item): item for item in baseline}
candidate_by_key = {method_key(item): item for item in candidate}
errors = []
for rule in rules:
    rule_id = rule.get("ruleId")
    expected = rule.get("expectedBaselineMethod")
    hits = rule.get("expectedHits")
    if not rule_id or not isinstance(expected, dict) or hits != 1:
        errors.append(f"invalid rule declaration: {rule_id!r}")
        continue
    identity = (expected.get("owner"), expected.get("name"), expected.get("descriptor"))
    before = base_by_key.get(identity)
    after = candidate_by_key.get(identity)
    if before is None:
        errors.append(f"{rule_id}: baseline identity is absent")
        continue
    if after is None:
        same_name = [key for key in candidate_by_key if key[:2] == identity[:2]]
        if same_name:
            errors.append(f"{rule_id}: descriptor changed")
        else:
            errors.append(f"{rule_id}: zero candidate hits")
        continue
    if after["codeSha256"] != before["codeSha256"]:
        errors.append(f"{rule_id}: implementation digest changed")
    if sorted(after["callTargets"]) != sorted(before["callTargets"]):
        errors.append(f"{rule_id}: call target set changed")

if errors:
    print(json.dumps({"status": "failed", "differences": sorted(errors)}, indent=2))
    raise SystemExit("VMP rule drift requires review")
print(json.dumps({
    "status": "passed",
    "rules_checked": len(rules),
    "boundary": "identity evidence passed; actual VMP execution and runtime are separate gates"
}, indent=2))

发布前完成规则迁移、候选验证和设备回归

门禁顺序建议为:冻结升级理由与依赖锁、生成同一 release 变体解析图、建立最终方法索引、关联 R8 mapping、展开规则、比较命中与调用边、执行 VMP、重新核对最终候选,再做验签与设备回归。任一输入或候选变化都使后续回执失效。

设备测试覆盖正常、拒绝、异常、取消、反射、JNI 和关键系统组件路径,记录保护前后输出、副作用次数、线程与错误类型。Android instrumented tests 适合验证依赖框架的语义,但单一设备通过不能代表全部 API、ABI 和厂商矩阵。报告必须写明真实覆盖。

关于范围改动的整体影响可继续阅读本站 /zh-cn/articles/vmp-scope-change-impact-analysis/;需要评审具体依赖升级后的保护规则,可在御盾中央平台 https://www.leonadev.com/console/ 提交脱敏的依赖图、候选摘要、mapping 和规则展开清单。

  • 只使用目标 release 变体的解析依赖图。
  • 从最终候选建立精确 owner/name/descriptor 索引。
  • 逐条记录零命中、单命中和多命中。
  • 把方法摘要与关键调用边纳入比较。
  • 先解决重复类和依赖冲突再迁移规则。
  • 候选变化后重新执行 VMP、验签与设备回归。
  • 单独审计 SDK 权限、组件、Native 与数据处理。
  • 内链和御盾中央行动入口保持可访问。

事实依据与适用边界

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

本文判断事实或工程依据适用限制
依赖升级影响的是直接与传递依赖解析后的实际组件图。Android Gradle dependency resolution 说明版本解析、依赖图和运行时兼容背景,支持保存 release 变体的最终选择。依赖树只能缩小范围,不能证明某个 SDK 是唯一根因或运行行为保持不变。
重复类和版本冲突必须从目标变体的解析结果处理。Debug Android dependency resolution 描述重复类与依赖版本冲突的诊断路径,支持在规则迁移前解决类身份问题。构建冲突消失不等于加固后运行时冲突、回调或业务语义已通过。
SDK 升级评审应考虑可靠性、安全和数据处理线索。Google Play SDK Index 提供部分 SDK 的版本采用、可靠性、安全与数据处理信息,可作为版本评审输入。SDK Index 不覆盖所有私有或开源 SDK,也不替代二进制清单、供应商证据和项目测试。
反射、JNI 和间接入口应使用精确 keep 规则。R8 keep rules best practices 说明宽泛 keep 会阻止优化并掩盖边界,支持把可达性规则与 VMP 选择分工。keep 规则只描述 R8 可达性,不定义业务价值、VMP 适用性或实际保护强度。
升级和规则迁移应保留来源、构建、验证与变更证据。NIST SP 800-218 SSDF 将软件供应链风险和安全开发、验证、发布证据纳入组织实践。SSDF 不定义 Android 方法身份、VMP 规则语言或御盾具体项目结论。
关键运行语义需要 Android 设备端 instrumentation 验证。Android instrumented tests 说明依赖真实 Android 运行时、组件和系统 API 的行为应在设备或模拟器执行。单一设备通过不能代表全部 API、ABI、厂商系统和生产使用路径。
规则文本未变化不能证明命中的业务语义未变化。工程判断:依赖、R8 和代码生成可改变 owner、descriptor、实现摘要与调用目标,而工具仍可能接受原规则。代码摘要和调用边只是语义线索,仍需源码评审与运行向量确认业务责任。
零命中和多命中都应默认阻断,而不是只记录 warning。工程判断:零命中意味着目标消失或规则失效,多命中意味着边界扩张或身份歧义,二者都无法证明最小保护范围。明确设计为多方法集合的规则必须逐项列出身份和期望数量,不能使用未展开通配。
升级前后方法索引必须来自最终 release 候选。工程判断:源码和依赖 JAR 尚未反映 R8、desugaring、生成代码、合并与删除,无法代表实际 DEX 方法集合。索引工具自身需要固定版本和完整性验证,静态观察仍不等于 VMP 实际执行。
本文没有提供任何项目命中率、性能、兼容、攻击阻断或客户结论。项目证据尚未接入:缺少目标依赖图、APK、mapping、规则展开回执与设备矩阵。文章只提供漂移分类、比较代码和可复核的发布门禁设计。

工程常见问题

为什么 VMP 工具退出码为零仍可能规则失效?

部分工具把未命中规则当作警告或静默忽略。必须导出逐规则展开结果并核对期望 owner、name、descriptor 与命中数量。

只比较 Gradle 版本目录能发现传递依赖变化吗?

不能。版本目录是声明输入,平台、约束、BOM 与冲突解析会改变最终选择;应保存目标 release configuration 的解析图。

方法名相同是否表示规则仍保护同一逻辑?

不表示。参数和返回类型、owner、访问标记、代码摘要与关键调用目标都可能变化,需要联合比较并做业务语义回归。

能否在升级后把整个 SDK 包纳入 VMP 避免漏保护?

不建议。包通配会纳入生成代码和平台胶水,扩大性能与兼容面,并掩盖具体业务责任是否仍存在。

R8 keep 规则能代替 VMP 漂移门禁吗?

不能。keep 负责可达性与优化边界,VMP 门禁还要确认保护对象的业务价值、精确身份、命中结果和运行语义。

发现 owner 搬迁后可以自动改规则并发布吗?

不可以。自动迁移只能提出候选,还需确认业务责任、descriptor、实现摘要和调用路径,再用最终候选和设备回归取证。

SDK Index 显示版本正常是否代表升级安全?

不代表。它只覆盖部分公开线索,仍需审计权限、组件、Native、数据处理、依赖图、最终候选与项目运行行为。

静态漂移检查通过后还要测什么?

重新执行 VMP 并核对最终方法,验签,再覆盖正常、拒绝、异常、反射、JNI 与系统组件路径,记录设备、API、ABI 和候选哈希。

想用自己的 App 验证?

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

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