扣丁書屋

我的 Android 應用安全方案梳理

作為獨立開發者,應用被破解是一件非常讓人煩惱的事情。之前有同學在我的一篇博文下面問,有沒有一些 Android 防破解的方法。在多次加固、破解、再加固、再破解的過程中,我也積累了一些思路和方法。這里分享一下,如果需要用到,可以作一個參考。

先說一個結論,也是我在 Stackoverflow 上面的一個國外程序員的答案,

anti_debug.png

就是說,APK 包已經在別人手上了,我們能做的不過是提升被破解的難度,如果真的遇到非?!皥讨钡?,要破解一樣被破解。如果邏輯非常值錢,那么最好還是把邏輯放到服務器上面。此外,加固也是一個可選的方案。不過目前市面上專業的加固價格并不美麗,各大平臺年費從 3 萬至 8 萬不等,并且對個人開發者并不友好。

下面是我開發過程中為了防止應用被破解采取的一些策略。

1、一些必要的基礎知識

首先,別人要破解你的軟件。如果只是在自己的手機上面使用,那么他可以修改系統的一些方法進行破解。這種不在我的考慮范圍內,因為他們的修改只在自己的手機上生效,構不成傳播。我關注的是 APK 文件被破解的情況。

我們在加密的時候會用到一些加密或者編碼方法。常見的有,非對稱加密算法 RSA 等;對稱加密算法 DES、3DES 和 AES 等;不可逆的加密 MD5、SHA256 等。

另外,我們會把重要的加密邏輯放到 Native 層來實現,所以一些 JNI 編程的方法也是需要的。不過,如果僅僅是用來作加密的話,對 C/C++ 的要求是沒那么高的。對在 Android 中使用 JNI,可以參考我之前的文章《在 Android 中使用 JNI 的總結》。

2、簽名校驗

2.1 基礎簽名校驗

在應用和 so 中作簽名校驗可以說是最基本的安全策略。在應用中作簽名校驗可以防止應用被二次打包。因為如果別人修改你的代碼,肯定要重新打包,此時簽名必然會改變。對 so 作簽名校驗是很有必要的,除了防止應用被打包,也可以防止你的 so 被別人盜用。

可以使用如下的代碼在 java 中進行簽名校驗,

private static String getAppSignatureHash(final String packageName, final String algorithm) {
    if (StringUtils.isSpace(packageName)) return "";
    Signature[] signature = getAppSignature(packageName);
    if (signature == null || signature.length <= 0) return "";
    return StringUtils.bytes2HexString(EncryptUtils.hashTemplate(signature[0].toByteArray(), algorithm))
            .replaceAll("(?<=[0-9A-F]{2})[0-9A-F]{2}", ":$0");
}

對于在 Native 層作簽名校驗,將上述方法翻譯成對應的 JNI 調用即可,這里就不贅述了。

上面是簽名校驗的邏輯,看似美好,實際上稍微碰到有點破解的經驗的就頂不住了。我之前遇到的一種破解上述簽名校驗的方法是,在自定義 Application 的 onCreate() 方法中讀取 APK 的簽名并存儲到全局變量中,然后 Hook 獲取應用簽名的方法,并把上述讀取到的真實的簽名信息返回,以此繞過簽名校驗邏輯。

2.2 Application 類型校驗

針對上述這種破解方式,我想到的第一個方法是對當前應用的 Application 類型作校驗。因為他們加載 Hook 的邏輯是在自定義的 Application 中完成的,如果他們的 Application 和我們自己的 Application 類路徑不一致,那么可以認定應用為破解版。

不過,這種方式作用也有限。我當時采用這種策略是考慮到有的破解者可能就是用一個腳本破解所有應用,所以改動一下可以防止這類破解者。但是,后來我也遇到一些“狠人”。因為我的軟件用了 360 加固,所以如果加固殼工程的 Application 也認為是合法的。于是,我就看到了有的破解者在我的加固包之上又做了一層加固…

2.3 另一種簽名校驗方法

上述簽名校驗容易被 Hook 繞過,我們還可以采用另一種簽名校驗方法。

記得之前在《使用 APT 開發組件化框架的若干細節問題》 這篇文章中提到過,ARouter 在加載 APT 生成的路由信息的時候,一種方式是獲取軟件的 APK,然后從 APK 的 dex 中獲取指定包名下的類文件。那么,我們是不是也可以借鑒這種方式來直接對 APK 進行簽名校驗呢?

首先,你可以采用下面的方法獲取軟件的 APK,

ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);

獲取 APK 簽名信息的方法比較多,這里我提供的是 Android 源碼中的打包文件的簽名代碼,代碼位置是:

https://android.googlesource.com/platform/tools/apksig/+/master

這樣,當我們拿到 APK 之后,使用上述方法直接對 APK 的簽名信息進行校驗即可。

3、對重要信息的加密

上述我們提到了一些常用的加密方法,這里介紹下我在設計軟件和系統的時候是如何對用戶的重要信息作加密處理的。

3.1 使用簽名字段防止偽造信息

