Compare commits

...

30 Commits

Author SHA1 Message Date
tek
0389d335ed Bumps version 2023-07-26 18:20:25 +02:00
tek
7f9acec108 Move verbose indicator information to debug 2023-07-26 15:12:58 +02:00
Tek
3ec3b86a45 Adds support for zip files in check-androidqf command (#372) 2023-07-26 13:53:54 +02:00
Donncha Ó Cearbhaill
57d4aca72e Refactor Android modules to remove duplication (#368)
* Remove duplicated detection logic from GetProp modules
* Deduplicate settings and processes
* Refactor detection in artifacts
* Improves Artifact class
---------

Co-authored-by: tek <tek@randhome.io>
2023-07-26 13:42:17 +02:00
github-actions[bot]
1d740ad802 Add new iOS versions and build numbers (#373)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-07-25 10:21:08 +02:00
Donncha Ó Cearbhaill
15ce1b7e64 Merge pull request #370 from mvt-project/android-backup-refactor
Refactor Android backup password handling and add tests
2023-07-22 20:17:47 +02:00
Donncha Ó Cearbhaill
d6fca2f8ae Fix bugs with running ADB commands 2023-07-22 20:16:23 +02:00
Donncha Ó Cearbhaill
cabb679ff1 Merge branch 'main' into android-backup-refactor 2023-07-22 19:59:42 +02:00
Donncha Ó Cearbhaill
829a9f0cf6 Merge pull request #371 from mvt-project/add-coverage
Add code test coverage reporting using pytest-cov
2023-07-22 19:56:04 +02:00
Donncha Ó Cearbhaill
52e0176d5d Add code test coverage reporting 2023-07-22 19:54:01 +02:00
Donncha Ó Cearbhaill
8d8bdf26de Fix black style checks 2023-07-22 19:52:25 +02:00
Donncha Ó Cearbhaill
34fa77ae4d Add documentation for new options 2023-07-22 19:49:59 +02:00
Donncha Ó Cearbhaill
ed7d6fb847 Add integration tests for 'mvt-android check-backup' 2023-07-22 19:26:05 +02:00
Donncha Ó Cearbhaill
a2386dbdf7 Refactor Android backup password handling and add tests 2023-07-22 19:17:27 +02:00
Donncha Ó Cearbhaill
019cfbb84e Merge pull request #363 from aticu/main
Add option to disable interactivity and pass Android backup password on CLI
2023-07-22 16:44:35 +02:00
Donncha Ó Cearbhaill
3d924e22ec Merge branch 'release/v2.4.0' 2023-07-21 12:17:32 +02:00
Donncha Ó Cearbhaill
ca3c1bade4 Bump version to v2.4.0
Bumping the minor version as we introduce some backwards-incompatible
API changes to module definition in #367.
2023-07-21 12:14:31 +02:00
Donncha Ó Cearbhaill
85877fd3eb Merge pull request #369 from mvt-project/move-indicator-checking
Move detection and alerts from run() to check_indicators()
2023-07-21 12:12:36 +02:00
Donncha Ó Cearbhaill
8015ff78e8 Fix black error 2023-07-21 12:10:45 +02:00
Donncha Ó Cearbhaill
1a07b9a78f Move syntax checking before unit tests 2023-07-21 11:30:59 +02:00
Donncha Ó Cearbhaill
0b88de9867 Move detection and alerts from run() to check_indicators() 2023-07-21 11:29:12 +02:00
Niclas Schwarzlose
0edc9d7b81 Add option to disable interactivity 2023-07-19 11:29:51 +02:00
Donncha Ó Cearbhaill
76d7534b05 Fix bug recording detections in WebkitResourceLoadStatistics module 2023-07-18 18:02:42 +02:00
Donncha Ó Cearbhaill
ae2ab02347 Merge pull request #367 from mvt-project/refactor-module-options
Add a module_options parameter to pass data from CLI to modules
2023-07-17 19:07:41 +02:00
Donncha Ó Cearbhaill
e2c623c40f Move --fast flag from being a top-level MVT module parameter to an option in a new module_options parameter 2023-07-17 18:52:35 +02:00
Christian Clauss
a6e1a3de12 Add GitHub Annotions to ruff output (#364)
* Add GitHub Annotions to ruff output
* Upgrade GitHub Actions
* No Py3.11
2023-07-15 14:42:13 +02:00
tek
e7270d6a07 Fixes import and adds test for PR 361 2023-07-10 22:55:22 +02:00
Niclas Schwarzlose
1968a0fca2 Improve appops parsing in dumpsys (#361)
Without this change the package doesn't get properly reset when a new
user starts.

See for example in this excerpt:

```
 1 |    Package com.android.bluetooth:
 2 |      READ_CONTACTS (allow):
 3 |        null=[
 4 |          Access: [pers-s] 2022-04-22 13:24:17.577 (-277d5h22m53s447ms)
 5 |        ]
 6 |      WAKE_LOCK (allow):
 7 |        null=[
 8 |          Access: [pers-s] 2023-01-24 17:45:49.712 (-1m21s312ms) duration=+3ms
 9 |        ]
10 |      GET_USAGE_STATS (default):
11 |        null=[
12 |          Reject: [pers-s]2022-04-22 13:23:53.964 (-277d5h23m17s60ms)
13 |        ]
14 |      BLUETOOTH_CONNECT (allow):
15 |        null=[
16 |          Access: [pers-s] 2022-04-22 13:23:53.988 (-277d5h23m17s36ms)
17 |        ]
18 |  Uid 1027:
19 |    state=pers
20 |    capability=LCMN
21 |    appWidgetVisible=false
22 |      LEGACY_STORAGE: mode=ignore
23 |    Package com.android.nfc:
24 |      WAKE_LOCK (allow):
25 |        null=[
26 |          Access: [pers-s] 2022-04-22 13:23:54.633 (-277d5h23m16s391ms) duration=+1s73ms
27 |        ]
```

Here the package "com.android.bluetooth" is not reset when in line 18,
so when "LEGACY_STORAGE:" in line 22 is encountered, it's added as
another permission to "com.android.bluetooth" with "access" set to
"ode=igno".

This PR fixes that by resetting the package whenever a new Uid is
encountered.

Co-authored-by: Niclas Schwarzlose <niclas.schwarzlose@reporter-ohne-grenzen.de>
2023-07-10 22:53:58 +02:00
Donncha Ó Cearbhaill
46cc54df74 Add information about public indicators and support avenues to documentation 2023-06-30 19:43:30 +02:00
Donncha Ó Cearbhaill
7046ff80d1 Add SMS read time in the MVT logs 2023-06-30 19:30:50 +02:00
135 changed files with 1259 additions and 675 deletions

View File

@@ -16,19 +16,19 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.9', '3.10']
python-version: ['3.8', '3.9', '3.10'] # , '3.11']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade setuptools
python -m pip install --upgrade pip
python -m pip install flake8 pytest safety stix2 pytest-mock
python -m pip install flake8 pytest safety stix2 pytest-mock pytest-cov
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
python -m pip install .
- name: Lint with flake8
@@ -39,5 +39,10 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Safety checks
run: safety check
- name: Test with pytest
run: pytest
- name: Test with pytest and coverage
run: pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=mvt tests/ | tee pytest-coverage.txt
- name: Pytest coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml

View File

@@ -1,21 +1,19 @@
name: Ruff
on: [push]
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
ruff_py3:
name: Ruff syntax check
runs-on: ubuntu-latest
steps:
- name: Setup Python
uses: actions/setup-python@v1
with:
python-version: 3.9
architecture: x64
- name: Checkout
uses: actions/checkout@master
- name: Install Dependencies
run: |
pip install ruff
pip install --user ruff
- name: ruff
run: |
ruff check .
ruff --format=github .

2
.gitignore vendored
View File

@@ -50,6 +50,8 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
pytest-coverage.txt
pytest.xml
# Translations
*.mo

View File

@@ -2,9 +2,10 @@ PWD = $(shell pwd)
check:
flake8
pytest -q
ruff check -q .
black --check .
pytest -q
clean:
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/mvt.egg-info

View File

@@ -35,7 +35,11 @@ $ mvt-android check-backup --output /path/to/results/ /path/to/backup.ab
INFO [mvt.android.modules.backup.sms] Extracted a total of 64 SMS messages
```
If the backup is encrypted, MVT will prompt you to enter the password.
If the backup is encrypted, MVT will prompt you to enter the password. A backup password can also be provided with the `--backup-password` command line option or through the `MVT_ANDROID_BACKUP_PASSWORD` environment variable. The same options can also be used to when analysing an encrypted backup collected through AndroidQF in the `mvt-android check-androidqf` command:
```bash
$ mvt-android check-backup --backup-password "password123" --output /path/to/results/ /path/to/backup.ab
```
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by MVT. Any matches will be highlighted in the terminal output.

View File

@@ -6,6 +6,9 @@
Mobile Verification Toolkit (MVT) is a tool to facilitate the [consensual forensic analysis](introduction.md#consensual-forensics) of Android and iOS devices, for the purpose of identifying traces of compromise.
It has been developed and released by the [Amnesty International Security Lab](https://www.amnesty.org/en/tech/) in July 2021 in the context of the [Pegasus Project](https://forbiddenstories.org/about-the-pegasus-project/) along with [a technical forensic methodology](https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/). It continues to be maintained by Amnesty International and other contributors.
In this documentation you will find instructions on how to install and run the `mvt-ios` and `mvt-android` commands, and guidance on how to interpret the extracted results.
## Resources

View File

@@ -12,6 +12,20 @@ Mobile Verification Toolkit (MVT) is a collection of utilities designed to facil
MVT is a forensic research tool intended for technologists and investigators. Using it requires understanding the basics of forensic analysis and using command-line tools. MVT is not intended for end-user self-assessment. If you are concerned with the security of your device please seek expert assistance.
## Indicators of Compromise
MVT supports using [indicators of compromise (IOCs)](https://github.com/mvt-project/mvt-indicators) to scan mobile devices for potential traces of targeting or infection by known spyware campaigns. This includes IOCs published by [Amnesty International](https://github.com/AmnestyTech/investigations/) and other research groups.
!!! warning
Public indicators of compromise are insufficient to determine that a device is "clean", and not targeted with a particular spyware tool. Reliance on public indicators alone can miss recent forensic traces and give a false sense of security.
Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
Such support is available to civil society through [Amnesty International's Security Lab](https://www.amnesty.org/en/tech/) or [Access Nows Digital Security Helpline](https://www.accessnow.org/help/).
More information about using indicators of compromise with MVT is available in the [documentation](iocs.md).
## Consensual Forensics
While MVT is capable of extracting and processing various types of very personal records typically found on a mobile phone (such as calls history, SMS and WhatsApp messages, etc.), this is intended to help identify potential attack vectors such as malicious SMS messages leading to exploitation.

View File

View File

@@ -0,0 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from mvt.common.artifact import Artifact
class AndroidArtifact(Artifact):
pass

View File

@@ -0,0 +1,59 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import re
from typing import Dict, List
from mvt.android.utils import warn_android_patch_level
from .artifact import AndroidArtifact
INTERESTING_PROPERTIES = [
"gsm.sim.operator.alpha",
"gsm.sim.operator.iso-country",
"persist.sys.timezone",
"ro.boot.serialno",
"ro.build.version.sdk",
"ro.build.version.security_patch",
"ro.product.cpu.abi",
"ro.product.locale",
"ro.product.vendor.manufacturer",
"ro.product.vendor.model",
"ro.product.vendor.name",
]
class GetProp(AndroidArtifact):
def parse(self, entry: str) -> None:
self.results: List[Dict[str, str]] = []
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
for line in entry.splitlines():
line = line.strip()
if line == "":
continue
matches = re.findall(rxp, line)
if not matches or len(matches[0]) != 2:
continue
entry = {"name": matches[0][0], "value": matches[0][1]}
self.results.append(entry)
def check_indicators(self) -> None:
for entry in self.results:
if entry["name"] in INTERESTING_PROPERTIES:
self.log.info("%s: %s", entry["name"], entry["value"])
if entry["name"] == "ro.build.version.security_patch":
warn_android_patch_level(entry["value"], self.log)
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_android_property_name(result.get("name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)

View File

@@ -0,0 +1,69 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .artifact import AndroidArtifact
class Processes(AndroidArtifact):
def parse(self, entry: str) -> None:
for line in entry.split("\n")[1:]:
proc = line.split()
# Skip empty lines
if len(proc) == 0:
continue
# Sometimes WCHAN is empty.
if len(proc) == 8:
proc = proc[:5] + [""] + proc[5:]
# Sometimes there is the security label.
if proc[0].startswith("u:r"):
label = proc[0]
proc = proc[1:]
else:
label = ""
# Sometimes there is no WCHAN.
if len(proc) < 9:
proc = proc[:5] + [""] + proc[5:]
self.results.append(
{
"user": proc[0],
"pid": int(proc[1]),
"ppid": int(proc[2]),
"virtual_memory_size": int(proc[3]),
"resident_set_size": int(proc[4]),
"wchan": proc[5],
"aprocress": proc[6],
"stat": proc[7],
"proc_name": proc[8].strip("[]"),
"label": label,
}
)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
proc_name = result.get("proc_name", "")
if not proc_name:
continue
# Skipping this process because of false positives.
if result["proc_name"] == "gatekeeperd":
continue
ioc = self.indicators.check_app_id(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
ioc = self.indicators.check_process(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)

View File

@@ -0,0 +1,71 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .artifact import AndroidArtifact
ANDROID_DANGEROUS_SETTINGS = [
{
"description": "disabled Google Play Services apps verification",
"key": "verifier_verify_adb_installs",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "package_verifier_enable",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "package_verifier_user_consent",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "upload_apk_enable",
"safe_value": "1",
},
{
"description": "disabled confirmation of adb apps installation",
"key": "adb_install_need_confirm",
"safe_value": "1",
},
{
"description": "disabled sharing of security reports",
"key": "send_security_reports",
"safe_value": "1",
},
{
"description": "disabled sharing of crash logs with manufacturer",
"key": "samsung_errorlog_agree",
"safe_value": "1",
},
{
"description": "disabled applications errors reports",
"key": "send_action_app_error",
"safe_value": "1",
},
{
"description": "enabled installation of non Google Play apps",
"key": "install_non_market_apps",
"safe_value": "0",
},
]
class Settings(AndroidArtifact):
def check_indicators(self) -> None:
for namespace, settings in self.results.items():
for key, value in settings.items():
for danger in ANDROID_DANGEROUS_SETTINGS:
# Check if one of the dangerous settings is using an unsafe
# value (different than the one specified).
if danger["key"] == key and danger["safe_value"] != value:
self.log.warning(
'Found suspicious "%s" setting "%s = %s" (%s)',
namespace,
key,
value,
danger["description"],
)
break

View File

@@ -9,11 +9,13 @@ import click
from mvt.common.cmd_check_iocs import CmdCheckIOCS
from mvt.common.help import (
HELP_MSG_ANDROID_BACKUP_PASSWORD,
HELP_MSG_FAST,
HELP_MSG_HASHES,
HELP_MSG_IOC,
HELP_MSG_LIST_MODULES,
HELP_MSG_MODULE,
HELP_MSG_NONINTERACTIVE,
HELP_MSG_OUTPUT,
HELP_MSG_SERIAL,
HELP_MSG_VERBOSE,
@@ -30,10 +32,12 @@ from .cmd_download_apks import DownloadAPKs
from .modules.adb import ADB_MODULES
from .modules.adb.packages import Packages
from .modules.backup import BACKUP_MODULES
from .modules.backup.helpers import cli_load_android_backup_password
from .modules.bugreport import BUGREPORT_MODULES
init_logging()
log = logging.getLogger("mvt")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
@@ -125,7 +129,7 @@ def download_apks(ctx, all_apks, virustotal, output, from_file, serial, verbose)
# ==============================================================================
@cli.command(
"check-adb",
help="Check an Android device over adb",
help="Check an Android device over ADB",
context_settings=CONTEXT_SETTINGS,
)
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@@ -141,16 +145,35 @@ def download_apks(ctx, all_apks, virustotal, output, from_file, serial, verbose)
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.pass_context
def check_adb(ctx, serial, iocs, output, fast, list_modules, module, verbose):
def check_adb(
ctx,
serial,
iocs,
output,
fast,
list_modules,
module,
non_interactive,
backup_password,
verbose,
):
set_verbose_logging(verbose)
module_options = {
"fast_mode": fast,
"interactive": not non_interactive,
"backup_password": cli_load_android_backup_password(log, backup_password),
}
cmd = CmdAndroidCheckADB(
results_path=output,
ioc_files=iocs,
module_name=module,
serial=serial,
fast_mode=fast,
module_options=module_options,
)
if list_modules:
@@ -232,14 +255,33 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(ctx, iocs, output, list_modules, verbose, backup_path):
def check_backup(
ctx,
iocs,
output,
list_modules,
non_interactive,
backup_password,
verbose,
backup_path,
):
set_verbose_logging(verbose)
# Always generate hashes as backups are generally small.
cmd = CmdAndroidCheckBackup(
target_path=backup_path, results_path=output, ioc_files=iocs, hashes=True
target_path=backup_path,
results_path=output,
ioc_files=iocs,
hashes=True,
module_options={
"interactive": not non_interactive,
"backup_password": cli_load_android_backup_password(log, backup_password),
},
)
if list_modules:
@@ -277,19 +319,35 @@ def check_backup(ctx, iocs, output, list_modules, verbose, backup_path):
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.argument("ANDROIDQF_PATH", type=click.Path(exists=True))
@click.pass_context
def check_androidqf(
ctx, iocs, output, list_modules, module, hashes, verbose, androidqf_path
ctx,
iocs,
output,
list_modules,
module,
hashes,
non_interactive,
backup_password,
verbose,
androidqf_path,
):
set_verbose_logging(verbose)
cmd = CmdAndroidCheckAndroidQF(
target_path=androidqf_path,
results_path=output,
ioc_files=iocs,
module_name=module,
hashes=hashes,
module_options={
"interactive": not non_interactive,
"backup_password": cli_load_android_backup_password(log, backup_password),
},
)
if list_modules:

View File

@@ -21,7 +21,7 @@ class CmdAndroidCheckADB(Command):
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
) -> None:
super().__init__(
target_path=target_path,
@@ -29,7 +29,7 @@ class CmdAndroidCheckADB(Command):
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
fast_mode=fast_mode,
module_options=module_options,
log=log,
)

View File

@@ -4,7 +4,10 @@
# https://license.mvt.re/1.1/
import logging
from typing import Optional
import os
import zipfile
from pathlib import Path
from typing import List, Optional
from mvt.common.command import Command
@@ -21,7 +24,7 @@ class CmdAndroidCheckAndroidQF(Command):
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
@@ -30,10 +33,35 @@ class CmdAndroidCheckAndroidQF(Command):
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
fast_mode=fast_mode,
module_options=module_options,
hashes=hashes,
log=log,
)
self.name = "check-androidqf"
self.modules = ANDROIDQF_MODULES
self.format: Optional[str] = None
self.archive: Optional[zipfile.ZipFile] = None
self.files: List[str] = []
def init(self):
if os.path.isdir(self.target_path):
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)
elif os.path.isfile(self.target_path):
self.format = "zip"
self.archive = zipfile.ZipFile(self.target_path)
self.files = self.archive.namelist()
def module_init(self, module):
if self.format == "zip":
module.from_zip_file(self.archive, self.files)
else:
parent_path = Path(self.target_path).absolute().parent.as_posix()
module.from_folder(parent_path, self.files)

View File

@@ -11,9 +11,8 @@ import tarfile
from pathlib import Path
from typing import List, Optional
from rich.prompt import Prompt
from mvt.android.modules.backup.base import BackupExtraction
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.android.parsers.backup import (
AndroidBackupParsingError,
InvalidBackupPassword,
@@ -35,7 +34,7 @@ class CmdAndroidCheckBackup(Command):
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
@@ -44,7 +43,7 @@ class CmdAndroidCheckBackup(Command):
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
fast_mode=fast_mode,
module_options=module_options,
hashes=hashes,
log=log,
)
@@ -72,7 +71,12 @@ class CmdAndroidCheckBackup(Command):
password = None
if header["encryption"] != "none":
password = Prompt.ask("Enter backup password", password=True)
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:

View File

@@ -25,7 +25,7 @@ class CmdAndroidCheckBugreport(Command):
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
@@ -34,7 +34,7 @@ class CmdAndroidCheckBugreport(Command):
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
fast_mode=fast_mode,
module_options=module_options,
hashes=hashes,
log=log,
)

View File

@@ -22,9 +22,9 @@ from adb_shell.exceptions import (
UsbDeviceNotFoundError,
UsbReadFailedError,
)
from rich.prompt import Prompt
from usb1 import USBErrorAccess, USBErrorBusy
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.android.parsers.backup import (
InvalidBackupPassword,
parse_ab_header,
@@ -44,7 +44,7 @@ class AndroidExtraction(MVTModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -52,7 +52,7 @@ class AndroidExtraction(MVTModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -311,6 +311,12 @@ class AndroidExtraction(MVTModule):
"You may need to set a backup password. \a"
)
if self.module_options.get("backup_password", None):
self.log.warning(
"Backup password already set from command line or environment "
"variable. You should use the same password if enabling encryption!"
)
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over
# the shell transport...
cmd = f"/system/bin/bu backup -nocompress '{package_name}' | base64"
@@ -329,7 +335,12 @@ class AndroidExtraction(MVTModule):
return parse_backup_file(backup_output, password=None)
for _ in range(0, 3):
backup_password = Prompt.ask("Enter backup password", password=True)
backup_password = prompt_or_load_android_backup_password(
self.log, self.module_options
)
if not backup_password:
# Fail as no backup password loaded for this encrypted backup
self.log.critical("No backup password provided.")
try:
decrypted_backup_tar = parse_backup_file(backup_output, backup_password)
return decrypted_backup_tar

View File

@@ -23,7 +23,7 @@ class ChromeHistory(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -31,7 +31,7 @@ class ChromeHistory(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -19,7 +19,7 @@ class DumpsysAccessibility(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class DumpsysAccessibility(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -19,7 +19,7 @@ class DumpsysActivities(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class DumpsysActivities(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -21,7 +21,7 @@ class DumpsysAppOps(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -29,7 +29,7 @@ class DumpsysAppOps(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -19,7 +19,7 @@ class DumpsysBatteryDaily(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class DumpsysBatteryDaily(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -19,7 +19,7 @@ class DumpsysBatteryHistory(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class DumpsysBatteryHistory(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -21,7 +21,7 @@ class DumpsysDBInfo(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -29,7 +29,7 @@ class DumpsysDBInfo(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -18,7 +18,7 @@ class DumpsysFull(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -26,7 +26,7 @@ class DumpsysFull(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -25,7 +25,7 @@ class DumpsysReceivers(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -33,7 +33,7 @@ class DumpsysReceivers(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -30,7 +30,7 @@ class Files(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -38,7 +38,7 @@ class Files(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -142,8 +142,10 @@ class Files(AndroidExtraction):
"Found %s files in primary Android tmp and media folders", len(self.results)
)
if self.fast_mode:
self.log.info("Flag --fast was enabled: skipping full file listing")
if self.module_options.get("fast_mode", None):
self.log.info(
"The `fast_mode` option was enabled: skipping full file listing"
)
else:
self.log.info("Processing full file listing. This may take a while...")
self.find_files("/")

View File

@@ -4,15 +4,14 @@
# https://license.mvt.re/1.1/
import logging
from datetime import datetime, timedelta
from typing import Optional
from mvt.android.parsers import parse_getprop
from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
from .base import AndroidExtraction
class Getprop(AndroidExtraction):
class Getprop(GetPropArtifact, AndroidExtraction):
"""This module extracts device properties from getprop command."""
def __init__(
@@ -20,7 +19,7 @@ class Getprop(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -28,40 +27,17 @@ class Getprop(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
self.results = {} if not results else results
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_android_property_name(result.get("name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("getprop")
self._adb_disconnect()
self.results = parse_getprop(output)
# Alert if phone is outdated.
for entry in self.results:
if entry.get("name", "") != "ro.build.version.security_patch":
continue
patch_date = datetime.strptime(entry["value"], "%Y-%m-%d")
if (datetime.now() - patch_date) > timedelta(days=6 * 30):
self.log.warning(
"This phone has not received security updates "
"for more than six months (last update: %s)",
entry["value"],
)
self.parse(output)
self.log.info("Extracted %d Android system properties", len(self.results))

View File

@@ -18,7 +18,7 @@ class Logcat(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -26,7 +26,7 @@ class Logcat(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -93,7 +93,7 @@ class Packages(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -101,7 +101,7 @@ class Packages(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -351,7 +351,7 @@ class Packages(AndroidExtraction):
result["timestamp"],
)
if not self.fast_mode:
if not self.module_options.get("fast_mode", None):
self.check_virustotal(packages_to_lookup)
self.log.info(

View File

@@ -6,10 +6,12 @@
import logging
from typing import Optional
from mvt.android.artifacts.processes import Processes as ProcessesArtifact
from .base import AndroidExtraction
class Processes(AndroidExtraction):
class Processes(ProcessesArtifact, AndroidExtraction):
"""This module extracts details on running processes."""
def __init__(
@@ -17,7 +19,7 @@ class Processes(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -25,66 +27,16 @@ class Processes(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
proc_name = result.get("proc_name", "")
if not proc_name:
continue
# Skipping this process because of false positives.
if result["proc_name"] == "gatekeeperd":
continue
ioc = self.indicators.check_app_id(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
ioc = self.indicators.check_process(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("ps -A")
for line in output.splitlines()[1:]:
line = line.strip()
if line == "":
continue
fields = line.split()
proc = {
"user": fields[0],
"pid": fields[1],
"parent_pid": fields[2],
"vsize": fields[3],
"rss": fields[4],
}
# Sometimes WCHAN is empty, so we need to re-align output fields.
if len(fields) == 8:
proc["wchan"] = ""
proc["pc"] = fields[5]
proc["name"] = fields[7]
elif len(fields) == 9:
proc["wchan"] = fields[5]
proc["pc"] = fields[6]
proc["name"] = fields[8]
self.results.append(proc)
self.parse(output)
self._adb_disconnect()
self.log.info("Extracted records on a total of %d processes", len(self.results))

View File

@@ -17,7 +17,7 @@ class RootBinaries(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -25,11 +25,16 @@ class RootBinaries(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
for root_binary in self.results:
self.detected.append(root_binary)
self.log.warning('Found root binary "%s"', root_binary)
def run(self) -> None:
root_binaries = [
"su",
@@ -60,7 +65,6 @@ class RootBinaries(AndroidExtraction):
if "which: not found" in output:
continue
self.detected.append(root_binary)
self.log.warning('Found root binary "%s"', root_binary)
self.results.append(root_binary)
self._adb_disconnect()

View File

@@ -19,7 +19,7 @@ class SELinuxStatus(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class SELinuxStatus(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -6,58 +6,12 @@
import logging
from typing import Optional
from mvt.android.artifacts.settings import Settings as SettingsArtifact
from .base import AndroidExtraction
ANDROID_DANGEROUS_SETTINGS = [
{
"description": "disabled Google Play Services apps verification",
"key": "verifier_verify_adb_installs",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "package_verifier_enable",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "package_verifier_user_consent",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "upload_apk_enable",
"safe_value": "1",
},
{
"description": "disabled confirmation of adb apps installation",
"key": "adb_install_need_confirm",
"safe_value": "1",
},
{
"description": "disabled sharing of security reports",
"key": "send_security_reports",
"safe_value": "1",
},
{
"description": "disabled sharing of crash logs with manufacturer",
"key": "samsung_errorlog_agree",
"safe_value": "1",
},
{
"description": "disabled applications errors reports",
"key": "send_action_app_error",
"safe_value": "1",
},
{
"description": "enabled installation of non Google Play apps",
"key": "install_non_market_apps",
"safe_value": "0",
},
]
class Settings(AndroidExtraction):
class Settings(SettingsArtifact, AndroidExtraction):
"""This module extracts Android system settings."""
def __init__(
@@ -65,7 +19,7 @@ class Settings(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -73,28 +27,13 @@ class Settings(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
self.results = {} if not results else results
def check_indicators(self) -> None:
for _, settings in self.results.items():
for key, value in settings.items():
for danger in ANDROID_DANGEROUS_SETTINGS:
# Check if one of the dangerous settings is using an unsafe
# value (different than the one specified).
if danger["key"] == key and danger["safe_value"] != value:
self.log.warning(
'Found suspicious setting "%s = %s" (%s)',
key,
value,
danger["description"],
)
break
def run(self) -> None:
self._adb_connect()

View File

@@ -49,7 +49,7 @@ class SMS(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -57,7 +57,7 @@ class SMS(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -24,7 +24,7 @@ class Whatsapp(AndroidExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -32,7 +32,7 @@ class Whatsapp(AndroidExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -6,6 +6,7 @@
import fnmatch
import logging
import os
import zipfile
from typing import Any, Dict, List, Optional, Union
from mvt.common.module import MVTModule
@@ -19,7 +20,7 @@ class AndroidQFModule(MVTModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Union[List[Dict[str, Any]], Dict[str, Any], None] = None,
) -> None:
@@ -27,17 +28,32 @@ class AndroidQFModule(MVTModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
self._path: str = target_path
self.files: List[str] = []
self.archive: Optional[zipfile.ZipFile] = None
self._path = target_path
self._files = []
def from_folder(self, parent_path: str, files: List[str]):
self.parent_path = parent_path
self.files = files
for root, dirs, files in os.walk(target_path):
for name in files:
self._files.append(os.path.join(root, name))
def from_zip_file(self, archive: zipfile.ZipFile, files: List[str]):
self.archive = archive
self.files = files
def _get_files_by_pattern(self, pattern):
return fnmatch.filter(self._files, pattern)
def _get_files_by_pattern(self, pattern: str):
return fnmatch.filter(self.files, pattern)
def _get_file_content(self, file_path):
if self.archive:
handle = self.archive.open(file_path)
else:
handle = open(os.path.join(self.parent_path, file_path), "rb")
data = handle.read()
handle.close()
return data

View File

@@ -19,7 +19,7 @@ class DumpsysAccessibility(AndroidQFModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class DumpsysAccessibility(AndroidQFModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -49,21 +49,21 @@ class DumpsysAccessibility(AndroidQFModule):
lines = []
in_accessibility = False
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.strip().startswith("DUMP OF SERVICE accessibility:"):
in_accessibility = True
continue
data = self._get_file_content(dumpsys_file[0])
for line in data.decode("utf-8").split("\n"):
if line.strip().startswith("DUMP OF SERVICE accessibility:"):
in_accessibility = True
continue
if not in_accessibility:
continue
if not in_accessibility:
continue
if line.strip().startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
if line.strip().startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
lines.append(line.rstrip())
self.results = parse_dumpsys_accessibility("\n".join(lines))

View File

@@ -19,7 +19,7 @@ class DumpsysActivities(AndroidQFModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class DumpsysActivities(AndroidQFModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -52,21 +52,21 @@ class DumpsysActivities(AndroidQFModule):
lines = []
in_package = False
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.strip() == "DUMP OF SERVICE package:":
in_package = True
continue
data = self._get_file_content(dumpsys_file[0])
for line in data.decode("utf-8").split("\n"):
if line.strip() == "DUMP OF SERVICE package:":
in_package = True
continue
if not in_package:
continue
if not in_package:
continue
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
lines.append(line.rstrip())
self.results = parse_dumpsys_activity_resolver_table("\n".join(lines))

View File

@@ -17,7 +17,7 @@ class DumpsysAppops(AndroidQFModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -25,7 +25,7 @@ class DumpsysAppops(AndroidQFModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -76,19 +76,19 @@ class DumpsysAppops(AndroidQFModule):
lines = []
in_package = False
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.startswith("DUMP OF SERVICE appops:"):
in_package = True
continue
data = self._get_file_content(dumpsys_file[0])
for line in data.decode("utf-8").split("\n"):
if line.startswith("DUMP OF SERVICE appops:"):
in_package = True
continue
if in_package:
if line.startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
if in_package:
if line.startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
lines.append(line.rstrip())
self.results = parse_dumpsys_appops("\n".join(lines))
self.log.info("Identified %d applications in AppOps Manager", len(self.results))

View File

@@ -24,7 +24,7 @@ class DumpsysPackages(AndroidQFModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[List[Dict[str, Any]]] = None,
) -> None:
@@ -32,7 +32,7 @@ class DumpsysPackages(AndroidQFModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -78,13 +78,12 @@ class DumpsysPackages(AndroidQFModule):
self.log.info("Dumpsys file not found")
return
with open(dumpsys_file[0]) as handle:
data = handle.read().split("\n")
data = self._get_file_content(dumpsys_file[0])
package = []
in_service = False
in_package_list = False
for line in data:
for line in data.decode("utf-8").split("\n"):
if line.strip().startswith("DUMP OF SERVICE package:"):
in_service = True
continue

View File

@@ -26,7 +26,7 @@ class DumpsysReceivers(AndroidQFModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Union[List[Any], Dict[str, Any], None] = None,
) -> None:
@@ -34,7 +34,7 @@ class DumpsysReceivers(AndroidQFModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -86,21 +86,21 @@ class DumpsysReceivers(AndroidQFModule):
in_receivers = False
lines = []
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.strip() == "DUMP OF SERVICE package:":
in_receivers = True
continue
data = self._get_file_content(dumpsys_file[0])
for line in data.decode("utf-8").split("\n"):
if line.strip() == "DUMP OF SERVICE package:":
in_receivers = True
continue
if not in_receivers:
continue
if not in_receivers:
continue
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
lines.append(line.rstrip())
self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines))

View File

@@ -4,29 +4,14 @@
# https://license.mvt.re/1.1/
import logging
from datetime import datetime, timedelta
from typing import Optional
from mvt.android.parsers.getprop import parse_getprop
from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
from .base import AndroidQFModule
INTERESTING_PROPERTIES = [
"gsm.sim.operator.alpha",
"gsm.sim.operator.iso-country",
"persist.sys.timezone",
"ro.boot.serialno",
"ro.build.version.sdk",
"ro.build.version.security_patch",
"ro.product.cpu.abi",
"ro.product.locale",
"ro.product.vendor.manufacturer",
"ro.product.vendor.model",
"ro.product.vendor.name",
]
class Getprop(AndroidQFModule):
class Getprop(GetPropArtifact, AndroidQFModule):
"""This module extracts data from get properties."""
def __init__(
@@ -34,7 +19,7 @@ class Getprop(AndroidQFModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -42,43 +27,19 @@ class Getprop(AndroidQFModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
self.results = []
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_android_property_name(result.get("name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self) -> None:
getprop_files = self._get_files_by_pattern("*/getprop.txt")
if not getprop_files:
self.log.info("getprop.txt file not found")
return
with open(getprop_files[0]) as f:
data = f.read()
self.results = parse_getprop(data)
for entry in self.results:
if entry["name"] in INTERESTING_PROPERTIES:
self.log.info("%s: %s", entry["name"], entry["value"])
if entry["name"] == "ro.build.version.security_patch":
last_patch = datetime.strptime(entry["value"], "%Y-%m-%d")
if (datetime.now() - last_patch) > timedelta(days=6 * 31):
self.log.warning(
"This phone has not received security "
"updates for more than six months "
"(last update: %s)",
entry["value"],
)
data = self._get_file_content(getprop_files[0]).decode("utf-8")
self.parse(data)
self.log.info("Extracted a total of %d properties", len(self.results))

View File

@@ -6,10 +6,12 @@
import logging
from typing import Optional
from mvt.android.artifacts.processes import Processes as ProcessesArtifact
from .base import AndroidQFModule
class Processes(AndroidQFModule):
class Processes(ProcessesArtifact, AndroidQFModule):
"""This module analyse running processes"""
def __init__(
@@ -17,7 +19,7 @@ class Processes(AndroidQFModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -25,75 +27,15 @@ class Processes(AndroidQFModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
proc_name = result.get("proc_name", "")
if not proc_name:
continue
# Skipping this process because of false positives.
if result["proc_name"] == "gatekeeperd":
continue
ioc = self.indicators.check_app_id(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
ioc = self.indicators.check_process(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def _parse_ps(self, data):
for line in data.split("\n")[1:]:
proc = line.split()
# Sometimes WCHAN is empty.
if len(proc) == 8:
proc = proc[:5] + [""] + proc[5:]
# Sometimes there is the security label.
if proc[0].startswith("u:r"):
label = proc[0]
proc = proc[1:]
else:
label = ""
# Sometimes there is no WCHAN.
if len(proc) < 9:
proc = proc[:5] + [""] + proc[5:]
self.results.append(
{
"user": proc[0],
"pid": int(proc[1]),
"ppid": int(proc[2]),
"virtual_memory_size": int(proc[3]),
"resident_set_size": int(proc[4]),
"wchan": proc[5],
"aprocress": proc[6],
"stat": proc[7],
"proc_name": proc[8].strip("[]"),
"label": label,
}
)
def run(self) -> None:
ps_files = self._get_files_by_pattern("*/ps.txt")
if not ps_files:
return
with open(ps_files[0]) as handle:
self._parse_ps(handle.read())
self.parse(self._get_file_content(ps_files[0]).decode("utf-8"))
self.log.info("Identified %d running processes", len(self.results))

View File

@@ -6,12 +6,12 @@
import logging
from typing import Optional
from mvt.android.modules.adb.settings import ANDROID_DANGEROUS_SETTINGS
from mvt.android.artifacts.settings import Settings as SettingsArtifact
from .base import AndroidQFModule
class Settings(AndroidQFModule):
class Settings(SettingsArtifact, AndroidQFModule):
"""This module analyse setting files"""
def __init__(
@@ -19,7 +19,7 @@ class Settings(AndroidQFModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class Settings(AndroidQFModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -38,29 +38,18 @@ class Settings(AndroidQFModule):
namespace = setting_file[setting_file.rfind("_") + 1 : -4]
self.results[namespace] = {}
data = self._get_file_content(setting_file)
for line in data.decode("utf-8").split("\n"):
line = line.strip()
try:
key, value = line.split("=", 1)
except ValueError:
continue
with open(setting_file) as handle:
for line in handle:
line = line.strip()
try:
key, value = line.split("=", 1)
except ValueError:
continue
try:
self.results[namespace][key] = value
except IndexError:
continue
for danger in ANDROID_DANGEROUS_SETTINGS:
if danger["key"] == key and danger["safe_value"] != value:
self.log.warning(
'Found suspicious setting "%s = %s" (%s)',
key,
value,
danger["description"],
)
break
try:
self.results[namespace][key] = value
except IndexError:
continue
self.log.info(
"Identified %d settings", sum([len(val) for val in self.results.values()])

View File

@@ -3,10 +3,10 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1
import getpass
import logging
from typing import Optional
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.android.parsers.backup import (
AndroidBackupParsingError,
InvalidBackupPassword,
@@ -26,7 +26,7 @@ class SMS(AndroidQFModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -34,7 +34,7 @@ class SMS(AndroidQFModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -58,7 +58,13 @@ class SMS(AndroidQFModule):
password = None
if header["encryption"] != "none":
password = getpass.getpass(prompt="Backup Password: ", stream=None)
password = prompt_or_load_android_backup_password(
self.log, self.module_options
)
if not password:
self.log.critical("No backup password provided.")
return
try:
tardata = parse_backup_file(data, password=password)
except InvalidBackupPassword:
@@ -90,8 +96,5 @@ class SMS(AndroidQFModule):
self.log.info("No backup data found")
return
with open(files[0], "rb") as handle:
data = handle.read()
self.parse_backup(data)
self.parse_backup(self._get_file_content(files[0]))
self.log.info("Identified %d SMS in backup data", len(self.results))

View File

@@ -20,7 +20,7 @@ class BackupExtraction(MVTModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -28,7 +28,7 @@ class BackupExtraction(MVTModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -0,0 +1,60 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
from rich.prompt import Prompt
MVT_ANDROID_BACKUP_PASSWORD = "MVT_ANDROID_BACKUP_PASSWORD"
def cli_load_android_backup_password(log, backup_password):
"""
Helper to load a backup password from CLI argument or environment variable
Used in MVT CLI command parsers.
"""
password_from_env = os.environ.get(MVT_ANDROID_BACKUP_PASSWORD, None)
if backup_password:
log.info(
"Your password may be visible in the process table because it "
"was supplied on the command line!"
)
if password_from_env:
log.info(
"Ignoring %s environment variable, using --backup-password argument instead",
MVT_ANDROID_BACKUP_PASSWORD,
)
return backup_password
elif password_from_env:
log.info(
"Using backup password from %s environment variable",
MVT_ANDROID_BACKUP_PASSWORD,
)
return password_from_env
def prompt_or_load_android_backup_password(log, module_options):
"""
Used in modules to either prompt or load backup password to use for encryption and decryption.
"""
if module_options.get("backup_password", None):
backup_password = module_options["backup_password"]
log.info(
"Using backup password passed from command line or environment variable."
)
# The default is to allow interactivity
elif module_options.get("interactive", True):
backup_password = Prompt.ask(prompt="Enter backup password", password=True)
else:
log.critical(
"Cannot decrypt backup because interactivity"
" was disabled and the password was not"
" supplied"
)
return None
return backup_password

View File

@@ -17,7 +17,7 @@ class SMS(BackupExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -25,7 +25,7 @@ class SMS(BackupExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -19,7 +19,7 @@ class Accessibility(BugReportModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class Accessibility(BugReportModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -19,7 +19,7 @@ class Activities(BugReportModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class Activities(BugReportModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -19,7 +19,7 @@ class Appops(BugReportModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class Appops(BugReportModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -20,7 +20,7 @@ class BugReportModule(MVTModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -28,7 +28,7 @@ class BugReportModule(MVTModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -19,7 +19,7 @@ class BatteryDaily(BugReportModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class BatteryDaily(BugReportModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -19,7 +19,7 @@ class BatteryHistory(BugReportModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -27,7 +27,7 @@ class BatteryHistory(BugReportModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -21,7 +21,7 @@ class DBInfo(BugReportModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -29,7 +29,7 @@ class DBInfo(BugReportModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -4,15 +4,14 @@
# https://license.mvt.re/1.1/
import logging
from datetime import datetime, timedelta
from typing import Optional
from mvt.android.parsers import parse_getprop
from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
from .base import BugReportModule
class Getprop(BugReportModule):
class Getprop(GetPropArtifact, BugReportModule):
"""This module extracts device properties from getprop command."""
def __init__(
@@ -20,7 +19,7 @@ class Getprop(BugReportModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -28,12 +27,12 @@ class Getprop(BugReportModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
self.results = {} if not results else results
self.results = [] if not results else results
def run(self) -> None:
content = self._get_dumpstate_file()
@@ -60,18 +59,5 @@ class Getprop(BugReportModule):
lines.append(line)
self.results = parse_getprop("\n".join(lines))
# Alert if phone is outdated.
for entry in self.results:
if entry["name"] == "ro.build.version.security_patch":
security_patch = entry["value"]
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
if (datetime.now() - patch_date) > timedelta(days=6 * 30):
self.log.warning(
"This phone has not received security updates "
"for more than six months (last update: %s)",
security_patch,
)
self.parse("\n".join(lines))
self.log.info("Extracted %d Android system properties", len(self.results))

View File

@@ -24,7 +24,7 @@ class Packages(BugReportModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -32,7 +32,7 @@ class Packages(BugReportModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -25,7 +25,7 @@ class Receivers(BugReportModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -33,7 +33,7 @@ class Receivers(BugReportModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -12,4 +12,3 @@ from .dumpsys import (
parse_dumpsys_dbinfo,
parse_dumpsys_receiver_resolver_table,
)
from .getprop import parse_getprop

View File

@@ -339,6 +339,17 @@ def parse_dumpsys_appops(output: str) -> List[Dict[str, Any]]:
if line.startswith(" Uid "):
uid = line[6:-1]
if entry:
perm["entries"].append(entry)
entry = {}
if package:
if perm:
package["permissions"].append(perm)
perm = {}
results.append(package)
package = {}
continue
if line.startswith(" Package "):
@@ -360,7 +371,7 @@ def parse_dumpsys_appops(output: str) -> List[Dict[str, Any]]:
}
continue
if line.startswith(" ") and line[6] != " ":
if package and line.startswith(" ") and line[6] != " ":
if entry:
perm["entries"].append(entry)
entry = {}

View File

@@ -1,26 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import re
from typing import Dict, List
def parse_getprop(output: str) -> List[Dict[str, str]]:
results = []
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
for line in output.splitlines():
line = line.strip()
if line == "":
continue
matches = re.findall(rxp, line)
if not matches or len(matches[0]) != 2:
continue
entry = {"name": matches[0][0], "value": matches[0][1]}
results.append(entry)
return results

19
mvt/android/utils.py Normal file
View File

@@ -0,0 +1,19 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from datetime import datetime, timedelta
def warn_android_patch_level(patch_level: str, log) -> bool:
"""Alert if Android patch level out-of-date"""
patch_date = datetime.strptime(patch_level, "%Y-%m-%d")
if (datetime.now() - patch_date) > timedelta(days=6 * 31):
log.warning(
"This phone has not received security updates "
"for more than six months (last update: %s)",
patch_level,
)
return True
return False

28
mvt/common/artifact.py Normal file
View File

@@ -0,0 +1,28 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
class Artifact:
"""
Main artifact class
"""
def __init__(self, *args, **kwargs):
self.results = []
self.detected = []
self.indicators = None
super().__init__(*args, **kwargs)
def parse(self, entry: str):
"""
Parse the artifact, adds the parsed information to self.results
"""
raise NotImplementedError
def check_indicators(self) -> None:
"""Check the results of this module against a provided list of
indicators coming from self.indicators
"""
raise NotImplementedError

View File

@@ -21,7 +21,7 @@ class CmdCheckIOCS(Command):
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
) -> None:
super().__init__(
target_path=target_path,
@@ -29,7 +29,7 @@ class CmdCheckIOCS(Command):
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
fast_mode=fast_mode,
module_options=module_options,
log=log,
)

View File

@@ -28,7 +28,7 @@ class Command:
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
hashes: bool = False,
log: logging.Logger = logging.getLogger(__name__),
) -> None:
@@ -40,9 +40,13 @@ class Command:
self.ioc_files = ioc_files if ioc_files else []
self.module_name = module_name
self.serial = serial
self.fast_mode = fast_mode
self.log = log
# This dictionary can contain options that will be passed down from
# the Command to all modules. This can for example be used to pass
# down a password to decrypt a backup or flags which are need by some modules.
self.module_options = module_options if module_options else {}
# This list will contain all executed modules.
# We can use this to reference e.g. self.executed[0].results.
self.executed = []
@@ -172,7 +176,7 @@ class Command:
m = module(
target_path=self.target_path,
results_path=self.results_path,
fast_mode=self.fast_mode,
module_options=self.module_options,
log=module_logger,
)

View File

@@ -9,6 +9,8 @@ HELP_MSG_IOC = "Path to indicators file (can be invoked multiple time)"
HELP_MSG_FAST = "Avoid running time/resource consuming features"
HELP_MSG_LIST_MODULES = "Print list of available modules and exit"
HELP_MSG_MODULE = "Name of a single module you would like to run instead of all"
HELP_MSG_NONINTERACTIVE = "Don't ask interactive questions during processing"
HELP_MSG_ANDROID_BACKUP_PASSWORD = "The backup password to use for an Android backup"
HELP_MSG_HASHES = "Generate hashes of all the files analyzed"
HELP_MSG_VERBOSE = "Verbose mode"

View File

@@ -6,8 +6,8 @@
import json
import logging
import os
from typing import Any, Dict, Iterator, List, Optional, Union
from functools import lru_cache
from typing import Any, Dict, Iterator, List, Optional, Union
import ahocorasick
from appdirs import user_data_dir
@@ -206,7 +206,7 @@ class Indicators:
break
for coll in collections:
self.log.info(
self.log.debug(
'Extracted %d indicators for collection with name "%s"',
coll["count"],
coll["name"],

View File

@@ -37,7 +37,7 @@ class MVTModule:
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[Dict[str, Any]] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Union[List[Dict[str, Any]], Dict[str, Any], None] = None,
) -> None:
@@ -59,7 +59,7 @@ class MVTModule:
self.file_path = file_path
self.target_path = target_path
self.results_path = results_path
self.fast_mode = fast_mode
self.module_options = module_options if module_options else {}
self.log = log
self.indicators = None
self.results = results if results else []

View File

@@ -3,12 +3,12 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import cProfile
import datetime
import hashlib
import logging
import os
import re
import cProfile
from typing import Any, Iterator, Union
from rich.logging import RichHandler

View File

@@ -3,4 +3,4 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
MVT_VERSION = "2.3.0"
MVT_VERSION = "2.4.1"

View File

@@ -219,13 +219,14 @@ def check_backup(
ctx, iocs, output, fast, list_modules, module, hashes, verbose, backup_path
):
set_verbose_logging(verbose)
module_options = {"fast_mode": fast}
cmd = CmdIOSCheckBackup(
target_path=backup_path,
results_path=output,
ioc_files=iocs,
module_name=module,
fast_mode=fast,
module_options=module_options,
hashes=hashes,
)
@@ -269,12 +270,14 @@ def check_backup(
@click.pass_context
def check_fs(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dump_path):
set_verbose_logging(verbose)
module_options = {"fast_mode": fast}
cmd = CmdIOSCheckFS(
target_path=dump_path,
results_path=output,
ioc_files=iocs,
module_name=module,
fast_mode=fast,
module_options=module_options,
hashes=hashes,
)

View File

@@ -22,7 +22,7 @@ class CmdIOSCheckBackup(Command):
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
@@ -31,7 +31,7 @@ class CmdIOSCheckBackup(Command):
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
fast_mode=fast_mode,
module_options=module_options,
hashes=hashes,
log=log,
)

View File

@@ -22,7 +22,7 @@ class CmdIOSCheckFS(Command):
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
hashes: bool = False,
) -> None:
super().__init__(
@@ -31,7 +31,7 @@ class CmdIOSCheckFS(Command):
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
fast_mode=fast_mode,
module_options=module_options,
hashes=hashes,
log=log,
)

View File

@@ -871,6 +871,10 @@
"version": "15.7.7",
"build": "19H357"
},
{
"version": "15.7.8",
"build": "19H364"
},
{
"build": "20A362",
"version": "16.0"
@@ -927,5 +931,9 @@
{
"version": "16.5.1",
"build": "20F75"
},
{
"version": "16.6",
"build": "20G75"
}
]

View File

@@ -22,7 +22,7 @@ class BackupInfo(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -30,7 +30,7 @@ class BackupInfo(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -26,7 +26,7 @@ class ConfigurationProfiles(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -34,7 +34,7 @@ class ConfigurationProfiles(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -26,7 +26,7 @@ class Manifest(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -34,7 +34,7 @@ class Manifest(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -91,19 +91,6 @@ class Manifest(IOSExtraction):
if not result.get("relative_path"):
continue
if result["domain"]:
if (
os.path.basename(result["relative_path"])
== "com.apple.CrashReporter.plist"
and result["domain"] == "RootDomain"
):
self.log.warning(
"Found a potentially suspicious "
'"com.apple.CrashReporter.plist" file created in RootDomain'
)
self.detected.append(result)
continue
if not self.indicators:
continue

View File

@@ -27,7 +27,7 @@ class ProfileEvents(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -35,7 +35,7 @@ class ProfileEvents(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -23,7 +23,7 @@ class IOSExtraction(MVTModule):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -31,7 +31,7 @@ class IOSExtraction(MVTModule):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -27,7 +27,7 @@ class Analytics(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -35,7 +35,7 @@ class Analytics(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -23,7 +23,7 @@ class AnalyticsIOSVersions(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -31,7 +31,7 @@ class AnalyticsIOSVersions(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -17,7 +17,7 @@ class CacheFiles(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -25,7 +25,7 @@ class CacheFiles(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -22,7 +22,7 @@ class Filesystem(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -30,7 +30,7 @@ class Filesystem(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)
@@ -57,7 +57,7 @@ class Filesystem(IOSExtraction):
self.detected.append(result)
# If we are instructed to run fast, we skip the rest.
if self.fast_mode:
if self.module_options.get("fast_mode", None):
continue
ioc = self.indicators.check_file_path_process(result["path"])

View File

@@ -27,7 +27,7 @@ class Netusage(NetBase):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -35,7 +35,7 @@ class Netusage(NetBase):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -25,7 +25,7 @@ class SafariFavicon(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -33,7 +33,7 @@ class SafariFavicon(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -23,7 +23,7 @@ class ShutdownLog(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -31,7 +31,7 @@ class ShutdownLog(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -25,7 +25,7 @@ class IOSVersionHistory(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -33,7 +33,7 @@ class IOSVersionHistory(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -27,7 +27,7 @@ class WebkitIndexedDB(WebkitBase):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -35,7 +35,7 @@ class WebkitIndexedDB(WebkitBase):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -25,7 +25,7 @@ class WebkitLocalStorage(WebkitBase):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -33,7 +33,7 @@ class WebkitLocalStorage(WebkitBase):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -25,7 +25,7 @@ class WebkitSafariViewService(WebkitBase):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -33,7 +33,7 @@ class WebkitSafariViewService(WebkitBase):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -27,7 +27,7 @@ class Applications(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -35,7 +35,7 @@ class Applications(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -25,7 +25,7 @@ class Calendar(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -33,7 +33,7 @@ class Calendar(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -5,7 +5,7 @@
import logging
import sqlite3
from typing import Union
from typing import Optional, Union
from mvt.common.utils import convert_mactime_to_iso
@@ -25,7 +25,7 @@ class Calls(IOSExtraction):
file_path: str = None,
target_path: str = None,
results_path: str = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: list = [],
) -> None:
@@ -33,7 +33,7 @@ class Calls(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -26,7 +26,7 @@ class ChromeFavicon(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -34,7 +34,7 @@ class ChromeFavicon(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -28,7 +28,7 @@ class ChromeHistory(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -36,7 +36,7 @@ class ChromeHistory(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -25,7 +25,7 @@ class Contacts(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -33,7 +33,7 @@ class Contacts(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -27,7 +27,7 @@ class FirefoxFavicon(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -35,7 +35,7 @@ class FirefoxFavicon(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

View File

@@ -31,7 +31,7 @@ class FirefoxHistory(IOSExtraction):
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
fast_mode: bool = False,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
@@ -39,7 +39,7 @@ class FirefoxHistory(IOSExtraction):
file_path=file_path,
target_path=target_path,
results_path=results_path,
fast_mode=fast_mode,
module_options=module_options,
log=log,
results=results,
)

Some files were not shown because too many files have changed in this diff Show More