先看结论与判断条件

  • 多 DEX 构建会将代码分散至多个文件,仅检查主 DEX 必然导致次级 DEX 中的核心逻辑遗漏。
  • R8 keep 规则仅定义混淆器可达性,不能直接作为 VMP 保护引擎的实际覆盖范围依据。
  • 业务入口清单必须细化至方法级别,并明确标注每个类在构建产物中预期的 DEX 归属。
  • 自动核对机制需在每次构建后比对真实 DEX 内容与保护清单,发现差异即阻断发布流程。
  • AAB 分发场景下必须使用 bundletool 生成目标设备 APK 集,以验证动态模块的 DEX 覆盖。
  • 工程判断:混淆后的清单可借助与该构建匹配的 mapping 转换名称再比对 DEX;该方法需用样本类验证,不能证明全部合成类都能正确映射。

多 DEX 架构下的保护盲区成因分析

当 Android 应用方法数超过六万五千五百三十六限制时,构建系统会自动启用 multidex 模式。该模式将字节码拆分为主 DEX 和多个次级 DEX 文件。主 DEX 通常仅包含应用启动必需的类和库支持代码,而大量业务逻辑如支付签名、数据加密往往被推入次级 DEX。若保护方案仅针对主 DEX 配置,这些核心资产将完全暴露。

构建变体对 DEX 分布有直接影响。Debug 与 Release 构建、不同的最低 SDK 版本设置以及自定义的 multidex keep 规则,都会改变类在 DEX 间的分配策略。同一个业务类在不同构建配置下可能位于不同的 DEX 文件中。这种不确定性使得静态的保护清单极易失效,导致开发者误以为代码已受保护。

缺乏物理 DEX 文件与业务入口的可追溯映射是根本问题。许多团队仅维护一份需要保护的类名列表,却未记录这些类预期出现在哪个具体的 DEX 文件中。一旦 R8 优化或依赖变化改变了类的分布,这份列表便与真实情况脱节。最终结果是核心算法以原始字节码形式存在于 APK 内,面临直接的逆向风险。

旧版 Dalvik 运行时的加载机制加剧了这一问题。在低版本系统中,二次 DEX 的加载时机和路径与 ART 环境存在差异。如果保护引擎未能适配这种加载行为,或者仅在应用启动初期扫描内存,那么延迟加载的次级 DEX 中的代码将在一段时间内处于无保护状态,给动态分析留下窗口。

不同构建配置下 DEX 分布差异与漏保护风险
构建配置类型主 DEX 典型内容次 DEX 潜在风险类漏保护后果
minSdkVersion 低于二十一入口 Activity 及 multidex 支持库后台加密工具与网络签名类核心算法明文暴露,易被提取
minSdkVersion 高于二十严格限定的启动必需类绝大部分业务逻辑与方法引用支付处理器与数据转换类 unprotected
自定义 multiDexKeepProGuard强制保留在主 DEX 的特定类未被规则覆盖的登录校验类凭证校验逻辑被绕过或窃取
不同 Product Flavor 注入风味特有的基础库类集合特定风味独有的加解密适配器部分渠道包核心功能完全裸奔
  • 检查构建配置是否启用了 multidex 模式
  • 确认主 DEX 与次 DEX 的文件数量及大小分布
  • 审查 multidex keep 规则是否意外排除了关键业务类
  • 验证不同 Flavor 构建产物的 DEX 结构一致性

构建业务入口清单与 DEX 索引映射

业务入口清单的建立不能依赖手工记忆,必须结合 AndroidManifest 注册表、反射调用分析以及 JNI native 方法声明进行系统性搜集。首先从 Manifest 中提取所有导出的组件类名,包括 Activity、Service、Receiver 和 Provider。随后利用静态扫描工具定位 Class.forName 等反射调用点,确保间接入口也被纳入清单。

