apk签名过程及多渠道

2019-06-13  本文已影响0人  嘻嘻疯子

apk签名过程及多渠道

公司业务渠道较多共有70多个渠道,打包时间较长,所以抽时间研究一下美团的多渠道打包。本文介绍常见的多渠道打包方式:productFlavors方式,apktool,美团1.0,美团2.0,腾讯 这些方式技术从旧到新,试图说起多渠道打包的脉络。

productFlavors

productFlavors不用切换项目分支就可以编译调试不同项目版本的APK,并且可以快速打包所有项目版本的APK。例如是开发第三方Android OS的时候,由于要给不同的厂商做定制,并且适配不同的硬件平台,所以发版本的时候,经常要切换项目分支,然后逐个编译APK。</br>关于更多productFlavor介绍参考:productFlavors详细使用 </br>
productFlavors多渠道打包具体详情参见:Gradle实战:Android多渠道打包方案汇总 </br>

早期的多渠道打包基本上是采用这种方式。首先,在AndroidManifest.xml中添加渠道信息占位符:

<meta-data android:name="InstallChannel" android:value="${InstallChannel}" />

然后,通过Gradle Plugin提供的productFlavors标签,添加渠道信息:

productFlavors{
    "YingYongBao"{
        manifestPlaceholders = [InstallChannel : "YingYongBao"]
    }
    "360"{
        manifestPlaceholders = [InstallChannel : "360"]
    }
}

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

但是,这种方式存在一些缺点:

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

针对上述问题市面上出现了很多第三方,其中比较突出的是apktool,mcxiaoke的packer-ng-plugin,美图的walle和腾讯的VasDolly

apk签名过程及多渠道方案

现市面上存在的多渠道打包方式的原理大都是改apk文件,如此会造成签名验证问题。如此要掌握多渠道,需要先了解apk的签名过程。apk的签名先后有v1,v2,v3三种。

签名相关的基础知识

在了解apk的签名方式之前,我们先要了解签名相关的基础知识

数据摘要

数据摘要算法是一种能产生特定输出格式的算法,其原理是根据一定的运算规则对原始数据进行某种形式的信息提取,被提取出的信息就是原始数据的消息摘要,也称为数据指纹。
一般情况下,数据摘要算法具有以下特点:

数字签名和数字证书

数字签名和数字证书是成对出现的,两者不可分离(数字签名主要用来校验数据的完整性,数字证书主要用来确保公钥的安全发放)。
要明白数字签名的概念,必须要了解数据的加密、传输和校验流程。一般情况下,要实现数据的可靠通信,需要解决以下两个问题:

  1. 确定数据的来源是其真正的发送者。
  2. 确保数据在传输过程中,没有被篡改,或者若被篡改了,可以及时发现。

而数字签名,就是为了解决这两个问题而诞生的。
首先,数据的发送者需要先申请一对公私钥对,并将公钥交给数据接收者。
然后,若数据发送者需要发送数据给接收者,则首先要根据原始数据,生成一份数字签名,然后把原始数据和数字签名一起发送给接收者。
数字签名由以下两步计算得来:

  1. 计算发送数据的数据摘要
  2. 用私钥对提取的数据摘要进行加密
    这样,数据接收者拿到的消息就包含了两块内容:
  3. 原始数据内容
  4. 附加的数字签名

接下来,接收者就会通过以下几步,校验数据的真实性:

  1. 用相同的摘要算法计算出原始数据的数据摘要。
  2. 用预先得到的公钥解密数字签名。
  3. 对比签名得到的数据是否一致,如果一致,则说明数据没有被篡改,否则数据就是脏数据了

因为私钥只有发送者才有,所以其他人无法伪造数字签名。这样通过数字签名就确保了数据的可靠传输。
综上所述,数字签名就是只有发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对发送者发送数据真实性的一个有效证明。

想法虽好,但是上面的整个流程,有一个前提,就是数据接收者能够正确拿到发送者的公钥。如果接收者拿到的公钥被篡改了,那么坏人就会被当成好人,而真正的数据发送者发送的数据则会被视作脏数据。那怎么才能保证公钥的安全性那?这就要靠数字证书来解决了。

数字证书是由有公信力的证书中心(CA)颁发给申请者的证书,主要包含了:证书的发布机构、证书的有效期、申请者的公钥、申请者信息、数字签名使用的算法,以及证书内容的数字签名。

可见,数字证书也用到了数字签名技术。只不过签名的内容是数据发送方的公钥,以及一些其它证书信息。
这样数据发送者发送的消息就包含了三部分内容:

  1. 原始数据内容
  2. 附加的数字签名
  3. 申请的数字证书。