首先,我的應用在做用戶鑒權的時候是通過服務器下發的字段來驗證的。為了防止服務器返回的信息被篡改以及在本地被用戶篡改,我為返回的鑒權信息增加了簽名字段。邏輯是這樣的,

  • 服務器查詢用戶信息之后根據預定義的規則拼接一個字符串,然后使用 SHA256 算法對拼接后的字符串做不可逆向的加密
  • 從服務器拿到用戶信息之后會直接丟到 SharedPreference 中(最好加密之后再存儲)
  • 當需要做用戶鑒權的時候,首先根據之前預定義的規則,對簽名字段做校驗以判斷鑒權信息是否給篡改
  • 如果鑒權信息被篡改,則默認為普通用戶權限

除了上述方法之外,為服務器配置 SSL 證書也是必不可少的?,F在很多云平臺都會提供一年免費的 Trust Asia 的證書(到期可再續費),免費使用即可。

3.2 對寫入到本地的鍵值對做處理

為了防止應用的邏輯被破解,當某些重要的信息(比如上面的鑒權信息)寫入到本地的時候,除了做上述處理,我對存儲到 SharedPreference 中的鍵也做了一層處理。主要是使用設備 ID 和鍵名稱拼接,做 SHA256 加密之后作為鍵值對的鍵。這里的設備 ID 就是 ANDROID_ID. 雖然 ANDROID_ID 用作設備 ID 并不可靠,但是在這個場景中它可以保證大部分用戶存儲到本地的鍵值對中的鍵是不同的,也就增加了破解者針對某個鍵值對進行破解的難度。

3.3 重要信息不要直接使用字符串

在代碼中直接使用字符串很容易被別人搜索到,一般對于重要的字符串信息,我們可以將其先轉換為整數數組。然后再在代碼中通過數組得到最終的字符串。比如下面的代碼用來將字符串轉換為 short 類型的數組,

static short[] getShortsFromBytes(String from) {
    byte[] bytesFrom = from.getBytes();
    int size = bytes.length%2==0 ? bytes.length/2 : bytes.length/2+1;
    short[] shorts = new short[size];
    int i = 0;
    short s = 0;
    for (byte b : bytes) {
        if (i % 2 == 0) {
            s = (short) (b << 8);
        } else {
            s = (short) (s | b);
        }
        shorts[i/2] = s;
        i++;
    }
    return shorts;
}

3.4 Jetpack 中的數據安全

除了上面的一些方法之外,Android 的 Jetpack 對數據安全開發了 Security 庫,適用于運行 Android 6.0 和更高版本的設備。Security 庫針對的是 Android 應用中讀寫文件的安全性。詳情可以閱讀官方文檔相關的內容:

更安全地處理數據:https://developer.android.com/topic/security/data

4、增強混淆字典

混淆之后可以讓別人反編譯我們的代碼之后閱讀起來更加困難。這在一定程度上可以增強應用的安全性。默認的混淆字典是 abc 等英文字母組成,還是具有一定的可讀性的。我們可以通過配置混淆字典進一步增加閱讀的難度:使用特殊符號、0oO 這種相近的字符甚至 java 的關鍵字來增加閱讀的難度。配置的方式是,

# 方法名等混淆指定配置
-obfuscationdictionary dict.txt
# 類名混淆指定配置
-classobfuscationdictionary dict.txt
# 包名混淆指定配置
-packageobfuscationdictionary dict.txt

一般來說,當我們自定義混淆字典的時候需要從下面兩個方面考慮,

  1. 混淆字典增加反編譯識別難度使代碼可讀性變差
  2. 減小方法和字段名長度從而減小包體積

對于 o0O 這種雖然可讀性變差了,但是代碼長度相比于默認混淆字典要長一些,這會增加我們應用的包體積。我在選擇混淆字典的時候使用的是比較難以記憶的字符。我把混淆字典放到了 Github 上面,需要的可以自取,

混淆字典:https://github.com/Shouheng88/LeafNote-Community/blob/main/dict.txt

下面是混淆之后的效果,

QQ截圖20220216230706.png

這既可以保證包體積不會增大,又增加了閱讀的難度。不過當我們反混淆的時候可能會遇到反混淆亂碼的問題,比如 SDK 默認的反混淆工具就有這個問題(工具本身的問題)。

5、so 安全性

對 so 的破解,我現在也沒有特別好的方法。之前我已經把一些需要高級權限的邏輯搬到了 native 層,但是最終一樣被破解。如果是專業的加固,會對 so 同時做加固。我個人目前對 so 也不是特別熟,之前被破解也是因為 so 的內容被修改。后面會對 so 相關的內容做進一步學習和補充。上面提到的 so 的簽名校驗可以作為安全性檢查之一,下面還有一些開發過程中的其他建議可以做參考。

5.1 不要使用布爾類型作為重要 native 方法的返回類型

使用布爾類型作為 native 方法的返回值的一個不好的地方是,別人破解起來會非常容易。因為對于布爾類型,它只有 true 和 false 兩種情況。所以,破解者可以很容易地通過將類的方法修改為直接返回 true 或者 false 來繞開校驗的邏輯。相對來說更好的方式是返回一個整數或者字符串。

