Compare commits

..

1 Commits

Author SHA1 Message Date
Donncha Ó Cearbhaill
ac157a4421 WIP: Add inital scoffolding for multiple alerting levels in MVT 2023-11-28 13:38:58 +01:00
62 changed files with 608 additions and 1521 deletions

View File

@@ -40,9 +40,7 @@ jobs:
- name: Safety checks
run: safety check
- name: Test with pytest and coverage
run: |
set -o pipefail
pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=mvt tests/ | tee pytest-coverage.txt
run: pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=mvt tests/ | tee pytest-coverage.txt
- name: Pytest coverage comment
continue-on-error: true # Workflows running on a fork can't post comments
uses: MishaKav/pytest-coverage-comment@main

View File

@@ -1,11 +0,0 @@
# Safety Security and License Configuration file
# We recommend checking this file into your source control in the root of your Python project
# If this file is named .safety-policy.yml and is in the same directory where you run `safety check` it will be used by default.
# Otherwise, you can use the flag `safety check --policy-file <path-to-this-file>` to specify a custom location and name for the file.
# To validate and review your policy file, run the validate command: `safety validate policy_file --path <path-to-this-file>`
security: # configuration for the `safety check` command
ignore-vulnerabilities: # Here you can list multiple specific vulnerabilities you want to ignore (optionally for a time period)
67599: # Example vulnerability ID
reason: disputed, inapplicable
70612:
reason: disputed, inapplicable

View File

