Compare commits

...
Author SHA1 Message Date
Janik Besendorf 7e1992a04b Handle malformed AndroidQF backups 2026-06-23 16:58:40 +02:00
6 changed files with 73 additions and 13 deletions

No files matched your search

+9 -2
View File
@@ -13,7 +13,7 @@ from typing import List, Optional
from mvt.android.artifacts.getprop import GetProp from mvt.android.artifacts.getprop import GetProp
from mvt.android.cmd_check_intrusion_logs import CmdAndroidCheckIntrusionLogs from mvt.android.cmd_check_intrusion_logs import CmdAndroidCheckIntrusionLogs
from mvt.android.cmd_check_backup import CmdAndroidCheckBackup from mvt.android.cmd_check_backup import CmdAndroidCheckBackup, InvalidAndroidBackup
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
from mvt.common.command import Command from mvt.common.command import Command
from mvt.common.indicators import Indicators from mvt.common.indicators import Indicators
@@ -240,7 +240,14 @@ class CmdAndroidCheckAndroidQF(Command):
hashes=self.hashes, hashes=self.hashes,
sub_command=True, sub_command=True,
) )
cmd.from_ab(backup) try:
cmd.from_ab(backup)
except InvalidAndroidBackup as exc:
self.log.warning(
"Skipping backup modules as backup.ab is malformed: %s", exc
)
return False
cmd.run() cmd.run()
self.timeline.extend(cmd.timeline) self.timeline.extend(cmd.timeline)
+22 -1
View File
@@ -27,6 +27,10 @@ from .modules.backup import BACKUP_MODULES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class InvalidAndroidBackup(Exception):
pass
class CmdAndroidCheckBackup(Command): class CmdAndroidCheckBackup(Command):
def __init__( def __init__(
self, self,
@@ -68,6 +72,10 @@ class CmdAndroidCheckBackup(Command):
self.__type = "ab" self.__type = "ab"
header = parse_ab_header(ab_file_bytes) header = parse_ab_header(ab_file_bytes)
if not header["backup"]: if not header["backup"]:
if self.sub_command:
raise InvalidAndroidBackup(
"Invalid backup format, file should be in .ab format"
)
log.critical("Invalid backup format, file should be in .ab format") log.critical("Invalid backup format, file should be in .ab format")
sys.exit(1) sys.exit(1)
@@ -83,12 +91,25 @@ class CmdAndroidCheckBackup(Command):
log.critical("Invalid backup password") log.critical("Invalid backup password")
sys.exit(1) sys.exit(1)
except AndroidBackupParsingError as exc: except AndroidBackupParsingError as exc:
if self.sub_command:
raise InvalidAndroidBackup(
f"Impossible to parse this backup file: {exc}"
) from exc
log.critical("Impossible to parse this backup file: %s", exc) log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead") log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1) sys.exit(1)
dbytes = io.BytesIO(tardata) dbytes = io.BytesIO(tardata)
self.__tar = tarfile.open(fileobj=dbytes) try:
self.__tar = tarfile.open(fileobj=dbytes)
except tarfile.TarError as exc:
if self.sub_command:
raise InvalidAndroidBackup(
f"Impossible to parse this backup file: {exc}"
) from exc
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
for member in self.__tar: for member in self.__tar:
self.__files.append(member.name) self.__files.append(member.name)
+2 -2
View File
@@ -58,7 +58,7 @@ class SMS(AndroidQFModule):
def parse_backup(self, data): def parse_backup(self, data):
header = parse_ab_header(data) header = parse_ab_header(data)
if not header["backup"]: if not header["backup"]:
self.log.critical("Invalid backup format, backup.ab was not analysed") self.log.warning("Invalid backup format, backup.ab was not analysed")
return return
password = None password = None
@@ -76,7 +76,7 @@ class SMS(AndroidQFModule):
self.log.critical("Invalid backup password") self.log.critical("Invalid backup password")
return return
except AndroidBackupParsingError: except AndroidBackupParsingError:
self.log.critical( self.log.warning(
"Impossible to parse this backup file, please use" "Impossible to parse this backup file, please use"
" Android Backup Extractor instead" " Android Backup Extractor instead"
) )
+10 -7
View File
@@ -48,13 +48,16 @@ def parse_ab_header(data):
'encryption': "none", 'version': 4} 'encryption': "none", 'version': 4}
""" """
if data.startswith(b"ANDROID BACKUP"): if data.startswith(b"ANDROID BACKUP"):
[_, version, is_compressed, encryption, _] = data.split(b"\n", 4) try:
return { [_, version, is_compressed, encryption, _] = data.split(b"\n", 4)
"backup": True, return {
"compression": (is_compressed == b"1"), "backup": True,
"version": int(version), "compression": (is_compressed == b"1"),
"encryption": encryption.decode("utf-8"), "version": int(version),
} "encryption": encryption.decode("utf-8"),
}
except (UnicodeDecodeError, ValueError):
pass
return {"backup": False, "compression": None, "version": None, "encryption": None} return {"backup": False, "compression": None, "version": None, "encryption": None}
+13 -1
View File
@@ -5,12 +5,24 @@
import hashlib import hashlib
from mvt.android.parsers.backup import parse_backup_file, parse_tar_for_sms from mvt.android.parsers.backup import (
parse_ab_header,
parse_backup_file,
parse_tar_for_sms,
)
from ..utils import get_artifact from ..utils import get_artifact
class TestBackupParsing: class TestBackupParsing:
def test_parse_incomplete_header(self):
assert parse_ab_header(b"ANDROID BACKUP\n") == {
"backup": False,
"compression": None,
"version": None,
"encryption": None,
}
def test_parsing_noencryption(self): def test_parsing_noencryption(self):
file = get_artifact("android_backup/backup.ab") file = get_artifact("android_backup/backup.ab")
with open(file, "rb") as f: with open(file, "rb") as f:
+17
View File
@@ -3,7 +3,9 @@
# Use of this software is governed by the MVT License 1.1 that can be found at # Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/ # https://license.mvt.re/1.1/
import logging
import os import os
import shutil
from click.testing import CliRunner from click.testing import CliRunner
@@ -68,3 +70,18 @@ class TestCheckAndroidqfCommand:
assert result.exit_code == 0 assert result.exit_code == 0
del os.environ["MVT_ANDROID_BACKUP_PASSWORD"] del os.environ["MVT_ANDROID_BACKUP_PASSWORD"]
settings.__init__() # Reset settings settings.__init__() # Reset settings
def test_check_malformed_backup_skips_backup_modules(self, tmp_path, caplog):
path = tmp_path / "androidqf"
shutil.copytree(os.path.join(get_artifact_folder(), "androidqf"), path)
(path / "backup.ab").write_bytes(b"")
runner = CliRunner()
with caplog.at_level(logging.WARNING):
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 not any(
record.levelname in {"CRITICAL", "FATAL"} for record in caplog.records
)