Android修煉之混淆

奇舞移動發表於2018-11-28

自嘲時刻

作為Java和Android開發者,大家應該都對混淆很熟悉了。網上也有各路大神提供的混淆模板,基本上直接拿來用就好。但我還是想捋一捋,因為工作中被混淆這傢伙“玩弄”了好幾次,必須把它記在小本本上。

介紹

基本概念

混淆,字面上來說就是把專案中的包名、類名、方法名和變數名等進行更改,用以迷惑別人。但混淆其實包含了程式碼壓縮、優化、校驗等過程,把混淆稱作ProGuard更合適。

ProGuard

PS:ProGuard官網
stuff.mit.edu/afs/sipb/pr…

ProGuard就是Java對Class檔案進行“混淆”的工具。直接貼圖吧:

ProGuard過程
(看到這個圖,大家有沒有想到什麼?——設計模式中的責任鏈模式。)

  1. shrink(壓縮):ProGuard會遞迴地確定哪些類和類成員被使用,而其他的則被丟棄。
  2. optimize(優化):ProGuard會進一步分析和優化方法。比如一些無用的引數會被丟棄,一些方法會做內聯。
  3. obfuscate(混淆):這個過程就是進行重新命名了,把原來包含註釋意義的類名、方法名等進行無意義重新命名。
  4. preverify(預校驗):這個步驟是將預校驗資訊新增到類中。

這四個步驟其實都是可選的。當然,Android中一般情況下我們都會保留前三個步驟,而忽略preverify過程,這樣可以加快混淆速度。

Android中的ProGuard

Android中預設整合了ProGuard工具,在sdk目錄的/tools/proguard中。混淆開啟方法:

android {
    ...
    // AS自動生成
    buildTypes {
        release {
            // 混淆開關
            minifyEnabled true
            // 移除無用的resource檔案
            shrinkResources true
            // proguard-android.txt表示預設的混淆規則
            // proguard-rules.pro表示自定義的混淆規則(檔名和字尾可以修改)
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
複製程式碼

預設的proguard-android.txt檔案在sdk目錄/tools/proguard中。該目錄下還有個proguard-android-optimize.txt檔案。而我們自定義的proguard-rules.pro檔案中有部分基礎混淆規則就是來自proguard-android-optimize.txt

混淆語法

混淆的語法都可以在上述的ProGuard官網中找到,這裡只介紹一些常用的語法規則。

1、保留類和類成員

保留 反之被刪除或重新命名 防止被重新命名
類和類成員 -keep -keepnames
僅類成員 -keepclassmembers -keepclassmembernames
如果擁有某成員,保留類和類成員 -keepclasseswithmembers -keepclasseswithmembernames

2、類成員中的一些符號

符號 作用
<init> 匹配所有構造器
<fields> 匹配所有域
<methods> 匹配所有方法
* 匹配所有域和方法

3、一些常用萬用字元

萬用字元 作用
* 匹配任意長度字元,但不含包名分隔符(.)
** 匹配任意長度字元,並且包含包名分隔符(.)
*** 匹配任意引數型別
... 匹配任意長度的任意型別引數
% 匹配任何原始型別
? 匹配類名中的任何單個字元

其他的一些語法基本上屬於無變化的那種,在混淆規則中基本都固定使用了,有需要的話可以自行在官方查詢。

自定義混淆模板

嗯...這模板基本都是大同小異的,這裡就直接貼出常用的混淆規則了。

# 優化演算法,一般不用修改,來自Google
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 程式碼混淆壓縮比,在0~7之間,預設為5,一般不做修改
-optimizationpasses 5
# 混合時不使用大小寫混合,混合後的類名為小寫
-dontusemixedcaseclassnames
# 不去忽略非公共庫的類
-dontskipnonpubliclibraryclasses
# 優化時允許訪問並修改有修飾符的類和類的成員 
-allowaccessmodification
# 專案混淆後產生對映檔案
-verbose
# 不做預校驗
-dontpreverify

# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses
# 避免混淆泛型
-keepattributes Signature
# 丟擲異常時保留程式碼行號
-keepattributes SourceFile,LineNumberTable

# 保留四大元件,自定義的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.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# 保留native方法
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留在Activity中的方法引數是view的方法,
# 這樣以來我們在layout中寫的onClick就不會被影響
-keepclassmembers class * extends android.app.Activity{
    public void *(android.view.View);
}

# 保留自定義View
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
   public <init>(android.content.Context);
   public <init>(android.content.Context, android.util.AttributeSet);
   public <init>(android.content.Context, android.util.AttributeSet, int);
}

# 保留列舉
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留Parcelable序列化物件
-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}

# 保留R檔案中的成員
-keepclassmembers class **.R$* {
    public static <fields>;
}

# 保留Serializable序列化的類不被混淆
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

# -------------------------------------------------------------------------------
# webView處理,專案中沒有使用到webView忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
    public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.webView, jav.lang.String);
}

