先看结论与判断条件

  • 重载方法的唯一身份由类、方法名和参数返回描述符共同组成,短方法名不能区分实际目标。
  • 源码签名、R8 mapping、最终方法清单和 VMP 规则必须来自同一候选构建,不能跨版本拼接。
  • keep 规则控制 R8 可达性与名称约束,VMP 规则控制保护范围,两者共享身份但不能互相替代。
  • 反射与间接入口需要精确保留和运行回归,扩大类级规则会掩盖身份绑定缺陷。
  • 门禁必须对每条规则执行零命中、唯一命中和多命中检查,并把最终命中方法写入不可变回执。
  • mapping 和 retrace 能帮助解释混淆名称关系,但不能修复错候选、错配置或未验证的保护结论。

先给结论:重载匹配必须精确到 descriptor

同一个类可以存在多个同名方法,参数类型、参数顺序或返回类型不同。VMP 规则若只写类名和方法名,可能同时命中多个重载,也可能在 R8 优化后命中完全不同的最终方法。正确的最小身份是 owner、name 和 JVM descriptor,规则生成器必须把三者作为不可拆分键,并记录它来自哪个源码资产。

名称混淆增加的是追踪步骤,不改变唯一身份原则。团队先在源码或编译后未混淆清单中登记目标重载,再使用同一发布构建生成的 R8 mapping 将它绑定到最终 owner、name 和 descriptor。最后从实际进入 VMP 的候选产物枚举方法,证明规则恰好命中一个对象。任何阶段缺失都应保持未确认。

门禁结果分为 unique、missing、ambiguous 和 forbidden-broad。unique 只说明规则精确命中,仍需语义回归;missing 表示目标被删除、内联、改形或清单错配;ambiguous 表示规则命中多个方法;forbidden-broad 表示使用类级或名称通配掩盖歧义。后三类不能靠增加更多通配符自动修复。

重载方法规则的四种门禁结果
结果含义常见原因发布动作
unique只命中一个最终方法身份完整且候选一致进入语义回归
missing未命中最终方法删除、内联或错 mapping阻止并追踪
ambiguous命中多个重载缺少 descriptor收窄规则
forbidden-broad使用类级通配为省事扩大范围拒绝自动放行
stale输入属于旧构建跨版本复用 mapping重建全部身份
verified唯一命中且回归绑定候选证据完整限定范围登记

JVM descriptor 才能区分同名重载

Java 和 Kotlin 源码通常以可读类型展示参数,类文件则使用 JVM descriptor 表达参数与返回类型。两个方法即使都叫 verify,只要一个接收字符串、另一个接收字节数组,就拥有不同 descriptor。规则系统若丢掉 descriptor,只能得到名称集合,无法证明选中了业务所有者指定的那个重载。

数组、基本类型、对象类型和多参数顺序都会进入 descriptor。装箱类型与基本类型不是同一身份,可空性通常不会单独改变 JVM descriptor,泛型信息也可能在擦除后收敛到更一般的类型。审查需要面对最终类文件,而不能从源码显示文本猜测。若多个源码声明擦除后发生桥接或生成入口,还要记录 synthetic 与 bridge 关系。

返回类型也应保留在完整 descriptor 中,即使某些规则语法主要靠参数区分。原因不是追求格式复杂,而是让身份记录能与类文件、mapping、调用图和验证工具直接比较。任何转换器若自行省略返回类型,都应有明确规范和往返测试,避免不同工具对同一规则产生不同解释。

方法身份字段及其责任
字段示例语义缺失风险门禁要求
owner方法所属最终类同名类或迁移错配完整内部类名
name最终方法名无法定位入口不接受空值
descriptor参数与返回类型重载歧义必须精确匹配
sourceIdentity源码资产归属命中但不知保护谁绑定业务清单
mappingDigest本构建名称关系跨版本误用不可变摘要
candidateDigest最终输入产物报告与包错配发布身份绑定

重载、桥接与生成方法会叠加歧义

