Android應用安全解決方案

語言: CN / TW / HK

前言

防止第三方反編譯篡改應用,防止資料隱私洩露,防止二次打包欺騙使用者。

1、一些必要的基礎知識

我們在加密的時候會用到一些加密或者編碼方法。常見的有,非對稱加密演算法 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 的簽名信息進行校驗即可。

2.4  Janus簽名機制漏洞

打包時選擇v1和v2簽名

應用簽名未校驗

增加簽名證書的校驗程式碼,降低App被二次打包的機率。

/**
    * 檢測簽名
    */
    private boolean checkSignature() {
        Context context = WXApplication.getInstance();
        try {
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
            Signature[] signatures = packageInfo.signatures;

            if (signatures != null) {
                for (Signature signature : packageInfo.signatures) {
                    //獲取MD5或者SHA1
                    MessageDigest md = MessageDigest.getInstance("SHA1");
                    md.update(signature.toByteArray());

                    String currentSignature = bytesToHexString(md.digest()).toUpperCase();

                    if ("YOUR SIGENATURE".equals(currentSignature)) {
                        return true;
                    }
                }
            } else {
                LogUtil.i("signatures ==null");
            }

        } catch (NameNotFoundException e) {
            e.printStackTrace();
            LogUtil.e(e);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            LogUtil.e(e);
        }

        return false;
    }

    /**
     * byte轉16進位制String
     *
     * @param src 資料來源
     * @return string
     */
    public String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder();
        if (src == null || src.length <= 0) {
            return "";
        }

        for (byte by : src) {
            int v = by & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }

3、對重要資訊的加密

上述我們提到了一些常用的加密方法,這裡介紹下我在設計軟體和系統的時候是如何對使用者的重要資訊作加密處理的。

3.1 使用簽名欄位防止偽造資訊

首先,我的應用在做使用者鑑權的時候是通過伺服器下發的欄位來驗證的。為了防止伺服器返回的資訊被篡改以及在本地被使用者篡改,我為返回的鑑權資訊增加了簽名欄位。邏輯是這樣的,

  • 伺服器查詢使用者資訊之後根據預定義的規則拼接一個字串,然後使用 SHA256 演算法對拼接後的字串做不可逆向的加密

  • 從伺服器拿到使用者資訊之後會直接丟到 SharedPreference 中(最好加密之後再儲存)

  • 當需要做使用者鑑權的時候,首先根據之前預定義的規則,對簽名欄位做校驗以判斷鑑權資訊是否給篡改

  • 如果鑑權資訊被篡改,則預設為普通使用者許可權

除了上述方法之外, 為伺服器配置 SSL 證書 也是必不可少的。現在很多雲平臺都會提供一年免費的 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

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

5、Webview明文儲存密碼風險

解決方案

WebView.getSettings().setSavePassword(false)

6、Webview File同源策略繞過漏洞

將不必要匯出的元件設定為不匯出,並顯式設定所註冊元件的“android:exported”屬性為false

如果需要匯出元件,禁止使用File域

WebView.getSettings.setAllowFileAccess(false);

7、應用資料任意備份風險

AndroidManifest.xml內關閉資料允許備份

application android:allowBackup=false

8、敏感函式呼叫風險

稽核包含敏感行為的函式呼叫,確保其使用是必要且限制於授權使用者的。

restartPackage - 關閉程序

9、隨機數不安全使用漏洞

禁止在生成隨機數之前呼叫setSeed()方法設定隨機種子或呼叫SecureRandom類的建構函式SecureRandom(byte[] seed),建議通過/dev/urandom或者/dev/random獲取的熵值來初始化偽隨機數生成器。

10、URL資訊檢測

