android簽名分析及漏洞修復

    本篇咱們來看看android的簽名機制。發佈出來的apk都是有META-INF文件夾,裏面包含以下三個文件:php

                         

  下面來一一解釋這三個文件的做用(打包apk時簽名過程):SignApk.main()html

  一、MANIFEST.MF:/build/tools/signapk/SignApk.java-addDigestsToManifest()java

    遍歷APK包中除了META-INF\ 文件夾之外的全部文件,利用SHA1算法生成這些文件的消息摘要,而後轉化爲對應的base64編碼。MANIFEST.MF存儲的是文件的摘要值,保證完整性,防止文件被篡改。anzhi的MANIFEST.MF以下:android

//    Manifest-Version: 1.0
//    Created-By: 1.0 (Android)

//    Name: res/layout/act_header.xml
//    SHA1-Digest: tiVog/vCbIpPfnZbtZOxN28MKIE=

//    Name: res/drawable-hdpi/bg_top_list_index_red.9.png
//    SHA1-Digest: Y91AQINPN6Y7pkZ6qnQuSVcwLfw=
......

  二、CERT.SF:/build/tools/signapk/SignApk.java-writeSignatureFile()
git

    xx.SF文件(xx爲使用者證書的自定義別名,默認爲CERT,即CERT.SF),保存的是MANIFEST.MF的摘要值, 以及MANIFEST.MF中每個摘要項的摘要值,而後轉化成對應的base64編碼。雖然該文件的後綴名.sf(SignatureFile)看起來是簽名文件,可是並無私鑰參與運算,也不保存任何簽名內容。anzhi的CERT.SF:算法

//    Signature-Version: 1.0
//    Created-By: 1.0 (Android)
//     SHA1-Digest-Manifest: GBijl3ytIYpo7tJr1NgfkgssLWA=

//     Name: res/layout/act_header.xml
//     SHA1-Digest: 2KdEJyEwgrLAHZTdwEpnH6Ud4pE=

//     Name: res/drawable-hdpi/bg_top_list_index_red.9.png
//     SHA1-Digest: jfdrZJNisF8zAIexeGba0VuZSMU=
......

   三、CERT.RSA:/build/tools/signapk/SignApk.java-writeSignatureBlock()apache

 

    .RSA / .DSA文件(後綴不一樣採用的簽名算法不一樣,.RSA使用的是RSA算法, .DSA使用的是數字簽名算法DSA,目前APK主要使用的是這兩種算法),保存的是第二項.SF文件的數字簽名,同時還會包括簽名採用的數字證書(公鑰—參考資料1)。特別說明,當使用多重證書籤名時,每個.sf文件必須有一個.RSA/.DSA文件與之對應,也就是說使用證書CERT1簽名時有CERT1.SF和CERT1.RSA,同時採用證書CERT2簽名時又會生成CERT2.SF和CERT2.RSA。app

  咱們看到這三個文件層層關聯,MANIFEST.MF保證apk完整性,CERT.SF對MANIFEST.MF hash來校驗,CERT.RSA利用密鑰對CERT.SF加密來校驗CERT.SF(這裏有個問題發現沒,若CERT.RSA的密鑰被更換,那麼...)。但咱們也必須認清幾點函數

一、 Android簽名機制實際上是對APK包完整性和發佈機構惟一性的一種校驗機制。ui

二、 Android簽名機制不能阻止APK包被修改,但修改後的再簽名沒法與原先的簽名保持一致。(擁有私鑰的狀況除外)。

三、 APK包加密的公鑰就打包在APK包內,且不一樣的私鑰對應不一樣的公鑰。換句話言之,不一樣的私鑰簽名的APK公鑰也必不相同。因此咱們能夠根據公鑰的對比,來判斷私鑰是否一致。

  剛剛上面說了CERT.RSA的密鑰的被更換,事情就大條了。如今咱們看看在安裝apk時android中是如何進行簽名驗證的。

