Android应用防护
本文由 Luzhuo 编写,转发请保留该信息.
原文: https://blog.csdn.net/Rozol/article/details/89262879
混淆
混淆主要作用是减少apk的大小.
代码混淆
参考资料: https://developer.android.com/studio/build/shrink-code.html
混淆会使编译时间变长, 所以只在release模式下开启即可.
android {
buildTypes {
release {
minifyEnabled true // 打开混淆
shrinkResources true // 打开资源压缩
zipAlignEnabled true // 让资源4字节对齐, 以减少内存消耗
buildConfigField "boolean", "LOG_DEBUG", "false" // 不显示log
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
在proguard-rules.pro
添加规则
-keep class me.luzhuo.idaso.MainActivity {*;}
混淆之后(ProGuard)会生成以后文件:
- dump.txt
- 说明 APK 中所有类文件的内部结构。
- mapping.txt
- 提供原始与混淆过的类、方法和字段名称之间的转换。(可反推回为原本的代码, 每次发布均需保留一个)
- seeds.txt
- 列出未进行混淆的类和成员。
- usage.txt
- 列出从 APK 移除的代码。
- 这些文件保存在
<module-name>/build/outputs/mapping/release/
中
混淆规则
-
*
和**
的区别-keep class me.luzhuo.idaso.* # 只保持 当前包 下的 -keep class me.luzhuo.idaso.** ## 保持 当前包 及其 所有子包 下的
-
保持 整个类 不被混淆
-keep class me.luzhuo.idaso.MainActivity {*;}
-
保持 接口的实现 不被混淆
-keep class * implements me.luzhuo.JavaScriptInterface {*;}
-
保持 整个包所有类 不被混淆
-keep class me.luzhuo.idaso.** {*;}
-
保持 继承类 不被混淆
-keep public class * extends android.app.Activity # 保持所有继承 android.app.Activity 的类
-
保持 内部类 不被混淆
-keepclassmembers class me.luzhuo.MainFragment$JavaScriptInterface { public *; } # 保持MainFragment的内部类JavaScriptInterface所有public内容 -keepclassmembers class me.luzhuo.MainFragment$* { *; } # 保持MainFragment的所有内部类
-
保持某类下 指定内容
-keep class me.luzhuo.MainActivity { <init>; // 所有构造器 <fields>; // 所有域 <methods>; // 所有方法 public <init>(int, int); // 指定构造器 }
-
保持某些指定 类型内容
-keepclassmembers class me.luzhuo.MainActivity { // public / private / protected public <methods>; // 所有public方法 } -keepclassmembers class me.luzhuo.MainActivity { public void show(java.lang.String) // 指定方法 }
-
保持某类指定 参数内容
-keep class me.luzhuo.MainActivity { public <init>(org.json.JSONObject); }
-
各种保持(keep)的区别
保持内容 | 保持代码和名字 | 保持名字 |
---|---|---|
类名 + 类成员 | -keep | -keepnames |
类成员 | -keepclassmembers | -keepclassmembernames |
若有某成员, 则类名+类成员 | -keepclasseswithmembers | -keepclasseswithmembernames |
举个栗子:
# 保持 native 方法不被混淆(jni要求类名和方法名均不能被混淆)
-keepclasseswithmembernames class * {
native <methods>;
}
完整混淆配置清单
# 迭代优化次数
-optimizationpasses 5
# 混淆算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 不使用大小写混合, 全为小写
-dontusemixedcaseclassnames
# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses
# 不做预校验, 快混淆速度
-dontpreverify
-verbose
-renamesourcefileattribute SourceFile
# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
-dontnote android.support.**
-dontwarn android.support.**
# 保持Annotation
-keepattributes *Annotation*,InnerClasses
# 保持 Annotation、内部类、泛型、匿名类
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod
# 重命名异常时记录的文件名称
# 保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**
# 保持R的资源
-keep class **.R$* {*;}
# 保留四大组件、Application等这些类不被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService
# 保持onClick方法
-keepclassmembers class * extends android.app.Activity{
public void *(android.view.View);
}
# 保持回调函数不被混淆
-keepclassmembers class * {
void *(**On*Event);
void *(**On*Listener);
}
# 保持native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保持枚不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保持Parcelable序列化不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# 保持Serializable序列化不被混淆
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保持Keep注解不被混淆
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
@android.support.annotation.Keep *;
}
# 删除android.util.Log的所有输出日志
-assumenosideeffects class android.util.Log {
public static *** v(...);
public static *** d(...);
public static *** i(...);
public static *** w(...);
public static *** e(...);
}
# ------ 第三方库要求的不被混淆 ------
资源混淆
开源库地址: https://github.com/shwenzhang/AndResGuard
AndResGuard主要帮你缩小apk大小, 例如将res/drawable/wechat
变为r/d/a
。
在Project的build.gradle里添加以下配置:
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.15'
}
}
在app下创建and_res_guard.gradle
文件, 并copy以下内容做修改:
apply plugin: 'AndResGuard'
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
use7zip = true
useSign = true
// 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
keepRoot = false
// 指定保持不被混淆的资源列表, 其他见 https://github.com/shwenzhang/AndResGuard/blob/master/doc/white_list.md
whiteList = [
// for your icon
"R.drawable.icon",
// for fabric
"R.string.com.crashlytics.*",
// for google-services
"R.string.google_app_id",
"R.string.gcm_defaultSenderId",
"R.string.default_web_client_id",
"R.string.ga_trackingId",
"R.string.firebase_database_url",
"R.string.google_api_key",
"R.string.google_crash_reporting_api_key"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.15'
//path = "/usr/local/bin/7za"
}
/**
* 可选: 如果不设置则会默认覆盖assemble输出的apk
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* 可选: 指定v1签名时生成jar文件的摘要算法
* 默认值为“SHA-1”
**/
// digestalg = "SHA-256"
}
在App下的build.gradle里添加
apply from: 'and_res_guard.gradle'
生成包: 打开右边Gradle, 需要生成Release版, 双击resguardRelease即可, 会在/build/output/apk/release/AndResGuard_{apk_name}/
生成相应的应用.
注:
- 需要配置签名才能正常打包, 这点很让人觉得讨厌.
- 若配置了代码混淆, 打包发布后便是 Code + Res 都混淆.
生成文件如下:
File | Description |
---|---|
resources.arsc | 修改后的resource.arsc文件 |
resource_mapping_output.txt | 资源混淆的mapping文件 |
packagename_unsigned.apk | 未签名apk |
packagename_signed.apk | sign模式, 已签名apk |
packagename_signed_7zip.apk | 7z重打包, 已签名apk |
packagename_signed_aligned.apk | sign模式, 对齐的, 已签名apk(可发布) |
packagename_signed_7zip_aligned.apk | 7z重打包, 对齐的, 已签名apk(可发布) |
配置签名信息:
在app的build.gradle里配置
android {
signingConfigs {
myConfig {
keyAlias 'key0'
keyPassword '***'
storeFile file('C:\\apk\\case.key.jks')
storePassword '***'
}
}
buildTypes {
release {
// ...
signingConfig signingConfigs.myConfig // 坑爹的AndRedGuard必须要签名(若不用则不需), 否则编译失败
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
签名保护
众所周知, 每个应用必须有一个唯一的签名, 没被签名的应用将不允许被安装.
为了防止被二次打包, 我们需要做个签名验证, 签名不对就退出应用.
1.编写获取应用的指纹
public class SignatureVerifyUtils {
// 正确的签名指纹的MD5
private static final String APP_SIGN_MD5 = "2A:55:B2:A8:81:95:5C:6F:60:2F:AD:D8:5D:BE:49:C7";
/**
* 获取应用签名
*/
public static String getSignatureMD5(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
byte[] cert = info.signatures[0].toByteArray();
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] publicKey = md.digest(cert);
StringBuilder hexString = new StringBuilder();
for (int i = 0; i < publicKey.length; i++) {
String appendString = Integer.toHexString(0xFF & publicKey[i]).toUpperCase(Locale.US);
if (appendString.length() == 1)
hexString.append("0");
hexString.append(appendString);
hexString.append(":");
}
hexString.deleteCharAt(hexString.length() - 1);
return hexString.toString();
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
// 对比签名
public static boolean isOwnApp(Context context){
return APP_SIGN_MD5.equals(getSignatureMD5(context));
}
}
2.启动应用时进行指纹比较, 指纹不对就停止运行
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// --- 签名验证 ---
// Java方式
if (!SignatureVerifyUtils.isOwnApp(getApplicationContext())) {
Process.killProcess(Process.myPid());
}
// JNI方式(略)
}
}
jni的方式主要是native层调用getSignatureMD5()获取指纹, 并且在native层完成比较, 如果对不上则exit(0);
退出.
获取签名文件的指纹
以上应用能获取到指纹, 那么我们如果从应用外获取签名的指纹呢.
1.通过命名方式
C:\Users\LZLuz>keytool -list -v -keystore C:\apk\case.key.jks
输入密钥库口令:
2.AndroidStudio自动的Gralde工具库, 运行signingReport.
3.使用jadx-gui工具打开apk包
4.把apk包下META-INF
文件夹里的CERT.RSA
拷贝出来, 执行以下命令查看
C:\Users\LZLuz>keytool -printcert -file C:\apk\KEY0.RSA
反调试检测
主要是检测/proc/[mypid]/status
里的TracerPid
字段的值是否为0, 不为0说明被其他进程调试, 则结束应用.
当程序运行以下代码时: 程序加载.so文件时, 会执行JNI_OnLoad函数; 卸载.so文件时, 会执行JNI_OnUnload函数.
static {
System.loadLibrary("native-lib");
}
所以, 我们要在JNI_OnLoad函数里去创建线程, 在子线程里去判断TracerPid字段的值, 代码如下:
#include <