分析 Android V2 新签名打包机制

作者:pisazzpan

Android Studio 2.2发布之后公示了很多新特性,其中一些特性继承在了gradle plugin当中,这些不易被我们发现,比如新的签名机制(APK Signature Scheme v2),本文对Android Gradle 2.2新推出来的新签名打包机制(V2新签名方案)作出相关分析,目前在Android 7.0以及之上版本已经对这套新签名机制提供了支持,因此随着版本的提升,新签名机制方案将是大势所趋。

新签名机制集成在了Gradle Plugin 2.2以及之上版本的插件当中,默认是开启的,它的优点在于加块了包签名验证的过程,减少了包安装的时间,增强了apk包的完整性保障。对于v2新签名打包机制,想在签名过后的apk包内容做任何改动都会导致在Android 7.0以及之上版本安装不成功。

最后本文实现了一种在apk的签名块中写入信息,读取信息,删除信息还原apk等功能,验证了在签名块中写入信息可以通过v2检验的例子。

现在我们就来一起看看新的签名机制是啥样的。

1.起源

在此之前我们得先大概了解一下老的签名方案(后面以v1表示老签名方案)是什么样的,看看它到底存在哪些问题使得google提出一套更好的签名校验机制呢。

  • v1签名方案

我们都知道在签名之后,打开apk包,在apk目录下的META—INF目录下一般有三个文件:MANIFEST.MF,CERT.SF,CERT.RSA三个文件,这里用不同的证书和签名方式得到的名字可能不同,以及对于多个证书的情况,就会对应有多个.MF,.SF,CERT.RSA文件。

(1) .MF文件

apk当中的原始文件信息用摘要算法如SHA1计算得到的摘要信息并用base64编码保存,以及对应采用的摘要算法如SHA1(这个算法的特性是不管多大的文件内容都能够得到长度相同的摘要信息但是不同的文件内容信息得到的摘要信息肯定不同)

(2) .SF文件

.MF文件的摘要信息以及.MF文件当中每个条目在用摘要算法计算得到的摘要信息并用base64编码保存

(3) .RSA文件

存放证书信息,公钥信息,以及用私钥对.SF文件的加密数据即签名信息,这段数据是无法伪造的,除非有私钥,另外.RSA文件还记录了所用的签名算法等信息。

Apk包在安装的时候,是按照从(3)到(1)的顺序依次校验的,先用公钥还原签名信息,然后和.SF文件中的信息比对,然后用同样的摘要算法对.MF文件里面的每一个条目计算对应的摘要信息,然后比对.MF文件是否一致。

在这个过程中,我们发现有两点:

(1) 在校验的过程中需要解压,因为.MF文件的摘要信息是基于原始未压缩文件内容,因此在校验的时候就需要解压出原始数据,而这个解压操作无疑是耗时操作。

(2) apk包的完整性校验不够强。这里可以看到如果我们在apk签名后,如果对apk包中没有涉及到原始文件内容的数据块做改变那么这层校验机制就会失效(如直接通过二进制改变apk包的无关数据块如核心中央目录注释字段写一些无关注释,然后用zipalign工具对齐,则apk包还是可以正常安装的,这样就绕过了v1层的校验机制)

由此v2模式的签名机制提出来了。看看是怎么改善上述两个问题的。

问题1:耗时问题

v2签名机制不存在解压原始数据,签名校验时间显著减少,因此安装时间也相应减少。

问题2:一致性校验是否够强

v2签名机制是直接基于apk的二进制内容做的签名信息(v2签名块本身不参与加密校验),因此打包后改变apk的原来三部分的任何字节都会导致签名校验不通过。

采用v2签名机制也有一些苦恼,那就是在签名之后对apk包中的任何改动都不再允许了,否则在大于等于Android 7.0之上版本就会安装不成功。默认的Android Gradle Plugin 2.2是开启了v2签名机制的,当然我们是可以选择关掉的,可以在build.gradle中的signConfig闭包中如下面配置:

v1SigningEnabled false
v2SigningEnabled false

当然我们v2签名机制也是提供了命令行方式可以签名的。用apksigner工具(jarsigner工具不兼容v2签名)

2. v2签名模式具体详解

简单来说,v2签名模式在原先apk块中增加了一个新的块(签名块),新的块存储了签名,摘要,签名算法,证书链,额外属性等信息,这个块有特定的格式,具体格式分析见后文,先看下现在apk成什么样子了。

apk的格式签名后变成了下面4个部分:

   Local file header 1   //第一部分开始
     compressed data
   data descriptor(可选)
   .....
   Local file header n
     compressed data
   data descriptor(可选)   //第一部分结束

   APK Signning Block      //签名块

   Central directory header1 //第二部分开始
   ....
   Central directory headern //第二部分结束
   End Of Central Directory  //第三部分