普通重载已经要求 descriptor,泛型擦除和协变返回还可能让编译器增加 bridge 方法。Kotlin 默认参数、扩展函数、suspend 函数和属性访问器也会产生与源码展示不同的类文件形态。规则评审必须区分手写业务实现、稳定调用入口和编译器生成适配层,不能把同名方法数量多理解为都属于同一资产。

目标资产可能经过多个入口到达同一实现,也可能一个源码入口在优化后分散到调用者。只保护短方法名会把旁路重载一起纳入,增加兼容与回归变量;只保护反编译器看起来最复杂的方法,又可能遗漏框架真实调用入口。正确记录允许一项资产关联多个最终方法,同时要求每个关联都有入口责任和排除理由。

规则生成前应先建立候选方法清单,标注 access flags、synthetic、bridge、调用者和源码线索。清单只是静态证据,不能证明运行时必经某个入口。对反射、序列化、JNI 或框架回调,还要从真实业务路径发起测试,确认 keep 规则与保护规则共同保持可发现性和语义。

R8 mapping 必须与候选构建一一对应

R8 的职责包括缩减、优化和名称混淆。源码方法可能被改名、删除、内联或改变最终调用布局,因此旧构建 mapping 不能用于新候选。流水线要保存 mapping 文件摘要、R8 配置摘要、工具版本、构建变体和候选摘要,并在生成 VMP 规则前确认这些输入来自同一次构建。

mapping 是名称关系证据,不是保护命中证明。它可以帮助把源码 owner 与方法名映射到混淆身份,也可以和 retrace 一起解释堆栈,但最终规则仍需对实际字节码方法枚举验证。若 mapping 中存在源方法而候选清单没有最终方法,门禁应调查优化结果,而不是凭 mapping 行直接登记已命中。

同一源码签名在不同变体中可能映射到不同最终身份,依赖和功能开关也会改变可达性。规则缓存键必须包含变体与 input hash,不能只按 slug、类名或日期复用。候选发生重建后,即使源码没有变化,也要重新核对 mapping 与方法清单,防止非确定依赖或工具变化造成静默漂移。

从源码资产到最终命中的证据链
阶段输入输出失败时不能做什么
资产登记源码签名与业务理由目标身份按包名猜测
R8 构建代码与优化配置mapping 和候选跨构建混用
身份转换目标与 mapping最终精确键丢弃 descriptor
规则生成最终键与语法VMP 精确规则扩大类级范围
命中验证规则与最终清单唯一命中回执只看命令成功
语义回归真实入口与候选结果和异常证据宣称普遍兼容

keep 规则与 VMP 规则共享身份但责任不同

R8 keep 规则用于控制可达性、优化和名称约束,VMP 规则用于选择保护对象。一个反射调用的方法可能需要精确 keep 才能保留,同时也可能因业务价值进入 VMP;另一个方法可能必须 keep 以维持框架契约,却不值得纳入高成本保护。把两类规则合并成一个宽泛类级清单,会丢失责任和失败归因。

Android 官方 keep 规则最佳实践强调间接入口需要精确规则,过宽规则会削弱优化并掩盖配置缺陷。这个原则可以支持身份清单设计,但不能直接定义 VMP 范围。团队应从同一方法键派生两份规则,并分别记录 keepReason 与 protectionReason。某一理由为空时,不得自动继承另一份规则。

Kotlin 反射依赖运行时可发现的类、成员和元数据,名称或签名变化可能影响查找。对反射路径,回归需要使用真实查找方式和参数类型,不应为了测试方便直接调用最终混淆方法。若反射失败,先区分 keep、mapping、描述符、元数据和保护变换,不能立即扩大所有重载的保护或保留范围。

规则门禁必须失败关闭并保留原因

零命中不是成功。它可能表示目标被 R8 删除或内联,也可能说明 mapping、descriptor、构建变体或候选文件错配。流水线必须返回 missing 并阻止发布,随后由资产所有者确认逻辑迁移到了哪里。悄悄忽略零命中会让配置文件存在却没有保护对象,形成最危险的假通过。