在移動應用的程式程式碼內部,可能存在大量開發人員或其他工作人員無意識留下的資訊內容。URL資訊檢測就是通過檢測移動應用程式程式碼內部所存在的URL地址資訊,儘可能呈現出應用中所有的URL資訊,便於應用開發者檢視並評估其安全性。移動應用釋出包中的URL地址資訊,可能會被盜取並惡意利用在正式伺服器上進行攻擊,攻擊安全薄弱的測試伺服器以獲取伺服器安全漏洞或者邏輯漏洞。

解決方案

1、核查並評估所有的URL資訊,判斷是否存在涉及內部業務等敏感資訊的URL地址,進行刪除;

2、儘量不要將與客戶端業務相關的URL資訊以硬編碼的方式寫在應用客戶端中,建議以動態的方式生成所需要請求的URL

11、殘留賬戶密碼資訊檢測

  1. 核查所有殘留的賬戶和密碼資訊,刪除與業務無關的賬戶和密碼。

  2. 儘量不要將與客戶端業務相關的賬戶密碼資訊以硬編碼的方式寫在應用客戶端中。

12、截圖攻擊風險

開發者審查應用中顯示或者輸入關鍵資訊的介面,在此類Activity建立時設定WindowManager.LayoutParams.FLAG_SECURE屬性,該屬效能防止螢幕被截圖和錄製

public class DemoActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE,  WindowManager.LayoutParams.FLAG_SECURE);
        setContentView(R.layout.main);
    }
}

13、未移除有風險的Webview系統隱藏介面漏洞

開發者顯式移除有風險的Webview系統隱藏介面。

以下為修復程式碼示例:

在使用Webview載入頁面之前,執行

webView.removeJavascriptInterface("searchBoxJavaBridge_");
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal");

14、Root裝置執行風險

開發者應在應用啟動時增加對應用執行環境的檢測,當發現執行裝置為Root裝置時,應禁止應用啟動。

 @Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
if(checkRootPathSU() || checkRootWhichSU()){
ToastUtils.showLong(getResources().getString(R.string.illegal_root));
finish();
return;
}
if(!checkSignature()){
ToastUtils.showLong(getResources().getString(R.string.illegal_apk_signatures));
finish();
return;
}
}
//通過檢測指定目錄下是否存在su程式來檢測執行環境是否為Root裝置
//當CheckRootPathSU返回值為true時,禁止應用啟動
public static boolean checkRootPathSU()
{
File f=null;
final String kSuSearchPaths[] = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/","/vendor/bin/"};
try{
for(int i=0;i<kSuSearchPaths.length;i++)
{
f=new File(kSuSearchPaths[i]+"su");
if(f!=null&&f.exists())
{
return true;
}
}
}catch(Exception e)
{
e.printStackTrace();
}
return false;
}
//通過which命令檢測系統PATH變數指定的路徑下是否存在su程式來檢測執行環境是否為Root裝置
//當CheckRootWhichSU返回值為true時,禁止應用啟動
public static boolean checkRootWhichSU() {
String[] strCmd = new String[] {"/system/xbin/which","su"};
ArrayList<String> execResult = executeCommand(strCmd);
if (execResult != null){
return true;
}else{
return false;
}
}
public static ArrayList<String> executeCommand(String[] shellCmd){
String line = null;
ArrayList<String> fullResponse = new ArrayList<String>();
Process localProcess = null;
try {
localProcess = Runtime.getRuntime().exec(shellCmd);
} catch (Exception e) {
return null;
}
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(localProcess.getOutputStream()));
BufferedReader in = new BufferedReader(new InputStreamReader(localProcess.getInputStream()));
try {
while ((line = in.readLine()) != null) {
fullResponse.add(line);
}
} catch (Exception e) {
e.printStackTrace();
}
return fullResponse;
}

15、不安全的瀏覽器呼叫漏洞

開發者需要在應用呼叫外部瀏覽器時對引擎版本進行檢測,當發現呼叫Chrome V8引擎並且版本低於4.2時停止呼叫外部瀏覽器並且提示使用者對呼叫的系統瀏覽器進行升級或者修改系統預設的瀏覽器。

關注我獲取更多知識或者投稿