您现在的位置是:首页 > 文章详情

也谈Android签名机制

日期:2018-08-02点击:369

1. 前言

关于Android的签名机制,在一个月前就看过了,当时还写了下流程,感觉没有太大的技术含量就没有记录。最近在看APK安装过程,突然又想起安装过程包含了APK的验证,关于APK的验证无非就是签名的逆过程。但是发现自己对签名过程好像模糊了很多,遂决定记录下签名过程。

2. 关于签名

Android的签名现在分为两个版本:v1和v2,因为v1版本签名过程的缺陷,造成了APK可能被攻击
v1签名:签名和摘要文件为APK解压后的META-INF文件夹下的*.MF、*.SF、*.RSA文件,其签名过程需要对文件进行解压并且计算每个文件的摘要。
v2签名:签名信息存储在ZIP文件格式中。7.0以上支持,7.0以下不支持,只能采用v1签名。

img_3b58623e45132444000f458058107d4b.png
v2签名块位置

3. v1签名源码

我用到的源码都是在在线源码网站上下载的,这里用到了SignApk.java文件。
我们都知道如果使用命令行签名的话,都是执行的main方法:

public static void main(String[] args) { // 对输入参数的解析和验证 ...... boolean signWholeFile = false; String providerClass = null; int alignment = 4; int minSdkVersion = 0; boolean signUsingApkSignatureSchemeV2 = true; int argstart = 0; // 对输入参数的解析和验证 ...... loadProviderIfNecessary(providerClass); String inputFilename = args[args.length - 2]; String outputFilename = args[args.length - 1]; JarFile inputJar = null; FileOutputStream outputFile = null; int hashes = 0; try { // 公钥文件 File firstPublicKeyFile = new File(args[argstart + 0]); X509Certificate[] publicKey = new X509Certificate[numKeys]; ...... long timestamp = 1230768000000L; timestamp -= TimeZone.getDefault().getOffset(timestamp); // 私钥文件 PrivateKey[] privateKey = new PrivateKey[numKeys]; for (int i = 0; i < numKeys; ++i) { int argNum = argstart + i * 2 + 1; privateKey[i] = readPrivateKey(new File(args[argNum])); } // 输入的文件 inputJar = new JarFile(new File(inputFilename), false); // Don't verify. // 输出文件 outputFile = new FileOutputStream(outputFilename); // 直接对整个文件签名,这里不看 if (signWholeFile) { SignApk.signWholeFile(inputJar, firstPublicKeyFile, publicKey[0], privateKey[0], timestamp, minSdkVersion, outputFile); } else { ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream(); JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf); outputJar.setLevel(9); // 1. 生成.MF信息内容 Manifest manifest = addDigestsToManifest(inputJar, hashes); copyFiles(manifest, inputJar, outputJar, timestamp, alignment); // 2. 对文件签名,生成.SF文件内容并且签名 signFile( manifest, publicKey, privateKey, timestamp, minSdkVersion, signUsingApkSignatureSchemeV2, outputJar); outputJar.close(); ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray()); v1SignedApkBuf.reset(); ByteBuffer[] outputChunks; // 使用v2签名 if (signUsingApkSignatureSchemeV2) { // Additionally sign the APK using the APK Signature Scheme v2. ByteBuffer apkContents = v1SignedApk; List<ApkSignerV2.SignerConfig> signerConfigs = createV2SignerConfigs( privateKey, publicKey, new String[]{APK_SIG_SCHEME_V2_DIGEST_ALGORITHM}); outputChunks = ApkSignerV2.sign(apkContents, signerConfigs); } else { // Output the JAR-signed APK as is. outputChunks = new ByteBuffer[]{v1SignedApk}; } // This assumes outputChunks are array-backed. To avoid this assumption, the // code could be rewritten to use FileChannel. for (ByteBuffer outputChunk : outputChunks) { outputFile.write( outputChunk.array(), outputChunk.arrayOffset() + outputChunk.position(), outputChunk.remaining()); outputChunk.position(outputChunk.limit()); } outputFile.close(); outputFile = null; return; } } ...... } 

v1签名过程很简单,一共分为了三个部分:

  1. 对非目录文件以及过滤文件进行摘要,存储在MANIFEST.MF文件中。
  2. 对MANIFEST.MF文件的进行摘要以及对MANIFEST.MF文件的每个条目内容进行摘要,存储在CERT.SF文件中。
  3. 使用指定的私钥对CERT.SF文件计算签名,然后将签名以及包含公钥信息的数字证书写入 CERT.RSA。
    这三个文件也就是我们APK解压后META-INF目录下的文件:


    img_c582ec7784c3419041df841aa2d9cdf5.png
    签名和摘要文件

3.1 .MF文件内容生成

上面已经说了,MANIFEST.MF的内容是通过addDigestsToManifest方法生成的,代码如下:

/** * 添加对所有不是目录文件的摘要(SHA1或SHA256) */ private static Manifest addDigestsToManifest(JarFile jar, int hashes) throws IOException, GeneralSecurityException { // 最上面那部分内容 Manifest input = jar.getManifest(); Manifest output = new Manifest(); Attributes main = output.getMainAttributes(); if (input != null) { main.putAll(input.getMainAttributes()); } else { main.putValue("Manifest-Version", "1.0"); main.putValue("Created-By", "1.0 (Android SignApk)"); } // 根据输入来选择摘要算法 MessageDigest md_sha1 = null; MessageDigest md_sha256 = null; if ((hashes & USE_SHA1) != 0) { md_sha1 = MessageDigest.getInstance("SHA1"); } if ((hashes & USE_SHA256) != 0) { md_sha256 = MessageDigest.getInstance("SHA256"); } byte[] buffer = new byte[4096]; int num; TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>(); // 把apk文件所有条目添加到treemap中 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(); // 如果不是目录并且不是特定的文件 attern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" + // Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 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); } Attributes attr = null; if (input != null) attr = input.getAttributes(name); attr = attr != null ? new Attributes(attr) : new Attributes(); for (Iterator<Object> i = attr.keySet().iterator(); i.hasNext(); ) { Object key = i.next(); if (!(key instanceof Attributes.Name)) { continue; } String attributeNameLowerCase = ((Attributes.Name) key).toString().toLowerCase(Locale.US); if (attributeNameLowerCase.endsWith("-digest")) { i.remove(); } } // 计算摘要 并且使用base64进行encode // Add SHA-1 digest if requested 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; } 

其过程分为三步:

  1. 添加最上面内容信息


    img_e15aec1542ec2907ea70f8815d0f1508.png
    上部分内容
  2. 将APK内容遍历,寻找不为目录并且没有被过滤的文件,对其进行摘要计算。
  3. 将摘要信息写入。
    验证一下:
    img_cb7416652869924af7de8b75814febce.png
    AndroidManifest.xml文件对应的摘要

    img_3519f81efc60fdc562005e7e463e7baa.png
    文件摘要

    这么看这两个值还不相同呢,但是我们仔细看下代码new String(Base64.encode(md_sha1.digest()), "ASCII")这里将摘要内容进行Base64编码后又将其转成String了,我们可以看下:
byte[] bytes = {(byte) 0xD0, (byte) 0xF9, (byte) 0xE4, 0x2B, (byte) 0xB2, (byte) 0xC7, (byte) 0xB0, 0x72, 0x45, (byte) 0x8C, 0x27, (byte) 0xC3, 0x7F, 0x3D, 0x01, 0x78, 0x5C, (byte) 0x82, (byte) 0xA8, (byte) 0xB5}; String ascii = new String(Base64.getEncoder().encode(bytes), "ASCII"); System.out.println(ascii); 

输出内容:


img_57ed3d1c943c57bbeaf92279f631dd59.png
输出

简化的代码如下:

 private static Manifest addDigestsToManifest(JarFile jarFile) throws IOException, NoSuchAlgorithmException { Manifest input = jarFile.getManifest(); Manifest output = new Manifest(); Attributes main = output.getMainAttributes(); if (input != null) { main.putAll(input.getMainAttributes()); } else { main.putValue("Manifest-Version", "1.0"); main.putValue("Created-By", "1.0 (Android SignApk)"); } MessageDigest sha1 = MessageDigest.getInstance("SHA1"); TreeMap<String, JarEntry> byName = new TreeMap<>(); Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry jarEntry = entries.nextElement(); byName.put(jarEntry.getName(), jarEntry); } byte[] data = new byte[4096]; int num = 0; for (JarEntry jarEntry : byName.values()) { if (!jarEntry.isDirectory()) { InputStream inputStream = jarFile.getInputStream(jarEntry); while ((num = inputStream.read(data)) > 0) { sha1.update(data, 0, num); } Attributes attributes = null; if (input != null) { attributes = input.getAttributes(jarEntry.getName()); } if (attributes == null) { attributes = new Attributes(); } attributes.putValue("SHA1-Digest", new String(Base64.getEncoder().encode(sha1.digest()), "ASCII")); output.getEntries().put(jarEntry.getName(), attributes); } } output.write(new FileOutputStream("C:\\Users\\nick\\Desktop\\MANIFEST.MF")); return output; } 

3.2 .SF文件内容生成

.SF文件内容是需要依赖.MF文件内容:

/** * Write a .SF file with a digest of the specified manifest. * 写入.sf文件 */ private static void writeSignatureFile(Manifest manifest, OutputStream out, int hash, boolean additionallySignedUsingAnApkSignatureScheme) 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)"); // v2签名 添加 if (additionallySignedUsingAnApkSignatureScheme) { main.putValue( ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME, ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE); } MessageDigest md = MessageDigest.getInstance( hash == USE_SHA256 ? "SHA256" : "SHA1"); PrintStream print = new PrintStream( new DigestOutputStream(new ByteArrayOutputStream(), md), true, "UTF-8"); manifest.write(print); print.flush(); main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest", new String(Base64.encode(md.digest()), "ASCII")); // 这段代码将上面的.MF的内容以 // Name: res/layout/fb_community_manage_activity.xml\r\nSHA1-Digest: 2JZzBj3bimvi5pwxQZH4LlJJTcg=\r\n\r\n // 获取其摘要,并且按照相同的格式存储 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", 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'); } } 

