第27章:移动应用安全加固:代码混淆、资源加密、SO文件加固、反调试与反Hook集成、完整性校验与防篡改
各位好,欢迎来到第27章。今天聊的话题,说白了就是“怎么给你的App穿上铠甲”。
我做了这么多年逆向,见过太多App被脱得精光。代码逻辑一目了然,资源文件随便解压,SO库直接被IDA Pro怼脸分析。嗯,这感觉就像你家的门锁是纸糊的。
所以这一章,我们集中火力,把加固的几大核心手段过一遍。我会结合我踩过的坑,给你讲清楚每个环节怎么做。
27.1 代码混淆:ProGuard与DexGuard
代码混淆是加固的第一道防线。它不增加安全性,但能大幅提高阅读成本。
27.1.1 ProGuard基础配置
ProGuard是Android官方推荐的混淆工具。它做三件事:压缩、优化、混淆。
我个人习惯在build.gradle里这样配:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
这里有个坑——反射调用。我在项目中遇到过,混淆后某个第三方SDK的类名变了,结果运行时直接崩溃。所以一定要保留规则:
-keep class com.thirdparty.sdk.** { *; }
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
27.1.2 DexGuard进阶
DexGuard是ProGuard的商业增强版。它多了几项能力:字符串加密、类加密、调用隐藏。
举个例子,字符串加密会把代码中的敏感字符串变成密文:
// 混淆前
String apiKey = "sk_live_abc123";
// 混淆后
String apiKey = decrypt("0x3A2F...");
DexGuard的配置方式类似,但需要额外指定加密规则:
-encryptstrings class com.example.api.** {
<fields>;
}
说实话,DexGuard价格不便宜。但如果你做金融类App,这笔钱值得花。
27.2 资源加密
资源文件是逆向的“软柿子”。APK解压后,图片、布局、字符串全裸奔。
我常用的做法是:对敏感资源进行AES加密,运行时动态解密。
具体流程:
- 在构建阶段,用脚本加密assets目录下的文件
- 打包进APK
- App启动时,用内置密钥解密并加载
代码示例:
public static byte[] decryptAsset(Context context, String fileName) {
try {
InputStream is = context.getAssets().open(fileName + ".enc");
byte[] encrypted = readBytes(is);
// AES解密,密钥从native层获取
byte[] key = getKeyFromNative();
return AES.decrypt(encrypted, key);
} catch (Exception e) {
Log.e("Resource", "解密失败", e);
return null;
}
}
27.3 SO文件加固:UPX与OLLVM
SO文件是逆向工程师的重点目标。IDA Pro一拖,函数逻辑全暴露。
27.3.1 UPX压缩
UPX是加壳工具。它把SO文件压缩,运行时再解压执行。
用法很简单:
upx --ultra-brute libnative.so -o libnative_packed.so
但UPX有个致命弱点——特征明显。UPX的魔数"UPX!"在文件头里,逆向工程师一眼就能认出来。我建议配合魔数修改一起用:
// 用十六进制编辑器把"UPX!"改成"MYX!"
// 然后修改解压代码中的魔数校验
27.3.2 OLLVM混淆
OLLVM是LLVM的混淆分支。它能把代码变成一团乱麻。
三种核心混淆:
- 控制流平坦化:把if-else变成switch-case迷宫
- 指令替换:把简单运算替换成复杂等价运算
- 虚假控制流:插入永远不会执行的分支,干扰分析
我在项目中用过OLLVM,效果确实好。但要注意:混淆后的SO体积会膨胀2-3倍。而且调试起来很痛苦——我曾经花了一整天,就为了定位一个混淆后的空指针崩溃。
27.4 反调试与反Hook集成
反调试和反Hook是“主动防御”。它们不是防止逆向,而是让逆向过程变得痛苦。
27.4.1 反调试常用手段
| 方法 | 原理 | 绕过难度 |
|---|---|---|
| ptrace检测 | 检查自身是否被调试器附加 | 低 |
| 进程名检测 | 检查/proc/self/status中的TracerPid | 中 |
| 时间差检测 | 检测代码执行时间是否异常 | 高 |
| 断点检测 | 扫描内存中的断点指令 | 高 |
代码示例(ptrace检测):
int anti_debug() {
if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) {
// 已经被调试
return 1;
}
ptrace(PTRACE_DETACH, 0, 1, 0);
return 0;
}
27.4.2 反Hook策略
Hook工具(如Frida、Xposed)是逆向工程师的利器。反Hook的核心是检测Hook框架的特征。
我常用的检测点:
- 检测Xposed的类是否存在(如
de.robv.android.xposed.XposedBridge) - 检测Frida的端口(默认27042)
- 检测/proc/self/maps中是否有frida-agent.so
27.5 完整性校验与防篡改
完整性校验确保你的App没有被修改。常见做法是签名校验和哈希校验。
27.5.1 签名校验
在Java层校验签名:
public static boolean checkSignature(Context context) {
try {
PackageInfo info = context.getPackageManager()
.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
String signature = info.signatures[0].toCharsString();
return EXPECTED_SIGNATURE.equals(signature);
} catch (Exception e) {
return false;
}
}
但Java层的校验很容易被Hook绕过。我建议在Native层再做一次,而且校验逻辑要分散到多个函数中。
27.5.2 哈希校验
对关键DEX和SO文件计算哈希值,运行时比对:
// Native层实现
int verify_dex_hash() {
unsigned char hash[SHA256_DIGEST_LENGTH];
calculate_sha256("/data/app/.../base.apk", hash);
return memcmp(hash, expected_hash, SHA256_DIGEST_LENGTH) == 0;
}
27.6 实战:对一个Demo应用进行全量加固
好了,理论讲完了。我们动手做一个完整的加固流程。
假设我们有一个Demo App,包含:
- 一个登录页面(Java)
- 一个加密工具类(Java)
- 一个Native层校验函数(C++)
加固步骤:
- 代码混淆:配置ProGuard,保留反射调用类,对核心逻辑使用DexGuard字符串加密
- 资源加密:对assets中的配置文件进行AES加密,密钥从Native层获取
- SO加固:对libnative.so使用OLLVM混淆,只混淆校验函数,其他函数保持原样
- 反调试集成:在Native层添加ptrace检测和时间差检测,检测到调试器后返回假数据
- 完整性校验:在Native层校验APK签名和DEX哈希值,校验失败则闪退
加固后的效果:
- Java代码被混淆,关键字符串不可读
- 资源文件无法直接解压使用
- SO文件逻辑混乱,IDA Pro分析困难
- 调试器附加后,App自动退出
- 修改APK后,App无法启动