寫給Android開發者的混淆使用手冊

寫在前面

你們好,我是光源。java

本文首發於個人我的公衆帳號,同時會在我的博客上同步。假若有任何建議還請移步博客點評,同時若是博客自己有修改或勘誤,也會在博客更新。android

綜述

毫無疑問,混淆是打包過程當中最重要的流程之一,在沒有特殊緣由的狀況下,全部 app 都應該開啓混淆。算法

首先,這裏說的的混淆實際上是包括了代碼壓縮、代碼混淆以及資源壓縮等的優化過程。依靠 ProGuard,混淆流程將主項目以及依賴庫中未被使用的類、類成員、方法、屬性移除,這有助於規避64K方法數的瓶頸;同時,將類、類成員、方法重命名爲無心義的簡短名稱,增長了逆向工程的難度。而依靠 Gradle 的 Android 插件,咱們將移除未被使用的資源,能夠有效減少 apk 安裝包大小。json

本文由兩部分構成,第一部分給出混淆的最佳實踐,力求讓零基礎的新手均可以直接使用混淆;第二部分會介紹一下混淆的總體、自定義混淆規則的語法與實踐、自定義資源保持的規則等。安全

1、Android混淆最佳實踐

1. 混淆配置

通常狀況下,app module 的 build.gradle 文件默認會有以下結構:markdown

android {    buildTypes {        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    } }複製代碼android {    buildTypes {        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    } }

由於開啓混淆會使編譯時間變長,因此debug模式下不該該開啓。咱們須要作的是:app

  1. releaseminifyEnabled的值改成true,打開混淆;函數

  2. 加上shrinkResources true,打開資源壓縮。工具

修改後文件內容以下:佈局

android {    buildTypes {        release {            minifyEnabled true            shrinkResources true            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    } }複製代碼android {    buildTypes {        release {            minifyEnabled true            shrinkResources true            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    } }

2. 自定義混淆規則

app module 下默認生成了項目的自定義混淆規則文件 proguard-rules.pro,多方調研後,一份適用於大部分項目的混淆規則最佳實踐以下:

#指定壓縮級別 -optimizationpasses 5 #不跳過非公共的庫的類成員 -dontskipnonpubliclibraryclassmembers #混淆時採用的算法 -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* #把混淆類中的方法名也混淆了 -useuniqueclassmembernames #優化時容許訪問並修改有修飾符的類和類的成員 -allowaccessmodification #將文件來源重命名爲「SourceFile」字符串 -renamesourcefileattribute SourceFile #保留行號 -keepattributes SourceFile,LineNumberTable #保持全部實現 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(); } #Fragment不須要在AndroidManifest.xml中註冊,須要額外保護下 -keep public class * extends android.support.v4.app.Fragment -keep public class * extends android.app.Fragment # 保持測試相關的代碼 -dontnote junit.framework.** -dontnote junit.runner.** -dontwarn android.test.** -dontwarn android.support.test.** -dontwarn org.junit.**複製代碼#指定壓縮級別 -optimizationpasses 5 #不跳過非公共的庫的類成員 -dontskipnonpubliclibraryclassmembers #混淆時採用的算法 -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* #把混淆類中的方法名也混淆了 -useuniqueclassmembernames #優化時容許訪問並修改有修飾符的類和類的成員 -allowaccessmodification #將文件來源重命名爲「SourceFile」字符串 -renamesourcefileattribute SourceFile #保留行號 -keepattributes SourceFile,LineNumberTable #保持全部實現 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(); } #Fragment不須要在AndroidManifest.xml中註冊,須要額外保護下 -keep public class * extends android.support.v4.app.Fragment -keep public class * extends android.app.Fragment # 保持測試相關的代碼 -dontnote junit.framework.** -dontnote junit.runner.** -dontwarn android.test.** -dontwarn android.support.test.** -dontwarn org.junit.**

真正通用的、須要添加的就是上面這些,除此以外,須要每一個項目根據自身的需求添加一些混淆規則:

  • 第三方庫所需的混淆規則。正規的第三方庫通常都會在接入文檔中寫好所需混淆規則,使用時注意添加。

  • 在運行時動態改變的代碼,例如反射。比較典型的例子就是會與 json 相互轉換的實體類。假如項目命名規範要求實體類都要放在model包下的話,能夠添加相似這樣的代碼把全部實體類都保持住: -keep public class **.*Model*.** {*;}

  • JNI中調用的類。

  • WebViewJavaScript調用的方法

  • Layout佈局使用的 View構造函數、android:onClick等。

3. 檢查混淆結果

混淆過的包必須進行檢查,避免因混淆引入的bug。

一方面,須要從代碼層面檢查。使用上文的配置進行混淆打包後在 <module-name>/build/outputs/mapping/release/ 目錄下會輸出如下文件:

  • dump.txt描述APK文件中全部類的內部結構

  • mapping.txt提供混淆先後類、方法、類成員等的對照表

  • seeds.txt列出沒有被混淆的類和成員

  • usage.txt列出被移除的代碼

咱們能夠根據 seeds.txt 文件檢查未被混淆的類和成員中是否已包含全部指望保留的,再根據 usage.txt 文件查看是否有被誤移除的代碼。

另外一方面,須要從測試方面檢查。將混淆過的包進行全方面測試,檢查是否有 bug 產生。

4. 解出混淆棧

混淆後的類、方法名等等難以閱讀,這當然會增長逆向工程的難度,但對追蹤線上 crash 也形成了阻礙。咱們拿到 crash 的堆棧信息後會發現很難定位,這時須要將混淆反解。

<sdk-root>/tools/proguard/  路徑下有附帶的的反解工具(Window 系統爲 proguardgui.bat,Mac 或 Linux 系統爲 proguardgui.sh)。

這裏以 Window 平臺爲例。雙擊運行 proguardgui.bat 後,能夠看到左側的一行菜單。點擊 ReTrace,選擇該混淆包對應的 mapping 文件(混淆後在 <module-name>/build/outputs/mapping/release/ 路徑下會生成 mapping.txt 文件,它的做用是提供混淆先後類、方法、類成員等的對照表),再將 crash 的 stack trace 黏貼進輸入框中,點擊右下角的 ReTrace ,混淆後的堆棧信息就顯示出來了。

以上使用 GUI 程序進行操做,另外一種方式是利用該路徑下的 retrace 工具經過命令行進行反解,命令是

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]複製代碼retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

例如:

retrace.bat -verbose mapping.txt obfuscated_trace.txt複製代碼retrace.bat -verbose mapping.txt obfuscated_trace.txt

注意事項:

1) 全部在 AndroidManifest.xml 涉及到的類已經自動被保持,所以不用特地去添加這塊混淆規則。(不少老的混淆文件裏會加,如今已經不必)

