Androiud多渠道打包分析

背景:目前项目在打渠道包的时候,采用的是AndroidManifest.xml配置渠道号,上线前一个个构建出来,全部构建完成耗时长达一个小时,这对于追求高效的工程师来讲是无法忍受的。当前有很多公司开源了多渠道打包的方案,如美团的walle,腾讯的VasDolly,还有技术达人个人研究的packer-ng-plugin,今天就来探索下多渠道打包的奥秘。

在了解多渠道打包之前,需要先了解下android的签名方式,这样才能知己知彼百战不殆!

APK 签名方案

Android 支持两种应用签名方案,一种是基于 JAR 签名的方案(v1 方案),另一种是 Android Nougat (Android 7.0) 中引入的 APK 签名方案 v2(v2 方案)。

为了最大限度地提高兼容性,应同时采用 v1 和 v2 这两种方案对应用进行签名。与只通过 v1 方案签名的应用相比,通过 v2 方案签名的应用能够更快速地安装到 Android Nougat 及更高版本的设备上。更低版本的 Android 平台会忽略 v2 签名,这就需要应用包含 v1 签名。

JAR 签名(v1 方案)

从一开始,APK 签名就是 Android 的一个有机部分。该方案基于签名的JAR。

v1 签名不保护 APK 的某些部分,例如 ZIP 元数据。APK 验证程序需要处理大量不可信(尚未经过验证)的数据结构,然后会舍弃不受签名保护的数据。这会导致相当大的受攻击面。此外,APK 验证程序必须解压所有已压缩的条目,而这需要花费更多时间和内存。为了解决这些问题,Android 7.0 中引入了 APK 签名方案 v2。

APK 签名方案 v2(v2 方案)

Android 7.0 中引入了 APK 签名方案 v2(v2 方案)。该方案会对 APK 的内容进行哈希处理和签名,然后将生成的“APK 签名分块”插入到 APK 中。

多渠道打包方案

Gradle插件模式

    1. 在AndroidManifest.xml中添加渠道信息:

            
  • 2.通过Gradle Plugin提供的productFlavors标签,添加渠道信息:
productFlavors.all { flavor ->
        flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
    }
    
flavorDimensions 'channel'
productFlavors {
    "yingyongbao" {
        dimension "channel"
    }
    "_360" {
        dimension "channel"
    }

Gradle编译生成多渠道包时,会用不同的渠道信息替换AndroidManifest.xml中的占位符。在代码中,也就可以直接读取AndroidManifest.xml中的渠道信息了。

ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(),
                    PackageManager.GET_META_DATA);
int channelId = appInfo.metaData.getInt("UMENG_CHANNEL");

当前方案存在的问题,在开头也说了,现在总结一下(引用VasDolly原文)

1.每生成一个渠道包,都要重新执行一遍构建流程,效率太低,浪费时间,不太适合多渠道包的场景。

2.Gradle会为每个渠道包生成一个不同的BuildConfig.java类,记录渠道信息,导致每个渠道包的DEX的CRC值都不同。一般情况下,这是没有影响的。但是如果你使用了微信的Tinker热补丁方案,那么就需要为不同的渠道包打不同的补丁,这完全是不可以接受的。(因为Tinker是通过对比基础包APK和新包APK生成差分补丁,然后再把补丁和基础包APK一起合成新包APK。这就要求用于生成差分补丁的基础包DEX和用于合成新包的基础包DEX是完全一致的,即:每一个基础渠道包的DEX文件是完全一致的,不然就会合成失败)

有没有可能,在不改变基础包的情况下,快速的打出多渠道包呢?

APK本身也是个zip压缩包,多渠道打包是根据zip包的文件格式来切入的,所以可以先来看下zip包的文件格式。

[local file header 1]
[local file header 1]
[file data 1]
[data descriptor 1]
. 
.
.
[local file header n]
[file data n]
[data descriptor n]
[archive decryption header] (EFS)
[archive extra data record] (EFS)
[central directory]
[zip64 end of central directory record]
[zip64 end of central directory locator] 
[end of central directory record]
  

zip主要由三部分组成:

压缩源文件数据区 中央目录 目录结束
local file header + file data + data descriptor central directory end of central directory record
记录了文件名、压缩算法、压缩前后的文件大小、修改时间、CRC32值等 包含了多个central directory file header(和第一部分的local file header一一对应),每个中央目录文件头主要记录了压缩算法、注释信息、对应local file header的偏移量等 主要记录了中央目录大小、偏移量和ZIP注释信息等