/libcore/luni/src/main/java/java/util/jar/JarVerifier.java
synchronized boolean readCertificates() { ... Iterator<String> it = metaEntries.keySet().iterator(); while (it.hasNext()) { String key = it.next(); if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) { verifyCertificate(key); // Check for recursive class load if (metaEntries == null) { return false; } it.remove(); } } return true; }

  readCertificates找以".DSA"、".RSA"、".EC"結尾的文件,讓verifyCertificate來校驗

private void verifyCertificate(String certFile) {
        // Found Digital Sig, .SF should already have been read
        String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
        byte[] sfBytes = metaEntries.get(signatureFile);
        if (sfBytes == null) {
            return;
        }

       byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME);
       // Manifest entry is required for any verifications.
       if (manifest == null) {
           return;
       }

        byte[] sBlockBytes = metaEntries.get(certFile);
        try {//verifySignature驗證SF文件
            Certificate[] signerCertChain = JarUtils.verifySignature(
                    new ByteArrayInputStream(sfBytes),
                    new ByteArrayInputStream(sBlockBytes));
         ......    
    // Verify manifest hash in .sf file
        Attributes attributes = new Attributes();
        HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
        try {
            ManifestReader im = new ManifestReader(sfBytes, attributes);
            im.readEntries(entries, null);
        } catch (IOException e) {
        return;
        }
        // Use .SF to verify the mainAttributes of the manifest
        // If there is no -Digest-Manifest-Main-Attributes entry in .SF
        // file, such as those created before java 1.5, then we ignore
        // such verification.
        if (mainAttributesEnd > 0 && !createdBySigntool) {
            String digestAttribute = "-Digest-Manifest-Main-Attributes";
            if (!verify(attributes, digestAttribute, manifest, 0, mainAttributesEnd, false, true)) {
                throw failedVerification(jarName, signatureFile);
            }
        }

        // Use .SF to verify the whole manifest.
        String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
        if (!verify(attributes, digestAttribute, manifest, 0, manifest.length, false, false)) {
            Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, Attributes> entry = it.next();
                Manifest.Chunk chunk = man.getChunk(entry.getKey());
                if (chunk == null) {
                    return;
                }
                if (!verify(entry.getValue(), "-Digest", manifest,
                        chunk.start, chunk.end, createdBySigntool, false)) {
                    throw invalidDigest(signatureFile, entry.getKey(), jarName);
               }
            }
        }
        ......
   }

   代碼流程很清晰,

   一、RSA驗證SF不被篡改——verifySignature

   二、SF驗證MF文件不被篡改

      在哪裏驗證apk文件有沒有篡改啊?(即驗證MF文件和app文件,等下分析哦)

  繼續看verifySignature(不要忘了咱們是來看RSA中的密鑰如何認證的哦);但在分析源碼以前你先看參考資料1和下面這幅證書鏈  

                       

                                                 證書鏈示意圖

/libcore/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java

public static Certificate[] verifySignature(InputStream signature, InputStream
            signatureBlock) throws IOException, GeneralSecurityException {
            ......
            return createChain(certs[issuerSertIndex], certs);
        }
private static X509Certificate[] createChain(X509Certificate  signer, X509Certificate[] candidates) {
        LinkedList chain = new LinkedList();
        chain.add(0, signer);

        // Signer is self-signed
        if (signer.getSubjectDN().equals(signer.getIssuerDN())){
            return (X509Certificate[])chain.toArray(new X509Certificate[1]);
        }

        Principal issuer = signer.getIssuerDN();
        X509Certificate issuerCert;
        int count = 1;
        while (true) {
            issuerCert = findCert(issuer, candidates);
            if( issuerCert == null) {
                break;
            }
            chain.add(issuerCert);
            count++;
            // 遞歸到根認證CA
            if (issuerCert.getSubjectDN().equals(issuerCert.getIssuerDN())) {
                break;
            }
            issuer = issuerCert.getIssuerDN();
        }
        return (X509Certificate[])chain.toArray(new X509Certificate[count]);
    }

    private static X509Certificate findCert(Principal issuer, X509Certificate[] candidates) {
        for (int i = 0; i < candidates.length; i++) {
            // 只用字符串來判斷
            if (issuer.equals(candidates[i].getSubjectDN())) {
                return candidates[i];
            }
        }
        return null;
    }

   看上圖證書鏈咱們可知,owner證書有效的前提是CA證書有效,而CA證書有效的前提是ROOT CA證書有效,ROOT CA證書的有效性由操做系統驗證。而在android系統裏,這部分由createChain函數來執行。createChain中用owner證書的IssuserDN—CA經過findCert函數來查找是否存在CA證書。findCert裏遍歷證書查看是否有證書的subjectDN == CA,若是有則表示此證書爲CA證書(若是不理解請繼續看參考資料1和證書鏈示意圖)。看得出這個findCert太隨意了值找證書而沒有Verify signature,致使這裏有bug,對此谷歌的修復方案以下

private static X509Certificate findCert(Principal issuer, X509Certificate[] candidates, X509Certificate subjectCert, boolean chainCheck) {  
    for (int i = 0; i < candidates.length; i++) {  
        if (issuer.equals(candidates[i].getSubjectDN())) {  
            if (chainCheck) {  
                try {  
                    subjectCert.verify(candidates[i].getPublicKey());  
                } catch (Exception e) {  
                    continue;  
                }  
            }  
            return candidates[i];  
        }  
    }  
    return null;  
} 

 

  ok,簽名原理搞清楚了,咱們來看看上面提到的bug利用,此bug存在android4.4.1如下的全部版本中。

 

 

參考資料:

一、數字證書原理

二、【原創】Android證書驗證存漏洞 開發者身份信息可被篡改

三、Android 簽名驗證機制

四、Android FakeID(Google Bug 13678484) 漏洞詳解

五、FakeID簽名漏洞分析及利用(Google Bug 13678484)

相關文章
相關標籤/搜索