2) proguard-android.txt 已經存在一些默認混淆規則,不必在 proguard-rules.pro 重複添加,該文件具體規則見附錄1:

2、混淆簡介

Android中的「混淆」能夠分爲兩部分,一部分是 Java 代碼的優化與混淆,依靠 proguard 混淆器來實現;另外一部分是資源壓縮,將移除項目及依賴的庫中未被使用的資源(資源壓縮嚴格意義上跟混淆沒啥關係,但通常咱們都會放一塊兒講)。

1. 代碼壓縮

代碼混淆是包含了代碼壓縮、優化、混淆等一系列行爲的過程。如上圖所示,混淆過程會有以下幾個功能:

  1. 壓縮。移除無效的類、類成員、方法、屬性等;

  2. 優化。分析和優化方法的二進制代碼;根據proguard-android-optimize.txt中的描述,優化可能會形成一些潛在風險,不能保證在全部版本的Dalvik上都正常運行。

  3. 混淆。把類名、屬性名、方法名替換爲簡短且無心義的名稱;

  4. 預校驗。添加預校驗信息。這個預校驗是做用在Java平臺上的,Android平臺上不須要這項功能,去掉以後還能夠加快混淆速度。

這四個流程默認開啓。

在 Android 項目中咱們能夠選擇將「優化」和「預校驗」關閉,對應命令是-dontoptimize-dontpreverify(固然,默認的 proguard-android.txt 文件已包含這兩條混淆命令,不須要開發者額外配置)。

2. 資源壓縮

資源壓縮將移除項目及依賴的庫中未被使用的資源,這在減小 apk 包體積上會有不錯的效果,通常建議開啓。具體作法是在 build.grade 文件中,將 shrinkResources 屬性設置爲 true。須要注意的是,只有在用minifyEnabled true開啓了代碼壓縮後,資源壓縮纔會生效

資源壓縮包含了「合併資源」和「移除資源」兩個流程。

「合併資源」流程中,名稱相同的資源被視爲重複資源會被合併。須要注意的是,這一流程不受shrinkResources屬性控制,也沒法被禁止, gradle 必然會作這項工做,由於假如不一樣項目中存在相同名稱的資源將致使錯誤。gradle 在四處地方尋找重複資源:

  • src/main/res/ 路徑

  • 不一樣的構建類型(debug、release等等)

  • 不一樣的構建渠道

  • 項目依賴的第三方庫

合併資源時按照以下優先級順序:

依賴 -> main -> 渠道 -> 構建類型複製代碼依賴 -> main -> 渠道 -> 構建類型

舉個例子,假如重複資源同時存在於main文件夾和不一樣渠道中,gradle 會選擇保留渠道中的資源。

同時,若是重複資源在同一層次出現,好比src/main/res/src/main/res/,則 gradle 沒法完成資源合併,這時會報資源合併錯誤。

