安卓應用簽名機制分析

Android 應用簽名是應用打包過程的重要步驟之一,Google 要求全部的應用必須被簽名才能夠安裝到 Android 操做系統中。Android 的簽名機制也爲開發者識別和更新本身應用提供了方便。本文嘗試對 Android 應用簽名機制進行簡單分析,我的理解有限,不免有紕漏之處,請多多拍磚。html

背景知識

想要搞清楚安卓應用簽名究竟是什麼東西,首先須要瞭解一些背景知識。java

數字摘要 Digital Digest

數字摘要主要做用是將任意長度的消息使用單向 HASH 算法摘要成一串固定長度的密文。經常使用的 HASH 算法包括 SHA-1, SHA-256, MD5 等等,MD5 中的 MD 就是 Message Digest 的縮寫。數字摘要有時也被稱爲數字指紋,消息摘要等等,其實表達的都是一個意思。它有如下三個特色:android

  • 輸出長度固定 例如 MD5 算法摘要信息有 128 比特,而 SHA-1 有 160 比特
  • 不考慮碰撞的狀況下,只要輸入原始數據不一樣,摘要也不會相同。即便稍微改變輸出,摘要就會變得徹底不一樣。相同的輸入也會產生相同的輸出
  • 單向不可逆。從摘要沒法恢復原始消息

數字摘要圖示

Keystore 文件

Android 簽名須要用到一種後綴名爲 keystore 的文件。在打 Debug 包的時候,若是沒有在 build.gradle 文件中指定的話,Gradle 就會自動爲咱們生成一個 keystore文件,保存在系統用戶根目錄 .android 文件夾內,名稱爲 debug.keystore. 咱們以它爲例看看 keystore 文件包含了什麼內容。git

經過 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 相似一個鑰匙倉庫,裏面有證書的全部者和發佈者信息,包含了私鑰和公鑰信息,並設置了密碼進行保護。那什麼是公鑰和私鑰呢。shell

公共密鑰系統 RSA

公鑰和私鑰都是公共密鑰系統裏的概念。最初全部的加密算法都屬於對稱加密,也就是說加密和解密使用的相同的密碼,通訊雙方如何安全溝通和保存密碼,是這種加密方法的主要難題。難保沒有豬隊友。安全

而在公共密鑰系統中,加密和解密使用的是不一樣的密鑰,分別稱爲公鑰和私鑰,公鑰意思就是全部人均可以知道,私鑰則只有全部者才持有,單從公鑰沒法在現有的計算能力下推導出私鑰。這樣一來就不存在溝經過程中泄露密鑰的問題,只要私鑰不泄露,通訊就一直是安全的。bash

公共密鑰系統能夠說是如今最最最重要密鑰系統,是互聯網的基石之一。oracle

公共密鑰系統能夠用來加密,也能夠用來簽名。加密方案中,是不但願別人知道個人消息,因此公鑰用於加密信息,私鑰用於解密信息;而簽名方案中,是不但願別人冒充我發消息,只有我才能發佈這個簽名,因此須要用私鑰進行簽名,公鑰用於驗證簽名。app

什麼是應用簽名

回到正題,咱們先來看看 Android 應用簽名發生在構建的哪一步。

Android 打包流程示意

在編譯過程當中,編譯器首先會將源代碼和資源進行編譯,生成 DEX 文件和一些編譯後的資源文件,而後 APK Manager 會根據配置使用 keystore 文件進行簽名,簽名後纔會將全部資源壓縮到一個 ZIP 包裏,這個 ZIP 包其實咱們安裝的時候用的 APK 文件。能夠看到簽名是在構建基本完成的時候發生的。

簽名過程

那 APK Manager 是如何使用 keystore 進行簽名的呢?咱們一步一步看到底發生了什麼。

  1. 首先對編譯後生成的全部的文件進行掃描,每一個文件生成一個數字摘要,保存在 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 被篡改,安裝就會失敗。那系統是如何校驗的呢?

簽名校驗過程

  1. 系統取得已安裝 APK 中保存的公鑰,用它對新 APK 中的 CERT.RSA 保存的簽名信息進行解密;對 CERT.SF 文件計算摘要,與上一步解密出來的信息進行比對,若是不一致說明 CERT.SF 被篡改,拒絕安裝
  2. 對 MANIFEST.SF 文件計算摘要,與 CERT.SF 文件中的信息進行對比,若是不一致,則說明 MANIFEST.SF 文件被篡改,拒絕安裝
  3. 對 APK 內全部其餘文件計算數字摘要,若是文件沒有出如今 MANIFEST.SF 或者摘要與 MANIFEST.SF 中包含的不相同,說明加入了新的文件或者文件被篡改,拒絕安裝

整個校驗過程當中,環環相扣,從 CERT.RSA -> CERT.SF -> MANIFEST.SF -> All Other Files,只要有一環失敗,系統就會終止 APK 的安裝.

若是給新版應用分配了新的簽名文件,那就必須更改包名,這樣系統纔會認爲是不一樣的應用。不然安裝就會失敗,提示簽名不一致。 INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES

簽名的其餘用途

除了用於安裝時校驗應用,簽名還有一些別的用途。

  1. 應用模塊化。Android 容許相同簽名的兩個應用使用一樣的 Linux UserId,這樣一來兩個應用就能夠共享數據存儲了。同時若是應用申請的話,兩個應用還能夠在同一個進程中運行。經過這種方式能夠模塊化部署應用,每一個模塊也能獨立的進行升級。猜測不少主題資源包可能就是經過這種方式來安裝的。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:sharedUserId="com.meng.sharedappid" package="meng.mainuicomponents">
複製代碼
  1. 共享代碼數據。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)
複製代碼
  1. 用來聲明安全級別。好比隸屬同一個公司的多個應用實現共享登錄功能,能夠各自實現本身的 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 的前邊。它能夠對第一三四,以及第二塊除了簽名部分的其餘區域提供一致性保護。

二代簽名概覽
在計算簽名的時候,會將這些部分的數據切割成 1MB 大小的 CHUNK,分別計算簽名,而後彙總後再計算一個總簽名,這麼作的主要目的是爲了並行計算,加快速度。
二代簽名細節
爲了不攻擊者在 7.0 以上系統中繞過 v2 簽名機制(好比刪除 APK Signing Block?),v2 簽名要求若是 APK 同時提供了 V1 簽名的話,須要在 META-INF/*.SF 文件中增長一行 X-Android-APK-Signed 屬性,這樣一來,支持 V2 簽名的系統在回滾到 V1 簽名的時候就會校驗是否存在這個屬性,若是存在,就會拒絕安裝 APK,這一切都是創建在 *.SF 文件被 V1 簽名保護的基礎之上。

V3

Android 9.0 引入了第三代簽名機制,主要增長一個功能叫 APK key rotation. 意思是容許開發者在更新 APK 的時候更換籤名。簽名的主要機制跟 V2 實際上是同樣的,只是從新設計了 APK Signing Block 的存儲結構,以支持更換籤名。這裏就再也不細說,能夠參考官方的 文檔

下圖是安裝一個 APK 時,系統對三代簽名校驗的流程示意。

三代簽名校驗流程示意

參考

by @monkeyM

相關文章
相關標籤/搜索