1. 程式人生 > >Android客戶端程式碼保護技術-完整性校驗

Android客戶端程式碼保護技術-完整性校驗

       由於Android系統固有的缺陷、Android應用分發渠道管理機制等問題,導致Android客戶端程式很容易被反編譯篡改/二次打包,經任意簽名後可在各個渠道或論壇中釋出,這不僅損害了開發者的智慧財產權,更可能威脅到使用者的敏感資訊及財產安全,因此客戶端程式自身的安全性尤為重要,本文以客戶端完整校驗為主題,提供幾種Android客戶端完整性校驗的實現思路,供廣大開發者參考。

思路1:對classes.dex檔案完整性校驗

       Android工程程式碼中的所有java程式碼經編譯和優化最終生成Dalvik虛擬機器可執行的DEX檔案,DEX檔案最終會打包在APK檔案中,針對APK程式碼的篡改攻擊就是針對該檔案,如通常使用apktool反編譯APK檔案,修改smali程式碼。APK包中的DEX檔案如下圖所示:

       因此,我們可以設計編寫校驗程式碼,實現在應用程式啟動時計算所程式安裝包中的classes.dex檔案的雜湊值(可通過CRC32、MD5等摘要演算法計算得到),然後與預先計算的dex檔案的雜湊值(該值可儲存在程式碼或配置檔案中、也可與伺服器通訊互動獲取)進行比較,以此驗證程式碼檔案是否被篡改。

       通過檢查classes.dex檔案的CRC32摘要值來判斷檔案是否被篡改的java實現程式碼如下所示:

    /**
     * 通過檢查classes.dex檔案的CRC32摘要值來判斷檔案是否被篡改
     *
     * @param orginalCRC 原始classes.dex檔案的CRC值
     */
    public static void apkVerifyWithCRC(Context context, String orginalCRC) {
        String apkPath = context.getPackageCodePath(); // 獲取Apk包儲存路徑
        try {
            ZipFile zipFile = new ZipFile(apkPath);
            ZipEntry dexEntry = zipFile.getEntry("classes.dex"); // 讀取ZIP包中的classes.dex檔案
            String dexCRC = String.valueOf(dexEntry.getCrc()); // 得到classes.dex檔案的CRC值
            if (!dexCRC.equals(orginalCRC)) { // 將得到的CRC值與原始的CRC值進行比較校驗
                Process.killProcess(Process.myPid()); // 驗證失敗則退出程式
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

思路2:對apk包做完整性校驗

       如果對apk包進行篡改,必會影響apk包的完整性校驗值,因此根據思路1,我們也可以對整個apk包做雜湊校驗。

       通過檢查apk包的MD5摘要值來判斷程式碼檔案是否被篡改的java實現程式碼如下圖所示:

    /**
     * 通過檢查apk包的MD5摘要值來判斷程式碼檔案是否被篡改
     *
     * @param orginalMD5 原始Apk包的MD5值
     */
    public static void apkVerifyWithMD5(Context context, String orginalMD5) {
        String apkPath = context.getPackageCodePath(); // 獲取Apk包儲存路徑
        try {
            MessageDigest dexDigest = MessageDigest.getInstance("MD5");
            byte[] bytes = new byte[1024];
            int byteCount;
            FileInputStream fis = new FileInputStream(new File(apkPath)); // 讀取apk檔案
            while ((byteCount = fis.read(bytes)) != -1) {
                dexDigest.update(bytes, 0, byteCount);
            }
            BigInteger bigInteger = new BigInteger(1, dexDigest.digest()); // 計算apk檔案的雜湊值
            String sha = bigInteger.toString(16);
            fis.close();
            if (!sha.equals(orginalMD5)) { // 將得到的雜湊值與原始的雜湊值進行比較校驗
                Process.killProcess(Process.myPid()); // 驗證失敗則退出程式
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

思路3:對簽名檔案中classes.dex雜湊值的校驗

        Android工程程式碼經編譯打包生成apk包後,開發者需要對其簽名才能在安卓市場上釋出供使用者下載和安裝。對apk包簽名後,會在原apk包結構基礎上加入META-INF檔案目錄。簽名後的apk包檔案目錄如下圖所示:

       META-INF檔案目錄下含有三個檔案:MANIFEST.MF檔案、ANDROIDD.SF檔案、ANDROIDD.RSA檔案,META_INF目錄檔案結構如下圖所示:

       其中,MANIFEST.MF檔案描述了在簽名時,簽名工具對apk包中各個檔案摘要計算後的雜湊值,並對雜湊值做了Base64編碼。MANIFEST.MF檔案中描述的classes.dex檔案的SHA-1雜湊值如下圖所示:

       一旦攻擊者對APK中反編譯並篡改程式碼,經二次打包簽名後的classes.dex檔案的SHA-1必定改變,因此,我們可以將該檔案中的classes.dex檔案的SHA-1雜湊值儲存起來作為校驗對比值,應用程式啟動時讀取apk安裝包中的MANIFEST.MF檔案,解析出classes.dex的SHA-1雜湊值,然後與原SHA-1雜湊值進行比較,判斷此APK包程式碼檔案是否被篡改。

       通過檢查簽名檔案classes.dex檔案的雜湊值來判斷程式碼檔案是否被篡改的java實現程式碼如下所示:

    /**
     * 通過檢查簽名檔案classes.dex檔案的雜湊值來判斷程式碼檔案是否被篡改
     *
     * @param orginalSHA 原始Apk包的SHA-1值
     */
    public static void apkVerifyWithSHA(Context context, String orginalSHA) {
        String apkPath = context.getPackageCodePath(); // 獲取Apk包儲存路徑
        try {
            MessageDigest dexDigest = MessageDigest.getInstance("SHA-1");
            byte[] bytes = new byte[1024];
            int byteCount;
            FileInputStream fis = new FileInputStream(new File(apkPath)); // 讀取apk檔案
            while ((byteCount = fis.read(bytes)) != -1) {
                dexDigest.update(bytes, 0, byteCount);
            }
            BigInteger bigInteger = new BigInteger(1, dexDigest.digest()); // 計算apk檔案的雜湊值
            String sha = bigInteger.toString(16);
            fis.close();
            if (!sha.equals(orginalSHA)) { // 將得到的雜湊值與原始的雜湊值進行比較校驗
                Process.killProcess(Process.myPid()); // 驗證失敗則退出程式
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

       以上三種完整性校驗實現思路的實現程式碼樣例採用Java語言實現,從安全形度來講,很容易通過反編譯篡改patch掉,因此在實現完整性校驗程式碼時還需參考以下幾點建議:

       1.預先計算的dex檔案的雜湊值、簽名檔案的classes.dex的SHA-1雜湊值,應避免直接明文硬編碼儲存在程式碼或配置檔案中,可對其採用非對稱加密儲存,或採取與服務端通訊的方式獲取。

       2.由於dex檔案很容易通過dex2jar、apktool反編譯後逆向分析和破解,因此該完整性校驗功能可進一步使用C/C++程式碼進行編寫實現。另外,進一步提高安全性,還可通過原始碼混淆,如:開源的obfuscator-llvm專案,或對.so動態庫加殼,增加逆向分析和破解的難度。