代码混淆与反混淆:一场猫鼠游戏
各位同学,今天我们来聊聊代码混淆与反混淆。说实话,这是逆向工程里最有意思的部分之一。你想想看,开发者费尽心思把代码藏起来,而我们逆向工程师又要想办法把它还原。这就像一场永无止境的猫鼠游戏。
我个人习惯把混淆看作是一层「壳」。它不是真正的安全,而是增加攻击者的时间成本。说白了,没有解不开的混淆,只有时间够不够的问题。
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 直接写在代码里,那混淆了类名有什么用?
所以,很多加固方案会额外做字符串加密。原理很简单:
- 在编译时,扫描所有字符串常量
- 用某种算法(如 XOR、AES)加密它们
- 在运行时,通过一个解密函数还原
举个例子,原始代码可能是:
String apiKey = "sk-1234567890abcdef";
加密后变成:
String apiKey = decrypt("0x3A,0x4B,0x5C,...");
解密函数通常用 native 层实现,增加逆向难度。我见过一些应用把解密函数放在 so 文件里,然后用 JNI 调用。嗯,这确实能挡住一些新手,但对老手来说,无非是多花点时间分析 so 文件而已。
反射调用
反射是混淆的天敌。为什么?因为反射调用时,类名和方法名是字符串形式传入的。如果这些字符串被混淆器改了,反射就会失败。
常见的做法是:
- 用
-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 混淆的还原工具。它的原理是:
- 分析混淆后的映射文件(mapping.txt)
- 根据映射关系,将混淆后的类名、方法名还原为原始名称
使用方式很简单:
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
知识体系总览
下面这张图总结了本章的核心内容,你可以对照着梳理思路:
从这张图可以看得很清楚:混淆和反混淆是镜像对称的。混淆做三件事——ProGuard、字符串加密、反射保护;反混淆就做对应的三件事——用 deguard 还原类名、用 Simplify 还原逻辑、手动分析加密字符串。
核心观点:没有绝对安全的混淆,只有相对安全的混淆。作为逆向工程师,我们的目标不是「破解一切」,而是「在有限时间内找到关键信息」。掌握反混淆工具的使用,能让你事半功倍。
好了,关于代码混淆与反混淆,我就讲到这里。记住,工具只是辅助,真正重要的是理解混淆的原理。原理懂了,遇到任何花样的混淆,你都能找到突破口。