Android代碼混淆與進(jìn)階

混淆是 Android 打包過(guò)程中最重要的流程之一,基本上所有 app 都應(yīng)該開(kāi)啟混淆,增加app的安全性?;煜鋵?shí)是包括了代碼壓縮、代碼混淆以及資源壓縮等的優(yōu)化過(guò)程。谷歌官方文檔Shrink Your Code and Resource,依靠 ProGuard,混淆流程將主項(xiàng)目以及依賴(lài)庫(kù)中未被使用的類(lèi)、類(lèi)成員、方法、屬性移除,這有助于規(guī)避64K方法數(shù)的瓶頸;同時(shí),將類(lèi)、類(lèi)成員、方法重命名為無(wú)意義的簡(jiǎn)短名稱(chēng),增加了逆向工程的難度。而依靠 Gradle 的 Android 插件,我們將移除未被使用的資源,可以有效減小 apk 安裝包大小。

配置方法

在 app module 目錄中的 build.gradle文件中的 android 節(jié)點(diǎn)下配置如下:

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

因?yàn)榛煜龝?huì)大大減慢編譯的速度,所以 debug 模式下不應(yīng)該開(kāi)啟混淆,minifyEnabled 設(shè)置為 true 表示開(kāi)啟混淆,shrinkResources 設(shè)置為 true 表示開(kāi)啟壓縮資源,proguardFiles 表示指定給項(xiàng)目的 ProGuard 文件,默認(rèn)情況下是指定了兩個(gè) Proguard rules 文件,一個(gè)是 Android 系統(tǒng)自帶的 proguard-android.txt,另外一個(gè)就是在 app module目錄下的 proguard-rules.pro,proguard-android.txt 我們可以在 Android SDK 的目錄中搜索即可找到該文件,完整路徑是 SDK 目錄下的 <sdk-root>/tools/proguard/。我們先看看 proguard-rules.pro 這個(gè)文件,它是留給開(kāi)發(fā)者來(lái)自定義混淆規(guī)則的,一般開(kāi)發(fā)者的混淆都寫(xiě)在這個(gè)文件中。

常見(jiàn)的混淆命令及含義

  • optimizationpasses 指定壓縮級(jí)別,默認(rèn)是5
  • dontoptimize 混淆時(shí)不優(yōu)化類(lèi)文件
  • dontusemixedcaseclassnames 不使用大小寫(xiě)混合類(lèi)名
  • dontskipnonpubliclibraryclasses 混淆jars中的非public classes
  • dontpreverify 混淆時(shí)不做預(yù)校驗(yàn)
  • dontwarn 不提示警告,避免打包時(shí)某些警告出現(xiàn)
  • ignorewarnings 忽略警告,避免打包時(shí)某些警告出現(xiàn)
  • verbose 混淆時(shí)記錄日志
  • assumenosideeffects 優(yōu)化時(shí)允許訪問(wèn)并修改類(lèi)和成員的訪問(wèn)修飾符,可能作用域會(huì)變大。
  • repackageclasses 可以把你的代碼以及所使用到的各種第三方庫(kù)代碼統(tǒng)統(tǒng)移動(dòng)到同一個(gè)包下。
  • obfuscationdictionary 后面加一個(gè)純文本文件路徑,它的作用是指定一個(gè)字典文件作為混淆字典。默認(rèn)情況下我們的代碼命名會(huì)被混淆成 abcdefg... 字母組合的內(nèi)容,需要修改可以使用這個(gè)配置項(xiàng)將字典修改成亂碼或中文內(nèi)容。
  • keep 防止類(lèi)和成員被移除或重命名
  • keepnames 防止類(lèi)和成員被重命名
  • keepclassmembers 防止成員被移除或重命名
  • keepclassmembernames 防止成員被重命名
  • keepclasseswithmembers 防止擁有該成員的類(lèi)被移除或重命名
  • keepclasseswithmembernames 防止擁有該成員的類(lèi)被重命名
  • keepattributes 當(dāng)“Annotation、Exceptions, Signature, Deprecated, SourceFile, SourceDir, LineNumberTable,InnerClasses”這些東西可能被移除時(shí)如果想保留,就使用該屬性keep住

混淆的格式一般是下面這兩種:

[混淆命令] [類(lèi)]

[混淆命令] [類(lèi)] {
    [成員];
}

其中"類(lèi)"是代表類(lèi)相關(guān)的限制條件,它最終會(huì)指定到某些符合限定條件的類(lèi),它可以是下列內(nèi)容:

  • 具體的類(lèi)
  • 類(lèi)訪問(wèn)修飾符(public private protected)
  • 通配符*,匹配任意長(zhǎng)度字符,但不含包名分隔符(.)
  • 通配符**,匹配任意長(zhǎng)度字符,并且包含包名分隔符(.)
  • extends,即可以指定類(lèi)的基類(lèi)
  • implement,匹配實(shí)現(xiàn)了某接口的類(lèi)
  • $,內(nèi)部類(lèi)

"成員"代表類(lèi)成員相關(guān)的限定條件,它將最終定位到某些符合該限定條件的類(lèi)成員。它的內(nèi)容可以使用:

  • <init> 匹配所有構(gòu)造器
  • <fields> 匹配所有域
  • <methods> 匹配所有方法
  • 通配符*,匹配任意長(zhǎng)度字符,但不含包名分隔符(.)
  • 通配符**,匹配任意長(zhǎng)度字符,并且包含包名分隔符(.)
  • 通配符***,匹配任意參數(shù)類(lèi)型
  • ...,匹配任意長(zhǎng)度的任意類(lèi)型參數(shù)。比如void test(…)就能匹配任意 void test(String a) 或者是 void test(int a, String b) 這些方法。
  • 訪問(wèn)修飾符(public、protected、private)

