代码混淆与反混淆:一场猫鼠游戏

各位同学,今天我们来聊聊代码混淆与反混淆。说实话,这是逆向工程里最有意思的部分之一。你想想看,开发者费尽心思把代码藏起来,而我们逆向工程师又要想办法把它还原。这就像一场永无止境的猫鼠游戏。

我个人习惯把混淆看作是一层「壳」。它不是真正的安全,而是增加攻击者的时间成本。说白了,没有解不开的混淆,只有时间够不够的问题。

ProGuard 混淆原理

ProGuard 是 Android 官方推荐的代码混淆工具。它主要做四件事:

  • 压缩(Shrink):移除未使用的类、方法和字段
  • 优化(Optimize):对字节码进行优化,比如合并指令
  • 混淆(Obfuscate):将类名、方法名、字段名重命名为无意义的短名称
  • 预校验(Preverify):为 Java 虚拟机添加预校验信息

我在项目中遇到过很多次,开发者只开了混淆,没开压缩。结果 APK 里一堆没用的类还在,白白增加了体积。嗯,这里要注意,minifyEnabled true 才是开启压缩和混淆的总开关。

ProGuard 的混淆规则写在 proguard-rules.pro 文件里。举个例子:

# 保留某个类不被混淆
-keep class com.example.MyClass { *; }

# 保留某个方法
-keepclassmembers class com.example.MyClass {
    public void myMethod();
}

# 保留所有 native 方法
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留枚举
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

为什么要保留这些?因为有些类是通过反射调用的,混淆后名字变了,反射就找不到目标了。我曾经帮一个团队排查过线上崩溃,就是因为混淆后某个 JSON 解析库的字段名变了,导致数据解析失败。

字符串加密

ProGuard 本身不加密字符串。它只是把类名和方法名改成 a、b、c 这样的名字。但字符串明文仍然留在代码里。你想想看,如果关键 API Key 直接写在代码里,那混淆了类名有什么用?

所以,很多加固方案会额外做字符串加密。原理很简单:

  1. 在编译时,扫描所有字符串常量
  2. 用某种算法(如 XOR、AES)加密它们
  3. 在运行时,通过一个解密函数还原

举个例子,原始代码可能是:

String apiKey = "sk-1234567890abcdef";

加密后变成:

String apiKey = decrypt("0x3A,0x4B,0x5C,...");

解密函数通常用 native 层实现,增加逆向难度。我见过一些应用把解密函数放在 so 文件里,然后用 JNI 调用。嗯,这确实能挡住一些新手,但对老手来说,无非是多花点时间分析 so 文件而已。

注意:字符串加密不是银弹。如果解密密钥硬编码在代码里,或者解密逻辑可以被静态分析,那加密就形同虚设。我曾经逆向过一个应用,它的解密函数就是简单的 XOR 0x55,一眼就看穿了。

反射调用

反射是混淆的天敌。为什么?因为反射调用时,类名和方法名是字符串形式传入的。如果这些字符串被混淆器改了,反射就会失败。

常见的做法是:

  • -keep 规则保留被反射调用的类
  • 或者,把反射调用的类名和方法名也加密

第二种做法更隐蔽。比如:

// 加密后的类名和方法名
String className = decrypt("encrypted_class_name");
String methodName = decrypt("encrypted_method_name");

Class<?> clazz = Class.forName(className);
Method method = clazz.getDeclaredMethod(methodName);
method.invoke(obj);

这样一来,即使你反编译了代码,看到的也是加密后的字符串。你得先找到解密函数,才能还原真正的类名和方法名。

我个人习惯在分析这类混淆时,先定位解密函数,然后 hook 它。这样所有加密字符串都会在运行时被还原,一劳永逸。

反混淆工具

有矛就有盾。下面介绍两款我常用的反混淆工具。

deguard

deguard 是一个专门针对 ProGuard 混淆的还原工具。它的原理是:

  1. 分析混淆后的映射文件(mapping.txt)
  2. 根据映射关系,将混淆后的类名、方法名还原为原始名称

使用方式很简单:

java -jar deguard.jar -m mapping.txt -i classes.dex -o output.dex

但有个前提——你得有 mapping.txt 文件。这文件通常只有开发者自己才有。如果没有 mapping.txt,deguard 就无能为力了。

我记得有一次,客户只给了我们一个混淆后的 APK,没有 mapping 文件。我们只能手动分析,那叫一个痛苦。后来我写了个脚本,通过对比不同版本的 APK,自动推断出一些类名的映射关系。虽然不能完全还原,但至少能看懂大部分逻辑了。

Simplify

Simplify 是一个更强大的反混淆工具。它不仅能还原 ProGuard 混淆,还能处理一些自定义的混淆方案。

Simplify 的核心思路是「执行跟踪」。它会在虚拟环境中运行 APK,记录所有执行路径,然后根据执行结果还原代码逻辑。

举个例子,如果代码里有这样的逻辑:

int a = 1;
int b = 2;
int c = a + b;
if (c == 3) {
    // 真实逻辑
} else {
    // 虚假逻辑
}

Simplify 会执行这段代码,发现 c == 3 永远为真,于是它会移除 else 分支,只保留真实逻辑。这样一来,代码就清晰多了。

使用方式:

java -jar simplify.jar -i input.apk -o output.apk
小技巧:Simplify 对时间敏感的逻辑处理得不太好。比如代码里有用 System.currentTimeMillis() 做判断的,Simplify 执行时的时间戳和真实运行时间不同,可能导致分析结果有误。我建议在分析这类逻辑时,手动 patch 一下时间戳。

知识体系总览

下面这张图总结了本章的核心内容,你可以对照着梳理思路:

代码混淆与反混淆知识体系 混淆(Obfuscation) ProGuard 混淆原理 压缩 → 优化 → 混淆 → 预校验 字符串加密 编译时加密 → 运行时解密 反射调用保护 -keep 规则 / 反射字符串加密 目标:增加逆向时间成本 反混淆(Deobfuscation) deguard 基于 mapping.txt 还原 Simplify 执行跟踪 → 移除死代码 → 还原逻辑 手动分析技巧 Hook 解密函数 / 对比不同版本 APK 目标:还原可读的代码逻辑

从这张图可以看得很清楚:混淆和反混淆是镜像对称的。混淆做三件事——ProGuard、字符串加密、反射保护;反混淆就做对应的三件事——用 deguard 还原类名、用 Simplify 还原逻辑、手动分析加密字符串。

核心观点:没有绝对安全的混淆,只有相对安全的混淆。作为逆向工程师,我们的目标不是「破解一切」,而是「在有限时间内找到关键信息」。掌握反混淆工具的使用,能让你事半功倍。

好了,关于代码混淆与反混淆,我就讲到这里。记住,工具只是辅助,真正重要的是理解混淆的原理。原理懂了,遇到任何花样的混淆,你都能找到突破口。


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