天天看點

Android 新一代多管道打包神器

作者 :李濤 ApkChannelPackage是一種快速多管道打包工具,同時支援基于V1簽名和V2簽名進行多管道打包。插件本身會自動檢測Apk使用的簽名方法,并選擇合适的多管道打包方式,對使用者來說完全透明。

衆所周知,因為國内Android應用分發市場的現狀,我們在釋出APP時,一般需要生成多個管道包,上傳到不同的應用市場。這些管道包需要包含不同的管道資訊,在APP和背景互動或者資料上報時,會帶上各自的管道資訊。這樣,我們就能統計到每個分發市場的下載下傳數、使用者數等關鍵資料。

既然我們需要進行多管道打包,那我們就看下最常見的多管道打包方案。

Gradle Plugin本身提供了多管道的打包政策:

首先,在AndroidManifest.xml中添加管道資訊占位符:

然後,通過Gradle Plugin提供的<code>productFlavors</code>标簽,添加管道資訊:

這樣,Gradle編譯生成多管道包時,會用不同的管道資訊替換AndroidManifest.xml中的占位符。我們在代碼中,也就可以直接讀取AndroidManifest.xml中的管道資訊了。

但是,這種方式存在一些缺點:

1)每生成一個管道包,都要重新執行一遍建構流程,效率太低,隻适用于管道較少的場景。

2)Gradle會為每個管道包生成一個不同的BuildConfig.java類,記錄管道資訊,導緻每個管道包的DEX的CRC值都不同。一般情況下,這是沒有影響的。但是如果你使用了微信的Tinker熱更新檔方案,那麼就需要為不同的管道包打不同的更新檔,這完全是不可以接受的。(因為Tinker是通過對比基礎包APK和新包APK生成差分更新檔,然後再把更新檔和基礎包APK一起合成新包APK。這就要求用于生成差分更新檔的基礎包DEX和用于合成新包的基礎包DEX是完全一緻的,即:每一個基礎管道包的DEX檔案是完全一緻的,不然就會合成失敗)

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的簽名和校驗機制。

在進一步學習V1和V2簽名之前,我們有必要學習一下簽名相關的基礎知識。

資料摘要算法是一種能産生特定輸出格式的算法,其原理是根據一定的運算規則對原始資料進行某種形式的資訊提取,被提取出的資訊就是原始資料的消息摘要,也稱為資料指紋。

一般情況下,資料摘要算法具有以下特點:

無論輸入資料有多大(長),計算出來的資料摘要的長度總是固定的。例如:MD5算法計算出的資料摘要有128Bit。

一般情況下(不考慮碰撞的情況下),隻要原始資料不同,那麼其對應的資料摘要就不會相同。同時,隻要原始資料有任何改動,那麼其資料摘要也會完全不同。即:相同的原始資料必有相同的資料摘要,不同的原始資料,其資料摘要也必然不同。

不可逆性,即隻能正向提取原始資料的資料摘要,而無法從資料摘要中恢複出原始資料。

著名的摘要算法有RSA公司的MD5算法和SHA系列算法。

數字簽名和數字證書是成對出現的,兩者不可分離(數字簽名主要用來校驗資料的完整性,數字證書主要用來確定公鑰的安全發放)。

要明白數字簽名的概念,必須要了解資料的加密、傳輸和校驗流程。一般情況下,要實作資料的可靠通信,需要解決以下兩個問題:

1.确定資料的來源是其真正的發送者。

2.確定資料在傳輸過程中,沒有被篡改,或者若被篡改了,可以及時發現。

而數字簽名,就是為了解決這兩個問題而誕生的。

首先,資料的發送者需要先申請一對公私鑰對,并将公鑰交給資料接收者。

然後,若資料發送者需要發送資料給接收者,則首先要根據原始資料,生成一份數字簽名,然後把原始資料和數字簽名一起發送給接收者。

數字簽名由以下兩步計算得來:

1.計算發送資料的資料摘要

2.用私鑰對提取的資料摘要進行加密

這樣,資料接收者拿到的消息就包含了兩塊内容:

1.原始資料内容

2.附加的數字簽名

接下來,接收者就會通過以下幾步,校驗資料的真實性:

用相同的摘要算法計算出原始資料的資料摘要。

用預先得到的公鑰解密數字簽名。

對比簽名得到的資料是否一緻,如果一緻,則說明資料沒有被篡改,否則資料就是髒資料了。

因為私鑰隻有發送者才有,是以其他人無法僞造數字簽名。這樣通過數字簽名就確定了資料的可靠傳輸。