常用的自定義混淆規(guī)則

  • 不混淆某個(gè)類(lèi)
-keep public class com.shenhuniurou.DemoActivity { *; }
  • 不混淆某個(gè)包所有的類(lèi)
-keep class com.shenhuniurou.demo.** { *; }
  • 不混淆某個(gè)類(lèi)的子類(lèi)
-keep public class * extends com.shenhuniurou.activity.BaseActivity { *; }
  • 不混淆所有類(lèi)名中包含了“model”的類(lèi)及其成員
-keep public class **.*model*.** {*;}
  • 不混淆某個(gè)接口的實(shí)現(xiàn)
-keep class * implements com.shenhuniurou.demo.DemoInterface { *; }
  • 不混淆某個(gè)類(lèi)的構(gòu)造方法
-keepclassmembers class com.shenhuniurou.demo.DemoClass { 
    public <init>(); 
}
  • 不混淆某個(gè)類(lèi)的特定的方法
-keepclassmembers class com.shenhuniurou.demo.DemoClass { 
    public void test(java.lang.String); 
}

下面我們?cè)倏纯碨DK自帶的默認(rèn)混淆文件中定義了哪些混淆規(guī)則

# 不使用大小寫(xiě)混合類(lèi)名
-dontusemixedcaseclassnames
# 混淆jars中的非public classes
-dontskipnonpubliclibraryclasses
# 混淆時(shí)記錄日志
-verbose

# 混淆時(shí)不優(yōu)化類(lèi)文件
-dontoptimize
# 混淆時(shí)進(jìn)行預(yù)校驗(yàn)
-dontpreverify

# 不混淆注解
-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# 保持 native 方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保持自定義View的getter/setter不被混淆
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# 保持Activity和其子類(lèi)的View作為參數(shù)的方法不被混淆
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# 保持枚舉不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保持通過(guò)Parcelable序列化的類(lèi)不被混淆
-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注解的相關(guān)的類(lèi)和方法字段以及構(gòu)造方法
-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>(...);
}

有了這些默認(rèn)的混淆規(guī)則之后,在開(kāi)發(fā)者自定義的混淆文件中就不需要再寫(xiě)了,開(kāi)發(fā)者一般需要關(guān)注的混淆規(guī)則是引用了第三方庫(kù)后或者使用了jni的類(lèi)、webview中調(diào)用了js方法,這些都需要開(kāi)發(fā)者來(lái)定義混淆規(guī)則。第三方庫(kù)的接入文檔一般都已經(jīng)寫(xiě)好了混淆規(guī)則,開(kāi)發(fā)者只需要拷貝至proguard-rules.pro文件中即可。

完成混淆后,混淆過(guò)的包必須進(jìn)行檢查,避免因混淆引入的bug。

在使用上面的配置進(jìn)行混淆打包后在 <module-name>/build/outputs/mapping/release/ 目錄下會(huì)輸出以下文件:

  • dump.txt 描述APK文件中所有類(lèi)的內(nèi)部結(jié)構(gòu)
  • mapping.txt 提供混淆前后類(lèi)、方法、類(lèi)成員等的對(duì)照表
  • seeds.txt 列出沒(méi)有被混淆的類(lèi)和成員
  • usage.txt 列出被移除的代碼

如果有使用渠道打包的話,那么輸出目錄是 <module-name>/build/outputs/mapping/channel/release/。我們可以根據(jù) seeds.txt 文件檢查未被混淆的類(lèi)和成員中是否已包含所有期望保留的,再根據(jù) usage.txt 文件查看是否有被誤移除的代碼。另外還需要從測(cè)試方面檢查。將混淆過(guò)的包進(jìn)行全方面測(cè)試,檢查是否有 bug 產(chǎn)生。

混淆資源文件

一般使用上述兩個(gè)混淆文件的配置混淆出來(lái)的apk,只是混淆了代碼,而對(duì)于項(xiàng)目中的資源文件名卻沒(méi)有混淆的,所以一般別人反編譯你的apk后,是可以清楚的看到你的資源文件的(包括命名和內(nèi)容)。微信團(tuán)隊(duì)開(kāi)源了一個(gè)能混淆資源文件的庫(kù)叫AndResGuard,AndResGuard是一個(gè)幫助你縮小APK大小的工具,他的原理類(lèi)似Java Proguard,但是只針對(duì)資源。他會(huì)將原本冗長(zhǎng)的資源路徑變短,例如將res/drawable/wechat變?yōu)閞/d/a下面是微信apk的資源目錄結(jié)構(gòu):

wechat_res_proguard

解碼混淆過(guò)的堆疊追蹤

混淆后的類(lèi)、方法名等等難以閱讀,這固然會(huì)增加逆向工程的難度,但對(duì)追蹤線上 crash 也造成了阻礙。我們拿到 crash 的堆棧信息后會(huì)發(fā)現(xiàn)很難定位,這時(shí)需要將混淆反解。
在 <sdk-root>/tools/proguard/bin 路徑下有附帶的的反解工具(Window 系統(tǒng)為 proguardgui.bat,Mac 或 Linux 系統(tǒng)為 proguardgui.sh)。
雙擊運(yùn)行 proguardgui.bat 后,可以看到左側(cè)的一行菜單。點(diǎn)擊 ReTrace,選擇該混淆包對(duì)應(yīng)的 mapping 文件(混淆后在<module-name>/build/outputs/mapping/release/ 路徑下會(huì)生成 mapping.txt 文件,它的作用是提供混淆前后類(lèi)、方法、類(lèi)成員等的對(duì)照表),再將 crash 的 stack trace 黏貼進(jìn)輸入框中,點(diǎn)擊右下角的 ReTrace ,混淆后的堆棧信息就顯示出來(lái)了。如下圖:

proguardgui
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容