概述
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應用本身是利大於弊的。