因基于v1方案的多渠道打包,会在目录结束部分做文章,这里把End of central directory record(EOCD)结构详细结构描述下。

目录结束标识存在于整个归档包的结尾,用于标记压缩的目录数据的结束。每个压缩文件必须有且只有一个EOCD记录。

Offset Bytes Description
0 4 End of central directory signature = 0x06054b50 核心目录结束标记(0x06054b50)
4 2 Number of this disk 当前磁盘编号
6 2 number of the disk with the start of the central directory 核心目录开始位置的磁盘编号
8 2 total number of entries in the central directory on this disk 该磁盘上所记录的核心目录数量
10 2 total number of entries in the central directory 核心目录结构总数
12 2 Size of central directory (bytes) 核心目录的大小
16 4 offset of start of central directory with respect to the starting disk number 核心目录开始位置相对于archive开始的位移
20 2 .ZIP file comment length(n) 注释长度
22 n .ZIP Comment 注释内容

有了以上对zip文件格式的认知,来看下android签名方案以及如何从签名方案下手来做多渠道打包。

基于V1签名的多渠道打包方案

根据之前的V1签名和校验机制可知,V1签名只会检验第一部分的所有压缩文件,而不理会后两部分内容。因此,只要把渠道信息写入到后两块内容就可以通过V1校验,而EOCD的注释字段无疑是最好的选择。

上面也说过,apk实际上就是普通的zip,在一个zip文件的最后允许写入N个字符的注释,zip末尾两个部分:2字节的的注释长度+N个字节的注释。

那么,我们只要把签名内容作为注释写入,再修改2字节的注释长度即可。

那么我们怎么知道一个apk有没有写入这个渠道信息呢?

我们可以在文件文件末尾写入一个特殊的字符串,当我们读取文件末尾为这个特殊的字符串,即可认为该apk写入了渠道信息。该特殊字符串称之为魔数

最终的渠道信息为:
渠道字符串+渠道字符串长度+魔数

在APK文件的注释字段,添加渠道信息。
整个方案包括以下几步:

1.复制APK
2.找到EOCD数据块
3.修改注释长度
4.添加渠道信息
5.添加渠道信息长度
6.添加魔数

接下来看如何从apk读取这个渠道信息呢?

根据上面的了解,添加魔数的好处是方便从后向前读取数据,定位渠道信息。 因此,读取渠道信息包括以下几步:

1.定位到魔数
2.向前读两个字节,确定渠道信息的长度LEN
3.继续向前读LEN字节,就是渠道信息了。

用二进制编辑器打开打包好的Apk,看末尾的几个字节,如图:

image.png

对这图可以分析如下:

  • 首先读取8个字节,对应一个特殊字符串“ltlovezh”
  • 往前两个字节为02 00,对应渠道信息长度,实际值为2.
  • 再往前读取2个字节为63 31,对照ASCII表,即可知为c1
    读取到渠道信息为:c1。

到这,渠道读写方案基本完了,再此不再深入,有时间在结合代码分析。

基于V2签名的多渠道打包方案

使用 APK 签名方案 v2 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APK 签名分块”内,v2 签名和签名者身份信息会存储在 APK 签名方案 v2 分块中。


Androiud多渠道打包分析_第1张图片
image.png

上图中,签名前和签名后的 APK
APK 签名方案 v2 是在 Android 7.0 (Nougat) 中引入的。为了使 APK 可在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APK 进行签名,然后再使用 v2 方案对其进行签名。

APK 签名分块

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

为了保护 APK 内容,APK 包含以下 4 个部分:

ZIP 条目的内容(从偏移量 0 处开始一直到“APK 签名分块”的起始位置)
APK 签名分块
ZIP 中央目录
ZIP 中央目录结尾

image.png

上图中,签名后的各个 APK 部分

APK 签名方案 v2 负责保护第 1、3、4 部分的完整性,以及第 2 部分包含的“APK 签名方案 v2 分块”中的 signed data 分块的完整性。

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

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

Androiud多渠道打包分析_第2张图片
image.png

上图中 APK 摘要

由于第 4 部分(ZIP 中央目录结尾)包含“ZIP 中央目录”的偏移量,因此该部分的保护比较复杂。当“APK 签名分块”的大小发生变化(例如,添加了新签名)时,偏移量也会随之改变。因此,在通过“ZIP 中央目录结尾”计算摘要时,必须将包含“ZIP 中央目录”偏移量的字段视为包含“APK 签名分块”的偏移量。

V2校验流程