接收者拿到数据后,首先会根据CA的公钥,解码出发送者的公钥。然后就与上面的校验流程完全相同了。

所以,数字证书主要解决了公钥的安全发放问题。
因此,包含数字证书的整个签名和校验流程如下图所示:

[图片上传失败...(image-ed00c-1560483246318)]

V1签名和多渠道打包方案

在android 7.0(N)之前是这种。

V1签名机制

默认情况下,APK使用的就是V1签名。解压APK后,在META-INF目录下,可以看到三个文件:MANIFEST.MF、CERT.SF、CERT.RSA。它们都是V1签名的产物。

其中,MANIFEST.MF文件内容如下所示:

[图片上传失败...(image-da5e67-1560483246319)]

它记录了APK中所有原始文件的数据摘要的Base64编码,而数据摘要算法就是SHA1。

CERT.SF文件内容如下所示:

[图片上传失败...(image-7ba85f-1560483246319)]

SHA1-Digest-Manifest-Main-Attributes主属性记录了MANIFEST.MF文件所有主属性的数据摘要的Base64编码。</br>SHA1-Digest-Manifest则记录了整个MANIFEST.MF文件的数据摘要的Base64编码。</br>其余的普通属性则和MANIFEST.MF中的属性一一对应,分别记录了对应数据块的数据摘要的Base64编码。例如:CERT.SF文件中skin_drawable_btm_line.xml对应的SHA1-Digest,就是下面内容的数据摘要的Base64编码。

Name: res/drawable/skin_drawable_btm_line.xml
SHA1-Digest: JqJbk6/AsWZMcGVehCXb33Cdtrk=
\r\n

这里要注意的是:最后一行的换行符是必不可少,需要参与计算的。

CERT.RSA文件包含了对CERT.SF文件的数字签名和开发者的数字证书。RSA就是计算数字签名使用的非对称加密算法。

V1签名的详细流程可参考SignApk.java,整个签名流程如下图所示:

[图片上传失败...(image-4ab8d7-1560483246319)]

整个签名机制的最终产物就是MANIFEST.MF、CERT.SF、CERT.RSA三个文件。

v1校验流程

在安装APK时,Android系统会校验签名,检查APK是否被篡改。代码流程是:PackageManagerService.java -> PackageParser.java,PackageParser类负责V1签名的具体校验。整个校验流程如下图所示:

[图片上传失败...(image-ea7c2f-1560483246319)]

若中间任何一步校验失败,APK就不能安装。

OK,了解了V1的签名和校验流程。我们来看下,V1签名是怎么保证APK文件不被篡改的?
首先,如果破坏者修改了APK中的任何文件,那么被篡改文件的数据摘要的Base64编码就和MANIFEST.MF文件的记录值不一致,导致校验失败。
其次,如果破坏者同时修改了对应文件在MANIFEST.MF文件中的Base64值,那么MANIFEST.MF中对应数据块的Base64值就和CERT.SF文件中的记录值不一致,导致校验失败。
最后,如果破坏者更进一步,同时修改了对应文件在CERT.SF文件中的Base64值,那么CERT.SF的数字签名就和CERT.RSA记录的签名不一致,也会校验失败。
那有没有可能继续伪造CERT.SF的数字签名那?理论上不可能,因为破坏者没有开发者的私钥。那破坏者是不是可以用自己的私钥和数字证书重新签名那,这倒是完全可以!

综上所述,任何对在MANIFEST.MF中有对应数字摘要的文件修改都会导致签名失败,除非重新签名。任何对在MANIFEST.MF文件的修改也会导致签名失败。
如此针对v1我们可以从以下3个方面下手避免添加渠道信息后导致签名失败。

  1. 添加不被签名包含的文件写入多渠道信息。我们发现在META-INF中新建的文件是不会改变签名结构的,如此可知META-INF中新建文件写入渠道信息,其中美团的第一代打包工具是这样做的。
  2. 我们可以通过逆向手段,添加渠道信息。即解压apk,添加渠道信息,重新签名。市面上apktool是这样弄的
  3. 修改apk文件。我们发现v1的apk分三部分:内容快,中央目录块和中央结束块(EOCD),其中EOCD是生成apk时自动加进去的,不受签名保护,如此可在其中添加渠道信息。市面上mcxiaoke的packer-ng-plugin和腾讯的VasDolly是采用这种原理

apktool

ApkTool是一个逆向分析工具,可以把APK解开,添加代码后,重新打包成APK,当然这些都是通过脚本实现的。因此,基于ApkTool的多渠道打包方案分为以下几步:

