Android 应用签名是应用打包过程的重要步骤之一,Google 要求所有的应用必须被签名才可以安装到 Android 操作系统中。Android 的签名机制也为开发者识别和更新自己应用提供了方便。本文尝试对 Android 应用签名机制进行简单分析,个人理解有限,难免有纰漏之处,请多多拍砖。
背景知识
想要搞清楚安卓应用签名到底是什么东西,首先需要了解一些背景知识。
数字摘要 Digital Digest
数字摘要主要作用是将任意长度的消息使用单向 HASH 算法摘要成一串固定长度的密文。常用的 HASH 算法包括 SHA-1, SHA-256, MD5 等等,MD5 中的 MD 就是 Message Digest 的缩写。数字摘要有时也被称为数字指纹,消息摘要等等,其实表达的都是一个意思。它有以下三个特点:
- 输出长度固定 例如 MD5 算法摘要信息有 128 比特,而 SHA-1 有 160 比特
- 不考虑碰撞的情况下,只要输入原始数据不同,摘要也不会相同。即使稍微改变输出,摘要就会变得完全不同。相同的输入也会产生相同的输出
- 单向不可逆。从摘要无法恢复原始消息
Keystore 文件
Android 签名需要用到一种后缀名为 keystore 的文件。在打 Debug 包的时候,如果没有在 build.gradle 文件中指定的话,Gradle 就会自动为我们生成一个 keystore文件,保存在系统用户根目录 .android 文件夹内,名称为 debug.keystore. 我们以它为例看看 keystore 文件包含了什么内容。
通过 keytool 工具来查看,默认的密码是 android. $ keytool -list -v -keystore debug.keystore -storepass android
结果如下:
密钥库类型: jks
密钥库提供方: SUN
您的密钥库包含 1 个条目
别名: androiddebugkey
创建日期: 2013-4-26
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
所有者: CN=Android Debug, O=Android, C=US
发布者: CN=Android Debug, O=Android, C=US
序列号: 517a38f2
有效期为 Fri Apr 26 15:46:58 CST 2013 至 Sun Apr 19 15:46:58 CST 2043
证书指纹:
MD5: 37:09:10:A9:F1:AE:9C:E4:C0:85:B9:35:D9:93:93:52
SHA1: F1:60:3F:72:2A:F2:3A:BC:BE:1C:DB:F6:F4:5B:FD:5E:34:8C:01:A9
SHA256: 86:C7:CB:D1:56:E7:D8:B8:AD:67:A7:A1:8F:C0:F6:E6:FC:E1:3D:45:AE:BC:F5:DF:B4:A9:F9:9A:F7:89:F7:0D
签名算法名称: SHA1withRSA
主体公共密钥算法: 1024 位 RSA 密钥
版本: 3
复制代码
可以看到,其实 keystore 类似一个钥匙仓库,里面有证书的所有者和发布者信息,包含了私钥和公钥信息,并设置了密码进行保护。那什么是公钥和私钥呢。
公共密钥系统 RSA
公钥和私钥都是公共密钥系统里的概念。最初所有的加密算法都属于对称加密,也就是说加密和解密使用的相同的密码,通信双方如何安全沟通和保存密码,是这种加密方法的主要难题。难保没有猪队友。
而在公共密钥系统中,加密和解密使用的是不同的密钥,分别称为公钥和私钥,公钥意思就是所有人都可以知道,私钥则只有所有者才持有,单从公钥无法在现有的计算能力下推导出私钥。这样一来就不存在沟通过程中泄露密钥的问题,只要私钥不泄露,通信就一直是安全的。
公共密钥系统可以说是现在最最最重要密钥系统,是互联网的基石之一。
公共密钥系统可以用来加密,也可以用来签名。加密方案中,是不希望别人知道我的消息,所以公钥用于加密信息,私钥用于解密信息;而签名方案中,是不希望别人冒充我发消息,只有我才能发布这个签名,所以需要用私钥进行签名,公钥用于验证签名。
什么是应用签名
回到正题,我们先来看看 Android 应用签名发生在构建的哪一步。
在编译过程中,编译器首先会将源代码和资源进行编译,生成 DEX 文件和一些编译后的资源文件,然后 APK Manager 会根据配置使用 keystore 文件进行签名,签名后才会将所有资源压缩到一个 ZIP 包里,这个 ZIP 包其实我们安装的时候用的 APK 文件。可以看到签名是在构建基本完成的时候发生的。
签名过程
那 APK Manager 是如何使用 keystore 进行签名的呢?我们一步一步看到底发生了什么。
- 首先对编译后生成的所有的文件进行扫描,每个文件生成一个数字摘要,保存在
META-INF/MANIFEST.MF
文件中
Name: res/drawable-xhdpi-v4/im_ic_keyboard_pressed.png
SHA-256-Digest: cqjOi3gUv9O0IBfgLOlIJUZTBwyCPcWbXIs/o6TMfTc
Name: classes.dex
SHA-256-Digest: FJCwLV1TyZuL1qxkDsJ6bXTmaSkK+JkKt5zmpDBc8Tg=
复制代码
我们看一下 im_ic_keyboard_pressed.png
这个文件的数字摘要到底是如何计算出来的。
- 第一步对文件进行 SHA-256 散列,得到一串 16 进制的散列值。
$ shasum -a 256 im_ic_keyboard_pressed.png
72a8ce8b7814bfd3b42017e02ce948254653070c823dc59b5c8b3fa3a4cc7d37 im_ic_keyboard_pressed.png
复制代码
- 第二步我们将其转换为二进制格式并进行 base64 编码
$ echo "72a8ce8b7814bfd3b42017e02ce948254653070c823dc59b5c8b3fa3a4cc7d37" | xxd -r -p | base64
cqjOi3gUv9O0IBfgLOlIJUZTBwyCPcWbXIs/o6TMfTc=
复制代码
可以看到跟我们在 MANIFEST.MF 中看到的值是能够对上的。 2. 对 MANIFEST.MF 文件生成数字摘要,并写入 CERT.SF,这里有一个细节,除了对整个文件做 HASH 外,还会将文件分成多段计算 HASH 同样保存在 CERT.SF 文件中【为什么,还没搞清楚】 3. 计算 CERT.SF 的数字摘要,并使用 RSA 私钥进行加密,生成签名 4. 将签名、公钥、哈希算法信息写入 CERT.RSA 文文件,并将这些文件添加到 APK 压缩包 META-INF 目录中
目前应用签名不需要申请可信的证书机构 (Certificate Authority) 签发的证书,开发者可以通过 keytool 来创建自签名的证书。
为什么要签名
应用签名不能保证 APK 不被篡改,只是为了能够校验出 APK 是否被篡改。在系统安装过程中,如果发现 APK 被篡改,安装就会失败。那系统是如何校验的呢?
签名校验过程
- 系统取得已安装 APK 中保存的公钥,用它对新 APK 中的 CERT.RSA 保存的签名信息进行解密;对 CERT.SF 文件计算摘要,与上一步解密出来的信息进行比对,如果不一致说明 CERT.SF 被篡改,拒绝安装
- 对 MANIFEST.SF 文件计算摘要,与 CERT.SF 文件中的信息进行对比,如果不一致,则说明 MANIFEST.SF 文件被篡改,拒绝安装
- 对 APK 内所有其他文件计算数字摘要,如果文件没有出现在 MANIFEST.SF 或者摘要与 MANIFEST.SF 中包含的不相同,说明加入了新的文件或者文件被篡改,拒绝安装
整个校验过程中,环环相扣,从 CERT.RSA -> CERT.SF -> MANIFEST.SF -> All Other Files,只要有一环失败,系统就会终止 APK 的安装.
如果给新版应用分配了新的签名文件,那就必须更改包名,这样系统才会认为是不同的应用。否则安装就会失败,提示签名不一致。
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES
签名的其他用途
除了用于安装时校验应用,签名还有一些别的用途。
- 应用模块化。Android 允许相同签名的两个应用使用同样的 Linux UserId,这样一来两个应用就可以共享数据存储了。同时如果应用申请的话,两个应用还可以在同一个进程中运行。通过这种方式可以模块化部署应用,每个模块也能独立的进行升级。猜想很多主题资源包可能就是通过这种方式来安装的。
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:sharedUserId="com.meng.sharedappid"
package="meng.mainuicomponents">
复制代码
- 共享代码数据。Android 提供了可以在使用同样签名的应用间共享代码的功能。另外一个前提也是两个应用设置了使用同样的 shareUserId。可以使用包名拿到兄弟 APP 的 Context,然后拿到 ClassLoader 加载兄弟 APP 的类,并可以实例化并通过反射来调用具体的方法。
val friendContext = this.createPackageContext("packageName", Context.CONTEXT_INCLUDE_CODE)
val friendClass = friendContext.classLoader.loadClass("packageName.className")
val noparams = arrayOf<Class<*>>() //say the function (functionName) required no inputs
friendClass.getMethod("functionName", *noparams).invoke(friendClass.newInstance(), null)
复制代码
- 用来声明安全级别。比如隶属同一个公司的多个应用实现共享登陆功能,可以各自实现自己的 ContentProvider,向外提供访问本应用数据的接口,但这个接口需要限制不能被其他第三方的应用读取,通过限制安全级别为签名级别,系统就能保证只有相同签名的应用才可以访问到这个 ContentProvider。以下是在 AndroidManifest.xml 文件中的使用示例。
<!-- 声明权限供兄弟 APP 使用 -->
<permission
android:name="com.xxx.permission.SHARED_ACCOUNT"
android:protectionLevel="signature" />
<!-- 申请获取兄弟 APP 的声明的权限 -->
<uses-permission android:name="com.xxx.broapp1.permission.SHARED_ACCOUNT" />
<uses-permission android:name="com.xxx.broapp2.permission.SHARED_ACCOUNT" />
<!-- 定义 ContentProvider 来供兄弟 APP 获取共享账户信息,指定度权限为签名声明的权限 -->
<provider
android:name="com.xxx.XXXXSharedAccountProvider"
android:authorities="com.xxx.shared_account"
android:exported="true"
android:readPermission="com.xxx.permission.SHARED_ACCOUNT" />
复制代码
签名机制的演进
目前签名模式有三代,基本原理都是一样的,只是在流程上有些不同。
V1
第一代是基于 JAR 文件签名,它主要的缺陷是只保护了一部分文件,而不是对整个 APK 文件做保护。这是因为所有文件都不可能包含了自身的签名,因为它不可能为自己签名后再把签名信息保存到自己内部,这是一个鸡生蛋蛋生鸡的问题,因为这个问题的存在,第一代签名机制会忽略所有以 .SF/.DSA/.RSA 的文件以及 META-INFO 目录下的所有文件。
所以攻击者就可以解压缩后在 APK/META-INF 目录新增一个含有恶意代码的文件,然后再压缩成 APK,同样是可以覆盖安装正版应用的,这样一来好好的应用就会被杀毒软件标记为恶意软件,从而达到攻击应用的目的。 除了容易被攻击外,应用安装起来也比较慢,因为安装器在校验时需要解压计算所有文件的数字摘要,确认没有被恶意修改。
美团打渠道包的方法本质上就利用了这个第一代签名的漏洞,在 META-INF 目录下新建了一个包含 vendor 名称的文件,从而不需要重新编译,只需要解压缩 APK,添加文件,重新压缩就完成了一个渠道包的生成,速度非常快。
V2
Android 7.0 引入了第二代签名,避免了第一代签名模式的问题,主要改进在于它在验证过程中,将整个 APK 文件当作一个整体,只校验 APK 文件的签名就可以了,从而一方面更严苛的避免了 APK 被篡改,另外一方面也不用加压缩后对所有文件进行校验,从而极大提升了安装速度。第二代签名向后兼容,使用新签名的 APK 可以安装到 <7.0 的系统上,但要求 APK 同时也进行 v1 的签名。
具体来说,第二代签名将整个 APK 文件进行签名,并将签名信息保存在了 APK 文件的的尾部 Central Directory 的前边。它可以对第一三四,以及第二块除了签名部分的其他区域提供一致性保护。
X-Android-APK-Signed
属性,这样一来,支持 V2 签名的系统在回滚到 V1 签名的时候就会校验是否存在这个属性,如果存在,就会拒绝安装 APK,这一切都是建立在 *.SF 文件被 V1 签名保护的基础之上。
V3
Android 9.0 引入了第三代签名机制,主要增加一个功能叫 APK key rotation. 意思是允许开发者在更新 APK 的时候更换签名。签名的主要机制跟 V2 其实是一样的,只是重新设计了 APK Signing Block 的存储结构,以支持更换签名。这里就不再细说,可以参考官方的 文档
下图是安装一个 APK 时,系统对三代签名校验的流程示意。
参考
- 应用签名 | Android Open Source Project
- APK签名流程详解 - 技海淘金 - 优快云博客
- Android signature verification vulnerability andexploitation
by @monkeyM