主要两个步骤:

  1. 计算.MF整个文件内容摘要,存放在上面的位置。
  2. 计算.MF每一项内容,将其拼接成Name: res/layout/fb_community_manage_activity.xml\r\nSHA1-Digest: 2JZzBj3bimvi5pwxQZH4LlJJTcg=\r\n\r\n格式并计算这段内容的摘要并以相同格式保存
    验证:
String s = "Name: AndroidManifest.xml\r\nSHA1-Digest: 0PnkK7LHsHJFjCfDfz0BeFyCqLU=\r\n\r\n"; MessageDigest messageDigest = MessageDigest.getInstance("SHA1"); messageDigest.update(s.getBytes()); byte[] digest = messageDigest.digest(); System.out.println(new String(Base64.getEncoder().encode(digest), "ASCII")); 

输出:


img_1ef90501e1017850ba2b80946ee1ecae.png
输出

img_1fd0a24346a337bd730ba301a169da8a.png
.SF文件内容

简化版:

 private static void signFile(Manifest outPut) throws NoSuchAlgorithmException, IOException { Manifest sf = new Manifest(); Attributes main = sf.getMainAttributes(); main.putValue("Signature-Version", "1.0"); main.putValue("Created-By", "1.0 (Android SignApk)"); MessageDigest messageDigest = MessageDigest.getInstance("SHA1"); PrintStream print = new PrintStream( new DigestOutputStream(new ByteArrayOutputStream(), messageDigest), true, "UTF-8"); outPut.write(print); print.flush(); main.putValue("SHA1-Digest-Manifest", new String(Base64.getEncoder().encode(messageDigest.digest()), "ASCII")); Map<String, Attributes> entries = outPut.getEntries(); for (Map.Entry<String, Attributes> stringAttributesEntry : entries.entrySet()) { print.print("Name: " + stringAttributesEntry.getKey() + "\r\n"); for (Map.Entry<Object, Object> att : stringAttributesEntry.getValue().entrySet()) { print.print(att.getKey() + ": " + att.getValue() + "\r\n"); } print.print("\r\n"); print.flush(); Attributes sfAttr = new Attributes(); sfAttr.putValue("SHA1-Digest", new String(Base64.getEncoder().encode(messageDigest.digest()), "ASCII")); sf.getEntries().put(stringAttributesEntry.getKey(), sfAttr); } sf.write(new FileOutputStream("C:\\Users\\nick\\Desktop\\CERT.SF")); } 

