Android学习心得(21) --- apk签名源码解析

来源:互联网 发布:telnet打开端口命令 编辑:程序博客网 时间:2024/05/02 08:14

新博客地址

blog.marssecure.com

重打包检测

对于apk重打包检测中,验证签名是一种很简单、很快捷的方法,为了更好理解签名,这一章主要讲解apk中签名文件内容

签名:

每一个apk发布之前都需要进行签名,不然不能进行安装,我们使用winrar来查看一个apk包
1
我们可以看到其目录结构,关注一下META-INF这个文件,里面包含三个文件
2
下面我们来解压缩,打开看看里面内容

源码位置 :

/build/tools/signapk/SignApk.java

MANIFEST.MF:

我们可以看到,该文件包含了多个Name和SHA1-Digest,其中name是文件名称,而SHA1-Digest则是其Base64格式的SHA1的哈希值
3
来看看源码中的实现

// Main函数中调用Manifest manifest = addDigestsToManifest(inputJar, hashes);  // 计算文件的hash值并放入Manifest中  // addDigestsToManifest函数    private static Manifest addDigestsToManifest(JarFile jar, int hashes)       ...       ...       ...        TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();        for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {            JarEntry entry = e.nextElement();            byName.put(entry.getName(), entry);        }        // 依次进行哈希值计算        for (JarEntry entry: byName.values()) {            String name = entry.getName();            if (!entry.isDirectory() &&                (stripPattern == null || !stripPattern.matcher(name).matches())) {                InputStream data = jar.getInputStream(entry);                // 哈希操作                while ((num = data.read(buffer)) > 0) {                    if (md_sha1 != null) md_sha1.update(buffer, 0, num);                    if (md_sha256 != null) md_sha256.update(buffer, 0, num);                }                // 添加name hash-digest属性                Attributes attr = null;                if (input != null) attr = input.getAttributes(name);                attr = attr != null ? new Attributes(attr) : new Attributes();                // 计算过程                if (md_sha1 != null) {                    attr.putValue("SHA1-Digest",                                  new String(Base64.encode(md_sha1.digest()), "ASCII"));                }                if (md_sha256 != null) {                    attr.putValue("SHA-256-Digest",                                  new String(Base64.encode(md_sha256.digest()), "ASCII"));                }                output.getEntries().put(name, attr);            }        }        return output;    }// copyFile存放manifestcopyFiles(manifest, inputJar, outputJar, timestamp); // 根据Manifest中name依次将input中文件放入output中

主要的计算过程是在addDigestsToManifest函数中,先生成TreeMap

CERT.SF:

打开看到该文件基本上也和MANIFEST.MF一样
5
来看看源码中的实现

// Main方法中调用signFile函数signFile(manifest, inputJar, publicKey, privateKey, outputJar);// 查看signFile函数private static void signFile(Manifest manifest, JarFile inputJar,                                 X509Certificate[] publicKey, PrivateKey[] privateKey,                                 JarOutputStream outputJar)        throws Exception {        // Assume the certificate is valid for at least an hour.        long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;  // 时间戳        // MANIFEST.MF        JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);        je.setTime(timestamp);        outputJar.putNextEntry(je);        manifest.write(outputJar);        int numKeys = publicKey.length;        for (int k = 0; k < numKeys; ++k) {            // CERT.SF / CERT#.SF            // 生成CERT.SF文件            je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :                              (String.format(CERT_SF_MULTI_NAME, k)));            je.setTime(timestamp);            outputJar.putNextEntry(je);            ByteArrayOutputStream baos = new ByteArrayOutputStream();            writeSignatureFile(manifest, baos, getAlgorithm(publicKey[k]));            byte[] signedData = baos.toByteArray();            outputJar.write(signedData);        }    }

主要的过程是在signFile中调用writeSignatureFile这个函数

 /** Write a .SF file with a digest of the specified manifest. */    private static void writeSignatureFile(Manifest manifest, OutputStream out,                                           int hash)        throws IOException, GeneralSecurityException {        Manifest sf = new Manifest();        Attributes main = sf.getMainAttributes();        main.putValue("Signature-Version", "1.0");        main.putValue("Created-By", "1.0 (Android SignApk)");        MessageDigest md = MessageDigest.getInstance(            hash == USE_SHA256 ? "SHA256" : "SHA1");        PrintStream print = new PrintStream(            new DigestOutputStream(new ByteArrayOutputStream(), md),            true, "UTF-8");        // Digest of the entire manifest        manifest.write(print);        print.flush();        main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",                      new String(Base64.encode(md.digest()), "ASCII"));        Map<String, Attributes> entries = manifest.getEntries();        for (Map.Entry<String, Attributes> entry : entries.entrySet()) {            // Digest of the manifest stanza for this entry.            print.print("Name: " + entry.getKey() + "\r\n");            for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {                print.print(att.getKey() + ": " + att.getValue() + "\r\n");            }            print.print("\r\n");            print.flush();            Attributes sfAttr = new Attributes();            sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",                            new String(Base64.encode(md.digest()), "ASCII"));            sf.getEntries().put(entry.getKey(), sfAttr);        }        CountOutputStream cout = new CountOutputStream(out);        sf.write(cout);        if ((cout.size() % 1024) == 0) {            cout.write('\r');            cout.write('\n');        }    }

将manifest中的所有value再做一次哈希计算,目的是验证MANIFEST.MF中值是否已经变化。
for (Map.Entry

CERT.RSA:

该文件是X.509格式证书,可以使用openssl进行查看
7
来看看源代码

// main函数中调用signFile函数signFile(manifest, inputJar, publicKey, privateKey, outputJar);// 调用writeSignatureBlock函数writeSignatureBlock(new CMSProcessableByteArray(signedData),                                publicKey[k], privateKey[k], outputJar);// 调用writeSignatureBlock函数private static void writeSignatureBlock(    CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,    OutputStream out)    throws IOException,           CertificateEncodingException,           OperatorCreationException,           CMSException {    // 公钥    ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);    certList.add(publicKey);    JcaCertStore certs = new JcaCertStore(certList);    // 私钥信息    CMSSignedDataGenerator gen = new CMSSignedDataGenerator();    ContentSigner signer = new JcaContentSignerBuilder(        getAlgorithm(publicKey) == USE_SHA256 ? "SHA256withRSA" : "SHA1withRSA")        .setProvider(sBouncyCastleProvider)        .build(privateKey);+9    gen.addSignerInfoGenerator(        new JcaSignerInfoGeneratorBuilder(            new JcaDigestCalculatorProviderBuilder()            .setProvider(sBouncyCastleProvider)            .build())        .setDirectSignature(true)        .build(signer, publicKey));    // 添加公钥信息    gen.addCertificates(certs);    CMSSignedData sigData = gen.generate(data, false);    ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());    DEROutputStream dos = new DEROutputStream(out);    dos.writeObject(asn1.readObject());}                            

使用私钥计算出签名,将签名和公钥一同放入其中。

验证:

1、如果apk文件改变了,apk在安装时候会验证apk中文件和MANIFEST.MF文件中对应的哈希值是否一致
2、还会验证MANIFEST.MF中摘要值和CERT.SF文件中是否一致
3、使用公钥解密CERT.MF中签名再一次进行验证
通过这三种验证,apk就能够实现签名验证

未来:

下一步会去研究一下Android端PackageManager安装apk时候签名验证的源代码,更好理解签名的应用
以上是apk签名学习,下面我们还会学习一个重打包检测系统中所需要的所有的知识并搭建出一个可用的重打包检测系统。希望对大家有所帮助,也希望你可以一直支持我们MarsSecure。

0 0
原创粉丝点击