美文网首页
Android应用的验签过程分析

Android应用的验签过程分析

作者: N8_xEnn7nA1 | 来源:发表于2017-07-17 17:59 被阅读0次

0x01 回顾

分析验签过程之前,有必要先回顾一下Android应用的签名过程:

  1. 对APK包中的每个文件做一次运算(Hash+Base64编码),将结果保存到META-INF/MANIFEST.MF文件中;
  2. 对MANIFEST.MF整个文件做一次运算(Hash+Base64编码),将结果保存到META-INF/CERT.SF文件的头属性中,再对MANIFEST.MF文件中的各个属性块做同样的运算(Hash+Base64编码),存放到CERT.SF的属性块中。
  3. 开发者用自己的私钥对CERT.SF进行签名,并将签名信息和包含公钥信息的数字证书一同保存到META-INF/CERT.RSA文件中。

因此,应用的验签过程其实也是围绕这三步来进行的。

0x02 相关源码的位置(AOSP 5.0.1_r1)

  • frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
  • frameworks/base/core/java/android/content/pm/PackageParser.java
  • libcore/luni/src/main/java/java/util/jar/StrictJarFile.java
  • libcore/luni/src/main/java/java/util/jar/JarVerifier.java
  • libcore/luni/src/main/java/java/util/jar/JarFile.java
  • libcore/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java

0x03 源码分析

APK的安装过程主要是由PackageManagerService这个核心服务类来完成的,所以我们可以从这个类入手,其中开始执行签名校验的在installPackageLI方法里,代码如下:

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
        ......
        PackageParser pp = new PackageParser();
        ......
       try {
            pp.collectCertificates(pkg, parseFlags);
            pp.collectManifestDigest(pkg);
        } catch (PackageParserException e) {
            res.setError("Failed collect during installPackageLI", e);
            return;
        }
 }

在这个方法中可以看到一个用来解析传入的APK包的类PackageParser,并且这里调用了PackageParser.collectCertificates方法来进行签名的校验。于是进入该方法:

public void collectCertificates(Package pkg, int flags) throws PackageParserException {
        ......
        collectCertificates(pkg, new File(pkg.baseCodePath), flags);
       ......
    }

如上,该方法由调用了一个函数重载,代码如下:

private static void collectCertificates(Package pkg, File apkFile, int flags)
            throws PackageParserException {
        final String apkPath = apkFile.getAbsolutePath();

        StrictJarFile jarFile = null;
        try {
            jarFile = new StrictJarFile(apkPath);
            ......
    }

由于该方法的代码较长,我们先分段看,先看上面的代码,很明显,是通过传入的apk文件来构造一个StrictJarFile对象,下面来看一下它的构造方法都做了些什么事情:

public StrictJarFile(String fileName) throws IOException {
        ......
        try {
            ......
            HashMap<String, byte[]> metaEntries = getMetaEntries();
            this.manifest = new Manifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
            this.verifier = new JarVerifier(fileName, manifest, metaEntries);

            isSigned = verifier.readCertificates() && verifier.isSignedJar();
      ......
    }

如上,首先调用getMetaEntries()方法将META-INF目录下每一个文件的文件名及其数据流存放到metaEntries这个HashMap对象中;然后通过MANIFEST.MF文件的数据流构造一个Manifest对象;接着利用得到的metaEntries和manifest来构造一个JarVerifier对象,最后调用JarVerifier的readCertificates()方法和isSignedJar()方法。下面先看JarVerifier.readCertificates方法:

synchronized boolean readCertificates() {
        if (metaEntries.isEmpty()) {
            return false;
        }

        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);
                it.remove();
            }
        }
        return true;
    }

如上,该方法首先判断META-INF目录是否为空,如果为空表示根本没签名,直接返回false。不为空的话就对metaEntries对象进行遍历,如果是证书文件,则将其传入verifyCertificate()方法进行校验,JarVerifier.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);
        ......
        byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
        ......
        byte[] sBlockBytes = metaEntries.get(certFile);
        try {
            Certificate[] signerCertChain = JarUtils.verifySignature(
                    new ByteArrayInputStream(sfBytes),
                    new ByteArrayInputStream(sBlockBytes));
            if (signerCertChain != null) {
                certificates.put(signatureFile, signerCertChain);
            }
        } catch (IOException e) {
            return;
        } catch (GeneralSecurityException e) {
            throw failedVerification(jarName, signatureFile);
        }
        ......
    }