清单粒度必须细化到方法级别。同一个类中的不同方法可能因继承关系或代码优化被分散在不同 DEX 中。例如父类在主 DEX 而子类在次 DEX,若只保护类名而不指定方法,可能导致子类中的敏感方法未被覆盖。因此,清单条目应明确标识类全限定名及其关键方法签名。

清单需具备随版本更新的维护方式。每个条目应关联所属业务模块和风险等级,并注明加入原因。例如支付签名方法属于高风险,必须在任何构建中受保护;而自动引入的日志工具类则不应占用保护资源。这种区分能减少无关干扰,提高验证效率。

对于采用 AAB 分发的应用,清单还需覆盖动态模块。按需模块的 DEX 在初次安装时可能尚未出现,因此只检查基包会漏掉后续交付的代码。应按目标 device spec 取得完整 APK 集,并将所有可能安装的 feature 模块核心类纳入核对;是否已受保护以最终模块产物和保护报告为准。

业务入口清单条目示例结构
类全限定名关键方法签名所属业务模块纳入保护原因
com.example.pay.PaySignersign(String data)支付交易模块持有私钥签名逻辑,泄露可伪造交易
com.example.crypto.KeyWrapperwrap(byte[] key)密钥管理模块JNI 调用前打包密钥,防内存转储
com.example.login.TokenProvidergetToken(Context ctx)用户认证模块返回 OAuth 刷新令牌,防凭证窃取
com.example.data.CipherHelperencrypt(byte[] src, int mode)本地数据存储数据库加密核心,防持久化数据泄露
  • 从 AndroidManifest 提取所有四大组件类名
  • 静态扫描代码库识别反射与 JNI 调用目标
  • 为每个入口类标注风险等级与业务归属
  • 确认动态模块中的核心类已加入总清单

从构建产物提取 DEX 类归属信息

鉴别类归属的第一步是提取 APK 中所有 DEX 文件的完整类列表。可使用 Android SDK 自带的 dexdump 工具,对每个 classes.dex 执行解析命令。输出结果包含类描述符,需将其转换为点分隔的全限定名。合并所有 DEX 的类列表并去重,即可得到 APK 级别的全类集合。

比全类集合更关键的是确定类位于哪个具体 DEX。多 DEX 应用中,保护引擎通常需外挂到每个 DEX 进行处理。若核心类被分到了 classes2.dex 而保护指令仅应用于 classes.dex,该类将以未保护形式存在。因此,必须为每个入口类打上实际所处的 DEX 文件名标签,生成映射表。

在 AAB 场景下,发布到设备的 APK 可能包含不同的 DEX 分割方式。标准做法是使用 bundletool 生成特定设备配置的 APK set。遍历 set 中所有 APK 文件,捕获各 DEX 中的类。由于 bundletool 在不提供签名时生成调试包,此步骤仅作为预发布检查,不能直接用作线上合规证明。

将提取的类集合与业务入口清单对比,可找出哪些入口类未出现在任何 DEX 中。这种情况通常表示代码已被 R8 完全移除或类名记录有误。若类存在于 DEX 但不在清单中,则说明清单更新滞后。这两种情况都需人工介入分析,以确定是正常优化还是配置错误。

DEX 分析工具与用途对比
工具名称主要功能描述典型输出内容适用验证环节
dexdump解析 DEX 文件结构输出类方法信息Class descriptor : Lcom/app/Secret;提取单个 DEX 类列表用于比对
bundletool从 AAB 生成特定配置 APK 集合base-arm64_v8a.apk 等文件集获取真实设备安装的完整 DEX
aapt2 dump badging解析 APK manifest 和资源元数据package name 与 versionCode 信息辅助建立自动化验证的版本标签
unzip -l列出 ZIP 归档内全部条目路径classes2.dex 文件大小与时间戳快速确认 APK 内 DEX 数量与名称
  • 配置 CI 环境 Android SDK 路径确保 dexdump 可用
  • 编写脚本遍历 APK 内所有 classes*.dex 文件
  • 解析 dexdump 输出构建类名到 DEX 文件的映射
  • 对 AAB 项目先生成 APK set 再执行提取操作

