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 本章知识体系

下面这张图,是我梳理的本章知识结构。你可以把它当作一个检查清单,看看自己漏了哪一步。

混淆与加固 代码混淆 ProGuard / R8 保留规则配置 资源混淆 AndResGuard 白名单配置 反调试 检测调试器 检测模拟器 完整性校验 签名校验 Hash比对 加固方案对比 DEX加固 VMP加固 SO加固 资源加固 整体加固

嗯,这张图基本涵盖了本章的所有内容。从上到下,从左到右,你可以看到一条清晰的防护链路:代码混淆→资源混淆→反调试→完整性校验→选择加固方案。每一步都不能少。

最后说一句:安全没有银弹。混淆和加固只是提高攻击成本,不是绝对安全。但话说回来,你把门槛抬高了,大部分攻击者就会转向更容易的目标。这就够了。


公众号:蓝海资料掘金营,微信deep3321