该方法首先通过传入的证书文件<CERT>.RSA的路径来获取<CERT>.SF的路径,然后通过前面得到的metaEntries来分别取得MANIFEST.MF、CERT.SF、CERT.RSA这三个文件的字节流:manifestBytes、sfBytes、sBlockBytes,然后将sfBytes和sBlockBytes传入JarUtils.verfySignature()方法中,进行数字签名的校验,校验的过程这里就不贴代码了,简单说就是用CERT.RSA这个文件中的包含的公钥对数字签名进行解密,将解密后的结果与CERT.SF文件hash运算后的结果进行比对,一致的话就返回证书链信息,并将证书链保存在certificates对象中,同时说明CERT.SF文件没有被篡改,另外,Jarverifier.isSignedJar()方法就是判断certificates是否为空,不为空返回true,空则返回false。否则就抛出GeneralSecurityException异常。接着上面继续看JarVerifier.verifyCertificate()方法:

        // 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;
        }

        // Do we actually have any signatures to look at?
        if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
            return;
        }

        boolean createdBySigntool = false;
        String createdBy = attributes.getValue("Created-By");

        if (mainAttributesEnd > 0 && !createdBySigntool) {
            String digestAttribute = "-Digest-Manifest-Main-Attributes";
            if (!verify(attributes, digestAttribute, manifestBytes, 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, manifestBytes, 0, manifestBytes.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 = manifest.getChunk(entry.getKey());
                if (chunk == null) {
                    return;
                }
                if (!verify(entry.getValue(), "-Digest", manifestBytes,
                        chunk.start, chunk.end, createdBySigntool, false)) {
                    throw invalidDigest(signatureFile, entry.getKey(), jarName);
                }
            }
        }
        metaEntries.put(signatureFile, null);
        signatures.put(signatureFile, entries);

根据代码中的注释也能够很清晰的了解到,这段代码主要就是通过读取CERT.SF,然后来验证MANIFEST.MF文件是否被篡改。来先看一下CERT文件的部分内容:



结合上图,再回到JarVerifier.verifyCertificate()方法的代码中来分析一下具体的流程吧:

首先读取CERT.SF文件,并创建与之相关的两个对象attributes和entries;
接着就通过attributes对象判断CERT.SF文件中是否存在"Signature-Version"属性,没有的话直接返回;
再判断CERT.SF文件中的"Created-By"属性的值是否包含"signtool"子串,有的话表示该apk是用其他签名工具签的名;如上图,这里用的是JDK自带jarsigner签的名,所以不含signtool字符串,这样的话之后就会调用JarVerifier.verify()方法来判断是否有"SHA1-Digest-Manifest-Main-Attributes"属性,有的话就校验它的值,看它是否为MANIFEST.MF的头属性块运算(Hash+Base64编码)后的值。可以看到,JarVerifier.verify()方法的第三个参数传的就是MANIFEST.MF的字节流。
JarVerifier.verify()方法的代码如下:

private boolean verify(Attributes attributes, String entry, byte[] data,
            int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
        for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
            String algorithm = DIGEST_ALGORITHMS[i];
            String hash = attributes.getValue(algorithm + entry);
            if (hash == null) {
                continue;
            }

            MessageDigest md;
            try {
                md = MessageDigest.getInstance(algorithm);
            } catch (NoSuchAlgorithmException e) {
                continue;
            }
            if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') {
                md.update(data, start, end - 1 - start);
            } else {
                md.update(data, start, end - start);
            }
            byte[] b = md.digest();
            byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
            return MessageDigest.isEqual(b, Base64.decode(hashBytes));
        }
        return ignorable;
    }

在这个方法中,由于不知道用的什么Hash算法,所以会遍历DIGEST_ALGORITHMS数组,该数组的内容如下:

private static final String[] DIGEST_ALGORITHMS = new String[] {
    "SHA-512",
    "SHA-384",
    "SHA-256",
    "SHA1",
};

将遍历到的算法名与字符串"-Digest-Manifest-Main-Attributes"组合,然后判断该属性是否存在,不存在则略过(continue;)。然后就是hash值的比对了。

