Android 安全是一个常被低估的领域。很多团队直到被逆向、被爬虫、被薅羊毛,才开始补课。
这篇会讲 Android 安全的几个核心层面:
APK 反编译有多容易。
代码混淆、加固、VMP 的基本原理。
本地数据、密钥、通信安全。
签名机制与 APK 完整性校验。
一、APK 反编译:你的代码其实是裸奔的
1.1 反编译三步走
# 1. 解压 APK
unzip app-release.apk -d app/
# 2. 把 classes.dex 转成 jar
d2j-dex2jar app/classes.dex -o app.jar
# 3. 用 JD-GUI 或 jadx 查看源码
jadx app-release.apk -d source/
不到一分钟,你的 Kotlin/Java 代码就差不多能看懂了。
1.2 资源文件也暴露
APK 里的 res/、assets/、AndroidManifest.xml 都是明文或容易还原的。图片、配置文件、密钥文件都能被直接拿走。
1.3 反编译能做什么?
分析你的接口和加密逻辑。
修改代码重新打包(如跳过登录、破解 VIP)。
提取密钥、Token、业务规则。
结论:没有任何手段能 100% 防反编译,安全的目标是提高逆向成本。
二、代码混淆:第一道防线
2.1 ProGuard / R8 做了什么?
缩短类名、方法名、字段名。
删除未使用的代码。
优化字节码。
打乱控制流(R8 有限支持)。
-keep public class com.example.MyApi { *; }
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
2.2 混淆不是加固
混淆只是让代码难读,但逻辑结构还在。配合 jadx 的变量重命名和反编译优化,熟练的逆向工程师仍然能看懂。
2.3 哪些不能混淆?
四大组件(Activity、Service、Receiver、Provider)。
JNI 方法名。
反射调用的类。
序列化/反序列化类。
注解模型类。
-keep class com.example.model.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
三、加固与 VMP:提高逆向成本
3.1 加固基本原理
加固通常包括:
解压原始 dex。
对 dex 加密或压缩。
运行时通过壳代码动态解密并加载。
原始 APK
│
▼
壳程序
│
▼
启动时解密 → 加载原始 dex → 执行
3.2 VMP(虚拟机保护)
VMP 是把关键代码翻译成自定义指令集,运行时用自己的虚拟机解释执行。逆向者需要:
还原自定义指令。
还原虚拟机执行逻辑。
才能理解原始代码。
成本远高于普通加固,但实现复杂、性能有损耗。
3.3 加固方案选型
建议:核心业务上 VMP,普通业务用加固 + 混淆。
四、本地数据安全
4.1 SharedPreferences 不安全
// 明文存储
val prefs = getSharedPreferences("config", Context.MODE_PRIVATE)
prefs.edit().putString("token", userToken).apply()
未 root 的设备上其他应用读不到,但 root 后可以轻松查看。
4.2 用 EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val prefs = EncryptedSharedPreferences.create(
context,
"secret_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
4.3 数据库加密
用 SQLCipher:
val factory = SupportFactory(SQLiteDatabase.getBytes("password".toCharArray()))
val db = Room.databaseBuilder(context, AppDb::class.java, "app.db")
.openHelperFactory(factory)
.build()
4.4 密钥不要硬编码
// 错误
const val API_SECRET = "sk_1234567890abcdef"
硬编码密钥等于白送。正确做法:
用 NDK 把密钥放 C/C++ 层。
用 TEE / 硬件安全模块。
用服务端动态下发 + 白盒加密。
五、通信安全
5.1 HTTPS 不是终点
证书固定(Certificate Pinning)防止中间人攻击。
防止代理抓包(但做不到 100%,只是提高门槛)。
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
5.2 请求签名
关键接口要做请求签名,防止重放攻击和参数篡改:
fun sign(params: Map<String, String>, secret: String): String {
val sorted = params.toSortedMap()
val base = sorted.entries.joinToString("&") { "${it.key}=${it.value}" }
return HmacSHA256(base, secret)
}
服务端校验签名,不一致则拒绝。
5.3 Token 安全
Access Token 有效期短(15 分钟)。
Refresh Token 存 EncryptedSharedPreferences。
关键操作二次验证(短信、指纹、人脸)。
六、签名与完整性校验
6.1 APK 签名机制
Android 要求 APK 必须签名。签名信息包括:
证书公钥。
每个文件摘要。
签名算法。
6.2 校验 APK 签名
fun verifySignature(context: Context): Boolean {
val info = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
val signature = info.signatures[0].toByteArray()
val digest = MessageDigest.getInstance("SHA-256").digest(signature)
return digest.toHex() == "EXPECTED_HASH"
}
6.3 运行时自检
加固方案通常会做运行时自检:
检查是否被二次签名。
检查是否运行在模拟器。
检查是否被调试器附加。
检查 Xposed / Frida 等 Hook 框架。
小结
Android 安全没有银弹,核心是分层防御 + 提高逆向成本:
混淆是底线,R8 必须开。
核心业务上 VMP 或第三方加固。
本地敏感数据用 EncryptedSharedPreferences / SQLCipher。
密钥不要硬编码,通信要 HTTPS + 签名 + 证书固定。
做运行时签名校验和反调试。
下篇预告
下一篇《第 15 篇:从 Android 工程师到移动端架构师》是这个阶段的收官:
技术广度与深度怎么平衡。
架构师的日常工作和核心能力。
如何规划从高级工程师到架构师的成长路径。
参考阅读
Android 官方安全文档
OWASP Mobile Security Testing Guide
R8 / ProGuard 官方文档
上一篇文章:第 13 篇《性能监控框架:从埋点到线上 APM 的闭环》