实施自动核对机制检测清单漂移

自动核对机制是一个嵌入 CI 流水线的检查步骤,在 APK 构建完成后立即触发。它提取产物内的全部 DEX,将其包含的类与预定义的保护清单比对。核心逻辑是以构建产物为事实依据,以保护清单为期望状态。任何出现在清单中却在产物 DEX 里找不到的类,均视为违例。

该步骤必须在签名和分发前执行,以防止未受保护的包流入生产环境。实现上,脚本接收构建完成的 APK 路径和保护清单文件作为参数。脚本负责解压 APK、遍历每个 DEX、导出类列表、与清单求差集。如果差集非空,脚本以非零状态码退出并向 CI 平台输出缺失明细。

机制本身不判断类丢失是否合法,只报告事实差异。若类被 R8 正确内联而消失但仍留在清单里,机制会报错。此时需人工审核后从清单删除。反之,若类仍在 DEX 中但未被保护引擎纳入,机制需结合保护引擎输出的已保护类列表进行交叉验证,才能发现此类漏保。

收到 CI 告警后,先确认目标类是被 R8 删除、改名,还是移动到其他 DEX 或动态模块。根据实际原因更新保护清单或 keep 规则,再生成新的 release 产物复查。告警没有对应处置记录时,不能沿用上一次构建的覆盖结论。

  • 在构建任务末尾增加自动核对脚本执行步骤
  • 传递产物路径与保护清单路径给脚本参数
  • 对多模块项目汇总各子清单进行统一校验
  • 设置脚本退出码区分缺失类与环境错误
  • 在输出中清晰列出缺失类名与预期位置
  • 定期审查日志避免因忽略警告而失效

利用 Mapping 文件应对混淆名称变化

发布版构建中 R8 会对类名进行混淆,保护清单中的原始名称无法直接在 DEX 中匹配。此时必须利用 R8 生成的 mapping.txt 文件,将清单里的原始类名转换为混淆后名称。mapping.txt 格式记录了原始名到混淆名的映射关系,脚本需解析此文件构建转换字典。

需注意部分类可能因 keep 规则而未混淆,它们在 mapping.txt 中不会出现映射项。脚本查找转换时若未命中,应将原始名视为混淆后名称直接使用。另外,若类已被 R8 完全移除,转换将得到空结果,自动核对机制必须将其列为缺失,提示人工确认。

混淆还带来配置匹配难题。若保护引擎配置针对原始类名,在混淆后的 DEX 中可能无法匹配目标。实施保护阶段应使用 mapping 文件将原始配置转换成混淆后配置,再注入保护逻辑。同样,自动核对机制在比较已保护类列表与 DEX 类列表时,也需先行统一名称空间。

名称空间不一致会导致假阳性,掩盖真实的漏保护问题。因此,完整的验证流程应同时接受保护清单和已保护输出两个文件,并在比对前统一使用同一份 mapping 文件进行转换。缺失任一部分都会降低发现漏保护的能力,导致验证结果不可信。

Mapping 转换与 DEX 归属验证示例
原始类名Mapping 混淆后类名实际所在 DEX 文件保护清单覆盖状态
com.example.pay.PaySignera.b.cclasses2.dex通过转换后匹配,标志为已覆盖
com.example.login.TokenProvider未出现在 mapping,名称受 keep 规则保护classes.dex使用原始名直接匹配,确认覆盖
com.example.old.LegacyHelper无映射行类已移除未在任何 DEX 中找到机制报告缺失,需人工判断移出
com.example.data.CipherHelperx.y.zclasses3.dex转换后发现归属变化,更新映射表
  • 解析 mapping.txt 构建原始名到混淆名字典
  • 处理未命中映射的类直接使用原始名匹配
  • 识别无映射行的类确认为已移除并报警
  • 在比对前统一保护配置与 DEX 的名称空间

