• android签名分析及漏洞修复


        本篇我们来看看android的签名机制。发布出来的apk都是有META-INF文件夹,里面包含如下三个文件:

                             

      下面来一一解释这三个文件的作用(打包apk时签名过程):SignApk.main()

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

        遍历APK包中除了META-INF 文件夹以外的所有文件,利用SHA1算法生成这些文件的消息摘要,然后转化为对应的base64编码。MANIFEST.MF存储的是文件的摘要值,保证完整性,防止文件被篡改。anzhi的MANIFEST.MF如下:

    //    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=
    ......

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

        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=
    ......

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

        .RSA / .DSA文件(后缀不同采用的签名算法不同,.RSA使用的是RSA算法, .DSA使用的是数字签名算法DSA,目前APK主要使用的是这两种算法),保存的是第二项.SF文件的数字签名,同时还会包括签名采用的数字证书(公钥—参考资料1)。特别说明,当使用多重证书签名时,每一个.sf文件必须有一个.RSA/.DSA文件与之对应,也就是说使用证书CERT1签名时有CERT1.SF和CERT1.RSA,同时采用证书CERT2签名时又会生成CERT2.SF和CERT2.RSA。

      我们看到这三个文件层层关联,MANIFEST.MF保证apk完整性,CERT.SF对MANIFEST.MF hash来校验,CERT.RSA利用密钥对CERT.SF加密来校验CERT.SF(这里有个问题发现没,若CERT.RSA的密钥被更换,那么...)。但我们也必须认清几点

    1、 Android签名机制其实是对APK包完整性和发布机构唯一性的一种校验机制。

    2、 Android签名机制不能阻止APK包被修改,但修改后的再签名无法与原先的签名保持一致。(拥有私钥的情况除外)。

    3、 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);
                   }
                }
            }
            ......
       }

       代码流程很清晰,

       1、RSA验证SF不被篡改——verifySignature

       2、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以下的所有版本中。

    参考资料:

    1、数字证书原理

    2、【原创】Android证书验证存漏洞 开发者身份信息可被篡改

    3、Android 签名验证机制

    4、Android FakeID(Google Bug 13678484) 漏洞详解

    5、FakeID签名漏洞分析及利用(Google Bug 13678484)

  • 相关阅读:
    $('div','li') 和 $('div , li') 和 $('div li') 区别
    javascript代码放在jsp页面中的位置总结
    使用spring @Scheduled注解执行定时任务
    Mybatis学习之与Spring整合
    Mybatis学习之注解
    Mybatis学习之一对多关联查询
    Jenkins Pipeline
    2020-11-22 Windows随笔
    Python BeautifulSoup4合并table单元格
    python call cmd
  • 原文地址:https://www.cnblogs.com/vendanner/p/5111116.html
Copyright © 2020-2023  润新知