Compare commits

...

6 Commits

Author SHA1 Message Date
Rory Flynn
dbb80d6320 Mark release 2.6.0 (#601) 2025-01-27 15:41:41 +01:00
Rory Flynn
a2493baead Documentation tweaks (#599)
* Adds link in install instructions to the command completion docs added in #597
* Small visual tweaks
2025-01-14 13:12:10 +01:00
Nim
0dc6228a59 Add command completion docs (#410) (#597)
Co-authored-by: Rory Flynn <75283103+roaree@users.noreply.github.com>
2025-01-14 12:04:07 +01:00
Rory Flynn
6e230bdb6a Autofix for ruff (#598) 2025-01-14 12:02:10 +01:00
Tek
2aa76c8a1c Fixes a bug on recent phones not having WIFI column in net usage (#580)
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
Co-authored-by: Rory Flynn <75283103+roaree@users.noreply.github.com>
2025-01-07 12:48:35 +01:00
github-actions[bot]
7d6dc9e6dc Add new iOS versions and build numbers (#595)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-01-07 12:07:57 +01:00
19 changed files with 136 additions and 66 deletions

View File

@@ -0,0 +1,43 @@
# Command Completion
MVT utilizes the [Click](https://click.palletsprojects.com/en/stable/) library for creating its command line interface.
Click provides tab completion support for Bash (version 4.4 and up), Zsh, and Fish.
To enable it, you need to manually register a special function with your shell, which varies depending on the shell you are using.
The following describes how to generate the command completion scripts and add them to your shell configuration.
> **Note: You will need to start a new shell for the changes to take effect.**
### For Bash
```bash
# Generates bash completion scripts
echo "$(_MVT_IOS_COMPLETE=bash_source mvt-ios)" > ~/.mvt-ios-complete.bash &&
echo "$(_MVT_ANDROID_COMPLETE=bash_source mvt-android)" > ~/.mvt-android-complete.bash
```
Add the following to `~/.bashrc`:
```bash
# source mvt completion scripts
. ~/.mvt-ios-complete.bash && . ~/.mvt-android-complete.bash
```
### For Zsh
```bash
# Generates zsh completion scripts
echo "$(_MVT_IOS_COMPLETE=zsh_source mvt-ios)" > ~/.mvt-ios-complete.zsh &&
echo "$(_MVT_ANDROID_COMPLETE=zsh_source mvt-android)" > ~/.mvt-android-complete.zsh
```
Add the following to `~/.zshrc`:
```bash
# source mvt completion scripts
. ~/.mvt-ios-complete.zsh && . ~/.mvt-android-complete.zsh
```
For more information, visit the official [Click Docs](https://click.palletsprojects.com/en/stable/shell-completion/#enabling-completion).

View File

@@ -98,3 +98,7 @@ You now should have the `mvt-ios` and `mvt-android` utilities installed.
**Notes:** **Notes:**
1. The `--force` flag is necessary to force the reinstallation of the package. 1. The `--force` flag is necessary to force the reinstallation of the package.
2. To revert to using a PyPI version, it will be necessary to `pipx uninstall mvt` first. 2. To revert to using a PyPI version, it will be necessary to `pipx uninstall mvt` first.
## Setting up command completions
See ["Command completions"](command_completion.md)

View File

@@ -51,7 +51,7 @@ class DumpsysAppopsArtifact(AndroidArtifact):
and perm["access"] == "allow" and perm["access"] == "allow"
): ):
self.log.info( self.log.info(
"Package %s with REQUEST_INSTALL_PACKAGES " "permission", "Package %s with REQUEST_INSTALL_PACKAGES permission",
result["package_name"], result["package_name"],
) )

View File

@@ -16,8 +16,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
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( self.log.warning(
"Found an installed package related to " 'Found an installed package related to rooting/jailbreaking: "%s"',
'rooting/jailbreaking: "%s"',
result["package_name"], result["package_name"],
) )
self.detected.append(result) self.detected.append(result)

View File

@@ -326,8 +326,7 @@ class AndroidExtraction(MVTModule):
if not header["backup"]: if not header["backup"]:
self.log.error( self.log.error(
"Extracting SMS via Android backup failed. " "Extracting SMS via Android backup failed. No valid backup data found."
"No valid backup data found."
) )
return None return None

View File

@@ -75,8 +75,7 @@ class Packages(AndroidExtraction):
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( self.log.warning(
"Found an installed package related to " 'Found an installed package related to rooting/jailbreaking: "%s"',
'rooting/jailbreaking: "%s"',
result["package_name"], result["package_name"],
) )
self.detected.append(result) self.detected.append(result)

View File

@@ -70,7 +70,7 @@ class SMS(AndroidExtraction):
"timestamp": record["isodate"], "timestamp": record["isodate"],
"module": self.__class__.__name__, "module": self.__class__.__name__,
"event": f"sms_{record['direction']}", "event": f"sms_{record['direction']}",
"data": f"{record.get('address', 'unknown source')}: \"{body}\"", "data": f'{record.get("address", "unknown source")}: "{body}"',
} }
def check_indicators(self) -> None: def check_indicators(self) -> None:

View File

@@ -44,8 +44,7 @@ class Packages(AndroidQFModule):
for result in self.results: for result in self.results:
if result["name"] in ROOT_PACKAGES: if result["name"] in ROOT_PACKAGES:
self.log.warning( self.log.warning(
"Found an installed package related to " 'Found an installed package related to rooting/jailbreaking: "%s"',
'rooting/jailbreaking: "%s"',
result["name"], result["name"],
) )
self.detected.append(result) self.detected.append(result)

View File

@@ -81,7 +81,7 @@ class Command:
os.path.join(self.results_path, "command.log") os.path.join(self.results_path, "command.log")
) )
formatter = logging.Formatter( formatter = logging.Formatter(
"%(asctime)s - %(name)s - " "%(levelname)s - %(message)s" "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
) )
file_handler.setLevel(logging.DEBUG) file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)

View File

@@ -383,8 +383,7 @@ class Indicators:
for ioc in self.get_iocs("urls"): for ioc in self.get_iocs("urls"):
if ioc["value"] == url: if ioc["value"] == url:
self.log.warning( self.log.warning(
"Found a known suspicious URL %s " 'Found a known suspicious URL %s matching indicator "%s" from "%s"',
'matching indicator "%s" from "%s"',
url, url,
ioc["value"], ioc["value"],
ioc["name"], ioc["name"],

View File

@@ -3,4 +3,4 @@
# 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/
MVT_VERSION = "2.5.4" MVT_VERSION = "2.6.0"

View File

@@ -100,7 +100,7 @@ def decrypt_backup(ctx, destination, password, key_file, hashes, backup_path):
if key_file: if key_file:
if MVT_IOS_BACKUP_PASSWORD in os.environ: if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info( log.info(
"Ignoring %s environment variable, using --key-file" "'%s' instead", "Ignoring %s environment variable, using --key-file'%s' instead",
MVT_IOS_BACKUP_PASSWORD, MVT_IOS_BACKUP_PASSWORD,
key_file, key_file,
) )
@@ -114,7 +114,7 @@ def decrypt_backup(ctx, destination, password, key_file, hashes, backup_path):
if MVT_IOS_BACKUP_PASSWORD in os.environ: if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info( log.info(
"Ignoring %s environment variable, using --password" "argument instead", "Ignoring %s environment variable, using --passwordargument instead",
MVT_IOS_BACKUP_PASSWORD, MVT_IOS_BACKUP_PASSWORD,
) )
@@ -168,8 +168,7 @@ def extract_key(password, key_file, backup_path):
if MVT_IOS_BACKUP_PASSWORD in os.environ: if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info( log.info(
"Ignoring %s environment variable, using --password " "Ignoring %s environment variable, using --password argument instead",
"argument instead",
MVT_IOS_BACKUP_PASSWORD, MVT_IOS_BACKUP_PASSWORD,
) )
elif MVT_IOS_BACKUP_PASSWORD in os.environ: elif MVT_IOS_BACKUP_PASSWORD in os.environ:

View File

@@ -1095,5 +1095,9 @@
{ {
"version": "18.2", "version": "18.2",
"build": "22C152" "build": "22C152"
},
{
"version": "18.2.1",
"build": "22C161"
} }
] ]

View File

@@ -41,7 +41,7 @@ class BackupInfo(IOSExtraction):
info_path = os.path.join(self.target_path, "Info.plist") info_path = os.path.join(self.target_path, "Info.plist")
if not os.path.exists(info_path): if not os.path.exists(info_path):
raise DatabaseNotFoundError( raise DatabaseNotFoundError(
"No Info.plist at backup path, unable to extract device " "information" "No Info.plist at backup path, unable to extract device information"
) )
with open(info_path, "rb") as handle: with open(info_path, "rb") as handle:

View File

@@ -110,8 +110,7 @@ class Manifest(IOSExtraction):
ioc = self.indicators.check_url(part) ioc = self.indicators.check_url(part)
if ioc: if ioc:
self.log.warning( self.log.warning(
'Found mention of domain "%s" in a backup file with ' 'Found mention of domain "%s" in a backup file with path: %s',
"path: %s",
ioc["value"], ioc["value"],
rel_path, rel_path,
) )

View File

@@ -74,7 +74,7 @@ class IOSExtraction(MVTModule):
if not shutil.which("sqlite3"): if not shutil.which("sqlite3"):
raise DatabaseCorruptedError( raise DatabaseCorruptedError(
"failed to recover without sqlite3 binary: please install " "sqlite3!" "failed to recover without sqlite3 binary: please install sqlite3!"
) )
if '"' in file_path: if '"' in file_path:
raise DatabaseCorruptedError( raise DatabaseCorruptedError(

View File

@@ -43,7 +43,7 @@ class SMS(IOSExtraction):
def serialize(self, record: dict) -> Union[dict, list]: def serialize(self, record: dict) -> Union[dict, list]:
text = record["text"].replace("\n", "\\n") text = record["text"].replace("\n", "\\n")
sms_data = f"{record['service']}: {record['guid']} \"{text}\" from {record['phone_number']} ({record['account']})" sms_data = f'{record["service"]}: {record["guid"]} "{text}" from {record["phone_number"]} ({record["account"]})'
records = [ records = [
{ {
"timestamp": record["isodate"], "timestamp": record["isodate"],

View File

@@ -100,7 +100,7 @@ class WebkitSessionResourceLog(IOSExtraction):
redirect_path += ", ".join(source_domains) redirect_path += ", ".join(source_domains)
redirect_path += " -> " redirect_path += " -> "
redirect_path += f"ORIGIN: \"{entry['origin']}\"" redirect_path += f'ORIGIN: "{entry["origin"]}"'
if len(destination_domains) > 0: if len(destination_domains) > 0:
redirect_path += " -> " redirect_path += " -> "

View File

@@ -38,44 +38,70 @@ class NetBase(IOSExtraction):
def _extract_net_data(self): def _extract_net_data(self):
conn = sqlite3.connect(self.file_path) conn = sqlite3.connect(self.file_path)
conn.row_factory = sqlite3.Row
cur = conn.cursor() cur = conn.cursor()
cur.execute( try:
cur.execute(
"""
SELECT
ZPROCESS.ZFIRSTTIMESTAMP,
ZPROCESS.ZTIMESTAMP,
ZPROCESS.ZPROCNAME,
ZPROCESS.ZBUNDLENAME,
ZPROCESS.Z_PK AS ZPROCESS_PK,
ZLIVEUSAGE.ZWIFIIN,
ZLIVEUSAGE.ZWIFIOUT,
ZLIVEUSAGE.ZWWANIN,
ZLIVEUSAGE.ZWWANOUT,
ZLIVEUSAGE.Z_PK AS ZLIVEUSAGE_PK,
ZLIVEUSAGE.ZHASPROCESS,
ZLIVEUSAGE.ZTIMESTAMP AS ZL_TIMESTAMP
FROM ZLIVEUSAGE
LEFT JOIN ZPROCESS ON ZLIVEUSAGE.ZHASPROCESS = ZPROCESS.Z_PK
UNION
SELECT ZFIRSTTIMESTAMP, ZTIMESTAMP, ZPROCNAME, ZBUNDLENAME, Z_PK,
NULL, NULL, NULL, NULL, NULL, NULL, NULL
FROM ZPROCESS WHERE Z_PK NOT IN
(SELECT ZHASPROCESS FROM ZLIVEUSAGE);
""" """
SELECT )
ZPROCESS.ZFIRSTTIMESTAMP, except sqlite3.OperationalError:
ZPROCESS.ZTIMESTAMP, # Recent phones don't have ZWIFIIN and ZWIFIOUT columns
ZPROCESS.ZPROCNAME, cur.execute(
ZPROCESS.ZBUNDLENAME, """
ZPROCESS.Z_PK, SELECT
ZLIVEUSAGE.ZWIFIIN, ZPROCESS.ZFIRSTTIMESTAMP,
ZLIVEUSAGE.ZWIFIOUT, ZPROCESS.ZTIMESTAMP,
ZLIVEUSAGE.ZWWANIN, ZPROCESS.ZPROCNAME,
ZLIVEUSAGE.ZWWANOUT, ZPROCESS.ZBUNDLENAME,
ZLIVEUSAGE.Z_PK, ZPROCESS.Z_PK AS ZPROCESS_PK,
ZLIVEUSAGE.ZHASPROCESS, ZLIVEUSAGE.ZWWANIN,
ZLIVEUSAGE.ZTIMESTAMP ZLIVEUSAGE.ZWWANOUT,
FROM ZLIVEUSAGE ZLIVEUSAGE.Z_PK AS ZLIVEUSAGE_PK,
LEFT JOIN ZPROCESS ON ZLIVEUSAGE.ZHASPROCESS = ZPROCESS.Z_PK ZLIVEUSAGE.ZHASPROCESS,
UNION ZLIVEUSAGE.ZTIMESTAMP AS ZL_TIMESTAMP
SELECT ZFIRSTTIMESTAMP, ZTIMESTAMP, ZPROCNAME, ZBUNDLENAME, Z_PK, FROM ZLIVEUSAGE
NULL, NULL, NULL, NULL, NULL, NULL, NULL LEFT JOIN ZPROCESS ON ZLIVEUSAGE.ZHASPROCESS = ZPROCESS.Z_PK
FROM ZPROCESS WHERE Z_PK NOT IN UNION
(SELECT ZHASPROCESS FROM ZLIVEUSAGE); SELECT ZFIRSTTIMESTAMP, ZTIMESTAMP, ZPROCNAME, ZBUNDLENAME, Z_PK,
""" NULL, NULL, NULL, NULL, NULL
) FROM ZPROCESS WHERE Z_PK NOT IN
(SELECT ZHASPROCESS FROM ZLIVEUSAGE);
"""
)
for row in cur: for row in cur:
# ZPROCESS records can be missing after the JOIN. # ZPROCESS records can be missing after the JOIN.
# Handle NULL timestamps. # Handle NULL timestamps.
if row[0] and row[1]: if row["ZFIRSTTIMESTAMP"] and row["ZTIMESTAMP"]:
first_isodate = convert_mactime_to_iso(row[0]) first_isodate = convert_mactime_to_iso(row["ZFIRSTTIMESTAMP"])
isodate = convert_mactime_to_iso(row[1]) isodate = convert_mactime_to_iso(row["ZTIMESTAMP"])
else: else:
first_isodate = row[0] first_isodate = row["ZFIRSTTIMESTAMP"]
isodate = row[1] isodate = row["ZTIMESTAMP"]
if row[11]: if row["ZL_TIMESTAMP"]:
live_timestamp = convert_mactime_to_iso(row[11]) live_timestamp = convert_mactime_to_iso(row["ZL_TIMESTAMP"])
else: else:
live_timestamp = "" live_timestamp = ""
@@ -83,16 +109,18 @@ class NetBase(IOSExtraction):
{ {
"first_isodate": first_isodate, "first_isodate": first_isodate,
"isodate": isodate, "isodate": isodate,
"proc_name": row[2], "proc_name": row["ZPROCNAME"],
"bundle_id": row[3], "bundle_id": row["ZBUNDLENAME"],
"proc_id": row[4], "proc_id": row["ZPROCESS_PK"],
"wifi_in": row[5], "wifi_in": row["ZWIFIIN"] if "ZWIFIIN" in row.keys() else None,
"wifi_out": row[6], "wifi_out": row["ZWIFIOUT"] if "ZWIFIOUT" in row.keys() else None,
"wwan_in": row[7], "wwan_in": row["ZWWANIN"],
"wwan_out": row[8], "wwan_out": row["ZWWANOUT"],
"live_id": row[9], "live_id": row["ZLIVEUSAGE_PK"],
"live_proc_id": row[10], "live_proc_id": row["ZHASPROCESS"],
"live_isodate": live_timestamp if row[11] else first_isodate, "live_isodate": live_timestamp
if row["ZL_TIMESTAMP"]
else first_isodate,
} }
) )
@@ -108,8 +136,6 @@ class NetBase(IOSExtraction):
) )
record_data_usage = ( record_data_usage = (
record_data + " " record_data + " "
f"WIFI IN: {record['wifi_in']}, "
f"WIFI OUT: {record['wifi_out']} - "
f"WWAN IN: {record['wwan_in']}, " f"WWAN IN: {record['wwan_in']}, "
f"WWAN OUT: {record['wwan_out']}" f"WWAN OUT: {record['wwan_out']}"
) )