0x01 回顾
分析验签过程之前,有必要先回顾一下Android应用的签名过程:
- 对APK包中的每个文件做一次运算(Hash+Base64编码),将结果保存到META-INF/MANIFEST.MF文件中;
- 对MANIFEST.MF整个文件做一次运算(Hash+Base64编码),将结果保存到META-INF/CERT.SF文件的头属性中,再对MANIFEST.MF文件中的各个属性块做同样的运算(Hash+Base64编码),存放到CERT.SF的属性块中。
- 开发者用自己的私钥对CERT.SF进行签名,并将签名信息和包含公钥信息的数字证书一同保存到META-INF/CERT.RSA文件中。
因此,应用的验签过程其实也是围绕这三步来进行的。
0x02 相关源码的位置(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这个核心服务类来完成的,所以我们可以从这个类入手,其中开始执行签名校验的在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,并且这里调用了PackageParser.collectCertificates方法来进行签名的校验。于是进入该方法:
public void collectCertificates(Package pkg, int flags) throws PackageParserException {
......
collectCertificates(pkg, new File(pkg.baseCodePath), flags);
......
}
如上,该方法由调用了一个函数重载,代码如下:
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文件来构造一个StrictJarFile对象,下面来看一下它的构造方法都做了些什么事情:
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();
......
}
如上,首先调用getMetaEntries()方法将META-INF目录下每一个文件的文件名及其数据流存放到metaEntries这个HashMap对象中;然后通过MANIFEST.MF文件的数据流构造一个Manifest对象;接着利用得到的metaEntries和manifest来构造一个JarVerifier对象,最后调用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这三个文件的字节流:manifestBytes、sfBytes、sBlockBytes,然后将sfBytes和sBlockBytes传入JarUtils.verfySignature()方法中,进行数字签名的校验,校验的过程这里就不贴代码了,简单说就是用CERT.RSA这个文件中的包含的公钥对数字签名进行解密,将解密后的结果与CERT.SF文件hash运算后的结果进行比对,一致的话就返回证书链信息,并将证书链保存在certificates对象中,同时说明CERT.SF文件没有被篡改,另外,Jarverifier.isSignedJar()方法就是判断certificates是否为空,不为空返回true,空则返回false。否则就抛出GeneralSecurityException异常。接着上面继续看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);
根据代码中的注释也能够很清晰的了解到,这段代码主要就是通过读取CERT.SF,然后来验证MANIFEST.MF文件是否被篡改。来先看一下CERT文件的部分内容:
结合上图,再回到JarVerifier.verifyCertificate()方法的代码中来分析一下具体的流程吧:
首先读取CERT.SF文件,并创建与之相关的两个对象attributes和entries;
接着就通过attributes对象判断CERT.SF文件中是否存在"Signature-Version"属性,没有的话直接返回;
再判断CERT.SF文件中的"Created-By"属性的值是否包含"signtool"子串,有的话表示该apk是用其他签名工具签的名;如上图,这里用的是JDK自带jarsigner签的名,所以不含signtool字符串,这样的话之后就会调用JarVerifier.verify()方法来判断是否有"SHA1-Digest-Manifest-Main-Attributes"属性,有的话就校验它的值,看它是否为MANIFEST.MF的头属性块运算(Hash+Base64编码)后的值。可以看到,JarVerifier.verify()方法的第三个参数传的就是MANIFEST.MF的字节流。
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数组,该数组的内容如下:
private static final String[] DIGEST_ALGORITHMS = new String[] {
"SHA-512",
"SHA-384",
"SHA-256",
"SHA1",
};
将遍历到的算法名与字符串"-Digest-Manifest-Main-Attributes"组合,然后判断该属性是否存在,不存在则略过(continue;)。然后就是hash值的比对了。
再次回到JarVerifier.verifyCertificate()方法的代码:
接下来就是再次调用JarVerifier.verifier()方法,不过这次是对MANIFEST.MF整个文件的Hash与CERT.SF的"SHA1--Digest-Manifest"属性的值进行比对,如果一致,则说明MANIFEST.MF没有被篡改,并将CERT.SF文件的信息添加到metaEntries和signatures的属性中。如果不一致,则遍历所有的属性块,看是哪一个属性块的值不正确。
到这里,StrictJarFile的构造方法就完成了。从上面的分析可以看到,验签的三个步骤中,有两步是再StrictJarFile的构造方法中完成的,分别是:CERT.SF是否被篡改,MANIFEST.MF是否被篡改。
接下来,让我们再回到PackageParser.collectCertificates()方法中,继续完成后续的校验分析,代码如下:
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);
}
}
如上,在创建了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);
}
}
这里调用了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;
}
该方法就是创建一个JarVerifier.VerifierEntry对象:
第一个参数name是文件名;
第二个参数是用来产生摘要的对象MessageDigest,且摘要算法algorithm也是同前面的方法一样,从DIGEST_ALGORITHMS数组中遍历,再根据MANIFEST.MF文件的属性名来得到;
第三个参数是MANIFEST.MF中所保存的对应文件名的Hash值;
MANIFEST.MF的部分内容如下:
第四个参数是对该APK进行签名的所有证书链信息。它为什么是二维数组?是因为Android允许用多个证书对apk进行签名,且它们的证书文件名必须不同。
最后一个参数是已经验证过的文件列表,VerifierEntry在完成了对指定文件的摘要验证之后会将该文件的信息加到其中。
接着,来看一下JarFile.JarFileInputStream的构造方法:
JarFileInputStream(InputStream is, long size, JarVerifier.VerifierEntry e) {
super(is);
entry = e;
count = size;
}
只是几个赋值操作,其中将前面得到的JarVerifier.VerifierEntry对象传入并赋值给这里的entry。
将视线在回到PackageParser.loadCertificates()方法中,经过上面的分析,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;
}
看起来只是对传入的字节输入流对象进行读取,直到读完,然后返回读到的字节数。但由于传入的是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;
}
}
如上,它会调用父类的read()方法进行读取,然后将读取到的数据传入entry.write()方法,最后在调用entry.verify()进行验证。这个entry就是前面创建的JarVerifier.VerifierEntry对象。来看一下JarVerifier.VerifierEntry.write()方法做了什么:
@Override
public void write(byte[] buf, int off, int nbytes) {
digest.update(buf, off, nbytes);
}
就是对数据进行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中;不一致的话就调用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()方法中继续看:
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的签名信息;如果安装过,则比对前后两次安装的签名信息,如果签名信息一致,则继续安装;如果前后签名不一致,则抛出异常INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES异常。所以平时我们在开发或者测试过程中安装应用时,如果抛出该异常,则说明已经有该包名的应用安装在设备上,且签名与你现在要安装的不同。必须先卸载已安装在设备上的才能继续安装。(INCONSISTENT就是不一致的意思)
0x04 小结
至此,Android的应用验签过程总算分析完了,可以看到,验签的过程刚好和签名的过程是相反的,下面总结一下:
- 所有的验签动作都是在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