一.背景知识
1.1 android权限分级
在android开发中权限主要分为以下四级
-
normal 级别:默认值,低风险权限,无需用户确认。
-
dangerous 级别:危险级别,需要用户确认才能授权。
-
signature 级别:只会赋予与申明权限的应用程序使用相同签名的应用权限。一般我们所说的系统权限(这边所谓的系统权限要和 sharedUserId 的system 区别开来不是一个概念)其实就是指的 system 应用所声明的权限,所以应用程序在使用这些权限的时候需要使用系统签名(不需要声明 sharedUserId 也是可以的)。
-
signatureOrSystem 级别:这个级别的权限可以授权与 “系统应用” 和 与声明权限具有相同签名的应用程序,所以这个算是一个折中的方案。需要注意的是 在4.3 以前 这个 “系统应用” 是泛指在system分区下的应用,而4.3以后是特指 /system/priv-app目录下的应用。
关于权限定义的源文件位于:frameworks\base\core\res\AndroidManifest.xml
这边要注意 系统权限应用(signature)和系统应用(system)的区别:
系统应用一般是厂商内置的应用,在目录 system/app 、/system/framework/、/vendor/app/下的都是系统应用,
系统权限应用是 指在 AndroidManifest.xml 申请了 android:sharedUserId="android.uid.system" 的应用,而这个是需要系统签名的。
这部分的知识在PKMS服务中会详细的介绍。
1.2 系统签名类型
android的标准签名key有:
-
testkey: testkey是作为android编译的时候默认的签名key,如果系统中的apk的android.mk中没有设置LOCAL_CERTIFICATE的值,就默认使用testkey
-
media:如果LOCAL_CERTIFICATE := media 就代表使用 media 来签名,这样apk就会跟系统中所有使用android.uid.media共享UID。用这个是该APK是media/download系统中的一环。
-
platform: 如果LOCAL_CERTIFICATE := platform 就代表使用platform来签名,这样的话这个apk就拥有了和system相同的签名,因为系统级别的签名也是使用的platform来签名
-
shared:如果LOCAL_CERTIFICATE := shared 就代表使用 shared 来签名,这样apk就会跟系统中所有使用android.uid.shared共享UID。用这个的一般是该APK需要和home/contacts进程共享数据。
在源码的/build/target/product/security里面看到对应的密钥,其中.pk8代表私钥,.pem公钥,一定是成对出现的。
查看.pem公钥方法
openssl x509 -in cert.pem(文件名) -noout -text
关于.pk8和.pem在下面会继续详细介绍。
1.3 签名相关知识
1.3.1 数据摘要
消息摘要算法(Message Digest Algorithm)是一种能产生特殊输出格式的算法,其原理是根据一定的运算规则对原始数据进行某种形式的信息提取,被提取出的信息就被称作原始数据的消息摘要。简单点说就是提取数据的“指纹”。
著名的摘要算法有RSA公司的MD5算法和SHA-1算法及其大量的变体。
消息摘要的主要特点有:
1)无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。例如应用MD5算法摘要的消息有128个比特位,用SHA-1算法摘要的消息最终有160比特位的输出
。
2)一般来说(不考虑碰撞的情况下),只要输入的原始数据不同,对其进行摘要以后产生的消息摘要也必不相同,即使原始数据稍有改变,输出的消息摘要便完全不同。但是,相同的输入必会产生相同的输出。
3)具有不可逆性,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的原始消息。
1.3.2 jarsign和signapk工具
这两个都是android系统中使用的签名工具。jarsign是Java本生自带的一个工具,他可以对jar进行签名的。而signapk是后面专门为了Android应用程序apk进行签名的工具,他们两的签名算法没什么区别,主要是签名时使用的文件不一样。
-
jarsign工具 签名时使用的是 keystore 文件。
-
signapk工具 签名时使用的是pk8、pem文件。
-
keystore 文件 和 pk8、pem文件 是可以转化的,在开发具有系统权限的应用的时候我们一般是将pk8、pem文件转化成keystore 文件,然后再在AS中设置调试编译打包使用的keystore 文件,这样就可以像开发普通应用一样去开发具有系统权限的应用,而不是每次打包后再用signapk工具重新签名。
这边补充下:keystore 是Eclipse 打包生成的签名。 而 .jks是Android studio 生成的签名!都是用来打包的,并保证应用的唯一性,它们是可以互相转换的。
因为我们现在常用的是jks文件,可以通过代码获取jks文件的公钥和私钥信息
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import javax.security.cert.Certificate;
public class JKSTesting {
public static PublicKey getPublicKey(String keyStoreFile,
String storeFilePass, String keyAlias) {
// 读取密钥是所要用到的工具类
KeyStore ks;
// 公钥类所对应的类
PublicKey pubkey = null;
try {
// 得到实例对象
ks = KeyStore.getInstance("JKS");
FileInputStream fin;
try {
// 读取JKS文件
fin = new FileInputStream(keyStoreFile);
try {
// 读取公钥
ks.load(fin, storeFilePass.toCharArray());
java.security.cert.Certificate cert = ks
.getCertificate(keyAlias);
pubkey = cert.getPublicKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
} catch (KeyStoreException e) {
e.printStackTrace();
}
return pubkey;
}
/**
* 得到私钥
*
* @param keyStoreFile
* 私钥文件
* @param storeFilePass
* 私钥文件的密码
* @param keyAlias
* 别名
* @param keyAliasPass
* 密码
* @return
*/
public static PrivateKey getPrivateKey(String keyStoreFile,
String storeFilePass, String keyAlias, String keyAliasPass) {
KeyStore ks;
PrivateKey prikey = null;
try {
ks = KeyStore.getInstance("JKS");
FileInputStream fin;
try {
fin = new FileInputStream(keyStoreFile);
try {
try {
ks.load(fin, storeFilePass.toCharArray());
// 先打开文件
prikey = (PrivateKey) ks.getKey(keyAlias, keyAliasPass
.toCharArray());
// 通过别名和密码得到私钥
} catch (UnrecoverableKeyException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
} catch (KeyStoreException e) {
e.printStackTrace();
}
return prikey;
}
public static void main(String[] args) {
PublicKey publicKey;
PrivateKey privateKey;
publicKey=getPublicKey("*******","*******", "*******");
privateKey=getPrivateKey("*******","*******", "*******","*******");
System.out.println("publicKey");
System.out.println(publicKey.toString());
System.out.println("privateKey");
System.out.println(privateKey.toString());
}
}
1.3.3 签名后生成的文件
解压签名后的apk文件,在 META-INF 文件夹中可以看到以下格式的文件
- .RSA或者DSA
- .SF
- MANIFEST.MF文件
Android中是允许使用多个 keystore 对apk进行签名的,这样在 META-INF 就会生成多对(.RSA和.SF)的文件,以 keystore 文件的 alias 的值为名称。如果是用 signapk工具 进行签名的话就会直接固定命名CERT。
1.3.3.1 MANIFEST.MF文件
我们先看下这个文件的内容
Name: lombok/installer/InstallerGUI$4.SCL.lombok
SHA1-Digest: ZSCgYd7ezY8EyCteY5YFVNSzqbg=
Name: res/drawable-hdpi-v4/modify_phone_num_step_first.png
SHA1-Digest: 1A/23eA5r36ww5luAwlaK5n42fc=
Name: res/drawable-hdpi-v4/right_kuohao_focus.png
SHA1-Digest: fPzTTOc+c1ogDOlHKpeJ0JGDt6U=
Name: res/layout/activity_modify_phone.xml
SHA1-Digest: aE+Ctjnxcu/xHp79HhRZ8LvnA/c=
Name: res/drawable-1920x1080/item_h_default.png
SHA1-Digest: KA0HpPJUVaKTpTnF5ouiJmRNyXo=
Name: lombok/delombok/LombokOptionsFactory$1.SCL.lombok
SHA1-Digest: QlP0zlFTJ9scHtfLL3/OZNhA2uw=
Name: AndroidManifest.xml
SHA1-Digest: lkuasUmz9QVhBrAh8JEMmyvL65c=
Name: lombok/installer/InstallerGUI$9.SCL.lombok
SHA1-Digest: ifEHckOynWf1wUlfPH/aoj73utk=
Name: lombok/core/LombokConfiguration.SCL.lombok
SHA1-Digest: XOaLbgugkPhHqkxGiDFxUvxNJSA=
Name: lombok/installer/eclipse/JbdsLocationProvider.SCL.lombok
SHA1-Digest: 3M+Q/7s7VwuoOWk1hpUJe8yBJjM=
Name: res/drawable-hdpi-v4/user_prog_entry_layer.png
SHA1-Digest: 5oJ0Hv4o+c7PkwJ4MOox2/xZmzU=
文件的内容是逐一遍历apk里面的所有条目,如果是目录就跳过,如果是一个文件,就用SHA1(或者SHA256)消息摘要算法提取出该文件的摘要然后进行BASE64编码后,作为“SHA1-Digest”属性的值写入到MANIFEST.MF文件中的一个块中。该块有一个“Name”属性,其值就是该文件在apk包中的路径。
1.3.3.2 SF文件
Signature-Version: 1.0
X-Android-APK-Signed: 2
SHA1-Digest-Manifest: oZ0/oKbtbxza48iz2t1zT9MQ/vw=
Created-By: 1.0 (Android)
Name: res/layout/play_progress_tip_layout.xml
SHA1-Digest: zGKChGzCciVxMBfsf/9QS1jA9f0=
Name: res/anim/boot_one_circle.xml
SHA1-Digest: P4oLIBrBAQqjEn2VUDXvvuYXfeI=
Name: res/drawable-hdpi-v4/myapp_del_no_focus.png
SHA1-Digest: GEyz0DWb4vzq33MLCBnxtTVbWQE=
Name: lombok/eclipse/handlers/HandleLog$LoggingFramework.SCL.lombok
SHA1-Digest: LSHMxv3Atl0Z7MCQaPf9yKvhax4=
Name: lombok/delombok/DelombokApp$1.SCL.lombok
SHA1-Digest: DfUlhS1EZ55lpPpz+vsDIf847No=
SF文件记载的内容:
- 计算这个MANIFEST.MF文件的整体SHA1值,再经过BASE64编码后,记录在CERT.SF主属性块(在文件头上)的“SHA1-Digest-Manifest”属性值值下
- 逐条计算MANIFEST.MF文件中每一个块的SHA1,并经过BASE64编码后,记录在CERT.SF中的同名块中,属性的名字是“SHA1-Digest
1.3.3.3 RSA文件
因为RSA文件加密了,所以我们需要用openssl命令才能查看其内容
openssl pkcs7 -inform DER -in CERT.RSA(文件名) -noout -print_certs –text
Certificate:
Data:
Version: 3 (0x2) //版本号
Serial Number: 610763924 (0x24678494) //证书序列号
Signature Algorithm: sha256WithRSAEncryption //算法
Issuer: CN=icntv //发行者名称
Validity
Not Before: Nov 5 15:43:01 2012 GMT //生效日期
Not After : Oct 12 15:43:01 2112 GMT //终止日期
Subject: CN=icntv //主体名称
//算法 参数 秘钥:主体公钥信息,包含开发者的加密算法以及密钥信息
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:80:03:44:30:c7:6a:93:e3:3a:94:8e:b6:a8:70:
e0:19:eb:00:de:85:a9:b8:57:b9:e5:2e:0a:db:c0:
1b:e4:56:c0:32:7c:f3:54:65:ca:20:02:df:6c:3b:
f6:7e:29:71:3d:aa:69:ac:e9:61:17:65:9c:9b:94:
74:40:a2:67:f8:65:26:2c:a0:cf:2a:2e:15:7d:04:
c2:c2:44:d7:28:8a:c0:a7:65:ac:e5:eb:c6:67:ce:
74:82:47:0a:af:3f:7d:a9:d8:de:49:39:9e:17:49:
b4:f7:86:39:e1:02:25:6b:6e:94:82:95:3b:f2:62:
07:6a:41:33:0b:3a:d2:31:a7:2b:68:c7:73:49:2f:
7b:5c:83:51:05:5b:a4:5d:01:cf:3a:b9:98:f4:a1:
6b:20:85:f9:3f:aa:e7:5e:e6:35:82:43:1e:d2:ed:
90:8c:24:47:22:06:95:ab:8b:fd:12:0f:23:13:74:
07:f4:02:e0:94:71:0e:7b:b8:ae:02:54:84:d6:e3:
eb:91:a5:41:7b:6e:c5:26:57:9d:b6:ab:58:f0:e4:
c4:d0:a1:9e:23:31:02:ff:60:98:a3:4e:2b:d2:58:
6c:d1:12:a8:95:cb:19:69:0d:76:bc:50:e1:ed:6a:
e3:a9:42:9a:b5:76:55:fb:34:41:b3:00:9a:99:cb:
9c:d9
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
6D:26:4A:D1:30:4C:15:D6:EE:9B:63:95:AF:C1:63:7F:8B:83:31:68
//算法 参数 已加密的hash值:用CA的私钥对证书的所有域以及这些域的Hash值一起加密
Signature Algorithm: sha256WithRSAEncryption
08:2d:00:a8:6e:ad:67:87:6b:20:31:b0:d3:0f:52:64:cf:23:
ab:05:3a:53:8b:30:05:3e:47:0a:5c:19:1a:b0:e9:8d:4b:1d:
2c:f6:41:33:ba:b3:67:0a:77:46:bb:46:57:ef:f9:66:d8:68:
5d:41:be:3c:1d:35:f3:a3:58:4a:f4:f0:7f:87:1b:e4:0c:0c:
84:91:08:23:4c:9f:82:44:da:11:b1:60:b9:db:da:f6:36:58:
25:f6:ae:dc:4c:24:0d:1f:65:05:ee:9c:9c:c5:64:4a:a0:21:
4c:53:5a:3c:3e:c6:ad:67:cb:36:5e:6d:58:85:f5:2f:dd:6d:
c8:cc:7b:09:ba:8c:bd:6f:e8:53:d1:88:8d:35:90:48:ba:db:
5b:16:22:31:46:da:aa:91:ba:50:57:a5:38:08:af:05:ae:a1:
ec:3d:4f:fe:06:88:c8:60:9d:b9:16:00:7a:74:c8:a1:85:53:
dd:b5:0c:b7:09:f3:d4:e5:07:29:12:05:c2:83:84:c3:96:ec:
ba:ad:12:ad:1d:cf:52:d3:e3:ce:47:6a:89:4a:b1:39:c3:d2:
b7:e0:d5:77:d9:02:e5:17:89:09:00:b3:cb:38:38:ef:97:90:
35:27:41:90:cf:b7:98:34:21:69:bd:64:ad:2b:23:85:97:96:
6b:74:56:9b
1.3.4 为什么android要用这样的方式加密
-
首先,如果你改变了apk包中的任何文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是验证失败,程序就不能成功安装。
-
其次,如果你对更改的过的文件相应的算出新的摘要值,然后更改MANIFEST.MF文件里面对应的属性值,那么必定与CERT.SF文件中算出的摘要值不一样,照样验证失败。
-
最后,如果你还不死心,继续计算MANIFEST.MF的摘要值,相应的更改CERT.SF里面的值,那么数字签名值必定与CERT.RSA文件中记录的不一样,还是失败。
那么能不能继续伪造数字签名呢?不可能,因为没有数字证书对应的私钥。
所以,如果要重新打包后的应用程序能再Android设备上安装,必须对其进行重签名。
1.3.5 总结
1.3.5.1 概念
-
数据指纹就是对一个数据源做SHA/MD5算法,这个值是唯一的
-
签名文件技术就是:数据指纹+RSA算法
-
证书文件中包含了公钥信息和其他信息
-
在Android签名之后,其中SF就是签名文件,RSA就是证书文件我们可以使用openssl来查看RSA文件中的证书信息和公钥信息
1.3.5.2 流程
-
对Apk中的每个文件做一次算法(数据摘要+Base64编码),保存到MANIFEST.MF文件中
-
对MANIFEST.MF整个文件做一次算法(数据摘要+Base64编码),存放到CERT.SF文件的头属性中,在对MANIFEST.MF文件中各个属性块做一次算法(数据摘要+Base64编码),存到到一个属性块中。
-
对CERT.SF文件做签名,内容存档到CERT.RSA中
关于签名相关知识因为本人对安全这块涉及很少,所以基本都是引用自blog(https://blog.csdn.net/jiangwei0910410003/article/details/50402000) 的内容
二.问题分析
前面我们对相关的知识做了梳理,现在回到我们的需求上来。已知需求为:要求系统级权限应用使用系统签名文件和指定的第三方签名文件都可以完成验证。在之前的PKMS服务的介绍中我们知道在应用安装和开机扫描的时候是需要做签名验证的,我们现在以应用安装为入口对源码进行跟踪。(关于PKMS服务的源码分析blog会在后续更新出来)
下面的代码基于android6.0,设计的代码文件路径如下:
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
2.1 installPackageLI
这边要先声明 installPackageLI 并不是一个应用安装的入口方法,应用的安装涉及到的服务还有 PackageManagerInstallerService 服务,我们可以简单的把应用的安装分为两个步骤:一是拷贝阶段,二是安装阶段。而这个过程的控制则是由 PackageManagerInstallerService 进行控制的,我们这边要介绍的 installPackageLI 方法算是 第二阶段的入口方法(因为本文主要是介绍签名验证,关于应用安装过程会有跳过,毕竟这个不是本文的重点,后续文章会介绍6.0应用安装的过程。)
installPackageLI 方法比较长,篇幅原因就不全部拷贝出来了,这边列举下该方法做了哪些东西
- 解析apk文件
- 收集应用的签名
- 检查安装参数是否带有强制替换参数,并做相应的处理
- 如果该应用已安装则做相应的验证,还需要判断升级的是否是系统应用
- 检查应用中定义的权限(这部分的判断会比较多,但不是这次分析的重点)
- 系统应用升级的话会对安装位置做检查不允许安装在sd卡上
- 执行 replacePackageLI 或者 installNewPackageLI
这边我们的重点在第二点收集应用的签名和第四点中调用的 verifySignaturesLP 方法。首先我们先看看第二点的代码
final PackageParser.Package pkg;
try {
pkg = pp.parsePackage(tmpPackageFile, parseFlags);
} catch (PackageParserException e) {
res.setError("Failed parse during installPackageLI", e);
return;
}
// Mark that we have an install time CPU ABI override.
pkg.cpuAbiOverride = args.abiOverride;
String pkgName = res.name = pkg.packageName;
if ((pkg.applicationInfo.flags&ApplicationInfo.FLAG_TEST_ONLY) != 0) {
if ((installFlags & PackageManager.INSTALL_ALLOW_TEST) == 0) {
res.setError(INSTALL_FAILED_TEST_ONLY, "installPackageLI");
return;
}
}
//收集应用签名
try {
pp.collectCertificates(pkg, parseFlags);
pp.collectManifestDigest(pkg);
} catch (PackageParserException e) {
res.setError("Failed collect during installPackageLI", e);
return;
}
代码很简单,涉及到一个新的类 ackageParser.Package,我们跟踪进 collectCertificates 方法中查看
2.2 collectCertificates
public void collectCertificates(Package pkg, int flags) throws PackageParserException {
pkg.mCertificates = null;
pkg.mSignatures = null;
pkg.mSigningKeys = null;
collectCertificates(pkg, new File(pkg.baseCodePath), flags);
if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) {
for (String splitCodePath : pkg.splitCodePaths) {
collectCertificates(pkg, new File(splitCodePath), flags);
}
}
}
这边我们重点关注重载方法 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);
}
}
// Verify that entries are signed consistently with the first entry
// we encountered. Note that for splits, certificates may have
// already been populated during an earlier parse of a base APK.
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());
}
}
}
} 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);
}
}
这边我们重点关注这里
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());
}
因为我们的需求是增加一个支持系统级权限的签名,所以关于apk和签名校对来保证apk未被篡改不是我们关注的重点,我们关注的重点应该是证书文件(RSA文件)里面的公钥和我们需要比对的公钥是否是一致的。
所以前面签名校对我们直接跳过,直接看这边获取到的公钥信息保存在上面代码所示的位置。
关于这个签名信息的获取过程建议查看姜老师的这篇blog:https://blog.csdn.net/jiangwei0910410003/article/details/50443505
现在我们获取到了apk的公钥信息,那么在哪里进行比较呢?这时候我们回到 2.1 中 第四点提到的 verifySignaturesLP 方法
我们先来看看这个方法里面都做了什么
2.4 verifySignaturesLP
private void verifySignaturesLP(PackageSetting pkgSetting, PackageParser.Package pkg)
throws PackageManagerException {
if (pkgSetting.signatures.mSignatures != null) {
// Already existing package. Make sure signatures match
boolean match = compareSignatures(pkgSetting.signatures.mSignatures, pkg.mSignatures)
== PackageManager.SIGNATURE_MATCH;
if (!match) {
match = compareSignaturesCompat(pkgSetting.signatures, pkg)
== PackageManager.SIGNATURE_MATCH;
}
if (!match) {
match = compareSignaturesRecover(pkgSetting.signatures, pkg)
== PackageManager.SIGNATURE_MATCH;
}
if (!match) {
throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "Package "
+ pkg.packageName + " signatures do not match the "
+ "previously installed version; ignoring!");
}
}
//这边是重点了 检查shared uid的签名信息
// Check for shared user signatures
if (pkgSetting.sharedUser != null && pkgSetting.sharedUser.signatures.mSignatures != null) {
// Already existing package. Make sure signatures match
boolean match = compareSignatures(pkgSetting.sharedUser.signatures.mSignatures,
pkg.mSignatures) == PackageManager.SIGNATURE_MATCH;
if (!match) {
match = compareSignaturesCompat(pkgSetting.sharedUser.signatures, pkg)
== PackageManager.SIGNATURE_MATCH;
}
if (!match) {
match = compareSignaturesRecover(pkgSetting.sharedUser.signatures, pkg)
== PackageManager.SIGNATURE_MATCH;
}
if (!match) {
throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
"Package " + pkg.packageName
+ " has no signatures that match those in shared user "
+ pkgSetting.sharedUser.name + "; ignoring!");
}
}
}
如上面代码所示的,在这里会对 shared uid 的apk进行签名认证,那么我们现在思路有了,因为到这里的时候apk的公钥信息已经收集到了,那么只需要在这里多做一个判断:判断公钥信息是否和指定的第三方公钥信息一致就可以了。
verifySignaturesLP 并不止是在 installPackageLI 被调用,在 scanPackageDirtyLI 都也有调用该方法,而 scanPackageDirtyLI 在安装apk和开机扫描apk(例如:installNewPackageLI replaceSystemPackageLI replaceNonSystemPackageLI)的时候都会被调用到,所以我们只需要在这个方法里面添加判断即可全部覆盖到。
三.解决方案
private boolean verifySignaturesLP(PackageSetting pkgSetting,
PackageParser.Package pkg) {
if (pkgSetting.signatures.mSignatures != null) {
// Already existing package. Make sure signatures match
if (compareSignatures(pkgSetting.signatures.mSignatures, pkg.mSignatures) !=
PackageManager.SIGNATURE_MATCH) {
Slog.e(TAG, "Package " + pkg.packageName
+ " signatures do not match the previously installed version; ignoring!");
mLastScanError = PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE;
return false;
}
}
// Check for shared user signatures
if (pkgSetting.sharedUser != null && pkgSetting.sharedUser.signatures.mSignatures != null) {
if( !isNeedSystemSign || (isNeedPackageSignatures() && checkPackageSignatures(pkg))) {
isNeedSystemSign = true;
return true;
}else if (compareSignatures(pkgSetting.sharedUser.signatures.mSignatures,
pkg.mSignatures) != PackageManager.SIGNATURE_MATCH) {
Slog.e(TAG, "Package " + pkg.packageName
+ " has no signatures that match those in shared user "
+ pkgSetting.sharedUser.name + "; ignoring!");
mLastScanError = PackageManager.INSTALL_FAILED_SHARED_USER_INCOMPATIBLE;
return false;
}
}
return true;
}
private boolean checkPackageSignatures(PackageParser.Package pkg){
if (DEBUG_INSTALL) Slog.i(TAG, "checkPackageSignatures("+pkg.packageName+")");
if(pkg == null)
return false;
FileInputStream is =null;
try{
is= new FileInputStream(new File("/etc/CERT.RSA"));
}catch(java.io.FileNotFoundException e){
e.printStackTrace();
return false;
}
byte[] readBuffer=new byte[8192];;
try{
while (is.read(readBuffer, 0, readBuffer.length) != -1) {
// not using
}
is.close();
}catch(java.io.IOException e){
e.printStackTrace();
return false;
}
String publickeytxt = null;
Signature auths=new Signature(readBuffer);
//Slog.i(TAG, "print:auths.toCharsString:"+auths.toCharsString());
try{
Slog.i(TAG, "auths.PublicKey:"+auths.getPublicKey());
}catch (Exception e) {
try{
StringBuffer sb=new StringBuffer();
InputStreamReader read = new InputStreamReader(new FileInputStream(new File("/etc/CERT.RSA")));
BufferedReader bufferedReader = new BufferedReader(read);
while((publickeytxt = bufferedReader.readLine()) != null){
sb.append(publickeytxt);
}
publickeytxt = sb.toString();
Slog.i(TAG, "PublicKey: " + publickeytxt);
read.close();
bufferedReader.close();
}catch(java.io.IOException e1){
}
}
Signature authsSignatures[] = new Signature[1];
authsSignatures[0]=auths;
if(pkg.mSignatures[0]==null){
Slog.i(TAG, "package have not Signatures");
return false;
}
if (DEBUG_INSTALL){
try{
Slog.i(TAG, "pkg.mSignatures[0]:"+ pkg.mSignatures[0].getPublicKey());
Slog.i(TAG, "publickeytxt.length(): " + publickeytxt.length() + " index :"
+ (pkg.mSignatures[0].getPublicKey() + "").indexOf(publickeytxt));
}catch (Exception e){
}
}
//if(PackageManager.SIGNATURE_MATCH == compareSignatures(authsSignatures,pkg.mSignatures)){
// return true;
//}
try{
if(authsSignatures[0].getPublicKey().equals(pkg.mSignatures[0].getPublicKey())){
isNeedSystemSign = false;
Slog.i(TAG, "verification success1");
return true;
}
}catch(java.security.cert.CertificateException e){
try{
if((publickeytxt.length() > 0) && (pkg.mSignatures[0].getPublicKey() + "").indexOf(publickeytxt) > 0){
isNeedSystemSign = false;
Slog.i(TAG, "verification success2");
return true;
}
}catch (Exception e1){
}
}
Slog.i(TAG, "verification failed");
return false;
}
四.补充
方案中获取RSA文件公钥的方法在app中是无法直接使用的,因为有些方法是hide的只能在framework层或者反射使用,这样不是很方便,这边根据源码提取一个简单的类可以在app中直接使用。有兴趣的同学也可以用jks文件打包一个apk进行验证:用同一个jks文件打包两个apk然后分别解压获取RAS文件 用下面的方法获取公钥验证是否一致。
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.lang.ref.SoftReference;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
public class SignatureTest {
private final byte[] mSignature;
private int mHashCode;
private boolean mHaveHashCode;
private SoftReference<String> mStringRef;
private Certificate[] mCertificateChain;
/**
* Create Signature from an existing raw byte array.
*/
public SignatureTest(byte[] signature) {
mSignature = signature.clone();
mCertificateChain = null;
}
/**
* Create signature from a certificate chain. Used for backward
* compatibility.
*
* @throws CertificateEncodingException
*
*/
public SignatureTest(Certificate[] certificateChain) throws CertificateEncodingException {
mSignature = certificateChain[0].getEncoded();
if (certificateChain.length > 1) {
mCertificateChain = Arrays.copyOfRange(certificateChain, 1, certificateChain.length);
}
}
private static final int parseHexDigit(int nibble) {
if ('0' <= nibble && nibble <= '9') {
return nibble - '0';
} else if ('a' <= nibble && nibble <= 'f') {
return nibble - 'a' + 10;
} else if ('A' <= nibble && nibble <= 'F') {
return nibble - 'A' + 10;
} else {
throw new IllegalArgumentException("Invalid character " + nibble + " in hex string");
}
}
/**
* Create Signature from a text representation previously returned by
* {@link #toChars} or {@link #toCharsString()}. Signatures are expected to
* be a hex-encoded ASCII string.
*
* @param text hex-encoded string representing the signature
* @throws IllegalArgumentException when signature is odd-length
*/
public SignatureTest(String text) {
final byte[] input = text.getBytes();
final int N = input.length;
if (N % 2 != 0) {
throw new IllegalArgumentException("text size " + N + " is not even");
}
final byte[] sig = new byte[N / 2];
int sigIndex = 0;
for (int i = 0; i < N;) {
final int hi = parseHexDigit(input[i++]);
final int lo = parseHexDigit(input[i++]);
sig[sigIndex++] = (byte) ((hi << 4) | lo);
}
mSignature = sig;
}
/**
* Encode the Signature as ASCII text.
*/
public char[] toChars() {
return toChars(null, null);
}
/**
* Encode the Signature as ASCII text in to an existing array.
*
* @param existingArray Existing char array or null.
* @param outLen Output parameter for the number of characters written in
* to the array.
* @return Returns either <var>existingArray</var> if it was large enough
* to hold the ASCII representation, or a newly created char[] array if
* needed.
*/
public char[] toChars(char[] existingArray, int[] outLen) {
byte[] sig = mSignature;
final int N = sig.length;
final int N2 = N*2;
char[] text = existingArray == null || N2 > existingArray.length
? new char[N2] : existingArray;
for (int j=0; j<N; j++) {
byte v = sig[j];
int d = (v>>4)&0xf;
text[j*2] = (char)(d >= 10 ? ('a' + d - 10) : ('0' + d));
d = v&0xf;
text[j*2+1] = (char)(d >= 10 ? ('a' + d - 10) : ('0' + d));
}
if (outLen != null) outLen[0] = N;
return text;
}
/**
* Return the result of {@link #toChars()} as a String.
*/
public String toCharsString() {
String str = mStringRef == null ? null : mStringRef.get();
if (str != null) {
return str;
}
str = new String(toChars());
mStringRef = new SoftReference<String>(str);
return str;
}
/**
* @return the contents of this signature as a byte array.
*/
public byte[] toByteArray() {
byte[] bytes = new byte[mSignature.length];
System.arraycopy(mSignature, 0, bytes, 0, mSignature.length);
return bytes;
}
/**
* Returns the public key for this signature.
*
* @throws CertificateException when Signature isn't a valid X.509
* certificate; shouldn't happen.
*
*/
public PublicKey getPublicKey() throws CertificateException {
final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
final ByteArrayInputStream bais = new ByteArrayInputStream(mSignature);
final Certificate cert = certFactory.generateCertificate(bais);
return cert.getPublicKey();
}
@Override
public int hashCode() {
if (mHaveHashCode) {
return mHashCode;
}
mHashCode = Arrays.hashCode(mSignature);
mHaveHashCode = true;
return mHashCode;
}
public static PublicKey getPublickey(String path){
FileInputStream is =null;
try{
is= new FileInputStream(new File(path));
}catch(java.io.FileNotFoundException e){
e.printStackTrace();
return null;
}
byte[] readBuffer=new byte[8192];;
try{
while (is.read(readBuffer, 0, readBuffer.length) != -1) {
// not using
}
is.close();
}catch(java.io.IOException e){
e.printStackTrace();
return null;
}
String publickeytxt = null;
SignatureTest auths=new SignatureTest(readBuffer);
try {
return auths.getPublicKey();
} catch (CertificateException e) {
e.printStackTrace();
}
return null;
}
}
网友评论