mirror of
https://github.com/mvt-project/mvt.git
synced 2026-10-24 08:08:25 +02:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9a57f5d10 | ||
|
|
ee24121a73 | ||
|
|
b06b5c36ac | ||
|
|
00d892d354 | ||
|
|
47ac8a5a85 | ||
|
|
0b5b3f2d7c | ||
|
|
85adb02eb9 | ||
|
|
91da901741 | ||
|
|
dcfd500112 | ||
|
|
a78894aaa5 | ||
|
|
24645cb718 | ||
|
|
104ffb167f | ||
|
|
097766a63b | ||
|
|
65b8114469 | ||
|
|
874f75bb4c | ||
|
|
3c8a581fd0 |
No files matched your search
@@ -73,11 +73,7 @@ rather than in MVT's own `config.yaml`.
|
||||
|
||||
### Commands on `mvt`
|
||||
|
||||
The `mvt` command hosts what belongs to neither platform: `version`,
|
||||
`completion`, `plugins` and `download-iocs`. A plugin command which is not
|
||||
about the acquisition of one platform, such as one which configures the plugin
|
||||
or synchronizes the indicators it uses, belongs there too, in the
|
||||
`mvt.cli_plugins` group:
|
||||
A MVT plugin command can also add sub-commands to the base `mvt` command. This can be used for commands which are not tied to a particular forensic platform:
|
||||
|
||||
```toml
|
||||
[project.entry-points."mvt.cli_plugins"]
|
||||
|
||||
@@ -78,7 +78,9 @@ MVT logs a warning. The nine pairs are:
|
||||
|
||||
`check-iocs` re-checks stored results rather than an acquisition. It matches
|
||||
every `<slug>.json` file in the results folder to the module with that slug.
|
||||
It then runs that module's `check_indicators()` again.
|
||||
It then runs that module's `check_indicators()` again. A module which
|
||||
implements `check_indicators()` is included in `check-iocs` for its platform.
|
||||
It does not need to declare the `check-iocs` pair.
|
||||
|
||||
### Writing a module
|
||||
|
||||
@@ -222,12 +224,14 @@ keep running and replace nothing. Every applied substitution is logged, so it
|
||||
is recorded in `command.log` when the command runs with an `--output` folder.
|
||||
|
||||
Every command resolves replacements on its own. `check-iocs` matches stored
|
||||
results files against the slugs of the modules available for that command, so a
|
||||
replacement checks the indicators of its own results only if it also declares
|
||||
the `("ios", "check-iocs")` pair; otherwise the built-in module it replaced
|
||||
re-checks the file. It also matches `--module` on the class name only, so pass
|
||||
a differently named replacement's own name there, not the name of the module
|
||||
it replaces.
|
||||
results files against the slugs of the modules available for that command. A
|
||||
replacement which subclasses the module it replaces inherits its
|
||||
`check_indicators()`. It is then part of `check-iocs` for its platform and
|
||||
re-checks the results file named after its slug. A replacement with no
|
||||
`check_indicators()` is not part of `check-iocs`. The built-in module it
|
||||
replaced re-checks the file. `check-iocs` matches `--module` on the class name
|
||||
only. Pass a differently named replacement's own name there, not the name of
|
||||
the module it replaces.
|
||||
|
||||
### Importing from MVT
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
+2
-4
@@ -47,10 +47,8 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
def cli(ctx, disable_update_check, disable_indicator_update_check, verbose):
|
||||
"""Mobile Verification Toolkit.
|
||||
|
||||
mvt-ios and mvt-android run the forensic analysis of an acquisition: each
|
||||
provides the check-* commands of its platform. This command hosts what
|
||||
belongs to neither platform; run it without a command to see the installed
|
||||
version and the list of what it offers.
|
||||
The 'mvt-ios' and 'mvt-android' CLI commands are used to perform
|
||||
forensic analysis on IOS and Android devices.
|
||||
"""
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["disable_version_check"] = disable_update_check
|
||||
|
||||
@@ -402,4 +402,14 @@ def module_supports_command(
|
||||
)
|
||||
return False
|
||||
|
||||
return (platform, command) in {tuple(entry) for entry in supported_commands}
|
||||
pairs = {tuple(entry) for entry in supported_commands}
|
||||
if (platform, command) in pairs:
|
||||
return True
|
||||
|
||||
# A module which implements check_indicators() is re-checked by check-iocs
|
||||
# for its platform. It does not need to declare the check-iocs pair.
|
||||
return (
|
||||
command == "check-iocs"
|
||||
and platform in {entry[0] for entry in pairs if entry}
|
||||
and module_class.check_indicators is not MVTModule.check_indicators
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -15,6 +15,7 @@ from mvt.common.cmd_check_iocs import CmdCheckIOCS
|
||||
from mvt.common.module import MVTModule
|
||||
from mvt.ios.cli import cli as ios_cli
|
||||
from mvt.ios.command_modules import IOS_CHECK_IOCS_MODULES
|
||||
from mvt.ios.modules.backup.manifest import Manifest
|
||||
|
||||
# Keep the banner of the group callback from checking for updates online.
|
||||
OFFLINE = ["--disable-update-check", "--disable-indicator-update-check"]
|
||||
@@ -39,34 +40,69 @@ class CustomResultsModule(MVTModule):
|
||||
self.checked.append(list(self.results))
|
||||
|
||||
|
||||
class BackupCheckerModule(MVTModule):
|
||||
"""An iOS module which implements check_indicators() without declaring check-iocs."""
|
||||
|
||||
slug = "backup_checker"
|
||||
supported_commands = (("ios", "check-backup"),)
|
||||
|
||||
checked: list = []
|
||||
|
||||
def run(self) -> None:
|
||||
pass
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
self.checked.append(list(self.results))
|
||||
|
||||
|
||||
class BugReportCheckerModule(MVTModule):
|
||||
"""The same for Android."""
|
||||
|
||||
slug = "bugreport_checker"
|
||||
supported_commands = (("android", "check-bugreport"),)
|
||||
|
||||
checked: list = []
|
||||
|
||||
def run(self) -> None:
|
||||
pass
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
self.checked.append(list(self.results))
|
||||
|
||||
|
||||
class BackupOnlyModule(MVTModule):
|
||||
"""A custom module which does not declare check-iocs."""
|
||||
"""A custom module which does not implement check_indicators()."""
|
||||
|
||||
slug = "backup_only"
|
||||
supported_commands = (("ios", "check-backup"),)
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
raise AssertionError("must not be re-checked")
|
||||
def run(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"platform, builtin_modules",
|
||||
[("ios", IOS_CHECK_IOCS_MODULES), ("android", ANDROID_CHECK_IOCS_MODULES)],
|
||||
"platform, builtin_modules, checker_module",
|
||||
[
|
||||
("ios", IOS_CHECK_IOCS_MODULES, BackupCheckerModule),
|
||||
("android", ANDROID_CHECK_IOCS_MODULES, BugReportCheckerModule),
|
||||
],
|
||||
)
|
||||
def test_check_iocs_rechecks_the_stored_results_of_custom_modules(
|
||||
platform, builtin_modules, tmp_path, caplog
|
||||
platform, builtin_modules, checker_module, tmp_path, caplog
|
||||
):
|
||||
# check-iocs matches every <slug>.json in the results folder to the module
|
||||
# with that slug, custom modules included, and runs its check_indicators()
|
||||
# again over the stored results.
|
||||
results = [{"domain": "example.org"}]
|
||||
(tmp_path / "custom_results.json").write_text(json.dumps(results))
|
||||
(tmp_path / f"{checker_module.slug}.json").write_text(json.dumps(results))
|
||||
(tmp_path / "backup_only.json").write_text(json.dumps(results))
|
||||
CustomResultsModule.checked.clear()
|
||||
checker_module.checked.clear()
|
||||
|
||||
cmd = CmdCheckIOCS(
|
||||
target_path=str(tmp_path),
|
||||
custom_modules=[CustomResultsModule, BackupOnlyModule],
|
||||
custom_modules=[CustomResultsModule, checker_module, BackupOnlyModule],
|
||||
platform=platform,
|
||||
)
|
||||
cmd.modules = builtin_modules
|
||||
@@ -74,29 +110,79 @@ def test_check_iocs_rechecks_the_stored_results_of_custom_modules(
|
||||
with caplog.at_level(logging.INFO):
|
||||
cmd.run()
|
||||
|
||||
# A module which declares the check-iocs pair is re-checked.
|
||||
assert CustomResultsModule.checked == [results]
|
||||
assert (
|
||||
'Loading results from "custom_results.json" with module CustomResultsModule'
|
||||
in caplog.text
|
||||
)
|
||||
# A module declaring only check-backup is not part of check-iocs.
|
||||
# So is a module which only implements check_indicators().
|
||||
assert checker_module.checked == [results]
|
||||
# A module which does neither is not part of check-iocs.
|
||||
assert "backup_only.json" not in caplog.text
|
||||
|
||||
|
||||
def test_check_iocs_lists_custom_modules_declaring_the_command(caplog):
|
||||
@pytest.mark.parametrize(
|
||||
"platform, builtin_modules, listed, not_listed",
|
||||
[
|
||||
(
|
||||
"ios",
|
||||
IOS_CHECK_IOCS_MODULES,
|
||||
"BackupCheckerModule",
|
||||
"BugReportCheckerModule",
|
||||
),
|
||||
(
|
||||
"android",
|
||||
ANDROID_CHECK_IOCS_MODULES,
|
||||
"BugReportCheckerModule",
|
||||
"BackupCheckerModule",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_check_iocs_lists_the_custom_modules_it_runs(
|
||||
platform, builtin_modules, listed, not_listed, caplog
|
||||
):
|
||||
cmd = CmdCheckIOCS(
|
||||
custom_modules=[CustomResultsModule, BackupOnlyModule],
|
||||
platform="ios",
|
||||
custom_modules=[
|
||||
CustomResultsModule,
|
||||
BackupCheckerModule,
|
||||
BugReportCheckerModule,
|
||||
BackupOnlyModule,
|
||||
],
|
||||
platform=platform,
|
||||
)
|
||||
cmd.modules = IOS_CHECK_IOCS_MODULES
|
||||
cmd.modules = builtin_modules
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
cmd.list_modules()
|
||||
|
||||
assert "CustomResultsModule" in caplog.text
|
||||
# The module which implements check_indicators() for this platform is listed.
|
||||
assert listed in caplog.text
|
||||
# The one for the other platform is not, and neither is BackupOnlyModule.
|
||||
assert not_listed not in caplog.text
|
||||
assert "BackupOnlyModule" not in caplog.text
|
||||
|
||||
|
||||
class ReplacementManifest(Manifest):
|
||||
"""A replacement for a built-in module which does not declare check-iocs."""
|
||||
|
||||
supported_commands = (("ios", "check-backup"),)
|
||||
replaces = Manifest
|
||||
|
||||
|
||||
def test_check_iocs_uses_a_replacement_of_a_built_in_module():
|
||||
# A replacement which subclasses a built-in module inherits its
|
||||
# check_indicators(). check-iocs then runs it in place of that module.
|
||||
cmd = CmdCheckIOCS(custom_modules=[ReplacementManifest], platform="ios")
|
||||
cmd.modules = IOS_CHECK_IOCS_MODULES
|
||||
|
||||
available = cmd._available_modules()
|
||||
|
||||
assert ReplacementManifest in available
|
||||
assert Manifest not in available
|
||||
|
||||
|
||||
LOADED_MODULE = '''
|
||||
from mvt.common.module import MVTModule
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -29,7 +29,6 @@ class TestMvtCommand:
|
||||
assert result.exit_code == 0
|
||||
assert "mvt-ios" in result.output
|
||||
assert "mvt-android" in result.output
|
||||
assert "check-*" in result.output
|
||||
|
||||
def test_version_prints_the_installed_version(self):
|
||||
result = CliRunner().invoke(cli, [*OFFLINE, "version"])
|
||||
|
||||
Reference in new issue
Block a user