其中第三部分有一个偏移值直接指向了第二部分的开始位置,而每个第二部分如Central directory header1,…,Central directory headern的有一个偏移字段指向了其中对应的第一部分。

Central directory header1--->Local file header1
...
Central directory headern--->Local file headern

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

2.1 v2签名块格式

接下来我们来看看具体的apk签名块的格式,具体参考源码:ApkSignerV2.java,如下图所示:

该格式分为4个部分,都很好理解,其中块大小不包含第一块本身的8字节:其中第二部分包含了v2模式块,v2模式块的具体格式稍复杂如下图右边所示,v2模式块对应的ID为:0x7109871a

2.2 apk分块计算摘要信息

从上面我们可以看到v2模式块有点类似于我们META-INF文件夹下的信息内容。那么对于上述当中摘要的信息又是怎么计算出来的呢。如下图:

首先将上述apk中第一部分,第二部分,第三部分按照1MB大小分割成一些小块。对于每个小块如图中第二层上每个摘要信息取的信息来源是级联对应块上0x05字节,块字节长度,块内容。顶部第一层的摘要信息取的信息来源是级联0x5a字节,对应块的数目,以及apk中第二层如按照对应顺序chunk1,chunk2…chunkn的摘要信息的级联。计算这个信息可以可以采用并行计算。

2.3 实际apk二进制信息查看签名块信息

接下来,来一个实际用gradle plugin 2.2打的apk包,来看看里面的信息内容是怎样的吧。

首先找到对应apk包中第三部分

找到标识符(0x06054b50),然后定位到偏移核心中央目录的偏移字段(0x01fc3ef2)。

然后定位到0x01fc3ef2,找到了apk中第二部分,按照现在apk的格式,在这个部分上面就应该是签名块了。看看这部分的数据是不是这样:

我们看到0x01fc3ef2处的4个字节为0x02014b50这刚好是核心重要目录的标识符,往上16个字节就是apk签名块的魔法数:41 50 4b 20 3 69 67 20 42 6c 6f 63 6b 20 34 32代表:”APK Sig Block 42“的固定值,以大端存储。

在往上8个字节就是签名块的大小:这里可以看到大小为:0x068d,所以这里我们根据块大小和魔法数位置可以快速定位到签名块的开始位置偏移处,通过计算0x01fc3eda-0x0000068d+0x00000010=0x01fc385d,定位到0x01fc385d处,往后8个字节也为块大小0x068d,发现两个块大小的确一致。接下来开始分析v2 scheme 块的格式,如下图所示:

由上面apk签名块格式中我们可以知道,在V2模式块部分当中,包含了长度,以及ID-value值,其中以特殊标识ID=0x7109871a开始,value值则为v2模式块当中的具体内容。

ID:0x7109871a
value:0x00000665

//从这里看到接下来的数据就是属于v2签名模式块的数据格式了,如图一右边所示以及图2所示
//接下来按照图二的格式依次解析:
....
可以看到在下面有一处为:
签名算法ID:0x00000103
长度前缀digest: 0x00000020

//其中0x0103表明用的签名算法是:RSASSA-PKCS1-v1_5 with SHA2-256 digest

3 v2签名校验过程

接下来我们来看v2签名的校验过程,整体大概流程如下图所示:

其中v2签名机制是在android 7.0以及以上版本才支持。因此对于android 7.0以及以上版本

在安装过程中,如果发现有v2签名块,则必须走v2签名机制,不能绕过。否则降级走v1签名机制。

v1和v2签名机制是可以同时存在的,其中对于v1和v2版本同时存在的时候,v1版本的META_INF的.SF文件属性当中有一个 X-Android-APK-Signed属性:

X-Android-APK-Signed: 2

因此如果想绕过v2走v1校验是不行的。

3.1 v2签名校验信息内容

1 .apk的签名块

1.1. apk签名块中的两个字节大小字段是否相等

1.2. apk的第三部分和第二部分是紧挨着的,且核心中央目录在前面。

1.3. apk第三部分后面没有任何多余的数据。

2 .定位到v2模式签名块,如果存在的话,即存在id=0x7109871a的ID,调到步骤3.否则降级为v1校验。

3 . 对于每个签名者的v2签名块,具体格式见上面图1

3.1 从签名当中选择系统支持的最强的签名算法ID

3.2 用公钥还原v2模式块中的签名信息,并比对是否和图1中原始的加密数据是否一致。

3.3 比对v2模式块中加密数据和签名信息所用的签名算法ID列表是否一致。

3.4 用同样的摘要算法计算apk内容对应块的摘要,和v2签名块中的摘要信息是否一致。

3.4 比对公钥信息当中所用数字证书是否一致。

校验成功定义:至少找到其中一个签名者的签名信息,以及每个签名者的签名信息都校验成功。

4 . v2新签名写入渠道号信息的实现