5.2 校驗方法的 native 特性

如果一個方法是 native 方法,我們可以通過判斷方法的屬性信息來判斷這個方法是否被修改。上面提到了有些 native 方法如果直接返回布爾類型,可能直接會被篡改為直接返回 true/false 的形式。此時,破解者就把 native 方法修改為普通的方法。所以,我們可以通過判斷方法的 native 特性,來判斷這個方法是否被別人做了手腳。下面是一個示例方法,

val method = cls.getMethod("method", Int::class.java)
Modifier.isNative(method.modifiers)

6、不要把校驗邏輯封裝到一個方法里

把一套邏輯封裝成一個方法對于常規業務的開發是一個好的習慣。但是把權限校驗的邏輯封裝到一個方法中就不一定了。因為別人只要把注意力放在你的這一個方法上面就足夠了。這樣,只要破解了這一個方法就可以破解你的應用中所有的安全校驗邏輯。

但是如果把同一個權限校驗的邏輯在所有需要做權限校驗的地方都拷貝一份,后續代碼維護起來也會非常困難。那么有沒有比較折衷的手段,既可以實現邏輯集中維護,又可以把權限校驗的邏輯分散到各個需要做權限校驗的地方呢?答案是有,只不過要求應用中使用的是 kotlin 語言。

使用 inline 實現權限校驗集中管理和分散調用:inline 是 kotlin 的一個關鍵字,效果類似于 C 語言中的內聯。編譯的時候會將 inline 方法中的邏輯內聯到調用的地方。我們只需要將我們的權限校驗的邏輯寫到 inline 方法中,然后在需要鑒權的地方調用這個 inline 方法,就可以實現權限校驗集中管理和分散調用。這樣如果需要破解我們的校驗邏輯,需要到每個地方依次進行破解。

此外,

1、權限校驗的邏輯最好和業務代碼交織在一起而不是分開寫。原因如上,分開寫別人只要破解這一個方法就夠了。 2、C/C++ 層也可以嘗試使用 inline 方法。

7、使用服務器做安全校驗

上面也說了最好的安全措施還是把重要的邏輯放到后端。不過,對于我開發的應用,因為它本身基本是離線使用的,所以,無法在操作過程中使用服務器做鑒權。對此,我使用了兩個方案來讓服務器參與到防破解中。

其一是,啟用版本配置,在應用配置中下發強制升級信息。最初為應用設計服務器的時候我就設計了應用從后端拉取配置信息的接口。這個接口也會同時下發應用的版本信息以及升級的類型。如果是強制升級,那么會彈出一個無法取消的對話框。這樣這個版本基本就無法繼續使用了。通過這個配置,我們可以通過服務器配置直接禁用被破解的應用版本。

其二,在執行需要高級權限的操作的時候上報服務器。服務器通過后端存儲的用戶信息判斷該用戶是否具備該權限。如果不具備權限,那么增加一條違規記錄,并記錄違規用戶的用戶信息。后臺通過可以配置的形式對單一用戶進行禁用。至于這里為什么不直接對用戶進行禁用的問題。正如《七武士》中的一個橋段一樣,好的防守總是會留一個入口。直接禁用很容易被破解者發現并做相應處理。

另外,最好不要直接拋出異常,彈出的 toast 不要使用明文字符串。因為,上述兩種方式都很容易讓別人直接定位到我們校驗的邏輯的位置。如果不得不拋異常,建議觸發 OOM!

總結

寫了那么多東西,我也無奈,破解比反破解要容易得多,以上是我在實踐過程中總結的一些基本的技巧。對于 Android 應用安全,我還有很多東西需要學習和了解。畢竟,對于應用層開發來說,安全是另一個專業領域的事情。我也只能“防君子不防小人”。后續我學習了更多的內容,做了更多的攻防戰,總結更多經驗之后再補充。唉,“本是同根生,相煎何太急”!


https://mp.weixin.qq.com/s/BQBevqzY0VUVvb7NZWGBKw

最多閱讀

簡化Android的UI開發 2年以前  |  515113次閱讀
Android 深色模式適配原理分析 1年以前  |  26416次閱讀
Android 樣式系統 | 主題背景覆蓋 1年以前  |  7953次閱讀
Android Studio 生成so文件 及調用 1年以前  |  5587次閱讀
30分鐘搭建一個android的私有Maven倉庫 3年以前  |  4751次閱讀
Android設計與開發工作流 2年以前  |  4413次閱讀
Google Enjarify:可代替dex2jar的dex反編譯 3年以前  |  4397次閱讀
Android多渠道打包工具:apptools 3年以前  |  4028次閱讀
移動端常見崩潰指標 2年以前  |  4014次閱讀
Google Java編程風格規范(中文版) 3年以前  |  3942次閱讀
Android-模塊化-面向接口編程 1年以前  |  3857次閱讀
Android內存異常機制(用戶空間)_NE 1年以前  |  3824次閱讀
Android UI基本技術點 3年以前  |  3790次閱讀
Android死鎖初探 2年以前  |  3734次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>