一、加固前:jadx 打开你的 APK,能看到什么
假设你写了一个授权校验类,源码大致是这样(这里把密钥和地址写在方法内部,而不是静态常量里):
public final class LicenseManager {
public boolean isPro(String deviceId) {
String signKey = "a7f3c1-lic-2026";
String verifyUrl = "https://api.example.com/v1/license/verify";
String sig = hmacSha256(signKey, deviceId);
String body = "{\"device\":\"" + deviceId + "\",\"sig\":\"" + sig + "\"}";
String resp = post(verifyUrl, body);
return "pro".equals(parsePlan(resp));
}
}
如果你没有开 R8 / ProGuard,jadx 反编译出来的结果和上面几乎一模一样—— 类名、方法名、字符串常量、控制流、算法步骤全部保留:
public final class LicenseManager {
public boolean isPro(String str) {
String hmacSha256 = hmacSha256(
"a7f3c1-lic-2026", str);
return "pro".equals(parsePlan(post(
"https://api.example.com/v1/license/verify",
"{\"device\":\"" + str
+ "\",\"sig\":\"" + hmacSha256 + "\"}")));
}
}
const-string v0, "a7f3c1-lic-2026"
const-string v1, "https://api.example.com/v1/license/verify"
invoke-static {v0, p1}, \
LLicenseManager;->hmacSha256(...)Ljava/lang/String;
move-result-object v0
new-instance v2, Ljava/lang/StringBuilder;
...
invoke-virtual {v2}, Ljava/lang/StringBuilder;
->toString()Ljava/lang/String;
move-result-object v2
invoke-static {v1, v2}, LLicenseManager;
->post(...)Ljava/lang/String;
也就是说,加固之前,任何一个拿到你 APK 的人都能直接得到:
- 接近源码的 Java:类结构、方法体、分支与循环,可读性很高;
- 明文字符串:接口地址、签名密钥、SQL、加密盐、第三方 API key;
- AndroidManifest.xml:所有组件、权限、入口、deeplink;
- resources.arsc 与 res/:布局、drawable、字符串资源;
- 可改的 smali:改完重新打包、随便签个名就能在设备上跑起来。
于是盗版分发、破解内购、扒接口刷量、抄袭核心算法、注入广告 SDK 再二次打包—— 这些事情的第一步都是同一件:用静态工具把它打开看看。
二、加固后:同一个方法变成什么样
加固做的本质是一件事:把"代码"变成"数据"。具体分五步:
- 解析方法的
code_item,把里面的指令原样搬出 DEX,放进加密容器assets/zorvault_data.bin; - 原来的方法位置替换成一个 exec shell:把参数装箱,交给原生层;
- 方法内的
const-string改成stringFromPool(索引),字符串本体进数据池; - 方法名、私有字段名与类名混淆默认开启,符合兼容性条件的名称改写为短名(
a、b、c…); - 注入
libzorvault.so,启动时用RegisterNatives把入口动态绑定到一个随机类名的壳类上,由解释器读数据、还原 opcode 后执行。
于是 jadx 再打开同一个包,同一个方法变成这样:
public final class a {
// 类名混淆默认开启;需要兼容的类保留原名
public boolean a(String str) { // 方法名已混淆
return ((Boolean) VmBridge.exec(
(0 << 16) | 42, // data_idx
new Object[]{ this, str }
)).booleanValue();
}
}
const/4 v0, 0x2a # data_idx
const/4 v1, 0x2
new-array v1, v1, [Ljava/lang/Object;
const/4 v2, 0x0
aput-object p0, v1, v2
const/4 v2, 0x1
aput-object p1, v1, v2
invoke-static {v0, v1}, LVmBridge;
->exec(I[Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v0
check-cast v0, Ljava/lang/Boolean;
invoke-virtual {v0}, Ljava/lang/Boolean;
->booleanValue()Z
move-result v0
return v0
signKey、verifyUrl、hmac 算法、JSON 拼接、返回判断——
全都不在 DEX 里了。剩下的只有"把参数装进数组,调一次 native,把结果拆箱"。
包里多了两个文件
lib/arm64-v8a/libzorvault.so ← 解释器 + 校验逻辑(约 0.66 MB) lib/armeabi-v7a/libzorvault.so (约 0.46 MB) assets/zorvault_data.bin ← 加密后的方法数据 zorvault_data.bin 布局: NMS2\x01 | nonce(12B) | AES-256-GCM 密文 + tag AAD: "ZORVAULT-ANDROID-STRING-POOL-V2"
二进制编辑器打开 zorvault_data.bin,看到的是密文。
把它单独抠出来也没用:任务密钥不在这个文件里,而在该任务专属的那份 SO 副本的
.zorvault.key 槽位里——数据与运行时一对一绑定,换一份 SO 就解不开。
libzorvault.so,
拿到的也只是一个通用解释器。业务逻辑不在解释器里,而在加密数据里。
想还原,必须同时拿到解释器、密钥槽位和那份数据,并把数据格式反解回 DEX。
三、逐项对比
| 你关心的东西 | 加固前(jadx / apktool) | 加固后(金铠甲加固) |
|---|---|---|
| 业务方法逻辑 | 接近源码,可逐行读 | 方法体消失,只剩调用原生层的壳 |
| 方法内字符串 | 明文:接口地址、密钥、SQL | 移入加密数据池,DEX 内不可见 |
| 方法名 | 原始命名(未开 R8 时) | 短名 a / b / c(默认开启) |
| 私有字段名 | 原始命名 | 短名(默认开启) |
| 类名 | 原始命名 | 默认开启;符合兼容性条件的类改为短名,需要兼容的类保留原名 |
| AndroidManifest.xml | 明文可读 | 仍可读(组件名必须保留,否则系统找不到) |
| resources.arsc / res | 可读 | 资源项名与路径已改写 |
| 改 smali 后重打包 | 可行,随便签名就能装 | DEX SHA-256 完整性校验 + 签名校验会触发 |
| Frida / Xposed 挂钩 | 无阻碍 | 有检测(默认开启),但非绝对 |
| 体积代价 | — | 每选一个 ABI 约 0.46–0.82 MB + 随方法数增长的数据文件 |
四、加固做不到什么
这部分比"能做到什么"更重要。我们把边界写清楚,免得你按错的预期去选型:
- 提高的是攻击成本,不是绝对防护。 攻击者完全控制设备时,任何客户端侧保护最终都可能被绕过。
- 定位是 L3 增强级,不是 L4 白盒。 没有白盒加密、没有密钥远程下发:密钥材料随包分发,具备足够静态分析能力的攻击者原则上可以还原。 支付、银行、证券这类场景不应把它当唯一手段。
-
部分方法不参与保护。
<init>、<clinit>、abstract、native、bridge、synthetic方法会被跳过;生命周期中的invoke-super已纳入转换,具体保护范围以规则与任务报告为准。 -
由此产生一个很容易踩的坑:
<clinit>不受保护,所以写成static final String常量的密钥, 字符串仍会以明文留在 DEX 里。别把密钥写成静态常量,放进方法内、并让该方法被保护。 - Manifest 与资源内容仍可读。 资源名被改写了,但资源本身不是加密的;组件名必须保留,否则系统加载不到。
- 有性能代价。 被保护方法由原生解释器执行、无 JIT 参与,热点路径会慢于原 DEX。
- 动态检测可绕过。 Frida 检测覆盖默认端口(27042 / 27043)与已知进程 / 库特征, 但自编译 Frida、自定义端口仍可能绕过。
五、怎么用才划算
- 先用 R8 / ProGuard 做一层基础混淆,再提交加固——两者可叠加,各管一层。
- 只保护核心方法,不要全量加固。 按规则挑授权校验、算法、协议、反作弊外围这类真正值钱的逻辑; 全量加固只会白白付出体积和性能代价。
- 看任务报告:方法覆盖率、未保护类清单、字符串加密站点数, 据覆盖率判断保护范围是否符合预期。
- 用真机跑一遍,确认功能与性能没有回退,再发布。
拿你自己的包,先看一眼加固前长什么样
新账户含 5 次永久有效的免费额度。加固后自己用 jadx 打开对比一遍,比任何介绍都直观。