复制一份新的APK
通过ApkTool工具,解压APK(apktool d origin.apk)
删除已有签名信息
添加渠道信息(可以在APK的任何文件添加渠道信息)
通过ApkTool工具,重新打包生成新APK(apktool b newApkDir)
重新签名
经过测试,这种方案完全是可行的。

优点:
不需要重新构建新渠道包,仅需要复制修改就可以了。并且因为是重新签名,所以同时支持V1和V2签名。

缺点:
ApkTool工具不稳定,曾经遇到过升级Gradle Plugin版本后,低版本ApkTool解压APK失败的情况。
生成新渠道包时,需要重新解包、打包和签名,而这几步操作又是相对比较耗时的。经过测试:生成企鹅电竞10个渠道包需要16分钟左右,虽然比Gradle Plugin方案减少很多耗时。但是若需要同时生成上百个渠道包,则需要几个小时,显然不适合渠道非常多的业务场景。

修改apk

apktool存在诸多缺点,针对v1我采用的还是添加文件和修改apk来添加渠道信息的。修改文件原理教简单,下面我们重点介绍修改apk

apk文件结构

修改apk得先知道其结构。APK文件本质上是一个ZIP压缩包,而ZIP格式是固定的,主要由三部分构成,如下图所示:

[图片上传失败...(image-74cf23-1560483246319)]

[图片上传失败...(image-58a6c5-1560483246319)]

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

向apk文件结构中写入渠道信息

既然找到了突破口,那么基于V1签名的多渠道打包方案就应运而生:在APK文件的注释字段,添加渠道信息。

整个方案包括以下几步:

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

[图片上传失败...(image-91f24-1560483246319)]

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

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

通过16进制编辑器,可以查看到添加渠道信息后的APK(小端模式),如下所示:

[图片上传失败...(image-d8a279-1560483246319)]

6C 74 6C 6F 76 75 7A 68是魔数,04 00表示渠道信息长度为4,6C 65 6F 6E就是渠道信息leon了。0E 00就是APK注释长度了,正好是15。

虽说整个方案很清晰,但是在找到EOCD数据块这步遇到一个问题。如果APK本身没有注释,那最后22字节就是EOCD。但是若APK本身已经包含了注释字段,那怎么确定EOCD的起始位置那?这里借鉴了系统V2签名确定EOCD位置的方案。整个计算流程如下图所示:

[图片上传失败...(image-b692b2-1560483246319)]

整个方案介绍完了,该方案的最大优点就是:不需要解压缩APK,不需要重新签名,只需要复制APK,在注释字段添加渠道信息。每个渠道包仅需几秒的耗时,非常适合渠道较多的APK。

但是好景不长,Android7.0之后新增了V2签名,该签名会校验整个APK的数据摘要,导致上述渠道打包方案失效。所以如果想继续使用上述方案,需要关闭Gradle Plugin中的V2签名选项,禁用V2签名。

V2签名和多渠道打包方案

为什么需要V2签名

从前面的V1签名介绍,可以知道V1存在两个弊端:

    • MANIFEST.MF中的数据摘要是基于原始未压缩文件计算的。因此在校验时,需要先解压出原始文件,才能进行校验。而解压操作无疑是耗时的。
    • V1签名仅仅校验APK第一部分中的文件,缺少对APK的完整性校验。因此,在签名后,我们还可以修改APK文件,例如:通过zipalign进行字节对齐后,仍然可以正常安装。

正是基于这两点,Google提出了V2签名,解决了上述两个问题:

  1. V2签名是对APK本身进行数据摘要计算,不存在解压APK的操作,减少了校验时间。
  2. V2签名是针对整个APK进行校验(不包含签名块本身),因此对APK的任何修改(包括添加注释、zipalign字节对齐)都无法通过V2签名的校验。
    关于第一点的耗时问题,这里有一份实验室数据(Nexus 6P、Android 7.1.1)可供参考。
APK安装耗时对比 取5次平均耗时(秒)
V1签名APK 11.64
V2签名APK 4.42

可见,V2签名对APK的安装速度还是提升不少的。

V2签名机制

不同于V1,V2签名会生成一个签名块,插入到APK中。因此,V2签名后的APK结构如下图所示:

[图片上传失败...(image-ba6827-1560483246319)]

APK签名块位于中央目录之前,文件数据之后。V2签名同时修改了EOCD中的中央目录的偏移量,使签名后的APK还符合ZIP结构。

APK签名块的具体结构如下图所示:

[图片上传失败...(image-54d006-1560483246319)]

  1. 首先是8字节的签名块大小,此大小不包含该字段本身的8字节;
  2. 其次就是ID-Value序列,就是一个4字节的ID和对应的数据;
  3. 然后又是一个8字节的签名块大小,与开始的8字节是相等的;最后是16字节的签名块魔数。
  4. 其中,ID为0x7109871a对应的Value就是V2签名块数据。