AAB 动态模块的特殊验证策略

AAB 动态模块按需分发,其 DEX 在初次安装时可能尚未出现在设备上,只扫描基础安装包会漏掉这部分产物。应按目标 device spec 生成或获取完整 APK 集,分别检查 base 与功能模块中的 DEX。模块是否处于未受保护状态,必须以该模块的最终产物和保护工具报告为准,不能从安装时机直接推断。

针对 AAB 进行验证时,不应直接解包 aab 文件,因其不含最终 DEX 分割信息。应使用 bundletool 生成全设备配置的 APK set 作为检查对象。对 set 中每个 APK 重复标准的多 DEX 核对流程。若无法全量遍历,至少覆盖主流架构和高密度配置对应的 APK。

验证脚本需记录每个 APK 的 split 信息,以便在出现缺失时精确定位是哪个模块的哪个 DEX 缺少了保护目标。动态模块特有的入口类,需确保其对应的 APK 确实存在于 set 中。若未出现,应检查模块构建配置是否正确包含了该类。

工程上可接受以调试签名 APK 的 DEX 列表代表最终 install-time 的类分布,但需文档化这个前提。最终发布候选的确认应当由实际签名流水线产生的 APK 再次进行自动核对,避免因工具保真度不足导致遗漏未受保护的代码。签名差异虽不影响类列表,但影响完整性校验。

  • 使用 bundletool 生成全量或选定配置 APK set
  • 遍历 set 目录对每个 APK 执行 DEX 类提取
  • 记录每个 APK 的 split 名称及其缺失类详情
  • 确认动态模块入口类对应的 APK 存在于 set
  • 在生产签名环境最终构建后重新运行核对
  • 协同保护引擎确认 hook 动态加载机制能力

自动化脚本实现与失败条件定义

以下脚本实现了自动核对机制的核心逻辑。它接收 APK 路径和保护清单文本文件作为参数,内部使用 unzip 和 dexdump 提取所有 DEX 中的类列表,与清单求差集。脚本严格遵循只读原则,不会修改 APK 或任何构建产物,确保诊断过程安全可控。

脚本在执行初期检查依赖工具 dexdump 是否在 PATH 中,若不存在则直接退出并返回错误码二,表示环境错误。如果提供的 APK 文件内未发现任何 dex 条目,脚本报告无 DEX 并返回错误码三。当成功抽取出类集合后,通过 grep 的差值输出缺失类。

若有任何缺失类,脚本返回错误码一,否则返回零表示清单中的类均能在当前类表中找到。对于 PR 触发的构建,CI 可据退出码停止后续任务。脚本不处理 mapping,适用于未混淆类名或已经外部转换过的清单;返回零并不证明这些类已被保护。

如果需要直接支持混淆,可先使用另一个解析 mapping 的脚本将保护清单中的原始类名转换为混淆名,生成新的临时清单,再将此临时清单传给本脚本。所有中间文件的管理由调用方负责,脚本自己仅创建临时目录并在退出后依赖系统回收,不主动删除任何文件。

  • 确保输入参数 APK 路径和清单文件有效存在
  • 验证 dexdump 工具在运行环境中可用
  • 检查临时目录创建权限是否充足
  • 确认 grep 差集计算逻辑正确无误
多 DEX 保护清单缺失类诊断脚本
#!/bin/bash
# 诊断多 DEX 保护缺失类,只读检查不修改任何构建产物
APK_PATH="$1"
PROTECTED_LIST="$2"

if [ ! -f "$APK_PATH" ] || [ ! -f "$PROTECTED_LIST" ]; then
    echo "Usage: $0 <apk> <protected-class-list.txt>"
    exit 1
fi

TEMP_DIR=$(mktemp -d)