綜上所述,數字簽名就是隻有發送者才能産生的别人無法僞造的一段數字串,這段數字串同時也是對發送者發送資料真實性的一個有效證明。

想法雖好,但是上面的整個流程,有一個前提,就是資料接收者能夠正确拿到發送者的公鑰。如果接收者拿到的公鑰被篡改了,那麼壞人就會被當成好人,而真正的資料發送者發送的資料則會被視作髒資料。那怎麼才能保證公鑰的安全性那?這就要靠數字證書來解決了。

數字證書是由有公信力的證書中心(CA)頒發給申請者的證書,主要包含了:證書的釋出機構、證書的有效期、申請者的公鑰、申請者資訊、數字簽名使用的算法,以及證書内容的數字簽名。

可見,數字證書也用到了數字簽名技術。隻不過簽名的内容是資料發送方的公鑰,以及一些其它證書資訊。

這樣資料發送者發送的消息就包含了三部分内容:

原始資料内容

附加的數字簽名

申請的數字證書。

接收者拿到資料後,首先會根據CA的公鑰,解碼出發送者的公鑰。然後就與上面的校驗流程完全相同了。

是以,數字證書主要解決了公鑰的安全發放問題。

是以,包含數字證書的整個簽名和校驗流程如下圖所示:

Android 新一代多管道打包神器

預設情況下,APK使用的就是V1簽名。解壓APK後,在META-INF目錄下,可以看到三個檔案:MANIFEST.MF、CERT.SF、CERT.RSA。它們都是V1簽名的産物。

其中,<code>MANIFEST.MF</code>檔案内容如下所示:

Android 新一代多管道打包神器

它記錄了APK中所有原始檔案的資料摘要的Base64編碼,而資料摘要算法就是<code>SHA1</code>。

<code>CERT.SF</code>檔案内容如下所示:

Android 新一代多管道打包神器

<code>SHA1-Digest-Manifest-Main-Attributes</code>主屬性記錄了<code>MANIFEST.MF</code>檔案所有主屬性的資料摘要的Base64編碼。<code>SHA1-Digest-Manifest</code>則記錄了整個<code>MANIFEST.MF</code>檔案的資料摘要的Base64編碼。

其餘的普通屬性則和<code>MANIFEST.MF</code>中的屬性一一對應,分别記錄了對應資料塊的資料摘要的Base64編碼。例如:<code>CERT.SF</code>檔案中skin_drawable_btm_line.xml對應的SHA1-Digest,就是下面内容的資料摘要的Base64編碼。

這裡要注意的是:最後一行的換行符是必不可少,需要參與計算的。

<code>CERT.RSA</code>檔案包含了<code>對CERT.SF</code>檔案的數字簽名和開發者的數字證書。RSA就是計算數字簽名使用的非對稱加密算法。

V1簽名的詳細流程可參考SignApk.java,整個簽名流程如下圖所示:

Android 新一代多管道打包神器

整個簽名機制的最終産物就是MANIFEST.MF、CERT.SF、CERT.RSA三個檔案。

在安裝APK時,Android系統會校驗簽名,檢查APK是否被篡改。代碼流程是:<code>PackageManagerService.java</code> -&gt; <code>PackageParser.java</code>,<code>PackageParser</code>類負責V1簽名的具體校驗。整個校驗流程如下圖所示:

Android 新一代多管道打包神器

若中間任何一步校驗失敗,APK就不能安裝。

OK,了解了V1的簽名和校驗流程。我們來看下,V1簽名是怎麼保證APK檔案不被篡改的?

首先,如果破壞者修改了APK中的任何檔案,那麼被篡改檔案的資料摘要的Base64編碼就和<code>MANIFEST.MF</code>檔案的記錄值不一緻,導緻校驗失敗。

其次,如果破壞者同時修改了對應檔案在<code>MANIFEST.MF</code>檔案中的Base64值,那麼<code>MANIFEST.MF</code>中對應資料塊的Base64值就和CERT.SF檔案中的記錄值不一緻,導緻校驗失敗。

最後,如果破壞者更進一步,同時修改了對應檔案在<code>CERT.SF</code>檔案中的Base64值,那麼<code>CERT.SF</code>的數字簽名就和<code>CERT.RSA</code>記錄的簽名不一緻,也會校驗失敗。

那有沒有可能繼續僞造<code>CERT.SF</code>的數字簽名那?理論上不可能,因為破壞者沒有開發者的私鑰。那破壞者是不是可以用自己的私鑰和數字證書重新簽名那,這倒是完全可以!