再次回到JarVerifier.verifyCertificate()方法的代码:
接下来就是再次调用JarVerifier.verifier()方法,不过这次是对MANIFEST.MF整个文件的Hash与CERT.SF的"SHA1--Digest-Manifest"属性的值进行比对,如果一致,则说明MANIFEST.MF没有被篡改,并将CERT.SF文件的信息添加到metaEntries和signatures的属性中。如果不一致,则遍历所有的属性块,看是哪一个属性块的值不正确。

到这里,StrictJarFile的构造方法就完成了。从上面的分析可以看到,验签的三个步骤中,有两步是再StrictJarFile的构造方法中完成的,分别是:CERT.SF是否被篡改,MANIFEST.MF是否被篡改。

接下来,让我们再回到PackageParser.collectCertificates()方法中,继续完成后续的校验分析,代码如下:

private static void collectCertificates(Package pkg, File apkFile, int flags)
            throws PackageParserException {
        final String apkPath = apkFile.getAbsolutePath();

        StrictJarFile jarFile = null;
        try {
            jarFile = new StrictJarFile(apkPath);

            // Always verify manifest, regardless of source
            final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
            if (manifestEntry == null) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
                        "Package " + apkPath + " has no manifest");
            }

            final List<ZipEntry> toVerify = new ArrayList<>();
            toVerify.add(manifestEntry);

            // If we're parsing an untrusted package, verify all contents
            if ((flags & PARSE_IS_SYSTEM) == 0) {
                final Iterator<ZipEntry> i = jarFile.iterator();
                while (i.hasNext()) {
                    final ZipEntry entry = i.next();

                    if (entry.isDirectory()) continue;
                    if (entry.getName().startsWith("META-INF/")) continue;
                    if (entry.getName().equals(ANDROID_MANIFEST_FILENAME)) continue;

                    toVerify.add(entry);
                }
            }

            for (ZipEntry entry : toVerify) {
                final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
                ......
            }
        } catch (GeneralSecurityException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
                    "Failed to collect certificates from " + apkPath, e);
        } catch (IOException | RuntimeException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "Failed to collect certificates from " + apkPath, e);
        } finally {
            closeQuietly(jarFile);
        }
    }

如上,在创建了StrictJarFile对象后,就对该对象进行遍历,将除了目录和META-INF目录下的文件外的所有文件的ZipEntry对象添加到toVerify这个列表中。然后遍历该列表,将每一个文件代表的ZipEntry对象传入PackageParser.loadCertificates()方法中,代码如下:

private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
            throws PackageParserException {
        InputStream is = null;
        try {
            // We must read the stream for the JarEntry to retrieve
            // its certificates.
            is = jarFile.getInputStream(entry);
            readFullyIgnoringContents(is);
            return jarFile.getCertificateChains(entry);
        } catch (IOException | RuntimeException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
                    "Failed reading " + entry.getName() + " in " + jarFile, e);
        } finally {
            IoUtils.closeQuietly(is);
        }
    }

这里调用了StrictJarFile.getInputStream()方法来获取InputStream对象,看下该方法:

public InputStream getInputStream(ZipEntry ze) {
        final InputStream is = getZipInputStream(ze);

        if (isSigned) {
            JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
            if (entry == null) {
                return is;
            }

            return new JarFile.JarFileInputStream(is, ze.getSize(), entry);
        }

        return is;
    }

这里主要是获取JarVerifier.VerifierEntry对象,最后返回一个JarFile.JarFileInputStream对象。看一下JarVerifier.initEntry()方法:

VerifierEntry initEntry(String name) {
        // If no manifest is present by the time an entry is found,
        // verification cannot occur. If no signature files have
        // been found, do not verify.
        if (manifest == null || signatures.isEmpty()) {
            return null;
        }

        Attributes attributes = manifest.getAttributes(name);
        // entry has no digest
        if (attributes == null) {
            return null;
        }

        ArrayList<Certificate[]> certChains = new ArrayList<Certificate[]>();
        Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
            HashMap<String, Attributes> hm = entry.getValue();
            if (hm.get(name) != null) {
                // Found an entry for entry name in .SF file
                String signatureFile = entry.getKey();
                Certificate[] certChain = certificates.get(signatureFile);
                if (certChain != null) {
                    certChains.add(certChain);
                }
            }
        }

        // entry is not signed
        if (certChains.isEmpty()) {
            return null;
        }
        Certificate[][] certChainsArray = certChains.toArray(new Certificate[certChains.size()][]);

        for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
            final String algorithm = DIGEST_ALGORITHMS[i];
            final String hash = attributes.getValue(algorithm + "-Digest");
            if (hash == null) {
                continue;
            }
            byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);

            try {
                return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
                        certChainsArray, verifiedEntries);
            } catch (NoSuchAlgorithmException ignored) {
            }
        }
        return null;
    }