这里有个问题,.SF文件在老的APK(可能是使用v1签名?)中确实是由上面代码生成,但是我看最新的APK文件中.SF文件内容和.MF文件内容一致,猜想可能是v1和v2签名的原因,具体不详。

3.3 签名

img_72533ca2e1f42b5fd7b9c4a16a827534.png
签名代码

上面我们得到了.SF文件的内容,通过私钥和公钥就可以对其获得签名信息,根据签名信息即可生成.RSA文件(没有验证过程)。

4.1 v2签名

先复制一下v1签名的漏洞:
1、安卓在4.4中引入了新的执行虚拟机ART,这个虚拟机经过重新的设计,实现了大量的优化,提高了应用的运行效率。与“Janus”有关的一个技术点是,ART允许运行一个raw dex,也就是一个纯粹的dex文件,不需要在外面包装一层zip。而ART的前任DALVIK虚拟机就要求dex必须包装在一个zip内部且名字是classes.dex才能运行。当然ART也支持运行包装在ZIP内部的dex文件,要区别文件是ZIP还是dex,就通过文件头的magic字段进行判断:ZIP文件的开头是‘PK’, 而dex文件的开头是’dex’.

2、ZIP文件的读取方式是通过在文件末尾定位central directory, 然后通过里面的索引定位到各个zip entry,每个entry解压之后都对应一个文件。