綜上所述,任何對APK檔案的修改,在安裝時都會失敗,除非對APK重新簽名。但是相同包名,不同簽名的APK也是不能同時安裝的。

由上述V1簽名和校驗機制可知,修改APK中的任何檔案都會導緻安裝失敗!那怎麼添加管道資訊那?隻能從APK的結構入手了。

APK檔案本質上是一個ZIP壓縮包,而ZIP格式是固定的,主要由三部分構成,如下圖所示:

Android 新一代多管道打包神器

第一部分是内容塊,所有的壓縮檔案都在這部分。每個壓縮檔案都有一個<code>local file header</code>,主要記錄了檔案名、壓縮算法、壓縮前後的檔案大小、修改時間、CRC32值等。

第二部分稱為中央目錄,包含了多個<code>central directory file header</code>(和第一部分的<code>local file header</code>一一對應),每個中央目錄檔案頭主要記錄了壓縮算法、注釋資訊、對應<code>local file header</code>的偏移量等,友善快速定位資料。

最後一部分是EOCD,主要記錄了中央目錄大小、偏移量和ZIP注釋資訊等,其詳細結構如下圖所示:

Android 新一代多管道打包神器

根據之前的V1簽名和校驗機制可知,V1簽名隻會檢驗第一部分的所有壓縮檔案,而不理會後兩部分内容。是以,隻要把管道資訊寫入到後兩塊内容就可以通過V1校驗,而EOCD的注釋字段無疑是最好的選擇。

既然找到了突破口,那麼基于V1簽名的多管道打包方案就應運而生:在APK檔案的注釋字段,添加管道資訊。

整個方案包括以下幾步:

複制APK

找到EOCD資料塊

修改注釋長度

添加管道資訊

添加管道資訊長度

添加魔數

添加管道資訊後的EOCD資料塊如下所示:

Android 新一代多管道打包神器

這裡添加魔數的好處是友善從後向前讀取資料,定位管道資訊。

是以,讀取管道資訊包括以下幾步:

定位到魔數

向前讀兩個位元組,确定管道資訊的長度LEN

繼續向前讀LEN位元組,就是管道資訊了。

通過16進制編輯器,可以檢視到添加管道資訊後的APK(小端模式),如下所示:

Android 新一代多管道打包神器

<code>6C 74 6C 6F 76 75 7A 68</code>是魔數,<code>04 00</code>表示管道資訊長度為4,<code>6C 65 6F 6E</code>就是管道資訊leon了。<code>0E 00</code>就是APK注釋長度了,正好是15。

雖說整個方案很清晰,但是在<code>找到EOCD資料塊</code>這步遇到一個問題。如果APK本身沒有注釋,那最後22位元組就是EOCD。但是若APK本身已經包含了注釋字段,那怎麼确定EOCD的起始位置那?這裡借鑒了系統V2簽名确定EOCD位置的方案。整個計算流程如下圖所示:

Android 新一代多管道打包神器

整個方案介紹完了,該方案的最大優點就是:不需要解壓縮APK,不需要重新簽名,隻需要複制APK,在注釋字段添加管道資訊。每個管道包僅需幾秒的耗時,非常适合管道較多的APK。

但是好景不長,Android7.0之後新增了V2簽名,該簽名會校驗整個APK的資料摘要,導緻上述管道打包方案失效。是以如果想繼續使用上述方案,需要關閉Gradle Plugin中的V2簽名選項,禁用V2簽名。

從前面的V1簽名介紹,可以知道V1存在兩個弊端:

1)<code>MANIFEST.MF</code>中的資料摘要是基于原始未壓縮檔案計算的。是以在校驗時,需要先解壓出原始檔案,才能進行校驗。而解壓操作無疑是耗時的。

2) V1簽名僅僅校驗APK第一部分中的檔案,缺少對APK的完整性校驗。是以,在簽名後,我們還可以修改APK檔案,例如:通過zipalign進行位元組對齊後,仍然可以正常安裝。

正是基于這兩點,Google提出了V2簽名,解決了上述兩個問題:

V2簽名是對APK本身進行資料摘要計算,不存在解壓APK的操作,減少了校驗時間。

V2簽名是針對整個APK進行校驗(不包含簽名塊本身),是以對APK的任何修改(包括添加注釋、zipalign位元組對齊)都無法通過V2簽名的校驗。

關于第一點的耗時問題,這裡有一份實驗室資料(Nexus 6P、Android 7.1.1)可供參考。