该方法就是创建一个JarVerifier.VerifierEntry对象:
第一个参数name是文件名;
第二个参数是用来产生摘要的对象MessageDigest,且摘要算法algorithm也是同前面的方法一样,从DIGEST_ALGORITHMS数组中遍历,再根据MANIFEST.MF文件的属性名来得到;
第三个参数是MANIFEST.MF中所保存的对应文件名的Hash值;
MANIFEST.MF的部分内容如下:


第四个参数是对该APK进行签名的所有证书链信息。它为什么是二维数组?是因为Android允许用多个证书对apk进行签名,且它们的证书文件名必须不同。
最后一个参数是已经验证过的文件列表,VerifierEntry在完成了对指定文件的摘要验证之后会将该文件的信息加到其中。

接着,来看一下JarFile.JarFileInputStream的构造方法:

JarFileInputStream(InputStream is, long size, JarVerifier.VerifierEntry e) {
            super(is);
            entry = e;

            count = size;
        }

只是几个赋值操作,其中将前面得到的JarVerifier.VerifierEntry对象传入并赋值给这里的entry。

将视线在回到PackageParser.loadCertificates()方法中,经过上面的分析,StrictJarFile.getInputStream()所返回的是JarFile.JarFileInputStream对象。接着将该对象传入PackageParser.readFullyIgnoringContents()方法中,来看下该方法做了什么:

public static long readFullyIgnoringContents(InputStream in) throws IOException {
        byte[] buffer = sBuffer.getAndSet(null);
        if (buffer == null) {
            buffer = new byte[4096];
        }

        int n = 0;
        int count = 0;
        while ((n = in.read(buffer, 0, buffer.length)) != -1) {
            count += n;
        }

        sBuffer.set(buffer);
        return count;
    }

看起来只是对传入的字节输入流对象进行读取,直到读完,然后返回读到的字节数。但由于传入的是InpuStream对象的子类对象JarFile.JarFileInputStream,而且它重写了read()方法,来看一下这个子类的read()方法做了什么事:

@override
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
            if (done) {
                return -1;
            }
            if (count > 0) {
                int r = super.read(buffer, byteOffset, byteCount);
                if (r != -1) {
                    int size = r;
                    if (count < size) {
                        size = (int) count;
                    }
                    entry.write(buffer, byteOffset, size);
                    count -= size;
                } else {
                    count = 0;
                }
                if (count == 0) {
                    done = true;
                    entry.verify();
                }
                return r;
            } else {
                done = true;
                entry.verify();
                return -1;
            }
        }

如上,它会调用父类的read()方法进行读取,然后将读取到的数据传入entry.write()方法,最后在调用entry.verify()进行验证。这个entry就是前面创建的JarVerifier.VerifierEntry对象。来看一下JarVerifier.VerifierEntry.write()方法做了什么:

@Override
public void write(byte[] buf, int off, int nbytes) {
    digest.update(buf, off, nbytes);
}

就是对数据进行hash。再来看一下JarVerifier.VerifierEntry.verify()方法做了什么:

void verify() {
    byte[] d = digest.digest();
    if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
        throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
    }
    verifiedEntries.put(name, certChains);
}

该方法就是将文件的Hash与MANIFEST中对应文件的Hash值进行比对,一致的话则将文件名和证书链添加到verifiedEntries中;不一致的话就调用JarVerifier.invalidDigest()方法抛出SecurityException异常,如下:

private static SecurityException invalidDigest(String signatureFile, String name, 
          String jarName) {
    throw new SecurityException(signatureFile + " has invalid digest for " + name +
                " in " + jarName);
}

