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