为 Android 7.1.2 增加跳过安装包 JAR 完整性验证的功能

0x00

此举是为了满足一些特定需求,例如在不修改 apk 签名的情况下修改其内部的文件,并成功安装覆盖原 app。

核心修改

核心是 frameworks/base/core/java/android/util/jar/StrictJarVerifier.java 文件,有两处验证。

1
2
3
4
5
6
7
void verify() {
- byte[] d = digest.digest();
- if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
- throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
- }
verifiedEntries.put(name, certChains);
}
  • 第二处是 第 451 行 验证整个 manifest 清单的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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();
StrictJarManifest.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);
- }
}
}

OK,删除这几行代码就可以跳过 apk 在安装时对 manifest 清单列表上的文件的校验。

增加设置

但是现在问题来了,这么一改,任何时候安装 apk 都会跳过对文件的验证,所以我们接下来给系统的 “设置” app 增加一个开关来控制这个功能。

project packages/apps/Settings/

  • 首先增加语言资源
1
2
3
4
5
6
7
8
9
10
11
12
--- a/res/values/cm_strings.xml
+++ b/res/values/cm_strings.xml
@@ -42,6 +42,9 @@
<string name="root_appops_title">Manage root accesses</string>
<string name="root_appops_summary">View and control the root rules</string>

+ <string name="settings_nojar_title">Disable JAR file verification</string>
+ <string name="settings_nojar_summary">Skip the certificate digest verification</string>
+
<!-- About phone screen, LineageOS version -->
<string name="mod_version">LineageOS version</string>
<string name="mod_version_default">Unknown</string>
  • 在 “开发者选项” 页面中增加一个开关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
--- a/res/xml/development_prefs.xml
+++ b/res/xml/development_prefs.xml
@@ -126,6 +126,11 @@
android:title="@string/root_appops_title"
android:summary="@string/root_appops_summary" />

+ <SwitchPreference
+ android:key="settings_nojar"
+ android:title="@string/settings_nojar_title"
+ android:summary="@string/settings_nojar_summary" />
+
<SwitchPreference
android:key="update_recovery"
android:title="@string/update_recovery_title"
  • 接下来修改 “开发者选项” 页面的逻辑代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
--- a/src/com/android/settings/DevelopmentSettings.java
+++ b/src/com/android/settings/DevelopmentSettings.java
@@ -215,6 +215,9 @@

private static final String ROOT_APPOPS_KEY = "root_appops";

+ private static final String SETTINGS_NOJAR_KEY = "settings_nojar";
+ private static final String SETTINGS_NOJAR_PROPERTY = "persist.sys.settings_nojar";
+
private static final String UPDATE_RECOVERY_PROPERTY = "persist.sys.recovery_update";

private static final String IMMEDIATELY_DESTROY_ACTIVITIES_KEY
@@ -328,6 +331,8 @@

private Preference mRootAppops;

+ private SwitchPreference mSettingsNoJar;
+
private SwitchPreference mForceResizable;

private SwitchPreference mColorTemperaturePreference;
@@ -560,6 +565,8 @@
mRootAppops = (Preference) findPreference(ROOT_APPOPS_KEY);
mRootAppops.setOnPreferenceClickListener(this);

+ mSettingsNoJar = (SwitchPreference) findPreference(SETTINGS_NOJAR_KEY);
+
if (!removeRootOptionsIfRequired()) {
if (FileUtils.fileExists("/system/xbin/su")) {
mRootAccess.setEntries(R.array.root_access_entries);
@@ -812,6 +819,7 @@
}
updateBluetoothDisableAbsVolumeOptions();
updateRootAccessOptions();
+ updateSettingsNoJarOptions();
updateAdbOverNetwork();
updateUpdateRecoveryOptions();
}
@@ -936,6 +944,15 @@
}
}

+ private void updateSettingsNoJarOptions() {
+ updateSwitchPreference(mSettingsNoJar,
+ SystemProperties.getBoolean(SETTINGS_NOJAR_PROPERTY, false));
+ }
+
+ private void writeSettingsNoJarOptions() {
+ SystemProperties.set(SETTINGS_NOJAR_PROPERTY, mSettingsNoJar.isChecked() ? "true" : "false");
+ }
+
public static boolean isRootForAppsEnabled() {
int value = SystemProperties.getInt(ROOT_ACCESS_PROPERTY, 0);
boolean daemonState =
@@ -2287,6 +2304,8 @@
}
mUpdateRecoveryDialog.setOnDismissListener(this);
}
+ } else if (preference == mSettingsNoJar) {
+ writeSettingsNoJarOptions();
} else {
return super.onPreferenceTreeClick(preference);
}

project frameworks/base/

  • StrictJarVerifier.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
--- a/core/java/android/util/jar/StrictJarVerifier.java
+++ b/core/java/android/util/jar/StrictJarVerifier.java
@@ -37,6 +37,7 @@
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
+import android.os.SystemProperties;
import android.util.ArraySet;
import android.util.apk.ApkSignatureSchemeV2Verifier;
import libcore.io.Base64;
@@ -69,6 +70,9 @@
"SHA-256",
"SHA1",
};

+ private static final String SETTINGS_NOJAR_PROPERTY = "persist.sys.settings_nojar";
+
+ private final boolean noVerify;
private final String jarName;
private final StrictJarManifest manifest;
@@ -101,13 +105,17 @@

private final Hashtable<String, Certificate[][]> verifiedEntries;

+ private final boolean noVerify;
+
VerifierEntry(String name, MessageDigest digest, byte[] hash,
- Certificate[][] certChains, Hashtable<String, Certificate[][]> verifedEntries) {
+ Certificate[][] certChains, Hashtable<String, Certificate[][]> verifedEntries,
+ boolean noVerify) {
this.name = name;
this.digest = digest;
this.hash = hash;
this.certChains = certChains;
this.verifiedEntries = verifedEntries;
+ this.noVerify = noVerify;
}

/**
@@ -139,7 +147,7 @@
*/
void verify() {
byte[] d = digest.digest();
- if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
+ if (!noVerify && !MessageDigest.isEqual(d, Base64.decode(hash))) {
throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
}
verifiedEntries.put(name, certChains);
@@ -180,6 +188,7 @@
this.mainAttributesEnd = manifest.getMainAttributesEnd();
this.signatureSchemeRollbackProtectionsEnforced =
signatureSchemeRollbackProtectionsEnforced;
+ this.noVerify = SystemProperties.getBoolean(SETTINGS_NOJAR_PROPERTY, false);
}

/**
@@ -239,7 +248,7 @@

try {
return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
- certChainsArray, verifiedEntries);
+ certChainsArray, verifiedEntries, noVerify);
} catch (NoSuchAlgorithmException ignored) {
}
}
@@ -448,7 +457,7 @@
if (chunk == null) {
return;
}
- if (!verify(entry.getValue(), "-Digest", manifestBytes,
+ if (!noVerify && !verify(entry.getValue(), "-Digest", manifestBytes,
chunk.start, chunk.end, createdBySigntool, false)) {
throw invalidDigest(signatureFile, entry.getKey(), jarName);
}

例行效果