先看结论与判断条件
- 先按业务资产和攻击收益选择手写核心逻辑,再沿依赖注入图确认生成装配层如何到达这些入口。
- 生成目录、编译任务、类来源和映射文件共同判断生成代码,不能只靠 Factory、Provider 等名称后缀。
- 生成工厂和成员注入器通常承担对象创建与连接,不应因为调用核心实现就自动继承同级 VMP 强度。
- 反射、JNI、框架发现和间接入口使用精确 keep 规则维持可达性,keep 与 VMP 清单必须分开管理。
- 不同 build variant 解析到的依赖、生成类和混淆结果可能不同,每个发布变体都需要独立产物清单。
- 设备回归应覆盖冷启动、作用域生命周期、延迟注入、异常传播和关键业务输入输出,并绑定同一候选。
保护目标是业务决策,不是生成代码数量
依赖注入会把对象创建、参数传递、作用域缓存和成员赋值展开成大量生成类。它们在 DEX 中占据可见体积,也常出现在关键业务调用链上,但这不等于每个生成方法都是高价值资产。VMP 范围应从被读取或修改后会造成直接损失的业务决策开始,例如授权、权益、风控、协议和核心算法,再确认这些实现通过哪些生成节点被创建和调用。
生成装配代码通常具有重复、结构化和可由源码重新产生的特点。高强度保护它们会扩大处理数量、构建差异和回归面,却未必显著增加攻击者理解核心算法的成本。工程判断是默认把生成层列为“保持兼容、验证连接”,把手写核心实现列为“评估 VMP”;只有生成方法本身承载定制校验、敏感常量或不可替代业务逻辑时,才作为例外进入保护。
边界不能靠包名一次圈选。生成代码可能与手写类位于同包,手写适配器也可能使用 Factory 或 Provider 命名。资产表至少记录源码所有者、源文件来源、生成任务、编译后类、调用入口、业务损失、执行频率和回滚条件。这样审阅者能解释某类为何保护、为何排除,而不是用“依赖注入相关代码全部保护”替代判断。
| 代码类型 | 主要职责 | 默认 VMP 决策 | 需要例外审查的信号 |
|---|---|---|---|
| 生成工厂 | 创建对象并传递依赖 | 通常排除 | 包含定制敏感分支 |
| 成员注入器 | 把依赖写入目标对象 | 通常排除 | 注入过程含业务校验 |
| 作用域缓存节点 | 维护实例生命周期 | 优先兼容检查 | 缓存决定直接影响权益 |
| 手写提供方法 | 构造或选择实现 | 逐方法评估 | 执行授权、路由或密钥决策 |
| 手写核心服务 | 完成业务算法和判断 | 优先候选 | 高频或启动路径需预算 |
| 框架发现入口 | 反射或间接定位组件 | 保持可达性 | 入口本身含敏感逻辑 |
用来源证据区分生成类和手写类
可靠分类应从构建产物向生成源回溯。编译流水线记录生成源目录、代码生成任务、源文件清单和 class 输出;R8 mapping 连接原始名称与最终名称;保护清单记录最终命中的方法。四类记录合并后,才能判断一个 DEX 类由哪个源产生、是否经过名称变化、最终是否被保护。只看最终类名后缀容易受混淆和项目命名影响。
生成来源也可能跨模块。某个库通过注解处理或其他编译插件产生代码,最终进入应用模块;传递依赖升级后,生成器版本和输出结构可能变化。Android Gradle dependency resolution 说明直接与传递依赖形成版本解析图,实际解析结果变化可能带来运行差异。项目应保存每个发布变体的解析图和生成器版本,不用仓库声明版本推断最终结果。
分类规则需要失败关闭但允许解释性例外。来源标记缺失、同一类同时被标为生成与手写、映射无法连接或生成任务未知时,不应自动归入排除清单。流水线把这些条目标为待审阅,要求负责人补来源证据。例外保护则记录业务原因、性能预算和回归入口,避免下一次生成器升级后继续沿用已经失效的类名规则。
| 证据 | 回答的问题 | 生成代码信号 | 异常动作 |
|---|---|---|---|
| 生成任务清单 | 哪个任务产生源文件 | 任务输出目录和生成器 | 未知任务待审阅 |
| 源文件索引 | 类来自手写还是生成路径 | 位于受控生成目录 | 路径冲突拒绝自动分类 |
| 依赖解析图 | 生成器和框架实际版本 | 解析到明确组件 | 版本漂移重新验收 |
| R8 mapping | 原始类如何映射到最终类 | 可连接生成源与 DEX | 映射缺失不做猜测 |
| 保护报告 | 哪些最终方法实际命中 | 生成类被意外纳入 | 与策略差异即阻断 |
| 资产责任表 | 哪些手写逻辑有业务价值 | 业务所有者和损失明确 | 无人负责不得标核心 |
依赖图要按发布变体分别固定
Android build variants 说明 build type、product flavor、source set、applicationId 和签名配置组合成不同变体。不同变体可能选择不同实现模块、测试替身、地区 SDK 或生成参数,使注入图、生成类和核心入口发生变化。一个变体的保护清单不能直接复制给另一个变体,即使两者共享大部分源码。
Debug Android dependency resolution 建议从目标变体对应的依赖树和实际解析结果定位重复类与版本冲突。这个方法同样适合保护边界排查:先确认出问题候选到底解析到哪个框架、生成器和传递库,再检查生成输出。构建冲突被修复只说明编译图恢复,不证明加固后的运行时连接和生命周期语义正确。
每个可发布变体应归档依赖锁定结果、生成任务、生成类摘要、手写资产清单、R8 配置、mapping 和 VMP 命中报告。流水线比较上一候选,新增生成类不自动进入保护,核心实现消失或移动则要求重新评审。工程判断是把变化分类为依赖变化、生成器变化、业务重构或保护规则漂移,避免所有差异都归咎于加固工具。
| 变化 | 可能影响 | 必须比较 | 不能直接推断 |
|---|---|---|---|
| build type 切换 | 替身实现和优化级别 | 实际类与签名配置 | release 与 debug 图相同 |
| product flavor 切换 | 地区或客户实现 | 核心资产和生成节点 | 一个 flavor 结果覆盖全部 |
| 依赖版本变化 | 生成器和运行时接口 | 解析图、生成源和回归 | 唯一根因就是依赖 |
| 生成参数变化 | 类数量与装配结构 | 任务参数和输出摘要 | 业务实现一定变化 |
| R8 配置变化 | 可达性、名称与优化 | mapping、usage 和 keep | VMP 范围自动正确 |
| VMP 规则变化 | 命中方法与运行表示 | 保护报告和候选摘要 | 构建成功等于兼容 |
R8 keep 规则与 VMP 清单承担不同职责
Enable app optimization with R8 说明 R8 会执行代码与资源缩减、优化和名称混淆。依赖注入生成代码若通过直接引用连接,R8 可以分析可达性;若框架通过反射、JNI、清单或其他间接方式发现入口,就需要保留必要名称、构造器、注解或成员。R8 keep rules best practices 强调精确规则,宽泛保留会掩盖边界错误并削弱优化。
keep 只回答 R8 是否保留或允许优化某个元素,不回答它是否应进入 VMP。把 keep 清单复制成 VMP 清单,会把框架入口、序列化模型和生成装配层全部当成核心资产;反过来,用 VMP 排除规则代替 keep,又可能让 R8 删除运行所需入口。两个文件应分别版本化,但通过相同资产标识、原始符号和最终 mapping 进行一致性检查。
精确 keep 规则需要真实证据。规则应说明哪个间接入口需要何种属性,测试用例覆盖该入口,usage 报告确认没有意外保留整个包。若为了修复崩溃临时添加通配 keep,应记录到期和缩小计划。工程判断是先恢复最小可达性,再评估手写核心方法的 VMP;不要用全包 keep 加全包保护让问题暂时消失。
| 配置 | 核心问题 | 主要证据 | 错误替代 |
|---|---|---|---|
| R8 keep | 间接入口是否保持可达或名称稳定 | 入口机制、usage 和回归 | 等同核心资产清单 |
| R8 optimization | 哪些代码被缩减、优化和混淆 | mapping、usage 和构建配置 | 等同 VMP 保护 |
| VMP include | 哪些高价值方法改变执行表示 | 资产价值、命中报告和验收 | 复制全包 keep |
| VMP exclude | 哪些敏感路径暂不处理 | 兼容、性能和故障边界 | 替代 R8 可达性 |
| mapping 归档 | 原始与最终符号如何连接 | 同摘要候选输出 | 另一个版本映射 |
| 设备回归 | 运行语义是否保持 | 同候选真实入口 | 只看构建成功 |
生成层通常排除,手写提供方法逐项评估
默认排除生成工厂并不意味着忽略依赖注入。保护后的手写构造器、提供方法和核心服务仍由生成层调用,参数顺序、异常传播、泛型桥接和生命周期都可能受影响。保护清单要沿调用图标注生成入口与手写目标,测试从真实注入入口触发核心逻辑,而不是直接 new 核心类绕过框架。
手写提供方法是常见边界。它可能只创建对象,也可能根据账号、地区、授权或风险选择实现。前者更接近装配层,后者可能是高价值业务决策。评审按输入、输出、分支价值、执行频率、线程、异常与可回滚性选择保护级别,不能仅因方法位于 Module 或 Provider 类就统一排除。
作用域和延迟获取也需要单独判断。单例缓存、会话作用域、延迟提供器和循环依赖可能让问题只在第二次调用、进程恢复或特定生命周期出现。工程判断是生成层保持低变化并用回归保护,核心实现按价值进入 VMP;若工具或运行时对某类生成结构存在已知边界,记录具体生成器版本和候选证据,不扩大成所有依赖注入代码都不能保护。
- 生成节点与手写目标在调用图中成对记录
- 手写提供方法按业务分支而非类名评估
- 测试从真实注入入口触发核心逻辑
- 作用域、延迟获取和异常路径分别覆盖
- 性能敏感构造和启动链先建立基线
- 例外规则绑定生成器版本与候选摘要
用来源清单识别生成类和保护漂移
自动门禁可以读取构建阶段导出的类来源清单,其中每项记录原始类名、sourceOrigin、generator、variant、最终映射名、业务资产标识和预期保护决策。实际保护报告再列出命中的最终类。验证器要求所有业务核心资产都有唯一手写来源并按策略命中,生成类默认不被高强度保护,除非带有可审阅例外原因。
下面 Python 示例只读取公开结构的 JSON,不扫描客户字节码,不包含真实包名,也不修改产物。它检查类标识唯一、来源类型有效、生成项带生成器、核心资产来自手写代码、实际保护集合不包含无例外生成类,并确认预期核心类全部命中。真实工程还要验证 mapping 和保护报告来自同一 APK 摘要。
名称后缀不参与最终决定。一个手写类叫 Factory 仍由 sourceOrigin=handwritten 识别,一个混淆后的生成类则依靠来源与 mapping 连接。门禁失败输出稳定资产标识和错误类别,不公开核心类全名。工程判断是把来源缺失当成阻塞,因为错误排除手写核心逻辑比多做一次人工分类风险更高。
- 来源清单由构建系统自动导出
- 生成器身份与发布变体同时记录
- mapping 和保护报告绑定同一候选
- 核心资产必须拥有唯一手写来源
- 生成类保护例外包含可审阅原因
- 未知保护命中和遗漏核心类都阻断发布
from pathlib import Path
import json
import sys
if len(sys.argv) != 2:
raise SystemExit("usage: check_di_boundary.py inventory.json")
source = Path(sys.argv[1])
if not source.is_file():
raise SystemExit("inventory file does not exist")
payload = json.loads(source.read_text(encoding="utf-8"))
classes = payload.get("classes")
protected = set(payload.get("protectedFinalClasses", []))
if not isinstance(classes, list) or not classes:
raise SystemExit("class source inventory is required")
allowed_origins = {"generated", "handwritten"}
seen = set()
failures = []
expected_protected = set()
for item in classes:
asset_id = item.get("assetId")
origin = item.get("sourceOrigin")
final_name = item.get("finalClass")
generator = item.get("generator")
decision = item.get("protectionDecision")
exception = item.get("exceptionReason")
if not asset_id or asset_id in seen:
failures.append(f"invalid or duplicate asset: {asset_id!r}")
continue
seen.add(asset_id)
if origin not in allowed_origins or not final_name:
failures.append(f"{asset_id}: source origin or final class is invalid")
continue
if origin == "generated" and not generator:
failures.append(f"{asset_id}: generated class has no generator identity")
if decision == "protect-core":
if origin != "handwritten":
failures.append(f"{asset_id}: core asset is not handwritten")
expected_protected.add(final_name)
if origin == "generated" and final_name in protected and not exception:
failures.append(f"{asset_id}: generated class protected without exception")
missing = sorted(expected_protected - protected)
unknown = sorted(protected - {item.get("finalClass") for item in classes})
for final_name in missing:
failures.append(f"expected core class was not protected: {final_name}")
for final_name in unknown:
failures.append(f"protected class is absent from inventory: {final_name}")
if failures:
print("dependency injection boundary gate failed:")
for failure in failures:
print(f"- {failure}")
raise SystemExit(2)
print(f"validated {len(seen)} class source records")设备回归覆盖生命周期而不只覆盖对象创建
Android instrumented tests 适合验证依赖真实 Android 运行时、组件和系统 API 的语义。依赖注入回归不能只断言容器能够创建对象,应从 Activity、Service、Worker、深链或其他真实入口触发,验证作用域实例是否复用、进程恢复后是否重建、延迟依赖何时初始化、异常是否按原契约传播,以及核心业务输入输出是否与基线一致。
启动路径尤其敏感。生成组件初始化、ContentProvider、Application 和首屏依赖可能在监控完成前执行;高强度处理手写构造器或提供方法后,异常和时延会被放大。测试要固定未保护基线与同一候选设备条件,分别观察冷启动、首次注入、重复获取和后台恢复。本文不给出通用性能数字,项目需依据真实预算记录分布。
单一设备通过不能代表完整 API、ABI 和厂商矩阵。发布证据应列出实际覆盖组合、未执行项和失败项。若某变体使用不同依赖或生成器,必须在对应变体候选上重复关键回归,不能用另一个 flavor 的成功结果证明当前包。
| 场景 | 关键断言 | 可能暴露的问题 | 证据要求 |
|---|---|---|---|
| 冷启动 | 组件与首屏依赖正常建立 | 初始化顺序和启动异常 | 同设备基线与候选 |
| 作用域复用 | 同作用域实例身份符合预期 | 缓存或生命周期变化 | 多次真实入口调用 |
| 进程恢复 | 状态与依赖安全重建 | 静态缓存和缺失绑定 | 杀进程后设备回归 |
| 延迟注入 | 首次使用时正确创建 | 受保护构造和线程问题 | 延迟前后状态记录 |
| 异常路径 | 错误类型与业务回退一致 | 异常包装或传播变化 | 输入、异常和结果 |
| 变体差异 | 各发布变体核心入口可达 | 依赖解析和生成图漂移 | 每个候选独立回执 |
发布证据把依赖、映射、保护和回归连起来
可复核证据包至少包含候选 APK 摘要、发布变体、依赖解析图、生成器版本、生成源清单、R8 配置、mapping、usage、VMP 策略、实际命中报告和设备回归。每项记录都指向同一构建身份,不能拿未混淆类清单配另一个版本 mapping,也不能用 debug 依赖图解释 release 候选。
出现问题时按最早差异定位。构建阶段重复类先查变体解析图;R8 后类缺失先查 keep 与 usage;保护后对象创建失败再查命中方法、构造语义和生成入口;设备特定问题则检查 API、ABI 和生命周期。一次只改变一个可解释变量并生成新候选,避免同时扩大 keep、排除整个包和替换依赖,让根因无法归属。
最终结论应写成指定候选中列出的手写核心资产进入保护,生成装配层按策略排除,并在列出的设备路径完成回归;不能声称所有依赖注入框架永久兼容或未测试生成器也通过。准备评估时,可通过御盾中央平台提交代表性 APK、变体依赖图、生成源清单、mapping 和保护范围,先固定资产与验收边界。
- 所有构建和回归记录绑定同一 APK 摘要
- 发布变体与依赖解析图一一对应
- 生成源、mapping 与最终保护命中可连接
- keep 规则与 VMP 策略分别归档
- 未覆盖设备和生成器版本明确标记
- 结论区分事实、工程判断与项目待验证项
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 直接和传递依赖会形成版本解析图,解析结果变化可能引发运行差异。 | Android Gradle dependency resolution 描述依赖声明、传递关系和版本解析。 | 依赖树只能缩小排查范围,不能证明某个 SDK 或生成器是唯一根因。 |
| 重复类和版本冲突应从目标变体的实际依赖树与解析结果定位。 | Debug Android dependency resolution 描述从依赖报告排查解析错误的方法。 | 构建冲突修复不等于加固后运行时装配和生命周期问题已经消失。 |
| R8 可以执行代码与资源缩减、优化和名称混淆。 | Enable app optimization with R8 描述 R8 在发布构建中的相关职责和输出。 | R8 优化不等于 VMP,也不证明抗动态分析或依赖注入运行兼容。 |
| 反射、JNI 和间接入口需要精确 keep 规则,宽泛规则会削弱优化并掩盖边界。 | R8 keep rules best practices 描述 keep 规则的精确性和常见间接入口。 | keep 规则只处理 R8 可达性与优化约束,不定义 VMP 保护范围。 |
| build type、product flavor、source set 等会组合成行为不同的发布变体。 | Android build variants 描述变体组合、source set、applicationId 和签名配置。 | 同一仓库不代表所有变体具有相同依赖、生成代码、证书和运行行为。 |
| 依赖真实 Android 运行时和组件生命周期的语义适合通过设备端测试验证。 | Android instrumented tests 描述在设备或模拟器上访问 Android 框架能力的测试。 | 单一设备通过不能代表完整 API、ABI、厂商与发布变体矩阵。 |
| 生成装配层应默认与手写核心业务资产分开评估 VMP。 | 工程判断:对象装配与业务决策的攻击收益、执行频率和故障半径不同。 | 若生成方法包含定制敏感逻辑,仍需项目证据支持例外保护。 |
| 代码来源、mapping、保护报告和设备回归应绑定同一候选摘要。 | 工程判断:只有同候选证据才能确认生成类排除与核心方法命中没有发生漂移。 | 静态连接通过后仍不能替代真实注入入口、生命周期和业务输入输出测试。 |
工程常见问题
依赖注入生成的 Factory 类是否都不需要 VMP?
不能绝对排除。默认按装配层处理,但若生成方法包含定制敏感分支或生成器改变逻辑,需要例外评审,并绑定性能和设备回归。
为什么不能按类名后缀识别生成代码?
手写类可能使用相同后缀,混淆后生成类名称也会变化。应使用生成任务、源路径、生成器身份和 mapping 连接到最终类。
R8 keep 规则能否直接复制成 VMP 保护清单?
不能。keep 负责可达性和优化约束,VMP 负责高价值方法的执行表示。复制会把大量框架和生成入口误当成核心资产。
只保护生成工厂调用的核心服务是否足够?
还要确认授权、路由或算法是否分散在手写提供方法和适配器中,并从真实注入入口执行设备回归,不能只看一个服务类。
一个 release 变体通过能否代表其他 flavor?
不能。flavor 可能解析不同依赖、生成器、实现和 R8 结果。每个发布候选都要保存自己的依赖图、mapping、保护命中和回归。
申请依赖注入代码 VMP 边界评估需要准备什么?
准备代表性 APK、发布变体、依赖解析图、生成源清单、R8 mapping、keep 规则和保护范围,再通过御盾中央平台提交申请。