「移除資源」流程則見名知意,須要注意的是,相似代碼,混淆資源移除也能夠定義哪些資源須要被保留,這點在下文給出。

3、自定義混淆規則

在上文「混淆配置」中有這樣一行代碼

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'複製代碼proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

這行代碼定義了混淆規則由兩部分構成:位於 SDK 的 tools/proguard/ 文件夾中的 proguard-android.txt 的內容以及默認放置於模塊根目錄的 proguard-rules.pro 的內容。前者是 SDK 提供的默認混淆文件(內容見附錄1),後者是開發者自定義混淆規則的地方。

1. 常見混淆命令:

  • optimizationpasses

  • dontoptimize

  • dontusemixedcaseclassnames

  • dontskipnonpubliclibraryclasses

  • dontpreverify

  • dontwarn

  • verbose

  • optimizations

  • keep

  • keepnames

  • keepclassmembers

  • keepclassmembernames

  • keepclasseswithmembers

  • keepclasseswithmembernames

在第一部分 Android 混淆最佳實踐中已介紹部分須要使用到的混淆命令,這裏再也不贅述,詳情請查閱官網。須要特別介紹的是與保持相關元素不參與混淆的規則相關的幾種命令:

命令 做用
-keep 防止類和成員被移除或者被重命名
-keepnames 防止類和成員被重命名
-keepclassmembers 防止成員被移除或者被重命名
-keepnames 防止成員被重命名
-keepclasseswithmembers 防止擁有該成員的類和成員被移除或者被重命名
-keepclasseswithmembernames 防止擁有該成員的類和成員被重命名

2. 保持元素不參與混淆的規則

形如:

[保持命令] [類] {    [成員] }複製代碼[保持命令] [類] {    [成員] }