V2签名块的生成可参考ApkSignerV2,整体结构和流程如下图所示:

[图片上传失败...(image-f003cc-1560483246319)]

  1. 首先,根据多个签名算法,计算出整个APK的数据摘要,组成左上角的APK数据摘要集;
  2. 接着,把最左侧一列的数据摘要、数字证书和额外属性组装起来,形成类似于V1签名的“MF”文件(第二列第一行);
  3. 其次,再用相同的私钥,不同的签名算法,计算出“MF”文件的数字签名,形成类似于V1签名的“SF”文件(第二列第二行);
  4. 然后,把第二列的类似MF文件、类似SF文件和开发者公钥一起组装成通过单个keystore签名后的v2签名块(第三列第一行)。
  5. 最后,把多个keystore签名后的签名块组装起来,就是完整的V2签名块了(Android中允许使用多个keystore对apk进行签名)。

上述流程比较繁琐。简而言之,单个keystore签名块主要由三部分组成,分别是上图中第二列的三个数据块:类似MF文件、类似SF文件和开发者公钥,其结构如下图所示:

[图片上传失败...(image-e251d8-1560483246319)]

除此之外,Google也优化了计算数据摘要的算法,使得可以并行计算,如下图所示:

[图片上传失败...(image-6be7d4-1560483246319)]

数据摘要的计算包括以下几步:

  1. 首先,将上述APK中文件内容块、中央目录、EOCD按照1MB大小分割成一些小块。
  2. 然后,计算每个小块的数据摘要,基础数据是0xa5 + 块字节长度 + 块内容。
  3. 最后,计算整体的数据摘要,基础数据是0x5a + 数据块的数量 + 每个数据块的摘要内容。

这样,每个数据块的数据摘要就可以并行计算,加快了V2签名和校验的速度。

V2校验流程

Android Gradle Plugin2.2之上默认会同时开启V1和V2签名,同时包含V1和V2签名的CERT.SF文件会有一个特殊的主属性,如下图所示:

[图片上传失败...(image-ecbad4-1560483246319)]

该属性会强制APK走V2校验流程(7.0之上),以充分利用V2签名的优势(速度快和更完善的校验机制)。
因此,同时包含V1和V2签名的APK的校验流程如下所示:

[图片上传失败...(image-11afa7-1560483246319)]

简而言之:优先校验V2,没有或者不认识V2,则校验V1。

这里引申出另外一个问题:APK签名时,只有V2签名,没有V1签名行不行?
经过尝试,这种情况是可以编译通过的,并且在Android 7.0之上也可以正确安装和运行。但是7.0之下,因为不认识V2,又没有V1签名,所以会报没有签名的错误。

OK,明确了Android平台对V1和V2签名的校验选择之后,我们来看下V2签名的具体校验流程(PackageManagerService.java -> PackageParser.java-> ApkSignatureSchemeV2Verifier.java),如下图所示:

[图片上传失败...(image-632fdb-1560483246319)]

其中,最强签名算法是根据该算法使用的数据摘要算法来对比产生的,比如:SHA512 > SHA256。

校验成功的定义是至少找到一个keystore对应的签名块,并且所有签名块都按照上述流程校验成功。

下面我们来看下V2签名是怎么保证APK不被篡改的?

首先,如果破坏者修改了APK文件的任何部分(签名块本身除外),那么APK的数据摘要就和“MF”数据块中记录的数据摘要不一致,导致校验失败。

其次,如果破坏者同时修改了“MF”数据块中的数据摘要,那么“MF”数据块的数字签名就和“SF”数据块中记录的数字签名不一致,导致校验失败。

然后,如果破坏者使用自己的私钥去加密生成“SF”数据块,那么使用开发者的公钥去解密“SF”数据块中的数字签名就会失败;

最后,更进一步,若破坏者甚至替换了开发者公钥,那么使用数字证书中的公钥校验签名块中的公钥就会失败,这也正是数字证书的作用。

综上所述,任何对APK的修改,在安装时都会失败,除非对APK重新签名。但是相同包名,不同签名的APK也是不能同时安装的。

其实也很简单,原来Android系统在校验APK的数据摘要时,首先会把EOCD的中央目录偏移量替换成签名块的偏移量,然后再计算数据摘要。而签名块的偏移量不就是v2签名之前的中央目录偏移量嘛!!!,因此,这样计算出的数据摘要就和“MF”数据块中的数据摘要完全一致了。具体代码逻辑,可参考ApkSignatureSchemeV2Verifier.java的416 ~ 420行

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

