Compare commits

...

11 Commits

Author SHA1 Message Date
Janik Besendorf
b7ff933213 Rename from_folder to from_dir in tests 2025-10-31 13:44:58 +01:00
Janik Besendorf
802ab11deb Fix imports and remove duplicate hashes param 2025-10-31 13:29:16 +01:00
besendorf
8cb75d9517 Merge branch 'main' into feature/deduplicate-adb-aqf-modules 2025-10-31 12:11:17 +01:00
besendorf
7009cddc8c webkit session resource: fail gracefully when date conversion fails (#664)
* webkit session resource: fail gracefully when date conversion fails

* fix syntax
2025-10-23 15:19:08 +02:00
besendorf
9b4d10139c Add Options to disable update checks (#674)
* reduce update check timeouts to 5s

* add error hadnling for Update checks

* Add CLI flags to disable version and indicator checks

* ruff syntax fix

* fix tests
2025-10-23 15:13:36 +02:00
Donncha Ó Cearbhaill
67863d8f8d Merge branch 'main' into feature/deduplicate-adb-aqf-modules 2025-10-21 15:14:26 +02:00
Janik Besendorf
5bf71e2268 add missing import 2025-10-05 18:13:31 +02:00
besendorf
caaffc8988 Merge branch 'main' into feature/deduplicate-adb-aqf-modules 2025-10-05 18:04:01 +02:00
Donncha Ó Cearbhaill
4c1cdf5129 Raise the proper NoAndroidQFBackup exception when a back-up isn't found 2025-02-11 15:04:48 +01:00
Donncha Ó Cearbhaill
a08c24b02a Deduplicate modules which are run by the sub-commands. 2025-02-10 20:32:51 +01:00
Donncha Ó Cearbhaill
5d696350dc Run bugreport and backup modules during check-androidqf
Adding support to automatically run ADB backup and bugreport modules
automatically when running the check-androidqf command. This is a first
step to deduplicate the code for Android modules.
2025-02-10 19:28:20 +01:00
77 changed files with 591 additions and 1373 deletions

View File

@@ -31,6 +31,8 @@ from mvt.common.help import (
HELP_MSG_HASHES,
HELP_MSG_CHECK_IOCS,
HELP_MSG_STIX2,
HELP_MSG_DISABLE_UPDATE_CHECK,
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
)
from mvt.common.logo import logo
from mvt.common.updates import IndicatorsUpdates
@@ -53,12 +55,37 @@ log = logging.getLogger("mvt")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
def _get_disable_flags(ctx):
"""Helper function to safely get disable flags from context."""
if ctx.obj is None:
return False, False
return (
ctx.obj.get("disable_version_check", False),
ctx.obj.get("disable_indicator_check", False),
)
# ==============================================================================
# Main
# ==============================================================================
@click.group(invoke_without_command=False)
def cli():
logo()
@click.option(
"--disable-update-check", is_flag=True, help=HELP_MSG_DISABLE_UPDATE_CHECK
)
@click.option(
"--disable-indicator-update-check",
is_flag=True,
help=HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
)
@click.pass_context
def cli(ctx, disable_update_check, disable_indicator_update_check):
ctx.ensure_object(dict)
ctx.obj["disable_version_check"] = disable_update_check
ctx.obj["disable_indicator_check"] = disable_indicator_update_check
logo(
disable_version_check=disable_update_check,
disable_indicator_check=disable_indicator_update_check,
)
# ==============================================================================
@@ -166,6 +193,8 @@ def check_adb(
module_name=module,
serial=serial,
module_options=module_options,
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
if list_modules:
@@ -212,6 +241,8 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
ioc_files=iocs,
module_name=module,
hashes=True,
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
if list_modules:
@@ -274,6 +305,8 @@ def check_backup(
"interactive": not non_interactive,
"backup_password": cli_load_android_backup_password(log, backup_password),
},
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
if list_modules:
@@ -338,6 +371,8 @@ def check_androidqf(
"interactive": not non_interactive,
"backup_password": cli_load_android_backup_password(log, backup_password),
},
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
if list_modules:
@@ -372,7 +407,13 @@ def check_androidqf(
@click.argument("FOLDER", type=click.Path(exists=True))
@click.pass_context
def check_iocs(ctx, iocs, list_modules, module, folder):
cmd = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module)
cmd = CmdCheckIOCS(
target_path=folder,
ioc_files=iocs,
module_name=module,
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
cmd.modules = BACKUP_MODULES + ADB_MODULES + BUGREPORT_MODULES
if list_modules:

View File

@@ -7,6 +7,7 @@ import logging
from typing import Optional
from mvt.common.command import Command
from mvt.common.indicators import Indicators
from .modules.adb import ADB_MODULES
@@ -19,18 +20,28 @@ class CmdAndroidCheckADB(Command):
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-adb"

View File

@@ -9,59 +9,186 @@ import zipfile
from pathlib import Path
from typing import List, Optional
from mvt.android.cmd_check_backup import CmdAndroidCheckBackup
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
from mvt.common.command import Command
from mvt.common.indicators import Indicators
from .modules.androidqf import ANDROIDQF_MODULES
from .modules.androidqf.base import AndroidQFModule
log = logging.getLogger(__name__)
class NoAndroidQFTargetPath(Exception):
pass
class NoAndroidQFBugReport(Exception):
pass
class NoAndroidQFBackup(Exception):
pass
class CmdAndroidCheckAndroidQF(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-androidqf"
self.modules = ANDROIDQF_MODULES
self.format: Optional[str] = None
self.archive: Optional[zipfile.ZipFile] = None
self.files: List[str] = []
self.__format: Optional[str] = None
self.__zip: Optional[zipfile.ZipFile] = None
self.__files: List[str] = []
def init(self):
if os.path.isdir(self.target_path):
self.format = "dir"
self.__format = "dir"
parent_path = Path(self.target_path).absolute().parent.as_posix()
target_abs_path = os.path.abspath(self.target_path)
for root, subdirs, subfiles in os.walk(target_abs_path):
for fname in subfiles:
file_path = os.path.relpath(os.path.join(root, fname), parent_path)
self.files.append(file_path)
self.__files.append(file_path)
elif os.path.isfile(self.target_path):
self.format = "zip"
self.archive = zipfile.ZipFile(self.target_path)
self.files = self.archive.namelist()
self.__format = "zip"
self.__zip = zipfile.ZipFile(self.target_path)
self.__files = self.__zip.namelist()
def module_init(self, module):
if self.format == "zip":
module.from_zip_file(self.archive, self.files)
def module_init(self, module: AndroidQFModule) -> None: # type: ignore[override]
if self.__format == "zip" and self.__zip:
module.from_zip(self.__zip, self.__files)
return
if not self.target_path:
raise NoAndroidQFTargetPath
parent_path = Path(self.target_path).absolute().parent.as_posix()
module.from_dir(parent_path, self.__files)
def load_bugreport(self) -> zipfile.ZipFile:
bugreport_zip_path = None
for file_name in self.__files:
if file_name.endswith("bugreport.zip"):
bugreport_zip_path = file_name
break
else:
raise NoAndroidQFBugReport
if self.__format == "zip" and self.__zip:
handle = self.__zip.open(bugreport_zip_path)
return zipfile.ZipFile(handle)
if self.__format == "dir" and self.target_path:
parent_path = Path(self.target_path).absolute().parent.as_posix()
module.from_folder(parent_path, self.files)
bug_report_path = os.path.join(parent_path, bugreport_zip_path)
return zipfile.ZipFile(bug_report_path)
raise NoAndroidQFBugReport
def load_backup(self) -> bytes:
backup_ab_path = None
for file_name in self.__files:
if file_name.endswith("backup.ab"):
backup_ab_path = file_name
break
else:
raise NoAndroidQFBackup
if self.__format == "zip" and self.__zip:
backup_file_handle = self.__zip.open(backup_ab_path)
return backup_file_handle.read()
if self.__format == "dir" and self.target_path:
parent_path = Path(self.target_path).absolute().parent.as_posix()
backup_path = os.path.join(parent_path, backup_ab_path)
with open(backup_path, "rb") as backup_file:
backup_ab_data = backup_file.read()
return backup_ab_data
raise NoAndroidQFBackup
def run_bugreport_cmd(self) -> bool:
try:
bugreport = self.load_bugreport()
except NoAndroidQFBugReport:
self.log.warning(
"Skipping bugreport modules as no bugreport.zip found in AndroidQF data."
)
return False
else:
cmd = CmdAndroidCheckBugreport(
target_path=None,
results_path=self.results_path,
ioc_files=self.ioc_files,
iocs=self.iocs,
module_options=self.module_options,
hashes=self.hashes,
sub_command=True,
)
cmd.from_zip(bugreport)
cmd.run()
self.detected_count += cmd.detected_count
self.timeline.extend(cmd.timeline)
self.timeline_detected.extend(cmd.timeline_detected)
def run_backup_cmd(self) -> bool:
try:
backup = self.load_backup()
except NoAndroidQFBackup:
self.log.warning(
"Skipping backup modules as no backup.ab found in AndroidQF data."
)
return False
else:
cmd = CmdAndroidCheckBackup(
target_path=None,
results_path=self.results_path,
ioc_files=self.ioc_files,
iocs=self.iocs,
module_options=self.module_options,
hashes=self.hashes,
sub_command=True,
)
cmd.from_ab(backup)
cmd.run()
self.detected_count += cmd.detected_count
self.timeline.extend(cmd.timeline)
self.timeline_detected.extend(cmd.timeline_detected)
def finish(self) -> None:
"""
Run the bugreport and backup modules if the respective files are found in the AndroidQF data.
"""
self.run_bugreport_cmd()
self.run_backup_cmd()

View File

@@ -20,6 +20,7 @@ from mvt.android.parsers.backup import (
parse_backup_file,
)
from mvt.common.command import Command
from mvt.common.indicators import Indicators
from .modules.backup import BACKUP_MODULES
@@ -32,20 +33,28 @@ class CmdAndroidCheckBackup(Command):
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-backup"
@@ -55,6 +64,34 @@ class CmdAndroidCheckBackup(Command):
self.backup_archive: Optional[tarfile.TarFile] = None
self.backup_files: List[str] = []
def from_ab(self, ab_file_bytes: bytes) -> None:
self.backup_type = "ab"
header = parse_ab_header(ab_file_bytes)
if not header["backup"]:
log.critical("Invalid backup format, file should be in .ab format")
sys.exit(1)
password = None
if header["encryption"] != "none":
password = prompt_or_load_android_backup_password(log, self.module_options)
if not password:
log.critical("No backup password provided.")
sys.exit(1)
try:
tardata = parse_backup_file(ab_file_bytes, password=password)
except InvalidBackupPassword:
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as exc:
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
dbytes = io.BytesIO(tardata)
self.backup_archive = tarfile.open(fileobj=dbytes)
for member in self.backup_archive:
self.backup_files.append(member.name)
def init(self) -> None:
if not self.target_path:
return
@@ -62,35 +99,8 @@ class CmdAndroidCheckBackup(Command):
if os.path.isfile(self.target_path):
self.backup_type = "ab"
with open(self.target_path, "rb") as handle:
data = handle.read()
header = parse_ab_header(data)
if not header["backup"]:
log.critical("Invalid backup format, file should be in .ab format")
sys.exit(1)
password = None
if header["encryption"] != "none":
password = prompt_or_load_android_backup_password(
log, self.module_options
)
if not password:
log.critical("No backup password provided.")
sys.exit(1)
try:
tardata = parse_backup_file(data, password=password)
except InvalidBackupPassword:
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as exc:
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
dbytes = io.BytesIO(tardata)
self.backup_archive = tarfile.open(fileobj=dbytes)
for member in self.backup_archive:
self.backup_files.append(member.name)
ab_file_bytes = handle.read()
self.from_ab(ab_file_bytes)
elif os.path.isdir(self.target_path):
self.backup_type = "folder"
@@ -109,6 +119,6 @@ class CmdAndroidCheckBackup(Command):
def module_init(self, module: BackupExtraction) -> None: # type: ignore[override]
if self.backup_type == "folder":
module.from_folder(self.target_path, self.backup_files)
module.from_dir(self.target_path, self.backup_files)
else:
module.from_ab(self.target_path, self.backup_archive, self.backup_files)

View File

@@ -11,6 +11,7 @@ from zipfile import ZipFile
from mvt.android.modules.bugreport.base import BugReportModule
from mvt.common.command import Command
from mvt.common.indicators import Indicators
from .modules.bugreport import BUGREPORT_MODULES
@@ -23,54 +24,80 @@ class CmdAndroidCheckBugreport(Command):
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-bugreport"
self.modules = BUGREPORT_MODULES
self.bugreport_format: str = ""
self.bugreport_archive: Optional[ZipFile] = None
self.bugreport_files: List[str] = []
self.__format: str = ""
self.__zip: Optional[ZipFile] = None
self.__files: List[str] = []
def from_dir(self, dir_path: str) -> None:
"""This method is used to initialize the bug report analysis from an
uncompressed directory.
"""
self.__format = "dir"
self.target_path = dir_path
parent_path = Path(dir_path).absolute().as_posix()
for root, _, subfiles in os.walk(os.path.abspath(dir_path)):
for file_name in subfiles:
file_path = os.path.relpath(os.path.join(root, file_name), parent_path)
self.__files.append(file_path)
def from_zip(self, bugreport_zip: ZipFile) -> None:
"""This method is used to initialize the bug report analysis from a
compressed archive.
"""
# NOTE: This will be invoked either by the CLI directly,or by the
# check-androidqf command. We need this because we want to support
# check-androidqf to analyse compressed archives itself too.
# So, we'll need to extract bugreport.zip from a 'androidqf.zip', and
# since nothing is written on disk, we need to be able to pass this
# command a ZipFile instance in memory.
self.__format = "zip"
self.__zip = bugreport_zip
for file_name in self.__zip.namelist():
self.__files.append(file_name)
def init(self) -> None:
if not self.target_path:
return
if os.path.isfile(self.target_path):
self.bugreport_format = "zip"
self.bugreport_archive = ZipFile(self.target_path)
for file_name in self.bugreport_archive.namelist():
self.bugreport_files.append(file_name)
self.from_zip(ZipFile(self.target_path))
elif os.path.isdir(self.target_path):
self.bugreport_format = "dir"
parent_path = Path(self.target_path).absolute().as_posix()
for root, _, subfiles in os.walk(os.path.abspath(self.target_path)):
for file_name in subfiles:
file_path = os.path.relpath(
os.path.join(root, file_name), parent_path
)
self.bugreport_files.append(file_path)
self.from_dir(self.target_path)
def module_init(self, module: BugReportModule) -> None: # type: ignore[override]
if self.bugreport_format == "zip":
module.from_zip(self.bugreport_archive, self.bugreport_files)
if self.__format == "zip":
module.from_zip(self.__zip, self.__files)
else:
module.from_folder(self.target_path, self.bugreport_files)
module.from_dir(self.target_path, self.__files)
def finish(self) -> None:
if self.bugreport_archive:
self.bugreport_archive.close()
if self.__zip:
self.__zip.close()

View File

@@ -4,15 +4,7 @@
# https://license.mvt.re/1.1/
from .chrome_history import ChromeHistory
from .dumpsys_accessibility import DumpsysAccessibility
from .dumpsys_activities import DumpsysActivities
from .dumpsys_appops import DumpsysAppOps
from .dumpsys_battery_daily import DumpsysBatteryDaily
from .dumpsys_battery_history import DumpsysBatteryHistory
from .dumpsys_dbinfo import DumpsysDBInfo
from .dumpsys_adbstate import DumpsysADBState
from .dumpsys_full import DumpsysFull
from .dumpsys_receivers import DumpsysReceivers
from .files import Files
from .getprop import Getprop
from .logcat import Logcat
@@ -32,15 +24,7 @@ ADB_MODULES = [
Getprop,
Settings,
SELinuxStatus,
DumpsysBatteryHistory,
DumpsysBatteryDaily,
DumpsysReceivers,
DumpsysActivities,
DumpsysAccessibility,
DumpsysDBInfo,
DumpsysADBState,
DumpsysFull,
DumpsysAppOps,
Packages,
Logcat,
RootBinaries,

View File

@@ -1,49 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact
from .base import AndroidExtraction
class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidExtraction):
"""This module extracts stats on accessibility."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys accessibility")
self._adb_disconnect()
self.parse(output)
for result in self.results:
self.log.info(
'Found installed accessibility service "%s"', result.get("service")
)
self.log.info(
"Identified a total of %d accessibility services", len(self.results)
)

View File

@@ -1,45 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_package_activities import (
DumpsysPackageActivitiesArtifact,
)
from .base import AndroidExtraction
class DumpsysActivities(DumpsysPackageActivitiesArtifact, AndroidExtraction):
"""This module extracts details on receivers for risky activities."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else []
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys package")
self._adb_disconnect()
self.parse(output)
self.log.info("Extracted %d package activities", len(self.results))

View File

@@ -1,45 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact
from .base import AndroidExtraction
class DumpsysADBState(DumpsysADBArtifact, AndroidExtraction):
"""This module extracts ADB keystore state."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys adb", decode=False)
self._adb_disconnect()
self.parse(output)
if self.results:
self.log.info(
"Identified a total of %d trusted ADB keys",
len(self.results[0].get("user_keys", [])),
)

View File

@@ -1,46 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact
from .base import AndroidExtraction
class DumpsysAppOps(DumpsysAppopsArtifact, AndroidExtraction):
"""This module extracts records from App-op Manager."""
slug = "dumpsys_appops"
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys appops")
self._adb_disconnect()
self.parse(output)
self.log.info(
"Extracted a total of %d records from app-ops manager", len(self.results)
)

View File

@@ -1,44 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact
from .base import AndroidExtraction
class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, AndroidExtraction):
"""This module extracts records from battery daily updates."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys batterystats --daily")
self._adb_disconnect()
self.parse(output)
self.log.info(
"Extracted %d records from battery daily stats", len(self.results)
)

View File

@@ -1,42 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact
from .base import AndroidExtraction
class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, AndroidExtraction):
"""This module extracts records from battery history events."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys batterystats --history")
self._adb_disconnect()
self.parse(output)
self.log.info("Extracted %d records from battery history", len(self.results))

View File

@@ -1,47 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfoArtifact
from .base import AndroidExtraction
class DumpsysDBInfo(DumpsysDBInfoArtifact, AndroidExtraction):
"""This module extracts records from battery daily updates."""
slug = "dumpsys_dbinfo"
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys dbinfo")
self._adb_disconnect()
self.parse(output)
self.log.info(
"Extracted a total of %d records from database information",
len(self.results),
)

View File

@@ -1,44 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact
from .base import AndroidExtraction
class DumpsysReceivers(DumpsysReceiversArtifact, AndroidExtraction):
"""This module extracts details on receivers for risky activities."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else {}
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys package")
self.parse(output)
self._adb_disconnect()
self.log.info("Extracted receivers for %d intents", len(self.results))

View File

@@ -3,42 +3,22 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .dumpsys_accessibility import DumpsysAccessibility
from .dumpsys_activities import DumpsysActivities
from .dumpsys_appops import DumpsysAppops
from .dumpsys_battery_daily import DumpsysBatteryDaily
from .dumpsys_battery_history import DumpsysBatteryHistory
from .dumpsys_dbinfo import DumpsysDBInfo
from .dumpsys_packages import DumpsysPackages
from .dumpsys_receivers import DumpsysReceivers
from .dumpsys_adb import DumpsysADBState
from .getprop import Getprop
from .packages import Packages
from .dumpsys_platform_compat import DumpsysPlatformCompat
from .processes import Processes
from .settings import Settings
from .sms import SMS
from .files import Files
from .root_binaries import RootBinaries
from .aqf_files import AQFFiles
from .aqf_getprop import AQFGetProp
from .aqf_packages import AQFPackages
from .aqf_processes import AQFProcesses
from .aqf_settings import AQFSettings
from .mounts import Mounts
from .root_binaries import RootBinaries
from .sms import SMS
ANDROIDQF_MODULES = [
DumpsysActivities,
DumpsysReceivers,
DumpsysAccessibility,
DumpsysAppops,
DumpsysDBInfo,
DumpsysBatteryDaily,
DumpsysBatteryHistory,
DumpsysADBState,
Packages,
DumpsysPlatformCompat,
Processes,
Getprop,
Settings,
AQFPackages,
AQFProcesses,
AQFGetProp,
AQFSettings,
AQFFiles,
SMS,
DumpsysPackages,
Files,
RootBinaries,
Mounts,
]

View File

@@ -21,8 +21,13 @@ SUSPICIOUS_PATHS = [
]
class Files(AndroidQFModule):
"""This module analyse list of files"""
class AQFFiles(AndroidQFModule):
"""
This module analyzes the files.json dump generated by AndroidQF.
The format needs to be kept in sync with the AndroidQF module code.
https://github.com/mvt-project/androidqf/blob/main/android-collector/cmd/find.go#L28
"""
def __init__(
self,

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
from .base import AndroidQFModule
class Getprop(GetPropArtifact, AndroidQFModule):
class AQFGetProp(GetPropArtifact, AndroidQFModule):
"""This module extracts data from get properties."""
def __init__(

View File

@@ -13,10 +13,10 @@ from .base import AndroidQFModule
from mvt.android.artifacts.file_timestamps import FileTimestampsArtifact
class LogsFileTimestamps(FileTimestampsArtifact, AndroidQFModule):
"""This module extracts records from battery daily updates."""
class AQFLogTimestamps(FileTimestampsArtifact, AndroidQFModule):
"""This module creates timeline for log files extracted by AQF."""
slug = "logfile_timestamps"
slug = "aqf_log_timestamps"
def __init__(
self,

View File

@@ -19,7 +19,7 @@ from mvt.android.utils import (
from .base import AndroidQFModule
class Packages(AndroidQFModule):
class AQFPackages(AndroidQFModule):
"""This module examines the installed packages in packages.json"""
def __init__(

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.processes import Processes as ProcessesArtifact
from .base import AndroidQFModule
class Processes(ProcessesArtifact, AndroidQFModule):
class AQFProcesses(ProcessesArtifact, AndroidQFModule):
"""This module analyse running processes"""
def __init__(

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.settings import Settings as SettingsArtifact
from .base import AndroidQFModule
class Settings(SettingsArtifact, AndroidQFModule):
class AQFSettings(SettingsArtifact, AndroidQFModule):
"""This module analyse setting files"""
def __init__(

View File

@@ -37,11 +37,11 @@ class AndroidQFModule(MVTModule):
self.files: List[str] = []
self.archive: Optional[zipfile.ZipFile] = None
def from_folder(self, parent_path: str, files: List[str]):
def from_dir(self, parent_path: str, files: List[str]) -> None:
self.parent_path = parent_path
self.files = files
def from_zip_file(self, archive: zipfile.ZipFile, files: List[str]):
def from_zip(self, archive: zipfile.ZipFile, files: List[str]) -> None:
self.archive = archive
self.files = files

View File

@@ -1,51 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact
from .base import AndroidQFModule
class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidQFModule):
"""This module analyses dumpsys accessibility"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace")
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE accessibility:")
self.parse(content)
for result in self.results:
self.log.info(
'Found installed accessibility service "%s"', result.get("service")
)
self.log.info(
"Identified a total of %d accessibility services", len(self.results)
)

View File

@@ -1,50 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_package_activities import (
DumpsysPackageActivitiesArtifact,
)
from .base import AndroidQFModule
class DumpsysActivities(DumpsysPackageActivitiesArtifact, AndroidQFModule):
"""This module extracts details on receivers for risky activities."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else []
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
# Get data and extract the dumpsys section
data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace")
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:")
# Parse it
self.parse(content)
self.log.info("Extracted %d package activities", len(self.results))

View File

@@ -1,51 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact
from .base import AndroidQFModule
class DumpsysADBState(DumpsysADBArtifact, AndroidQFModule):
"""This module extracts ADB keystore state."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
full_dumpsys = self._get_file_content(dumpsys_file[0])
content = self.extract_dumpsys_section(
full_dumpsys,
b"DUMP OF SERVICE adb:",
binary=True,
)
self.parse(content)
if self.results:
self.log.info(
"Identified a total of %d trusted ADB keys",
len(self.results[0].get("user_keys", [])),
)

View File

@@ -1,46 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact
from .base import AndroidQFModule
class DumpsysAppops(DumpsysAppopsArtifact, AndroidQFModule):
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
# Extract section
data = self._get_file_content(dumpsys_file[0])
section = self.extract_dumpsys_section(
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE appops:"
)
# Parse it
self.parse(section)
self.log.info("Identified %d applications in AppOps Manager", len(self.results))

View File

@@ -1,46 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact
from .base import AndroidQFModule
class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, AndroidQFModule):
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
# Extract section
data = self._get_file_content(dumpsys_file[0])
section = self.extract_dumpsys_section(
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE batterystats:"
)
# Parse it
self.parse(section)
self.log.info("Extracted a total of %d battery daily stats", len(self.results))

View File

@@ -1,46 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact
from .base import AndroidQFModule
class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, AndroidQFModule):
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
# Extract section
data = self._get_file_content(dumpsys_file[0])
section = self.extract_dumpsys_section(
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE batterystats:"
)
# Parse it
self.parse(section)
self.log.info("Extracted a total of %d battery daily stats", len(self.results))

View File

@@ -1,46 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfoArtifact
from .base import AndroidQFModule
class DumpsysDBInfo(DumpsysDBInfoArtifact, AndroidQFModule):
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
# Extract dumpsys DBInfo section
data = self._get_file_content(dumpsys_file[0])
section = self.extract_dumpsys_section(
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE dbinfo:"
)
# Parse it
self.parse(section)
self.log.info("Identified %d DB Info entries", len(self.results))

View File

@@ -1,62 +0,0 @@
# 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/
import logging
from typing import Any, Dict, List, Optional
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
from mvt.android.modules.adb.packages import (
DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
)
from .base import AndroidQFModule
class DumpsysPackages(DumpsysPackagesArtifact, AndroidQFModule):
"""This module analyse dumpsys packages"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[List[Dict[str, Any]]] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if len(dumpsys_file) != 1:
self.log.info("Dumpsys file not found")
return
data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace")
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:")
self.parse(content)
for result in self.results:
dangerous_permissions_count = 0
for perm in result["permissions"]:
if perm["name"] in DANGEROUS_PERMISSIONS:
dangerous_permissions_count += 1
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
self.log.info(
'Found package "%s" requested %d potentially dangerous permissions',
result["package_name"],
dangerous_permissions_count,
)
self.log.info("Extracted details on %d packages", len(self.results))

View File

@@ -1,44 +0,0 @@
# 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/
import logging
from typing import Optional
from mvt.android.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatArtifact
from .base import AndroidQFModule
class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, AndroidQFModule):
"""This module extracts details on uninstalled apps."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace")
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE platform_compat:")
self.parse(content)
self.log.info("Found %d uninstalled apps", len(self.results))

View File

@@ -1,49 +0,0 @@
# 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/
import logging
from typing import Any, Dict, List, Optional, Union
from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact
from .base import AndroidQFModule
class DumpsysReceivers(DumpsysReceiversArtifact, AndroidQFModule):
"""This module analyse dumpsys receivers"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Union[List[Any], Dict[str, Any], None] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.results = results if results else {}
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
data = self._get_file_content(dumpsys_file[0])
dumpsys_section = self.extract_dumpsys_section(
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE package:"
)
self.parse(dumpsys_section)
self.log.info("Extracted receivers for %d intents", len(self.results))

View File

@@ -19,7 +19,13 @@ from .base import AndroidQFModule
class SMS(AndroidQFModule):
"""This module analyse SMS file in backup"""
"""
This module analyse SMS file in backup
XXX: We should also de-duplicate this AQF module, but first we
need to add tests for loading encrypted SMS backups through the backup
sub-module.
"""
def __init__(
self,

View File

@@ -37,10 +37,7 @@ class BackupExtraction(MVTModule):
self.tar = None
self.files = []
def from_folder(self, backup_path: Optional[str], files: List[str]) -> None:
"""
Get all the files and list them
"""
def from_dir(self, backup_path: Optional[str], files: List[str]) -> None:
self.backup_path = backup_path
self.files = files
@@ -58,14 +55,16 @@ class BackupExtraction(MVTModule):
return fnmatch.filter(self.files, pattern)
def _get_file_content(self, file_path: str) -> bytes:
if self.ab:
if self.tar:
try:
member = self.tar.getmember(file_path)
except KeyError:
return None
handle = self.tar.extractfile(member)
else:
elif self.backup_path:
handle = open(os.path.join(self.backup_path, file_path), "rb")
else:
raise ValueError("No backup path or tar file provided")
data = handle.read()
handle.close()

View File

@@ -50,13 +50,13 @@ class SMS(BackupExtraction):
def run(self) -> None:
sms_path = "apps/com.android.providers.telephony/d_f/*_sms_backup"
for file in self._get_files_by_pattern(sms_path):
self.log.info("Processing SMS backup file at %s", file)
self.log.debug("Processing SMS backup file at %s", file)
data = self._get_file_content(file)
self.results.extend(parse_sms_file(data))
mms_path = "apps/com.android.providers.telephony/d_f/*_mms_backup"
for file in self._get_files_by_pattern(mms_path):
self.log.info("Processing MMS backup file at %s", file)
self.log.debug("Processing MMS backup file at %s", file)
data = self._get_file_content(file)
self.results.extend(parse_sms_file(data))

View File

@@ -3,31 +3,31 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .accessibility import Accessibility
from .activities import Activities
from .appops import Appops
from .battery_daily import BatteryDaily
from .battery_history import BatteryHistory
from .dbinfo import DBInfo
from .getprop import Getprop
from .packages import Packages
from .platform_compat import PlatformCompat
from .receivers import Receivers
from .adb_state import DumpsysADBState
from .dumpsys_accessibility import DumpsysAccessibility
from .dumpsys_activities import DumpsysActivities
from .dumpsys_appops import DumpsysAppops
from .dumpsys_battery_daily import DumpsysBatteryDaily
from .dumpsys_battery_history import DumpsysBatteryHistory
from .dumpsys_dbinfo import DumpsysDBInfo
from .dumpsys_getprop import DumpsysGetProp
from .dumpsys_packages import DumpsysPackages
from .dumpsys_platform_compat import DumpsysPlatformCompat
from .dumpsys_receivers import DumpsysReceivers
from .dumpsys_adb_state import DumpsysADBState
from .fs_timestamps import BugReportTimestamps
from .tombstones import Tombstones
BUGREPORT_MODULES = [
Accessibility,
Activities,
Appops,
BatteryDaily,
BatteryHistory,
DBInfo,
Getprop,
Packages,
PlatformCompat,
Receivers,
DumpsysAccessibility,
DumpsysActivities,
DumpsysAppops,
DumpsysBatteryDaily,
DumpsysBatteryHistory,
DumpsysDBInfo,
DumpsysGetProp,
DumpsysPackages,
DumpsysPlatformCompat,
DumpsysReceivers,
DumpsysADBState,
BugReportTimestamps,
Tombstones,

View File

@@ -39,9 +39,7 @@ class BugReportModule(MVTModule):
self.extract_files: List[str] = []
self.zip_files: List[str] = []
def from_folder(
self, extract_path: Optional[str], extract_files: List[str]
) -> None:
def from_dir(self, extract_path: str, extract_files: List[str]) -> None:
self.extract_path = extract_path
self.extract_files = extract_files

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArti
from .base import BugReportModule
class Accessibility(DumpsysAccessibilityArtifact, BugReportModule):
class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule):
"""This module extracts stats on accessibility."""
def __init__(

View File

@@ -13,7 +13,7 @@ from mvt.android.artifacts.dumpsys_package_activities import (
from .base import BugReportModule
class Activities(DumpsysPackageActivitiesArtifact, BugReportModule):
class DumpsysActivities(DumpsysPackageActivitiesArtifact, BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact
from .base import BugReportModule
class Appops(DumpsysAppopsArtifact, BugReportModule):
class DumpsysAppops(DumpsysAppopsArtifact, BugReportModule):
"""This module extracts information on package from App-Ops Manager."""
def __init__(

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtif
from .base import BugReportModule
class BatteryDaily(DumpsysBatteryDailyArtifact, BugReportModule):
class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, BugReportModule):
"""This module extracts records from battery daily updates."""
def __init__(

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryA
from .base import BugReportModule
class BatteryHistory(DumpsysBatteryHistoryArtifact, BugReportModule):
class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, BugReportModule):
"""This module extracts records from battery daily updates."""
def __init__(

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfoArtifact
from .base import BugReportModule
class DBInfo(DumpsysDBInfoArtifact, BugReportModule):
class DumpsysDBInfo(DumpsysDBInfoArtifact, BugReportModule):
"""This module extracts records from battery daily updates."""
slug = "dbinfo"

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
from .base import BugReportModule
class Getprop(GetPropArtifact, BugReportModule):
class DumpsysGetProp(GetPropArtifact, BugReportModule):
"""This module extracts device properties from getprop command."""
def __init__(

View File

@@ -12,7 +12,7 @@ from mvt.android.utils import DANGEROUS_PERMISSIONS, DANGEROUS_PERMISSIONS_THRES
from .base import BugReportModule
class Packages(DumpsysPackagesArtifact, BugReportModule):
class DumpsysPackages(DumpsysPackagesArtifact, BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatA
from mvt.android.modules.bugreport.base import BugReportModule
class PlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule):
class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule):
"""This module extracts details on uninstalled apps."""
def __init__(

View File

@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact
from .base import BugReportModule
class Receivers(DumpsysReceiversArtifact, BugReportModule):
class DumpsysReceivers(DumpsysReceiversArtifact, BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(

View File

@@ -22,6 +22,10 @@ class CmdCheckIOCS(Command):
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
super().__init__(
target_path=target_path,
@@ -30,7 +34,11 @@ class CmdCheckIOCS(Command):
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-iocs"

View File

@@ -27,11 +27,15 @@ class Command:
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
log: logging.Logger = logging.getLogger(__name__),
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
self.name = ""
self.modules = []
@@ -42,6 +46,9 @@ class Command:
self.module_name = module_name
self.serial = serial
self.log = log
self.sub_command = sub_command
self.disable_version_check = disable_version_check
self.disable_indicator_check = disable_indicator_check
# This dictionary can contain options that will be passed down from
# the Command to all modules. This can for example be used to pass
@@ -60,8 +67,12 @@ class Command:
# Load IOCs
self._create_storage()
self._setup_logging()
self.iocs = Indicators(log=log)
self.iocs.load_indicators_files(self.ioc_files)
if iocs is not None:
self.iocs = iocs
else:
self.iocs = Indicators(self.log)
self.iocs.load_indicators_files(self.ioc_files)
def _create_storage(self) -> None:
if self.results_path and not os.path.exists(self.results_path):
@@ -247,6 +258,10 @@ class Command:
except NotImplementedError:
pass
# We only store the timeline from the parent/main command
if self.sub_command:
return
self._store_timeline()
self._store_info()

View File

@@ -15,6 +15,8 @@ HELP_MSG_HASHES = "Generate hashes of all the files analyzed"
HELP_MSG_VERBOSE = "Verbose mode"
HELP_MSG_CHECK_IOCS = "Compare stored JSON results to provided indicators"
HELP_MSG_STIX2 = "Download public STIX2 indicators"
HELP_MSG_DISABLE_UPDATE_CHECK = "Disable MVT version update check"
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK = "Disable indicators update check"
# IOS Specific
HELP_MSG_DECRYPT_BACKUP = "Decrypt an encrypted iTunes backup"

View File

@@ -12,74 +12,85 @@ from .updates import IndicatorsUpdates, MVTUpdates
from .version import MVT_VERSION
def check_updates() -> None:
def check_updates(
disable_version_check: bool = False, disable_indicator_check: bool = False
) -> None:
log = logging.getLogger("mvt")
# First we check for MVT version updates.
try:
mvt_updates = MVTUpdates()
latest_version = mvt_updates.check()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
rich_print(
"\t\t[bold]Note: Could not check for MVT updates.[/bold] "
"You may be working offline. Please update MVT regularly."
)
except Exception as e:
log.error("Error encountered when trying to check latest MVT version: %s", e)
else:
if latest_version:
if not disable_version_check:
try:
mvt_updates = MVTUpdates()
latest_version = mvt_updates.check()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
rich_print(
f"\t\t[bold]Version {latest_version} is available! "
"Upgrade mvt with `pip3 install -U mvt` or with `pipx upgrade mvt`[/bold]"
"\t\t[bold]Note: Could not check for MVT updates.[/bold] "
"You may be working offline. Please update MVT regularly."
)
# Then we check for indicators files updates.
ioc_updates = IndicatorsUpdates()
# Before proceeding, we check if we have downloaded an indicators index.
# If not, there's no point in proceeding with the updates check.
if ioc_updates.get_latest_update() == 0:
rich_print(
"\t\t[bold]You have not yet downloaded any indicators, check "
"the `download-iocs` command![/bold]"
)
return
# We only perform this check at a fixed frequency, in order to not
# overburden the user with too many lookups if the command is being run
# multiple times.
should_check, hours = ioc_updates.should_check()
if not should_check:
rich_print(
f"\t\tIndicators updates checked recently, next automatic check "
f"in {int(hours)} hours"
)
return
try:
ioc_to_update = ioc_updates.check()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
rich_print(
"\t\t[bold]Note: Could not check for indicator updates.[/bold] "
"You may be working offline. Please update MVT indicators regularly."
)
except Exception as e:
log.error("Error encountered when trying to check latest MVT indicators: %s", e)
else:
if ioc_to_update:
rich_print(
"\t\t[bold]There are updates to your indicators files! "
"Run the `download-iocs` command to update![/bold]"
except Exception as e:
log.error(
"Error encountered when trying to check latest MVT version: %s", e
)
else:
rich_print("\t\tYour indicators files seem to be up to date.")
if latest_version:
rich_print(
f"\t\t[bold]Version {latest_version} is available! "
"Upgrade mvt with `pip3 install -U mvt` or with `pipx upgrade mvt`[/bold]"
)
# Then we check for indicators files updates.
if not disable_indicator_check:
ioc_updates = IndicatorsUpdates()
# Before proceeding, we check if we have downloaded an indicators index.
# If not, there's no point in proceeding with the updates check.
if ioc_updates.get_latest_update() == 0:
rich_print(
"\t\t[bold]You have not yet downloaded any indicators, check "
"the `download-iocs` command![/bold]"
)
return
# We only perform this check at a fixed frequency, in order to not
# overburden the user with too many lookups if the command is being run
# multiple times.
should_check, hours = ioc_updates.should_check()
if not should_check:
rich_print(
f"\t\tIndicators updates checked recently, next automatic check "
f"in {int(hours)} hours"
)
return
try:
ioc_to_update = ioc_updates.check()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
rich_print(
"\t\t[bold]Note: Could not check for indicator updates.[/bold] "
"You may be working offline. Please update MVT indicators regularly."
)
except Exception as e:
log.error(
"Error encountered when trying to check latest MVT indicators: %s", e
)
else:
if ioc_to_update:
rich_print(
"\t\t[bold]There are updates to your indicators files! "
"Run the `download-iocs` command to update![/bold]"
)
else:
rich_print("\t\tYour indicators files seem to be up to date.")
def logo() -> None:
def logo(
disable_version_check: bool = False, disable_indicator_check: bool = False
) -> None:
rich_print("\n")
rich_print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
rich_print("\t\thttps://mvt.re")
rich_print(f"\t\tVersion: {MVT_VERSION}")
check_updates()
check_updates(disable_version_check, disable_indicator_check)
rich_print("\n")

View File

@@ -24,7 +24,11 @@ INDICATORS_CHECK_FREQUENCY = 12
class MVTUpdates:
def check(self) -> str:
res = requests.get(settings.PYPI_UPDATE_URL, timeout=15)
try:
res = requests.get(settings.PYPI_UPDATE_URL, timeout=5)
except requests.exceptions.RequestException as e:
log.error("Failed to check for updates, skipping updates: %s", e)
return ""
data = res.json()
latest_version = data.get("info", {}).get("version", "")
@@ -93,7 +97,12 @@ class IndicatorsUpdates:
url = self.github_raw_url.format(
self.index_owner, self.index_repo, self.index_branch, self.index_path
)
res = requests.get(url, timeout=15)
try:
res = requests.get(url, timeout=5)
except requests.exceptions.RequestException as e:
log.error("Failed to retrieve indicators index from %s: %s", url, e)
return None
if res.status_code != 200:
log.error(
"Failed to retrieve indicators index located at %s (error %d)",
@@ -105,7 +114,12 @@ class IndicatorsUpdates:
return yaml.safe_load(res.content)
def download_remote_ioc(self, ioc_url: str) -> Optional[str]:
res = requests.get(ioc_url, timeout=15)
try:
res = requests.get(ioc_url, timeout=15)
except requests.exceptions.RequestException as e:
log.error("Failed to download indicators file from %s: %s", ioc_url, e)
return None
if res.status_code != 200:
log.error(
"Failed to download indicators file from %s (error %d)",
@@ -171,7 +185,12 @@ class IndicatorsUpdates:
file_commit_url = (
f"https://api.github.com/repos/{owner}/{repo}/commits?path={path}"
)
res = requests.get(file_commit_url, timeout=15)
try:
res = requests.get(file_commit_url, timeout=5)
except requests.exceptions.RequestException as e:
log.error("Failed to get details about file %s: %s", file_commit_url, e)
return -1
if res.status_code != 200:
log.error(
"Failed to get details about file %s (error %d)",

View File

@@ -37,6 +37,8 @@ from mvt.common.help import (
HELP_MSG_CHECK_IOCS,
HELP_MSG_STIX2,
HELP_MSG_CHECK_IOS_BACKUP,
HELP_MSG_DISABLE_UPDATE_CHECK,
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
)
from .cmd_check_backup import CmdIOSCheckBackup
from .cmd_check_fs import CmdIOSCheckFS
@@ -53,12 +55,37 @@ MVT_IOS_BACKUP_PASSWORD = "MVT_IOS_BACKUP_PASSWORD"
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
def _get_disable_flags(ctx):
"""Helper function to safely get disable flags from context."""
if ctx.obj is None:
return False, False
return (
ctx.obj.get("disable_version_check", False),
ctx.obj.get("disable_indicator_check", False),
)
# ==============================================================================
# Main
# ==============================================================================
@click.group(invoke_without_command=False)
def cli():
logo()
@click.option(
"--disable-update-check", is_flag=True, help=HELP_MSG_DISABLE_UPDATE_CHECK
)
@click.option(
"--disable-indicator-update-check",
is_flag=True,
help=HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
)
@click.pass_context
def cli(ctx, disable_update_check, disable_indicator_update_check):
ctx.ensure_object(dict)
ctx.obj["disable_version_check"] = disable_update_check
ctx.obj["disable_indicator_check"] = disable_indicator_update_check
logo(
disable_version_check=disable_update_check,
disable_indicator_check=disable_indicator_update_check,
)
# ==============================================================================
@@ -219,6 +246,8 @@ def check_backup(
module_name=module,
module_options=module_options,
hashes=hashes,
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
if list_modules:
@@ -266,6 +295,8 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dum
module_name=module,
module_options=module_options,
hashes=hashes,
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
if list_modules:
@@ -300,7 +331,13 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dum
@click.argument("FOLDER", type=click.Path(exists=True))
@click.pass_context
def check_iocs(ctx, iocs, list_modules, module, folder):
cmd = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module)
cmd = CmdCheckIOCS(
target_path=folder,
ioc_files=iocs,
module_name=module,
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
cmd.modules = BACKUP_MODULES + FS_MODULES + MIXED_MODULES
if list_modules:

View File

@@ -7,6 +7,7 @@ import logging
from typing import Optional
from mvt.common.command import Command
from mvt.common.indicators import Indicators
from .modules.backup import BACKUP_MODULES
from .modules.mixed import MIXED_MODULES
@@ -20,20 +21,28 @@ class CmdIOSCheckBackup(Command):
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
sub_command: bool = False,
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-backup"

View File

@@ -7,6 +7,7 @@ import logging
from typing import Optional
from mvt.common.command import Command
from mvt.common.indicators import Indicators
from .modules.fs import FS_MODULES
from .modules.mixed import MIXED_MODULES
@@ -20,20 +21,27 @@ class CmdIOSCheckFS(Command):
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: bool = False,
sub_command: bool = False,
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-fs"

View File

@@ -127,6 +127,24 @@ class WebkitSessionResourceLog(IOSExtraction):
browsing_stats = file_plist["browsingStatistics"]
for item in browsing_stats:
most_recent_interaction, last_seen = None, None
if "mostRecentUserInteraction" in item:
try:
most_recent_interaction = convert_datetime_to_iso(
item["mostRecentUserInteraction"]
)
except Exception:
self.log.error(
f'Error converting date of Safari resource"most recent interaction": {item["mostRecentUserInteraction"]}'
)
if "lastSeen" in item:
try:
last_seen = convert_datetime_to_iso(item["lastSeen"])
except Exception:
self.log.error(
f'Error converting date of Safari resource"last seen": {item["lastSeen"]}'
)
items.append(
{
"origin": item.get("PrevalentResourceOrigin", ""),
@@ -139,10 +157,8 @@ class WebkitSessionResourceLog(IOSExtraction):
"subresourceUnderTopFrameOrigins", ""
),
"user_interaction": item.get("hadUserInteraction"),
"most_recent_interaction": convert_datetime_to_iso(
item["mostRecentUserInteraction"]
),
"last_seen": convert_datetime_to_iso(item["lastSeen"]),
"most_recent_interaction": most_recent_interaction,
"last_seen": last_seen,
}
)

View File

@@ -22,7 +22,7 @@ class TestBackupModule:
for root, subdirs, subfiles in os.walk(os.path.abspath(backup_path)):
for fname in subfiles:
files.append(os.path.relpath(os.path.join(root, fname), backup_path))
mod.from_folder(backup_path, files)
mod.from_dir(backup_path, files)
run_module(mod)
assert len(mod.results) == 2
assert len(mod.results[0]["links"]) == 1

View File

@@ -1,27 +0,0 @@
# 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_adb import DumpsysADBState
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysADBModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysADBState(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 1
assert len(m.detected) == 0
adb_statedump = m.results[0]
assert "user_keys" in adb_statedump
assert len(adb_statedump["user_keys"]) == 1

View File

@@ -1,24 +0,0 @@
# 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_battery_daily import DumpsysBatteryDaily
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysBatteryDailyModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysBatteryDaily(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 3
assert len(m.timeline) == 3
assert len(m.detected) == 0

View File

@@ -1,24 +0,0 @@
# 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_battery_history import DumpsysBatteryHistory
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysBatteryHistoryModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysBatteryHistory(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 6
assert len(m.timeline) == 0
assert len(m.detected) == 0

View File

@@ -1,24 +0,0 @@
# 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_dbinfo import DumpsysDBInfo
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysDBInfoModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysDBInfo(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 6
assert len(m.timeline) == 0
assert len(m.detected) == 0

View File

@@ -1,23 +0,0 @@
# 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_platform_compat import DumpsysPlatformCompat
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysPlatformCompatModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysPlatformCompat(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 2
assert len(m.detected) == 0

View File

@@ -1,23 +0,0 @@
# 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_accessibility import DumpsysAccessibility
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysAccessibilityModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysAccessibility(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 4
assert len(m.detected) == 0

View File

@@ -1,29 +0,0 @@
# 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_appops import DumpsysAppops
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysAppOpsModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysAppops(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 12
assert len(m.timeline) == 16
detected_by_ioc = [
detected for detected in m.detected if detected.get("matched_indicator")
]
assert len(m.detected) == 1
assert len(detected_by_ioc) == 0

View File

@@ -1,46 +0,0 @@
# 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/
import logging
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_packages import DumpsysPackages
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysPackagesModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysPackages(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 2
assert len(m.detected) == 0
assert len(m.timeline) == 6
assert (
m.results[0]["package_name"]
== "com.samsung.android.provider.filterprovider"
)
def test_detection_pkgname(self, indicator_file):
data_path = get_android_androidqf()
m = DumpsysPackages(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)
ind.ioc_collections[0]["app_ids"].append("com.sec.android.app.DataCreate")
m.indicators = ind
run_module(m)
assert len(m.results) == 2
assert len(m.detected) == 1
assert len(m.timeline) == 6
assert m.detected[0]["package_name"] == "com.sec.android.app.DataCreate"

View File

@@ -1,23 +0,0 @@
# 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_receivers import DumpsysReceivers
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysReceiversModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysReceivers(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 4
assert len(m.detected) == 0

View File

@@ -6,7 +6,7 @@
import logging
from pathlib import Path
from mvt.android.modules.androidqf.files import Files
from mvt.android.modules.androidqf.aqf_files import AQFFiles
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
@@ -15,10 +15,10 @@ from ..utils import get_android_androidqf, list_files
class TestAndroidqfFilesAnalysis:
def test_androidqf_files(self):
data_path = get_android_androidqf()
m = Files(target_path=data_path, log=logging)
m = AQFFiles(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 3
assert len(m.timeline) == 6

View File

@@ -7,7 +7,7 @@ import logging
import zipfile
from pathlib import Path
from mvt.android.modules.androidqf.getprop import Getprop
from mvt.android.modules.androidqf.aqf_getprop import AQFGetProp
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
@@ -17,10 +17,10 @@ from ..utils import get_android_androidqf, get_artifact, list_files
class TestAndroidqfGetpropAnalysis:
def test_androidqf_getprop(self):
data_path = get_android_androidqf()
m = Getprop(target_path=data_path, log=logging)
m = AQFGetProp(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 10
assert m.results[0]["name"] == "dalvik.vm.appimageformat"
@@ -30,9 +30,9 @@ class TestAndroidqfGetpropAnalysis:
def test_getprop_parsing_zip(self):
fpath = get_artifact("androidqf.zip")
m = Getprop(target_path=fpath, log=logging)
m = AQFGetProp(target_path=fpath, log=logging)
archive = zipfile.ZipFile(fpath)
m.from_zip_file(archive, archive.namelist())
m.from_zip(archive, archive.namelist())
run_module(m)
assert len(m.results) == 10
assert m.results[0]["name"] == "dalvik.vm.appimageformat"
@@ -42,10 +42,10 @@ class TestAndroidqfGetpropAnalysis:
def test_androidqf_getprop_detection(self, indicator_file):
data_path = get_android_androidqf()
m = Getprop(target_path=data_path, log=logging)
m = AQFGetProp(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)
ind.ioc_collections[0]["android_property_names"].append("dalvik.vm.heapmaxfree")

View File

@@ -85,7 +85,7 @@ class TestAndroidqfMountsModule:
m = Mounts(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)

View File

@@ -8,7 +8,7 @@ from pathlib import Path
import pytest
from mvt.android.modules.androidqf.packages import Packages
from mvt.android.modules.androidqf.aqf_packages import AQFPackages
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
@@ -31,8 +31,8 @@ def file_list(data_path):
@pytest.fixture()
def module(parent_data_path, file_list):
m = Packages(target_path=parent_data_path, log=logging)
m.from_folder(parent_data_path, file_list)
m = AQFPackages(target_path=parent_data_path, log=logging)
m.from_dir(parent_data_path, file_list)
return m

View File

@@ -6,7 +6,7 @@
import logging
from pathlib import Path
from mvt.android.modules.androidqf.processes import Processes
from mvt.android.modules.androidqf.aqf_processes import AQFProcesses
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
@@ -15,10 +15,10 @@ from ..utils import get_android_androidqf, list_files
class TestAndroidqfProcessesAnalysis:
def test_androidqf_processes(self):
data_path = get_android_androidqf()
m = Processes(target_path=data_path, log=logging)
m = AQFProcesses(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 15
assert len(m.timeline) == 0

View File

@@ -32,7 +32,7 @@ def file_list(data_path):
@pytest.fixture()
def module(parent_data_path, file_list):
m = RootBinaries(target_path=parent_data_path, log=logging)
m.from_folder(parent_data_path, file_list)
m.from_dir(parent_data_path, file_list)
return m
@@ -108,7 +108,7 @@ class TestAndroidqfRootBinaries:
# Test behavior when no root_binaries.json file is present
empty_file_list = []
m = RootBinaries(target_path=parent_data_path, log=logging)
m.from_folder(parent_data_path, empty_file_list)
m.from_dir(parent_data_path, empty_file_list)
run_module(m)

View File

@@ -5,7 +5,7 @@
from pathlib import Path
from mvt.android.modules.androidqf.settings import Settings
from mvt.android.modules.androidqf.aqf_settings import AQFSettings
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
@@ -14,10 +14,10 @@ from ..utils import get_android_androidqf, list_files
class TestSettingsModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = Settings(target_path=data_path)
m = AQFSettings(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 1
assert "random" in m.results.keys()

View File

@@ -21,7 +21,7 @@ class TestAndroidqfSMSAnalysis:
m = SMS(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 2
assert len(m.timeline) == 0
@@ -36,7 +36,7 @@ class TestAndroidqfSMSAnalysis:
)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 1
@@ -52,7 +52,7 @@ class TestAndroidqfSMSAnalysis:
)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert prompt_mock.call_count == 1
assert len(m.results) == 1
@@ -67,7 +67,7 @@ class TestAndroidqfSMSAnalysis:
)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 0
assert "Invalid backup password" in caplog.text
@@ -82,7 +82,7 @@ class TestAndroidqfSMSAnalysis:
)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 0
assert (

View File

@@ -6,9 +6,9 @@
import os
from pathlib import Path
from mvt.android.modules.bugreport.appops import Appops
from mvt.android.modules.bugreport.getprop import Getprop
from mvt.android.modules.bugreport.packages import Packages
from mvt.android.modules.bugreport.dumpsys_appops import DumpsysAppops
from mvt.android.modules.bugreport.dumpsys_getprop import DumpsysGetProp
from mvt.android.modules.bugreport.dumpsys_packages import DumpsysPackages
from mvt.android.modules.bugreport.tombstones import Tombstones
from mvt.common.module import run_module
@@ -26,12 +26,12 @@ class TestBugreportAnalysis:
folder_files.append(
os.path.relpath(os.path.join(root, file_name), parent_path)
)
m.from_folder(fpath, folder_files)
m.from_dir(fpath, folder_files)
run_module(m)
return m
def test_appops_module(self):
m = self.launch_bug_report_module(Appops)
m = self.launch_bug_report_module(DumpsysAppops)
assert len(m.results) == 12
assert len(m.timeline) == 16
@@ -42,7 +42,7 @@ class TestBugreportAnalysis:
assert len(detected_by_ioc) == 0
def test_packages_module(self):
m = self.launch_bug_report_module(Packages)
m = self.launch_bug_report_module(DumpsysPackages)
assert len(m.results) == 2
assert (
m.results[0]["package_name"]
@@ -53,7 +53,7 @@ class TestBugreportAnalysis:
assert len(m.results[1]["permissions"]) == 32
def test_getprop_module(self):
m = self.launch_bug_report_module(Getprop)
m = self.launch_bug_report_module(DumpsysGetProp)
assert len(m.results) == 0
def test_tombstones_modules(self):

View File

@@ -32,7 +32,8 @@ class TestCheckAndroidqfCommand:
path = os.path.join(get_artifact_folder(), "androidqf_encrypted")
result = runner.invoke(check_androidqf, [path])
assert prompt_mock.call_count == 1
# Called twice, once in AnroidQF SMS module and once in Backup SMS module
assert prompt_mock.call_count == 2
assert result.exit_code == 0
def test_check_encrypted_backup_cli(self, mocker):