「類」表明類相關的限定條件,它將最終定位到某些符合該限定條件的類。它的內容可使用:

  • 具體的類

  • 訪問修飾符(publicprotectedprivate

  • 通配符*,匹配任意長度字符,但不含包名分隔符(.)

  • 通配符**,匹配任意長度字符,而且包含包名分隔符(.)

  • extends,便可以指定類的基類

  • implement,匹配實現了某接口的類

  • $,內部類

「成員」表明類成員相關的限定條件,它將最終定位到某些符合該限定條件的類成員。它的內容可使用:

  • 匹配全部構造器
  • 匹配全部域
  • 匹配全部方法
  • 通配符*,匹配任意長度字符,但不含包名分隔符(.)

  • 通配符**,匹配任意長度字符,而且包含包名分隔符(.)

  • 通配符***,匹配任意參數類型

  • ,匹配任意長度的任意類型參數。好比void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 這些方法。

  • 訪問修飾符(publicprotectedprivate

舉個例子,假如須要將name.huihui.test包下全部繼承 Activitypublic類及其構造函數都保持住,能夠這樣寫:

-keep public class name.huihui.test.** extends Android.app.Activity {    <init> }複製代碼-keep public class name.huihui.test.** extends Android.app.Activity {    <init> }

3. 經常使用的自定義混淆規則

  • 不混淆某個類

    -keep public class name.huihui.example.Test { *; }複製代碼-keep public class name.huihui.example.Test { *; }
  • 不混淆某個包全部的類

    -keep class name.huihui.test.** { *; }複製代碼-keep class name.huihui.test.** { *; }
  • 不混淆某個類的子類

    -keep public class * extends name.huihui.example.Test { *; }複製代碼-keep public class * extends name.huihui.example.Test { *; }
  • 不混淆全部類名中包含了「model」的類及其成員

    -keep public class **.*model*.** {*;}複製代碼-keep public class **.*model*.** {*;}
  • 不混淆某個接口的實現

    -keep class * implements name.huihui.example.TestInterface { *; }複製代碼-keep class * implements name.huihui.example.TestInterface { *; }
  • 不混淆某個類的構造方法

    -keepclassmembers class name.huihui.example.Test {  public <init>(); }複製代碼-keepclassmembers class name.huihui.example.Test {  public <init>(); }
  • 不混淆某個類的特定的方法

    -keepclassmembers class name.huihui.example.Test {  public void test(java.lang.String); }複製代碼-keepclassmembers class name.huihui.example.Test {  public void test(java.lang.String); }

4、自定義資源保持規則

1. keep.xml

shrinkResources true開啓資源壓縮後,全部未被使用的資源默認被移除。假如你須要定義哪些資源必須被保留,在 res/raw/ 路徑下建立一個 xml 文件,例如 keep.xml

經過一些屬性的設置能夠實現定義資源保持的需求,可配置的屬性有:

  • tools:keep 定義哪些資源須要被保留(資源之間用「,」隔開)

  • tools:discard 定義哪些資源須要被移除(資源之間用「,」隔開)

  • tools:shrinkMode 開啓嚴格模式

當代碼中經過 Resources.getIdentifier()  用動態的字符串來獲取並使用資源時,普通的資源引用檢查就可能會有問題。例如,以下代碼會致使全部以「img_」開頭的資源都被標記爲已使用。

String name = String.format("img_%1d", angle + 1); res = getResources().getIdentifier(name, "drawable", getPackageName());複製代碼String name = String.format("img_%1d", angle + 1); res = getResources().getIdentifier(name, "drawable", getPackageName());

咱們能夠設置 tools:shrinkModestrict 來開啓嚴格模式,使只有確實被使用的資源被保留。

以上就是自定義資源保持規則相關的配置,舉個例子:

<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools"    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"    tools:discard="@layout/unused2"    tools:shrinkMode="strict"/>複製代碼<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools"    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"    tools:discard="@layout/unused2"    tools:shrinkMode="strict"/>

2. 移除替代資源

一些替代資源,例如多語言支持的 strings.xml,多分辨率支持的 layout.xml 等,在咱們不須要使用又不想刪除掉時,可使用資源壓縮將它們移除。

咱們使用 resConfig 屬性來指定須要支持的屬性,例如

android {    defaultConfig {        ...        resConfigs "en", "fr"    } }複製代碼android {    defaultConfig {        ...        resConfigs "en", "fr"    } }

其餘未顯式聲明的語言資源將被移除。

參考資料

  • Shrink Your Code and Resources

  • proguard

  • Android安全攻防戰,反編譯與混淆技術徹底解析(下)

  • Android混淆從入門到精通

  • Android代碼混淆之ProGuard

附錄

  1. proguard-android.txt文件內容

#包名不混合大小寫 -dontusemixedcaseclassnames #不跳過非公共的庫的類 -dontskipnonpubliclibraryclasses #混淆時記錄日誌 -verbose #關閉預校驗 -dontpreverify #不優化輸入的類文件 -dontoptimize #保護註解 -keepattributes *Annotation* #保持全部擁有本地方法的類名及本地方法名 -keepclasseswithmembernames class * {    native <methods>; } #保持自定義View的get和set相關方法 -keepclassmembers public class * extends android.view.View {   void set*(***);   *** get*(); } #保持Activity中View及其子類入參的方法 -keepclassmembers class * extends android.app.Activity {   public void *(android.view.View); } #枚舉 -keepclassmembers enum * {    **[] $VALUES;    public *; } #Parcelable -keepclassmembers class * implements android.os.Parcelable {  public static final android.os.Parcelable$Creator CREATOR; } #R文件的靜態成員 -keepclassmembers class **.R$* {    public static <fields>; } -dontwarn android.support.** #keep相關注解 -keep class android.support.annotation.Keep -keep @android.support.annotation.Keep class * {*;} -keepclasseswithmembers class * {    @android.support.annotation.Keep <methods>; } -keepclasseswithmembers class * {    @android.support.annotation.Keep <fields>; } -keepclasseswithmembers class * {    @android.support.annotation.Keep <init>(...); }複製代碼#包名不混合大小寫 -dontusemixedcaseclassnames #不跳過非公共的庫的類 -dontskipnonpubliclibraryclasses #混淆時記錄日誌 -verbose #關閉預校驗 -dontpreverify #不優化輸入的類文件 -dontoptimize #保護註解 -keepattributes *Annotation* #保持全部擁有本地方法的類名及本地方法名 -keepclasseswithmembernames class * {    native <methods>; } #保持自定義View的get和set相關方法 -keepclassmembers public class * extends android.view.View {   void set*(***);   *** get*(); } #保持Activity中View及其子類入參的方法 -keepclassmembers class * extends android.app.Activity {   public void *(android.view.View); } #枚舉 -keepclassmembers enum * {    **[] $VALUES;    public *; } #Parcelable -keepclassmembers class * implements android.os.Parcelable {  public static final android.os.Parcelable$Creator CREATOR; } #R文件的靜態成員 -keepclassmembers class **.R$* {    public static <fields>; } -dontwarn android.support.** #keep相關注解 -keep class android.support.annotation.Keep -keep @android.support.annotation.Keep class * {*;} -keepclasseswithmembers class * {    @android.support.annotation.Keep <methods>; } -keepclasseswithmembers class * {    @android.support.annotation.Keep <fields>; } -keepclasseswithmembers class * {    @android.support.annotation.Keep <init>(...); }

寫在最後

十分感謝你能看到最後,因本人水平有限,若有錯漏還請不吝賜教,能夠直接回復公衆號或者去博客底下評論,在此先行謝過。

同時,假若有這方面的困惑或者討論,也十分歡迎聯繫我。

相關文章
相關標籤/搜索