# 提取所有 DEX 文件
unzip -q "$APK_PATH" "*.dex" -d "$TEMP_DIR"
DEX_FILES=$(find "$TEMP_DIR" -name "*.dex" | sort)

if [ -z "$DEX_FILES" ]; then
    echo "APK 内未发现任何 DEX 文件"
    exit 2
fi

# 收集 APK 中所有类
> "$TEMP_DIR/all_classes.txt"
for dex in $DEX_FILES; do
    dexdump -l "$dex" 2>/dev/null | grep 'Class descriptor' | sed -e "s/.*'\(.*\)'/\1/" -e "s/\//<period>/g" -e "s/;$//" | sed "s/<period>/./g" >> "$TEMP_DIR/all_classes.txt"
done

sort -u "$TEMP_DIR/all_classes.txt" > "$TEMP_DIR/unique_classes.txt"

# 比较保护清单与 DEX 类列表
MISSING_CLASSES=$(grep -vx -f "$TEMP_DIR/unique_classes.txt" "$PROTECTED_LIST")

if [ -n "$MISSING_CLASSES" ]; then
    echo "以下保护清单中的类未在 APK 的 DEX 文件中发现:"
    echo "$MISSING_CLASSES"
    exit 1
else
    echo "所有保护清单中的类均已出现在 DEX 文件中。"
    exit 0
fi

持续集成中的清单演进与修复路径

将自动核对接入 CI 后,每次 release 构建都应执行跨 DEX 类覆盖检查。任务放在 assembleRelease 之后、签名之前,避免对已知不合格的产物继续签名。对于 PR 触发的构建,可以只在保护清单、R8 规则或模块依赖变化时运行完整检查,其余提交执行快速抽查。

当机制检测到新类缺失时,最直接的修复方式是将该类的保护需求反馈给安全工程师,判断该类是否确实需要保护。若需要,则将其加入保护清单并更新相关的保护配置;若不需要,可从 DEX 类列表中排除并记录排除理由。严禁直接忽略机制的失败告警。

如果缺失是由于 R8 无意中移除了关键类,开发者必须修改 keep 规则或重构代码以防止裁剪。在任何情况下,不能仅将机制告知的失败静音,即直接删除清单中的条目却不分析原因,这会使保护范围逐渐缩小,最终导致核心资产裸露。

长期维护中,保护清单的准确度会直接影响安全团队排查效率。推荐将清单文件与应用源代码共同存入版本控制系统,利用 Code Review 流程确保任何对清单的修改都有合理的解释。定期人工审查清单,删除不再存在的类,补充新功能引入的高风险入口。

  • 在 CI 脚本中定义自动核对 job 配置依赖 artifact
  • 保护清单文件内置于代码仓库确保变更有记录
  • 机制触发失败时禁止直接跳过必须评估修复
  • 定期审查清单中保护类与实际业务入口对应
  • 对 AAB 项目同步建立动态模块清单审核流程
  • 保留至少三个发布版本的核对日志便于审计

事实依据与适用边界

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

