1
mirror of https://github.com/mvt-project/mvt synced 2025-10-21 22:42:15 +02:00

Compare commits

...

11 Commits

Author SHA1 Message Date
tek
06bf7b9cb1 Bumps version 2023-03-29 14:44:59 +02:00
tek
b5d7e528de Adds indicators for android properties 2023-03-29 12:57:41 +02:00
tek
70c6f0c153 Adds latest iOS version 2023-03-27 23:10:25 +02:00
tek
49491800fb Improves typing 2023-03-24 19:02:02 +01:00
tek
1ad176788b Updates install instructions from sources 2023-03-24 15:11:21 +01:00
Donncha Ó Cearbhaill
11d58022cf Change checksum log message to info instead of warning 2023-03-03 21:21:32 +00:00
tek
cc205bfab0 Adds missing iOS versions 2023-03-02 15:47:37 -05:00
tek
671cd07200 Fixes a bug with YAML parsing of github workflow 2023-03-01 17:34:35 -05:00
tek
7581f81464 removes duplicated flake8 workflow 2023-03-01 16:50:33 -05:00
tek
4ed8ff51ff Improves code PEP8 compliance and adds ruff check 2023-03-01 16:43:08 -05:00
tek
fc4e2a9029 Improves logcat logging in mvt-android check-adb 2023-03-01 16:34:28 -05:00
42 changed files with 218 additions and 129 deletions

View File

@@ -1,26 +0,0 @@
name: Flake8
on:
push:
paths:
- '*.py'
jobs:
flake8_py3:
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 flake8
run: pip install flake8
- name: Run flake8
uses: suo/flake8-github-action@releases/v1
with:
checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -16,8 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
# python-version: [3.7, 3.8, 3.9]
python-version: [3.8, 3.9]
python-version: ['3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v2

21
.github/workflows/ruff.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Ruff
on: [push]
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
- name: ruff
run: |
ruff check .

View File

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

View File

@@ -54,7 +54,7 @@ Then you can install MVT directly from [pypi](https://pypi.org/project/mvt/)
pip3 install mvt
```
Or from the source code:
If you want to have the latest features in development, you can install MVT directly from the source code. If you installed MVT previously from pypi, you should first uninstall it using `pip3 uninstall mvt` and then install from the source code:
```bash
git clone https://github.com/mvt-project/mvt.git

View File

@@ -9,10 +9,9 @@ import click
from rich.logging import RichHandler
from mvt.common.cmd_check_iocs import CmdCheckIOCS
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_IOC,
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_HASHES, HELP_MSG_IOC,
HELP_MSG_LIST_MODULES, HELP_MSG_MODULE,
HELP_MSG_OUTPUT, HELP_MSG_SERIAL,
HELP_MSG_HASHES)
HELP_MSG_OUTPUT, HELP_MSG_SERIAL)
from mvt.common.logo import logo
from mvt.common.updates import IndicatorsUpdates

View File

@@ -9,15 +9,15 @@ import os
import sys
import tarfile
from pathlib import Path
from typing import Callable, Optional, List
from typing import List, Optional
from rich.prompt import Prompt
from mvt.android.modules.backup.base import BackupExtraction
from mvt.android.parsers.backup import (AndroidBackupParsingError,
InvalidBackupPassword, parse_ab_header,
parse_backup_file)
from mvt.common.command import Command
from mvt.android.modules.backup.base import BackupExtraction
from .modules.backup import BACKUP_MODULES

View File

@@ -6,11 +6,11 @@
import logging
import os
from pathlib import Path
from typing import Callable, Optional, List
from typing import List, Optional
from zipfile import ZipFile
from mvt.common.command import Command
from mvt.android.modules.bugreport.base import BugReportModule
from mvt.common.command import Command
from .modules.bugreport import BUGREPORT_MODULES

View File

