L1 基础
字符串/资源/标识符混淆、Manifest 处理、基础防二次打包。
本页从指令变换、数据格式与运行时实现三个层面说明加固实际做了什么, 并明确列出不支持的 opcode、被跳过的方法类型,以及尚未提供的能力与原因。 定位为面向中小企业的增强级(L3)加固,非金融支付等强监管场景。
最后更新:2026 年 9 月 17 日
需要提高逆向成本、但无需金融级对抗强度的 Android 应用
需要白盒强度或合规资质的场景,不应作为唯一手段
"常见等级"指该能力在行业内通常所属档位,不对应任何具体厂商的产品划分。
字符串/资源/标识符混淆、Manifest 处理、基础防二次打包。
DEX 整体加壳加密、完整性校验、签名校验、反调试。
方法级代码虚拟化(dex2c)、opcode 随机映射、反注入与 Hook 检测、运行时主动防御。
控制流平坦化、白盒密钥、RASP、远程密钥下发、实时风控。
以下为当前预编译运行时与打包链路实际支持的组合。
| 项目 | 支持情况 | 说明 |
|---|---|---|
| 最低 Android 版本 | Android 5.0(API 21) | 由注入运行时的 NDK API level 决定;构建脚本默认值即为 21。低于该版本的系统将无法加载运行时 |
| minSdk 处理 | 随包自适应 | 加固时解析 AndroidManifest 的 minSdkVersion(AAB 从 protobuf 读取,AAR 无 manifest 时按 21 处理),并据此调整代码生成 |
| minSdk < 23 的差异 | 有额外处理 | 低于 23 时需要考虑与父类 direct 方法的冲突,代码生成会按该情形调整 |
| 目标版本 | 无硬性上限 | 不改写 targetSdkVersion,由你的构建配置决定 |
| CPU 架构 | 4 种可选 | armeabi-v7a(32 位)、arm64-v8a(64 位)、x86、x86_64;按所选 ABI 注入对应运行时 |
| APK 签名 | v1 + v2 | 使用 apksigner 同时启用 v1(JAR 签名)与 v2(APK 签名方案 v2);v2 在 Android 7.0 以下不生效,此时回落到 v1 |
| AAB 签名 | 仅 v1 | AAB 使用 jarsigner 做 JAR 签名,且商店会重新签名与重新打包 |
| 包格式 | APK / AAB / AAR | AAR 只处理 classes.jar,不含应用级签名校验 |
minSdk 低于 21,需要以更低的 API level 重新构建运行时才能使用;
当前预编译产物按 21 构建。
加固不是简单加壳,而是对 DEX 方法体做变换,并注入一个原生运行时来解释执行这些方法。
lib/arm64-v8a/libzorvault.so ← 解释器 + 校验逻辑 lib/armeabi-v7a/libzorvault.so lib/x86/libzorvault.so lib/x86_64/libzorvault.so assets/zorvault_data.bin ← 加密后的方法数据
运行时按所选 ABI 注入;数据文件为加密容器,随包交付。
原方法体 ──▶ exec shell(调用原生层) const-string ──▶ stringFromPool(I) 其余指令 ──▶ 搬入 zorvault_data.bin classesN.dex ──▶ 重写并重新编号
方法体不再以 DEX 字节码形式留在包内,由原生解释器按数据解释执行。
NMS2\x01 | nonce(12B) | AES-256-GCM ciphertext+tag AAD: "ZORVAULT-ANDROID-STRING-POOL-V2"
任务密钥来自 64 字节随机 seed,经 SHA-256(带域分隔)派生为 32 字节密钥;
seed 写入该任务 SO 副本的 .zorvault.key 槽位。明文数据会被拒绝加载。
ZRVT\x02 | num_dex(u32) | decoder[256]
per-DEX: name_len(u16) | name
string pool | type pool
field/method pool
方法数据(指令原样保留)
decoder[256] 为该任务随机生成的 opcode 映射表,产物中的指令以映射后的
opcode 存储,运行时查表还原为标准 opcode。
决定反编译后还能看到多少原始逻辑,是加固的核心环节。
| 能力 | 实现要点 | 等级 | 状态 | 技术边界 |
|---|---|---|---|---|
| 方法体抽取 | 解析 code_item,指令字节原样搬入方法数据(不做翻译);随后把原方法替换为调用原生层的 exec shell |
L3 | 具备 | 仅处理规则接受的方法,非全量 |
| 池索引重映射 | string / type / field / method 四类索引从原 DEX 重映射到数据内的引用池;随指令一并修正 | L3 | 具备 | volatile 字段指令(0xf5–0xf8)的字段索引亦纳入重映射 |
| opcode 随机映射 | 每任务生成 256 字节 decoder 表;产物指令用映射后的 opcode,运行时查表还原 | L3 | 具备 | 映射不跨应用复用,同一包重复加固也不同 |
| exec shell 生成 | 原方法改为 execX(data_idx, Object[]);基本类型参数装箱(Integer.valueOf 等),对象直存;返回值拆箱并补 check-cast 以通过校验器 |
L3 | 具备 | data_idx = dex_idx << 16 | method_idx,保证跨 DEX 唯一 |
| 控制结构保真 | try/catch 的 uleb128 重算、handler 重映射;switch payload 原样搬运;分支偏移按替换后长度重算 | L3 | 具备 | goto 偏移溢出时升级为 goto/16 |
| 字符串加密 | const-string 改写为 invoke-static stringFromPool(I) + move-result-object;字符串进数据池 |
L2 | 具备 | 仅覆盖被保护方法内命中的站点;替换会改变指令长度,故需重算分支与 try 范围 |
| 资源混淆 | 改写 resources.arsc 的资源项名与路径串,并同步改名包内 res/ 文件 |
L2 | 具备 | AAPT2 标准布局的路径由 (类型, 项名, 配置) 推导而非存储,故必须同步改文件名;.9.png 后缀保留 |
| 方法名混淆 | 把壳 DEX 与 VM 数据中的方法名改写为无意义短名(a、b、c…),壳 DEX 与运行时引用池同步改名,不影响执行 | L1–L2 | 具备(默认开启) | 仅改名安全的方法:private、非构造、非 native、非生命周期,且继承链上无对外名字契约;构建异常时自动放弃混淆回退原名 |
| 字段名混淆 | 私有字段名改写为短名,并同步壳 DEX 的 field_ids 与 VM 字段池 | L1–L2 | 具备(默认开启) | 跳过 synthetic、序列化契约名,以及所在类含 native 方法或实现 Serializable 的字段;构建异常时自动回退原名 |
| 类名混淆 | 把壳 DEX 与 VM 数据中的类描述符改写为短名 | L1–L2 | 可选(默认关闭) | 仅 APK;AAB/AAR 的 manifest 为 protobuf 格式,无法提取组件名单,自动跳过。已内置保守过滤(manifest 组件、Serializable 闭包、异常类、含 native 的类、注解/枚举、反射字符串命中的类、资源绑定类),但类名的反射/序列化用法多样,建议充分测试后再启用 |
| 控制流平坦化 | 打散基本块并以状态机调度 | L4 | 未提供 | 明确不做:需重写大量基本块,兼容性风险与体积/性能代价不成比例 |
| 能力 | 实现要点 | 等级 | 状态 | 技术边界 |
|---|---|---|---|---|
| 任务级加密 | 每任务 64 字节随机 seed → SHA-256(domain || seed) 派生 32 字节密钥 → AES-256-GCM;nonce 12 字节 |
L3 | 具备 | 不跨应用复用固定密钥 |
| 密钥绑定 | seed 写入该任务 SO 副本的 .zorvault.key 槽位(64 字节),数据与运行时一对一绑定 |
L3 | 具备 | 槽位被替换后即失效,已 patch 的 SO 不能再次用于加固 |
| 篡改认证 | GCM 认证加密 + 固定 AAD;密文被改则解密失败 | L3 | 具备 | 不接受未加密的明文数据,避免绕过解密层 |
| 白盒加密 / 远程下发 | 密钥不以可还原形式存在于包内,或运行时由服务端下发 | L4 | 未提供 | 密钥材料随包分发,具备静态分析能力的攻击者原则上可还原 —— 这是与 L4 的主要差距 |
| 能力 | 实现要点 | 等级 | 状态 | 技术边界 |
|---|---|---|---|---|
| 启动与注册 | JNI_OnLoad 取 Env、设置隐藏 API 豁免、读取并解密数据;随后用 RegisterNatives 把 8 个入口动态绑定到随机类名的壳类 |
L3 | 具备 | 壳类名随机化后静态 JNI 符号名不再匹配,必须走动态注册 |
| 解释执行 | switch 派发;寄存器文件 + fp_flags 标记对象寄存器;引用池按索引解析并缓存 |
L3 | 具备 | 方法/字段解析结果带缓存,避免每条指令重复 FindClass/GetMethodID |
| 引用生命周期 | 写入对象寄存器前先释放该寄存器原有局部引用;异常路径处理完即释放 | L3 | 具备 | JNI 局部引用表有上限(通常 512),循环内频繁抛异常曾导致累积,当前已修正 |
| 数组访问语义 | 下标按无符号比较判定越界,负索引与超大索引均触发 ArrayIndexOutOfBoundsException |
L3 | 具备 | 早期按有符号比较,负索引会绕过检查并越界读写,属于会静默损坏数据的缺陷,已修正 |
| 反调试 | ptrace(TRACEME) 自检;被占用时回退读 /proc/self/status 的 TracerPid |
L2 | 具备 | 自检会占用追踪槽位,可能与外部调试器或崩溃采集组件竞争 |
| 反注入 / 反 Hook | 扫描 GOT 表改写、inline hook 及已知注入特征 | L3 | 部分具备 | 特征库为静态:改名或换路径的注入框架可能不匹配 |
| Frida 主动检测 | 扫描 Frida 默认监听端口(27042 / 27043,同时查 /proc/net/tcp 与 tcp6)、遍历 /proc 查找 frida-server / gdbserver 进程,并匹配 frida-agent / frida-gadget / gum-js-loop 等注入特征 |
L3 | 具备(默认开启) | 端口检测可覆盖"改名 / 改路径"的 Frida;改用自定义端口或自编译 Frida 仍可能绕过 |
| Root 环境检测 | 检查常见 su 二进制落点(/system/xbin/su、/system/bin/su、/magisk/.core/bin/su 等)与 Root 管理包目录(Magisk / SuperSU 等) |
L2–L3 | 具备(默认开启) | Root 管理包目录在 Android 10+ 受权限限制读不到,主要依据 /system、/sbin 下的 su;检测路径与特征均混淆存储 |
| 多开环境检测 | 在 /proc/self/maps 与私有数据目录中匹配多开容器特征(平行空间、双开助手、VirtualApp 等)及 /virtual/、:work、/clone/ 等路径片段 |
L2–L3 | 具备(默认开启) | 只用高置信特征,避免把正常 SDK 误判为多开;改名 / 换壳的多开框架可能不匹配 |
| 模拟器检测 | 检查 qemu 设备路径(/dev/qemu_pipe 等)与 /proc/cpuinfo 中的 goldfish / qemu 特征 |
L2–L3 | 具备(默认开启) | 可用 disable_emulator_check 关闭(手游渠道包等确有模拟器用户的场景);AAR 无 security section,不生效 |
| Java 层包名校验 | 在壳类的 <clinit> 中校验包名,位于加载原生库之前 |
L2 | 具备 | 即使原生库被剥离,该校验仍会执行,可作为兜底 |
| 违规处置 | 检测到违规后可选择:抛异常(默认)、静默返回零值 / 空引用、终止进程 | L2 | 具备 | 默认抛异常,便于宿主与开发者发现排查;未知取值也回退为抛异常。静默策略能隐藏检测点,但可能掩盖问题,需按业务显式配置 |
| 能力 | 实现要点 | 等级 | 状态 | 技术边界 |
|---|---|---|---|---|
| DEX 完整性 | 自实现 ZIP 解析(尾部定位 EOCD → 中央目录 → 条目),对 DEX 计算 SHA-256 并与加固时记录值比对 | L2 | 具备 | 加固产物的 DEX 为未压缩(STORED)存储;被重新压缩会判为篡改而非跳过校验 |
| 签名校验 | 校验签名证书指纹,识别重签名 | L2 | 具备 | AAB 不适用:商店会重新签名,绑定必然失效 |
| 重打包与对齐 | 清理 META-INF → 重签(v1/v2)→ 校验签名 → 对齐(so 16KB、dex 4B、arsc 16KB)→ 多 DEX 重新编号 | L2 | 具备 | 自定义签名材料不持久化 |
| 方法级降级 | 方法含不支持指令时只跳过该方法,同 DEX 内其余方法照常处理 | L3 | 具备 | 早期为整 DEX 降级,会造成大片代码未保护;已收敛为方法级 |
| 结果可核查 | 输出方法覆盖率、未保护类清单、字符串加密站点数 | L3 | 具备 | 覆盖率分母为"规则接受的方法数",规则主动排除的不计为未保护 |
| 构建一致性校验 | 比对加固服务与运行时的数据格式版本指纹,不一致则拒绝产出 | L3 | 具备 | 防止"代码已改、运行时未同步"导致可安装但会崩的产物 |
这些是由实现机制决定的硬边界,加固后请据任务报告中的覆盖率判断是否符合预期。
这些指令解释器尚未支持,遇到即整方法不保护(不会回退整 DEX,但该方法保持原样):
iget-*-quick
iput-*-quick
invoke-virtual-quick
invoke-super-quick
实际影响:这些 quickened 指令只会出现在被优化过的 odex 中,正常构建的 APK 不会命中。
invoke-custom / invoke-polymorphic / const-method-handle /
const-method-type 等 Java 8+ 指令自 codegen 版本 2 起已完整支持,含这些指令的方法同样会被保护。
出于类初始化安全与语义正确性考虑,这些方法不会被抽取:
<init>
<clinit>
abstract
native
bridge
synthetic
此外,生命周期方法中对父类的 invoke-super 调用也会跳过,避免类初始化阶段崩溃。
被保护方法改由原生解释器执行,热点路径会慢于原 DEX(无 JIT 参与);
每选一个 ABI 都会注入对应运行时。以当前版本为例,
libzorvault.so 单 ABI 约 0.46–0.82 MB(armeabi-v7a ≈ 0.46 MB,arm64-v8a ≈ 0.66 MB,
x86_64 ≈ 0.75 MB,x86 ≈ 0.82 MB),四个 ABI 全选约 2.7 MB,
另有随方法数增长的数据文件。
建议按规则只保护核心逻辑,而非全量保护。
原因:需要重写大量基本块与指令序列,兼容性风险显著(校验失败、机型相关异常), 且体积与运行时性能代价大。对"提高逆向成本"这一中小企业诉求而言收益风险不成比例,明确不纳入路线。
原因:未提供。密钥材料随包分发,具备足够能力的攻击者可静态还原。 非强监管场景下通常不是首要风险,但不应期待其达到白盒强度。
原因:分发机制限制而非实现缺失。Google Play 会重新签名并重新打包 AAB, 加固时绑定的签名必然失效,因此 AAB 产物只写入包名与开关。
新账户含 5 次永久有效的免费额度。任务报告会给出方法覆盖率、未保护类清单与字符串加密站点数。
Android 5.0(API 21)及以上;armeabi-v7a、arm64-v8a、x86、x86_64 四种 ABI 可选。
<init>、<clinit>、abstract、native、bridge、synthetic 方法会被跳过,保持原样执行;正常 APK 不会出现的 quickened 指令(iget/iput-*-quick、invoke-virtual/super-quick)所在的方法也会跳过。invoke-custom / invoke-polymorphic 等 Java 8+ 指令已支持保护。
受保护方法无 JIT 参与,热点路径慢于原始 DEX;建议仅保护核心业务方法,而非全量加固。
定位为增强级(L3):方法级代码虚拟化、opcode 随机映射、反注入与 Hook 检测、运行时主动防御,非 L4 白盒强度。
标识符混淆已提供:方法名与字段名混淆默认开启,类名混淆为可选(默认关闭,仅 APK)。控制流平坦化暂未提供,建议先用 R8/ProGuard 混淆后再提交,两者可叠加。
Google Play 会重新签名并重打包 AAB,加固时绑定的签名必然失效,故 AAB 产物只写入包名与开关。