# --------------------------保留JS介面-------------------------------------------

# --------------------------保留反射類-------------------------------------------

# --------------------------保留實體類-------------------------------------------

# --------------------------第三方庫的混淆規則-----------------------------------

複製程式碼

以上都是一些常用的混淆規則,其中部分規則其實都在Android預設混淆檔案中宣告過了。所以具體的自定義混淆規則還是得根據專案進行變化。

混淆產物

混淆後一般都有下面幾個檔案:

  1. dump.txt:描述APK檔案中所有類的內部結構;
  2. mapping.txt:提供混淆前後類、方法、類成員等的對照表
  3. seeds.txt:列出沒有被混淆的類和成員
  4. usage.txt:列出被移除的程式碼

遇到混淆問題時,我通常是通過檢視mapping.txt檔案來分析原因的。
如果在進行Crash追蹤中遇到了困難,可以使用sdk目錄下/tools/proguard/bin中的proguardgui.bat視覺化工具進行混淆定位。示例如圖:

追蹤混淆Crash棧

混淆引發的“Xue案”

場景一
問題描述:業務方整合了我們的一個SDK,SDK的一個頁面中會顯示Banner圖,而Banner圖所用的是SDK中自定義的ViewPager元件,結果業務方那邊無法顯示Banner圖。
原因分析:通過反編譯apk,比對mapping.txt檔案,發現Banner圖的自定義Adapter中關鍵方法都被清除了。檢視混淆檔案,發現只keep了support v4的ViewPager,並沒有keep住PagerAdapter。而SDK中又是通過compileOnly(provided)方式依賴v4包的,導致自定義的Adapter在工程編譯的時候找不到被引用的關係,然後就被混淆給優化掉了。
經驗:針對compileOnly(provided)依賴的庫,一定要注意相關類的混淆,特別是SDK開發者,因為混淆導致的問題一般較難定位。

場景二
問題描述:業務方又整合了我們的一個SDK(沒錯,是“又”),SDK有一個換膚功能,業務方可以通過構造特定格式的資原始檔來實現皮膚替換。但在業務通過公司編譯工具編譯後的apk中無法實現換膚功能。
原因分析:機智的我很快想到了混淆問題,於是讓業務方加上保留所有R檔案的規則,結果還是失敗。於是開始了長時間的打log看log過程,還是確定問題出在資源名的混淆上。最後發現業務方雖然在本地編譯環境中keep了資源名,但在公司的編譯環境中還是對資源進行了混淆,導致換膚失敗。
經驗:對於和資源相關的功能,一定要注意R檔案的混淆。不要輕易相信業務,因為他們經常有你預料不到的操作(手動滑稽...)。

工作中遇到過很多次混淆相關的問題,這裡就不多說了,只要大家細心點就好了。

總結

  1. 混淆其實不算是什麼新鮮的話題,但對於Java和Android開發者來說是必不可少的,還是要對其有敬畏之心(嚴肅臉...)。
  2. 混淆的好處不言而喻增加了反編譯的難度,優化了程式碼。但僅僅是增加難度而已,對於有心之人還是能從反編譯中找到蛛絲馬跡。所以,對於敏感程式碼和資料使用更安全的保護措施是非常必要的,比如加固(硬廣一波:360加固保)。

參考資料

  1. Android安全攻防戰,反編譯與混淆技術完全解析(下)
  2. Android Studio混淆模板及常用第三方混淆
  3. 寫給 Android 開發者的混淆使用手冊
  4. ProGuard

關注微信公眾號,最新技術乾貨實時推送

image

相關文章