10、混淆与加固:代码混淆、资源混淆、反调试、完整性校验、加固方案对比
各位同学,今天我们来聊聊应用安全的最后一道防线——混淆与加固。
说实话,前面我们讲了那么多漏洞和防御,代码写得再安全,如果APK被人直接拖进反编译工具,那一切等于白干。我见过太多开发同学,辛辛苦苦写了半年,结果被人三分钟扒了个底朝天。嗯,这章我们就来解决这个问题。
10.1 代码混淆:让反编译者头疼的第一步
代码混淆,说白了就是给你的代码「改个名」。把那些有意义的类名、方法名、变量名,全部换成a、b、c、d这种毫无意义的字母。
我个人习惯用ProGuard或R8。Android Gradle插件从3.4.0开始默认使用R8,它比ProGuard更快,效果也更好。
核心配置:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
}
这里有个坑,我必须要提醒你。我曾经在一个项目中,因为忘了保留某个反射调用的类,结果线上崩溃了一大片。那叫一个惨啊……
避坑指南:
我曾经遇到过,混淆后Gson解析全部失败。原因是数据类被混淆了,Gson反射找不到字段。解决方案很简单:
-keep class com.example.model.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
10.2 资源混淆:不只是改个名那么简单
你以为混淆完代码就安全了?天真。资源文件一样能暴露你的业务逻辑。
资源混淆,就是把res目录下的文件名、资源ID全部打乱。比如原来叫ic_launcher.png,混淆后变成a.png。这样攻击者想通过资源名猜测功能,基本没戏。
我推荐使用微信开源的AndResGuard。它不改变资源本身,只改变资源路径和文件名。
配置示例:
apply plugin: 'AndResGuard'
andResGuard {
mappingFile = null
use7zip = true
keepRoot = false
whiteList = [
"R.mipmap.ic_launcher",
"R.drawable.splash_bg"
]
}
注意:启动页、图标等资源要加入白名单,否则系统找不到会闪退。
10.3 反调试:让动态分析寸步难行
代码混淆防的是静态分析,反调试防的是动态调试。你想想看,攻击者把APK跑在模拟器里,用Frida或Xposed hook你的函数,那你的加密算法、签名校验全白搭。
反调试的手段有很多,我挑几个常用的说说:
- 检测调试器:通过
Debug.isDebuggerConnected()判断是否被调试 - 检测进程名:检查
/proc/self/status中的TracerPid是否为0 - 检测模拟器:检查Build属性、设备特征、传感器数据
- 时间检测:单步调试会导致时间异常,通过时间差判断
一个简单的反调试示例:
public static boolean isBeingDebugged() {
// 方法一:系统API
if (Debug.isDebuggerConnected()) return true;
// 方法二:检查TracerPid
try {
BufferedReader br = new BufferedReader(
new FileReader("/proc/self/status"));
String line;
while ((line = br.readLine()) != null) {
if (line.contains("TracerPid")) {
int pid = Integer.parseInt(
line.split(":")[1].trim());
if (pid != 0) return true;
}
}
} catch (Exception e) { /* 忽略 */ }
return false;
}
注意:反检测代码本身不能太明显。我曾经见过有人把反调试写在onCreate第一行,结果攻击者直接跳过那行代码。建议分散在多个地方,甚至用JNI实现。
10.4 完整性校验:防止APK被篡改
完整性校验,就是检查APK有没有被人动过手脚。常见的做法是校验签名、校验Hash值。
我习惯的做法是:在服务端下发一个签名Hash,客户端在启动时计算自身签名,两者比对。如果不一致,说明APK被重打包了。
签名校验代码:
public static boolean checkSignature(Context context) {
try {
PackageInfo info = context.getPackageManager()
.getPackageInfo(context.getPackageName(),
PackageManager.GET_SIGNATURES);
Signature[] signatures = info.signatures;
// 计算签名Hash
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(signatures[0].toByteArray());
String hash = bytesToHex(digest);
// 与服务端下发的Hash比对
return SERVER_HASH.equals(hash);
} catch (Exception e) {
return false;
}
}
小技巧:不要把Hash硬编码在Java代码里,太容易被找到。我建议放在JNI层的so文件里,或者从服务端动态获取。
10.5 加固方案对比:选对工具事半功倍
市面上加固方案很多,我简单做个对比。注意,这不是广告,是我个人踩坑后的经验总结。
| 加固方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| DEX加固 | 加密DEX文件,运行时解密 | 实现简单,兼容性好 | 容易被dump内存 | 中小型应用 |
| VMP加固 | 将DEX转为自定义指令 | 反编译难度极高 | 性能损耗大,包体增大 | 金融、支付类应用 |
| SO加固 | 加密so文件,加壳保护 | 保护native代码 | 兼容性问题较多 | 游戏、算法核心 |
| 资源加固 | 加密资源文件 | 防止资源被提取 | 影响启动速度 | 有敏感资源的应用 |
| 整体加固 | 多重方案组合 | 安全性最高 | 成本高,维护复杂 | 高安全需求应用 |
我个人建议:如果你的应用涉及金融、支付、核心算法,至少上VMP加固。如果只是普通应用,DEX加固+代码混淆基本够用。
10.6 本章知识体系
下面这张图,是我梳理的本章知识结构。你可以把它当作一个检查清单,看看自己漏了哪一步。
嗯,这张图基本涵盖了本章的所有内容。从上到下,从左到右,你可以看到一条清晰的防护链路:代码混淆→资源混淆→反调试→完整性校验→选择加固方案。每一步都不能少。
最后说一句:安全没有银弹。混淆和加固只是提高攻击成本,不是绝对安全。但话说回来,你把门槛抬高了,大部分攻击者就会转向更容易的目标。这就够了。
公众号:蓝海资料掘金营,微信deep3321