APK安裝耗時對比

取5次平均耗時(秒)

V1簽名APK

11.64

V2簽名APK

4.42

可見,V2簽名對APK的安裝速度還是提升不少的。

不同于V1,V2簽名會生成一個簽名塊,插入到APK中。是以,V2簽名後的APK結構如下圖所示:

Android 新一代多管道打包神器

APK簽名塊位于中央目錄之前,檔案資料之後。V2簽名同時修改了EOCD中的中央目錄的偏移量,使簽名後的APK還符合ZIP結構。

APK簽名塊的具體結構如下圖所示:

Android 新一代多管道打包神器

首先是8位元組的簽名塊大小,此大小不包含該字段本身的8位元組;其次就是ID-Value序列,就是一個4位元組的ID和對應的資料;然後又是一個8位元組的簽名塊大小,與開始的8位元組是相等的;最後是16位元組的簽名塊魔數。

其中,ID為0x7109871a對應的Value就是V2簽名塊資料。

V2簽名塊的生成可參考ApkSignerV2,整體結構和流程如下圖所示:

Android 新一代多管道打包神器

首先,根據多個簽名算法,計算出整個APK的資料摘要,組成左上角的APK資料摘要集;

接着,把最左側一列的資料摘要、數字證書和額外屬性組裝起來,形成類似于V1簽名的“MF”檔案(第二列第一行);

其次,再用相同的私鑰,不同的簽名算法,計算出“MF”檔案的數字簽名,形成類似于V1簽名的“SF”檔案(第二列第二行);

然後,把第二列的<code>類似MF檔案</code>、<code>類似SF檔案</code>和<code>開發者公鑰</code>一起組裝成通過單個keystore簽名後的v2簽名塊(第三列第一行)。

最後,把多個keystore簽名後的簽名塊組裝起來,就是完整的V2簽名塊了(Android中允許使用多個keystore對apk進行簽名)。

上述流程比較繁瑣。簡而言之,單個keystore簽名塊主要由三部分組成,分别是上圖中第二列的三個資料塊:<code>類似MF檔案</code>、<code>類似SF檔案</code>和<code>開發者公鑰</code>,其結構如下圖所示:

Android 新一代多管道打包神器

除此之外,Google也優化了計算資料摘要的算法,使得可以并行計算,如下圖所示:

Android 新一代多管道打包神器

資料摘要的計算包括以下幾步:

首先,将上述APK中檔案内容塊、中央目錄、EOCD按照1MB大小分割成一些小塊。

然後,計算每個小塊的資料摘要,基礎資料是0xa5 + 塊位元組長度 + 塊内容。

最後,計算整體的資料摘要,基礎資料是0x5a + 資料塊的數量 + 每個資料塊的摘要内容。

這樣,每個資料塊的資料摘要就可以并行計算,加快了V2簽名和校驗的速度。

Android Gradle Plugin2.2之上預設會同時開啟V1和V2簽名,同時包含V1和V2簽名的CERT.SF檔案會有一個特殊的主屬性,如下圖所示:

Android 新一代多管道打包神器

該屬性會強制APK走V2校驗流程(7.0之上),以充分利用V2簽名的優勢(速度快和更完善的校驗機制)。

是以,同時包含V1和V2簽名的APK的校驗流程如下所示:

Android 新一代多管道打包神器

簡而言之:優先校驗V2,沒有或者不認識V2,則校驗V1。

這裡引申出另外一個問題:APK簽名時,隻有V2簽名,沒有V1簽名行不行?

經過嘗試,這種情況是可以編譯通過的,并且在Android 7.0之上也可以正确安裝和運作。但是7.0之下,因為不認識V2,又沒有V1簽名,是以會報沒有簽名的錯誤。

OK,明确了Android平台對V1和V2簽名的校驗選擇之後,我們來看下V2簽名的具體校驗流程(<code>PackageManagerService.java</code> -&gt; <code>PackageParser.java</code>-&gt; <code>ApkSignatureSchemeV2Verifier.java</code>),如下圖所示:

Android 新一代多管道打包神器

其中,最強簽名算法是根據該算法使用的資料摘要算法來對比産生的,比如:SHA512 &gt; SHA256。

校驗成功的定義是至少找到一個keystore對應的簽名塊,并且所有簽名塊都按照上述流程校驗成功。

下面我們來看下V2簽名是怎麼保證APK不被篡改的?

