Android下載文件合法性完整性校驗

轉載自:http://blog.csdn.net/l2show/article/details/48182367

一.概述

       因爲之前項目有動態熱修復的功能,在修復的過程中會從服務器上下載一個新的dex文件來替換老的dex文件,所以就牽扯到文件身份效驗的問題.通常接口會下發一個MD5值,只是一個MD5值的話就只能做一個完整性效驗,並不能確定文件的合法性,如果攻擊者模擬接口下發一個正確的MD5值,照樣可以替換文件.所以這裏就在效驗MD5完整性之後再根據簽名做合法性效驗.

二.實現

1.文件完整性效驗

       這裏字符串取MD5就不做贅述了.既然要效驗文件的完整性,那麼就牽扯到取文件MD5摘要,這裏是用JDK中的MessageDigest通過讀取文件的二進制流,使用update累計更新文件流的MD5摘要來獲取整個文件的MD5摘要.拿到文件MD5之後跟接口下發的對比就OK了.所以單純做文件完整性效驗還是很簡單的.

[java] view plain copy
  1. /** 
  2.  * get file md5 
  3.  * @param file 
  4.  * @return 
  5.  * @throws NoSuchAlgorithmException 
  6.  * @throws IOException 
  7.  */  
  8. private static String getFileMD5(File file) throws NoSuchAlgorithmException, IOException {  
  9.     if (!file.isFile()) {  
  10.         return null;  
  11.     }  
  12.     MessageDigest digest;  
  13.     FileInputStream in;  
  14.     byte buffer[] = new byte[1024];  
  15.     int len;  
  16.     digest = MessageDigest.getInstance("MD5");  
  17.     in = new FileInputStream(file);  
  18.     while ((len = in.read(buffer, 01024)) != -1) {  
  19.         digest.update(buffer, 0, len);  
  20.     }  
  21.     in.close();  
  22.     BigInteger bigInt = new BigInteger(1, digest.digest());  
  23.     return bigInt.toString(16);  
  24. }  
2.文件合法性效驗

       文件合法性的效驗相比完整性效驗就要複雜不少,這裏合法性是根據簽名來做的,所以起碼要簡單瞭解簽名的過程,簽名之後文件發生了什麼變化和怎麼獲取文件的簽名信息等.
       1)簽名後產出

       生成簽名文件和簽名的過程這裏就不細說了,主要分析一下簽名之後的一些情況.解壓簽名之後的文件可以看到經過簽名生成出來了一個META-INF文件夾,裏面一般都是三個文件用來存放所有文件的效驗以及簽名信息.


       MANIFEST.MF:可以明文查看,對所有文件取BASE64哈希值.


       ANDROIDK.SF:可以明文查看,對所有文件的前三行取BASE64哈希值.


       ANDROIDK.RSA:前面兩個文件都只是對文件做了一個哈希摘要,並沒有簽名公鑰等信息,RSA文件裏面就包含了我們所需要的信息,其中包含有開發者信息,開發者公鑰以及CA根據前兩個文件的摘要信息經過私鑰加密後的密文.RSA文件是不能查看明文的,這裏可以使用openssl的命令來輸出該文件的信息.openssl pkcs7 -inform DER -in ANDROIDK.RSA -noout -print_certs -text, 從這個文件的數據結構來看本章需要用到的其實就是公鑰,獲取文件自身證書信息的公鑰,然後對比app自身的簽名公鑰就可以判斷文件的合法性.


       2)獲取app自身簽名

       通過上面的介紹可以瞭解到簽過名的文件都可以獲取到一個基於RSA算法的RSA public key,這裏就通過效驗這個公鑰的方式來驗證合法性,app自身的簽名信息可以通過PackageInfo獲取,獲取到之後經過字符串轉換和截取,將RSA public key部分摘取出來就OK了.

