apk的签名信息在apk解压后的META-INF目录下。这三个文件分别是MANIFEST.MF,CERT.SF,CERT.RSA。下面会说明这三个文件的作用以及生成方式。
MANIFEST.MF
MANIFEST.MF会将整个apk中所有的文件进行SHA1-BASE64编码,生成的摘要信息就是SHA1-Digest。下面的就是MANIFEST.MF中记录AndroidManifest.xml的摘要信息。
Name: AndroidManifest.xml
SHA1-Digest: 7AWE28hOihCw2ecRxVkh0LRpQw0=
关于计算文件SHA1可以使用linux的sha1sum工具,直接输出文件sha1。值得注意的是,比如生成的SHA1是以字符串形式输出在屏幕上的,在计算base64时,不能直接将字符串计算base64,而应该将SHA1两位一组,将其十六进制对应的UTF8的字符进行base64!
比如说,AndroidManifest.xml的SHA1为ec0584dbc84e8a10b0d9e711c55921d0b469430d。我们需要将ec0584dbc84e8a10b0d9e711c55921d0b469430d按照(ec)(05)(84)(db)…(0d)组合起来,每一组看做一个十六进制数,然后将这个十六进制数转成utf8对应的字符。最后将这个字符串计算base64输出,输出的结果就是SHA1-Digest的值。
在这里用python写了一个小的测试工具,仅供参考:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import base64
def test(hexstr):
hexarray = []
for i in range(0, len(hexstr), 2):
hex = hexstr[i] + hexstr[i + 1]
hexarray.append(chr(int(hex, 16)))
return reduce(lambda a, b: a + b, hexarray)
if __name__ == "__main__":
a = test("ec0584dbc84e8a10b0d9e711c55921d0b469430d") # 替换成sha1即可,输出为base64后的结果
print base64.b64encode(a)
CERT.SF
CERT.SF有两部组成,一部分是头部信息,SHA1-Digest-Manifest记录了刚才MANIFEST.MF的SHA1-BASE64的摘要;另一部分和刚刚MANIFEST.MF结构完全相同,只是SHA1-Digest不一样。其实CERT.SF中的SHA1-Digest是MANIFEST.MF对应项目的SHA1-BASE64后结果。
Signature-Version: 1.0
X-Android-APK-Signed: 2
SHA1-Digest-Manifest: uJqQ/1VgrhamBP3SWFDL1kvRaWU=
Created-By: 1.0 (Android SignApk)
...
Name: AndroidManifest.xml
SHA1-Digest: fua03tTT6MzoR5ljlADI+FymSUI=
...
主要说明下CERT.SF是如何计算出来的,因为其他文章说明的不是很清晰。
还是以AndroidManifest.xml这一项为例。其实计算CERT.SF某一项目的SHA1-Digest,是将MANIFEST.MF对应项目的整体并拼接\r\n进行计算的。需要注意的是,在ubuntu中,换行只是\n,但是在计算SHA1-Digest时,必须要将\n换成\r\n。同时,在全部换行结束后,要再在末尾添加一个\r\n。所以对于上面的AndroidManifest.xml而言,想要计算CERT.SF中对应的SHA1-Digest,就是要计算如下字符串的SHA1-BASE64。计算方式如前所述。
Name: AndroidManifest.xml\r\nSHA1-Digest: 7AWE28hOihCw2ecRxVkh0LRpQw0=\r\n\r\n
Android中对应的源码如下:
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
sf.getEntries().put(entry.getKey(), sfAttr);
}
注意的是,: 后面有个空格。从上面代码可见,每一个key-value后面拼接一个\r\n,同时在所有key-value输出完毕有,又会拼接一个\r\n。拼接后的字符串再计算SHA1-BASE64就是CERT.SF中的SHA1-Digest。
CERT.RSA
CERT.RSA主要保存的是证书的公钥信息和私钥对CERT.SF进行SHA1withRSA加密的签名。正常情况下,系统可以通过公钥信息对签名进行解密,得出的结果应该是CERT.SF的SHA1计算结果。
可能出现的篡改情况
-
对apk的某个文件进行修改
1.1 如果不重新计算MANIFEST.MF以及CERT.SF,就会导致两个文件中的对应项目的SHA1-Digest与修改后的文件不一致,从而验证失败
1.2 如果重新计算MANIFEST.MF以及CERT.SF,CERT.SF的SHA1就会发生变化,导致利用公钥验签失败
-
对apk的添加或者删除某个文件
结果同上
-
添加/删除/修改某个文件后,再对CERT.SF重新签名,并生成新的CERT.RSA
由于签名的私钥不会携带在apk中,理论上非app的所有者是不能搞到私钥的,所以想用原来的私钥对apk签名可以认为理论上是行不通的。在这种条件下,可以认为这个app已经可以被认定为不属于原来应用发布者的app。如果这个被篡改后的app被装到手机上,即使包名相同,将有两种情况会发生:
3.1 原来的手机上已经装了没有被篡改后的app,由于证书和原来的app不同,系统不会安装这个app。
3.2 原来的手机上没有装被篡改后的app,这时会安装成功。但假设有一天这个被篡改的app东窗事发,偷跑了这个用户几个亿的比特币之类的,也跟原来的应用开发者没有关系,因为从这个被篡改的apk中得出,这个apk不是他们发布的apk,自然也不是开发者偷跑的几个亿。所以,不要随便安装未知来源的app!
所以,android的验签机制是自签名机制。这套机制保证的并不是app一定不会被篡改,而是可以证明被篡改后app不再是原来开发者的app。