多命中同样不能通过。若一个规则匹配两个重载,系统不能自行选择第一个,也不能把两个都加入以求保险。报告应列出所有候选的完整身份、flags 和调用者,要求修复 descriptor 或拆分资产映射。扩大保护面会增加运行变量,同时仍无法证明真正目标是其中哪个。

通配规则只在明确定义、数量固定且所有方法责任相同的极少场景下讨论,但本意图默认拒绝类级和名称级通配。每个例外需要规则所有者、业务资产、预期命中数量和回归集合。实际数量变化时立刻失败,不能把新增依赖方法自动纳入旧结论。

精确规则的失败关闭策略
检查通过条件失败输出修复边界
输入绑定摘要全部一致stale-input重建身份链
descriptor语法完整可解析invalid-descriptor只修签名字段
最终匹配恰好一个方法missing 或 ambiguous追踪或收窄
通配检测没有禁用通配forbidden-broad生成精确规则
资产关联存在保护理由unowned-target补资产评审
回归绑定入口和候选一致unverified-semantics执行目标用例

用结构化代码检查 mapping、descriptor 与规则命中

自动检查器应使用结构化输入,而不是依赖反编译界面截图。source_targets 保存源码 owner、name、descriptor 和资产标识;mapping 保存源码键到最终键的关系;final_methods 来自候选字节码枚举;rules 保存准备交给 VMP 的精确键。所有输入都需要候选摘要与 mapping 摘要,在外层由流水线验证一致。

下面的 Python 示例读取一个脱敏 JSON 报告,先验证 JVM descriptor 基本格式和禁止通配字符,再把每个源码目标映射到最终身份。它要求映射唯一、最终方法存在且规则恰好命中一次。任何 missing、ambiguous、stale 或 broad 都通过可达失败路径退出,不会修改类文件、APK 或保护配置。

脚本输出 targetBindings 和 ruleMatches,供审核人核对资产、源码签名和最终方法。它没有分析实际调用图,也不判断 VMP 能力、兼容或防护强度。通过只意味着输入报告内部一致;正式发布仍需真实工具命中回执、入口测试、异常回归和最终候选绑定。

重载方法精确身份与规则命中检查器
from pathlib import Path
import json
import re
import sys

if len(sys.argv) != 2:
    raise SystemExit("usage: verify_vmp_method_rules.py report.json")

report_path = Path(sys.argv[1]).resolve()
if not report_path.is_file():
    raise SystemExit(f"rule report missing: {report_path.name}")

report = json.loads(report_path.read_text(encoding="utf-8"))
descriptor_pattern = re.compile(r"^\([^)]*\).+$")
for field in ("source_targets", "mapping", "final_methods", "rules"):
    if not isinstance(report.get(field), list) or not report[field]:
        raise SystemExit(f"{field} must be a non-empty array")

def identity(record, label):
    if not isinstance(record, dict):
        raise SystemExit(f"{label} entry is not an object")
    owner = record.get("owner")
    name = record.get("name")
    descriptor = record.get("descriptor")
    if not all(isinstance(value, str) and value for value in (owner, name, descriptor)):
        raise SystemExit(f"{label} entry lacks owner, name, or descriptor")
    if any(marker in owner or marker in name or marker in descriptor for marker in ("*", "?")):
        raise SystemExit(f"{label} entry uses a forbidden wildcard")
    if not descriptor_pattern.fullmatch(descriptor):
        raise SystemExit(f"{label} entry has an invalid JVM descriptor")
    return owner, name, descriptor

final_methods = {identity(item, "final_methods") for item in report["final_methods"]}
rules = [identity(item, "rules") for item in report["rules"]]
if len(set(rules)) != len(rules):
    raise SystemExit("rules contains duplicate method identities")

