1. 程式人生 > >Android高階混淆和程式碼保護技術

Android高階混淆和程式碼保護技術

本文有兩部分內容,一部分講混淆,一部分介紹一些混淆之下的安全手段。基準原則都是:在保證不麻煩到自身 以及 能夠正常閱讀異常日誌的前提下,儘可能提高混淆強度和保護程式碼安全。

本文原文地址:http://drakeet.me/android-advanced-proguard-and-security/

混淆

Android 官方集成了 Proguard 以供我們進行程式碼混淆工作,關於 Proguard 你可以搜尋到各種它的 rules 解釋,這些文章千篇一律,因此我不再贅述,只說一些特別的有用的技巧:

一般情況下,Android 的 gradle 中都會預設寫著:

  1. 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。對此,提供我的配置以供參考:

  1. release { 
  2.     debuggable false
  3.     minifyEnabled true
  4.     zipAlignEnabled true
  5.     shrinkResources true
  6.     signingConfig signingConfigs.release 
  7.     proguardFile 'proguard-common.pro'
  8.     proguardFile 'proguard-rules.pro'
  9.     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 之前把日誌程式碼全部刪掉,這麼做不僅有助於提升效能,而且日誌程式碼往往會保留很多我們的意圖和許多可被反編譯的字串:

  1. -assumenosideeffects class android.util.Log { 
  2.     publicstatic boolean isLoggable(java.lang.String, int); 
  3.     publicstaticint d(...); 
  4.     publicstaticint w(...); 
  5.     publicstaticint v(...); 
  6.     publicstaticint i(...); 

三,對於你不得不留下的一些硬編碼和日誌內容,可以採用編碼形式替換,如 你可以規定 "4001" 代表某種錯誤,而不是在你的程式碼裡寫入這個錯誤的具體描述字串。這麼做的話,你需要有個地方記下這些編碼對映的內容,關於此有個技巧:你可以再建立一個常量類,其內容是一堆靜態字串物件,針對上面那個例子,你可以把真正的錯誤資訊作為一個字串變數的名字,而把它的值寫成一個編碼,如下:

  1. publicstatic final String SHOULD_REGISTER_FIRST_ERROR = "ssrrffe"

這樣當你在看沒混淆的程式碼引用這個靜態變數,你能夠一目瞭然它的意思。而反編譯者看到的則是:

  1. publicstatic final String abc = "ssrrffe"

命名看不懂,值也看不懂。

四,把 AppKey 之類特別敏感的字串內容藏在 native so 檔案中。

關於字串技巧的內容差不多就這樣了,能做到這些就不錯了,還有一些極端做法不多說,為了阻礙黑客閱讀,自己也變得非常麻煩,雙刃劍,這不是我們想要的結果。

然後我們講另一個混淆後代碼的軟肋,就是一些我們不得不 keep 的內容,如果是閉源 SDK 開發者,需要 keep 的內容將會更多,幾乎只要是 public 的類、變數,方法,全部要 keep,那麼針對這個問題,我們該怎麼辦?介紹一個方法:

給這些需要 keep 的內容設定委託者,然後將委託者投入大海之中。

很玄乎吧?哈哈,這麼講有助於記憶。其實和我們在混淆章節說的藏葉於林的思想是一樣的。如果一個類不得不 keep,那就把它所做的全部內容都轉交給一個 private 或 internal 的類物件去完成,這個委託類物件程式碼可以完全混淆,然後你再把這個委託類通過混淆工具藏在大量的程式碼之中,這樣就足夠給反編譯者帶來了很大的麻煩,相比直接獲取邏輯程式碼,這麼做以後要找到實體的邏輯程式碼將費勁得多。

因此,如果你知道有這麼一個方式,其實你完全可以不使用餓了麼提供的那個 Activity 和 View 混淆工具,也能很好地保護你的 Activity 和 View。

不過一般情況我們無需所有內容都保護,只要把關鍵、核心內容委託出去就可以了。

最後的最後,我們還需要做的就是防止反編譯者重新打包,全方位絕人之路呀,能做的就是在程式碼中加入簽名驗證,並做雙向依賴。關於此我寫過一個類似阿里黑匣子的東西,能夠在 native 檢查簽名和加解密內容,後續也有計劃整理開源,這裡暫且就不多說了。

除此之外,我專門寫過一篇叫作《Android 金鑰保護和 C/S 網路傳輸安全理論指南》 的文章,感興趣可以之後移步閱讀。

總之,程式碼安全和混淆是一個意識加技巧的問題,但都不難,掌握以上內容就已經十分好了。分享到此結束,如有疑問或問題歡迎來信交流。