0x01 回顧
分析驗簽過程之前,有必要先回顧一下Android應用的簽名過程:
- 對APK包中的每個文件做一次運算(Hash+Base64編碼)闪檬,將結(jié)果保存到META-INF/MANIFEST.MF文件中谤牡;
- 對MANIFEST.MF整個文件做一次運算(Hash+Base64編碼)壶谒,將結(jié)果保存到META-INF/CERT.SF文件的頭屬性中橱脸,再對MANIFEST.MF文件中的各個屬性塊做同樣的運算(Hash+Base64編碼)葫男,存放到CERT.SF的屬性塊中抱冷。
- 開發(fā)者用自己的私鑰對CERT.SF進行簽名,并將簽名信息和包含公鑰信息的數(shù)字證書一同保存到META-INF/CERT.RSA文件中梢褐。
因此旺遮,應用的驗簽過程其實也是圍繞這三步來進行的。
0x02 相關(guān)源碼的位置(AOSP 5.0.1_r1)
- frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
- frameworks/base/core/java/android/content/pm/PackageParser.java
- libcore/luni/src/main/java/java/util/jar/StrictJarFile.java
- libcore/luni/src/main/java/java/util/jar/JarVerifier.java
- libcore/luni/src/main/java/java/util/jar/JarFile.java
- libcore/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java
0x03 源碼分析
APK的安裝過程主要是由PackageManagerService這個核心服務類來完成的盈咳,所以我們可以從這個類入手耿眉,其中開始執(zhí)行簽名校驗的在installPackageLI方法里,代碼如下:
private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
......
PackageParser pp = new PackageParser();
......
try {
pp.collectCertificates(pkg, parseFlags);
pp.collectManifestDigest(pkg);
} catch (PackageParserException e) {
res.setError("Failed collect during installPackageLI", e);
return;
}
}
在這個方法中可以看到一個用來解析傳入的APK包的類PackageParser鱼响,并且這里調(diào)用了PackageParser.collectCertificates方法來進行簽名的校驗鸣剪。于是進入該方法:
public void collectCertificates(Package pkg, int flags) throws PackageParserException {
......
collectCertificates(pkg, new File(pkg.baseCodePath), flags);
......
}
如上,該方法由調(diào)用了一個函數(shù)重載丈积,代碼如下:
private static void collectCertificates(Package pkg, File apkFile, int flags)
throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();
StrictJarFile jarFile = null;
try {
jarFile = new StrictJarFile(apkPath);
......
}
由于該方法的代碼較長西傀,我們先分段看,先看上面的代碼桶癣,很明顯拥褂,是通過傳入的apk文件來構(gòu)造一個StrictJarFile對象,下面來看一下它的構(gòu)造方法都做了些什么事情:
public StrictJarFile(String fileName) throws IOException {
......
try {
......
HashMap<String, byte[]> metaEntries = getMetaEntries();
this.manifest = new Manifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
this.verifier = new JarVerifier(fileName, manifest, metaEntries);
isSigned = verifier.readCertificates() && verifier.isSignedJar();
......
}
如上牙寞,首先調(diào)用getMetaEntries()方法將META-INF目錄下每一個文件的文件名及其數(shù)據(jù)流存放到metaEntries這個HashMap對象中饺鹃;然后通過MANIFEST.MF文件的數(shù)據(jù)流構(gòu)造一個Manifest對象;接著利用得到的metaEntries和manifest來構(gòu)造一個JarVerifier對象间雀,最后調(diào)用JarVerifier的readCertificates()方法和isSignedJar()方法悔详。下面先看JarVerifier.readCertificates方法:
synchronized boolean readCertificates() {
if (metaEntries.isEmpty()) {
return false;
}
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);
it.remove();
}
}
return true;
}
如上,該方法首先判斷META-INF目錄是否為空惹挟,如果為空表示根本沒簽名茄螃,直接返回false。不為空的話就對metaEntries對象進行遍歷连锯,如果是證書文件归苍,則將其傳入verifyCertificate()方法進行校驗,JarVerifier.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);
......
byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
......
byte[] sBlockBytes = metaEntries.get(certFile);
try {
Certificate[] signerCertChain = JarUtils.verifySignature(
new ByteArrayInputStream(sfBytes),
new ByteArrayInputStream(sBlockBytes));
if (signerCertChain != null) {
certificates.put(signatureFile, signerCertChain);
}
} catch (IOException e) {
return;
} catch (GeneralSecurityException e) {
throw failedVerification(jarName, signatureFile);
}
......
}
該方法首先通過傳入的證書文件<CERT>.RSA的路徑來獲取<CERT>.SF的路徑运怖,然后通過前面得到的metaEntries來分別取得MANIFEST.MF拼弃、CERT.SF、CERT.RSA這三個文件的字節(jié)流:manifestBytes摇展、sfBytes吻氧、sBlockBytes,然后將sfBytes和sBlockBytes傳入JarUtils.verfySignature()方法中,進行數(shù)字簽名的校驗盯孙,校驗的過程這里就不貼代碼了鲁森,簡單說就是用CERT.RSA這個文件中的包含的公鑰對數(shù)字簽名進行解密,將解密后的結(jié)果與CERT.SF文件hash運算后的結(jié)果進行比對振惰,一致的話就返回證書鏈信息歌溉,并將證書鏈保存在certificates對象中,同時說明CERT.SF文件沒有被篡改报账,另外研底,Jarverifier.isSignedJar()方法就是判斷certificates是否為空,不為空返回true透罢,空則返回false榜晦。否則就拋出GeneralSecurityException異常。接著上面繼續(xù)看JarVerifier.verifyCertificate()方法:
// 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;
}
// Do we actually have any signatures to look at?
if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
return;
}
boolean createdBySigntool = false;
String createdBy = attributes.getValue("Created-By");
if (mainAttributesEnd > 0 && !createdBySigntool) {
String digestAttribute = "-Digest-Manifest-Main-Attributes";
if (!verify(attributes, digestAttribute, manifestBytes, 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, manifestBytes, 0, manifestBytes.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 = manifest.getChunk(entry.getKey());
if (chunk == null) {
return;
}
if (!verify(entry.getValue(), "-Digest", manifestBytes,
chunk.start, chunk.end, createdBySigntool, false)) {
throw invalidDigest(signatureFile, entry.getKey(), jarName);
}
}
}
metaEntries.put(signatureFile, null);
signatures.put(signatureFile, entries);
根據(jù)代碼中的注釋也能夠很清晰的了解到羽圃,這段代碼主要就是通過讀取CERT.SF乾胶,然后來驗證MANIFEST.MF文件是否被篡改。來先看一下CERT文件的部分內(nèi)容:
結(jié)合上圖朽寞,再回到JarVerifier.verifyCertificate()方法的代碼中來分析一下具體的流程吧:
首先讀取CERT.SF文件识窿,并創(chuàng)建與之相關(guān)的兩個對象attributes和entries;
接著就通過attributes對象判斷CERT.SF文件中是否存在"Signature-Version"屬性脑融,沒有的話直接返回喻频;
再判斷CERT.SF文件中的"Created-By"屬性的值是否包含"signtool"子串,有的話表示該apk是用其他簽名工具簽的名肘迎;如上圖甥温,這里用的是JDK自帶jarsigner簽的名,所以不含signtool字符串妓布,這樣的話之后就會調(diào)用JarVerifier.verify()方法來判斷是否有"SHA1-Digest-Manifest-Main-Attributes"屬性姻蚓,有的話就校驗它的值,看它是否為MANIFEST.MF的頭屬性塊運算(Hash+Base64編碼)后的值匣沼≌玻可以看到,JarVerifier.verify()方法的第三個參數(shù)傳的就是MANIFEST.MF的字節(jié)流释涛。
JarVerifier.verify()方法的代碼如下:
private boolean verify(Attributes attributes, String entry, byte[] data,
int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
String algorithm = DIGEST_ALGORITHMS[i];
String hash = attributes.getValue(algorithm + entry);
if (hash == null) {
continue;
}
MessageDigest md;
try {
md = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
continue;
}
if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') {
md.update(data, start, end - 1 - start);
} else {
md.update(data, start, end - start);
}
byte[] b = md.digest();
byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
return MessageDigest.isEqual(b, Base64.decode(hashBytes));
}
return ignorable;
}
在這個方法中加叁,由于不知道用的什么Hash算法,所以會遍歷DIGEST_ALGORITHMS數(shù)組枢贿,該數(shù)組的內(nèi)容如下:
private static final String[] DIGEST_ALGORITHMS = new String[] {
"SHA-512",
"SHA-384",
"SHA-256",
"SHA1",
};
將遍歷到的算法名與字符串"-Digest-Manifest-Main-Attributes"組合殉农,然后判斷該屬性是否存在,不存在則略過(continue;)局荚。然后就是hash值的比對了。
再次回到JarVerifier.verifyCertificate()方法的代碼:
接下來就是再次調(diào)用JarVerifier.verifier()方法,不過這次是對MANIFEST.MF整個文件的Hash與CERT.SF的"SHA1--Digest-Manifest"屬性的值進行比對耀态,如果一致轮傍,則說明MANIFEST.MF沒有被篡改,并將CERT.SF文件的信息添加到metaEntries和signatures的屬性中首装。如果不一致创夜,則遍歷所有的屬性塊,看是哪一個屬性塊的值不正確仙逻。
到這里驰吓,StrictJarFile的構(gòu)造方法就完成了。從上面的分析可以看到系奉,驗簽的三個步驟中檬贰,有兩步是再StrictJarFile的構(gòu)造方法中完成的,分別是:CERT.SF是否被篡改缺亮,MANIFEST.MF是否被篡改翁涤。
接下來,讓我們再回到PackageParser.collectCertificates()方法中萌踱,繼續(xù)完成后續(xù)的校驗分析葵礼,代碼如下:
private static void collectCertificates(Package pkg, File apkFile, int flags)
throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();
StrictJarFile jarFile = null;
try {
jarFile = new StrictJarFile(apkPath);
// Always verify manifest, regardless of source
final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
if (manifestEntry == null) {
throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Package " + apkPath + " has no manifest");
}
final List<ZipEntry> toVerify = new ArrayList<>();
toVerify.add(manifestEntry);
// If we're parsing an untrusted package, verify all contents
if ((flags & PARSE_IS_SYSTEM) == 0) {
final Iterator<ZipEntry> i = jarFile.iterator();
while (i.hasNext()) {
final ZipEntry entry = i.next();
if (entry.isDirectory()) continue;
if (entry.getName().startsWith("META-INF/")) continue;
if (entry.getName().equals(ANDROID_MANIFEST_FILENAME)) continue;
toVerify.add(entry);
}
}
for (ZipEntry entry : toVerify) {
final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
......
}
} catch (GeneralSecurityException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"Failed to collect certificates from " + apkPath, e);
} catch (IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath, e);
} finally {
closeQuietly(jarFile);
}
}
如上,在創(chuàng)建了StrictJarFile對象后并鸵,就對該對象進行遍歷鸳粉,將除了目錄和META-INF目錄下的文件外的所有文件的ZipEntry對象添加到toVerify這個列表中。然后遍歷該列表园担,將每一個文件代表的ZipEntry對象傳入PackageParser.loadCertificates()方法中届谈,代碼如下:
private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
throws PackageParserException {
InputStream is = null;
try {
// We must read the stream for the JarEntry to retrieve
// its certificates.
is = jarFile.getInputStream(entry);
readFullyIgnoringContents(is);
return jarFile.getCertificateChains(entry);
} catch (IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed reading " + entry.getName() + " in " + jarFile, e);
} finally {
IoUtils.closeQuietly(is);
}
}
這里調(diào)用了StrictJarFile.getInputStream()方法來獲取InputStream對象,看下該方法:
public InputStream getInputStream(ZipEntry ze) {
final InputStream is = getZipInputStream(ze);
if (isSigned) {
JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
if (entry == null) {
return is;
}
return new JarFile.JarFileInputStream(is, ze.getSize(), entry);
}
return is;
}
這里主要是獲取JarVerifier.VerifierEntry對象粉铐,最后返回一個JarFile.JarFileInputStream對象疼约。看一下JarVerifier.initEntry()方法:
VerifierEntry initEntry(String name) {
// If no manifest is present by the time an entry is found,
// verification cannot occur. If no signature files have
// been found, do not verify.
if (manifest == null || signatures.isEmpty()) {
return null;
}
Attributes attributes = manifest.getAttributes(name);
// entry has no digest
if (attributes == null) {
return null;
}
ArrayList<Certificate[]> certChains = new ArrayList<Certificate[]>();
Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
HashMap<String, Attributes> hm = entry.getValue();
if (hm.get(name) != null) {
// Found an entry for entry name in .SF file
String signatureFile = entry.getKey();
Certificate[] certChain = certificates.get(signatureFile);
if (certChain != null) {
certChains.add(certChain);
}
}
}
// entry is not signed
if (certChains.isEmpty()) {
return null;
}
Certificate[][] certChainsArray = certChains.toArray(new Certificate[certChains.size()][]);
for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
final String algorithm = DIGEST_ALGORITHMS[i];
final String hash = attributes.getValue(algorithm + "-Digest");
if (hash == null) {
continue;
}
byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
try {
return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
certChainsArray, verifiedEntries);
} catch (NoSuchAlgorithmException ignored) {
}
}
return null;
}
該方法就是創(chuàng)建一個JarVerifier.VerifierEntry對象:
第一個參數(shù)name是文件名蝙泼;
第二個參數(shù)是用來產(chǎn)生摘要的對象MessageDigest程剥,且摘要算法algorithm也是同前面的方法一樣,從DIGEST_ALGORITHMS數(shù)組中遍歷汤踏,再根據(jù)MANIFEST.MF文件的屬性名來得到织鲸;
第三個參數(shù)是MANIFEST.MF中所保存的對應文件名的Hash值;
MANIFEST.MF的部分內(nèi)容如下:
第四個參數(shù)是對該APK進行簽名的所有證書鏈信息溪胶。它為什么是二維數(shù)組搂擦?是因為Android允許用多個證書對apk進行簽名,且它們的證書文件名必須不同哗脖。
最后一個參數(shù)是已經(jīng)驗證過的文件列表瀑踢,VerifierEntry在完成了對指定文件的摘要驗證之后會將該文件的信息加到其中扳还。
接著,來看一下JarFile.JarFileInputStream的構(gòu)造方法:
JarFileInputStream(InputStream is, long size, JarVerifier.VerifierEntry e) {
super(is);
entry = e;
count = size;
}
只是幾個賦值操作橱夭,其中將前面得到的JarVerifier.VerifierEntry對象傳入并賦值給這里的entry氨距。
將視線在回到PackageParser.loadCertificates()方法中,經(jīng)過上面的分析棘劣,StrictJarFile.getInputStream()所返回的是JarFile.JarFileInputStream對象俏让。接著將該對象傳入PackageParser.readFullyIgnoringContents()方法中,來看下該方法做了什么:
public static long readFullyIgnoringContents(InputStream in) throws IOException {
byte[] buffer = sBuffer.getAndSet(null);
if (buffer == null) {
buffer = new byte[4096];
}
int n = 0;
int count = 0;
while ((n = in.read(buffer, 0, buffer.length)) != -1) {
count += n;
}
sBuffer.set(buffer);
return count;
}
看起來只是對傳入的字節(jié)輸入流對象進行讀取茬暇,直到讀完首昔,然后返回讀到的字節(jié)數(shù)。但由于傳入的是InpuStream對象的子類對象JarFile.JarFileInputStream糙俗,而且它重寫了read()方法勒奇,來看一下這個子類的read()方法做了什么事:
@override
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
if (done) {
return -1;
}
if (count > 0) {
int r = super.read(buffer, byteOffset, byteCount);
if (r != -1) {
int size = r;
if (count < size) {
size = (int) count;
}
entry.write(buffer, byteOffset, size);
count -= size;
} else {
count = 0;
}
if (count == 0) {
done = true;
entry.verify();
}
return r;
} else {
done = true;
entry.verify();
return -1;
}
}
如上,它會調(diào)用父類的read()方法進行讀取臼节,然后將讀取到的數(shù)據(jù)傳入entry.write()方法撬陵,最后在調(diào)用entry.verify()進行驗證。這個entry就是前面創(chuàng)建的JarVerifier.VerifierEntry對象网缝。來看一下JarVerifier.VerifierEntry.write()方法做了什么:
@Override
public void write(byte[] buf, int off, int nbytes) {
digest.update(buf, off, nbytes);
}
就是對數(shù)據(jù)進行hash巨税。再來看一下JarVerifier.VerifierEntry.verify()方法做了什么:
void verify() {
byte[] d = digest.digest();
if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
}
verifiedEntries.put(name, certChains);
}
該方法就是將文件的Hash與MANIFEST中對應文件的Hash值進行比對,一致的話則將文件名和證書鏈添加到verifiedEntries中粉臊;不一致的話就調(diào)用JarVerifier.invalidDigest()方法拋出SecurityException異常草添,如下:
private static SecurityException invalidDigest(String signatureFile, String name,
String jarName) {
throw new SecurityException(signatureFile + " has invalid digest for " + name +
" in " + jarName);
}
到這里,第三步扼仲,即校驗APK所有文件是否有被篡改远寸,也已完成。再次回到PackageParser.collectCertificates()方法中繼續(xù)看:
for (ZipEntry entry : toVerify) {
final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
if (ArrayUtils.isEmpty(entryCerts)) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Package " + apkPath + " has no certificates at entry "
+ entry.getName());
}
final Signature[] entrySignatures = convertToSignatures(entryCerts);
if (pkg.mCertificates == null) {
pkg.mCertificates = entryCerts;
pkg.mSignatures = entrySignatures;
pkg.mSigningKeys = new ArraySet<PublicKey>();
for (int i=0; i < entryCerts.length; i++) {
pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
}
} else {
if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
+ " has mismatched certificates at entry "
+ entry.getName());
}
}
}
loadCertificates()之后的代碼屠凶,主要就是判斷該APK是否原來安裝過驰后,如果沒安裝過,則保存該APK的簽名信息矗愧;如果安裝過灶芝,則比對前后兩次安裝的簽名信息,如果簽名信息一致唉韭,則繼續(xù)安裝夜涕;如果前后簽名不一致,則拋出異常INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES異常属愤。所以平時我們在開發(fā)或者測試過程中安裝應用時女器,如果拋出該異常,則說明已經(jīng)有該包名的應用安裝在設(shè)備上住诸,且簽名與你現(xiàn)在要安裝的不同驾胆。必須先卸載已安裝在設(shè)備上的才能繼續(xù)安裝涣澡。(INCONSISTENT就是不一致的意思)
0x04 小結(jié)
至此,Android的應用驗簽過程總算分析完了俏拱,可以看到暑塑,驗簽的過程剛好和簽名的過程是相反的吼句,下面總結(jié)一下:
- 所有的驗簽動作都是在JarVerifier這個類里面完成的锅必;
- 在JarVerifier.verifyCertificate()方法中完成了以下兩步:
- 使用CERT.RSA校驗CERT.SF,看CERT.SF是否被篡改惕艳;
- 使用CERT.SF校驗MANIFEST.MF搞隐,看MANIFEST.MF是否被篡改;
- 在JarVerifier.VerifierEntry.verify()方法中完成最后一步:
- 使用MANIFEST.MF來校驗所有文件远搪,看有沒有文件被篡改劣纲,或者有沒有文件被刪
除,又或者有沒有添加新的文件谁鳍。
- 使用MANIFEST.MF來校驗所有文件远搪,看有沒有文件被篡改劣纲,或者有沒有文件被刪
0x05 參考文獻
http://blog.csdn.net/roland_sun/article/details/42029019
https://www.cnblogs.com/JeffreySun/archive/2010/06/24/1627247.html
http://netsecurity.51cto.com/art/201108/287971.htm