通过漏洞就可以知道系统在解压ZIP文件时根据其末尾来解压,但是执行的过程又根据其头部来执行,这样就可以通过注入新的dex在头部来实现攻击的目的。

v2签名官方文档,以下内容来自官方文档:

APK 签名方案 v2 是一种全文件签名方案,该方案能够发现对 APK 的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。
使用 APK 签名方案 v2 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APK 签名分块”内,v2 签名和签名者身份信息会存储在APK 签名方案 v2 分块中。


img_3b58623e45132444000f458058107d4b.png
image.png

APK 签名分块

为了保持与当前 APK 格式向后兼容,v2 及更高版本的 APK 签名会存储在“APK 签名分块”内,该分块是为了支持 APK 签名方案 v2 而引入的一个新容器。在 APK 文件中,“APK 签名分块”位于“ZIP 中央目录”(位于文件末尾)之前并紧邻该部分。
该分块包含多个“ID-值”对,所采用的封装方式有助于更轻松地在 APK 中找到该分块。APK 的 v2 签名会存储为一个“ID-值”对,其中 ID 为 0x7109871a。

APK 签名方案 v2 分块

APK 由一个或多个签名者/身份签名,每个签名者/身份均由一个签名密钥来表示。该信息会以“APK 签名方案 v2 分块”的形式存储。对于每个签名者,都会存储以下信息:
(签名算法、摘要、签名)元组。摘要会存储起来,以便将签名验证和 APK 内容完整性检查拆开进行。
表示签名者身份的 X.509 证书链。
采用键值对形式的其他属性。
对于每位签名者,都会使用收到的列表中支持的签名来验证 APK。签名算法未知的签名会被忽略。如果遇到多个支持的签名,则由每个实现来选择使用哪个签名。这样一来,以后便能够以向后兼容的方式引入安全系数更高的签名方法。建议的方法是验证安全系数最高的签名。

v2与v1签名最大的区别就是v2修改了APK文件的内容,将其签名块放到了APK文件中(v2签名验证需要验证APK文件的这部分)。
签名块生成,这里代码用的时ApkSignerV2.java