@@ -31,6 +31,7 @@ class ChromeHistory(AndroidExtraction):
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = []
def serialize(self, record: dict) -> Union[dict, list]:
return {
@@ -55,6 +56,7 @@ class ChromeHistory(AndroidExtraction):
:param db_path: Path to the History database to process.
"""
assert isinstance(self.results, list) # assert results type for mypy
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("""

View File

@@ -39,7 +39,7 @@ class Files(AndroidExtraction):
log=log, results=results)
self.full_find = False
def serialize(self, record: dict) -> Union[dict, list]:
def serialize(self, record: dict) -> Union[dict, list, None]:
if "modified_time" in record:
return {
"timestamp": record["modified_time"],
@@ -62,6 +62,9 @@ class Files(AndroidExtraction):
self.detected.append(result)
def backup_file(self, file_path: str) -> None:
if not self.results_path:
return
local_file_name = file_path.replace("/", "_").replace(" ", "-")
local_files_folder = os.path.join(self.results_path, "files")
if not os.path.exists(local_files_folder):
@@ -79,6 +82,7 @@ class Files(AndroidExtraction):
file_path, local_file_path)
def find_files(self, folder: str) -> None:
assert isinstance(self.results, list)
if self.full_find:
cmd = f"find '{folder}' -type f -printf '%T@ %m %s %u %g %p\n' 2> /dev/null"
output = self._adb_command(cmd)
@@ -121,8 +125,7 @@ class Files(AndroidExtraction):
for entry in self.results:
self.log.info("Found file in tmp folder at path %s",
entry.get("path"))
if self.results_path:
self.backup_file(entry.get("path"))
self.backup_file(entry.get("path"))
for media_folder in ANDROID_MEDIA_FOLDERS:
self.find_files(media_folder)

View File

@@ -30,6 +30,16 @@ class Getprop(AndroidExtraction):
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")
@@ -38,13 +48,14 @@ class Getprop(AndroidExtraction):
self.results = parse_getprop(output)
# Alert if phone is outdated.
security_patch = self.results.get("ro.build.version.security_patch", "")
if security_patch:
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
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)",
security_patch)
entry["value"])
self.log.info("Extracted %d Android system properties",
len(self.results))

View File

@@ -30,9 +30,9 @@ class Logcat(AndroidExtraction):
self._adb_connect()
# Get the current logcat.
output = self._adb_command("logcat -d")
output = self._adb_command("logcat -d -b all \"*:V\"")
# Get the locat prior to last reboot.
last_output = self._adb_command("logcat -L")
last_output = self._adb_command("logcat -L -b all \"*:V\"")
if self.results_path:
logcat_path = os.path.join(self.results_path,

View File

@@ -4,7 +4,7 @@
# https://license.mvt.re/1.1/
import logging
from typing import Optional, Union, List
from typing import List, Optional, Union
from rich.console import Console
from rich.progress import track

View File

@@ -6,7 +6,7 @@
import fnmatch
import logging
import os
from typing import Union, List, Dict, Any, Optional
from typing import Any, Dict, List, Optional, Union
from mvt.common.module import MVTModule

View File

@@ -4,14 +4,12 @@
# https://license.mvt.re/1.1/
import logging
from datetime import datetime
from typing import Optional, Union, List, Any, Dict
from typing import Any, Dict, List, Optional, Union
from mvt.android.modules.adb.packages import (DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
ROOT_PACKAGES)
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
from mvt.common.utils import convert_datetime_to_iso
from .base import AndroidQFModule

View File

@@ -4,7 +4,7 @@
# https://license.mvt.re/1.1/
import logging
from typing import Optional, List, Dict, Union, Any
from typing import Any, Dict, List, Optional, Union
from mvt.android.modules.adb.dumpsys_receivers import (
INTENT_DATA_SMS_RECEIVED, INTENT_NEW_OUTGOING_CALL,

View File

@@ -7,7 +7,7 @@ import logging
from datetime import datetime, timedelta
from typing import Optional
from mvt.android.parsers import getprop
from mvt.android.parsers.getprop import parse_getprop
from .base import AndroidQFModule
@@ -41,7 +41,17 @@ class Getprop(AndroidQFModule):
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.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")
@@ -52,15 +62,15 @@ class Getprop(AndroidQFModule):
with open(getprop_files[0]) as f:
data = f.read()
self.results = getprop.parse_getprop(data)
self.results = parse_getprop(data)
for entry in self.results:
if entry in INTERESTING_PROPERTIES:
self.log.info("%s: %s", entry, self.results[entry])
if entry == "ro.build.version.security_patch":
last_patch = datetime.strptime(self.results[entry], "%Y-%m-%d")
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)", self.results[entry])
"(last update: %s)", entry["value"])
self.log.info("Extracted a total of %d properties", len(self.results))