首先,如果破壞者修改了APK檔案的任何部分(簽名塊本身除外),那麼APK的資料摘要就和“MF”資料塊中記錄的資料摘要不一緻,導緻校驗失敗。

其次,如果破壞者同時修改了“MF”資料塊中的資料摘要,那麼“MF”資料塊的數字簽名就和“SF”資料塊中記錄的數字簽名不一緻,導緻校驗失敗。

然後,如果破壞者使用自己的私鑰去加密生成“SF”資料塊,那麼使用開發者的公鑰去解密“SF”資料塊中的數字簽名就會失敗;

最後,更進一步,若破壞者甚至替換了開發者公鑰,那麼使用數字證書中的公鑰校驗簽名塊中的公鑰就會失敗,這也正是數字證書的作用。

綜上所述,任何對APK的修改,在安裝時都會失敗,除非對APK重新簽名。但是相同包名,不同簽名的APK也是不能同時安裝的。

到這裡,V2簽名已經介紹完了。但是在最後一步“資料摘要校驗”這裡,隐藏了一個點,不知道有沒有人發現?

因為,我們V2簽名塊中的資料摘要是針對APK的檔案内容塊、中央目錄和EOCD三塊内容計算的。但是在寫入簽名塊後,修改了EOCD中的中央目錄偏移量,那麼在進行V2簽名校驗時,理論上在“資料摘要校驗”這步應該會校驗失敗啊!但是為什麼V2簽名可以校驗通過那?

這個問題很重要,因為我們下面要介紹的基于V2簽名的多管道打包方案也會修改EOCD的中央目錄偏移量。

其實也很簡單,原來Android系統在校驗APK的資料摘要時,首先會把EOCD的中央目錄偏移量替換成簽名塊的偏移量,然後再計算資料摘要。而簽名塊的偏移量不就是v2簽名之前的中央目錄偏移量嘛!!!,是以,這樣計算出的資料摘要就和“MF”資料塊中的資料摘要完全一緻了。具體代碼邏輯,可參考ApkSignatureSchemeV2Verifier.java的416 ~ 420行。

在上節V2簽名的校驗流程中,有一個很重要的細節:Android系統隻會關注ID為0x7109871a的V2簽名塊,并且忽略其他的ID-Value,同時V2簽名隻會保護APK本身,不包含簽名塊。

是以,基于V2簽名的多管道打包方案就應運而生:在APK簽名塊中添加一個ID-Value,存儲管道資訊。

找到APK的EOCD塊

找到APK簽名塊

擷取已有的ID-Value Pair

添加包含管道資訊的ID-Value

基于所有的ID-Value生成新的簽名塊

修改EOCD的中央目錄的偏移量(上面已介紹過:修改EOCD的中央目錄偏移量,不會導緻資料摘要校驗失敗)

用新的簽名塊替代舊的簽名塊,生成帶有管道資訊的APK

實際上,除了管道資訊,我們可以在APK簽名塊中添加任何輔助資訊。

Android 新一代多管道打包神器

<code>6C 65 6F 6E</code>就是我們的管道資訊<code>leon</code>。向前4個位元組:<code>FF 55 11 88</code>就是我們添加的ID,再向前8個位元組:<code>08 00 00 00 00 00 00 00</code>就是我們的ID-Value的長度,正好是8。

整個方案介紹完了,該方案的最大優點就是:支援7.0之上新增的V2簽名,同時兼有V1方案的所有優點。

那麼如何保證通過這些方案生成的管道包,能夠在所有Android平台上正确安裝那?

原來Google提供了一個同時支援V1和V2簽名和校驗的工具:apksig。它包括一個apksigner指令行和一個apksig類庫。其中前者就是Android SDK build-tools下面的指令行工具。而我們正是借助後面的apksig來進行管道包強校驗,它可以保證管道包在apk Minsdk ~ 最高版本之間都校驗通過。詳細代碼可參考VerifyApk.java

目前市面上的多管道打包工具主要有packer-ng-plugin和美團的Walle。下表是我們的ApkChannelPackage和它們之間的簡單對比。

多管道打包工具對比

ApkChannelPackage

packer-ng-plugin

Walle

V1簽名方案

支援

不支援

V2簽名方案

帶有注釋的APK

根據已有APK生成管道包

指令行工具

強校驗

這裡我之是以同時支援V1和V2簽名方案,主要是擔心後續Android平台加強簽名校驗機制,導緻V2多管道打包方案行不通,可以無痛切換到V1簽名方案。後續我也會盡快支援指令行工具。

具體的接入流程可參考APKChannelPackage插件接入文檔。