1. 程式人生 > >寫給Android開發者的混淆使用手冊

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

元素 配置 final 文件 orm ide build tco 方法名

轉自:http://huihui.name/2016/10/23/%E5%86%99%E7%BB%99Android%E5%BC%80%E5%8F%91%E8%80%85%E7%9A%84%E6%B7%B7%E6%B7%86%E4%BD%BF%E7%94%A8%E6%89%8B%E5%86%8C/

寫在前面

大家好,我是光源。

本文首發於我的個人公眾賬號,同時會在個人博客上同步。假如有任何建議還請移步博客點評,同時如果博客本身有修改或勘誤,也會在博客更新。

綜述

毫無疑問,混淆是打包過程中最重要的流程之一,在沒有特殊原因的情況下,所有 app 都應該開啟混淆。

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

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

一、Android混淆最佳實踐

1. 混淆配置

一般情況下,app module 的 build.gradle 文件默認會有如下結構:

1
2
3
4
5
6
7
8
9

android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile(‘proguard-android.txt‘), ‘proguard-rules.pro‘
}
}
}

因為開啟混淆會使編譯時間變長,所以debug模式下不應該開啟。我們需要做的是:

  1. releaseminifyEnabled的值改為true,打開混淆;
  2. 加上shrinkResources true,打開資源壓縮。

修改後文件內容如下:

1
2
3
4
5
6
7
8
9
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile(‘proguard-android.txt‘), ‘proguard-rules.pro‘
}
}
}

2. 自定義混淆規則

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#指定壓縮級別
-optimizationpasses 5

#不跳過非公共的庫的類成員
-dontskipnonpubliclibraryclassmembers

#混淆時采用的算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*

#把混淆類中的方法名也混淆了
-useuniqueclassmembernames

#優化時允許訪問並修改有修飾符的類和類的成員
-allowaccessmodification

#將文件來源重命名為“SourceFile”字符串
-renamesourcefileattribute SourceFile
#保留行號
-keepattributes SourceFile,LineNumberTable
#保持泛型
-keepattributes Signature

#保持所有實現 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 工具通過命令行進行反解,命令是

1
retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

例如:

1
retrace.bat -verbose mapping.txt obfuscated_trace.txt

註意事項:

1) 所有在 AndroidManifest.xml 涉及到的類已經自動被保持,因此不用特意去添加這塊混淆規則。(很多老的混淆文件裏會加,現在已經沒必要)

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

二、混淆簡介

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等等)
  • 不同的構建渠道
  • 項目依賴的第三方庫

合並資源時按照如下優先級順序:

1
依賴 -> main -> 渠道 -> 構建類型

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

同時,如果重復資源在同一層次出現,比如src/main/res/src/main/res2/,則 gradle 無法完成資源合並,這時會報資源合並錯誤。

“移除資源”流程則見名知意,需要註意的是,類似代碼,混淆資源移除也可以定義哪些資源需要被保留,這點在下文給出。

三、自定義混淆規則

在上文“混淆配置”中有這樣一行代碼

1
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. 保持元素不參與混淆的規則

形如:

1
2
3
[保持命令] [類] {
[成員]
}

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

  • 具體的類
  • 訪問修飾符(publicprotectedprivate
  • 通配符*,匹配任意長度字符,但不含包名分隔符(.)
  • 通配符**,匹配任意長度字符,並且包含包名分隔符(.)
  • extends,即可以指定類的基類
  • implement,匹配實現了某接口的類
  • $,內部類

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

  • 匹配所有構造器
  • 匹配所有域
  • 匹配所有方法
  • 通配符*,匹配任意長度字符,但不含包名分隔符(.)
  • 通配符**,匹配任意長度字符,並且包含包名分隔符(.)
  • 通配符***,匹配任意參數類型
  • ,匹配任意長度的任意類型參數。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 這些方法。
  • 訪問修飾符(publicprotectedprivate

舉個例子,假如需要將name.huihui.test包下所有繼承Activitypublic類及其構造函數都保持住,可以這樣寫:

1
2
3
-keep public class name.huihui.test.** extends Android.app.Activity {
<init>
}

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

  • 不混淆某個類

    1
    -keep public class name.huihui.example.Test { *; }
  • 不混淆某個包所有的類

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

    1
    -keep public class * extends name.huihui.example.Test { *; }
  • 不混淆所有類名中包含了“model”的類及其成員

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

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

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

    1
    2
    3
    -keepclassmembers class name.huihui.example.Test { 
    public void test(java.lang.String);
    }
  • 不混淆某個類的內部類

    1
    2
    3
    -keep class name.huihui.example.Test$* {
    *;
    }

四、自定義資源保持規則

1. keep.xml

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

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

  • tools:keep 定義哪些資源需要被保留(資源之間用“,”隔開)
  • tools:discard 定義哪些資源需要被移除(資源之間用“,”隔開)
  • tools:shrinkMode 開啟嚴格模式

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

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

我們可以設置 tools:shrinkModestrict 來開啟嚴格模式,使只有確實被使用的資源被保留。

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

1
2
3
4
5
<?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 屬性來指定需要支持的屬性,例如

1
2
3
4
5
6
android {
defaultConfig {
...
resConfigs "en", "fr"
}
}

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

參考資料

  • Shrink Your Code and Resources
  • proguard
  • Android安全攻防戰,反編譯與混淆技術完全解析(下)
  • Android混淆從入門到精通
  • Android代碼混淆之ProGuard

附錄

  1. proguard-android.txt文件內容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#包名不混合大小寫
-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>(...);
}

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