Skip to content
Open

V2 #716

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5d69635
Run bugreport and backup modules during check-androidqf
DonnchaC Feb 7, 2025
a08c24b
Deduplicate modules which are run by the sub-commands.
DonnchaC Feb 10, 2025
4c1cdf5
Raise the proper NoAndroidQFBackup exception when a back-up isn't found
DonnchaC Feb 11, 2025
064b9fb
Remove check-adb command and update docs
DonnchaC Feb 15, 2025
6bac787
Remove check-apk code and old dependencies
DonnchaC Feb 15, 2025
1b03002
Major refactor to add structured alerting and typed indicators
DonnchaC Feb 15, 2025
ca0bc46
Fix up, remove ADB module base
DonnchaC Feb 15, 2025
2d54766
Rework old detections tracking into stuctured alert levels
DonnchaC Feb 19, 2025
70d646a
Quote STIX path in log line
DonnchaC Oct 6, 2025
05ad7d2
Fix profile events log line
DonnchaC Oct 6, 2025
e9e6216
Close open archive (zip/tar) file handles
DonnchaC Oct 6, 2025
2302e74
Merge refactor/structured-alerting into v2
besendorf Nov 7, 2025
af8c566
Fix root_binaries and mounts modules to use alertstore
besendorf Nov 7, 2025
301582d
Update tests to use alertstore instead of detected attribute
besendorf Nov 7, 2025
5b1f4df
Fix alertstore method calls - use high() instead of warning()
besendorf Nov 7, 2025
4b6a101
Fix remaining test errors
besendorf Nov 7, 2025
d4b970c
Log alerts on add
besendorf Nov 7, 2025
d259ab4
Remove slug from alertstore calls
besendorf Nov 7, 2025
b1f0a2d
update alerts.py
besendorf Nov 7, 2025
c6837a4
update alerts.py
besendorf Nov 7, 2025
cc7781e
move indicator_match to alert object
besendorf Nov 7, 2025
6d1d499
.
besendorf Nov 7, 2025
801c464
- Remove timeline_detected and route to alertstore
besendorf Nov 7, 2025
c779009
fix typing for mypy
besendorf Dec 20, 2025
6a76191
Merge branch 'main' into v2
besendorf Jan 27, 2026
088a3f4
Remove unused type imports
besendorf Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 14 additions & 20 deletions docs/android/adb.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,26 @@ mvt-android check-adb --output /path/to/results
If you have previously started an adb daemon MVT will alert you and require you to kill it with `adb kill-server` and relaunch the command.