本文判断事实或工程依据适用限制
多 DEX 构建可能把代码分散到不同 dex 产物,且旧的 Dalvik 加载路径会影响 DEX 归并。Android multidex启用 multidex 不代表保护清单已覆盖全部 DEX;文档描述的是构建与加载行为,未涉及运行时保护引擎的覆盖范围。
R8 的职责包括代码缩减、优化和名称混淆,发布构建需保留对应 keep 规则和输出。Enable app optimization with R8R8 的编译优化不等于 VMP 保护,也不证明应用具备抗动态分析或内存篡改能力。
反射、JNI 和间接入口需要精确 keep 规则,宽泛的规则会掩盖边界错误并削弱优化。R8 keep rules best practiceskeep 规则只描述 R8 对代码可达性的保留策略,不能作为 VMP 保护范围的定义依据。
AAB 由 base、feature、配置与资产模块组成,最终设备安装的是由根据屏幕密度、CPU 架构等条件生成的派生 APK 集。Android App Bundle formatAAB 本身不是设备上直接安装的最终 APK,因此其内部 DEX 分布不能用于最终保护完整性的直接证据。
bundletool 可从 AAB 生成、检查特定设备配置的 APK set,支持未签名或调试签名。bundletool未显式提供发布签名信息时,工具可能使用调试签名,此时生成的 APK 不能直接作为发布候选亦不能代表最终签名包的所有安全属性。
移动端抗篡改与抗逆向属于纵深防御控制,不能替代服务端授权和完整发布链的安全保证。OWASP MASVS-RESILIENCE控制目录的存在不证明某个候选包已达到任何防护强度,仅表明具备该类别控制的实现意图。
保护清单必须精确映射到每个 DEX 中的目标类与方法,否则无法保证多 DEX 环境下的覆盖完整性。工程判断该判断基于对 Multidex 行为的分析,不依托具体的项目测量数据,实际覆盖程度需通过构建产物逐一核实。
基于构建产物的自动核对机制能够在开发阶段发现保护清单与真实 DEX 内容的差异,防止未受保护类进入发布流程。工程判断该机制仅保障构建阶段的清单一致性,不能防御运行时的动态代码加载或补丁包引入的未保护类。

工程常见问题

仅将主 DEX 作为保护目标会遗漏哪些风险点?

在多 DEX 应用中,所有非主 DEX 文件中的类均可能被遗漏。这些类可能包含支付签名、加密实现、令牌管理等高敏感逻辑。如果保护引擎只处理由构建系统指明的主 DEX 列表,那么分散在次级 DEX 中的代码将完全以未保护形式存在,攻击者可直接逆向。务必通过全量 DEX 枚举来确认覆盖。

如何快速确认某个类落在了哪个 DEX 中?

解压 APK,对所有 classes*.dex 分别运行 dexdump 命令或在 Android Studio 中使用 APK Analyzer 打开 APK,查看每个 DEX 节点下的类列表。满足 CI 自动化要求时可使用 bundletool 和 shell 脚本批量处理,生成类到 DEX 文件的映射文件,实现快速定位。

自动核对机制报告了缺失类,但实际对应业务功能仍正常,是否可忽略?

不可以直接忽略。缺失类可能已被 R8 移除或内联,需人工确认该类是否确实不属于必要的保护目标。如果是因 keep 规则错误导致关键逻辑消失,可能会导致最终产物功能异常;即使功能正常,该缺失也意味着保护清单已过时,需要更新以避免将来新引入的类被误认为缺失而扰乱机制。

混淆后保护清单中的原始类名无法匹配 DEX 怎么办?

可使用与该 release 构建匹配的 mapping.txt 转换原始类名,再与 DEX 类表比对;没有映射的类按原名查找。受 keep 规则保护的入口可能保留原名,合成类也可能没有一一对应关系,因此先用一组已知样本验证转换结果,再决定是否接入 CI。

AAB 动态模块的 DEX 在安装时未全部出现,如何保证它们以后也被保护?

验证时使用 bundletool 生成目标设备群的完整 APK set,对每个模块内的 DEX 都进行检查,确保其核心类已纳入保护清单。运行时侧,保护引擎必须能够 hook 系统或应用自身的 DEX 加载事件,在动态模块的 DEX 被初次加载前完成保护处理,否则该模块代码将短期暴露。

自动核对脚本可以在没有 Android SDK 的 CI 环境运行吗?

需要 Android SDK build-tools 中的 dexdump 工具,通常 CI 镜像可以预先安装 SDK 或仅提供必要的命令行工具。若 CI 环境不可安装,可将 APK 验证流程做成独立的 Docker 镜像并集成进流水线,镜像中包含最小化的工具集。脚本本身仅依赖 unzip 和 dexdump。

想用自己的 App 验证?

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

继续阅读: APK、DEX 与签名完整性如何一起验收