Add clearotacerts Magisk module to intentionally make A/B OTAs fail

Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
This commit is contained in:
Andrew Gunnerson
2022-07-06 17:16:39 -04:00
parent e638463fdf
commit ba7109cc43
5 changed files with 115 additions and 1 deletions
+13 -1
View File
@@ -20,7 +20,7 @@ avbroot applies two patches to the boot images:
* Any operation that causes an unsigned or differently-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). This includes:
* Performing a regular (unpatched) A/B OTA update. Disabling the `Automatic system updates` option in Android's Developer Options is recommended.
* Performing a regular (unpatched) A/B OTA update.
* The `Direct install` method for updating Magisk. Magisk updates must be done by repatching as well.
@@ -125,6 +125,18 @@ To update Android or Magisk:
4. Reboot.
### Blocking A/B OTA Updates
Unpatched OTA updates are already blocked in recovery due to the replacing of the OTA signature verification certificates. To disable OTAs while booted into Android, turn off `Automatic system updates` in Android's Developer Options.
To intentionally make A/B OTAs fail while booted into Android (to prevent accidental manual updates), build the `clearotacerts` module:
```bash
python clearotacerts/build.py
```
and flash the `clearotacerts/dist/clearotacerts-<version>.zip` file in Magisk. The module simply overrides `/system/etc/security/otacerts.zip` at runtime with an empty zip so that even if an OTA is downloaded, signature verification will fail.
### Implementation Details
* avbroot relies on AOSP's avbtool, OTA utilities, and signapk. These are collections of applications that aren't meant to be used as libraries, but avbroot shoehorns them in anyway. Aside from signapk, these tools are not called via CLI because avbroot requires more control over the operations being performed than what is provided via the CLI interfaces. This "integration" is incredibly hacky and will likely require changes whenever the submodules are updated to point to newer AOSP commits.
@@ -0,0 +1,33 @@
#!/sbin/sh
#################
# Initialization
#################
umask 022
# echo before loading util_functions
ui_print() { echo "$1"; }
require_new_magisk() {
ui_print "*******************************"
ui_print " Please install Magisk v20.4+! "
ui_print "*******************************"
exit 1
}
#########################
# Load util_functions.sh
#########################
OUTFD=$2
ZIPFILE=$3
mount /data 2>/dev/null
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
. /data/adb/magisk/util_functions.sh
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
install_module
exit 0
@@ -0,0 +1 @@
#MAGISK
+62
View File
@@ -0,0 +1,62 @@
import io
import os
import shutil
import sys
import zipfile
def build_empty_zip():
stream = io.BytesIO()
with zipfile.ZipFile(stream, 'w') as z:
pass
return stream.getvalue()
def parse_props(raw_prop):
result = {}
for line in raw_prop.decode('UTF-8').splitlines():
k, delim, v = line.partition('=')
if not delim:
raise ArgumentError(f'Malformed line: {repr(line)}')
result[k.strip()] = v.strip()
return result
def main():
dist_dir = os.path.join(sys.path[0], 'dist')
os.makedirs(dist_dir, exist_ok=True)
module_prop_path = os.path.join(sys.path[0], 'module.prop')
with open(os.path.join(sys.path[0], 'module.prop'), 'rb') as f:
module_prop_raw = f.read()
module_prop = parse_props(module_prop_raw)
name = module_prop['name']
version = module_prop['version'].removeprefix('v')
zip_path = os.path.join(dist_dir, f'{name}-{version}.zip')
with zipfile.ZipFile(zip_path, 'w') as z:
for name, data in (
('META-INF/com/google/android/update-binary', None),
('META-INF/com/google/android/updater-script', None),
('module.prop', module_prop_raw),
('system/etc/security/otacerts.zip', build_empty_zip()),
):
# Build our own ZipInfo to ensure archive is reproducible
info = zipfile.ZipInfo(name)
with z.open(info, 'w') as f_out:
if data is not None:
f_out.write(data)
else:
with open(os.path.join(sys.path[0], name), 'rb') as f_in:
shutil.copyfileobj(f_in, f_out)
if __name__ == '__main__':
main()
+6
View File
@@ -0,0 +1,6 @@
id=com.chiller3.avbroot.clearotacerts
name=clearotacerts
version=v1.0
versionCode=1
author=chenxiaolong
description=Block A/B OTAs by clearing verification certificates