View File

@@ -1,7 +1,7 @@
# Mobile Verification Toolkit (MVT) - Private
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 Claudio Guarnieri.
# This file is part of MVT Private and its content is confidential.
# Please refer to the project maintainers before sharing with others.
# 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

View File

@@ -7,7 +7,7 @@ import fnmatch
import logging
import os
from tarfile import TarFile
from typing import Optional, List
from typing import List, Optional
from mvt.common.module import MVTModule

View File

@@ -6,7 +6,7 @@
import fnmatch
import logging
import os
from typing import Optional, List
from typing import List, Optional
from zipfile import ZipFile
from mvt.common.module import MVTModule

View File

@@ -4,8 +4,8 @@
# https://license.mvt.re/1.1/
import re
from typing import List, Dict, Any
from datetime import datetime
from typing import Any, Dict, List
from mvt.common.utils import convert_datetime_to_iso

View File

@@ -4,10 +4,11 @@
# https://license.mvt.re/1.1/
import re
from typing import Dict, List
def parse_getprop(output: str) -> dict:
results = {}
def parse_getprop(output: str) -> List[Dict[str, str]]:
results = []
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
for line in output.splitlines():
@@ -19,8 +20,10 @@ def parse_getprop(output: str) -> dict:
if not matches or len(matches[0]) != 2:
continue
key = matches[0][0]
value = matches[0][1]
results[key] = value
entry = {
"name": matches[0][0],
"value": matches[0][1]
}
results.append(entry)
return results

View File

@@ -30,6 +30,7 @@ class CmdCheckIOCS(Command):
self.name = "check-iocs"
def run(self) -> None:
assert self.target_path is not None
all_modules = []
for entry in self.modules:
if entry not in all_modules:

View File