!!! warning
MVT relies on the Python library [adb-shell](https://pypi.org/project/adb-shell/) to connect to an Android device, which relies on libusb for the USB transport. Because of known driver issues, Windows users [are recommended](https://github.com/JeffLIrion/adb_shell/issues/118) to install appropriate drivers using [Zadig](https://zadig.akeo.ie/). Alternatively, an easier option might be to use the TCP transport and connect over Wi-Fi as describe next.

## Connecting over Wi-FI
The `mvt-android check-adb` command has been deprecated and removed from MVT.

When connecting to the device over USB is not possible or not working properly, an alternative option is to connect over the network. In order to do so, first launch an adb daemon at a fixed port number:
The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT due to several technical and forensic limitations.

```bash
adb tcpip 5555
```
## Reasons for Deprecation

Then you can specify the IP address of the phone with the adb port number to MVT like so:
1. **Inconsistent Data Collection Across Devices**
Android devices vary significantly in their system architecture, security policies, and available diagnostic logs. This inconsistency makes it difficult to ensure that MVT can reliably collect necessary forensic data across all devices.

```bash
mvt-android check-adb --serial 192.168.1.20:5555 --output /path/to/results
```
2. **Incomplete Forensic Data Acquisition**
The `check-adb` command did not retrieve a full forensic snapshot of all available data on the device. For example, critical logs such as the **full bugreport** were not systematically collected, leading to potential gaps in forensic analysis. This can be a serious problem in scenarios where the analyst only had one time access to the Android device.

Where `192.168.1.20` is the correct IP address of your device.
4. **Code Duplication and Difficulty Ensuring Consistent Behavior Across Sources**
Similar forensic data such as "dumpsys" logs were being loaded and parsed by MVT's ADB, AndroidQF and Bugreport commands. Multiple modules were needed to handle each source format which created duplication leading to inconsistent
behavior and difficulties in maintaining the code base.

!!! warning
The `check-adb` workflow shown above is deprecated. If you can acquire an AndroidQF acquisition from the device (recommended), use the AndroidQF project to create that acquisition: https://github.com/mvt-project/androidqf/

AndroidQF acquisitions provide a more stable, reproducible analysis surface and are the preferred workflow going forward.

## MVT modules requiring root privileges
5. **Alignment with iOS Workflow**
MVT’s forensic workflow for iOS relies on pre-extracted artifacts, such as iTunes backups or filesystem dumps, rather than preforming commands or interactions directly on a live device. Removing the ADB functionality ensures a more consistent methodology across both Android and iOS mobile forensic.

!!! warning
Deprecated: many `mvt-android check-adb` workflows are deprecated and will be removed in a future release. Whenever possible, prefer acquiring an AndroidQF acquisition using the AndroidQF project (https://github.com/mvt-project/androidqf/).
## Alternative: Using AndroidQF for Forensic Data Collection

Of the currently available `mvt-android check-adb` modules a handful require root privileges to function correctly. This is because certain files, such as browser history and SMS messages databases are not accessible with user privileges through adb. These modules are to be considered OPTIONALLY available in case the device was already jailbroken. **Do NOT jailbreak your own device unless you are sure of what you are doing!** Jailbreaking your phone exposes it to considerable security risks!
To replace the deprecated ADB-based approach, forensic analysts should use [AndroidQF](https://github.com/mvt-project/androidqf) for comprehensive data collection, followed by MVT for forensic analysis. The workflow is outlined in the MVT [Android methodology](./methodology.md)
46 changes: 38 additions & 8 deletions docs/android/methodology.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,53 @@
# Methodology for Android forensic

Unfortunately Android devices provide much less observability than their iOS cousins. Android stores very little diagnostic information useful to triage potential compromises, and because of this `mvt-android` capabilities are limited as well.
Unfortunately Android devices provide fewer complete forensically useful datasources than their iOS cousins. Unlike iOS, the Android backup feature only provides a limited about of relevant data.

Android diagnostic logs such as *bugreport files* can be inconsistent in format and structure across different Android versions and device vendors. The limited diagnostic information available makes it difficult to triage potential compromises, and because of this `mvt-android` capabilities are limited as well.

However, not all is lost.

## Check installed Apps
## Check Android devices with AndroidQF and MVT

The [AndroidQF](https://github.com/mvt-project/androidqf) tool can be used to collect a wide range of forensic artifacts from an Android device including an Android backup, a bugreport file, and a range of system logs. MVT natively supports analyzing the generated AndroidQF output for signs of device compromise.

### Why Use AndroidQF?

- **Complete and raw data extraction**
AndroidQF collects full forensic artifacts using an on-device forensic collection agent, ensuring that no crucial data is overlooked. The data collection does not depended on the shell environment or utilities available on the device.

- **Consistent and standardized output**
By collecting a predefined and complete set of forensic files, AndroidQF ensures consistency in data acquisition across different Android devices.

- **Future-proof analysis**
Since the full forensic artifacts are preserved, analysts can extract new evidence or apply updated analysis techniques without requiring access to the original device.

Because malware attacks over Android typically take the form of malicious or backdoored apps, the very first thing you might want to do is to extract and verify all installed Android packages and triage quickly if there are any which stand out as malicious or which might be atypical.
- **Cross-platform tool without dependencies**
AndroidQF is a standalone Go binary which can be used to remotely collect data from an Android device without the device owner needing to install MVT or a Python environment.

While it is out of the scope of this documentation to dwell into details on how to analyze Android apps, MVT does allow to easily and automatically extract information about installed apps, download copies of them, and quickly look them up on services such as [VirusTotal](https://www.virustotal.com).
### Workflow for Android Forensic Analysis with AndroidQF

!!! info "Using VirusTotal"
Please note that in order to use VirusTotal lookups you are required to provide your own API key through the `MVT_VT_API_KEY` environment variable. You should also note that VirusTotal enforces strict API usage. Be mindful that MVT might consume your hourly search quota.
With AndroidQF the analysis process is split into a separate data collection and data analysis stages.

1. **Extract Data Using AndroidQF**
Deploy the AndroidQF forensic collector to acquire all relevant forensic artifacts from the Android device.

2. **Analyze Extracted Data with MVT**
Use the `mvt-android check-androidqf` command to perform forensic analysis on the extracted artifacts.

By separating artifact collection from forensic analysis, this approach ensures a more reliable and scalable methodology for Android forensic investigations.

For more information, refer to the [AndroidQF project documentation](https://github.com/mvt-project/androidqf).

## Check the device over Android Debug Bridge

Some additional diagnostic information can be extracted from the phone using the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb). `mvt-android` allows to automatically extract information including [dumpsys](https://developer.android.com/studio/command-line/dumpsys) results, details on installed packages (without download), running processes, presence of root binaries and packages, and more.
The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT.

See the [Android ADB documentation](./adb.md) for more information.

## Check an Android Backup (SMS messages)

Although Android backups are becoming deprecated, it is still possible to generate one. Unfortunately, because apps these days typically favor backup over the cloud, the amount of data available is limited. Currently, `mvt-android check-backup` only supports checking SMS messages containing links.
Although Android backups are becoming deprecated, it is still possible to generate one. Unfortunately, because apps these days typically favor backup over the cloud, the amount of data available is limited.

The `mvt-android check-androidqf` command will automatically check an Android backup and SMS messages if an SMS backup is included in the AndroidQF extraction.

The `mvt-android check-backup` command can also be used directly with an Android backup file.
19 changes: 1 addition & 18 deletions docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,4 @@ Test if the image was created successfully:
docker run -it mvt
```

If a prompt is spawned successfully, you can close it with `exit`.


## Docker usage with Android devices

If you wish to use MVT to test an Android device you will need to enable the container's access to the host's USB devices. You can do so by enabling the `--privileged` flag and mounting the USB bus device as a volume:

```bash
docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt
```

**Please note:** the `--privileged` parameter is generally regarded as a security risk. If you want to learn more about this check out [this explainer on container escapes](https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/) as it gives access to the whole system.

Recent versions of Docker provide a `--device` parameter allowing to specify a precise USB device without enabling `--privileged`:

```bash
docker run -it --device=/dev/<your_usb_port> mvt
```
If a prompt is spawned successfully, you can close it with `exit`.
42 changes: 29 additions & 13 deletions src/mvt/android/artifacts/artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,39 @@ def extract_dumpsys_section(
:param binary: whether the dumpsys should be pared as binary or not (bool)
:return: section extracted (string or bytes)
"""
lines = []
in_section = False
delimiter = "------------------------------------------------------------------------------"
delimiter_str = "------------------------------------------------------------------------------"
delimiter_bytes = b"------------------------------------------------------------------------------"

if binary:
delimiter = delimiter.encode("utf-8")
lines_bytes = []
for line in dumpsys.splitlines(): # type: ignore[union-attr]
if line.strip() == separator: # type: ignore[arg-type]
in_section = True
continue

if not in_section:
continue

if line.strip().startswith(delimiter_bytes): # type: ignore[arg-type]
break

lines_bytes.append(line) # type: ignore[arg-type]

for line in dumpsys.splitlines():
if line.strip() == separator:
in_section = True
continue
return b"\n".join(lines_bytes) # type: ignore[return-value,arg-type]
else:
lines_str = []
for line in dumpsys.splitlines(): # type: ignore[union-attr]
if line.strip() == separator: # type: ignore[arg-type]
in_section = True
continue

if not in_section:
continue
if not in_section:
continue

if line.strip().startswith(delimiter):
break
if line.strip().startswith(delimiter_str): # type: ignore[arg-type]
break

lines.append(line)
lines_str.append(line) # type: ignore[arg-type]

return b"\n".join(lines) if binary else "\n".join(lines)
return "\n".join(lines_str) # type: ignore[return-value,arg-type]
9 changes: 5 additions & 4 deletions src/mvt/android/artifacts/dumpsys_accessibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ def check_indicators(self) -> None:
return

for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue

def parse(self, content: str) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/mvt/android/artifacts/dumpsys_adb.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def parse_xml(self, xml_data):
return keystore

@staticmethod
def calculate_key_info(user_key: bytes) -> str:
def calculate_key_info(user_key: bytes) -> dict:
if b" " in user_key:
key_base64, user = user_key.split(b" ", 1)
else:
Expand Down
70 changes: 35 additions & 35 deletions src/mvt/android/artifacts/dumpsys_appops.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# https://license.mvt.re/1.1/

from datetime import datetime
from typing import Any, Dict, List, Union
from typing import Any

from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
from mvt.common.utils import convert_datetime_to_iso

from .artifact import AndroidArtifact


RISKY_PERMISSIONS = ["REQUEST_INSTALL_PACKAGES"]
RISKY_PACKAGES = ["com.android.shell"]

Expand All @@ -20,9 +20,9 @@ class DumpsysAppopsArtifact(AndroidArtifact):
Parser for dumpsys app ops info
"""

def serialize(self, record: dict) -> Union[dict, list]:
def serialize(self, result: ModuleAtomicResult) -> ModuleSerializedResult:
records = []
for perm in record["permissions"]:
for perm in result["permissions"]:
if "entries" not in perm:
continue

Expand All @@ -33,7 +33,7 @@ def serialize(self, record: dict) -> Union[dict, list]:
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to "
"data": f"{result['package_name']} access to "
f"{perm['name']}: {entry['access']}",
}
)
Expand All @@ -43,51 +43,51 @@ def serialize(self, record: dict) -> Union[dict, list]:
def check_indicators(self) -> None:
for result in self.results:
if self.indicators:
ioc = self.indicators.check_app_id(result.get("package_name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
ioc_match = self.indicators.check_app_id(result.get("package_name"))
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue

detected_permissions = []
# We use a placeholder entry to create a basic alert even without permission entries.
placeholder_entry = {"access": "Unknown", "timestamp": ""}

for perm in result["permissions"]:
if (
perm["name"] in RISKY_PERMISSIONS
# and perm["access"] == "allow"
):
detected_permissions.append(perm)
for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]):
self.log.warning(
"Package '%s' had risky permission '%s' set to '%s' at %s",
result["package_name"],
perm["name"],
entry["access"],
for entry in sorted(
perm["entries"] or [placeholder_entry],
key=lambda x: x["timestamp"],
):
cleaned_result = result.copy()
cleaned_result["permissions"] = [perm]
self.alertstore.medium(
f"Package '{result['package_name']}' had risky permission '{perm['name']}' set to '{entry['access']}' at {entry['timestamp']}",
entry["timestamp"],
cleaned_result,
)

elif result["package_name"] in RISKY_PACKAGES:
detected_permissions.append(perm)
for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]):
self.log.warning(
"Risky package '%s' had '%s' permission set to '%s' at %s",
result["package_name"],
perm["name"],
entry["access"],
for entry in sorted(
perm["entries"] or [placeholder_entry],
key=lambda x: x["timestamp"],
):
cleaned_result = result.copy()
cleaned_result["permissions"] = [perm]
self.alertstore.medium(
f"Risky package '{result['package_name']}' had '{perm['name']}' permission set to '{entry['access']}' at {entry['timestamp']}",
entry["timestamp"],
cleaned_result,
)

if detected_permissions:
# We clean the result to only include the risky permission, otherwise the timeline
# will be polluted with all the other irrelevant permissions
cleaned_result = result.copy()
cleaned_result["permissions"] = detected_permissions
self.detected.append(cleaned_result)

def parse(self, output: str) -> None:
self.results: List[Dict[str, Any]] = []
perm = {}
package = {}
entry = {}
# self.results: List[Dict[str, Any]] = []
perm: dict[str, Any] = {}
package: dict[str, Any] = {}
entry: dict[str, Any] = {}
uid = None
in_packages = False

Expand Down
Loading
Loading