diff --git a/src/mvt/android/artifacts/tombstone_crashes.py b/src/mvt/android/artifacts/tombstone_crashes.py index bcfb389..1b6e115 100644 --- a/src/mvt/android/artifacts/tombstone_crashes.py +++ b/src/mvt/android/artifacts/tombstone_crashes.py @@ -191,9 +191,14 @@ class TombstoneCrashArtifact(AndroidArtifact): def _load_key_value_line( self, line: str, key: str, destination_key: str, tombstone: dict ) -> bool: - line_key, value = line.split(":", 1) - if line_key != key: - raise ValueError(f"Expected key {key}, got {line_key}") + # The caller matched the key as a bare prefix, so a longer word starting + # with it arrives here: `Caused by: …` inside an abort message reaches + # the `Cause` key. That is a different line, not a broken file — say so + # by declining it, and let the remaining keys have their turn. Raising + # here discarded the whole tombstone, crash and stack trace included. + line_key, separator, value = line.partition(":") + if not separator or line_key != key: + return False value_clean = value.strip().strip("'") if destination_key == "uid": diff --git a/src/mvt/android/cmd_check_androidqf.py b/src/mvt/android/cmd_check_androidqf.py index ffa3e13..99ac5a3 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -292,9 +292,7 @@ class CmdAndroidCheckAndroidQF(Command): try: cmd.from_ab(backup) except InvalidAndroidBackup as exc: - self.log.warning( - "Skipping backup modules as backup.ab is malformed: %s", exc - ) + self.log.warning("Skipping backup modules: %s", exc) return False cmd.run() diff --git a/src/mvt/android/cmd_check_backup.py b/src/mvt/android/cmd_check_backup.py index b75bb34..94be4ee 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -87,11 +87,15 @@ class CmdAndroidCheckBackup(Command): if header["encryption"] != "none": password = prompt_or_load_android_backup_password(log, self.module_options) if not password: + if self.sub_command: + raise InvalidAndroidBackup("No backup password provided") log.critical("No backup password provided.") sys.exit(1) try: tardata = parse_backup_file(ab_file_bytes, password=password) except InvalidBackupPassword: + if self.sub_command: + raise InvalidAndroidBackup("Invalid backup password") log.critical("Invalid backup password") sys.exit(1) except AndroidBackupParsingError as exc: diff --git a/tests/android/test_artifact_tombstone_caused_by.py b/tests/android/test_artifact_tombstone_caused_by.py new file mode 100644 index 0000000..9a2df85 --- /dev/null +++ b/tests/android/test_artifact_tombstone_caused_by.py @@ -0,0 +1,54 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 The MVT Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ +"""A `Caused by:` line must not discard the whole text tombstone. + +Keys are matched as bare prefixes, so `Caused by: …` — an ordinary line inside +an abort message — reached the `Cause` key, failed the key comparison and +raised, which `Tombstones.run()` logged while dropping the entire crash record. +Seen on a 1.6 MB tombstone whose protobuf twin was zero bytes: the crash then +had no representation at all. +""" + +import datetime + +from mvt.android.artifacts.tombstone_crashes import TombstoneCrashArtifact + +TOMBSTONE = b"""\ +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +Build fingerprint: 'Xiaomi/vili_eea/vili:13/TKQ1.220829.002/V14.0.10.0:user/release-keys' +Revision: '0' +ABI: 'arm64' +Timestamp: 2023-08-24 14:54:47.999124034+0300 +Process uptime: 12199s +Cmdline: com.example.game +pid: 8044, tid: 26222, name: UnityMain >>> com.example.game <<< +uid: 10235 +signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr -------- +Abort message: 'No pending exception expected: java.lang.SecurityException: listen + at void android.os.Parcel.readException() (Parcel.java:2920) +Caused by: android.os.RemoteException: Remote stack trace: +\tat com.android.server.TelephonyRegistry.listen(TelephonyRegistry.java:1096) +""" + +WITH_CAUSE = TOMBSTONE + b"Cause: null pointer dereference\n" + + +class TestTombstoneCausedBy: + def _parse(self, content): + artifact = TombstoneCrashArtifact() + artifact.results = [] + artifact.parse("tombstone_23", datetime.datetime(2023, 8, 24), content) + return artifact.results + + def test_caused_by_line_does_not_discard_the_tombstone(self): + results = self._parse(TOMBSTONE) + assert len(results) == 1 + assert results[0]["pid"] == 8044 + assert results[0]["process_name"] == "UnityMain" + assert results[0]["uid"] == 10235 + + def test_the_real_cause_key_is_still_parsed(self): + results = self._parse(WITH_CAUSE) + assert results[0]["cause"] == "null pointer dereference" diff --git a/tests/android/test_check_backup_optional_failure.py b/tests/android/test_check_backup_optional_failure.py new file mode 100644 index 0000000..72dae42 --- /dev/null +++ b/tests/android/test_check_backup_optional_failure.py @@ -0,0 +1,39 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 The MVT Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ +"""An encrypted backup.ab must not take the whole check-androidqf run with it. + +`CmdAndroidCheckBackup.from_ab()` already raises `InvalidAndroidBackup` instead +of exiting when it runs as a sub-command (`check-androidqf` catches that and +skips the backup modules), for a wrong file format and for a parse error. The +password branches used to call `sys.exit(1)` unconditionally, which ends the +parent run inside `finish()` — before the intrusion-logs command and before the +timeline, alerts, urls, info and run-manifest are written. +""" + +import pytest + +from mvt.android.cmd_check_backup import CmdAndroidCheckBackup, InvalidAndroidBackup + +ENCRYPTED_AB_HEADER = b"ANDROID BACKUP\n5\n0\nAES-256\n" + b"\x00" * 64 + + +class TestCheckBackupOptionalFailure: + def _cmd(self, tmp_path, sub_command): + return CmdAndroidCheckBackup( + target_path=None, + results_path=str(tmp_path), + module_options={"interactive": False}, + sub_command=sub_command, + ) + + def test_missing_password_raises_when_nested(self, tmp_path): + cmd = self._cmd(tmp_path, sub_command=True) + with pytest.raises(InvalidAndroidBackup): + cmd.from_ab(ENCRYPTED_AB_HEADER) + + def test_missing_password_still_exits_on_its_own_command(self, tmp_path): + cmd = self._cmd(tmp_path, sub_command=False) + with pytest.raises(SystemExit): + cmd.from_ab(ENCRYPTED_AB_HEADER) diff --git a/tests/test_check_android_androidqf.py b/tests/test_check_android_androidqf.py index 2253a50..4e6ef98 100644 --- a/tests/test_check_android_androidqf.py +++ b/tests/test_check_android_androidqf.py @@ -155,7 +155,7 @@ class TestCheckAndroidqfCommand: result = runner.invoke(check_androidqf, [str(path)]) assert result.exit_code == 0 - assert "Skipping backup modules as backup.ab is malformed" in caplog.text + assert "Skipping backup modules: Invalid backup format" in caplog.text assert not any( record.levelname in {"CRITICAL", "FATAL"} for record in caplog.records )