@@ -3,17 +3,18 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import hashlib
import json
import logging
import os
import sys
from datetime import datetime
from typing import Callable, Optional
from typing import Optional
from mvt.common.indicators import Indicators
from mvt.common.module import run_module, save_timeline, MVTModule
from mvt.common.utils import convert_datetime_to_iso, generate_hashes_from_path, get_sha256_from_file_path
from mvt.common.module import MVTModule, run_module, save_timeline
from mvt.common.utils import (convert_datetime_to_iso,
generate_hashes_from_path,
get_sha256_from_file_path)
from mvt.common.version import MVT_VERSION
@@ -121,7 +122,7 @@ class Command:
if self.target_path and (os.environ.get("MVT_HASH_FILES") or self.hashes):
info_hash = get_sha256_from_file_path(info_path)
self.log.warning("Reference hash of the info.json file : %s", info_hash)
self.log.info("Reference hash of the info.json file: \"%s\"", info_hash)
def generate_hashes(self) -> None:
"""

View File

@@ -6,7 +6,7 @@
import json
import logging
import os
from typing import Optional, Union
from typing import Any, Dict, Iterator, List, Optional, Union
from appdirs import user_data_dir
@@ -23,7 +23,7 @@ class Indicators:
def __init__(self, log=logging.Logger) -> None:
self.log = log
self.ioc_collections = []
self.ioc_collections: List[Dict[str, Any]] = []
self.total_ioc_count = 0
def _load_downloaded_indicators(self) -> None:
@@ -72,6 +72,7 @@ class Indicators:
"files_sha256": [],
"app_ids": [],
"ios_profile_ids": [],
"android_property_names": [],
"count": 0,
}
@@ -121,6 +122,11 @@ class Indicators:
ioc_coll=collection,
ioc_coll_list=collection["ios_profile_ids"])
elif key == "android-property:name":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["android_property_names"])
def parse_stix2(self, file_path: str) -> None:
"""Extract indicators from a STIX2 file.
@@ -209,7 +215,7 @@ class Indicators:
self.log.info("Loaded a total of %d unique indicators",
self.total_ioc_count)
def get_iocs(self, ioc_type: str) -> Union[dict, None]:
def get_iocs(self, ioc_type: str) -> Union[Iterator[Dict[str, Any]], None]:
for ioc_collection in self.ioc_collections:
for ioc in ioc_collection.get(ioc_type, []):
yield {
@@ -519,3 +525,23 @@ class Indicators:
return ioc
return None
def check_android_property_name(self, property_name: str) -> Optional[dict]:
"""Check the android property name against the list of indicators.
:param property_name: Name of the Android property
:type property_name: str
:returns: Indicator details if matched, otherwise None
"""
if property_name is None:
return None
for ioc in self.get_iocs("android_property_names"):
if property_name.lower() == ioc["value"].lower():
self.log.warning("Found a known suspicious Android property \"%s\" "
"matching indicators from \"%s\"", property_name,
ioc["name"])
return ioc
return None

View File

@@ -7,7 +7,7 @@ import csv
import logging
import os
import re
from typing import Callable, Optional, Union, List, Any, Dict
from typing import Any, Dict, List, Optional, Union
import simplejson as json
@@ -28,7 +28,7 @@ class MVTModule:
"""This class provides a base for all extraction modules."""
enabled = True
slug = None
slug: Optional[str] = None
def __init__(
self,
@@ -61,12 +61,12 @@ class MVTModule:
self.log = log
self.indicators = None
self.results = results if results else []
self.detected = []
self.timeline = []
self.timeline_detected = []
self.detected: List[Dict[str, Any]] = []
self.timeline: List[Dict[str, str]] = []
self.timeline_detected: List[Dict[str, str]] = []
@classmethod
def from_json(cls, json_path: str, log: logging.Logger = None):
def from_json(cls, json_path: str, log: logging.Logger):
with open(json_path, "r", encoding="utf-8") as handle:
results = json.load(handle)
if log:
@@ -116,7 +116,7 @@ class MVTModule:
with open(detected_json_path, "w", encoding="utf-8") as handle:
json.dump(self.detected, handle, indent=4, default=str)
def serialize(self, record: dict) -> Union[dict, list]:
def serialize(self, record: dict) -> Union[dict, list, None]:
raise NotImplementedError
@staticmethod
@@ -159,7 +159,7 @@ class MVTModule:
raise NotImplementedError
def run_module(module: Callable) -> None:
def run_module(module: MVTModule) -> None:
module.log.info("Running module %s...", module.__class__.__name__)
try:

View File

@@ -6,6 +6,7 @@
import logging
import os
from datetime import datetime
from typing import Optional, Tuple
import requests
import yaml
@@ -83,7 +84,7 @@ class IndicatorsUpdates:
with open(self.latest_update_path, "w", encoding="utf-8") as handle:
handle.write(str(timestamp))
def get_remote_index(self) -> dict:
def get_remote_index(self) -> Optional[dict]:
url = self.github_raw_url.format(self.index_owner, self.index_repo,
self.index_branch, self.index_path)
res = requests.get(url)
@@ -94,7 +95,7 @@ class IndicatorsUpdates:
return yaml.safe_load(res.content)
def download_remote_ioc(self, ioc_url: str) -> str:
def download_remote_ioc(self, ioc_url: str) -> Optional[str]:
res = requests.get(ioc_url)
if res.status_code != 200:
log.error("Failed to download indicators file from %s (error %d)",
@@ -116,6 +117,9 @@ class IndicatorsUpdates:
os.makedirs(MVT_INDICATORS_FOLDER)
index = self.get_remote_index()
if not index:
return
for ioc in index.get("indicators", []):
ioc_type = ioc.get("type", "")
@@ -171,7 +175,7 @@ class IndicatorsUpdates:
return latest_commit_ts
def should_check(self) -> (bool, int):
def should_check(self) -> Tuple[bool, int]:
now = datetime.utcnow()
latest_check_ts = self.get_latest_check()
latest_check_dt = datetime.fromtimestamp(latest_check_ts)
@@ -182,7 +186,7 @@ class IndicatorsUpdates:
if diff_hours >= INDICATORS_CHECK_FREQUENCY:
return True, 0
return False, INDICATORS_CHECK_FREQUENCY - diff_hours
return False, int(INDICATORS_CHECK_FREQUENCY - diff_hours)
def check(self) -> bool:
self.set_latest_check()
@@ -197,6 +201,9 @@ class IndicatorsUpdates:
return True
index = self.get_remote_index()
if not index:
return False
for ioc in index.get("indicators", []):
if ioc.get("type", "") != "github":
continue

View File

@@ -3,11 +3,11 @@
# 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
import datetime
import hashlib
import os
import re
from typing import Union, Iterator
from typing import Any, Iterator, Union
def convert_chrometime_to_datetime(timestamp: int) -> datetime.datetime:
@@ -51,7 +51,7 @@ def convert_unix_to_utc_datetime(
return datetime.datetime.utcfromtimestamp(float(timestamp))
def convert_unix_to_iso(timestamp: int) -> str:
def convert_unix_to_iso(timestamp: Union[int, float, str]) -> str:
"""Converts a unix epoch to ISO string.
:param timestamp: Epoc timestamp to convert.
@@ -125,7 +125,7 @@ def check_for_links(text: str) -> list:
# Note: taken from here:
# https://stackoverflow.com/questions/57014259/json-dumps-on-dictionary-with-bytes-for-keys
def keys_bytes_to_string(obj) -> str:
def keys_bytes_to_string(obj: Any) -> Any:
"""Convert object keys from bytes to string.
:param obj: Object to convert from bytes to string.

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.2.2"
MVT_VERSION = "2.2.3"

View File

@@ -3,18 +3,18 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import json
import logging
import os
import json
import click
from rich.logging import RichHandler
from rich.prompt import Prompt
from mvt.common.cmd_check_iocs import CmdCheckIOCS
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_IOC,
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_HASHES, HELP_MSG_IOC,
HELP_MSG_LIST_MODULES, HELP_MSG_MODULE,
HELP_MSG_OUTPUT, HELP_MSG_HASHES)
HELP_MSG_OUTPUT)
from mvt.common.logo import logo
from mvt.common.options import MutuallyExclusiveOption
from mvt.common.updates import IndicatorsUpdates

View File

@@ -2,9 +2,8 @@
# 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 typing import Dict
from logging import Logger
from typing import Optional
from typing import Dict, Optional
import packaging
@@ -264,12 +263,16 @@ IPHONE_IOS_VERSIONS = [
{"build": "19H117", "version": "15.7.1"},
{"build": "19H218", "version": "15.7.2"},
{"build": "20A362", "version": "16.0"},
{"build": "20A371", "version": "16.0.1"},
{"build": "20A380", "version": "16.0.2"},
{"build": "20A392", "version": "16.0.3"},
{"build": "20B82", "version": "16.1"},
{"build": "20B101", "version": "16.1.1"},
{"build": "20B110", "version": "16.1.2"},
{"build": "20C65", "version": "16.2"},
{"build": "20D47", "version": "16.3"},
{"build": "20D67", "version": "16.3.1"}
{"build": "20D67", "version": "16.3.1"},
{"build": "20E247", "version": "16.4"}
]

6
ruff.toml Normal file
View File

@@ -0,0 +1,6 @@
# Never enforce `E501` (line length violations).
ignore = ["E501"]
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
[per-file-ignores]
"__init__.py" = ["F401"]

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