在 Android 7.0 中,可以根据 APK 签名方案 v2(v2 方案)或 JAR 签名(v1 方案)验证 APK。更低版本的平台会忽略 v2 签名,仅验证 v1 签名。


Androiud多渠道打包分析_第3张图片
image.png

上图 中APK 签名验证过程(新步骤以红色显示)

APK 签名方案 v2 验证

  1. 找到“APK 签名分块”并验证以下内容:
    1. “APK 签名分块”的两个大小字段包含相同的值。
    2. “ZIP 中央目录结尾”紧跟在“ZIP 中央目录”记录后面。
    3. “ZIP 中央目录结尾”之后没有任何数据。
  2. 找到“APK 签名分块”中的第一个“APK 签名方案 v2 分块”。如果 v2 分块存在,则继续执行第 3 步。否则,回退至使用 v1 方案验证 APK。
  3. 对“APK 签名方案 v2 分块”中的每个 signer 执行以下操作:
    1. signatures 中选择安全系数最高的受支持 signature algorithm ID。安全系数排序取决于各个实现/平台版本。
    2. 使用 public key 并对照 signed data 验证 signatures 中对应的 signature。(现在可以安全地解析 signed data 了。)
    3. 验证 digestssignatures 中的签名算法 ID 列表(有序列表)是否相同。(这是为了防止删除/添加签名。)
    4. 使用签名算法所用的同一种摘要算法计算 APK 内容的摘要。
    5. 验证计算出的摘要是否与 digests 中对应的 digest 相同。
    6. 验证 certificates 中第一个 certificate 的 SubjectPublicKeyInfo 是否与 public key 相同。
  4. 如果找到了至少一个 signer,并且对于每个找到的 signer,第 3 步都取得了成功,APK 验证将会成功。

注意:如果第 3 步或第 4 步失败了,则不得使用 v1 方案验证 APK。

基于V2签名的多渠道打包方案

在看一个问题,V2签名是怎么保证APK不被篡改的?
首先,如果破坏者修改了APK文件的任何部分(签名块本身除外),那么APK的数据摘要就和“MF”数据块中记录的数据摘要不一致,导致校验失败。 其次,如果破坏者同时修改了“MF”数据块中的数据摘要,那么“MF”数据块的数字签名就和“SF”数据块中记录的数字签名不一致,导致校验失败。 然后,如果破坏者使用自己的私钥去加密生成“SF”数据块,那么使用开发者的公钥去解密“SF”数据块中的数字签名就会失败; 最后,更进一步,若破坏者甚至替换了开发者公钥,那么使用数字证书中的公钥校验签名块中的公钥就会失败,这也正是数字证书的作用。
综上所述,任何对APK的修改,在安装时都会失败,除非对APK重新签名。但是相同包名,不同签名的APK也是不能同时安装的。

基于V2签名的多渠道打包方案
在上节V2签名的校验流程中,有一个很重要的细节:Android系统只会关注ID为0x7109871a的V2签名块,并且忽略其他的ID-Value,同时V2签名只会保护APK本身,不包含签名块,因此,k添加一个ID-Value,存储渠道信息,即解决了基于V2签名的多渠道打包问题。

方案包括以下几步:

找到APK的EOCD块
找到APK签名块
获取已有的ID-Value Pair
添加包含渠道信息的ID-Value
基于所有的ID-Value生成新的签名块
修改EOCD的中央目录的偏移量(上面已介绍过:修改EOCD的中央目录偏移量,不会导致数据摘要校验失败)
用新的签名块替代旧的签名块,生成带有渠道信息的APK

实际上,除了渠道信息,我们可以在APK签名块中添加任何辅助信息。

多渠道包的强校验

那么如何保证通过这些方案生成的渠道包,能够在所有Android平台上正确安装呢?

Google提供了一个同时支持V1和V2签名和校验的工具:apksig。它包括一个apksigner命令行和一个apksig类库。其中前者就是Android SDK build-tools下面的命令行工具。而我们正是借助后面的apksig来进行渠道包强校验,它可以保证渠道包在apk Minsdk ~ Maxsdk之间都校验通过。

至此,多渠道打包方案基本看完了,还有其他方案,暂未分析,有兴趣的可以自行研究。

Androiud多渠道打包分析_第4张图片
image.png

参考文献:
APK 签名方案 v2
应用签名
VasDolly
walle
带你了解腾讯开源的多渠道打包技术 VasDolly源码解析

你可能感兴趣的:(Androiud多渠道打包分析)