至此,v2模式的新签名机制就结束了。新的v2模式签名机制提醒我们在v2签名之后,对apk本身做任何改动都会导致校验不通过的情况,导致在android 7.0以及之上都会安装不成功。K歌由于一些原因需要在rdm上打包签名的最后一步会对apk的注释字段会写部分信息,那么按照新的v2机制,则rdm打出来的包在Android 7.0必然存在安装不上的情况。下面是失败提醒:

因此对于Android 7.0的签名过程必然是发生在最后一步,如果非要写部分信息,考虑是否可以写到签名块当中,因为签名块本身是没有加密的。这里也实现了一下渠道包的写入,经过检验,成功通过了v1和v2的校验。

核心部分就是下面这个函数。具体可以查看源码github(https://github.com/pmathticol/AndroidSignApkToolV2 )。

public byte[] getAfterWriteChanaelSignatureBlock(byte[] v2SchemeBlockBytes, String chanael) throws ApkParseException {
        //上面就是原始的4个块,接下来开始重点改造第二个块,然后重写initalEocd对应的偏移即可喽。
        if (v2SchemeBlockBytes == null || v2SchemeBlockBytes.length < 1) {
            printoutString("get v2SchemeBlockBytes is not correct");
            throw new ApkParseException("error get v2SchemeBlockBytes");
        }

        byte[] chanelBytes = chanael.getBytes(Charset.forName("UTF-8"));
        int resultSize =
                8 // size
                        + 8 + 4 + v2SchemeBlockBytes.length // v2Block as ID-value pair
                        + 4 + chanelBytes.length
                        + 8 // size
                        + 16 // magic
                ;
        ByteBuffer result = ByteBuffer.allocate(resultSize);
        result.order(ByteOrder.LITTLE_ENDIAN);
        long blockSizeFieldValue = resultSize - 8;
        result.putLong(blockSizeFieldValue); //被修改了

        long pairSizeFieldValue = 4 + v2SchemeBlockBytes.length; //这个值还没变
        result.putLong(pairSizeFieldValue);
        result.putInt(ApkSignerV2UtilTool.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
        result.put(v2SchemeBlockBytes);

        result.putInt(CHANEL_ID);
        result.put(chanelBytes);

        result.putLong(blockSizeFieldValue);
        result.put(ApkSignerV2UtilTool.APK_SIGNING_BLOCK_MAGIC);

        return result.array();
    }

具体用法如下:

java -jar AndroidSignApkToolV2-1.1.0.jar -input 原apk -output 签名后apk -chanael 渠道号名字

由于涉猎知识有限,本文讲述的内容难免有误。如有误的地方,望不吝指出。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏屈定‘s Blog

工作--如何封装第三方服务?

业务开发中经常会对接某某第三方服务,因此会经常写一些SDK供服务使用,一种比较好的做法就是使用命令模式封装第三方服务,命令模式对于调用方来说简洁明了,也正是封装...

632
来自专栏前端那些事

Express4.x API (三):Response (译)

Express4.x API 译文 系列文章 技术库更迭较快,很难使译文和官方的API保持同步,更何况更多的大神看英文和中文一样的流畅,不会花时间去翻译--,所...

16310
来自专栏编程一生

linux内核中听过就能记住的概念

942
来自专栏枕边书

用Lua定制Redis命令

前言 Redis作为一个非常成功的数据库,提供了非常丰富的数据类型和命令,使用这些,我们可以轻易而高效地完成很多缓存操作,可是总有一些比较特殊的问题或需求需要解...

3687
来自专栏网络

Nginx 系列实用教程#2:性能

协作翻译 原文:Nginx Tutorial #2: Performance 链接:https://www.netguru.co/codestories/ngi...

1886
来自专栏月牙寂

k8s源码分析-----kubelet(7)containerRuntime

第一时间获取文章,可以关注本人公众号 月牙寂道长 yueyajidaozhang

3786
来自专栏美团技术团队

Android远程调试的探索与实现

作为移动开发者,最头疼的莫过于遇到产品上线以后出现了bug,但是本地开发环境又无法复现的情况。常见的调查线上棘手问题方式大概如下: ? 以上两种方法在之前调查线...

3783
来自专栏FreeBuf

Java反序列化漏洞从理解到实践

一、前言 在学习新事物时,我们需要不断提醒自己一点:纸上得来终觉浅,绝知此事要躬行。这也是为什么我们在学到知识后要付诸实践的原因所在。在本文中,我们会深入分析大...

24210
来自专栏web前端教室

聊一下get和post的区别?

image.png 网上看了些资料,看了半天,往深里想,我自己也说不清楚了。所以决定写个东西分享一下,给你们分享一下,我就会了不少,哈哈。这就是分享就是学习...

17510
来自专栏JetpropelledSnake

Python Web学习笔记之并发编程的孤儿进程与僵尸进程

1193

扫码关注云+社区