Android中籤名原理和安全性分析之META-INF文件講解

之前已經來了好幾篇和RSA加密相關的文章了,這次還是趁熱打鐵,看一下RSA在APK簽名中的應用,最後我們分析一下爲什麼這種方式能夠具有安全性。RSA對apk簽名的體現就在apk文件中的META-INF文件夾中,我們先來拿一個例子分析一下。

以最新的QQ6.6.2的apk爲例,現在的解壓工具默認就可以解壓apk了,所以也不要先改成zip然後解壓了,解壓後apk裏面的文件大概就是這樣的:

裏面有一個文件夾名字叫做META-INF,這個文件夾裏的東西就是今天要了解的全部內容了。先看一下里面有什麼:

可以看到,裏面有三個文件,當然了,如果你用到了其他的一些技術,可能裏面不止這三個了。需要說明的是,MF的名字是確定的,就是MANIFEST.MF,其他的兩個文件默認的文件名是CERT,但是呢,這個名字可以隨意修改,只要SF和RSA的文件的名字一樣就可以了,所以這裏QQ的名字是ANDROIDR。我們來看一下這三個文件分別是什麼作用。先來看MANIFEST.MF。

打開這個文件(記事本就可以)可以看到內容大約是這個樣子:

看到第一行是指明是manifest文件,第二行是由誰創建的,然後下面的是關鍵內容。可以看到,這個文件中除了第一二行外,其餘的部分都長的差不多都是這樣的格式:

1
2
Name: xxxx
SHA1-Digest: yyyy

一個名字,一個特殊的字符串。Name就是文件名,SHA1-Digest就是這個文件的SHA1摘要值的Base64表示值。裏面所有的這種格式內容包含了apk包中除了META-INF文件夾外的所有的文件的文件名和對應的SHA1摘要Base64值。

我們可以拿其中一個進行驗證,就拿第一個來說。第一個文件的名字是R/o/lbs.xml,我們找到這個文件:

在百度上搜一個工具“在線文件sha1”,如果你懶,可以直接用我搜到的結果:

http://www.atool.org/file_hash.php

把剛纔那個文件拖進去查看它的SHA1值:

可以看到文件的SHA1值爲81701b62b32a81f704b38425bbc58f5ba0927561,繼續看它的Base64的值,在百度上搜索hex base64,如果你懶,直接用我搜到的結果:

http://tomeko.net/online_tools/hex_to_base64.php?lang=en

將剛纔的SHA1值填進去計算一下:

可以看到結果是gXAbYrMqgfcEs4Qlu8WPW6CSdWE=,可以看到和我們看到的MANIFEST.MF裏的那個值是一樣的。其他的內容讀者可以自己驗證。

如果計算有誤,那可能是你的解壓方式有問題,記得要直接解壓,不要用apktool等工具解壓。
再說一遍結論:MANIFEST.MF文件保存了我們apk裏的除METE-INF外的文件的摘要信息。

再看第二個文件ANDROIDR.SF(默認叫CERT.SF)。

打開文件查看內容:

是不是特別眼熟,格式幾乎和MANIFEST.MF一樣。前面四行目前我們需要注意的是SHA1-Digest-Manifest,這個的值是我們剛纔的MANIFEST.MF的文件的SHA1-Base64的值,讀者自行校驗。其餘的部分和MANIFEST.MF一樣,都是Name和SHA1-Digest,而且Name也是一樣的,就是SHA1-Digest的值不一樣,這個是什麼含義呢?實際這個的值是對應的MANIFEST.MF裏的Name和SHA1-Digest的SHA1-Base64值。沒有明白?例如上面的R/o/lbs.xml的值爲9C9DPqgNa7HLHjnqFy6QIC+iHOI=,這個值是MANIFEST.MF裏的:

1
2
Name: R/o/lbs.xml
SHA1-Digest: gXAbYrMqgfcEs4Qlu8WPW6CSdWE=

再加上兩個CRLF組成的,就是說多兩個回車換行。這裏需要注意的是我們在驗證的時候把上面的兩句話保存在文本文件中,然後千萬不要在記事本里添加回車換行,可以試試AndroidStudio裏添加兩個回車換行,這樣按上面的步驟計算SHA1和Base64後就能看到結果了。

結論就是:SF文件裏保存的是MANIFEST.MF文件的SHA1-Base64的值和除META-INF外所有文件的SHA1摘要Base64值的SHA1摘要Base64值。

接下來看ANDROIDR.RSA(默認叫CERT.RSA)。

這下我們就不能直接看文件內容了,因爲這個RSA文件裏包含了公鑰和私鑰簽名後的一些信息。我們用下面的命令來查看一下RSA文件的內容:

openssl pkcs7 -inform DER -in ANDROIDR.RSA -noout -print_certs -text

他的基本格式是這樣的:

可以看到有有效期等等信息,RSA公鑰用的是1024位的,並且簽名加密算法是sha1WithRSAEncryption。我猜測最下面的信息應該就是SF文件的SHA1摘要信息的私鑰簽名值,爲什麼是猜測?因爲這個我還沒驗證出來。不過雖然沒有驗證,但是猜測應該是基本確定的,因爲看內容知道它是128byte的,我們知道RSA加密的結果是和公私鑰長度一致的,並且加密的內容不能超過公私鑰的長度,而SHA1是的結果是20byte的,沒有超過這個長度。從上面的命令可以看出這個RSA文件是pkcs7格式的(Android裏常見的好像就pkcs7和pkcs12),這個格式我也不是特別瞭解,所以不敢亂說了。

先留下這個結果,可能以後有時間了再來驗證一下:

94a9b80e80691645dd42d6611775a855f71bcd4d77cb60a8e29404035a5e00b21bcc5d4a562482126bd91b6b0e50709377ceb9ef8c2efd12cc8b16afd9a159f350bb270b14204ff065d843832720702e28b41491fbc3a205f5f2f42526d67f17614d8a974de6487b2c866efede3b4e49a0f916baa3c1336fd2ee1b1629652049

如果我們想直接拿到公鑰,還可以這樣來幹:

> openssl pkcs7 -inform DER -print_certs -out cert.pem -in ANDROIDR.RSA

> cat cert.pem

這樣會直接生成pem文件,直接查看pem文件就是公鑰的值了,這裏公鑰的值爲:

文本記錄一下:

            -----BEGIN CERTIFICATE-----
    MIICUzCCAbygAwIBAgIES7sDYTANBgkqhkiG9w0BAQUFADBtMQ4wDAYDVQQGEwVD
    aGluYTEPMA0GA1UECAwG5YyX5LqsMQ8wDQYDVQQHDAbljJfkuqwxDzANBgNVBAoM
    BuiFvuiurzEbMBkGA1UECwwS5peg57q/5Lia5Yqh57O757ufMQswCQYDVQQDEwJR
    UTAgFw0xMDA0MDYwOTQ4MTdaGA8yMjg0MDEyMDA5NDgxN1owbTEOMAwGA1UEBhMF
    Q2hpbmExDzANBgNVBAgMBuWMl+S6rDEPMA0GA1UEBwwG5YyX5LqsMQ8wDQYDVQQK
    DAbohb7orq8xGzAZBgNVBAsMEuaXoOe6v+S4muWKoeezu+e7nzELMAkGA1UEAxMC
    UVEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKFel1Yhb2lMWRXgtSkJUlQ2
    fE5k+u/weuE0iNlGYVpY3cMaQV9xfQGe3G0wuWA9Pip7PeCrfgz1Lf7jk3O8Ry+p
    lwJ9eY1Z+B1SWmns8Vbohf0eJ5CSQ4ayIwzJDjt63JVgPdz0xAvccvItsPIWqZw3
    HTv4nLpleMYGmeig1TaVAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAlKm4DoBpFkXd
    QtZhF3WoVfcbzU13y2Co4pQEA1peALIbzF1KViSCEmvZG2sOUHCTd86574wu/RLM
    ixav2aFZ81C7JwsUIE/wZdhDgycgcC4otBSR+8OiBfXy9CUm1n8XYU2Kl03mSHss
    hm7+3jtOSaD5FrqjwTNv0u4bFillIEk=
            -----END CERTIFICATE-----

如果我的猜測沒有錯誤的話,用這個公鑰解開那個被私鑰簽名的值獲得的結果應該就是SF文件的SHA1值了。

最後總結一下apk簽名的整個流程:

一、對Apk中的每個文件做一次算法(數據SHA1摘要+Base64編碼),保存到MANIFEST.MF文件中

二、對MANIFEST.MF整個文件做一次算法(數據SHA1摘要+Base64編碼),存放到CERT.SF文件的頭屬性中,在對MANIFEST.MF文件中各個屬性塊做一次算法(數據SHA1摘要+Base64編碼),存到到一個屬性塊中。

三、對CERT.SF文件做簽名,內容存檔到CERT.RSA中

整體基本就是這個樣子,現在補充一些上面沒有說到的,爲什麼RSA和SF文件名字可以隨意指定,只要一致就可以,稍微看一下源碼就知道了:

key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")

上面的是我從代碼裏拷出來的一個if語句的條件,可以看到,在apk安裝驗證的時候找RSA文件並不是通過名字找的,而是通過後綴找的。同樣我們可以看到,apk的簽名算法不只是支持RSA,實際DSA和EC也是支持的,並且它們混合起來用也是行的(外面有個循環語句,我沒有貼出來)。
接着來看一下和SF相關的代碼:

String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";

可以看到,找SF文件是通過RSA文件的名字來找的,所以需要SF和RSA的文件名字一致。對於SF文件的內容有兩點需要補充,

其一是有時候SF文件中可能會有SHA1-Digest-Manifest-Main-Attributes這樣的值,它的含義是META-INF目錄下MANIFEST.MF文件內,頭屬性塊的hash值。

其二是這個文件可能內容和我們說的不一樣,例如:

可以看到MF文件和SF文件的下部分內容居然一樣!!!這個結果讓當時的我疑惑不解,甚至想要放棄本篇內容的研究,因爲我在網上搜了很多應用,結果都不是我看到的這個樣子,難道我的是特殊的?疑惑了好幾天,不過經過了一段時間的折磨,終於還是稍微明白了些。我查看了我們公司的歷史apk包,二分查找對比,最後發現在SF文件中有寫Android Gradle 2.2.2的包就會導致MF和SF下部分一樣(看來我們公司還是挺超前的)。最後在網上終於找到了一個相關內容,那就是Android Gradle Plugin在2.2.0時的打包機制變化了!具體的變化我用兩句話分別概括改變和原因:

新簽名驗證的是整個apk的二進制文件,不同於之前的每個文件來驗證完整性,一個apk可以同時支持新舊兩種方法的簽名,所以它保持了向前的兼容性。
原因是1安全,驗證整體二進制,不再可以修改壓縮包裏的文件;2速度,安裝無需解壓縮,縮短安裝時間。

可能就是這個新的簽名機制導致了MF和SF文件的內容相同,所以我們上面的結論只是用於Android Gradle Plugin在2.2.0之前。

對於RSA文件,需要說明的是本例中的公私鑰的長度是1024位,實際你可能看到的是2048位的了,並且簽名加密算法本例是sha1WithRSAEncryption,實際可能你看到的是sha256WithRSAEncryption。

補充就這麼多,apk安裝驗證的過程實際就是和這個過程正好相反嘛,簡單的說一下就是:

找到RSA文件用公鑰解密私鑰的簽名後的信息,如果能解密,這一步通過;

解密後的值和SF的SHA1值進行比對,如果一致,這一步通過;

查看SF文件中的MF文件的SHA1-Base64值,如果和MF的計算值一樣,這一步通過;

計算MF中的Name/SHA1-Digest屬性塊的SHA1-Base64值和SF裏的對比,如果一致,這一步通過;

計算除META-INF外的每個文件的SHA1-Base64值和MF裏的對比,如果一致,這一步通過。

以上基本就是apk安裝時的驗證過程,當然如果是覆蓋安裝就是多一個操作,那就是身份的驗證,如果想要覆蓋,那麼必須包名一致,簽名一致,這裏的簽名一致實際說的就是公鑰需要一致。

接下來纔是本文的最終目的,分析一下這樣做的必要性,我們通過反證法來說明。

假如我們是一個非法者,想要篡改apk內容,我們怎麼做呢?如果我們只把原文件改動了(比如加入了自己的病毒代碼),那麼重新打包後系統就會認爲文件的SHA1-Base64值和MF的不一致導致安裝失敗,既然這樣,那我們就改一下MF讓他們一致唄?如果只是這樣那麼系統就會發現MF文件的內容的SHA1-Base64與SF不一致,還是會安裝失敗,既然這樣,那我們就改一下SF和MF一致唄?如果這麼做了,系統就會發現RSA解密後的值和SF的SHA1不一致,安裝失敗。那麼我們讓加密後的值和SF的SHA1一致就好了唄,但是呢,這個用來簽名加密的是私鑰,公鑰隨便玩,但是私鑰我們卻沒有,所以沒法做到一致。所以說上面的過程環環相扣,最後指向了RSA非對稱加密的保證。有人說,那我可以直接重簽名啊,這樣所有的信息就一致了啊,是的,沒錯,重簽名後就可以安裝了,這就是說簽名機制只是保證了apk的完整性,具體是不是自己的apk包,系統並不知道,那我們上面說的安全性是怎麼保證的呢?那就是我們可以隨便籤名,隨便安裝,但是在覆蓋安裝的時候由於我們的簽名和作者的簽名不一致,導致我們重簽名後的apk無法覆蓋掉原作者的。這就保證了已經安裝的apk的接下來的安全鏈的正確性。當然了,如果你的手機上來就直接安裝了一個第三方的非法簽名的apk,那麼原作者的官方apk也不能再安裝了,因爲系統認爲他是非法的。

最後,上面說了,無法做到修改apk後重簽名來覆蓋原作者的apk,那麼如果手機上本來就沒有原作者的apk包呢,單獨給我們一個apk我們能玩出什麼花樣呢?這些留在以後吧,預告一下之後的可能的內容:反編譯是怎麼搞呢?二次打包如何搞呢?怎麼植入代碼呢?等等等等。

結束語:2016年馬上就要完了,本篇文章也是2016年的最後一篇,並且,本篇也是博客文章的第100篇。實際我自己也沒有想到,當初的一個隨意的開端讓我堅持了這麼久。堅持原創,不轉載抄襲,用心去驗證每一個結論,而非隨網絡人云亦云。即使有時候感覺心很累,但是最終還是沒有放棄。隨着知識的深入度增加,可能研究用的時間會越來越長,以後的文章發佈頻率會減小,但是我肯定還是會堅持到最後的。希望2017我們的努力能夠得到回報,希望所有程序員們都能有美好的未來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章