本篇咱們來看看android的簽名機制。發佈出來的apk都是有META-INF文件夾,裏面包含以下三個文件:php
下面來一一解釋這三個文件的做用(打包apk時簽名過程):SignApk.main()html
一、MANIFEST.MF:/build/tools/signapk/SignApk.java-addDigestsToManifest()java
遍歷APK包中除了META-INF\ 文件夾之外的全部文件,利用SHA1算法生成這些文件的消息摘要,而後轉化爲對應的base64編碼。MANIFEST.MF存儲的是文件的摘要值,保證完整性,防止文件被篡改。anzhi的MANIFEST.MF以下:android
// Manifest-Version: 1.0 // Created-By: 1.0 (Android) // Name: res/layout/act_header.xml // SHA1-Digest: tiVog/vCbIpPfnZbtZOxN28MKIE= // Name: res/drawable-hdpi/bg_top_list_index_red.9.png // SHA1-Digest: Y91AQINPN6Y7pkZ6qnQuSVcwLfw= ......
二、CERT.SF:/build/tools/signapk/SignApk.java-writeSignatureFile()
git
xx.SF文件(xx爲使用者證書的自定義別名,默認爲CERT,即CERT.SF),保存的是MANIFEST.MF的摘要值, 以及MANIFEST.MF中每個摘要項的摘要值,而後轉化成對應的base64編碼。雖然該文件的後綴名.sf(SignatureFile)看起來是簽名文件,可是並無私鑰參與運算,也不保存任何簽名內容。anzhi的CERT.SF:算法
// Signature-Version: 1.0 // Created-By: 1.0 (Android) // SHA1-Digest-Manifest: GBijl3ytIYpo7tJr1NgfkgssLWA= // Name: res/layout/act_header.xml // SHA1-Digest: 2KdEJyEwgrLAHZTdwEpnH6Ud4pE= // Name: res/drawable-hdpi/bg_top_list_index_red.9.png // SHA1-Digest: jfdrZJNisF8zAIexeGba0VuZSMU= ......
三、CERT.RSA:/build/tools/signapk/SignApk.java-writeSignatureBlock()apache
.RSA / .DSA文件(後綴不一樣採用的簽名算法不一樣,.RSA使用的是RSA算法, .DSA使用的是數字簽名算法DSA,目前APK主要使用的是這兩種算法),保存的是第二項.SF文件的數字簽名,同時還會包括簽名採用的數字證書(公鑰—參考資料1)。特別說明,當使用多重證書籤名時,每個.sf文件必須有一個.RSA/.DSA文件與之對應,也就是說使用證書CERT1簽名時有CERT1.SF和CERT1.RSA,同時採用證書CERT2簽名時又會生成CERT2.SF和CERT2.RSA。app
咱們看到這三個文件層層關聯,MANIFEST.MF保證apk完整性,CERT.SF對MANIFEST.MF hash來校驗,CERT.RSA利用密鑰對CERT.SF加密來校驗CERT.SF(這裏有個問題發現沒,若CERT.RSA的密鑰被更換,那麼...)。但咱們也必須認清幾點函數
一、 Android簽名機制實際上是對APK包完整性和發佈機構惟一性的一種校驗機制。ui
二、 Android簽名機制不能阻止APK包被修改,但修改後的再簽名沒法與原先的簽名保持一致。(擁有私鑰的狀況除外)。
三、 APK包加密的公鑰就打包在APK包內,且不一樣的私鑰對應不一樣的公鑰。換句話言之,不一樣的私鑰簽名的APK公鑰也必不相同。因此咱們能夠根據公鑰的對比,來判斷私鑰是否一致。
剛剛上面說了CERT.RSA的密鑰的被更換,事情就大條了。如今咱們看看在安裝apk時android中是如何進行簽名驗證的。
/libcore/luni/src/main/java/java/util/jar/JarVerifier.java
synchronized boolean readCertificates() { ... Iterator<String> it = metaEntries.keySet().iterator(); while (it.hasNext()) { String key = it.next(); if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) { verifyCertificate(key); // Check for recursive class load if (metaEntries == null) { return false; } it.remove(); } } return true; }
readCertificates找以".DSA"、".RSA"、".EC"結尾的文件,讓verifyCertificate來校驗
private void verifyCertificate(String certFile) { // Found Digital Sig, .SF should already have been read String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF"; byte[] sfBytes = metaEntries.get(signatureFile); if (sfBytes == null) { return; } byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME); // Manifest entry is required for any verifications. if (manifest == null) { return; } byte[] sBlockBytes = metaEntries.get(certFile); try {//verifySignature驗證SF文件 Certificate[] signerCertChain = JarUtils.verifySignature( new ByteArrayInputStream(sfBytes), new ByteArrayInputStream(sBlockBytes)); ...... // Verify manifest hash in .sf file Attributes attributes = new Attributes(); HashMap<String, Attributes> entries = new HashMap<String, Attributes>(); try { ManifestReader im = new ManifestReader(sfBytes, attributes); im.readEntries(entries, null); } catch (IOException e) { return; } // Use .SF to verify the mainAttributes of the manifest // If there is no -Digest-Manifest-Main-Attributes entry in .SF // file, such as those created before java 1.5, then we ignore // such verification. if (mainAttributesEnd > 0 && !createdBySigntool) { String digestAttribute = "-Digest-Manifest-Main-Attributes"; if (!verify(attributes, digestAttribute, manifest, 0, mainAttributesEnd, false, true)) { throw failedVerification(jarName, signatureFile); } } // Use .SF to verify the whole manifest. String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest"; if (!verify(attributes, digestAttribute, manifest, 0, manifest.length, false, false)) { Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Attributes> entry = it.next(); Manifest.Chunk chunk = man.getChunk(entry.getKey()); if (chunk == null) { return; } if (!verify(entry.getValue(), "-Digest", manifest, chunk.start, chunk.end, createdBySigntool, false)) { throw invalidDigest(signatureFile, entry.getKey(), jarName); } } } ...... }
代碼流程很清晰,
一、RSA驗證SF不被篡改——verifySignature
二、SF驗證MF文件不被篡改
在哪裏驗證apk文件有沒有篡改啊?(即驗證MF文件和app文件,等下分析哦)
繼續看verifySignature(不要忘了咱們是來看RSA中的密鑰如何認證的哦);但在分析源碼以前你先看參考資料1和下面這幅證書鏈
證書鏈示意圖
/libcore/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java
public static Certificate[] verifySignature(InputStream signature, InputStream signatureBlock) throws IOException, GeneralSecurityException { ...... return createChain(certs[issuerSertIndex], certs); }
private static X509Certificate[] createChain(X509Certificate signer, X509Certificate[] candidates) { LinkedList chain = new LinkedList(); chain.add(0, signer); // Signer is self-signed if (signer.getSubjectDN().equals(signer.getIssuerDN())){ return (X509Certificate[])chain.toArray(new X509Certificate[1]); } Principal issuer = signer.getIssuerDN(); X509Certificate issuerCert; int count = 1; while (true) { issuerCert = findCert(issuer, candidates); if( issuerCert == null) { break; } chain.add(issuerCert); count++; // 遞歸到根認證CA if (issuerCert.getSubjectDN().equals(issuerCert.getIssuerDN())) { break; } issuer = issuerCert.getIssuerDN(); } return (X509Certificate[])chain.toArray(new X509Certificate[count]); } private static X509Certificate findCert(Principal issuer, X509Certificate[] candidates) { for (int i = 0; i < candidates.length; i++) { // 只用字符串來判斷 if (issuer.equals(candidates[i].getSubjectDN())) { return candidates[i]; } } return null; }
看上圖證書鏈咱們可知,owner證書有效的前提是CA證書有效,而CA證書有效的前提是ROOT CA證書有效,ROOT CA證書的有效性由操做系統驗證。而在android系統裏,這部分由createChain函數來執行。createChain中用owner證書的IssuserDN—CA經過findCert函數來查找是否存在CA證書。findCert裏遍歷證書查看是否有證書的subjectDN == CA,若是有則表示此證書爲CA證書(若是不理解請繼續看參考資料1和證書鏈示意圖)。看得出這個findCert太隨意了值找證書而沒有Verify signature,致使這裏有bug,對此谷歌的修復方案以下
private static X509Certificate findCert(Principal issuer, X509Certificate[] candidates, X509Certificate subjectCert, boolean chainCheck) { for (int i = 0; i < candidates.length; i++) { if (issuer.equals(candidates[i].getSubjectDN())) { if (chainCheck) { try { subjectCert.verify(candidates[i].getPublicKey()); } catch (Exception e) { continue; } } return candidates[i]; } } return null; }
ok,簽名原理搞清楚了,咱們來看看上面提到的bug利用,此bug存在android4.4.1如下的全部版本中。
參考資料:
一、數字證書原理
二、【原創】Android證書驗證存漏洞 開發者身份信息可被篡改