public static ByteBuffer[] sign( ByteBuffer inputApk, List<SignerConfig> signerConfigs) throws ApkParseException, InvalidKeyException, SignatureException { ByteBuffer originalInputApk = inputApk; inputApk = originalInputApk.slice(); inputApk.order(ByteOrder.LITTLE_ENDIAN); // 获取EoCD位置以及对ZIP文件的校验 int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk); ...... inputApk.clear(); ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset); ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset); byte[] eocdBytes = new byte[inputApk.remaining()]; inputApk.get(eocdBytes); ByteBuffer eocd = ByteBuffer.wrap(eocdBytes); eocd.order(inputApk.order()); Set<Integer> contentDigestAlgorithms = new HashSet<>(); for (SignerConfig signerConfig : signerConfigs) { for (int signatureAlgorithm : signerConfig.signatureAlgorithms) { contentDigestAlgorithms.add( getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm)); } } // Compute digests of APK contents. Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest try { // 计算内容摘要 contentDigests = computeContentDigests( contentDigestAlgorithms, new ByteBuffer[]{beforeCentralDir, centralDir, eocd}); } catch (DigestException e) { throw new SignatureException("Failed to compute digests of APK", e); } // 生成签名块 ByteBuffer apkSigningBlock = ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests)); centralDirOffset += apkSigningBlock.remaining(); eocd.clear(); ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset); originalInputApk.position(originalInputApk.limit()); beforeCentralDir.clear(); centralDir.clear(); eocd.clear(); // Insert APK Signing Block immediately before the ZIP Central Directory. // 将内容重组 // 1. ZIP 条目的内容(从偏移量 0 处开始一直到“APK 签名分块”的起始位置) // 2. APK 签名分块 // 3. ZIP 中央目录 // 4. ZIP 中央目录结尾 return new ByteBuffer[]{ beforeCentralDir, apkSigningBlock, centralDir, eocd, }; } 

代码也不多,分为三步:

  1. 计算内容摘要。
  2. 对内容摘要进行签名,并生成签名块。
  3. 将签名块添加到原APK文件内容中。

4.1 计算内容摘要

img_35cee8151fc43902c7e9d82238630d1b.png
签名后的各个 APK 部分

第 1、3 和 4 部分的完整性通过其内容的一个或多个摘要来保护,这些摘要存储在 signed data 分块中,而这些分块则通过一个或多个签名来保护。

第 1、3 和 4 部分的摘要采用以下计算方式,类似于两级 Merkle 树。 每个部分都会被拆分成多个大小为 1 MB(220 个字节)的连续块。每个部分的最后一个块可能会短一些。每个块的摘要均通过字节 0xa5 的连接、块的长度(采用小端字节序的 uint32 值,以字节数计)和块的内容进行计算。顶级摘要通过字节 0x5a 的连接、块数(采用小端字节序的 uint32 值)以及块的摘要的连接(按照块在 APK 中显示的顺序)进行计算。摘要以分块方式计算,以便通过并行处理来加快计算速度。

img_55658faa6b37353baa2ab03c5938e774.png
签名块生成原理

生成代码如下:

/** * 计算内容摘要 * * @param digestAlgorithms 摘要算法 * @param contents 内容,这里三块 * @return 摘要Map * @throws DigestException */ private static Map<Integer, byte[]> computeContentDigests( Set<Integer> digestAlgorithms, ByteBuffer[] contents) throws DigestException { // 计算分成1M大小的数量 int chunkCount = 0; for (ByteBuffer input : contents) { chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); } // 设置摘要算法和摘要内容的Map final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size()); for (int digestAlgorithm : digestAlgorithms) { int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); byte[] concatenationOfChunkCountAndChunkDigests = new byte[5 + chunkCount * digestOutputSizeBytes]; concatenationOfChunkCountAndChunkDigests[0] = 0x5a; setUnsignedInt32LittleEngian( chunkCount, concatenationOfChunkCountAndChunkDigests, 1); digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests); } int chunkIndex = 0; byte[] chunkContentPrefix = new byte[5]; chunkContentPrefix[0] = (byte) 0xa5; // Optimization opportunity: digests of chunks can be computed in parallel. // 遍历内容 for (ByteBuffer input : contents) { while (input.hasRemaining()) { // 检查剩下的大小,取其和1M中小的哪个 int chunkSize = Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); final ByteBuffer chunk = getByteBuffer(input, chunkSize); // 遍历摘要算法 for (int digestAlgorithm : digestAlgorithms) { String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); MessageDigest md; try { md = MessageDigest.getInstance(jcaAlgorithmName); } catch (NoSuchAlgorithmException e) { throw new DigestException( jcaAlgorithmName + " MessageDigest not supported", e); } chunk.clear(); // 设置chunkContentPrefix为0xa5 + chunk.remaining() setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1); md.update(chunkContentPrefix); md.update(chunk); // 获得刚才保存的摘要算法对应的内容,0xa5+length,剩下全为0 byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks.get(digestAlgorithm); // 期望的长度 int expectedDigestSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); // 通过摘要算法后已经修改内容的长度 // 这里是将concatenationOfChunkCountAndChunkDigests内容更新为最新的摘要 int actualDigestSizeBytes = md.digest( concatenationOfChunkCountAndChunkDigests, 5 + chunkIndex * expectedDigestSizeBytes, expectedDigestSizeBytes); if (actualDigestSizeBytes != expectedDigestSizeBytes) { throw new DigestException( "Unexpected output size of " + md.getAlgorithm() + " digest: " + actualDigestSizeBytes); } } chunkIndex++; } } // 对concatenationOfChunkCountAndChunkDigests也就是我们每一块 0xa5 + chunkCount + (0xa5+length+内容的摘要)* chunkCount Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size()); for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) { int digestAlgorithm = entry.getKey(); byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue(); String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); MessageDigest md; try { md = MessageDigest.getInstance(jcaAlgorithmName); } catch (NoSuchAlgorithmException e) { throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e); } result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests)); } return result; } 

原理也简单,就是将其他部分内容分成1M大小的块,每个块的摘要均通过字节 0xa5 的连接、块的长度(采用小端字节序的 uint32 值,以字节数计)和块的内容进行摘要计算,将计算的结果放到以0xa5+length开头的数组中,最后将其进行摘要计算。

4.2 签名并且生成签名块

签名时对我们刚刚得到的摘要信息进行签名,签名的过程无非就是通过公钥和私钥进行签名计算,生成对应的签名信息(签名过程省略)。
签名块的生成:

/** * 生成签名块 * * @param apkSignatureSchemeV2Block apk签名 * @return */ private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) { // FORMAT: // uint64: size (excluding this field) // repeated ID-value pairs: // uint64: size (excluding this field) // uint32: ID // (size - 4) bytes: value // uint64: size (same as the one above) // uint128: magic int resultSize = 8 // size + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair + 8 // size + 16 // magic ; ByteBuffer result = ByteBuffer.allocate(resultSize); result.order(ByteOrder.LITTLE_ENDIAN); long blockSizeFieldValue = resultSize - 8; // size of block,以字节数(不含此字段)计 (uint64) result.putLong(blockSizeFieldValue); long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length; // size result.putLong(pairSizeFieldValue); // id 0x7109871a result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID); // (size - 4) bytes签名块 result.put(apkSignatureSchemeV2Block); // size of block,以字节数计 - 与第一个字段相同 (uint64) result.putLong(blockSizeFieldValue); // magic“APK 签名分块 42”(16 个字节) result.put(APK_SIGNING_BLOCK_MAGIC); return result.array(); } 

签名块按照格式:

“APK 签名分块”的格式如下(所有数字字段均采用小端字节序): size of block,以字节数(不含此字段)计 (uint64) 带 uint64 长度前缀的“ID-值”对序列: ID (uint32) value(可变长度:“ID-值”对的长度 - 4 个字节) size of block,以字节数计 - 与第一个字段相同 (uint64) magic“APK 签名分块 42”(16 个字节 

生成。

4.3 生成签名后APK

生成签名后的APK很简单,我们已经获得了每块的内容:

1. Contents of ZIP entries 2. Central Directory 3. End of Central Directory APK Signing Block 

我们只需要将内容合并即可,合并顺序为:

1. Contents of ZIP entries 2. APK Signing Block 3. Central Directory 4. End of Central Directory 

代码为:

// Insert APK Signing Block immediately before the ZIP Central Directory. // 将内容重组 // 1. ZIP 条目的内容(从偏移量 0 处开始一直到“APK 签名分块”的起始位置) // 2. APK 签名分块 // 3. ZIP 中央目录 // 4. ZIP 中央目录结尾 return new ByteBuffer[]{ beforeCentralDir, apkSigningBlock, centralDir, eocd, }; 

5 后记

啰里啰唆说了一大堆,终于将签名过程写完了。在Android APK安装时肯定会有对APK的签名信息验证的过程,这部分如果有时间去看Android APK安装流程时再仔细分析了。

原文链接:https://yq.aliyun.com/articles/665821
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章