Android高級混淆和代碼保護技術
這是一篇關于 Android 代碼保護的文章,旨在介紹代碼混淆、防止逆向工程的各種高級技巧。大家都很忙,我也趕著回去繼續開發我的新應用,因此話不多說,越干(gan, 一聲)越好。
開始之前,值得一說的是,本文超過五千字,完全由我開發的「純純寫作」書寫而成,純純寫作主打安全、寫作體驗和永不丟失內容,于是本著珍愛生命,我用純純寫作來寫這篇文章。
本文有兩部分內容,一部分講混淆,一部分介紹一些混淆之下的安全手段。基準原則都是:在保證不麻煩到自身 以及 能夠正常閱讀異常日志的前提下,盡可能提高混淆強度和保護代碼安全。
本文原文地址:http://drakeet.me/android-advanced-proguard-and-security/
混淆
Android 官方集成了 Proguard 以供我們進行代碼混淆工作,關于 Proguard 你可以搜索到各種它的 rules 解釋,這些文章千篇一律,因此我不再贅述,只說一些特別的有用的技巧:
一般情況下,Android 的 gradle 中都會默認寫著:
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
這一行代碼很多人不了解。它的意思是,指定了兩個 Proguard rules 文件,一個是通過getDefaultProguardFile() 方法獲得官方自帶的混淆規則文件路徑,另一個是與當前 gradle 相同目錄下的 proguard-rules.pro 文件路徑。
后者就在我們項目中,由我們書寫的,沒什么好說的,我們要關注的是前者這個默認 Proguard 文件,它的內容是什么你有曾探究過嗎?沒有的話,你可以在你的系統文件里搜索proguard-android.txt 就應該能把它找出來,具體自己去看,我就說一些關鍵的,這個默認文件中幫我們聲明了許多混淆規則內容,包括:keep 所有繼承自 View 的類,keep 所有繼承自 Activity 的類,keep 所有 JavascriptInterface、native 方法聲明,以及 keep 一些注解了@Keep 的內容。
所以你知道為什么默認情況下,即使你自己一條規則都沒有加入,你的自定義 View 和 Activity 都被保留下來了吧,至少類名都沒有被混淆。
那么為什么官方默認會幫我們寫下這些?為什么 View 和 Activity 默認情況下應該被保留呢?
簡單來說,因為 Proguard 原本是為 Java 打造的,它無法搜索到我們 AndroidManifest、布局等文件中引用了哪些 Java 類,因此如果 Java 代碼變了而 XML 文件中的引用沒變,就會造成反射失敗。所以這些被 XML 使用到的類需要 keep 住。
對于這個問題,餓了么 的團隊提供了一個鮮為人知的 gradle 插件 用來無傷混淆 Activity 和 View,這個項目叫 Mess:https://github.com/eleme/Mess ,具體內容各位可以稍后自行去閱讀其文檔和教程,鏈接***都還會附于末尾。簡單來說,Mess 彌補了 Proguard 不能檢索 XML 文件的缺點,幫 Proguard 完成了 Activity 和 View 的改名及 mapping。
話說回來,前面我建議各位都去逐行了解下默認混淆配置文件,因為只有這樣,你才知道整個混淆工具幫你做了什么,了解清楚之后,我建議的一個做法是,把這個默認文件拷貝到你的項目目錄之下,刪掉 getDefaultProguardFile('proguard-android.txt'),再引入現存于你目錄之下的原默認文件。這么做的好處是,方便你修改這個默認文件,因為它有些內容是不必要或者可以更改的。不過基本上我們可以保留其原樣。復制過來的另一個好處是,避免其被外方更新導致你引用過來后產生變數。總之,proguardFiles 這個配置項(其實是一個 gradle 方法)可以接受***個 rules 文件路徑,它的參數是一個可變字符串參數,不過為了避免代碼橫向發展,我更愿意使用另一個方法,叫 proguardFile,注意,少了一個 s 有沒有,它接受單個參數,相當于 add 一個 rules。對此,提供我的配置以供參考:
- release {
- debuggable false
- minifyEnabled true
- zipAlignEnabled true
- shrinkResources true
- signingConfig signingConfigs.release
- proguardFile 'proguard-common.pro'
- proguardFile 'proguard-rules.pro'
- proguardFile 'proguard-rules-google-ads.pro'}
其中 proguard-common.pro 這個文件就是上述我說的復制過來的官方默認配置文件,它被我放在當前 module 目錄之下和 proguard-rules.pro 并列。這么寫很清楚而且便于復用。
講完基本內容之后,我決定再介紹兩條特別實用的 Proguard rules:
-repackageclasses
-repackageclasses 這條規則配置特別強大,它可以把你的代碼以及所使用到的各種第三方庫代碼統統移動到同一個包下,可能有人知道這條配置,但僅僅知道它還不能發揮它***的作用,默認情況下,你只要在 rules 文件中寫上 -repackageclasses 這一行代碼就可以了,它會把上述的代碼文件都移動到根包目錄下,即在 / 包之下,這樣當有人反編譯了你的 APK,將會在根包之下看到 成千上萬 的類文件并列著,除此之外,由于我們有時不得不 keep 一些類文件,于是你應用的包名層次仍然會存在,有一些沒被完全混淆的類將繼續存留在你的包名之下,這些類文件就相對得不到很好的保護。于是我要介紹一個小技巧,就是 -repackageclasses 后跟上一個你應用的包名,如:
-repackageclasses com.drakeet.purewriter.debug
這么做以后,最終 Proguard 會將包括第三方庫的所有類文件都移動到你的包名之下,所謂藏葉于林,這時候那些你未能完全混淆的類也可以藏身在這類文件大海之中,而且這些類文件名都會被混淆成 abcd 字母組合的名字。
需要注意的是,-repackageclasses + 你的包名 這種做法存在混淆 bug,而默認 -repackageclasses 不加包名不會出現 bug,所以初次使用此法需要進行測試,否則請退而求其次,關于這個 bug 的具體內容不多說,很贅述。
第二個實用 rules 配置項:-obfuscationdictionary
-obfuscationdictionary 后面加一個純文本文件路徑,它的作用是指定一個字典文件作為混淆字典。默認情況下我們的代碼命名會被混淆成 abcdefg... 字母組合的內容,需要修改可以使用這個配置項將字典修改成亂碼或中文內容。亂碼命名可以令反編譯者懷疑人生。中文命名則能夠破壞一些反編譯軟件的正常工作,而且有的中文命名還能起到亂花漸欲迷人眼的效果,比如 GitHub 上較為流行的某長者的話語作為字典,在此不便貼出(可能會有人身危險),各位可以自行搜索,找不到別怪我。這些話語作為代碼命名,可以令反編譯者沉浸其中,無心分析代碼 :P。
***,關于混淆的內容,我們還有一塊軟肋,就是資源文件,Proguard 完全不會管我們的資源文件,因此如果資源文件名沒有做保護的話,很容易被順藤摸瓜找到關聯的 Java 代碼,對此,微信團隊提供了一個好用的資源混淆工具,它不僅能幫你全面混淆資源文件,還能幫你縮減資源文件的整體體積,這個工具叫 AndResGuard,開源地址:https://github.com/shwenzhang/AndResGuard
好了,終于簡單講完了一些關于混淆的要點,關于混淆其實還有許多小內容,比如可以使用consumerProguardFiles 為一個 library 或 SDK 項目配置混淆文件,這樣當某個 app 引用了你這個庫,無需再配置相關混淆內容,該 app 就會自動從 consumerProguardFiles 配置的文件中讀取需要進行的 keep 動作,這對于庫開發者是很有用的一個功能。更多就不細說了,文章末尾我會附上我的混淆配置文件片段。
安全
有了代碼混淆還不夠,我們需要更多技巧來保護我們的代碼,特別是對于需要做混淆但又需要暴露許多 API 的 SDK 開發者來說。混淆是基礎,代碼安全是意識。
首先我們要知道我們混淆代碼是如何被攻破的,其實對于反編譯者來說,最簡單的入手點就是字符串搜索,我們硬編碼留在代碼里的字符串值都會在反編譯過程中被原樣恢復,因此這是我們首要關注對象。避免被通過字符串攻破,我們應該做到以下幾點:
一,不要硬編碼寫入字符串值,即使你不得不這么做,也至少應該另起一個類,比如叫做HardStrings,用于靜態存放這些硬編碼的字符串。這樣反編譯者只能搜索到你這個常量類,而較難以搜索到這些字符串常量被哪里引用。
二,在 release 混淆過程中刪除 Log 代碼,使用 -assumenosideeffects 這個配置項可以幫我們在編譯成 APK 之前把日志代碼全部刪掉,這么做不僅有助于提升性能,而且日志代碼往往會保留很多我們的意圖和許多可被反編譯的字符串:
- -assumenosideeffects class android.util.Log {
- public static boolean isLoggable(java.lang.String, int);
- public static int d(...);
- public static int w(...);
- public static int v(...);
- public static int i(...);
- }
三,對于你不得不留下的一些硬編碼和日志內容,可以采用編碼形式替換,如 你可以規定 "4001" 代表某種錯誤,而不是在你的代碼里寫入這個錯誤的具體描述字符串。這么做的話,你需要有個地方記下這些編碼映射的內容,關于此有個技巧:你可以再創建一個常量類,其內容是一堆靜態字符串對象,針對上面那個例子,你可以把真正的錯誤信息作為一個字符串變量的名字,而把它的值寫成一個編碼,如下:
- public static final String SHOULD_REGISTER_FIRST_ERROR = "ssrrffe";
這樣當你在看沒混淆的代碼引用這個靜態變量,你能夠一目了然它的意思。而反編譯者看到的則是:
- public static final String abc = "ssrrffe";
命名看不懂,值也看不懂。
四,把 AppKey 之類特別敏感的字符串內容藏在 native so 文件中。
關于字符串技巧的內容差不多就這樣了,能做到這些就不錯了,還有一些極端做法不多說,為了阻礙黑客閱讀,自己也變得非常麻煩,雙刃劍,這不是我們想要的結果。
然后我們講另一個混淆后代碼的軟肋,就是一些我們不得不 keep 的內容,如果是閉源 SDK 開發者,需要 keep 的內容將會更多,幾乎只要是 public 的類、變量,方法,全部要 keep,那么針對這個問題,我們該怎么辦?介紹一個方法:
給這些需要 keep 的內容設置委托者,然后將委托者投入大海之中。
很玄乎吧?哈哈,這么講有助于記憶。其實和我們在混淆章節說的藏葉于林的思想是一樣的。如果一個類不得不 keep,那就把它所做的全部內容都轉交給一個 private 或 internal 的類對象去完成,這個委托類對象代碼可以完全混淆,然后你再把這個委托類通過混淆工具藏在大量的代碼之中,這樣就足夠給反編譯者帶來了很大的麻煩,相比直接獲取邏輯代碼,這么做以后要找到實體的邏輯代碼將費勁得多。
因此,如果你知道有這么一個方式,其實你完全可以不使用餓了么提供的那個 Activity 和 View 混淆工具,也能很好地保護你的 Activity 和 View。
不過一般情況我們無需所有內容都保護,只要把關鍵、核心內容委托出去就可以了。
***的***,我們還需要做的就是防止反編譯者重新打包,全方位絕人之路呀,能做的就是在代碼中加入簽名驗證,并做雙向依賴。關于此我寫過一個類似阿里黑匣子的東西,能夠在 native 檢查簽名和加解密內容,后續也有計劃整理開源,這里暫且就不多說了。
除此之外,我專門寫過一篇叫作《Android 密鑰保護和 C/S 網絡傳輸安全理論指南》 的文章,感興趣可以之后移步閱讀。
總之,代碼安全和混淆是一個意識加技巧的問題,但都不難,掌握以上內容就已經十分好了。分享到此結束,如有疑問或問題歡迎來信交流。