在上节V2签名的校验流程中,有一个很重要的细节:Android系统只会关注ID为0x7109871a的V2签名块,并且忽略其他的ID-Value,同时V2签名只会保护APK本身,不包含签名块。

因此,基于V2签名的多渠道打包方案就应运而生:在APK签名块中添加一个ID-Value,存储渠道信息。

整个方案包括以下几步:

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

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

通过16进制编辑器,可以查看到添加渠道信息后的APK(小端模式),如下所示:

[图片上传失败...(image-20e35e-1560483246319)]

V3签名和多渠道打包方案

在android 9.0(N)引入的

为什么要有v3

主要是为了换签名

生成签名的时,可以指定一个有效时间,这个时间默认为 25 年,并且 Google Play 也有硬性规定,上架的 App 签名有效期必须在 2033-10-22 日期之后。所以只要不是手欠修改了这个有效期,在当下这个时刻,是不会有问题,毕竟到现在还没有一款 App 存在 25 年。当然还有可能是公司被收购 需要改签名
有些问题不在眼前,却是真实存在的。对于一款上架的 App,最重要的就是用户,而当签名失效之后,我们只能被迫换签名,此时因为签名校验无法通过,就会导致旧用户无法覆盖安装。这些历史用户唯一的选择,就是卸载后重新安装。
好在这不仅仅是你我的问题,天塌下来有个子高的顶着,所以别担心,Google 已经着手在解决这个问题了。

v3签名块结构

v3版本签名块也分成同样的三部分,与v2不同的是在SignerData部分,v3新增了attr块,其中是由更小的level块组成。每个level块中可以存储一个证书信息。前一个level块证书验证下一个level证书,以此类推。最后一个level块的证书,要符合SignerData中本身的证书,即用来签名整个APK的公钥所属于的证书。两个版本的签名块结构如下:

[图片上传失败...(image-95af1f-1560483246319)]

v3验证签名流程

因为签名的验证就是发生在一个apk包的安装过程中,所以为了更清楚验证签名的时机,有必要了解整个安装的分类与大致流程。Android安装应用主要有如下四种方式:

但是其实无论通过哪种方式安装都要通过PackageManagerService来完成安装的主要工作,最终在PMS中会去验证签名信息,流程如下

[图片上传失败...(image-fd0b3a-1560483246319)]

安装过程中如果发现有v3签名块,则必须使用v3签名的验证机制,不能绕过。否则才使用v2签名的验证机制,以此类推。

验证完整性

数据完整性校验v3与v2版本相同,原理如下:

[图片上传失败...(image-29bcb8-1560483246319)]

签名块包括对apk第一部分,第二部分,第三部分的二进制内容做加密保护,摘要算法以及签名算法。签名块本身不做加密,这里需要特殊注意的是由于第三部分包含了对第二部分的引用偏移,因此如果签名块做了改变,比如在签名过程中增加一种签名算法,或者增加签名者等信息就会导致这个引用偏移发生改变,因此在算摘要的时候需要剔除这个因素要以第三部分对签名块的偏移来做计算。

验证证书

v2版本签名验证证书步骤:

[图片上传失败...(image-6bfa12-1560483246319)]

v3版本签名验证证书步骤:(前三步同v2)

[图片上传失败...(image-af6403-1560483246319)]

新特性场景举例

其实就是当开发者需要更换证书时,即可直接用新证书新的私钥进行签名。不过为了让老应用相信新的证书,则需要用老证书来保证。举个例子,有两个level块:level 1与level 2:

v3多渠道方案

略 原理和v2同

参考:

https://www.jianshu.com/p/332525b09a88
https://github.com/Meituan-Dianping/walle/
https://segmentfault.com/a/1190000015554496
https://juejin.im/entry/5a586bfaf265da3e2c3808c5
https://blog.csdn.net/u010818425/article/details/52319382
https://github.com/Tencent/VasDolly
https://cloud.tencent.com/developer/article/1004884
http://picksomething.cn/2018/05/08/Android%E5%A4%9A%E6%B8%A0%E9%81%93%E6%89%B9%E9%>87%8F%E6%89%93%E5%8C%85%EF%BC%8C%E6%94%AF%E6%8C%81%E5%8F%8B%E7%9B%9F%E5%92%8C%E7%AC%>AC%E4%B8%89%E6%96%B9%E5%8A%A0%E5%9B%BA/
http://twei.site/2016/08/31/MarkdownPad-2-%E6%94%AF%E6%8C%81%E8%A1%A8%E6%A0%BC/

上一篇下一篇

猜你喜欢

热点阅读