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動態庫加殼,增加逆向分析和破解的難度。