APK签名机制说明

背景

由于业务需求,我们需要统计用户下载安装apk的来源,所以针对不同的应用市场或活动页面,我们发布的apk都带了对应的渠道信息,针对apk携带对应的渠道信息,网上已经有很多成熟的方案了,例如使用gradle来构建不同的渠道包等方式。然而考虑到打包效率的问题,我们并没有使用gradle来构建多渠道的方式,而是使用美团的自动化生成渠道包的方式来实现的。

由于这种方式并不能适配APK Signature Scheme v2机制,所以我们的项目中一直都关闭了使用v2的签名机制。然后由于最近一些安全的问题,所以我们不得不在项目中开始使用v2机制来进行签名apk,虽然美团针对v2签名机制提供了另外的生成渠道包方案,但是还是想在使用之前,更多了解一下android的签名机制。

关于APP签名

APP签字在Android系统中有着极其重要的作用,主要体现在如下:

  1. 校验apk是否伪造或被修篡改
  2. 升级APP时校验目标apk是否与原app一致
  3. 代码或数据共享,一个应用可以为另一个使用相同证书签名的应用程序公开自己的功能或数据

APK格式

​apk文件,本质上就是一个zip格式的文件:


​而apk签名,则是基于此文件格式,添加apk文件中部分数据的数字签名信息;而在检验的时候,读取该部分签名信息并对其所对应的部分数据进行校验。

Zip文件格式

因为apk本质就是一个zip格式的文件,所以,在开始了解apk的签名之前,有必要了解一下zip的文件格式:

.ZIP文件格式总览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[文件标头 1]
[文件数据 1]
[数据描述符记录 1]
.
.
.
[文件标头 n]
[文件数据 n]
[数据描述符记录 n]
[归档解密标头]
[归档额外数据记录]
[中央目录结构]
[中央目录记录的 Zip64 结尾]
[中央目录定位器的 Zip64 结尾]
[中央目录记录的结尾]

其中有几个数据在后续的漏洞和签名分析中,有重要作用,在这进行一下详细说明:
Local File Header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- A 文件标头 --------------------

文件标头签名 4 字节 [开始 0] (0x04034b50)
所需版本 2 字节 [开始 4]
一般用途位标记 2 字节 [开始 6]
压缩方法 2 字节 [开始 8] (8=DEFLATE; 0=UNCOMPRESSED)
文件的最后修改时间 2 字节 [开始 10]
文件的最后修改日期 2 字节 [开始 12]
crc-32 4 字节 [开始 14]
压缩后的大小 4 字节 [开始 18]
解压缩后的大小 4 字节 [开始 22]
文件名长度 2 字节 [开始 26]
额外字段长度 2 字节 [开始 28]

文件名 变量
额外字段 变量

End of Central Directory

1
2
3
4
5
6
7
8
9
10
11
- I 中央目录记录的结尾 --------------------

中央目录记录签名 4 字节 [开始 0] (0x06054b50) 注:使用“冒泡”从文件尾追查上来,找到这个签名。
磁盘编号 2 字节 [开始 4]
中央目录开始磁盘编号 2 字节 [开始 6]
本磁盘上在中央目录里的入口总数 2 字节 [开始 8]
中央目录里的入口总数 2 字节 [开始 10] 注:文件总数,文件夹也算一个文件。
中央目录的大小 4 字节 [开始 12]
中央目录对第一张磁盘的偏移量 4 字节 [开始 16]
.ZIP 文件注释长度 2 字节 [开始 20]
.ZIP 文件注释 变量 [开始 22]

而解析zip文件的过程,则是通过从文件尾部逆向查找End of Central Directory的起始标识,从而解析对应的Central Directory,进而解析zip里面的所有文件信息。

APK Signature Scheme v1

​v1 scheme是基于jar签名来实现,而jar的签名机制,则是往zip文件中,插入一个META-INFO文件夹,然后将所有与签名相关的信息都保存在这个文件夹下。同样,通过对比已签名和未签名apk解压后的META-INFO文件夹下的文件,以分析v1 scheme签名的实现原理:

相比未签名的apk,已签名的apk的META-INFO文件夹下,多了{name}.RSA和{name}.SF文件,根据JAR文件格式描述可知:

  • MANIFEST.MF 保存了apk中所有需要校验的文件所对应的摘要
  • {name}.SF 保存了MANIFEST.MF中所对应每块数据所对应的摘要
  • {name}.RSA 保存了对应非对称加密算法所使用的公钥和{name}.SF文件经过非对称加密后的密文


以上三个文件的生成逻辑,都可以在build/tools/signapk/src/com/android/signapk/SignApk.java源码文件中找到。

  • MANIFEST.MF文件的生成,在的addDigestsToManifest方法中。签名工具会遍历apk文件中的所有文件(过滤其中的一些文件),并针对这些文件通过摘要算法生成摘要信息并base64编码之后保存在MANIFEST.MF文件中:



  • {name}.SF文件的生成,则在writeSignatureFile方法中。签名工具会读取MANIFEST.MF文件中的所有section,然后针对每个section通过摘要算法生成摘要信息并base64编码之后保存在{name}.SF文件中,此处很容易让人误解{name}.SF文件中的内容是经过非对称加密之后所产生的,然而并非如此,该方法连加密所需要用到的公钥和私钥都没有被传入,只是针对每个section进行摘要并base64编码而已:



  • {name}.RSA文件的生成,在writeSignatureBlock方法中。该方法将生成的{name}.SF文件经过非对称加密生成密文之后,与公钥合并生成{name}.RSA文件:


而在对apk进行验签的过程,则是与生成apk相反的过程,系统会先根据{name}.RSA中的公钥和密文,还原出原文并与{name}.SF对比,以验证{name}.SF文件是否被修改过,然后在根据MANIFEST.MF文件(读取MANIFEST.MF的时候,会根据MANIFEST.MF中section信息进行验证apk中的文件是否会修改过的)中的section与{name}.SF文件中的section进行对比验证。

由于v1签名不会对META-INFO文件夹内的文件进行校验,所以可以通过在META-INFO文件夹下添加文件以达到APK携带渠道等信息的目的。

APK Signature Scheme v2

鉴于APK Sinature Scheme v1存在安全漏洞,所以google推出了新的签名校验的方式:APK Signature Scheme v2。
v2是针对整个文件进行签名校验的机制,所以具有更高的安全性和更好的校验速度。其实现机制在于,在ZIP 的Central Directory 部分前面插入一个APK Signature Block,如下图:

APK Signature Scheme v2 Signing Block

Signing Block 格式如下:

1
2
3
4
5
6
7
8
[size of block]                         8 字节
[length prefixed ID-values pairs] 8 字节 + 4 字节 + (length - 4)字节 (8字节的长度,标识后面这对id和value的长度)
.
.
.
[length prefixed ID-values pairs]
[size of block] 8 字节
[magic] 16 字节,内容为"APK Sig Block 42"

而apk的签名校验信息(公钥及加密的密文)则是存在ID为0x7109871a的value中,所以获取apk签名校验信息的过程,其实就是查找Signing Block中id为0x7109871a的过程:


而在查找签名校验信息的过程中,系统会过滤ID为其他值的键值对。

APK Signature Scheme v2签名方式

v2签名会计算除Signing Block之外的所有数据的摘要,然后使用非对称加密之后保存在Signing block中:

在对apk进行签名的时候,签名工具会将原的Central Directory前的数据(不包含signing block)、Central Directory和End of Central Directory分别进行分段并计算摘要(每段都分成1M大小的数据,然后计算0xa5 + Chunk Lenght(4 byte) + Chunk的摘要),然后再针对计算出来的摘要数据(计算0xa5 + Chunks Count(4 byte) + Chunks Digests)再进行一次摘要计算,从而得到最终待加密的摘要数据,最后合并摘要加密后的数据和证书的公钥组成Signing Block。
其中有个让人困惑的地方是,签名工具在Central Directory前面插入Signing Block之后,会导致End of Central Directory中保存的Central Directory offset变更(不然会破坏其原有的ZIP格式而导致无法解压出其中的文件):

那么这必然会导致后续验签的时候,根据End of Central Directory计算出来的摘要数据与之前不匹配。很多资料或者博文都没有提到这点是怎么解决的,甚至还有资料说Central Directory offset字段不参与计算摘要的,但是通过查看计算摘要信息的算法中,并没有对该字段进行特殊处理的:

其中contents数组对应上面的beforeCentralDir, centralDir 和 eocd
既然这个问题,在生成签名的时候没有绕过,那么只可能是在验签的时候进行绕过了。事实上,在验签的过程中,系统在获取到End of Central Directory数据之后,会先将Central Directory offset字段还原回没有Signing Block的时候Central Directory的offset(即Signing Block的起始位置):

关于APK Shgnature Scheme v1的漏洞

“MasterKey”漏洞 & “Janus”漏洞 & “9950697”漏洞

参考独家分析:安卓“Janus”漏洞的产生原理及利用过程

APK Signature Scheme v2的不足

由于Android的签名校验机制是向前兼容的,所以Signature Scheme v2是在v1的基础上,再次进行了全文件的摘要验签,以保证即使使用v2进行签名,在Android N之前的系统中也能够正常校验签名,所以本质上使用v2签名,同时也会有v1的签名信息的。而Android N及之后的签名校验流程如下:

系统会先检测APK是否存在v2的Signing Block,如果没有,则执行v1的校验机制。由于Android N之前的系统,并不存在v2的校验机制,所以即使使用了v2的签名,在Android N之前的系统上,也不能做到防篡改的(毕竟之前的系统已经发布出去了,没法进行修改逻辑)。
鉴于以上验签流程,可能有人会想通过破坏v2签名的APK的Signing Block块,以达到让Android N及以上系统走v1校验的流程,从而使用漏洞来篡改APK。但可惜此方式是行不同的,对比v1和v2签名的APK,v2并不只是多了Signing Block块,而且对应的{name}.SF文件内容也不一样:

v2的{name}.SF文件中,标识了APK是使用Signature Scheme v2进行签名的,如果检测不到Signing Block,同样会报找不到签名信息错误的。

参考文献

1. Application Signing
2. APK Signature Scheme v2
3. APK Signature Scheme v1
4. JAR File Specification
5. Zip (file format)
6. .ZIP File Format Specification

7. 美团Android自动化之旅—生成渠道包
8. 新一代开源Android渠道包生成工具Walle
9. 独家分析:安卓“Janus”漏洞的产生原理及利用过程