概述
Android代码混淆是让Android项目避免轻易被逆向分析,防止代码安全泄露的手段之一。它将工程中的Android代码用简单抽象的字母或单词代替原有的代码名称。使代码丧失可读性从而使逆向工程师难以阅读,增加逆向成本。当逆向成本大于逆向收益的时候,逆向代码也就失去意义。
除此之外,由于代码混淆用简单抽象的单词代替原有长而通俗易懂的代码,因而减少APK的体积。而且,使用代码混淆后,利用Gradle为Android提供的插件,能将项目中未使用的资源安全移除,大大减少APK体积。
Android SDK 自带了混淆工具Proguard。它位于SDK根目录\tools\proguard下面。如果开启了混淆,Proguard默认情况下会对所有代码,包括第三方包都进行混淆,可是有些代码或者第三方包是不能混淆的,这就需要我们手动编写混淆规则来保持不能被混淆的部分。
我们使用Jadx工具反编译APK并查看源码,然后比较下APK进行代码混淆和不进行代码混淆的效果。首先来看看未进行代码混淆的时候:
接下来看看对该APK进行代码混淆(如何混淆一会说)之后,Jadx反编译出来的代码情况:
代码混淆
了解了代码混淆的效果和作用,我们来看看如何实现混淆。
开启混淆
使用Android Studio正式打包时默认是不开启代码混淆的,如果需要开启代码混淆,可以在 app 模块下的build.gradle
文件中修改 minifyEnabled false
为 true。代码结构如下:
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.wiwide.soldout"
minSdkVersion 19
targetSdkVersion 25
versionCode 6
versionName "2.0.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// 是否进行混淆
minifyEnabled true
// 混淆文件的位置
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
···
}
}
}
这段代码中,minifyEnabled
就是我们开启代码混淆的开关,系统默认为false,既不开启代码混淆。proguardFiles是指定项目中所使用的混淆规则配置文件。其中getDefaultProguardFile(‘proguard-android.txt’)
是指定系统默认的混淆规则,而proguard-rules.pro
则代表当前项目的混淆规则。
proguard-android.txt
位于Android SDK目录下/tools/proguard目录下,proguard-rules.pro
位于app module下。一般而言,通过修改proguard-rules.pro
来改变当前项目的混淆规则。此外,通过添加shrinkResources true
能将项目中未使用的资源移除。
【说明】代码混淆后会产生如下几个位于< module-name >/build/outputs/mapping/release/目录下的文件:
文件名 | 说明 |
---|---|
dump.txt | 说明 APK 中所有类文件的内部结构。 |
mapping.txt | 提供原始与混淆过的类、方法和字段名称之间的转换。 |
seeds.txt | 列出未进行混淆的类和成员。 |
usage.txt | 列出从 APK 移除的代码。 |
【注意】一般而言,项目的debug版本无需开启代码混淆,因为代码混淆会额外增加apk的构建时间。但是,用于测试的版本也要与生产版本一样开启代码混淆,以免在生产版本出现测试版本未曾出现的bug。
配置混淆
一般而言,默认 ProGuard 配置文件 (proguard-android.txt) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。但是,ProGuard可能会移除应用真正需要的代码。举例来说,它可能错误移除代码的情况包括:例如:
- 当应用引用的类只来自 AndroidManifest.xml 文件时;
- 当应用调用的方法来自 Java 原生接口 (JNI) 时;
- 当应用在运行时(例如使用反射或自检)操作代码时。
测试应用应该能够发现因不当移除的代码而导致的错误,但您也可以通过查看 < module-name >/build/outputs/mapping/release/
中保存的 usage.txt 输出文件来检查移除了哪些代码。
默认配置
学习自定义proguard-rules.pro之前,我们先来看看系统默认的代码混淆规则proguard-android.txt文件。假如proguard-rules.pro文件没用配置任何混淆规则,系统将以proguard-android.txt里的规则为准开启混淆。
#混淆时不使用大小写混合类名
-dontusemixedcaseclassnames
#不跳过library中非public的类
-dontskipnonpubliclibraryclasses
#打印混淆的详细信息
-verbose
# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
#不进行优化
-dontoptimize
#不进行预校验,能加快混淆速度
-dontpreverify
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.
#不混淆注解中的参数
-keepattributes *Annotation*
#不混淆特定的类
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
#不混淆包含native方法的类和native方法
-keepclasseswithmembernames class * {
native <methods>;
}
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
#不混淆继承View类的getXX()和setXX()方法以保证属性动画正常运行。
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick
#不混淆Activity中public void修饰的含有View参数的方法以保证在xml配置的onclick正常使用
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
#不混淆枚举类的values()和valueOf()方法
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
#不混淆实现Parcelable接口的类的CREATOR成员
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
#不混淆R文件的所有public类成员,如android.R类
-keepclassmembers class **.R$* {
public static <fields>;
}
# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
#不对android.support包下的代码警告。例如,app使用了低于某些类的support包版本导致出现警告问题。
-dontwarn android.support.**
# Understand the @Keep support annotation.
#不混淆Keep注解
-keep class android.support.annotation.Keep
#不混淆带Keep注解的类
-keep @android.support.annotation.Keep class * {*;}
#如果类的方法使用了Keep注解,不混淆类与类方法
-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}
#如果类的属性使用了Keep注解,不混淆类与类成员
-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}
#如果类的构造方法使用了Keep注解,不混淆类与类成员
-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}
通过proguard-android.txt文件,我们大致知道了当开启混淆时,系统默认的混淆规则,当自定义proguard-rule.pro文件时我们就无需重复以上的混淆规则。此外,通过proguard-android.txt我们大致知道如何自定义混淆规则。
配置规则
1、关于keep关键词:
关键词 | 说明 |
---|---|
keep | 不混淆类和类成员 |
keepnames | 不混淆类和类成员,但类和类成员未被引用会被移除 |
keepclassmembers | 不混淆类成员 |
keepclassmembernames | 不混淆类成员,但未被引用的类成员会被移除 |
keepclasseswithmembers | 不混淆带有指定条件类成员的类和类成员 |
keepclasseswithmembernames | 不混淆带有指定条件的类和类成员,但未被引用的类和类成员会被移除 |
或者:
2、相关通配符
通配符 | 说明 |
---|---|
* | 匹配任意长度字符,但不包含包名分隔符”.” |
** | 匹配任意长度字符,但包含包名分隔符”.” |
*** | 匹配任意参数类型。例如*** getSchool(***)可匹配String getSchool(String) |
… | 匹配任意长度的任意类型的参数。例如void setSchool(…)可匹配void setSchool(String name,int age) |
< filed > | 匹配类或接口中所有字段 |
< method > | 匹配类、接口的所有方法 |
< init > | 匹配类中所有构造方法 |
3、具体解析
先看如下两个比较常用的命令,很多童鞋可能会比较迷惑以下两者的区别。
-keep class cn.hadcn.test.**
-keep class cn.hadcn.test.*
- 一颗星
表示只是保持该包下的类名,而子包下的类名还是会被混淆; - 两颗星
表示把本包和所含子包下的类名都保持;用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了。 - 这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了:
-keep class cn.hadcn.test.* {*;}
在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extends,implements等这些Java规则。如下例子就避免所有继承Activity的类被混淆:
-keep public class * extends android.app.Activity
如果我们要保留一个类中的内部类不被混淆则需要用$符号,如下例子表示保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。
-keepclassmembers class cc.ninty.chat.ui.fragment.ScriptFragment$JavaScriptInterface {
public *;
}
到这里对混淆配置文件的语法已经基本了解,系统的proguard-android.txt已经为我们完成大部分基础的混淆配置工作。我们只需要写好自己项目的proguard-rules.pro文件,对照 proguard-android.txt照葫芦画瓢就能自定义自己项目的 proguard-rules.pro。
对于开启混淆的项目,我们应该从类的创建就要思考这个类是否混淆,大到一个模块,小到一个类的字段,以免到项目庞大以后再来思考混淆就更加难办了。例如,当项目使用了反射机制,对于反射的类及其成员要根据实际情况避免混淆。又例如Gson所要解析成的实体类的字段也要避免混淆。
总结
代码混淆也是一项很有难度的事情,随着项目越来越庞大,混淆的难度也越来越大。一个错误的混淆很可能导致整个APP无法正常运行。因此,学习混淆应该循序渐进,积累更多经验。
通过代码混淆增加逆向的难度和减少APK的体积是成熟的Android应用应该具备的,同时也会增加开发者的开发难度。对于要不要开启代码混淆,应该从项目本身实际情况(成本、收益、开发难度)而定,不要盲目为了混淆而开启混淆项目的可控性下降,当然如果开发人员拥有过硬的混淆代码配置经验,代码混淆对于APP应用本身是利大于弊的。