[java] view plain copy
  1. /** 
  2.  * get local app rsa public key 
  3.  * @param ctx 
  4.  * @return 
  5.  * @throws IOException 
  6.  * @throws PackageManager.NameNotFoundException 
  7.  * @throws CertificateException 
  8.  */  
  9. private static String getLocalSignature(Context ctx) throws IOException,  
  10.         PackageManager.NameNotFoundException, CertificateException {  
  11.     String signCode = null;  
  12.     //get signature info depends on package name  
  13.     PackageInfo packageInfo = ctx.getPackageManager().getPackageInfo(  
  14.             ctx.getPackageName(), PackageManager.GET_SIGNATURES);  
  15.     Signature[] signs = packageInfo.signatures;  
  16.     Signature sign = signs[0];  
  17.     CertificateFactory certFactory = CertificateFactory  
  18.             .getInstance("X.509");  
  19.     X509Certificate cert = (X509Certificate) certFactory  
  20.             .generateCertificate(new ByteArrayInputStream(sign.toByteArray()));  
  21.     String pubKey = cert.getPublicKey().toString();  
  22.     String ss = subPublicSignature(pubKey);  
  23.     ss = ss.replace(",""");  
  24.     ss = ss.toLowerCase();  
  25.     int aa = ss.indexOf("modulus");  
  26.     int bb = ss.indexOf("publicexponent");  
  27.     signCode = ss.substring(aa + 8, bb);  
  28.   
  29.     return signCode;  
  30. }  
       3)獲取外部文件簽名
       獲取外部文件簽名的過程其實可以參考Android內部效驗apk文件的過程,Android源碼中的PackageParser類會在安裝apk之前對apk文件做合法性效驗,但是遺憾的是這個類被標註爲hide了,所以我們不能直接使用.那麼就只剩下兩個方法了,一個是通過反射使用PackageParser的方法,一個看源碼中效驗這部分的實現然後摳出來自己實現.

[java] view plain copy
  1. /** 
  2.  * Package archive parsing 
  3.  * 
  4.  * {@hide} 
  5.  */  
  6. public class PackageParser {  
  7.     //source/frameworks/base/core/java/android/content/pm  
  8. }  
       在當前的使用場景下不推薦使用反射,一是使用反射降低效率還有風險,二是效驗這部分並沒有依賴Android其他部分,只是依賴了JDK中的JarFile.所以扣源碼自己實現來得比較實在,這裏就不分析使用反射驗證的過程了,直接上源碼.

       從PackageParser類中的collectCertificates方法中可以看到如下代碼片段.首先根據文件路徑將簽名後的apk,jar或zip文件裝載到JarFile中(JarFile是繼承自ZipFile),然後獲取文件內容部的某個文件(這部分代碼塊是獲取的manifest文件),再獲取到該文件的證書信息.只要能拿到證書信息,那麼拿到公鑰什麼的都是小case了.

[java] view plain copy
  1. public boolean collectCertificates(Package pkg, int flags) {  
  2.     //.................  
  3.     JarFile jarFile = new JarFile(mArchiveSourcePath);  
  4.     //.................  
  5.     JarEntry jarEntry = jarFile.getJarEntry(ANDROID_MANIFEST_FILENAME);  
  6.     //.................  
  7.     certs = loadCertificates(jarFile, jarEntry, readBuffer);  
  8.     pkg.mSignatures = null;  
  9.     //.................  
  10. }  
       這個loadCertificates方法需要特別說一下,因爲一開始我是看完源碼之後自己寫實現的,看這個方法的時候每注意註釋,就把讀文件流什麼也沒幹的步驟略過了,直接通過JarEntry.getCertificates獲取證書.結果換了好幾個簽過名的文件都獲取不到證書,重新看了下源碼才發現註釋中的必須使用JarEntry讀文件流才能接收到證書信息......不作死就不會死.拿到證書之後就跟之前2)中的步驟一樣了,直接get公鑰,然後截取字符串將RSA public key截出來,最後跟2)中的結果比對就可以做合法性效驗了.

[java] view plain copy
  1. private static Certificate[] loadCertificates(JarFile jarFile, JarEntry je,  
  2.                                               byte[] readBuffer) throws IOException {  
  3.     // We must read the stream for the JarEntry to retrieve  
  4.     // its certificates.  
  5.     InputStream is = new BufferedInputStream(jarFile.getInputStream(je));  
  6.     while (is.read(readBuffer, 0, readBuffer.length) != -1) {  
  7.         // not using  
  8.     }  
  9.     is.close();  
  10.     return je != null ? je.getCertificates() : null;  
  11. }  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章