mapping_by_source = {}
for entry in report["mapping"]:
    if not isinstance(entry, dict):
        raise SystemExit("mapping entry is not an object")
    source = identity(entry.get("source"), "mapping source")
    target = identity(entry.get("target"), "mapping target")
    mapping_by_source.setdefault(source, []).append(target)

bindings = []
for target in report["source_targets"]:
    source = identity(target, "source_targets")
    asset = target.get("asset")
    if not isinstance(asset, str) or not asset:
        raise SystemExit("source target lacks a business asset identifier")
    mapped = mapping_by_source.get(source, [])
    if len(mapped) == 0:
        raise SystemExit(f"source target is missing from mapping: {source}")
    if len(mapped) > 1:
        raise SystemExit(f"source target has ambiguous mappings: {source}")
    final_identity = mapped[0]
    if final_identity not in final_methods:
        raise SystemExit(f"mapped method is absent from final inventory: {final_identity}")
    match_count = sum(1 for rule in rules if rule == final_identity)
    if match_count != 1:
        raise SystemExit(f"target must have exactly one VMP rule: {final_identity}")
    bindings.append({"asset": asset, "source": source, "final": final_identity, "rule_matches": match_count})

unused_rules = [rule for rule in rules if rule not in {tuple(item["final"]) for item in bindings}]
if unused_rules:
    raise SystemExit(f"rules contains unowned final methods: {unused_rules}")

print(json.dumps({"status": "unique-method-bindings", "bindings": bindings}, ensure_ascii=False, indent=2))

命中回执要与 retrace 和语义回归配对

retrace 用同一构建的 mapping 还原混淆 Java 或 Kotlin 堆栈,能帮助定位异常对应的源码位置。它不能处理 Native 符号,也不能修复错误候选或错误规则。发布证据应把命中回执和 mapping 保存,发生崩溃时再用候选身份选择正确 mapping,避免旧映射生成看似可读但实际错误的诊断。

语义回归必须从真实入口调用目标重载,覆盖正常输入、边界类型、空值约定、异常、并发和反射路径。测试断言关注输出、副作用和异常类型,而不是只确认方法被调用。若规则只保护一个重载,还要验证相邻重载仍按预期工作,防止描述符转换或桥接关系产生意外影响。

命中唯一和回归通过仍不是绝对防护结论。OWASP MASVS 将抗篡改与抗逆向放在纵深防御范围,不能替代服务端授权和完整发布链。VMP 规则可以提高特定客户端方法的分析与修改成本,但项目结论必须绑定真实候选和验证范围,不能根据配置数量宣称攻击必然失败。

发布证据必须让下一次构建可以重算

NIST SSDF 强调来源、构建、验证和变更证据。对应重载规则,证据包至少包含业务资产清单、源码签名、mapping 摘要、最终方法清单、精确规则、命中结果、工具版本、候选摘要和语义回归。每次构建都应从输入重新生成,而不是人工复制上一版本规则。

报告要区分事实、工程判断和限制。可以登记某条精确键在候选中唯一命中、某个入口测试通过、某份 mapping 与候选摘要一致;不能写成所有重载均安全、没有绕过路径、性能无影响或产品已阻断攻击。没有真实项目数据时不提供客户、排名、流量或量化防护结论。