@@ -26,7 +26,7 @@ MVT supports using public [indicators of compromise (IOCs)](https://github.com/m
>
> Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
>
>Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/get-help/?c=mvt_docs) or through our forensic partnership with [Access Nows Digital Security Helpline](https://www.accessnow.org/help/).
>Such support is available to civil society through [Amnesty International's Security Lab](https://www.amnesty.org/en/tech/) or through our forensic partnership with [Access Nows Digital Security Helpline](https://www.accessnow.org/help/).
More information about using indicators of compromise with MVT is available in the [documentation](https://docs.mvt.re/en/latest/iocs/).

View File

@@ -7,27 +7,11 @@ Before proceeding, please note that MVT requires Python 3.6+ to run. While it sh
First install some basic dependencies that will be necessary to build all required tools:
```bash
sudo apt install python3 python3-venv python3-pip sqlite3 libusb-1.0-0
sudo apt install python3 python3-pip libusb-1.0-0 sqlite3
```
*libusb-1.0-0* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
(Recommended) Set up `pipx`
For Ubuntu 23.04 or above:
```bash
sudo apt install pipx
pipx ensurepath
```
For Ubuntu 22.04 or below:
```
python3 -m pip install --user pipx
python3 -m pipx ensurepath
```
Other distributions: check for a `pipx` or `python-pipx` via your package manager.
When working with Android devices you should additionally install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you prefer to install a package made available by your distribution of choice, please make sure the version is recent to ensure compatibility with modern Android devices.
## Dependencies on macOS
@@ -37,7 +21,7 @@ Running MVT on macOS requires Xcode and [homebrew](https://brew.sh) to be instal
In order to install dependencies use:
```bash
brew install python3 pipx libusb sqlite3
brew install python3 libusb sqlite3
```
*libusb* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
@@ -58,43 +42,24 @@ It is recommended to try installing and running MVT from [Windows Subsystem Linu
## Installing MVT
### Installing from PyPI with pipx (recommended)
1. Install `pipx` following the instructions above for your OS/distribution. Make sure to run `pipx ensurepath` and open a new terminal window.
2. ```bash
pipx install mvt
```
If you haven't done so, you can add this to your `.bashrc` or `.zshrc` file in order to add locally installed PyPI binaries to your `$PATH`:
You now should have the `mvt-ios` and `mvt-android` utilities installed. If you run into problems with these commands not being found, ensure you have run `pipx ensurepath` and opened a new terminal window.
### Installing from PyPI directly into a virtual environment
You can use `pipenv`, `poetry` etc. for your virtual environment, but the provided example is with the built-in `venv` tool:
1. Create the virtual environment in a folder in the current directory named `env`:
```bash
python3 -m venv env
export PATH=$PATH:~/.local/bin
```
2. Activate the virtual environment:
Then you can install MVT directly from [PyPI](https://pypi.org/project/mvt/)
```bash
source env/bin/activate
pip3 install mvt
```
3. Install `mvt` into the virtual environment:
```bash
pip install mvt
```
The `mvt-ios` and `mvt-android` utilities should now be available as commands whenever the virtual environment is active.
### Installing from git source with pipx
If you want to have the latest features in development, you can install MVT directly from the source code in git.
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
pipx install --force git+https://github.com/mvt-project/mvt.git
git clone https://github.com/mvt-project/mvt.git
cd mvt
pip3 install .
```
You now should have the `mvt-ios` and `mvt-android` utilities installed.
**Notes:**
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.

View File

@@ -21,7 +21,7 @@ MVT supports using [indicators of compromise (IOCs)](https://github.com/mvt-proj
Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/get-help/?c=mvt_docs) or [Access Nows Digital Security Helpline](https://www.accessnow.org/help/).
Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/contact-us/) or [Access Nows Digital Security Helpline](https://www.accessnow.org/help/).
More information about using indicators of compromise with MVT is available in the [documentation](iocs.md).

View File

@@ -45,10 +45,10 @@ Once the idevice tools are available you can check if everything works fine by c
ideviceinfo
```
This should show many details on the connected iOS device. If you are connecting the device to your laptop for the first time, it will require to unlock and enter the PIN code on the mobile device. If it complains that no device is connected and the mobile device is indeed plugged in through the USB cable, you might need to do this first, although typically the pairing is automatically done when connecting the device:
This should some many details on the connected iOS device. If you are connecting the device to your laptop for the first time, it will require to unlock and enter the PIN code on the mobile device. If it complains that no device is connected and the mobile device is indeed plugged in through the USB cable, you might need to do this first, although typically the pairing is automatically done when connecting the device:
```bash
sudo usbmuxd -f -v
sudo usbmuxd -f -d
idevicepair pair
```

View File

@@ -1,165 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import re
from .artifact import AndroidArtifact
# The AOSP dumpstate code is available at https://cs.android.com/android/platform/superproject/+/master:frameworks/native/cmds/dumpstate/
# The dumpstate code is used to generate bugreports on Android devices. It looks like there are
# bugs in the code that leave some sections with out ending lines. We need to handle these cases.
#
# The approach here is to flag probably broken section, and to search for plausible new section headers
# to close the previous section. This is a heuristic approach, and may not work in all cases. We can't do
# this for all sections as we will detect subsections as new sections.
SECTION_BROKEN_TERMINATORS = [
b"VM TRACES AT LAST ANR",
b"DIGITAL_HALL",
]
class DumpStateArtifact(AndroidArtifact):
def __init__(self, *args, **kwargs):
self.dumpstate_sections = []
self.dumpstate_header = {}
self.unparsed_lines = []
super().__init__(*args, **kwargs)
def _parse_dumpstate_header(self, header_text):
"""
Parse dumpstate header metadata
"""
fields = {}
for line in header_text.splitlines():
if line.startswith(b"="):
continue
if b":" in line:
# Save line if it's a key-value pair.
key, value = line.split(b":", 1)
fields[key] = value[1:]
if not line and fields:
# Finish if we get an empty line and already parsed lines
break
else:
# Skip until we find lines
continue
self.dumpstate_header = fields
return fields
def _get_section_header(self, header_match):
"""
Create internal dictionary to track dumpsys section.
"""
section_full = header_match.group(0).strip(b"-").strip()
section_name = header_match.group(1).rstrip()
if header_match.group(2):
section_command = header_match.group(2).strip(b"()")
else:
# Some headers can missing the command
section_command = ""
has_broken_terminator = False
for broken_section in SECTION_BROKEN_TERMINATORS:
if broken_section in section_name:
has_broken_terminator = True
break
section = {
"section_name": section_name,
"section_command": section_command,
"section_full": section_full,
"missing_terminator": has_broken_terminator,
"lines": [],
"error": False,
}
self.dumpstate_sections.append(section)
return section
def parse_dumpstate(self, text: str) -> list:
"""
Extract all sections from a full dumpstate file.
:param text: content of the full dumpstate file (string)
"""
# Parse the header
self._parse_dumpstate_header(text)
header = b"------ "
# Regexes to parse headers
section_name_re = re.compile(rb"------ ([\w\d\s\-\/\&]+)(\(.*\))? ------")
end_of_section_re = re.compile(rb"------ End of .* ------")
missing_file_error_re = re.compile(rb"\*\*\* (.*): No such file or directory")
generic_error_re = re.compile(rb"\*\*\* (.*) (?<!\*\*\*)$")
section = None
# Parse each line in dumpstate and look for headers
for line in text.splitlines():
if not section:
# If we find an end section when not in a section, we can skip
# It's probably the trailing line of a section.
end_of_section_match = re.match(end_of_section_re, line)
if end_of_section_match:
self.unparsed_lines.append(line)
continue
possible_section_header = re.match(section_name_re, line)
if possible_section_header:
section = self._get_section_header(possible_section_header)
# print("found section", section)
continue
else:
# We continue to next line as we weren't already in a section
self.unparsed_lines.append(line)
continue
if line.lstrip().startswith(header):
# This may be an internal section, or the terminator for our current section
# Ending looks like: ------ 0.557s was the duration of 'DUMPSYS CRITICAL' ------
# Check that we have the end for the right command.
section_command_in_quotes = b"'" + section["section_name"] + b"'"
if (
section_command_in_quotes in line
or section["section_full"]
in line # Needed for 0.070s was the duration of 'KERNEL LOG (dmesg)'
):
# Add end line and finish up the section
section["lines"].append(line)
section = None
continue
# If we haven't closed previous, but this matches a section header, we can try close.
# Probably a bug where not closed properly. We explicitly flag known broken fields.
# This fails on these blocks if we dont blacklist. Maybe we need to make a blacklist of badly closed items
# ------ DUMP BLOCK STAT ------
# ------ BLOCK STAT (/sys/block/dm-20) ------
possible_section_header = re.match(section_name_re, line)
if possible_section_header and section["missing_terminator"]:
section = self._get_section_header(possible_section_header)
else:
# Probably terminator for subsection, ignore and treat as a regular line.
pass
# Handle lines with special meaning
# TODO: This is failing as sometime errors are followed by a terminator and sometimes not.
if re.match(missing_file_error_re, line) or re.match(
generic_error_re, line
):
# The line in a failed file read which is dumped without an header end section.
section["failed"] = True
section["lines"].append(line)
section = None
else:
section["lines"].append(line)
return self.dumpstate_sections

View File

@@ -4,7 +4,6 @@
# https://license.mvt.re/1.1/
from .artifact import AndroidArtifact
import re
class DumpsysAccessibilityArtifact(AndroidArtifact):
@@ -26,8 +25,6 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
:param content: content of the accessibility section (string)
"""
# "Old" syntax
in_services = False
for line in content.splitlines():
if line.strip().startswith("installed services:"):
@@ -38,7 +35,6 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
continue
if line.strip() == "}":
# At end of installed services
break
service = line.split(":")[1].strip()
@@ -49,19 +45,3 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
"service": service,
}
)
# "New" syntax - AOSP >= 14 (?)
# Looks like:
# Enabled services:{{com.azure.authenticator/com.microsoft.brooklyn.module.accessibility.BrooklynAccessibilityService}, {com.agilebits.onepassword/com.agilebits.onepassword.filling.accessibility.FillingAccessibilityService}}
for line in content.splitlines():
if line.strip().startswith("Enabled services:"):
matches = re.finditer(r"{([^{]+?)}", line)
for match in matches:
# Each match is in format: <package_name>/<service>
package_name, _, service = match.group(1).partition("/")
self.results.append(
{"package_name": package_name, "service": service}
)

View File

@@ -1,203 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import re
from typing import Any, Dict, List, Union
from mvt.android.utils import ROOT_PACKAGES
from .artifact import AndroidArtifact
class DumpsysPackagesArtifact(AndroidArtifact):
def check_indicators(self) -> None:
for result in self.results:
if result["package_name"] in ROOT_PACKAGES:
self.log.warning(
"Found an installed package related to "
'rooting/jailbreaking: "%s"',
result["package_name"],
)
self.detected.append(result)
continue
if not self.indicators:
continue
ioc = self.indicators.check_app_id(result.get("package_name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def serialize(self, record: dict) -> Union[dict, list]:
records = []
timestamps = [
{"event": "package_install", "timestamp": record["timestamp"]},
{
"event": "package_first_install",
"timestamp": record["first_install_time"],
},
{"event": "package_last_update", "timestamp": record["last_update_time"]},
]
for timestamp in timestamps:
records.append(
{
"timestamp": timestamp["timestamp"],
"module": self.__class__.__name__,
"event": timestamp["event"],
"data": f"Install or update of package {record['package_name']}",
}
)
return records
@staticmethod
def parse_dumpsys_package_for_details(output: str) -> Dict[str, Any]:
"""
Parse one entry of a dumpsys package information
"""
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"permissions": [],
"requested_permissions": [],
}
in_install_permissions = False
in_runtime_permissions = False
in_declared_permissions = False
in_requested_permissions = True
for line in output.splitlines():
if in_install_permissions:
if line.startswith(" " * 4) and not line.startswith(" " * 6):
in_install_permissions = False
else:
lineinfo = line.strip().split(":")
permission = lineinfo[0]
granted = None
if "granted=" in lineinfo[1]:
granted = "granted=true" in lineinfo[1]
details["permissions"].append(
{"name": permission, "granted": granted, "type": "install"}
)
if in_runtime_permissions:
if not line.startswith(" " * 8):
in_runtime_permissions = False
else:
lineinfo = line.strip().split(":")
permission = lineinfo[0]
granted = None
if "granted=" in lineinfo[1]:
granted = "granted=true" in lineinfo[1]
details["permissions"].append(
{"name": permission, "granted": granted, "type": "runtime"}
)
if in_declared_permissions:
if not line.startswith(" " * 6):
in_declared_permissions = False
else:
permission = line.strip().split(":")[0]
details["permissions"].append(
{"name": permission, "type": "declared"}
)
if in_requested_permissions:
if not line.startswith(" " * 6):
in_requested_permissions = False
else:
details["requested_permissions"].append(line.strip())
if line.strip().startswith("userId="):
details["uid"] = line.split("=")[1].strip()
elif line.strip().startswith("versionName="):
details["version_name"] = line.split("=")[1].strip()
elif line.strip().startswith("versionCode="):
details["version_code"] = line.split("=", 1)[1].strip()
elif line.strip().startswith("timeStamp="):
details["timestamp"] = line.split("=")[1].strip()
elif line.strip().startswith("firstInstallTime="):
details["first_install_time"] = line.split("=")[1].strip()
elif line.strip().startswith("lastUpdateTime="):
details["last_update_time"] = line.split("=")[1].strip()
elif line.strip() == "install permissions:":
in_install_permissions = True
elif line.strip() == "runtime permissions:":
in_runtime_permissions = True
elif line.strip() == "declared permissions:":
in_declared_permissions = True
elif line.strip() == "requested permissions:":
in_requested_permissions = True
return details
def parse_dumpsys_packages(self, output: str) -> List[Dict[str, Any]]:
"""
Parse the dumpsys package service data
"""
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
results = []
package_name = None
package = {}
lines = []
for line in output.splitlines():
if line.startswith(" Package ["):
if len(lines) > 0:
details = self.parse_dumpsys_package_for_details("\n".join(lines))
package.update(details)
results.append(package)
lines = []
package = {}
matches = pkg_rxp.findall(line)
if not matches:
continue
package_name = matches[0]
package["package_name"] = package_name
continue
if not package_name:
continue
lines.append(line)
if len(lines) > 0:
details = self.parse_dumpsys_package_for_details("\n".join(lines))
package.update(details)
results.append(package)
return results
def parse(self, content: str):
"""
Parse the Dumpsys Package section for activities
Adds results to self.results
:param content: content of the package section (string)
"""
self.results = []
package = []
in_package_list = False
for line in content.split("\n"):
if line.startswith("Packages:"):
in_package_list = True
continue
if not in_package_list:
continue
if line.strip() == "":
break
package.append(line)
self.results = self.parse_dumpsys_packages("\n".join(package))

View File

@@ -51,11 +51,6 @@ ANDROID_DANGEROUS_SETTINGS = [
"key": "install_non_market_apps",
"safe_value": "0",
},
{
"description": "enabled accessibility services",
"key": "accessibility_enabled",
"safe_value": "0",
},
]

View File

@@ -72,7 +72,7 @@ def version():
is_flag=True,
help="Extract all packages installed on the phone, including system packages",
)
@click.option("--virustotal", "-V", is_flag=True, help="Check packages on VirusTotal")
@click.option("--virustotal", "-v", is_flag=True, help="Check packages on VirusTotal")
@click.option(
"--output",
"-o",

View File

@@ -4,25 +4,86 @@
# https://license.mvt.re/1.1/
import logging
from typing import Optional, Union
from typing import List, Optional, Union
from rich.console import Console
from rich.progress import track
from rich.table import Table
from rich.text import Text
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
from mvt.android.utils import (
DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
ROOT_PACKAGES,
SECURITY_PACKAGES,
SYSTEM_UPDATE_PACKAGES,
)
from mvt.android.parsers.dumpsys import parse_dumpsys_package_for_details
from mvt.common.virustotal import VTNoKey, VTQuotaExceeded, virustotal_lookup
from .base import AndroidExtraction
DANGEROUS_PERMISSIONS_THRESHOLD = 10
DANGEROUS_PERMISSIONS = [
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.CAMERA",
"android.permission.DISABLE_KEYGUARD",
"android.permission.PROCESS_OUTGOING_CALLS",
"android.permission.READ_CALENDAR",
"android.permission.READ_CALL_LOG",
"android.permission.READ_CONTACTS",
"android.permission.READ_PHONE_STATE",
"android.permission.READ_SMS",
"android.permission.RECEIVE_MMS",
"android.permission.RECEIVE_SMS",
"android.permission.RECEIVE_WAP_PUSH",
"android.permission.RECORD_AUDIO",
"android.permission.SEND_SMS",
"android.permission.SYSTEM_ALERT_WINDOW",
"android.permission.USE_CREDENTIALS",
"android.permission.USE_SIP",
"com.android.browser.permission.READ_HISTORY_BOOKMARKS",
]
ROOT_PACKAGES: List[str] = [
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.thirdparty.superuser",
"com.yellowes.su",
"com.koushikdutta.rommanager",
"com.koushikdutta.rommanager.license",
"com.dimonvideo.luckypatcher",
"com.chelpus.lackypatch",
"com.ramdroid.appquarantine",
"com.ramdroid.appquarantinepro",
"com.devadvance.rootcloak",
"com.devadvance.rootcloakplus",
"de.robv.android.xposed.installer",
"com.saurik.substrate",
"com.zachspong.temprootremovejb",
"com.amphoras.hidemyroot",
"com.amphoras.hidemyrootadfree",
"com.formyhm.hiderootPremium",
"com.formyhm.hideroot",
"me.phh.superuser",
"eu.chainfire.supersu.pro",
"com.kingouser.com",
"com.topjohnwu.magisk",
]
SECURITY_PACKAGES = [
"com.policydm",
"com.samsung.android.app.omcagent",
"com.samsung.android.securitylogagent",
"com.sec.android.soagent",
]
SYSTEM_UPDATE_PACKAGES = [
"com.android.updater",
"com.google.android.gms",
"com.huawei.android.hwouc",
"com.lge.lgdmsclient",
"com.motorola.ccc.ota",
"com.oneplus.opbackup",
"com.oppo.ota",
"com.transsion.systemupdate",
"com.wssyncmldm",
]
class Packages(AndroidExtraction):
"""This module extracts the list of installed packages."""
@@ -173,9 +234,7 @@ class Packages(AndroidExtraction):
if line.strip() == "Packages:":
in_packages = True
return DumpsysPackagesArtifact.parse_dumpsys_package_for_details(
"\n".join(lines)
)
return parse_dumpsys_package_for_details("\n".join(lines))
def _get_files_for_package(self, package_name: str) -> list:
command = f"pm path {package_name}"

View File

@@ -12,7 +12,6 @@ from .dumpsys_dbinfo import DumpsysDBInfo
from .dumpsys_packages import DumpsysPackages
from .dumpsys_receivers import DumpsysReceivers
from .getprop import Getprop
from .packages import Packages
from .processes import Processes
from .settings import Settings
from .sms import SMS
@@ -25,7 +24,6 @@ ANDROIDQF_MODULES = [
DumpsysDBInfo,
DumpsysBatteryDaily,
DumpsysBatteryHistory,
Packages,
Processes,
Getprop,
Settings,

View File

@@ -12,7 +12,7 @@ from .base import AndroidQFModule
class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidQFModule):
"""This module analyses dumpsys accessibility"""
"""This module analyse dumpsys accessbility"""
def __init__(
self,

View File

@@ -4,18 +4,19 @@
# https://license.mvt.re/1.1/
import logging
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
from mvt.android.modules.adb.packages import (
DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
ROOT_PACKAGES,
)
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
from .base import AndroidQFModule
class DumpsysPackages(DumpsysPackagesArtifact, AndroidQFModule):
class DumpsysPackages(AndroidQFModule):
"""This module analyse dumpsys packages"""
def __init__(
@@ -36,15 +37,70 @@ class DumpsysPackages(DumpsysPackagesArtifact, AndroidQFModule):
results=results,
)
def serialize(self, record: dict) -> Union[dict, list]:
entries = []
for entry in ["timestamp", "first_install_time", "last_update_time"]:
if entry in record:
entries.append(
{
"timestamp": record[entry],
"module": self.__class__.__name__,
"event": entry,
"data": f"Package {record['package_name']} "
f"({record['uid']})",
}
)
return entries
def check_indicators(self) -> None:
for result in self.results:
if result["package_name"] in ROOT_PACKAGES:
self.log.warning(
"Found an installed package related to "
'rooting/jailbreaking: "%s"',
result["package_name"],
)
self.detected.append(result)
continue
if not self.indicators:
continue
ioc = self.indicators.check_app_id(result.get("package_name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if len(dumpsys_file) != 1:
self.log.info("Dumpsys file not found")
return
data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace")
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:")
self.parse(content)
data = self._get_file_content(dumpsys_file[0])
package = []
in_service = False
in_package_list = False
for line in data.decode("utf-8").split("\n"):
if line.strip().startswith("DUMP OF SERVICE package:"):
in_service = True
continue
if in_service and line.startswith("Packages:"):
in_package_list = True
continue
if not in_service or not in_package_list:
continue
if line.strip() == "":
break
package.append(line)
self.results = parse_dumpsys_packages("\n".join(package))
for result in self.results:
dangerous_permissions_count = 0

View File

@@ -1,97 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
import json
from mvt.android.utils import (
ROOT_PACKAGES,
BROWSER_INSTALLERS,
PLAY_STORE_INSTALLERS,
THIRD_PARTY_STORE_INSTALLERS,
)
from .base import AndroidQFModule
class Packages(AndroidQFModule):
"""This module examines the installed packages in packages.json"""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Optional[list] = None,
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
for result in self.results:
if result["name"] in ROOT_PACKAGES:
self.log.warning(
"Found an installed package related to "
'rooting/jailbreaking: "%s"',
result["name"],
)
self.detected.append(result)
continue
# Detections for apps installed via unusual methods
if result["installer"] in THIRD_PARTY_STORE_INSTALLERS:
self.log.warning(
'Found a package installed via a third party store (installer="%s"): "%s"',
result["installer"],
result["name"],
)
elif result["installer"] in BROWSER_INSTALLERS:
self.log.warning(
'Found a package installed via a browser (installer="%s"): "%s"',
result["installer"],
result["name"],
)
elif result["installer"] == "null" and result["system"] is False:
self.log.warning(
'Found a non-system package installed via adb or another method: "%s"',
result["name"],
)
elif result["installer"] in PLAY_STORE_INSTALLERS:
pass
if not self.indicators:
continue
ioc = self.indicators.check_app_id(result.get("name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
for package_file in result.get("files", []):
ioc = self.indicators.check_file_hash(package_file["sha256"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self) -> None:
packages = self._get_files_by_pattern("*/packages.json")
if not packages:
self.log.error(
"packages.json file not found in this androidqf bundle. Possibly malformed?"
)
return
self.results = json.loads(self._get_file_content(packages[0]))
self.log.info("Found %d packages in packages.json", len(self.results))

View File

@@ -4,15 +4,19 @@
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from typing import Optional, Union
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
from mvt.android.utils import DANGEROUS_PERMISSIONS, DANGEROUS_PERMISSIONS_THRESHOLD
from mvt.android.modules.adb.packages import (
DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
ROOT_PACKAGES,
)
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
from .base import BugReportModule
class Packages(DumpsysPackagesArtifact, BugReportModule):
class Packages(BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(
@@ -33,18 +37,83 @@ class Packages(DumpsysPackagesArtifact, BugReportModule):
results=results,
)
def serialize(self, record: dict) -> Union[dict, list]:
records = []
timestamps = [
{"event": "package_install", "timestamp": record["timestamp"]},
{
"event": "package_first_install",
"timestamp": record["first_install_time"],
},
{"event": "package_last_update", "timestamp": record["last_update_time"]},
]
for timestamp in timestamps:
records.append(
{
"timestamp": timestamp["timestamp"],
"module": self.__class__.__name__,
"event": timestamp["event"],
"data": f"Install or update of package {record['package_name']}",
}
)
return records
def check_indicators(self) -> None:
for result in self.results:
if result["package_name"] in ROOT_PACKAGES:
self.log.warning(
"Found an installed package related to "
'rooting/jailbreaking: "%s"',
result["package_name"],
)
self.detected.append(result)
continue
if not self.indicators:
continue
ioc = self.indicators.check_app_id(result.get("package_name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self) -> None:
data = self._get_dumpstate_file()
if not data:
content = self._get_dumpstate_file()
if not content:
self.log.error(
"Unable to find dumpstate file. "
"Did you provide a valid bug report archive?"
)
return
data = data.decode("utf-8", errors="replace")
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:")
self.parse(content)
in_package = False
in_packages_list = False
lines = []
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE package:":
in_package = True
continue
if not in_package:
continue
if line.strip() == "Packages:":
in_packages_list = True
continue
if not in_packages_list:
continue
if line.strip() == "":
break
lines.append(line)
self.results = parse_dumpsys_packages("\n".join(lines))
for result in self.results:
dangerous_permissions_count = 0

View File

@@ -0,0 +1,131 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import re
from typing import Any, Dict, List
def parse_dumpsys_package_for_details(output: str) -> Dict[str, Any]:
"""
Parse one entry of a dumpsys package information
"""
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"permissions": [],
"requested_permissions": [],
}
in_install_permissions = False
in_runtime_permissions = False
in_declared_permissions = False
in_requested_permissions = True
for line in output.splitlines():
if in_install_permissions:
if line.startswith(" " * 4) and not line.startswith(" " * 6):
in_install_permissions = False
else:
lineinfo = line.strip().split(":")
permission = lineinfo[0]
granted = None
if "granted=" in lineinfo[1]:
granted = "granted=true" in lineinfo[1]
details["permissions"].append(
{"name": permission, "granted": granted, "type": "install"}
)
if in_runtime_permissions:
if not line.startswith(" " * 8):
in_runtime_permissions = False
else:
lineinfo = line.strip().split(":")
permission = lineinfo[0]
granted = None
if "granted=" in lineinfo[1]:
granted = "granted=true" in lineinfo[1]
details["permissions"].append(
{"name": permission, "granted": granted, "type": "runtime"}
)
if in_declared_permissions:
if not line.startswith(" " * 6):
in_declared_permissions = False
else:
permission = line.strip().split(":")[0]
details["permissions"].append({"name": permission, "type": "declared"})
if in_requested_permissions:
if not line.startswith(" " * 6):
in_requested_permissions = False
else:
details["requested_permissions"].append(line.strip())
if line.strip().startswith("userId="):
details["uid"] = line.split("=")[1].strip()
elif line.strip().startswith("versionName="):
details["version_name"] = line.split("=")[1].strip()
elif line.strip().startswith("versionCode="):
details["version_code"] = line.split("=", 1)[1].strip()
elif line.strip().startswith("timeStamp="):
details["timestamp"] = line.split("=")[1].strip()
elif line.strip().startswith("firstInstallTime="):
details["first_install_time"] = line.split("=")[1].strip()
elif line.strip().startswith("lastUpdateTime="):
details["last_update_time"] = line.split("=")[1].strip()
elif line.strip() == "install permissions:":
in_install_permissions = True
elif line.strip() == "runtime permissions:":
in_runtime_permissions = True
elif line.strip() == "declared permissions:":
in_declared_permissions = True
elif line.strip() == "requested permissions:":
in_requested_permissions = True
return details
def parse_dumpsys_packages(output: str) -> List[Dict[str, Any]]:
"""
Parse the dumpsys package service data
"""
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
results = []
package_name = None
package = {}
lines = []
for line in output.splitlines():
if line.startswith(" Package ["):
if len(lines) > 0:
details = parse_dumpsys_package_for_details("\n".join(lines))
package.update(details)
results.append(package)
lines = []
package = {}
matches = pkg_rxp.findall(line)
if not matches:
continue
package_name = matches[0]
package["package_name"] = package_name
continue
if not package_name:
continue
lines.append(line)
if len(lines) > 0:
details = parse_dumpsys_package_for_details("\n".join(lines))
package.update(details)
results.append(package)
return results

View File

@@ -3,7 +3,6 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from datetime import datetime, timedelta
from typing import List
def warn_android_patch_level(patch_level: str, log) -> bool:
@@ -18,88 +17,3 @@ def warn_android_patch_level(patch_level: str, log) -> bool:
return True
return False
ROOT_PACKAGES: List[str] = [
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.thirdparty.superuser",
"com.yellowes.su",
"com.koushikdutta.rommanager",
"com.koushikdutta.rommanager.license",
"com.dimonvideo.luckypatcher",
"com.chelpus.lackypatch",
"com.ramdroid.appquarantine",
"com.ramdroid.appquarantinepro",
"com.devadvance.rootcloak",
"com.devadvance.rootcloakplus",
"de.robv.android.xposed.installer",
"com.saurik.substrate",
"com.zachspong.temprootremovejb",
"com.amphoras.hidemyroot",
"com.amphoras.hidemyrootadfree",
"com.formyhm.hiderootPremium",
"com.formyhm.hideroot",
"me.phh.superuser",
"eu.chainfire.supersu.pro",
"com.kingouser.com",
"com.topjohnwu.magisk",
]
DANGEROUS_PERMISSIONS_THRESHOLD = 10
DANGEROUS_PERMISSIONS = [
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.CAMERA",
"android.permission.DISABLE_KEYGUARD",
"android.permission.PROCESS_OUTGOING_CALLS",
"android.permission.READ_CALENDAR",
"android.permission.READ_CALL_LOG",
"android.permission.READ_CONTACTS",
"android.permission.READ_PHONE_STATE",
"android.permission.READ_SMS",
"android.permission.RECEIVE_MMS",
"android.permission.RECEIVE_SMS",
"android.permission.RECEIVE_WAP_PUSH",
"android.permission.RECORD_AUDIO",
"android.permission.SEND_SMS",
"android.permission.SYSTEM_ALERT_WINDOW",
"android.permission.USE_CREDENTIALS",
"android.permission.USE_SIP",
"com.android.browser.permission.READ_HISTORY_BOOKMARKS",
]
SECURITY_PACKAGES = [
"com.policydm",
"com.samsung.android.app.omcagent",
"com.samsung.android.securitylogagent",
"com.sec.android.soagent",
]
SYSTEM_UPDATE_PACKAGES = [
"com.android.updater",
"com.google.android.gms",
"com.huawei.android.hwouc",
"com.lge.lgdmsclient",
"com.motorola.ccc.ota",
"com.oneplus.opbackup",
"com.oppo.ota",
"com.transsion.systemupdate",
"com.wssyncmldm",
]
# Apps installed from the Play store have this installer
PLAY_STORE_INSTALLERS = ["com.android.vending"]
# Installer id for apps from common 3rd party stores
THIRD_PARTY_STORE_INSTALLERS = ["com.aurora.store", "org.fdroid.fdroid"]
# Packages installed via a browser have these installers
BROWSER_INSTALLERS = [
"com.google.android.packageinstaller",
"com.android.packageinstaller",
]

138
mvt/common/alerting.py Normal file
View File

@@ -0,0 +1,138 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from enum import Enum
class AlertLevel(Enum):
"""
informational: Rule is intended for enrichment of events, e.g. by tagging them. No case or alerting should be triggered by such rules because it is expected that a huge amount of events will match these rules.
low: Notable event but rarely an incident. Low rated events can be relevant in high numbers or combination with others. Immediate reaction shouldnt be necessary, but a regular review is recommended.
medium: Relevant event that should be reviewed manually on a more frequent basis.
high: Relevant event that should trigger an internal alert and requires a prompt review.
critical: Highly relevant event that indicates an incident. Critical events should be reviewed immediately.
"""
INFORMATIONAL = 0
LOW = 10
MEDIUM = 20
HIGH = 30
CRITICAL = 40
class AlertStore(object):
"""
Track all of the alerts and detections generated during an analysis.
Results can be logged as log messages or in JSON format for processing by other tools.
"""
def __init__(self) -> None:
self.alerts = []
def add_alert(
self, level, message=None, event_time=None, event=None, ioc=None, detected=True
):
"""
Add an alert to the alert store.
"""
self.alerts.append(
Alert(
level=level,
message=message,
event_time=event_time,
event=event,
ioc=ioc,
detected=detected,
)
)
def informational(
self, message=None, event_time=None, event=None, ioc=None, detected=False
):
self.add_alert(
AlertLevel.INFORMATIONAL,
message=message,
event_time=event_time,
event=event,
ioc=ioc,
detected=detected,
)
def low(self, message=None, event_time=None, event=None, ioc=None, detected=False):
self.add_alert(
AlertLevel.LOW,
message=message,
event_time=event_time,
event=event,
ioc=ioc,
detected=detected,
)
def medium(
self, message=None, event_time=None, event=None, ioc=None, detected=False
):
self.add_alert(
AlertLevel.MEDIUM,
message=message,
event_time=event_time,
event=event,
ioc=ioc,
detected=detected,
)
def high(self, message=None, event_time=None, event=None, ioc=None, detected=False):
self.add_alert(
AlertLevel.HIGH,
message=message,
event_time=event_time,
event=event,
ioc=ioc,
detected=detected,
)
def critical(
self, message=None, event_time=None, event=None, ioc=None, detected=False
):
self.add_alert(
AlertLevel.CRITICAL,
message=message,
event_time=event_time,
event=event,
ioc=ioc,
detected=detected,
)
class Alert(object):
"""
An alert generated by an MVT module.
"""
def __init__(self, level, message, event_time, event, ioc, detected):
self.level = level
self.message = message
self.event_time = event_time
self.event = event
self.ioc = ioc
self.detected = detected
def __repr__(self):
return f"<Alert level={self.level} message={self.message} event_time={self.event_time} event={self.event}>"
def __str__(self):
return f"{self.level} {self.message} {self.event_time} {self.event}"
def to_log(self):
return f"{self.level} {self.message} {self.event_time} {self.event}"
def to_json(self):
return {
"level": self.level,
"message": self.message,
"event_time": self.event_time,
"event": self.event,
"ioc": self.ioc,
"detected": self.detected,
}

View File

@@ -85,15 +85,6 @@ class Command:
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
# MVT can be run in a loop
# Old file handlers stick around in subsequent loops
# Remove any existing logging.FileHandler instances
for handler in logger.handlers:
if isinstance(handler, logging.FileHandler):
logger.removeHandler(handler)
# And finally add the new one
logger.addHandler(file_handler)
def _store_timeline(self) -> None:
@@ -169,27 +160,6 @@ class Command:
def finish(self) -> None:
raise NotImplementedError
def _show_disable_adb_warning(self) -> None:
"""Warn if ADB is enabled"""
if type(self).__name__ in ["CmdAndroidCheckADB", "CmdAndroidCheckAndroidQF"]:
self.log.info(
"Please disable Developer Options and ADB (Android Debug Bridge) on the device once finished with the acquisition. "
"ADB is a powerful tool which can allow unauthorized access to the device."
)
def _show_support_message(self) -> None:
support_message = "Please seek reputable expert help if you have serious concerns about a possible spyware attack. Such support is available to human rights defenders and civil society through Amnesty International's Security Lab at https://securitylab.amnesty.org/get-help/?c=mvt"
if self.detected_count == 0:
self.log.info(
f"[bold]NOTE:[/bold] Using MVT with public indicators of compromise (IOCs) [bold]WILL NOT[/bold] automatically detect advanced attacks.\n\n{support_message}",
extra={"markup": True},
)
else:
self.log.warning(
f"[bold]NOTE: Detected indicators of compromise[/bold]. Only expert review can confirm if the detected indicators are signs of an attack.\n\n{support_message}",
extra={"markup": True},
)
def run(self) -> None:
try:
self.init()
@@ -238,6 +208,3 @@ class Command:
self._store_timeline()
self._store_info()
self._show_disable_adb_warning()
self._show_support_message()

View File

@@ -10,7 +10,7 @@ from .version import MVT_VERSION
def check_updates() -> None:
# First we check for MVT version updates.
# First we check for MVT version udpates.
mvt_updates = MVTUpdates()
try:
latest_version = mvt_updates.check()

View File

@@ -9,65 +9,47 @@ import requests
from tld import get_tld
SHORTENER_DOMAINS = [
"0rz.tw",
"1drv.ms",
"1link.in",
"1url.com",
"2big.at",
"2.gp",
"2pl.us",
"2tu.us",
"2ya.com",
"3.ly",
"4sq.com",
"4url.cc",
"6url.com",
"7.ly",
"a.gg",
"a.nf",
"a2a.me",
"abbrr.com",
"adf.ly",
"adjix.com",
"a.gg",
"alturl.com",
"a.nf",
"anon.to",
"apple.news",
"atu.ca",
"b23.ru",
"bacn.me",
"bc.vc",
"bfy.tw",
"binged.it",
"bit.do",
"bit.ly",
"bizj.us",
"bkite.com",
"bloat.me",
"budurl.com",
"buff.ly",
"buk.me",
"burnurl.com",
"chilp.it",
"chn.ge",
"clck.ru",
"clickmeter.com",
"cli.gs",
"c-o.in",
"chilp.it",
"clck.ru",
"cli.gs",
"clickmeter.com",
"cort.as",
"cut.ly",
"cutt.ly",
"cuturl.com",
"dai.ly",
"dailym.ai",
"db.tt",
"decenturl.com",
"decenturl.com",
"dfl8.me",
"digbig.com",
"digg.com",
"disq.us",
"dlvr.it",
"doiop.com",
"do.my",
"dwarfurl.com",
"dy.fi",
"easyuri.com",
@@ -76,35 +58,27 @@ SHORTENER_DOMAINS = [
"esyurl.com",
"ewerl.com",
"fa.b",
"fa.by",
"fb.me",
"fff.to",
"ff.im",
"fff.to",
"fhurl.com",
"fire.to",
"firsturl.de",
"firsturl.net",
"flic.kr",
"flq.us",
"fly2.ws",
"fon.gs",
"forms.gle",
"fwd4.me",
"gdurl.com",
"gg.gg",
"gl.am",
"go2cut.com",
"go2.me",
"go.9nl.com",
"go2.me",
"go2cut.com",
"goo.gl",
"goshrink.com",
"got.by",
"gowat.ch",
"gri.ms",
"gurl.es",
"hellotxt.com",
"hex.io",
"hongkiat.shorturl.com",
"hover.com",
"href.in",
"ht.ly",
@@ -113,15 +87,13 @@ SHORTENER_DOMAINS = [
"hurl.it",
"hurl.me",
"hurl.ws",
"ibb.co",
"icanhaz.com",
"idek.net",
"inreply.to",
"iscool.net",
"is.gd",
"iscool.net",
"iterasi.net",
"jijr.com",
"j.mp",
"jmp2.net",
"just.as",
"kissa.be",
@@ -129,23 +101,21 @@ SHORTENER_DOMAINS = [
"klck.me",
"korta.nu",
"krunchd.com",
"lat.ms",
"liip.to",
"liltext.com",
"lin.cr",
"linkbee.com",
"linkbun.ch",
"liurl.cn",
"lnkd.in",
"lnk.gd",
"lnk.in",
"ln-s.net",
"ln-s.ru",
"lnk.gd",
"lnk.in",
"lnkd.in",
"loopt.us",
"lru.jp",
"lt.tl",
"lurl.no",
"lyhyt.eu",
"metamark.net",
"migre.me",
"minilien.com",
@@ -153,71 +123,52 @@ SHORTENER_DOMAINS = [
"minurl.fr",
"moourl.com",
"myurl.in",
"nbcnews.to",
"ne1.net",
"njx.me",
"nn.nf",
"notlong.com",
"n.pr",
"nsfw.in",
"nyti.ms",
"om.ly",
"onforb.es",
"on.mktw.net",
"ow.ly",
"o-x.fr",
"pca.st",
"om.ly",
"ow.ly",
"pd.am",
"pic.gd",
"ping.fm",
"piurl.com",
"pnt.me",
"politi.co",
"poprl.com",
"posted.at",
"post.ly",
"posted.at",
"profile.to",
"q.gs",
"qicute.com",
"qlnk.net",
"qr.ae",
"qte.me",
"quip-art.com",
"rb6.me",
"rb.gy",
"read.bi",
"redir.ec",
"redirx.com",
"redr.me",
"reut.rs",
"rickroll.it",
"r.im",
"ri.ms",
"rickroll.it",
"riz.gd",
"rsmonkey.com",
"rubyurl.com",
"ru.ly",
"rubyurl.com",
"s7y.us",
"safe.mn",
"sharein.com",
"sharetabs.com",
"shorl.com",
"short.ie",
"short.to",
"shortlinks.co.uk",
"shortna.me",
"short.to",
"shorturl.at",
"shorturl.com",
"shoturl.us",
"shout.to",
"shrinkify.com",
"shrinkster.com",
"shrten.com",
"shrt.st",
"shrten.com",
"shrunkin.com",
"shw.me",
"simurl.com",
"smsh.me",
"sn.im",
"snipr.com",
"snipurl.com",
@@ -228,30 +179,24 @@ SHORTENER_DOMAINS = [
"starturl.com",
"sturly.com",
"su.pr",
"t.cn",
"t.co",
"tcrn.ch",
"tgr.ph",
"thrdl.es",
"tighturl.com",
"tiny.cc",
"tiny.pl",
"tiny123.com",
"tinyarro.ws",
"tiny.cc",
"tinylink.in",
"tiny.pl",
"tiny.tw",
"tinytw.it",
"tinyuri.ca",
"tinyurl.com",
"tinyvid.io",
"t.me",
"tnij.org",
"tnw.to",
"togoto.us",
"to.ly",
"traceurl.com",
"togoto.us",
"tr.im",
"tr.my",
"traceurl.com",
"turo.us",
"tweetburner.com",
"twirl.at",
@@ -261,62 +206,49 @@ SHORTENER_DOMAINS = [
"twiturl.de",
"twurl.cc",
"twurl.nl",
"u6e.de",
"ub0.cc",
"ukl.me.uk",
"u.mavrev.com",
"u.nu",
"u6e.de",
"ub0.cc",
"updating.me",
"ur1.ca",
"url.co.uk",
"url.ie",
"url4.eu",
"urlao.com",
"urlbrief.com",
"url.co.uk",
"urlcover.com",
"urlcut.com",
"urlenco.de",
"urlhawk.com",
"url.ie",
"urlkiss.com",
"urlot.com",
"urlpire.com",
"urlx.ie",
"urlx.org",
"urlzen.com",
"use.my",
"u.to",
"v.gd",
"virl.com",
"vl.am",
"vurl.com",
"vzturl.com",
"w3t.org",
"wapo.st",
"wapurl.co.uk",
"wipi.es",
"wp.me",
"xaddr.com",
"x.co",
"x.se",
"xaddr.com",
"xeeurl.com",
"xr.com",
"xrl.in",
"xrl.us",
"x.se",
"xurl.es",
"xurl.jp",
"xzb.cc",
"ye.pe",
"yep.it",
"yfrog.com",
"yhoo.it",
"ymlp.com",
"yuarel.com",
"yweb.com",
"zi.ma",
"zi.pe",
"zipmyurl.com",
"zurl.to",
"zurl.ws",
"zz.gd",
]

View File

@@ -53,23 +53,20 @@ def convert_chrometime_to_datetime(timestamp: int) -> datetime.datetime:
def convert_datetime_to_iso(date_time: datetime.datetime) -> str:
"""Converts datetime to ISO string.
:param datetime: datetime, naive or timezone aware
:param datetime: datetime.
:type datetime: datetime.datetime
:returns: ISO datetime string in YYYY-mm-dd HH:MM:SS.ms format.
:rtype: str
"""
if not date_time:
try:
return date_time.strftime("%Y-%m-%d %H:%M:%S.%f")
except Exception:
return ""
if date_time.tzinfo:
# Timezone aware object - convert to UTC
date_time = date_time.astimezone(tz=datetime.timezone.utc)
return date_time.strftime("%Y-%m-%d %H:%M:%S.%f")
def convert_unix_to_utc_datetime(
timestamp: Union[int, float, str],
timestamp: Union[int, float, str]
) -> datetime.datetime:
"""Converts a unix epoch timestamp to UTC datetime.

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.5.4"
MVT_VERSION = "2.4.3"

View File

@@ -169,7 +169,7 @@
},
{
"identifier": "iPhone14,8",
"description": "iPhone 14 Plus"
"decription": "iPhone 14 Plus"
},
{
"identifier": "iPhone15,2",

View File

@@ -883,14 +883,6 @@
"version": "15.8",
"build": "19H370"
},
{
"version": "15.8.1",
"build": "19H380"
},
{
"version": "15.8.2",
"build": "19H384"
},
{
"build": "20A362",
"version": "16.0"
@@ -968,30 +960,6 @@
"version": "16.7.2",
"build": "20H115"
},
{
"version": "16.7.3",
"build": "20H232"
},
{
"version": "16.7.4",
"build": "20H240"
},
{
"version": "16.7.5",
"build": "20H307"
},
{
"version": "16.7.6",
"build": "20H320"
},
{
"version": "16.7.7",
"build": "20H330"
},
{
"version": "16.7.8",
"build": "20H343"
},
{
"version": "17.0",
"build": "21A327"
@@ -1027,57 +995,5 @@
{
"version": "17.1.1",
"build": "21B91"
},
{
"version": "17.1.2",
"build": "21B101"
},
{
"version": "17.2",
"build": "21C62"
},
{
"version": "17.2.1",
"build": "21C66"
},
{
"version": "17.3",
"build": "21D50"
},
{
"version": "17.3.1",
"build": "21D61"
},
{
"version": "17.4",
"build": "21E219"
},
{
"version": "17.4.1",
"build": "21E236"
},
{
"version": "17.4.1",
"build": "21E237"
},
{
"version": "17.5",
"build": "21F79"
},
{
"version": "17.5.1",
"build": "21F90"
},
{
"version": "17.6.1",
"build": "21G93"
},
{
"version": "17.6.1",
"build": "21G101"
},
{
"version": "18",
"build": "22A3354"
}
]

View File

@@ -8,6 +8,7 @@ import io
import logging
import os
import plistlib
import sqlite3
from typing import Optional
from mvt.common.module import DatabaseNotFoundError
@@ -123,7 +124,7 @@ class Manifest(IOSExtraction):
self.log.info("Found Manifest.db database at path: %s", manifest_db_path)
conn = self._open_sqlite_db(manifest_db_path)
conn = sqlite3.connect(manifest_db_path)
cur = conn.cursor()
cur.execute("SELECT * FROM Files;")

View File

@@ -49,7 +49,7 @@ class IOSExtraction(MVTModule):
"""
# TODO: Find a better solution.
if not forced:
conn = self._open_sqlite_db(file_path)
conn = sqlite3.connect(file_path)
cur = conn.cursor()
try:
@@ -91,9 +91,6 @@ class IOSExtraction(MVTModule):
self.log.info("Database at path %s recovered successfully!", file_path)
def _open_sqlite_db(self, file_path: str) -> sqlite3.Connection:
return sqlite3.connect(f"file:{file_path}?immutable=1", uri=True)
def _get_backup_files_from_manifest(
self, relative_path: Optional[str] = None, domain: Optional[str] = None
) -> Iterator[dict]:
@@ -112,7 +109,7 @@ class IOSExtraction(MVTModule):
base_sql = "SELECT fileID, domain, relativePath FROM Files WHERE "
try:
conn = self._open_sqlite_db(manifest_db_path)
conn = sqlite3.connect(manifest_db_path)
cur = conn.cursor()
if relative_path and domain:
cur.execute(

View File

@@ -85,7 +85,7 @@ class Analytics(IOSExtraction):
def _extract_analytics_data(self):
artifact = self.file_path.split("/")[-1]
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
try:

View File

@@ -64,7 +64,7 @@ class CacheFiles(IOSExtraction):
def _process_cache_file(self, file_path):
self.log.info("Processing cache file at path: %s", file_path)
conn = self._open_sqlite_db(file_path)
conn = sqlite3.connect(file_path)
cur = conn.cursor()
try:

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import logging
import sqlite3
from typing import Optional, Union
from mvt.common.utils import convert_mactime_to_iso
@@ -60,7 +61,7 @@ class SafariFavicon(IOSExtraction):
self.detected.append(result)
def _process_favicon_db(self, file_path):
conn = self._open_sqlite_db(file_path)
conn = sqlite3.connect(file_path)
# Fetch valid icon cache.
cur = conn.cursor()

View File

@@ -70,9 +70,6 @@ class ShutdownLog(IOSExtraction):
def process_shutdownlog(self, content):
current_processes = []
recent_processes = []
times_delayed = 0
delay = 0.0
for line in content.split("\n"):
line = line.strip()
@@ -81,22 +78,9 @@ class ShutdownLog(IOSExtraction):
{
"pid": line[line.find("pid: ") + 5 : line.find(" (")],
"client": line[line.find("(") + 1 : line.find(")")],
"delay": delay,
"times_delayed": times_delayed,
}
)
elif line.startswith("After "):
# Consider the previous processes
# End of the current processes
for p in current_processes:
recent_processes.append(p)
delay += float(line.split(" ")[1][:-2])
times_delayed += 1
current_processes = []
elif line.startswith("SIGTERM: "):
for p in current_processes:
recent_processes.append(p)
try:
mac_timestamp = int(line[line.find("[") + 1 : line.find("]")])
except ValueError:
@@ -108,21 +92,16 @@ class ShutdownLog(IOSExtraction):
isodate = convert_mactime_to_iso(mac_timestamp, from_2001=False)
for process in recent_processes:
for current_process in current_processes:
self.results.append(
{
"isodate": isodate,
"pid": process["pid"],
"client": process["client"],
"delay": process["delay"],
"times_delayed": process["times_delayed"],
"pid": current_process["pid"],
"client": current_process["client"],
}
)
current_processes = []
recent_processes = []
times_delayed = 0
delay = 0.0
self.results = sorted(self.results, key=lambda entry: entry["isodate"])

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import logging
import sqlite3
from typing import Optional, Union
from mvt.common.utils import convert_mactime_to_iso
@@ -81,7 +82,7 @@ class Calendar(IOSExtraction):
"""
Parse the calendar database
"""
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute(

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import logging
import sqlite3
from typing import Optional, Union
from mvt.common.utils import convert_mactime_to_iso
@@ -52,7 +53,7 @@ class Calls(IOSExtraction):
)
self.log.info("Found Calls database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute(
"""
@@ -69,9 +70,9 @@ class Calls(IOSExtraction):
"isodate": convert_mactime_to_iso(row[0]),
"duration": row[1],
"location": row[2],
"number": (
row[3].decode("utf-8") if row[3] and row[3] is bytes else row[3]
),
"number": row[3].decode("utf-8")
if row[3] and row[3] is bytes
else row[3],
"provider": row[4],
}
)

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import logging
import sqlite3
from typing import Optional, Union
from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso
@@ -65,7 +66,7 @@ class ChromeFavicon(IOSExtraction):
)
self.log.info("Found Chrome favicon cache database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
# Fetch icon cache
cur = conn.cursor()

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import logging
import sqlite3
from typing import Optional, Union
from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso
@@ -66,7 +67,7 @@ class ChromeHistory(IOSExtraction):
)
self.log.info("Found Chrome history database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute(
"""

View File

@@ -44,7 +44,7 @@ class Contacts(IOSExtraction):
)
self.log.info("Found Contacts database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
try:
cur.execute(

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import logging
import sqlite3
from typing import Optional, Union
from mvt.common.utils import convert_unix_to_iso
@@ -67,7 +68,7 @@ class FirefoxFavicon(IOSExtraction):
)
self.log.info("Found Firefox favicon database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute(
"""

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import logging
import sqlite3
from typing import Optional, Union
from mvt.common.utils import convert_unix_to_iso
@@ -67,7 +68,7 @@ class FirefoxHistory(IOSExtraction):
)
self.log.info("Found Firefox history database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute(
"""

View File

@@ -280,7 +280,7 @@ class InteractionC(IOSExtraction):
)
self.log.info("Found InteractionC database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
try:

View File

@@ -76,7 +76,7 @@ class SafariBrowserState(IOSExtraction):
def _process_browser_state_db(self, db_path):
self._recover_sqlite_db_if_needed(db_path)
conn = self._open_sqlite_db(db_path)
conn = sqlite3.connect(db_path)
cur = conn.cursor()
try:

View File

@@ -5,6 +5,7 @@
import logging
import os
import sqlite3
from typing import Optional, Union
from mvt.common.url import URL
@@ -114,7 +115,7 @@ class SafariHistory(IOSExtraction):
def _process_history_db(self, history_path):
self._recover_sqlite_db_if_needed(history_path)
conn = self._open_sqlite_db(history_path)
conn = sqlite3.connect(history_path)
cur = conn.cursor()
cur.execute(
"""

View File

@@ -83,7 +83,7 @@ class Shortcuts(IOSExtraction):
)
self.log.info("Found Shortcuts database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
conn.text_factory = bytes
cur = conn.cursor()
try:

View File

@@ -44,36 +44,29 @@ class SMS(IOSExtraction):
def serialize(self, record: dict) -> Union[dict, list]:
text = record["text"].replace("\n", "\\n")
sms_data = f"{record['service']}: {record['guid']} \"{text}\" from {record['phone_number']} ({record['account']})"
records = [
return [
{
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "sms_received",
"data": sms_data,
},
{
"timestamp": record["isodate_read"],
"module": self.__class__.__name__,
"event": "sms_read",
"data": sms_data,
},
]
# If the message was read, we add an extra event.
if record["isodate_read"]:
records.append(
{
"timestamp": record["isodate_read"],
"module": self.__class__.__name__,
"event": "sms_read",
"data": sms_data,
}
)
return records
def check_indicators(self) -> None:
for message in self.results:
alert_old = "ALERT: State-sponsored attackers may be targeting your iPhone"
alert_new = "ALERT: Apple detected a targeted mercenary spyware attack against your iPhone"
if message.get("text", "").startswith(alert_old) or message.get(
"text", ""
).startswith(alert_new):
self.log.warning(
"Apple warning about state-sponsored attack received on the %s",
message["isodate"],
alert = "ALERT: State-sponsored attackers may be targeting your iPhone"
if message.get("text", "").startswith(alert):
self.alerts.medium(
f"Apple warning about state-sponsored attack received on the {message['isodate']}",
event_time=message["isodate"],
event=message,
)
if not self.indicators:
@@ -94,7 +87,7 @@ class SMS(IOSExtraction):
self.log.info("Found SMS database at path: %s", self.file_path)
try:
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute(
"""
@@ -111,7 +104,7 @@ class SMS(IOSExtraction):
conn.close()
if "database disk image is malformed" in str(exc):
self._recover_sqlite_db_if_needed(self.file_path, forced=True)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute(
"""

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import logging
import sqlite3
from base64 import b64encode
from typing import Optional, Union
@@ -55,12 +56,6 @@ class SMSAttachments(IOSExtraction):
def check_indicators(self) -> None:
for attachment in self.results:
# Check for known malicious filenames.
if self.indicators and self.indicators.check_file_path(
attachment["filename"]
):
self.detected.append(attachment)
if (
attachment["filename"].startswith("/var/tmp/")
and attachment["filename"].endswith("-1")
@@ -77,7 +72,7 @@ class SMSAttachments(IOSExtraction):
self._find_ios_database(backup_ids=SMS_BACKUP_IDS, root_paths=SMS_ROOT_PATHS)
self.log.info("Found SMS database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute(
"""

View File

@@ -95,7 +95,7 @@ class TCC(IOSExtraction):
self.detected.append(result)
def process_db(self, file_path):
conn = self._open_sqlite_db(file_path)
conn = sqlite3.connect(file_path)
cur = conn.cursor()
db_version = "v3"
try:

View File

@@ -73,7 +73,7 @@ class WebkitResourceLoadStatistics(IOSExtraction):
self._recover_sqlite_db_if_needed(db_path)
conn = self._open_sqlite_db(db_path)
conn = sqlite3.connect(db_path)
cur = conn.cursor()
try:

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import logging
import sqlite3
from typing import Optional, Union
from mvt.common.utils import check_for_links, convert_mactime_to_iso
@@ -68,7 +69,7 @@ class Whatsapp(IOSExtraction):
)
self.log.info("Found WhatsApp database at path: %s", self.file_path)
conn = self._open_sqlite_db(self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
# Query all messages and join tables which can contain media attachments

View File

@@ -31,7 +31,7 @@ install_requires =
iOSbackup >=0.9.923
adb-shell >=0.4.3
libusb1 >=3.0.0
cryptography >=42.0.5
cryptography >=38.0.1
pyyaml >=6.0
pyahocorasick >= 2.0.0

View File

@@ -1,45 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from mvt.android.artifacts.dumpstate_artifact import DumpStateArtifact
from ..utils import get_artifact
class TestAndroidArtifactDumpState:
def _parse_dump_state(self):
"""
Load the test artifact
"""
file = get_artifact("android_data/bugreport/dumpstate.txt")
with open(file, "rb") as f:
data = f.read()
dumpstate = DumpStateArtifact()
dumpstate.parse_dumpstate(data)
return dumpstate
def test_extract_dumpstate_sections(self):
"""
Test parsing of dumpstate sections
"""
dumpstate = self._parse_dump_state()
assert len(dumpstate.dumpstate_sections) == 4
assert len(dumpstate.dumpstate_header) == 4
assert dumpstate.dumpstate_header.get(b"Bugreport format version") == b"2.0"
for section in dumpstate.dumpstate_sections:
if section["section_name"] == b"SYSTEM LOG":
assert len(section["lines"]) == 5
assert section["lines"][0].startswith(b"--------- beginning of system")
elif section["section_name"] == b"MODEM CRASH HISTORY":
# Test parsing where section only has an error message
assert len(section["lines"]) == 1
assert (
section["lines"][0]
== b"*** /data/tombstones//modem/mcrash_history: No such file or directory"
)
assert len(dumpstate.unparsed_lines) == 11

View File

@@ -26,18 +26,6 @@ class TestDumpsysAccessibilityArtifact:
== "com.android.settings/com.samsung.android.settings.development.gpuwatch.GPUWatchInterceptor"
)
def test_parsing_v14_aosp_format(self):
da = DumpsysAccessibilityArtifact()
file = get_artifact("android_data/dumpsys_accessibility_v14_or_later.txt")
with open(file) as f:
data = f.read()
assert len(da.results) == 0
da.parse(data)
assert len(da.results) == 1
assert da.results[0]["package_name"] == "com.malware.accessibility"
assert da.results[0]["service"] == "com.malware.service.malwareservice"
def test_ioc_check(self, indicator_file):
da = DumpsysAccessibilityArtifact()
file = get_artifact("android_data/dumpsys_accessibility.txt")

View File

@@ -1,42 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
from mvt.common.indicators import Indicators
from ..utils import get_artifact
class TestDumpsysPackagesArtifact:
def test_parsing(self):
dpa = DumpsysPackagesArtifact()
file = get_artifact("android_data/dumpsys_packages.txt")
with open(file) as f:
data = f.read()
assert len(dpa.results) == 0
dpa.parse(data)
assert len(dpa.results) == 2
assert (
dpa.results[0]["package_name"]
== "com.samsung.android.provider.filterprovider"
)
assert dpa.results[0]["version_name"] == "5.0.07"
def test_ioc_check(self, indicator_file):
dpa = DumpsysPackagesArtifact()
file = get_artifact("android_data/dumpsys_packages.txt")
with open(file) as f:
data = f.read()
dpa.parse(data)
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)
ind.ioc_collections[0]["app_ids"].append("com.sec.android.app.DataCreate")
dpa.indicators = ind
assert len(dpa.detected) == 0
dpa.check_indicators()
assert len(dpa.detected) == 1

View File

@@ -1,87 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import pytest
from pathlib import Path
from mvt.android.modules.androidqf.packages import Packages
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
@pytest.fixture()
def data_path():
return get_android_androidqf()
@pytest.fixture()
def parent_data_path(data_path):
return Path(data_path).absolute().parent.as_posix()
@pytest.fixture()
def file_list(data_path):
return list_files(data_path)
@pytest.fixture()
def module(parent_data_path, file_list):
m = Packages(target_path=parent_data_path, log=logging)
m.from_folder(parent_data_path, file_list)
return m
class TestAndroidqfPackages:
def test_packages_list(self, module):
run_module(module)
# There should just be 7 packages listed, no detections
assert len(module.results) == 7
assert len(module.timeline) == 0
assert len(module.detected) == 0
def test_non_appstore_warnings(self, caplog, module):
run_module(module)
# Not a super test to be searching logs for this but heuristic detections not yet formalised
assert (
'Found a non-system package installed via adb or another method: "com.whatsapp"'
in caplog.text
)
assert (
'Found a package installed via a browser (installer="com.google.android.packageinstaller"): '
'"app.revanced.manager.flutter"' in caplog.text
)
assert (
'Found a package installed via a third party store (installer="org.fdroid.fdroid"): "org.nuclearfog.apollo"'
in caplog.text
)
def test_packages_ioc_package_names(self, module, indicators_factory):
module.indicators = indicators_factory(app_ids=["com.malware.blah"])
run_module(module)
assert len(module.detected) == 1
assert module.detected[0]["name"] == "com.malware.blah"
assert module.detected[0]["matched_indicator"]["value"] == "com.malware.blah"
def test_packages_ioc_sha256(self, module, indicators_factory):
module.indicators = indicators_factory(
files_sha256=[
"31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa"
]
)
run_module(module)
assert len(module.detected) == 1
assert module.detected[0]["name"] == "com.malware.muahaha"
assert (
module.detected[0]["matched_indicator"]["value"]
== "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa"
)

View File

@@ -1,25 +1,3 @@
========================================================
== dumpstate: 2024-04-21 10:00:11
========================================================
Build: TP1A.220624.014
Uptime: up 0 weeks, 0 days, 0 hours, 20 minutes, load average: 20.00, 19.92, 15.46
Bugreport format version: 2.0
Dumpstate info: id=1 pid=21015 dry_run=0 parallel_run=1 args=/system/bin/dumpstate -S bugreport_mode=
------ DUMPSYS CRITICAL (/system/bin/dumpsys) ------
-------------------------------------------------------------------------------
DUMP OF SERVICE CRITICAL SurfaceFlinger:
now = 1202781815070
Build configuration: [sf PRESENT_TIME_OFFSET=0 FORCE_HWC_FOR_RBG_TO_YUV=1 MAX_VIRT_DISPLAY_DIM=0 RUNNING_WITHOUT_SYNC_FRAMEWORK=0 NUM_FRAMEBUFFER_SURFACE_BUFFERS=3]
Display identification data:
Display 0 (HWC display 0): no identification data
Wide-Color information:
Device has wide color built-in display: 0
Device uses color management: 1
Currently running services:
AAS
AODManagerService
@@ -268,16 +246,6 @@ Packages:
com.instagram.direct.share.handler.DirectMultipleExternalMediaShareActivity
com.instagram.share.handleractivity.ClipsShareHandlerActivity
com.instagram.direct.share.handler.DirectMultipleExternalMediaShareActivityInterop
------ 0.557s was the duration of 'DUMPSYS CRITICAL' ------
------ 0.023s was the duration of 'DUMPSYS CRITICAL PROTO' ------
------ SERIALIZE PERFETTO TRACE (perfetto --save-for-bugreport) ------
------ 0.036s was the duration of 'SERIALIZE PERFETTO TRACE' ------
------ End of SERIALIZE PERFETTO TRACE (perfetto --save-for-bugreport) ------
------ MODEM CRASH HISTORY (/data/tombstones//modem/mcrash_history) ------
*** /data/tombstones//modem/mcrash_history: No such file or directory
------ SYSTEM LOG (logcat -v threadtime -v printable -v uid -d *:v) ------
--------- beginning of system
05-28 09:44:19.845 root 578 578 I vold : Vold 3.0 (the awakening) firing up
05-28 09:44:19.845 root 578 578 D vold : Detected support for: exfat ext4 f2fs ntfs vfat
05-28 09:44:19.849 root 578 578 W vold : [libfs_mgr]Warning: unknown flag: resize
------ 0.417s was the duration of 'SYSTEM LOG' ------

View File

@@ -1,46 +0,0 @@
DUMP OF SERVICE accessibility:
Service host process PID: 633
Threads in use: 0/14
Client PIDs: 2445, 2428, 2405, 2235, 2215, 2164, 2148, 2130, 2104, 2062, 1879, 1824, 1736, 1748, 1746, 1617, 1605, 1606, 1582, 1538, 1523, 1474, 1307, 1237, 1118, 1081, 1076, 1023, 997, 978, 882, 187
ACCESSIBILITY MANAGER (dumpsys accessibility)
currentUserId=0
hasWindowMagnificationConnection=false
Magnifier on display#0
MagnificationConfig[mode: 1, activated: false, scale: 1.0, centerX: 360.0, centerY: 640.0]
Magnification region=SkRegion((3,3,717,1277))
IdOfLastServiceToMagnify=-1
SupportWindowMagnification=true
WindowMagnificationConnectionState=DISCONNECTED
User state[
attributes:{id=0, touchExplorationEnabled=false, serviceHandlesDoubleTap=false, requestMultiFingerGestures=false, requestTwoFingerPassthrough=false, sendMotionEventsEnabledfalse, displayMagnificationEnabled=false, autoclickEnabled=false, nonInteractiveUiTimeout=0, interactiveUiTimeout=0, installedServiceCount=2, magnificationModes={0=1}, magnificationCapabilities=3, audioDescriptionByDefaultEnabled=false, magnificationFollowTypingEnabled=true, alwaysOnMagnificationEnabled=true}
shortcut key:{}
button:{}
button target:{null}
Bound services:{Service[label=Accessibility service, feedbackType[FEEDBACK_SPOKEN, FEEDBACK_HAPTIC, FEEDBACK_AUDIBLE, FEEDBACK_VISUAL, FEEDBACK_GENERIC, FEEDBACK_BRAILLE], capabilities=11, eventTypes=TYPES_ALL_MASK, notificationTimeout=1000, requestA11yBtn=false]}
Enabled services:{{com.malware.accessibility/com.malware.service.malwareservice}}
Binding services:{}
Crashed services:{}
Client list info:{
Client list callbacks: 6
Client list killed: false
Client list broadcasts count: -1
Registered clients:{
[com.malware.accessibility][com.android.launcher3][com.android.systemui][com.android.launcher3][com.android.settings.intelligence][com.android.inputmethod.latin]}]
Window attributes:[{1=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 2=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 3=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 4=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 5=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 6=AccessibilityWindowAttributes{mAccessibilityWindowTitle=nullmLocales=[en_US]}, 7=AccessibilityWindowAttributes{mAccessibilityWindowTitle=HomemLocales=[en_US]}, 9=AccessibilityWindowAttributes{mAccessibilityWindowTitle=SettingsmLocales=[en_US]}, 10=AccessibilityWindowAttributes{mAccessibilityWindowTitle=Settings SuggestionsmLocales=[en_US]}}]
A11yInputFilter Info :
Enabled features of Display [0] = [KeyboardInterceptor]
Global client list info:{
Client list callbacks: 4
Client list killed: false
Client list broadcasts count: -1
Registered clients:{
[com.android.permissioncontroller][com.android.dynsystem, com.android.server.telecom, com.android.keychain, com.android.settings, com.android.localtransport, com.android.wallpaperbackup, com.android.inputdevices, com.android.providers.settings, android, com.android.emulator.multidisplay, com.android.location.fused][com.android.emulator.multidisplay, com.android.wallpaperbackup, com.android.settings, com.android.keychain, com.android.dynsystem, com.android.inputdevices, com.android.providers.settings, com.android.localtransport, com.android.server.telecom, android, com.android.location.fused][com.android.systemui]
Proxy manager state:
Number of proxy connections: 0
Registered proxy connections:
Accessibility Display Listener:
SystemUI uid: 10076
1 valid display: 0

View File

@@ -1,233 +0,0 @@
[
{
"name": "com.whatsapp",
"files": [
{
"path": "/data/app/~~/com.whatsapp-~~/base.apk",
"local_name": "",
"md5": "5870bd06e642de410c54705226ecfa9a",
"sha1": "6cb06e9ab5619345f930c2b2096b4dd013a10ec9",
"sha256": "744ed47f8176ec423840344c33e88bd2c96e8988cda0797f3415bb5229efc12b",
"sha512": "f222f742b0bd302c82e202bc78f7ff8b2de4acfc8d606994245ffa80998b003e215cad82cae023abe4f65c0da0a56fa9890e9bb3a753af6dac848a753ac07aee",
"error": "",
"verified_certificate": true,
"certificate": {
"Md5": "556c6019249bbc0cab70495178d3a9d1",
"Sha1": "38a0f7d505fe18fec64fbf343ecaaaf310dbd799",
"Sha256": "3987d043d10aefaf5a8710b3671418fe57e0e19b653c9df82558feb5ffce5d44",
"ValidFrom": "2010-06-25T23:07:16Z",
"ValidTo": "2044-02-15T23:07:16Z",
"Issuer": "C=US, ST=California, L=Santa Clara, O=WhatsApp Inc., OU=Engineering, CN=Brian Acton",
"Subject": "C=US, ST=California, L=Santa Clara, O=WhatsApp Inc., OU=Engineering, CN=Brian Acton",
"SignatureAlgorithm": "DSA-SHA1",
"SerialNumber": 1277507236
},
"certificate_error": "",
"trusted_certificate": true
}
],
"installer": "null",
"uid": 10271,
"disabled": false,
"system": false,
"third_party": true
},
{
"name": "app.revanced.manager.flutter",
"files": [
{
"path": "/data/app/~~==/app.revanced.manager.flutter-==/base.apk",
"local_name": "",
"md5": "aae9b55c6f2592233518bb5a173e8505",
"sha1": "9185a83dae0fc8a0ba79f89f3c84fe8a038f93af",
"sha256": "6ddb76f6180ca8bc0a11d5b343ac9ad8f137a351f20c080e989ca4310973d319",
"sha512": "923a57d4cdf2e7d48539307abbd12f982d61f393a1d058ceef0f6109301d21fedf0fe73c667f8add37fb35da570ac35c6b911360d9bf0389aa0bbbd53103ff46",
"error": "",
"verified_certificate": true,
"certificate": {
"Md5": "f822d70f449d798f0688e2c7358a429c",
"Sha1": "93adc8e2bd1687644f1143e184bcbfd57912ff2c",
"Sha256": "b6362c6ea7888efd15c0800f480786ad0f5b133b4f84e12d46afba5f9eac1223",
"ValidFrom": "2022-09-14T11:45:44Z",
"ValidTo": "2050-01-30T11:45:44Z",
"Issuer": "C=Unknown, ST=Unknown, L=Unknown, O=ReVanced, OU=ReVanced, CN=ReVanced Manager",
"Subject": "C=Unknown, ST=Unknown, L=Unknown, O=ReVanced, OU=ReVanced, CN=ReVanced Manager",
"SignatureAlgorithm": "SHA256-RSA",
"SerialNumber": 710526530
},
"certificate_error": "",
"trusted_certificate": false
}
],
"installer": "com.google.android.packageinstaller",
"uid": 10266,
"disabled": false,
"system": false,
"third_party": true
},
{
"name": "com.google.android.youtube",
"files": [
{
"path": "/data/app/~~==/com.google.android.youtube-==/base.apk",
"local_name": "",
"md5": "3ec11b187ec6195e9ca4b5be671eba34",
"sha1": "33a9a89836690966498ba106283e76eff430365b",
"sha256": "a81b6392ab855905763272cf1a248b0d09fc675a91eabe7ef4ed589356a35241",
"sha512": "c736fbd07fe52539d8e96f6489a49c915c2bac472f0203f6187d167e2e3623f07db9e70b0fbc0494f6eeffb66a4cf71da56ad70503dc8138512faa3c1e847174",
"error": "",
"verified_certificate": true,
"certificate": {
"Md5": "d046fc5d1fc3cd0e57c5444097cd5449",
"Sha1": "24bb24c05e47e0aefa68a58a766179d9b613a600",
"Sha256": "3d7a1223019aa39d9ea0e3436ab7c0896bfb4fb679f4de5fe7c23f326c8f994a",
"ValidFrom": "2008-12-02T02:07:58Z",
"ValidTo": "2036-04-19T02:07:58Z",
"Issuer": "C=US, ST=CA, L=Mountain View, O=Google, Inc, OU=Google, Inc, CN=Unknown",
"Subject": "C=US, ST=CA, L=Mountain View, O=Google, Inc, OU=Google, Inc, CN=Unknown",
"SignatureAlgorithm": "MD5-RSA",
"SerialNumber": 1228183678
},
"certificate_error": "",
"trusted_certificate": true
}
],
"installer": "com.google.android.packageinstaller",
"uid": 10194,
"disabled": false,
"system": true,
"third_party": false
},
{
"name": "org.fdroid.fdroid",
"files": [
{
"path": "/data/app/~~-==/org.fdroid.fdroid-==/base.apk",
"local_name": "",
"md5": "1f7524d15b3d229e5e89af609551e640",
"sha1": "4ce271a8ac2afb9f584f1deb165f1ab4768c50b0",
"sha256": "dc3bb88f6419ee7dde7d1547a41569aa03282fe00e0dc43ce035efd7c9d27d75",
"sha512": "40e9bfaf6c2833078e370c85001adcb7493851a5146d2b4067a9909266a0d7904f80825f040c8c6e0cb59ec6e8c0825d522ff963f6db780b049a24d47f81b289",
"error": "",
"verified_certificate": true,
"certificate": {
"Md5": "17c55c628056e193e95644e989792786",
"Sha1": "05f2e65928088981b317fc9a6dbfe04b0fa13b4e",
"Sha256": "43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab",
"ValidFrom": "2010-07-23T17:10:24Z",
"ValidTo": "2037-12-08T17:10:24Z",
"Issuer": "C=UK, ST=Unknown, L=Wetherby, O=Unknown, OU=Unknown, CN=Ciaran Gultnieks",
"Subject": "C=UK, ST=Unknown, L=Wetherby, O=Unknown, OU=Unknown, CN=Ciaran Gultnieks",
"SignatureAlgorithm": "SHA1-RSA",
"SerialNumber": 1279905024
},
"certificate_error": "",
"trusted_certificate": false
}
],
"installer": "com.google.android.packageinstaller",
"uid": 10267,
"disabled": false,
"system": false,
"third_party": true
},
{
"name": "org.nuclearfog.apollo",
"files": [
{
"path": "/data/app/~~==/org.nuclearfog.apollo-==/base.apk",
"local_name": "",
"md5": "69f611758cc911f472fcabad6151684a",
"sha1": "1f5e450ef1901e245d4828735e0e93f0f94fb4da",
"sha256": "00bdfc80a397b449bef89dd2051ddd3c9d2a64e954176420b40c90a2af956799",
"sha512": "2af8037e0e226cba9f32227f709afc32fd8871c0077f73d00d59353d67ab843cb6641a5e0101d494699aeb91dcd136767fe9d76b30df65e1a1153f3c5b51a837",
"error": "",
"verified_certificate": true,
"certificate": {
"Md5": "2bef3d492d62fad190a8b6b7d71d42a4",
"Sha1": "cad23563b5be0c33611d827ee0da6ad5ef3be39a",
"Sha256": "e1a418c51baa829917daa2e86d7509a8a10470e44280c20146b70ea550bfe1ab",
"ValidFrom": "2022-01-15T20:17:10Z",
"ValidTo": "2047-01-09T20:17:10Z",
"Issuer": "C=DE, ST=Saarland, CN=nuclearfog",
"Subject": "C=DE, ST=Saarland, CN=nuclearfog",
"SignatureAlgorithm": "SHA256-RSA",
"SerialNumber": 75365821
},
"certificate_error": "",
"trusted_certificate": false
}
],
"installer": "org.fdroid.fdroid",
"uid": 10272,
"disabled": false,
"system": false,
"third_party": true
},
{
"name": "com.malware.blah",
"files": [
{
"path": "/data/app/~~-==/com.malware.blah-==/base.apk",
"local_name": "",
"md5": "349ba2de140fccaf2ed2ac20f66e711f",
"sha1": "2cc5b4a70ada9229fb50d30f525392f2d66f58d6",
"sha256": "79a3569fbb63a9167ad8a2dad963616bb01474c87d769c7640f6d6810c448eae",
"sha512": "df1bbbfa6e895054b36093548558ee0d9fbf61ef09e617d3b3b158ba9f9c11825dbbf7e84711331afb80fc24ea0e5aa07a9db1919932c109c34fefec3c02d184",
"error": "",
"verified_certificate": true,
"certificate": {
"Md5": "54d5b5aca1e7e76bb1a26c61a9381b93",
"Sha1": "4ba9d1f82adb7be841bcf53b03ddae857747199a",
"Sha256": "c3e8cafdcd10e7cd9b2ec67f7abd4447b840431126066f6b16ed42151d2b4d64",
"ValidFrom": "2021-01-15T22:03:53Z",
"ValidTo": "2051-01-15T22:03:53Z",
"Issuer": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android",
"Subject": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android",
"SignatureAlgorithm": "SHA256-RSA",
"SerialNumber": 955466096586930338769951715633687128507538251257
},
"certificate_error": "",
"trusted_certificate": false
}
],
"installer": "null",
"uid": 10058,
"disabled": false,
"system": true,
"third_party": false
},
{
"name": "com.malware.muahaha",
"files": [
{
"path": "/data/app/~~-==/com.malware.meh-==/base.apk",
"local_name": "",
"md5": "349ba2de140fccaf2ed2ac20f66e711f",
"sha1": "2cc5b4a70ada9229fb50d30f525392f2d66f58d6",
"sha256": "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa",
"sha512": "df1bbbfa6e895054b36093548558ee0d9fbf61ef09e617d3b3b158ba9f9c11825dbbf7e84711331afb80fc24ea0e5aa07a9db1919932c109c34fefec3c02d184",
"error": "",
"verified_certificate": true,
"certificate": {
"Md5": "54d5b5aca1e7e76bb1a26c61a9381b93",
"Sha1": "4ba9d1f82adb7be841bcf53b03ddae857747199a",
"Sha256": "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa",
"ValidFrom": "2021-01-15T22:03:53Z",
"ValidTo": "2051-01-15T22:03:53Z",
"Issuer": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android",
"Subject": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android",
"SignatureAlgorithm": "SHA256-RSA",
"SerialNumber": 955466096586930338769951715633687128507538251257
},
"certificate_error": "",
"trusted_certificate": false
}
],
"installer": "null",
"uid": 10058,
"disabled": false,
"system": true,
"third_party": false
}
]

View File

@@ -42,14 +42,6 @@ class TestDateConversions:
converted = convert_unix_to_utc_datetime(TEST_DATE_EPOCH)
assert convert_datetime_to_iso(converted) == TEST_DATE_ISO
def test_convert_timezone_aware_to_iso(self):
assert (
convert_datetime_to_iso(
datetime.strptime("2024-09-30 11:21:20+0200", "%Y-%m-%d %H:%M:%S%z")
)
== "2024-09-30 09:21:20.000000"
)
class TestHashes:
def test_hash_from_file(self):
@@ -62,7 +54,7 @@ class TestHashes:
def test_hash_from_folder(self):
path = os.path.join(get_artifact_folder(), "androidqf")
hashes = list(generate_hashes_from_path(path, logging))
assert len(hashes) == 6
assert len(hashes) == 5
# Sort the files to have reliable order for tests.
hashes = sorted(hashes, key=lambda x: x["file_path"])
assert hashes[0]["file_path"] == os.path.join(path, "backup.ab")

View File

@@ -8,8 +8,6 @@ import os
import pytest
from .artifacts.generate_stix import generate_test_stix_file
import logging
from mvt.common.indicators import Indicators
@pytest.fixture(scope="session", autouse=True)
@@ -26,31 +24,3 @@ def clean_test_env(request, tmp_path_factory):
del os.environ["MVT_STIX2"]
except KeyError:
pass
@pytest.fixture()
def indicators_factory(indicator_file):
def f(
domains=[],
emails=[],
file_names=[],
processes=[],
app_ids=[],
android_property_names=[],
files_sha256=[],
):
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)
ind.ioc_collections[0]["domains"].extend(domains)
ind.ioc_collections[0]["emails"].extend(emails)
ind.ioc_collections[0]["file_names"].extend(file_names)
ind.ioc_collections[0]["processes"].extend(processes)
ind.ioc_collections[0]["app_ids"].extend(app_ids)
ind.ioc_collections[0]["android_property_names"].extend(android_property_names)
ind.ioc_collections[0]["files_sha256"].extend(files_sha256)
return ind
return f

View File

@@ -17,7 +17,7 @@ class TestSMSModule:
m = SMS(target_path=get_ios_backup_folder())
run_module(m)
assert len(m.results) == 1
assert len(m.timeline) == 2
assert len(m.timeline) == 2 # SMS received and read events.
assert len(m.detected) == 0
def test_detection(self, indicator_file):

View File

@@ -4,22 +4,31 @@
# https://license.mvt.re/1.1/
import logging
import pytest
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
from mvt.ios.modules.fs.filesystem import Filesystem
from ..utils import get_ios_backup_folder
from ..utils import delete_tmp_db_files, get_ios_backup_folder
@pytest.fixture()
def cleanup_tmp_artifacts():
ios_backup_folder = get_ios_backup_folder()
delete_tmp_db_files(ios_backup_folder)
return
class TestFilesystem:
def test_filesystem(self):
def test_filesystem(self, cleanup_tmp_artifacts):
m = Filesystem(target_path=get_ios_backup_folder())
run_module(m)
assert len(m.results) == 15
assert len(m.timeline) == 15
assert len(m.detected) == 0
def test_detection(self, indicator_file):
def test_detection(self, indicator_file, cleanup_tmp_artifacts):
m = Filesystem(target_path=get_ios_backup_folder())
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)