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

Compare commits

..

41 Commits

Author SHA1 Message Date
Donncha Ó Cearbhaill
2d16218489 Bump version to v1.3.2 2021-12-17 12:24:41 +01:00
Donncha Ó Cearbhaill
3215e797ec Bug fixes for config profile and shortcut module 2021-12-16 22:58:36 +01:00
Donncha Ó Cearbhaill
e65a598903 Add link to Cytrox indicators of compromise in docs 2021-12-16 21:01:56 +01:00
Donncha Ó Cearbhaill
e80c02451c Bump version to 1.3.1. Skipping 1.3 as a tag already exists 2021-12-16 19:27:58 +01:00
Donncha Ó Cearbhaill
5df50f864c Merge branch 'main' into main 2021-12-16 19:21:18 +01:00
Donncha Ó Cearbhaill
45b31bb718 Add support for indentifying known malicious file paths over ADB 2021-12-16 19:16:24 +01:00
Donncha Ó Cearbhaill
e10f1767e6 Update WhatsApp module to search for links in attachments 2021-12-16 18:46:31 +01:00
tek
d64277c0bf Adds missing iOS version 2021-12-16 18:39:22 +01:00
Donncha Ó Cearbhaill
3f3261511a Add module to search for known malicious or suspicious configuration profiles 2021-12-16 17:57:26 +01:00
Donncha Ó Cearbhaill
4cfe75e2d4 Add module to parse iOS Shortcuts and search for malicious actions 2021-12-16 17:47:08 +01:00
tek
cdd90332f7 Adds timeline support to TCC iOS module 2021-12-16 13:57:44 +01:00
tek
d9b29b3739 Fixes indicator issue in the android cli 2021-12-16 12:51:57 +01:00
tek
79bb7d1d4b Fixes indiator parsing bug 2021-12-13 18:37:05 +01:00
tek
a653cb3cfc Implements loading STIX files from env variable MVT_STIX2 2021-12-10 16:11:59 +01:00
tek
b25cc48be0 Fixes issue in Safari Browser State for older iOS versions 2021-12-06 15:04:52 +01:00
tek
40bd9ddc1d Fixes issue with different TCC database versions 2021-12-03 20:31:12 +01:00
Tek
deb95297da Merge pull request #219 from workingreact/main
Fix ConfigurationProfiles
2021-12-03 19:56:43 +01:00
tek
02014b414b Add warning for apple notification 2021-12-03 19:42:35 +01:00
tek
7dd5fe7831 Catch and recover malformed SMS database 2021-12-03 17:46:41 +01:00
workingreact
11d1a3dcee fix typo 2021-12-02 18:31:07 +01:00
workingreact
74f9db2bf2 fix ConfigurationProfiles 2021-12-02 16:55:14 +01:00
tek
356bddc3af Adds new iOS versions 2021-11-28 17:43:50 +01:00
Nex
512f40dcb4 Standardized code with flake8 2021-11-19 15:27:51 +01:00
Nex
b3a464ba58 Removed unused imports 2021-11-19 14:54:53 +01:00
Nex
529df85f0f Sorted imports 2021-11-04 12:58:35 +01:00
Nex
19a6da8fe7 Merge pull request #213 from panelmix/main
Replace NetworkingAnalytics with Analytics
2021-11-02 15:02:57 +01:00
panelmix
34c997f923 Replace NetworkingAnalytics with Analytics 2021-11-02 13:29:12 +01:00
Nex
02bf903411 Bumped version 2021-10-30 13:40:25 +02:00
Nex
7019375767 Merge pull request #210 from hurtcrushing/main
Search for entries in ZPROCESS but not in ZLIVEUSAGE
2021-10-27 14:22:40 +02:00
Nex
34dd27c5d2 Added iPhone 13 2021-10-26 18:33:07 +02:00
Nex
a4d6a08a8b Added iOS 15.1 2021-10-26 18:09:31 +02:00
hurtcrushing
635d3a392d change warning to info 2021-10-25 14:54:03 +02:00
hurtcrushing
2d78bddbba Search for entries in ZPROCESS but not in ZLIVEUSAGE 2021-10-25 14:34:18 +02:00
Nex
c1938d2ead Merge branch 'main' of github.com:mvt-project/mvt 2021-10-25 11:18:12 +02:00
Nex
104b01e5cd Fixed links to docs 2021-10-25 09:19:10 +02:00
Nex
7087e8adb2 Merge pull request #209 from mvt-project/dependabot/pip/docs/mkdocs-1.2.3
Bump mkdocs from 1.2.1 to 1.2.3 in /docs
2021-10-23 20:17:18 +02:00
dependabot[bot]
67608ac02b Bump mkdocs from 1.2.1 to 1.2.3 in /docs
Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.2.1 to 1.2.3.
- [Release notes](https://github.com/mkdocs/mkdocs/releases)
- [Commits](https://github.com/mkdocs/mkdocs/compare/1.2.1...1.2.3)

---
updated-dependencies:
- dependency-name: mkdocs
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-23 11:56:25 +00:00
Nex
6d8de5b461 Bumped version 2021-10-23 13:51:44 +02:00
Nex
b0177d6104 Upgraded adb-shell 2021-10-23 13:51:33 +02:00
tek
e0c9a44b10 Merge branch 'main' of github.com:mvt-project/mvt 2021-10-21 21:17:31 +02:00
tek
ef8c1ae895 Adds recent iOS versions 2021-10-21 21:17:09 +02:00
70 changed files with 850 additions and 295 deletions

View File

@@ -15,15 +15,15 @@ It has been developed and released by the [Amnesty International Security Lab](h
## Installation ## Installation
MVT can be installed from sources or from [PyPi](https://pypi.org/project/mvt/) (you will need some dependencies, check the [documentation](https://docs.mvt.re/en/latest/install.html)): MVT can be installed from sources or from [PyPi](https://pypi.org/project/mvt/) (you will need some dependencies, check the [documentation](https://docs.mvt.re/en/latest/install/)):
``` ```
pip3 install mvt pip3 install mvt
``` ```
Alternatively, you can decide to run MVT and all relevant tools through a [Docker container](https://docs.mvt.re/en/latest/docker.html). Alternatively, you can decide to run MVT and all relevant tools through a [Docker container](https://docs.mvt.re/en/latest/docker/).
**Please note:** MVT is best run on Linux or Mac systems. [It does not currently support running natively on Windows.](https://docs.mvt.re/en/latest/install.html#mvt-on-windows) **Please note:** MVT is best run on Linux or Mac systems. [It does not currently support running natively on Windows.](https://docs.mvt.re/en/latest/install/#mvt-on-windows)
## Usage ## Usage
@@ -31,4 +31,4 @@ MVT provides two commands `mvt-ios` and `mvt-android`. [Check out the documentat
## License ## License
The purpose of MVT is to facilitate the ***consensual forensic analysis*** of devices of those who might be targets of sophisticated mobile spyware attacks, especially members of civil society and marginalized communities. We do not want MVT to enable privacy violations of non-consenting individuals. In order to achieve this, MVT is released under its own license. [Read more here.](https://docs.mvt.re/en/latest/license.html) The purpose of MVT is to facilitate the ***consensual forensic analysis*** of devices of those who might be targets of sophisticated mobile spyware attacks, especially members of civil society and marginalized communities. We do not want MVT to enable privacy violations of non-consenting individuals. In order to achieve this, MVT is released under its own license. [Read more here.](https://docs.mvt.re/en/latest/license/)

View File

@@ -28,10 +28,17 @@ The `--iocs` option can be invoked multiple times to let MVT import multiple STI
mvt-ios check-backup --iocs ~/iocs/malware1.stix --iocs ~/iocs/malware2.stix2 /path/to/backup mvt-ios check-backup --iocs ~/iocs/malware1.stix --iocs ~/iocs/malware2.stix2 /path/to/backup
``` ```
It is also possible to load STIX2 files automatically from the environment variable `MVT_STIX2`:
```bash
export MVT_STIX2="/home/user/IOC1.stix2:/home/user/IOC2.stix2"
```
## Known repositories of STIX2 IOCs ## Known repositories of STIX2 IOCs
- The [Amnesty International investigations repository](https://github.com/AmnestyTech/investigations) contains STIX-formatted IOCs for: - The [Amnesty International investigations repository](https://github.com/AmnestyTech/investigations) contains STIX-formatted IOCs for:
- [Pegasus](https://en.wikipedia.org/wiki/Pegasus_(spyware)) ([STIX2](https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-07-18_nso/pegasus.stix2)) - [Pegasus](https://en.wikipedia.org/wiki/Pegasus_(spyware)) ([STIX2](https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-07-18_nso/pegasus.stix2))
- [Predator from Cytrox](https://citizenlab.ca/2021/12/pegasus-vs-predator-dissidents-doubly-infected-iphone-reveals-cytrox-mercenary-spyware/) ([STIX2](https://github.com/AmnestyTech/investigations/tree/master/2021-12-16_cytrox/cytrox.stix2))
- [This repository](https://github.com/Te-k/stalkerware-indicators) contains IOCs for Android stalkerware including [a STIX MVT-compatible file](https://github.com/Te-k/stalkerware-indicators/blob/master/stalkerware.stix2). - [This repository](https://github.com/Te-k/stalkerware-indicators) contains IOCs for Android stalkerware including [a STIX MVT-compatible file](https://github.com/Te-k/stalkerware-indicators/blob/master/stalkerware.stix2).
Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs. Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs.

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
mkdocs==1.2.1 mkdocs==1.2.3
mkdocs-autorefs mkdocs-autorefs
mkdocs-material mkdocs-material
mkdocs-material-extensions mkdocs-material-extensions

View File

@@ -9,7 +9,9 @@ import os
import click import click
from rich.logging import RichHandler from rich.logging import RichHandler
from mvt.common.help import * from mvt.common.help import HELP_MSG_MODULE, HELP_MSG_IOC
from mvt.common.help import HELP_MSG_FAST, HELP_MSG_OUTPUT, HELP_MSG_LIST_MODULES
from mvt.common.help import HELP_MSG_SERIAL
from mvt.common.indicators import Indicators, IndicatorsFileBadFormat from mvt.common.indicators import Indicators, IndicatorsFileBadFormat
from mvt.common.logo import logo from mvt.common.logo import logo
from mvt.common.module import run_module, save_timeline from mvt.common.module import run_module, save_timeline
@@ -26,6 +28,7 @@ logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")]) RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
#============================================================================== #==============================================================================
# Main # Main
#============================================================================== #==============================================================================
@@ -104,10 +107,11 @@ def download_apks(ctx, all_apks, virustotal, koodous, all_checks, output, from_f
default=[], help=HELP_MSG_IOC) default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), @click.option("--output", "-o", type=click.Path(exists=False),
help=HELP_MSG_OUTPUT) help=HELP_MSG_OUTPUT)
@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("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE) @click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.pass_context @click.pass_context
def check_adb(ctx, iocs, output, list_modules, module, serial): def check_adb(ctx, iocs, output, fast, list_modules, module, serial):
if list_modules: if list_modules:
log.info("Following is the list of available check-adb modules:") log.info("Following is the list of available check-adb modules:")
for adb_module in ADB_MODULES: for adb_module in ADB_MODULES:
@@ -139,7 +143,8 @@ def check_adb(ctx, iocs, output, list_modules, module, serial):
if module and adb_module.__name__ != module: if module and adb_module.__name__ != module:
continue continue
m = adb_module(output_folder=output, log=logging.getLogger(adb_module.__module__)) m = adb_module(output_folder=output, fast_mode=fast,
log=logging.getLogger(adb_module.__module__))
if serial: if serial:
m.serial = serial m.serial = serial
@@ -191,7 +196,7 @@ def check_backup(ctx, iocs, output, backup_path, serial):
log.critical("The path you specified is a not a folder!") log.critical("The path you specified is a not a folder!")
if os.path.basename(backup_path) == "backup.ab": if os.path.basename(backup_path) == "backup.ab":
log.info("You can use ABE (https://github.com/nelenkov/android-backup-extractor) " \ log.info("You can use ABE (https://github.com/nelenkov/android-backup-extractor) "
"to extract 'backup.ab' files!") "to extract 'backup.ab' files!")
ctx.exit(1) ctx.exit(1)
@@ -202,7 +207,7 @@ def check_backup(ctx, iocs, output, backup_path, serial):
if serial: if serial:
m.serial = serial m.serial = serial
if iocs: if len(indicators.ioc_count) > 0:
indicators.log = m.log indicators.log = m.log
m.indicators = indicators m.indicators = indicators

View File

@@ -7,7 +7,6 @@ import json
import logging import logging
import os import os
import pkg_resources
from tqdm import tqdm from tqdm import tqdm
from mvt.common.module import InsufficientPrivileges from mvt.common.module import InsufficientPrivileges
@@ -17,6 +16,7 @@ from .modules.adb.packages import Packages
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# TODO: Would be better to replace tqdm with rich.progress to reduce # TODO: Would be better to replace tqdm with rich.progress to reduce
# the number of dependencies. Need to investigate whether # the number of dependencies. Need to investigate whether
# it's possible to have a similar callback system. # it's possible to have a similar callback system.
@@ -138,7 +138,7 @@ class DownloadAPKs(AndroidExtraction):
packages_selection.append(package) packages_selection.append(package)
log.info("Selected only %d packages which are not marked as system", log.info("Selected only %d packages which are not marked as system",
len(packages_selection)) len(packages_selection))
if len(packages_selection) == 0: if len(packages_selection) == 0:
log.info("No packages were selected for download") log.info("No packages were selected for download")

View File

@@ -13,6 +13,7 @@ from rich.text import Text
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def koodous_lookup(packages): def koodous_lookup(packages):
log.info("Looking up all extracted files on Koodous (www.koodous.com)") log.info("Looking up all extracted files on Koodous (www.koodous.com)")
log.info("This might take a while...") log.info("This might take a while...")

View File

@@ -13,6 +13,7 @@ from rich.text import Text
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_virustotal_report(hashes): def get_virustotal_report(hashes):
apikey = "233f22e200ca5822bd91103043ccac138b910db79f29af5616a9afe8b6f215ad" apikey = "233f22e200ca5822bd91103043ccac138b910db79f29af5616a9afe8b6f215ad"
url = f"https://www.virustotal.com/partners/sysinternals/file-reports?apikey={apikey}" url = f"https://www.virustotal.com/partners/sysinternals/file-reports?apikey={apikey}"
@@ -36,6 +37,7 @@ def get_virustotal_report(hashes):
log.error("Unexpected response from VirusTotal: %s", res.status_code) log.error("Unexpected response from VirusTotal: %s", res.status_code)
return None return None
def virustotal_lookup(packages): def virustotal_lookup(packages):
log.info("Looking up all extracted files on VirusTotal (www.virustotal.com)") log.info("Looking up all extracted files on VirusTotal (www.virustotal.com)")
@@ -48,6 +50,7 @@ def virustotal_lookup(packages):
total_unique_hashes = len(unique_hashes) total_unique_hashes = len(unique_hashes)
detections = {} detections = {}
def virustotal_query(batch): def virustotal_query(batch):
report = get_virustotal_report(batch) report = get_virustotal_report(batch)
if not report: if not report:

View File

@@ -25,6 +25,7 @@ log = logging.getLogger(__name__)
ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey") ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey")
ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub") ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub")
class AndroidExtraction(MVTModule): class AndroidExtraction(MVTModule):
"""This class provides a base for all Android extraction modules.""" """This class provides a base for all Android extraction modules."""
@@ -89,7 +90,7 @@ class AndroidExtraction(MVTModule):
except OSError as e: except OSError as e:
if e.errno == 113 and self.serial: if e.errno == 113 and self.serial:
log.critical("Unable to connect to the device %s: did you specify the correct IP addres?", log.critical("Unable to connect to the device %s: did you specify the correct IP addres?",
self.serial) self.serial)
sys.exit(-1) sys.exit(-1)
else: else:
break break

View File

@@ -16,6 +16,7 @@ log = logging.getLogger(__name__)
CHROME_HISTORY_PATH = "data/data/com.android.chrome/app_chrome/Default/History" CHROME_HISTORY_PATH = "data/data/com.android.chrome/app_chrome/Default/History"
class ChromeHistory(AndroidExtraction): class ChromeHistory(AndroidExtraction):
"""This module extracts records from Android's Chrome browsing history.""" """This module extracts records from Android's Chrome browsing history."""

View File

@@ -10,6 +10,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class DumpsysBatterystats(AndroidExtraction): class DumpsysBatterystats(AndroidExtraction):
"""This module extracts stats on battery consumption by processes.""" """This module extracts stats on battery consumption by processes."""
@@ -30,7 +31,7 @@ class DumpsysBatterystats(AndroidExtraction):
handle.write(stats) handle.write(stats)
log.info("Records from dumpsys batterystats stored at %s", log.info("Records from dumpsys batterystats stored at %s",
stats_path) stats_path)
history = self._adb_command("dumpsys batterystats --history") history = self._adb_command("dumpsys batterystats --history")
if self.output_folder: if self.output_folder:

View File

@@ -10,6 +10,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class DumpsysFull(AndroidExtraction): class DumpsysFull(AndroidExtraction):
"""This module extracts stats on battery consumption by processes.""" """This module extracts stats on battery consumption by processes."""
@@ -30,6 +31,6 @@ class DumpsysFull(AndroidExtraction):
handle.write(stats) handle.write(stats)
log.info("Full dumpsys output stored at %s", log.info("Full dumpsys output stored at %s",
stats_path) stats_path)
self._adb_disconnect() self._adb_disconnect()

View File

@@ -10,6 +10,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class DumpsysProcstats(AndroidExtraction): class DumpsysProcstats(AndroidExtraction):
"""This module extracts stats on memory consumption by processes.""" """This module extracts stats on memory consumption by processes."""

View File

@@ -4,7 +4,6 @@
# https://license.mvt.re/1.1/ # https://license.mvt.re/1.1/
import logging import logging
import os
from .base import AndroidExtraction from .base import AndroidExtraction
@@ -15,6 +14,7 @@ ACTION_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
ACTION_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED" ACTION_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
ACTION_PHONE_STATE = "android.intent.action.PHONE_STATE" ACTION_PHONE_STATE = "android.intent.action.PHONE_STATE"
class DumpsysReceivers(AndroidExtraction): class DumpsysReceivers(AndroidExtraction):
"""This module extracts details on receivers for risky activities.""" """This module extracts details on receivers for risky activities."""
@@ -67,16 +67,16 @@ class DumpsysReceivers(AndroidExtraction):
if activity == ACTION_NEW_OUTGOING_SMS: if activity == ACTION_NEW_OUTGOING_SMS:
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"", self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
receiver) receiver)
elif activity == ACTION_SMS_RECEIVED: elif activity == ACTION_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"", self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
receiver) receiver)
elif activity == ACTION_DATA_SMS_RECEIVED: elif activity == ACTION_DATA_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"", self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
receiver) receiver)
elif activity == ACTION_PHONE_STATE: elif activity == ACTION_PHONE_STATE:
self.log.info("Found a receiver monitoring telephony state: \"%s\"", self.log.info("Found a receiver monitoring telephony state: \"%s\"",
receiver) receiver)
self.results.append({ self.results.append({
"activity": activity, "activity": activity,

View File

@@ -5,29 +5,120 @@
import logging import logging
import os import os
import stat
import datetime
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from .base import AndroidExtraction from .base import AndroidExtraction
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Files(AndroidExtraction): class Files(AndroidExtraction):
"""This module extracts the list of installed packages.""" """This module extracts the list of files on the device."""
def __init__(self, file_path=None, base_folder=None, output_folder=None, def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]): serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder, super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode, output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results) log=log, results=results)
self.full_find = None
def find_path(self, file_path):
"""Checks if Android system supports full find command output"""
# Check find command params on first run
# Run find command with correct args and parse results.
# Check that full file printf options are suppported on first run.
if self.full_find == None:
output = self._adb_command(f"find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
if not (output or output.strip().splitlines()):
# Full find command failed to generate output, fallback to basic file arguments
self.full_find = False
else:
self.full_find = True
found_files = []
if self.full_find == True:
# Run full file command and collect additonal file information.
output = self._adb_command(f"find '{file_path}' -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
for file_line in output.splitlines():
[unix_timestamp, mode, size, owner, group, full_path] = file_line.rstrip().split(" ", 5)
mod_time = convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(int(float(unix_timestamp))))
found_files.append({
"path": full_path,
"modified_time": mod_time,
"mode": mode,
"is_suid": (int(mode, 8) & stat.S_ISUID) == 2048,
"is_sgid": (int(mode, 8) & stat.S_ISGID) == 1024,
"size": size,
"owner": owner,
"group": group,
})
else:
# Run a basic listing of file paths.
output = self._adb_command(f"find '{file_path}' 2> /dev/null")
for file_line in output.splitlines():
found_files.append({
"path": file_line.rstrip()
})
return found_files
def serialize(self, record):
if "modified_time" in record:
return {
"timestamp": record["modified_time"],
"module": self.__class__.__name__,
"event": "file_modified",
"data": record["path"],
}
def check_suspicious(self):
"""Check for files with suspicious permissions"""
for result in sorted(self.results, key=lambda item: item["path"]):
if result.get("is_suid"):
self.log.warning("Found an SUID file in a non-standard directory \"%s\".",
result["path"])
self.detected.append(result)
def check_indicators(self):
"""Check file list for known suspicious files or suspicious properties"""
self.check_suspicious()
if not self.indicators:
return
for result in self.results:
if self.indicators.check_filename(result["path"]):
self.log.warning("Found a known suspicous filename at path: \"%s\"", result["path"])
self.detected.append(result)
if self.indicators.check_file_path(result["path"]):
self.log.warning("Found a known suspicous file at path: \"%s\"", result["path"])
self.detected.append(result)
def run(self): def run(self):
self._adb_connect() self._adb_connect()
found_file_paths = []
output = self._adb_command("find / -type f 2> /dev/null") DATA_PATHS = ["/data/local/tmp/", "/sdcard/", "/tmp/"]
if output and self.output_folder: for path in DATA_PATHS:
files_txt_path = os.path.join(self.output_folder, "files.txt") file_info = self.find_path(path)
with open(files_txt_path, "w") as handle: found_file_paths.extend(file_info)
handle.write(output)
log.info("List of visible files stored at %s", files_txt_path) # Store results
self.results.extend(found_file_paths)
self.log.info("Found %s files in primary Android data directories.", len(found_file_paths))
if self.fast_mode:
self.log.info("Flag --fast was enabled: skipping full file listing")
else:
self.log.info("Flag --fast was not enabled: processing full file listing. "
"This may take a while...")
output = self.find_path("/")
if output and self.output_folder:
self.results.extend(output)
log.info("List of visible files stored in files.json")
self._adb_disconnect() self._adb_disconnect()

View File

@@ -12,6 +12,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Packages(AndroidExtraction): class Packages(AndroidExtraction):
"""This module extracts the list of installed packages.""" """This module extracts the list of installed packages."""
@@ -49,11 +50,10 @@ class Packages(AndroidExtraction):
root_packages = root_packages_string.decode("utf-8").split("\n") root_packages = root_packages_string.decode("utf-8").split("\n")
root_packages = [rp.strip() for rp in root_packages] root_packages = [rp.strip() for rp in root_packages]
for result in self.results: for result in self.results:
if result["package_name"] in root_packages: if result["package_name"] in root_packages:
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"", self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
result["package_name"]) result["package_name"])
self.detected.append(result) self.detected.append(result)
if result["package_name"] in self.indicators.ioc_app_ids: if result["package_name"] in self.indicators.ioc_app_ids:
self.log.warning("Found a malicious package name: \"%s\"", self.log.warning("Found a malicious package name: \"%s\"",

View File

@@ -9,6 +9,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Processes(AndroidExtraction): class Processes(AndroidExtraction):
"""This module extracts details on running processes.""" """This module extracts details on running processes."""

View File

@@ -12,6 +12,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class RootBinaries(AndroidExtraction): class RootBinaries(AndroidExtraction):
"""This module extracts the list of installed packages.""" """This module extracts the list of installed packages."""

View File

@@ -15,12 +15,12 @@ log = logging.getLogger(__name__)
SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db" SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
SMS_BUGLE_QUERY = """ SMS_BUGLE_QUERY = """
SELECT SELECT
ppl.normalized_destination AS number, ppl.normalized_destination AS number,
p.timestamp AS timestamp, p.timestamp AS timestamp,
CASE WHEN m.sender_id IN CASE WHEN m.sender_id IN
(SELECT _id FROM participants WHERE contact_id=-1) (SELECT _id FROM participants WHERE contact_id=-1)
THEN 2 ELSE 1 END incoming, p.text AS text THEN 2 ELSE 1 END incoming, p.text AS text
FROM messages m, conversations c, parts p, FROM messages m, conversations c, parts p,
participants ppl, conversation_participants cp participants ppl, conversation_participants cp
WHERE (m.conversation_id = c._id) WHERE (m.conversation_id = c._id)
@@ -31,14 +31,15 @@ WHERE (m.conversation_id = c._id)
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db" SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
SMS_MMSMS_QUERY = """ SMS_MMSMS_QUERY = """
SELECT SELECT
address AS number, address AS number,
date_sent AS timestamp, date_sent AS timestamp,
type as incoming, type as incoming,
body AS text body AS text
FROM sms; FROM sms;
""" """
class SMS(AndroidExtraction): class SMS(AndroidExtraction):
"""This module extracts all SMS messages containing links.""" """This module extracts all SMS messages containing links."""
@@ -62,7 +63,7 @@ class SMS(AndroidExtraction):
return return
for message in self.results: for message in self.results:
if not "text" in message: if "text" not in message:
continue continue
message_links = check_for_links(message["text"]) message_links = check_for_links(message["text"])
@@ -77,7 +78,7 @@ class SMS(AndroidExtraction):
""" """
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cur = conn.cursor() cur = conn.cursor()
if (self.SMS_DB_TYPE == 1): if (self.SMS_DB_TYPE == 1):
cur.execute(SMS_BUGLE_QUERY) cur.execute(SMS_BUGLE_QUERY)
elif (self.SMS_DB_TYPE == 2): elif (self.SMS_DB_TYPE == 2):

View File

@@ -16,6 +16,7 @@ log = logging.getLogger(__name__)
WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db" WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db"
class Whatsapp(AndroidExtraction): class Whatsapp(AndroidExtraction):
"""This module extracts all WhatsApp messages containing links.""" """This module extracts all WhatsApp messages containing links."""
@@ -39,7 +40,7 @@ class Whatsapp(AndroidExtraction):
return return
for message in self.results: for message in self.results:
if not "data" in message: if "data" not in message:
continue continue
message_links = check_for_links(message["data"]) message_links = check_for_links(message["data"])

View File

@@ -5,4 +5,4 @@
from .sms import SMS from .sms import SMS
BACKUP_MODULES = [SMS,] BACKUP_MODULES = [SMS]

View File

@@ -24,7 +24,7 @@ class SMS(MVTModule):
return return
for message in self.results: for message in self.results:
if not "body" in message: if "body" not in message:
continue continue
message_links = check_for_links(message["body"]) message_links = check_for_links(message["body"])

View File

@@ -12,6 +12,7 @@ from .url import URL
class IndicatorsFileBadFormat(Exception): class IndicatorsFileBadFormat(Exception):
pass pass
class Indicators: class Indicators:
"""This class is used to parse indicators from a STIX2 file and provide """This class is used to parse indicators from a STIX2 file and provide
functions to compare extracted artifacts to the indicators. functions to compare extracted artifacts to the indicators.
@@ -27,13 +28,27 @@ class Indicators:
self.ioc_files = [] self.ioc_files = []
self.ioc_files_sha256 = [] self.ioc_files_sha256 = []
self.ioc_app_ids = [] self.ioc_app_ids = []
self.ios_profile_ids = []
self.ioc_count = 0 self.ioc_count = 0
self._check_env_variable()
def _add_indicator(self, ioc, iocs_list): def _add_indicator(self, ioc, iocs_list):
if ioc not in iocs_list: if ioc not in iocs_list:
iocs_list.append(ioc) iocs_list.append(ioc)
self.ioc_count += 1 self.ioc_count += 1
def _check_env_variable(self):
"""
Checks if a variable MVT_STIX2 contains path to STIX Files
"""
if "MVT_STIX2" in os.environ:
paths = os.environ["MVT_STIX2"].split(":")
for path in paths:
if os.path.isfile(path):
self.parse_stix2(path)
else:
self.log.info("Invalid STIX2 path %s in MVT_STIX2 environment variable", path)
def parse_stix2(self, file_path): def parse_stix2(self, file_path):
"""Extract indicators from a STIX2 file. """Extract indicators from a STIX2 file.
@@ -74,6 +89,9 @@ class Indicators:
elif key == "app:id": elif key == "app:id":
self._add_indicator(ioc=value, self._add_indicator(ioc=value,
iocs_list=self.ioc_app_ids) iocs_list=self.ioc_app_ids)
elif key == "configuration-profile:id":
self._add_indicator(ioc=value,
iocs_list=self.ios_profile_ids)
elif key == "file:hashes.sha256": elif key == "file:hashes.sha256":
self._add_indicator(ioc=value, self._add_indicator(ioc=value,
iocs_list=self.ioc_files_sha256) iocs_list=self.ioc_files_sha256)
@@ -115,7 +133,7 @@ class Indicators:
else: else:
# If it's not shortened, we just use the original URL object. # If it's not shortened, we just use the original URL object.
final_url = orig_url final_url = orig_url
except Exception as e: except Exception:
# If URL parsing failed, we just try to do a simple substring # If URL parsing failed, we just try to do a simple substring
# match. # match.
for ioc in self.ioc_domains: for ioc in self.ioc_domains:
@@ -231,7 +249,7 @@ class Indicators:
return False return False
def check_file(self, file_path) -> bool: def check_filename(self, file_path) -> bool:
"""Check the provided file path against the list of file indicators. """Check the provided file path against the list of file indicators.
:param file_path: File path or file name to check against file :param file_path: File path or file name to check against file
@@ -246,7 +264,39 @@ class Indicators:
file_name = os.path.basename(file_path) file_name = os.path.basename(file_path)
if file_name in self.ioc_files: if file_name in self.ioc_files:
self.log.warning("Found a known suspicious file: \"%s\"", file_path)
return True return True
return False return False
def check_file_path(self, file_path) -> bool:
"""Check the provided file path against the list of file indicators.
:param file_path: File path or file name to check against file
indicators
:type file_path: str
:returns: True if the file path matched an indicator, otherwise False
:rtype: bool
"""
if not file_path:
return False
for ioc_file in self.ioc_files:
# Strip any trailing slash from indicator paths to match directories.
if file_path.startswith(ioc_file.rstrip("/")):
return True
return False
def check_profile(self, profile_uuid) -> bool:
"""Check the provided configuration profile UUID against the list of indicators.
:param profile_uuid: Profile UUID to check against configuration profile indicators
:type profile_uuid: str
:returns: True if the UUID in indicator list, otherwise False
:rtype: bool
"""
if profile_uuid in self.ios_profile_ids:
return True
return False

View File

@@ -16,7 +16,7 @@ def logo():
try: try:
latest_version = check_for_updates() latest_version = check_for_updates()
except: except Exception:
pass pass
else: else:
if latest_version: if latest_version:

View File

@@ -10,18 +10,19 @@ import re
import simplejson as json import simplejson as json
from .indicators import Indicators
class DatabaseNotFoundError(Exception): class DatabaseNotFoundError(Exception):
pass pass
class DatabaseCorruptedError(Exception): class DatabaseCorruptedError(Exception):
pass pass
class InsufficientPrivileges(Exception): class InsufficientPrivileges(Exception):
pass pass
class MVTModule(object): class MVTModule(object):
"""This class provides a base for all extraction modules.""" """This class provides a base for all extraction modules."""

View File

@@ -250,6 +250,7 @@ SHORTENER_DOMAINS = [
"zz.gd", "zz.gd",
] ]
class URL: class URL:
def __init__(self, url): def __init__(self, url):
@@ -273,7 +274,7 @@ class URL:
# TODO: Properly handle exception. # TODO: Properly handle exception.
try: try:
return get_tld(self.url, as_object=True, fix_protocol=True).parsed_url.netloc.lower().lstrip("www.") return get_tld(self.url, as_object=True, fix_protocol=True).parsed_url.netloc.lower().lstrip("www.")
except: except Exception:
return None return None
def get_top_level(self): def get_top_level(self):
@@ -288,7 +289,7 @@ class URL:
# TODO: Properly handle exception. # TODO: Properly handle exception.
try: try:
return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower() return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower()
except: except Exception:
return None return None
def check_if_shortened(self) -> bool: def check_if_shortened(self) -> bool:

View File

@@ -45,7 +45,7 @@ def convert_chrometime_to_unix(timestamp):
:returns: Unix epoch timestamp. :returns: Unix epoch timestamp.
""" """
epoch_start = datetime.datetime(1601, 1 , 1) epoch_start = datetime.datetime(1601, 1, 1)
delta = datetime.timedelta(microseconds=timestamp) delta = datetime.timedelta(microseconds=timestamp)
return epoch_start + delta return epoch_start + delta
@@ -64,6 +64,7 @@ def convert_timestamp_to_iso(timestamp):
except Exception: except Exception:
return None return None
def check_for_links(text): def check_for_links(text):
"""Checks if a given text contains HTTP links. """Checks if a given text contains HTTP links.
@@ -74,6 +75,7 @@ def check_for_links(text):
""" """
return re.findall("(?P<url>https?://[^\s]+)", text, re.IGNORECASE) return re.findall("(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
def get_sha256_from_file_path(file_path): def get_sha256_from_file_path(file_path):
"""Calculate the SHA256 hash of a file from a file path. """Calculate the SHA256 hash of a file from a file path.
@@ -88,6 +90,7 @@ def get_sha256_from_file_path(file_path):
return sha256_hash.hexdigest() return sha256_hash.hexdigest()
# Note: taken from here: # Note: taken from here:
# https://stackoverflow.com/questions/57014259/json-dumps-on-dictionary-with-bytes-for-keys # https://stackoverflow.com/questions/57014259/json-dumps-on-dictionary-with-bytes-for-keys
def keys_bytes_to_string(obj): def keys_bytes_to_string(obj):

View File

@@ -6,7 +6,8 @@
import requests import requests
from packaging import version from packaging import version
MVT_VERSION = "1.2.12" MVT_VERSION = "1.3.2"
def check_for_updates(): def check_for_updates():
res = requests.get("https://pypi.org/pypi/mvt/json") res = requests.get("https://pypi.org/pypi/mvt/json")

View File

@@ -10,7 +10,9 @@ import click
from rich.logging import RichHandler from rich.logging import RichHandler
from rich.prompt import Prompt from rich.prompt import Prompt
from mvt.common.help import * from mvt.common.help import HELP_MSG_MODULE, HELP_MSG_IOC
from mvt.common.help import HELP_MSG_FAST, HELP_MSG_OUTPUT
from mvt.common.help import HELP_MSG_LIST_MODULES
from mvt.common.indicators import Indicators, IndicatorsFileBadFormat from mvt.common.indicators import Indicators, IndicatorsFileBadFormat
from mvt.common.logo import logo from mvt.common.logo import logo
from mvt.common.module import run_module, save_timeline from mvt.common.module import run_module, save_timeline
@@ -30,6 +32,7 @@ log = logging.getLogger(__name__)
# Set this environment variable to a password if needed. # Set this environment variable to a password if needed.
PASSWD_ENV = "MVT_IOS_BACKUP_PASSWORD" PASSWD_ENV = "MVT_IOS_BACKUP_PASSWORD"
#============================================================================== #==============================================================================
# Main # Main
#============================================================================== #==============================================================================
@@ -172,7 +175,7 @@ def check_backup(ctx, iocs, output, fast, backup_path, list_modules, module):
log=logging.getLogger(backup_module.__module__)) log=logging.getLogger(backup_module.__module__))
m.is_backup = True m.is_backup = True
if iocs: if indicators.ioc_count > 0:
m.indicators = indicators m.indicators = indicators
m.indicators.log = m.log m.indicators.log = m.log

View File

@@ -14,6 +14,7 @@ from iOSbackup import iOSbackup
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class DecryptBackup: class DecryptBackup:
"""This class provides functions to decrypt an encrypted iTunes backup """This class provides functions to decrypt an encrypted iTunes backup
using either a password or a key file. using either a password or a key file.

View File

@@ -1,15 +1,19 @@
# Mobile Verification Toolkit (MVT) # Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors. # Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at # Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/ # https://license.mvt.re/1.1/
import os
import plistlib import plistlib
from base64 import b64encode from base64 import b64encode
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction from ..base import IOSExtraction
CONF_PROFILES_DOMAIN = "SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles" CONF_PROFILES_DOMAIN = "SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
class ConfigurationProfiles(IOSExtraction): class ConfigurationProfiles(IOSExtraction):
"""This module extracts the full plist data from configuration profiles.""" """This module extracts the full plist data from configuration profiles."""
@@ -19,23 +23,73 @@ class ConfigurationProfiles(IOSExtraction):
output_folder=output_folder, fast_mode=fast_mode, output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results) log=log, results=results)
def serialize(self, record):
if not record["install_date"]:
return
payload_name = record['plist'].get('PayloadDisplayName')
payload_description = record['plist'].get('PayloadDescription')
return {
"timestamp": record["install_date"],
"module": self.__class__.__name__,
"event": "configuration_profile_install",
"data": f"{record['plist']['PayloadType']} installed: {record['plist']['PayloadUUID']} - {payload_name}: {payload_description}"
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if result["plist"].get("PayloadUUID"):
payload_content = result["plist"]["PayloadContent"][0]
# Alert on any known malicious configuration profiles in the indicator list.
if self.indicators.check_profile(result["plist"]["PayloadUUID"]):
self.log.warning(f"Found a known malicious configuration profile \"{result['plist']['PayloadDisplayName']}\" with UUID '{result['plist']['PayloadUUID']}'.")
self.detected.append(result)
continue
# Highlight suspicious configuration profiles which may be used to hide notifications.
if payload_content["PayloadType"] in ["com.apple.notificationsettings"]:
self.log.warning(f"Found a potentially suspicious configuration profile \"{result['plist']['PayloadDisplayName']}\" with payload type '{payload_content['PayloadType']}'.")
self.detected.append(result)
continue
def run(self): def run(self):
for conf_file in self._get_backup_files_from_manifest(domain=CONF_PROFILES_DOMAIN): for conf_file in self._get_backup_files_from_manifest(domain=CONF_PROFILES_DOMAIN):
conf_rel_path = conf_file["relative_path"]
# Filter out all configuration files that are not configuration profiles.
if not conf_rel_path or not os.path.basename(conf_rel_path).startswith("profile-"):
continue
conf_file_path = self._get_backup_file_from_id(conf_file["file_id"]) conf_file_path = self._get_backup_file_from_id(conf_file["file_id"])
if not conf_file_path: if not conf_file_path:
continue continue
with open(conf_file_path, "rb") as handle: with open(conf_file_path, "rb") as handle:
conf_plist = plistlib.load(handle) try:
conf_plist = plistlib.load(handle)
except:
conf_plist = {}
if "SignerCerts" in conf_plist: if "SignerCerts" in conf_plist:
conf_plist["SignerCerts"] = [b64encode(x) for x in conf_plist["SignerCerts"]] conf_plist["SignerCerts"] = [b64encode(x) for x in conf_plist["SignerCerts"]]
if "PushTokenDataSentToServerKey" in conf_plist:
conf_plist["PushTokenDataSentToServerKey"] = b64encode(conf_plist["PushTokenDataSentToServerKey"])
if "LastPushTokenHash" in conf_plist:
conf_plist["LastPushTokenHash"] = b64encode(conf_plist["LastPushTokenHash"])
if "PayloadContent" in conf_plist:
for x in range(len(conf_plist["PayloadContent"])):
if "PERSISTENT_REF" in conf_plist["PayloadContent"][x]:
conf_plist["PayloadContent"][x]["PERSISTENT_REF"] = b64encode(conf_plist["PayloadContent"][x]["PERSISTENT_REF"])
self.results.append({ self.results.append({
"file_id": conf_file["file_id"], "file_id": conf_file["file_id"],
"relative_path": conf_file["relative_path"], "relative_path": conf_file["relative_path"],
"domain": conf_file["domain"], "domain": conf_file["domain"],
"plist": conf_plist, "plist": conf_plist,
"install_date": convert_timestamp_to_iso(conf_plist.get("InstallDate")),
}) })
self.log.info("Extracted details about %d configuration profiles", len(self.results)) self.log.info("Extracted details about %d configuration profiles", len(self.results))

View File

@@ -28,8 +28,8 @@ class Manifest(IOSExtraction):
"""Unserialized plist objects can have keys which are str or byte types """Unserialized plist objects can have keys which are str or byte types
This is a helper to try fetch a key as both a byte or string type. This is a helper to try fetch a key as both a byte or string type.
:param dictionary: param key: :param dictionary:
:param key: :param key:
""" """
return dictionary.get(key.encode("utf-8"), None) or dictionary.get(key, None) return dictionary.get(key.encode("utf-8"), None) or dictionary.get(key, None)
@@ -38,7 +38,7 @@ class Manifest(IOSExtraction):
def _convert_timestamp(timestamp_or_unix_time_int): def _convert_timestamp(timestamp_or_unix_time_int):
"""Older iOS versions stored the manifest times as unix timestamps. """Older iOS versions stored the manifest times as unix timestamps.
:param timestamp_or_unix_time_int: :param timestamp_or_unix_time_int:
""" """
if isinstance(timestamp_or_unix_time_int, datetime.datetime): if isinstance(timestamp_or_unix_time_int, datetime.datetime):
@@ -72,7 +72,7 @@ class Manifest(IOSExtraction):
return return
for result in self.results: for result in self.results:
if not "relative_path" in result: if "relative_path" not in result:
continue continue
if not result["relative_path"]: if not result["relative_path"]:
continue continue
@@ -83,7 +83,7 @@ class Manifest(IOSExtraction):
self.detected.append(result) self.detected.append(result)
continue continue
if self.indicators.check_file(result["relative_path"]): if self.indicators.check_filename(result["relative_path"]):
self.log.warning("Found a known malicious file at path: %s", result["relative_path"]) self.log.warning("Found a known malicious file at path: %s", result["relative_path"])
self.detected.append(result) self.detected.append(result)
continue continue
@@ -133,7 +133,7 @@ class Manifest(IOSExtraction):
"owner": self._get_key(file_metadata, "UserID"), "owner": self._get_key(file_metadata, "UserID"),
"size": self._get_key(file_metadata, "Size"), "size": self._get_key(file_metadata, "Size"),
}) })
except: except Exception:
self.log.exception("Error reading manifest file metadata for file with ID %s and relative path %s", self.log.exception("Error reading manifest file metadata for file with ID %s and relative path %s",
file_data["fileID"], file_data["relativePath"]) file_data["fileID"], file_data["relativePath"])
pass pass

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