到这里,第三步,即校验APK所有文件是否有被篡改,也已完成。再次回到PackageParser.collectCertificates()方法中继续看:

        for (ZipEntry entry : toVerify) {
                final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
                if (ArrayUtils.isEmpty(entryCerts)) {
                    throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                            "Package " + apkPath + " has no certificates at entry "
                            + entry.getName());
                }
                final Signature[] entrySignatures = convertToSignatures(entryCerts);

                if (pkg.mCertificates == null) {
                    pkg.mCertificates = entryCerts;
                    pkg.mSignatures = entrySignatures;
                    pkg.mSigningKeys = new ArraySet<PublicKey>();
                    for (int i=0; i < entryCerts.length; i++) {
                        pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
                    }
                } else {
                    if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
                        throw new PackageParserException(
                                INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
                                        + " has mismatched certificates at entry "
                                        + entry.getName());
                    }
                }
            }

loadCertificates()之后的代码,主要就是判断该APK是否原来安装过,如果没安装过,则保存该APK的签名信息;如果安装过,则比对前后两次安装的签名信息,如果签名信息一致,则继续安装;如果前后签名不一致,则抛出异常INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES异常。所以平时我们在开发或者测试过程中安装应用时,如果抛出该异常,则说明已经有该包名的应用安装在设备上,且签名与你现在要安装的不同。必须先卸载已安装在设备上的才能继续安装。(INCONSISTENT就是不一致的意思)

0x04 小结

至此,Android的应用验签过程总算分析完了,可以看到,验签的过程刚好和签名的过程是相反的,下面总结一下:

  1. 所有的验签动作都是在JarVerifier这个类里面完成的;
  2. 在JarVerifier.verifyCertificate()方法中完成了以下两步:
    1. 使用CERT.RSA校验CERT.SF,看CERT.SF是否被篡改;
    2. 使用CERT.SF校验MANIFEST.MF,看MANIFEST.MF是否被篡改;
  3. 在JarVerifier.VerifierEntry.verify()方法中完成最后一步:
    1. 使用MANIFEST.MF来校验所有文件,看有没有文件被篡改,或者有没有文件被删
      除,又或者有没有添加新的文件。

0x05 参考文献

http://blog.csdn.net/roland_sun/article/details/42029019
https://www.cnblogs.com/JeffreySun/archive/2010/06/24/1627247.html
http://netsecurity.51cto.com/art/201108/287971.htm

相关文章

  • Android应用的验签过程分析

    0x01 回顾 分析验签过程之前,有必要先回顾一下Android应用的签名过程: 对APK包中的每个文件做一次运算...

  • Android应用处理MotionEvent的过程

    基于Android 7.0源码分析 应用收到Motion事件传递至Activity的过程 应用对于Motion事件...

  • Android签名认证分析

    **一.Android签名验证流程基础知识及源码解析:**** Android应用程序签名过程分析:http://...

  • 支付宝支付相关

    支付宝PC端支付,回调验签失败。 除了各个参数输入正确,需要特别注意的是验签的公钥是用支付宝公钥,而不是应用公钥。...

  • swift-RSA(五)-签名/验签

    RSA支持加解密,也支持签名/验签。使用rsa如何签名/验签呢? 1.rsa 签名 2.rsa 验签 3.调用

  • Android系统启动流程(源码分析)

    源码查看工具:Source Insight参考:Android系统启动流程Android应用程序启动过程源代码分析...

  • java安全入门篇之接口验签(原创)

    文章大纲 一、加密与验签介绍二、接口验签实操三、项目源码下载 一、加密与验签介绍   大多数公共网络是不安全的,一...

  • Android 8.0 Activity启动过程分析

    图解Activity启动过程 上图大致分析出了Android-8.0应用根Activity的启动过程,其他Acti...

  • 以太坊签名与验签

    前言 以太坊的签名与验签可以用来metamask钱包登录,多签钱包等应用。但没有全面的文档进行相关的解释,这里笔者...

  • 2019-11-11

    签名验签 现在国内网银基本上用的都是公钥解密,私钥签名加密。不对称加密可百度一下。银行U盾签名验签的过程是:1.申...

网友评论

      本文标题:Android应用的验签过程分析

      本文链接:https://www.haomeiwen.com/subject/ismfkxtx.html