准备实际规则评审时,可先查看[App VMP 保护范围选择指南](/zh-cn/vmp-protection-selection/),整理源码资产、完整 descriptor、R8 mapping、最终方法索引和回归入口。需要提交候选,可从[御盾中央平台申请加固评估](https://www.leonadev.com/console/)。材料应脱敏,登录、价格、购买和控制台操作统一由中央平台承接。

  • 所有重载目标使用 owner、name 和完整 descriptor
  • 源码目标、mapping、最终清单和候选来自同一构建
  • keepReason 与 protectionReason 分开记录
  • 每条规则都通过零命中、唯一命中和多命中检查
  • 类级和名称级通配默认被门禁拒绝
  • 真实入口、反射、异常和相邻重载均进入回归
  • 命中、兼容和防护结论保持不同证据边界

事实依据与适用边界

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

本文判断事实或工程依据适用限制
R8 负责代码缩减、优化和名称混淆,源码身份不一定等于最终候选方法身份。Enable app optimization with R8 说明发布构建中的 R8 优化职责。R8 不等于 VMP,文档也不证明具体规则已经命中或达到防护强度。
反射、JNI 和间接入口需要精确 keep 规则,过宽规则会掩盖可达性边界。R8 keep rules best practices 说明间接调用场景的精确保留规则原则。keep 规则只约束 R8 行为,不能替代 VMP 选择和候选级语义回归。
混淆后的 Java 或 Kotlin 堆栈需要与同一构建的 mapping 配对还原。R8 retrace 说明使用 mapping 还原混淆堆栈的方法。retrace 不处理 Native 符号,也不能修复错候选、错 mapping 或错保护规则。
Kotlin 反射依赖运行时可发现的类、成员和元数据,签名变化可能影响查找。Kotlin reflection 说明运行时访问类与成员的反射能力。该文档不覆盖所有框架生成代码,也不证明特定反射路径已通过回归。
移动端抗篡改和抗逆向属于纵深防御控制。OWASP MASVS-RESILIENCE 给出移动端韧性控制类别。控制目录不证明某个候选包达到具体强度,也不能替代服务端授权。
安全发布应保存来源、构建、验证与变更证据,并纳入供应链风险。NIST SP 800-218 SSDF 给出组织级安全软件开发与发布实践。SSDF 不定义 VMP 规则语法,也不证明某个候选已经通过。
重载方法规则必须包含完整 JVM descriptor,并在最终候选中唯一命中。工程判断:短方法名无法区分同类重载,唯一键能支持自动失败关闭。唯一命中只证明身份绑定,不证明运行兼容、业务正确或防护效果。
mapping 必须与候选构建一一对应,不能跨版本复用。工程判断:优化、混淆、变体和依赖会改变最终方法身份与可达性。摘要一致仍需验证转换器、规则语法和实际工具命中。
发布证据应绑定源码资产、mapping、最终清单、规则、命中回执和测试。项目证据尚未接入:本文列出必须保存的字段,不声称当前候选已完成验收。配置存在、脚本通过或堆栈可读不能单独证明保护完成。
本文不提供客户、性能、排名或攻击阻断结论。项目证据尚未接入:没有真实候选和运行回执时不扩张事实。公开代码只用于身份与规则诊断,不能替代具体产品和候选验证。

工程常见问题

只写类名和方法名为什么不能匹配重载?

同一类可有多个同名方法,只有参数和返回 descriptor 才能形成完整身份。短名称规则可能零命中或同时命中多个对象。

把同名重载全部加入 VMP 是否更保险?

不保险。它扩大兼容和回归变量,仍不能证明真正业务目标是哪一个。应精确绑定资产和最终 descriptor。

有 R8 mapping 是否就能证明 VMP 规则命中?

不能。mapping 提供名称关系,还要在实际输入候选中枚举最终方法,并用规则执行唯一命中检查。

keep 规则已经精确,VMP 规则可以复用吗?

可以共享方法身份,但不能直接复用责任。keep 控制 R8 行为,VMP 控制保护范围,两者需要各自理由和验证。

规则零命中时可以忽略并继续发布吗?

不可以。零命中可能表示目标被删除、内联或输入错配,必须追踪资产最终位置,不能把配置存在当成保护完成。

retrace 能修复用错 mapping 的诊断吗?

不能。retrace 依赖与候选匹配的 mapping,输入错误可能生成误导结果。先用候选摘要选择正确映射。

反射调用的重载怎样做回归?

使用真实反射名称、参数类型和元数据从业务入口调用,覆盖成功、缺失、类型错误和异常,而不是直接调用混淆方法。

规则检查脚本通过是否等于保护已经完成?

不等于。脚本只证明报告内部的身份和唯一命中一致,仍需真实工具回执、候选绑定和语义回归。

想用自己的 App 验证?

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

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