Compare commits

..

43 Commits

Author SHA1 Message Date
Donncha Ó Cearbhaill
6b2a18d457 Fix YAML format 2025-02-21 15:45:36 +01:00
Donncha Ó Cearbhaill
ca41f7f106 Always open automatic PRs as drafts (#609) 2025-02-21 15:35:06 +01:00
github-actions[bot]
55ddd86ad5 Add new iOS versions and build numbers (#607)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-02-21 15:24:27 +01:00
Donncha Ó Cearbhaill
b184eeedf4 Handle XML encoded ADB keystore and fix parsing bugs (#605) 2025-02-07 02:00:24 +01:00
Donncha Ó Cearbhaill
4e97e85350 Load Android device timezone info and add additional file modification logs (#567)
* Use local timestamp for Files module timeline.

Most other Android timestamps appear to be local time. The
results timeline is more useful if all the timestamps
are consistent. I would prefer to use UTC, but that would
mean converting all the other timestamps to UTC as well. We probably
do not have sufficient information to do that accurately,
especially if the device is moving between timezones..

* Add file timestamp modules to add logs into timeline

* Handle case were we cannot load device timezone

* Fix crash if prop file does not exist

* Move _get_file_modification_time to BugReportModule

* Add backport for timezone and fix Tombstone module to use local time.

* Fix import for backported Zoneinfo

* Fix ruff error
2025-02-06 20:51:15 +01:00
Donncha Ó Cearbhaill
e5865b166e Merge pull request #568 from mvt-project/feature/tombstone-parser
Add parser for Android tombstone files
2025-02-06 20:15:21 +01:00
Donncha Ó Cearbhaill
a2dabb4267 Fix generate-proto-parsers Makefile command 2025-02-06 20:11:54 +01:00
Donncha Ó Cearbhaill
b7595b62eb Add initial tombstone parser
This supports parsing tombstone files from Android bugreports. The parser
can load both the legacy text format and the new binary protobuf format.
2025-02-06 20:07:05 +01:00
Donncha Ó Cearbhaill
02c02ca15c Merge branch 'main' into feature/tombstone-parser 2025-02-03 18:44:00 +01:00
Donncha Ó Cearbhaill
6da33394fe Merge pull request #592 from mvt-project/feature/config-file
Reworking handling of config options
2025-01-30 13:32:53 +01:00
Donncha Ó Cearbhaill
086871e21d Merge branch 'main' into feature/config-file 2025-01-30 13:15:28 +01:00
Donncha Ó Cearbhaill
f32830c649 Merge pull request #603 from mvt-project/feature/add-suspicious-android-setting
Add additional Android security warnings
2025-01-30 13:12:14 +01:00
Donncha Ó Cearbhaill
edcad488ab Merge branch 'main' into feature/add-suspicious-android-setting 2025-01-30 13:10:00 +01:00
Donncha Ó Cearbhaill
43901c96a0 Add improved heuristic detections to AppOps module 2025-01-30 13:02:26 +01:00
Donncha Ó Cearbhaill
0962383b46 Alert on potentially suspicious permissions from ADB 2025-01-30 11:48:19 +01:00
Donncha Ó Cearbhaill
34cd08fd9a Add additional Android security setting to warn on 2025-01-30 11:35:18 +01:00
github-actions[bot]
579b53f7ec Add new iOS versions and build numbers (#602)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-01-28 01:27:17 +01:00
Rory Flynn
dbb80d6320 Mark release 2.6.0 (#601) 2025-01-27 15:41:41 +01:00
Donncha Ó Cearbhaill
0fbf24e82a Merge branch 'main' into feature/config-file 2025-01-14 14:33:40 +01:00
Rory Flynn
a2493baead Documentation tweaks (#599)
* Adds link in install instructions to the command completion docs added in #597
* Small visual tweaks
2025-01-14 13:12:10 +01:00
Nim
0dc6228a59 Add command completion docs (#410) (#597)
Co-authored-by: Rory Flynn <75283103+roaree@users.noreply.github.com>
2025-01-14 12:04:07 +01:00
Rory Flynn
6e230bdb6a Autofix for ruff (#598) 2025-01-14 12:02:10 +01:00
Tek
2aa76c8a1c Fixes a bug on recent phones not having WIFI column in net usage (#580)
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
Co-authored-by: Rory Flynn <75283103+roaree@users.noreply.github.com>
2025-01-07 12:48:35 +01:00
github-actions[bot]
7d6dc9e6dc Add new iOS versions and build numbers (#595)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-01-07 12:07:57 +01:00
Donncha Ó Cearbhaill
458195a0ab Fix optional typing syntax for Python 3.8 2024-12-25 00:28:02 +00:00
Donncha Ó Cearbhaill
52e854b8b7 Add missing import 2024-12-25 00:23:36 +00:00
Donncha Ó Cearbhaill
0f1eec3971 Add Pydantic dependencies 2024-12-25 00:21:42 +00:00
Donncha Ó Cearbhaill
f4425865c0 Add missed modules using updated settings module 2024-12-25 00:14:14 +00:00
Donncha Ó Cearbhaill
28c0c86c4e Update MVT code to use config file rather than raw env variables 2024-12-25 00:09:29 +00:00
Donncha Ó Cearbhaill
154e6dab15 Add config file parser for MVT 2024-12-24 23:30:18 +00:00
Donncha Ó Cearbhaill
0c73e3e8fa Merge pull request #587 from mvt-project/feature/uninstalled-apps
Add a module to parse uninstalled apps from dumpsys data
2024-12-16 00:03:23 +01:00
Donncha Ó Cearbhaill
9b5f2d89d5 Merge branch 'main' into feature/uninstalled-apps 2024-12-16 00:00:12 +01:00
Donncha Ó Cearbhaill
3da61c8da8 Fix ruff checks 2024-12-15 23:22:36 +01:00
Tek
5b2fe3baec Reorganize code in iOS app module (#586) 2024-12-14 10:04:47 +01:00
Donncha Ó Cearbhaill
a3a7789547 Merge pull request #584 from mvt-project/enhance-community-guidelines
Update MVT contributor guidelines
2024-12-13 23:01:58 +01:00
Donncha Ó Cearbhaill
d3fcc686ff Update contribution guidelines 2024-12-13 22:45:41 +01:00
github-actions[bot]
4bcc0e5f27 Add new iOS versions and build numbers (#583)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-12-12 14:43:59 +01:00
tes
9d81b5bfa8 Add a module to parse uninstalled apps from dumpsys data, for both bugreport and AndroidQF output, and match them against package name IoCs. 2024-12-11 16:47:19 -03:00
github-actions[bot]
22fce280af Add new iOS versions and build numbers (#572)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-11-20 11:02:09 +01:00
Donncha Ó Cearbhaill
4739d8853e Merge pull request #570 from mvt-project/fix/files-detection-bug
Fix error to due extra equal character in Files detection
2024-10-31 20:04:33 +01:00
Donncha Ó Cearbhaill
ace01ff7fb Merge branch 'main' into fix/files-detection-bug 2024-10-31 19:59:53 +01:00
Donncha Ó Cearbhaill
7e4f0aec4d Fix error to due extra equal character in Files detection 2024-10-31 19:59:29 +01:00
github-actions[bot]
57647583cc Add new iOS versions and build numbers (#569)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-10-29 04:17:03 +01:00
59 changed files with 1146 additions and 211 deletions

View File

@@ -21,6 +21,7 @@ jobs:
title: '[auto] Update iOS releases and versions'
commit-message: Add new iOS versions and build numbers
branch: auto/add-new-ios-releases
draft: true
body: |
This is an automated pull request to update the iOS releases and version numbers.
add-paths: |

View File

@@ -1,19 +1,65 @@
# Contributing
# Contributing to Mobile Verification Toolkit (MVT)
Thank you for your interest in contributing to Mobile Verification Toolkit (MVT)! Your help is very much appreciated.
We greatly appreciate contributions to MVT!
Your involvement, whether through identifying issues, improving functionality, or enhancing documentation, is very much appreciated. To ensure smooth collaboration and a welcoming environment, we've outlined some key guidelines for contributing below.
## Getting started
Contributing to an open-source project like MVT might seem overwhelming at first, but we're here to support you!
Whether you're a technologist, a frontline human rights defender, a field researcher, or someone new to consensual spyware forensics, there are many ways to make meaningful contributions.
Here's how you can get started:
1. **Explore the codebase:**
- Browse the repository to get familar with MVT. Many MVT modules are simple in functionality and easy to understand.
- Look for `TODO:` or `FIXME:` comments in the code for areas that need attention.
2. **Check Github issues:**
- Look for issues tagged with ["help wanted"](https://github.com/mvt-project/mvt/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or ["good first issue"](https://github.com/mvt-project/mvt/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to find tasks that are beginner-friendly or where input from the community would be helpful.
3. **Ask for guidance:**
- If you're unsure where to start, feel free to open a [discussion](https://github.com/mvt-project/mvt/discussions) or comment on an issue.
## How to contribute:
1. **Report issues:**
- Found a bug? Please check existing issues to see if it's already reported. If not, open a new issue. Mobile operating systems and databases are constantly evolving, an new errors may appear spontaniously in new app versions.
**Please provide as much information as possible about the prodblem including: any error messages, steps to reproduce the problem, and any logs or screenshots that can help.**
## Where to start
2. **Suggest features:**
- If you have an idea for new functionality, create a feature request issue and describe your proposal.
Starting to contribute to a somewhat complex project like MVT might seem intimidating. Unless you have specific ideas of new functionality you would like to submit, some good starting points are searching for `TODO:` and `FIXME:` comments throughout the code. Alternatively you can check if any GitHub issues existed marked with the ["help wanted"](https://github.com/mvt-project/mvt/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) tag.
3. **Submit code:**
- Fork the repository and create a new branch for your changes.
- Ensure your changes align with the code style guidelines (see below).
- Open a pull request (PR) with a clear description of your changes and link it to any relevant issues.
4. **Documentation contributions:**
- Improving documentation is just as valuable as contributing code! If you notice gaps or inaccuracies in the documentation, feel free to submit changes or suggest updates.
## Code style
Please follow these code style guidelines for consistency and readability:
When contributing code to
- **Indentation**: use 4 spaces per tab.
- **Quotes**: Use double quotes (`"`) by default. Use single quotes (`'`) for nested strings instead of escaping (`\"`), or when using f-formatting.
- **Maximum line length**:
- Aim for lines no longer than 80 characters.
- Exceptions are allowed for long log lines or strings, which may extend up to 100 characters.
- Wrap lines that exceed 100 characters.
- **Indentation**: we use 4-spaces tabs.
Follow [PEP 8 guidelines](https://peps.python.org/pep-0008/) for indentation and overall Python code style. All MVT code is automatically linted with [Ruff](https://github.com/astral-sh/ruff) before merging.
- **Quotes**: we use double quotes (`"`) as a default. Single quotes (`'`) can be favored with nested strings instead of escaping (`\"`), or when using f-formatting.
Please check your code before opening a pull request by running `make ruff`
- **Maximum line length**: we strongly encourage to respect a 80 characters long lines and to follow [PEP8 indentation guidelines](https://peps.python.org/pep-0008/#indentation) when having to wrap. However, if breaking at 80 is not possible or is detrimental to the readability of the code, exceptions are tolerated. For example, long log lines, or long strings can be extended to 100 characters long. Please hard wrap anything beyond 100 characters.
## Community and support
We aim to create a supportive and collaborative environment for all contributors. If you run into any challenges, feel free to reach out through the discussions or issues section of the repository.
Your contributions, big or small, help improve MVT and are always appreciated.

View File

@@ -27,9 +27,8 @@ test-requirements:
generate-proto-parsers:
# Generate python parsers for protobuf files
PROTO_DIR="src/mvt/android/parsers/proto/"; \
PROTO_FILES=$$(find $(PROTO_DIR) -iname "*.proto"); \
protoc -I$(PROTO_DIR) --python_betterproto_out=$(PROTO_DIR) $$PROTO_FILES
PROTO_FILES=$$(find src/mvt/android/parsers/proto/ -iname "*.proto"); \
protoc -Isrc/mvt/android/parsers/proto/ --python_betterproto_out=src/mvt/android/parsers/proto/ $$PROTO_FILES
clean:
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/src/mvt.egg-info

View File

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

View File

@@ -98,3 +98,7 @@ You now should have the `mvt-ios` and `mvt-android` utilities installed.
**Notes:**
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.
## Setting up command completions
See ["Command completions"](command_completion.md)

View File

@@ -33,6 +33,9 @@ dependencies = [
"pyyaml >=6.0",
"pyahocorasick >= 2.0.0",
"betterproto >=1.2.0",
"pydantic >= 2.10.0",
"pydantic-settings >= 2.7.0",
'backports.zoneinfo; python_version < "3.9"',
]
requires-python = ">= 3.8"

View File

@@ -10,7 +10,7 @@ from .artifact import AndroidArtifact
class DumpsysADBArtifact(AndroidArtifact):
multiline_fields = ["user_keys"]
multiline_fields = ["user_keys", "keystore"]
def indented_dump_parser(self, dump_data):
"""
@@ -67,9 +67,28 @@ class DumpsysADBArtifact(AndroidArtifact):
return res
def parse_xml(self, xml_data):
"""
Parse XML data from dumpsys ADB output
"""
import xml.etree.ElementTree as ET
keystore = []
keystore_root = ET.fromstring(xml_data)
for adb_key in keystore_root.findall("adbKey"):
key_info = self.calculate_key_info(adb_key.get("key").encode("utf-8"))
key_info["last_connected"] = adb_key.get("lastConnection")
keystore.append(key_info)
return keystore
@staticmethod
def calculate_key_info(user_key: bytes) -> str:
key_base64, user = user_key.split(b" ", 1)
if b" " in user_key:
key_base64, user = user_key.split(b" ", 1)
else:
key_base64, user = user_key, b""
key_raw = base64.b64decode(key_base64)
key_fingerprint = hashlib.md5(key_raw).hexdigest().upper()
key_fingerprint_colon = ":".join(
@@ -115,8 +134,24 @@ class DumpsysADBArtifact(AndroidArtifact):
if parsed.get("debugging_manager") is None:
self.log.error("Unable to find expected ADB entries in dumpsys output") # noqa
return
# Keystore can be in different levels, as the basic parser
# is not always consistent due to different dumpsys formats.
if parsed.get("keystore"):
keystore_data = b"\n".join(parsed["keystore"])
elif parsed["debugging_manager"].get("keystore"):
keystore_data = b"\n".join(parsed["debugging_manager"]["keystore"])
else:
parsed = parsed["debugging_manager"]
keystore_data = None
# Keystore is in XML format on some devices and we need to parse it
if keystore_data and keystore_data.startswith(b"<?xml"):
parsed["debugging_manager"]["keystore"] = self.parse_xml(keystore_data)
else:
# Keystore is not XML format
parsed["debugging_manager"]["keystore"] = keystore_data
parsed = parsed["debugging_manager"]
# Calculate key fingerprints for better readability
key_info = []

View File

@@ -11,6 +11,10 @@ from mvt.common.utils import convert_datetime_to_iso
from .artifact import AndroidArtifact
RISKY_PERMISSIONS = ["REQUEST_INSTALL_PACKAGES"]
RISKY_PACKAGES = ["com.android.shell"]
class DumpsysAppopsArtifact(AndroidArtifact):
"""
Parser for dumpsys app ops info
@@ -45,15 +49,39 @@ class DumpsysAppopsArtifact(AndroidArtifact):
self.detected.append(result)
continue
detected_permissions = []
for perm in result["permissions"]:
if (
perm["name"] == "REQUEST_INSTALL_PACKAGES"
and perm["access"] == "allow"
perm["name"] in RISKY_PERMISSIONS
# and perm["access"] == "allow"
):
self.log.info(
"Package %s with REQUEST_INSTALL_PACKAGES " "permission",
result["package_name"],
)
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"],
entry["timestamp"],
)
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"],
entry["timestamp"],
)
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]] = []
@@ -121,11 +149,16 @@ class DumpsysAppopsArtifact(AndroidArtifact):
if line.startswith(" "):
# Permission entry like:
# Reject: [fg-s]2021-05-19 22:02:52.054 (-314d1h25m2s33ms)
access_type = line.split(":")[0].strip()
if access_type not in ["Access", "Reject"]:
# Skipping invalid access type. Some entries are not in the format we expect
continue
if entry:
perm["entries"].append(entry)
entry = {}
entry["access"] = line.split(":")[0].strip()
entry["access"] = access_type
entry["type"] = line[line.find("[") + 1 : line.find("]")]
try:

View File

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

View File

@@ -0,0 +1,42 @@
# 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 .artifact import AndroidArtifact
class DumpsysPlatformCompatArtifact(AndroidArtifact):
"""
Parser for uninstalled apps listed in platform_compat section.
"""
def check_indicators(self) -> None:
if not self.indicators:
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)
continue
def parse(self, data: str) -> None:
for line in data.splitlines():
if not line.startswith("ChangeId(168419799; name=DOWNSCALED;"):
continue
if line.strip() == "":
break
# Look for rawOverrides field
if "rawOverrides={" in line:
# Extract the content inside the braces for rawOverrides
overrides_field = line.split("rawOverrides={", 1)[1].split("};", 1)[0]
for entry in overrides_field.split(", "):
# Extract app name
uninstall_app = entry.split("=")[0].strip()
self.results.append({"package_name": uninstall_app})

View File

@@ -16,6 +16,11 @@ ANDROID_DANGEROUS_SETTINGS = [
"key": "package_verifier_enable",
"safe_value": "1",
},
{
"description": "disabled APK package verification",
"key": "package_verifier_state",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "package_verifier_user_consent",

View File

@@ -3,11 +3,270 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import datetime
from typing import List, Optional, Union
import pydantic
import betterproto
from mvt.common.utils import convert_datetime_to_iso
from mvt.android.parsers.proto.tombstone import Tombstone
from .artifact import AndroidArtifact
TOMBSTONE_DELIMITER = "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***"
# Map the legacy crash file keys to the new format.
TOMBSTONE_TEXT_KEY_MAPPINGS = {
"Build fingerprint": "build_fingerprint",
"Revision": "revision",
"ABI": "arch",
"Timestamp": "timestamp",
"Process uptime": "process_uptime",
"Cmdline": "command_line",
"pid": "pid",
"tid": "tid",
"name": "process_name",
"binary_path": "binary_path",
"uid": "uid",
"signal": "signal_info",
"code": "code",
"Cause": "cause",
}
class SignalInfo(pydantic.BaseModel):
code: int
code_name: str
name: str
number: Optional[int] = None
class TombstoneCrashResult(pydantic.BaseModel):
"""
MVT Result model for a tombstone crash result.
Needed for validation and serialization, and consistency between text and protobuf tombstones.
"""
file_name: str
file_timestamp: str # We store the timestamp as a string to avoid timezone issues
build_fingerprint: str
revision: int
arch: Optional[str] = None
timestamp: str # We store the timestamp as a string to avoid timezone issues
process_uptime: Optional[int] = None
command_line: Optional[List[str]] = None
pid: int
tid: int
process_name: Optional[str] = None
binary_path: Optional[str] = None
selinux_label: Optional[str] = None
uid: Optional[int] = None
signal_info: SignalInfo
cause: Optional[str] = None
extra: Optional[str] = None
class TombstoneCrashArtifact(AndroidArtifact):
def parse(self, content: bytes) -> None:
""" "
Parser for Android tombstone crash files.
This parser can parse both text and protobuf tombstone crash files.
"""
def serialize(self, record: dict) -> Union[dict, list]:
return {
"timestamp": record["timestamp"],
"module": self.__class__.__name__,
"event": "Tombstone",
"data": (
f"Crash in '{record['process_name']}' process running as UID '{record['uid']}' in file '{record['file_name']}' "
f"Crash type '{record['signal_info']['name']}' with code '{record['signal_info']['code_name']}'"
),
}
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_process(result["process_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
if result.get("command_line", []):
command_name = result.get("command_line")[0].split("/")[-1]
ioc = self.indicators.check_process(command_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
SUSPICIOUS_UIDS = [
0, # root
1000, # system
2000, # shell
]
if result["uid"] in SUSPICIOUS_UIDS:
self.log.warning(
f"Potentially suspicious crash in process '{result['process_name']}' "
f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}"
)
self.detected.append(result)
def parse_protobuf(
self, file_name: str, file_timestamp: datetime.datetime, data: bytes
) -> None:
"""
Parse Android tombstone crash files."""
Parse Android tombstone crash files from a protobuf object.
"""
tombstone_pb = Tombstone().parse(data)
tombstone_dict = tombstone_pb.to_dict(betterproto.Casing.SNAKE)
# Add some extra metadata
tombstone_dict["timestamp"] = self._parse_timestamp_string(
tombstone_pb.timestamp
)
tombstone_dict["file_name"] = file_name
tombstone_dict["file_timestamp"] = convert_datetime_to_iso(file_timestamp)
tombstone_dict["process_name"] = self._proccess_name_from_thread(tombstone_dict)
# Confirm the tombstone is valid, and matches the output model
tombstone = TombstoneCrashResult.model_validate(tombstone_dict)
self.results.append(tombstone.model_dump())
def parse(
self, file_name: str, file_timestamp: datetime.datetime, content: bytes
) -> None:
"""
Parse text Android tombstone crash files.
"""
# Split the tombstone file into a dictonary
tombstone_dict = {
"file_name": file_name,
"file_timestamp": convert_datetime_to_iso(file_timestamp),
}
lines = content.decode("utf-8").splitlines()
for line in lines:
if not line.strip() or TOMBSTONE_DELIMITER in line:
continue
for key, destination_key in TOMBSTONE_TEXT_KEY_MAPPINGS.items():
self._parse_tombstone_line(line, key, destination_key, tombstone_dict)
# Validate the tombstone and add it to the results
tombstone = TombstoneCrashResult.model_validate(tombstone_dict)
self.results.append(tombstone.model_dump())
def _parse_tombstone_line(
self, line: str, key: str, destination_key: str, tombstone: dict
) -> bool:
if not line.startswith(f"{key}"):
return None
if key == "pid":
return self._load_pid_line(line, tombstone)
elif key == "signal":
return self._load_signal_line(line, tombstone)
elif key == "Timestamp":
return self._load_timestamp_line(line, tombstone)
else:
return self._load_key_value_line(line, key, destination_key, tombstone)
def _load_key_value_line(
self, line: str, key: str, destination_key: str, tombstone: dict
) -> bool:
line_key, value = line.split(":", 1)
if line_key != key:
raise ValueError(f"Expected key {key}, got {line_key}")
value_clean = value.strip().strip("'")
if destination_key in ["uid", "revision"]:
tombstone[destination_key] = int(value_clean)
elif destination_key == "process_uptime":
# eg. "Process uptime: 40s"
tombstone[destination_key] = int(value_clean.rstrip("s"))
elif destination_key == "command_line":
# XXX: Check if command line should be a single string in a list, or a list of strings.
tombstone[destination_key] = [value_clean]
else:
tombstone[destination_key] = value_clean
return True
def _load_pid_line(self, line: str, tombstone: dict) -> bool:
pid_part, tid_part, name_part = [part.strip() for part in line.split(",")]
pid_key, pid_value = pid_part.split(":", 1)
if pid_key != "pid":
raise ValueError(f"Expected key pid, got {pid_key}")
pid_value = int(pid_value.strip())
tid_key, tid_value = tid_part.split(":", 1)
if tid_key != "tid":
raise ValueError(f"Expected key tid, got {tid_key}")
tid_value = int(tid_value.strip())
name_key, name_value = name_part.split(":", 1)
if name_key != "name":
raise ValueError(f"Expected key name, got {name_key}")
name_value = name_value.strip()
process_name, binary_path = self._parse_process_name(name_value, tombstone)
tombstone["pid"] = pid_value
tombstone["tid"] = tid_value
tombstone["process_name"] = process_name
tombstone["binary_path"] = binary_path
return True
def _parse_process_name(self, process_name_part, tombstone: dict) -> bool:
process_name, process_path = process_name_part.split(">>>")
process_name = process_name.strip()
binary_path = process_path.strip().split(" ")[0]
return process_name, binary_path
def _load_signal_line(self, line: str, tombstone: dict) -> bool:
signal, code, _ = [part.strip() for part in line.split(",", 2)]
signal = signal.split("signal ")[1]
signal_code, signal_name = signal.split(" ")
signal_name = signal_name.strip("()")
code_part = code.split("code ")[1]
code_number, code_name = code_part.split(" ")
code_name = code_name.strip("()")
tombstone["signal_info"] = {
"code": int(code_number),
"code_name": code_name,
"name": signal_name,
"number": int(signal_code),
}
return True
def _load_timestamp_line(self, line: str, tombstone: dict) -> bool:
timestamp = line.split(":", 1)[1].strip()
tombstone["timestamp"] = self._parse_timestamp_string(timestamp)
return True
@staticmethod
def _parse_timestamp_string(timestamp: str) -> str:
timestamp_date, timezone = timestamp.split("+")
# Truncate microseconds before parsing
timestamp_without_micro = timestamp_date.split(".")[0] + "+" + timezone
timestamp_parsed = datetime.datetime.strptime(
timestamp_without_micro, "%Y-%m-%d %H:%M:%S%z"
)
# HACK: Swap the local timestamp to UTC, so keep the original time and avoid timezone conversion.
local_timestamp = timestamp_parsed.replace(tzinfo=datetime.timezone.utc)
return convert_datetime_to_iso(local_timestamp)
@staticmethod
def _proccess_name_from_thread(tombstone_dict: dict) -> str:
if tombstone_dict.get("threads"):
for thread in tombstone_dict["threads"].values():
if thread.get("id") == tombstone_dict["tid"] and thread.get("name"):
return thread["name"]
return "Unknown"

View File

@@ -12,8 +12,6 @@ from typing import List, Optional
from mvt.common.command import Command
from .modules.androidqf import ANDROIDQF_MODULES
from .modules.bugreport import BUGREPORT_MODULES
from .modules.bugreport.base import BugReportModule
log = logging.getLogger(__name__)
@@ -41,11 +39,7 @@ class CmdAndroidCheckAndroidQF(Command):
)
self.name = "check-androidqf"
# We can load AndroidQF and bugreport modules here, as
# AndroidQF dump will contain a bugreport.
self.modules = ANDROIDQF_MODULES + BUGREPORT_MODULES
# TODO: Check how to namespace and deduplicate modules.
self.modules = ANDROIDQF_MODULES
self.format: Optional[str] = None
self.archive: Optional[zipfile.ZipFile] = None
@@ -60,44 +54,12 @@ class CmdAndroidCheckAndroidQF(Command):
for fname in subfiles:
file_path = os.path.relpath(os.path.join(root, fname), parent_path)
self.files.append(file_path)
elif os.path.isfile(self.target_path):
self.format = "zip"
self.archive = zipfile.ZipFile(self.target_path)
self.files = self.archive.namelist()
def load_bugreport(self):
# Refactor this file list loading
# First we need to find the bugreport file location
bugreport_zip_path = None
for file_name in self.files:
if file_name.endswith("bugreport.zip"):
bugreport_zip_path = file_name
break
else:
self.log.warning("No bugreport.zip found in the AndroidQF dump")
return None
if self.format == "zip":
# Create handle to the bugreport.zip file inside the AndroidQF dump
handle = self.archive.open(bugreport_zip_path)
bugreport_zip = zipfile.ZipFile(handle)
else:
# Load the bugreport.zip file from the extracted AndroidQF dump on disk.
parent_path = Path(self.target_path).absolute().parent.as_posix()
bug_report_path = os.path.join(parent_path, bugreport_zip_path)
bugreport_zip = zipfile.ZipFile(bug_report_path)
return bugreport_zip
def module_init(self, module):
if isinstance(module, BugReportModule):
bugreport_archive = self.load_bugreport()
if not bugreport_archive:
return
module.from_zip(bugreport_archive, bugreport_archive.namelist())
return
if self.format == "zip":
module.from_zip_file(self.archive, self.files)
else:

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ from .dumpsys_receivers import DumpsysReceivers
from .dumpsys_adb import DumpsysADBState
from .getprop import Getprop
from .packages import Packages
from .dumpsys_platform_compat import DumpsysPlatformCompat
from .processes import Processes
from .settings import Settings
from .sms import SMS
@@ -29,6 +30,7 @@ ANDROIDQF_MODULES = [
DumpsysBatteryHistory,
DumpsysADBState,
Packages,
DumpsysPlatformCompat,
Processes,
Getprop,
Settings,

View File

@@ -56,11 +56,17 @@ class AndroidQFModule(MVTModule):
Android log files to UTC/timezone-aware timestamps.
"""
get_prop_files = self._get_files_by_pattern("*/getprop.txt")
prop_data = self._get_file_content(get_prop_files[0]).decode("utf-8")
if not get_prop_files:
self.log.warning(
"Could not find getprop.txt file. "
"Some timestamps and timeline data may be incorrect."
)
return None
from mvt.android.artifacts.getprop import GetProp
properties_artifact = GetProp()
prop_data = self._get_file_content(get_prop_files[0]).decode("utf-8")
properties_artifact.parse(prop_data)
timezone = properties_artifact.get_device_timezone()
if timezone:

View File

@@ -0,0 +1,44 @@
# 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
from mvt.android.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatArtifact
from .base import AndroidQFModule
class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, AndroidQFModule):
"""This module extracts details on uninstalled apps."""
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 run(self) -> None:
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
if not dumpsys_file:
return
data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace")
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE platform_compat:")
self.parse(content)
self.log.info("Found %d uninstalled apps", len(self.results))

View File

@@ -6,7 +6,11 @@
import datetime
import json
import logging
from zoneinfo import ZoneInfo
try:
import zoneinfo
except ImportError:
from backports import zoneinfo
from typing import Optional, Union
from mvt.android.modules.androidqf.base import AndroidQFModule
@@ -75,7 +79,7 @@ class Files(AndroidQFModule):
for result in self.results:
ioc = self.indicators.check_file_path(result["path"])
if ioc:
result["matched_indicator"] == ioc
result["matched_indicator"] = ioc
self.detected.append(result)
continue
@@ -108,10 +112,10 @@ class Files(AndroidQFModule):
def run(self) -> None:
if timezone := self._get_device_timezone():
device_timezone = ZoneInfo(timezone)
device_timezone = zoneinfo.ZoneInfo(timezone)
else:
self.log.warning("Unable to determine device timezone, using UTC")
device_timezone = ZoneInfo("UTC")
device_timezone = zoneinfo.ZoneInfo("UTC")
for file in self._get_files_by_pattern("*/files.json"):
rawdata = self._get_file_content(file).decode("utf-8", errors="ignore")

View File

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

View File

@@ -3,10 +3,11 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
from rich.prompt import Prompt
from mvt.common.config import settings
MVT_ANDROID_BACKUP_PASSWORD = "MVT_ANDROID_BACKUP_PASSWORD"
@@ -16,24 +17,24 @@ def cli_load_android_backup_password(log, backup_password):
Used in MVT CLI command parsers.
"""
password_from_env = os.environ.get(MVT_ANDROID_BACKUP_PASSWORD, None)
password_from_env_or_config = settings.ANDROID_BACKUP_PASSWORD
if backup_password:
log.info(
"Your password may be visible in the process table because it "
"was supplied on the command line!"
)
if password_from_env:
if password_from_env_or_config:
log.info(
"Ignoring %s environment variable, using --backup-password argument instead",
MVT_ANDROID_BACKUP_PASSWORD,
"MVT_ANDROID_BACKUP_PASSWORD",
)
return backup_password
elif password_from_env:
elif password_from_env_or_config:
log.info(
"Using backup password from %s environment variable",
MVT_ANDROID_BACKUP_PASSWORD,
"Using backup password from %s environment variable or config file",
"MVT_ANDROID_BACKUP_PASSWORD",
)
return password_from_env
return password_from_env_or_config
def prompt_or_load_android_backup_password(log, module_options):

View File

@@ -11,9 +11,11 @@ from .battery_history import BatteryHistory
from .dbinfo import DBInfo
from .getprop import Getprop
from .packages import Packages
from .platform_compat import PlatformCompat
from .receivers import Receivers
from .adb_state import DumpsysADBState
from .fs_timestamps import BugReportTimestamps
from .tombstones import Tombstones
BUGREPORT_MODULES = [
Accessibility,
@@ -24,7 +26,9 @@ BUGREPORT_MODULES = [
DBInfo,
Getprop,
Packages,
PlatformCompat,
Receivers,
DumpsysADBState,
BugReportTimestamps,
Tombstones,
]

View File

@@ -2,7 +2,7 @@
# Copyright (c) 2021-2023 The MVT Authors.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
import datetime
import fnmatch
import logging
import os
@@ -92,3 +92,11 @@ class BugReportModule(MVTModule):
return None
return self._get_file_content(dumpstate_logs[0])
def _get_file_modification_time(self, file_path: str) -> dict:
if self.zip_archive:
file_timetuple = self.zip_archive.getinfo(file_path).date_time
return datetime.datetime(*file_timetuple)
else:
file_stat = os.stat(os.path.join(self.extract_path, file_path))
return datetime.datetime.fromtimestamp(file_stat.st_mtime)

View File

@@ -3,9 +3,7 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import logging
import datetime
from typing import Optional
from mvt.common.utils import convert_datetime_to_iso
@@ -36,14 +34,6 @@ class BugReportTimestamps(FileTimestampsArtifact, BugReportModule):
results=results,
)
def _get_file_modification_time(self, file_path: str) -> dict:
if self.zip_archive:
file_timetuple = self.zip_archive.getinfo(file_path).date_time
return datetime.datetime(*file_timetuple)
else:
file_stat = os.stat(os.path.join(self.extract_path, file_path))
return datetime.datetime.fromtimestamp(file_stat.st_mtime)
def run(self) -> None:
filesystem_files = self._get_files_by_pattern("FS/*")

View File

@@ -0,0 +1,48 @@
# 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
from mvt.android.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatArtifact
from mvt.android.modules.bugreport.base import BugReportModule
class PlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule):
"""This module extracts details on uninstalled apps."""
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 run(self) -> None:
data = self._get_dumpstate_file()
if not data:
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 platform_compat:")
self.parse(content)
self.log.info("Found %d uninstalled apps", len(self.results))

View File

@@ -42,18 +42,23 @@ class Tombstones(TombstoneCrashArtifact, BugReportModule):
)
return
for tombstone_file in tombstone_files:
if tombstone_file.endswith("*.pb"):
self.log.info("Skipping protobuf tombstone file: %s", tombstone_file)
continue
print(tombstone_file)
for tombstone_file in sorted(tombstone_files):
tombstone_filename = tombstone_file.split("/")[-1]
modification_time = self._get_file_modification_time(tombstone_file)
tombstone_data = self._get_file_content(tombstone_file)
tombstone = self.parse_tombstone(tombstone_data)
print(tombstone)
break
# self.log.info(
# "Extracted a total of %d database connection pool records",
# len(self.results),
# )
try:
if tombstone_file.endswith(".pb"):
self.parse_protobuf(
tombstone_filename, modification_time, tombstone_data
)
else:
self.parse(tombstone_filename, modification_time, tombstone_data)
except ValueError as e:
# Catch any exceptions raised during parsing or validation.
self.log.error(f"Error parsing tombstone file {tombstone_file}: {e}")
self.log.info(
"Extracted a total of %d tombstone files",
len(self.results),
)

View File

@@ -17,6 +17,7 @@ from mvt.common.utils import (
generate_hashes_from_path,
get_sha256_from_file_path,
)
from mvt.common.config import settings
from mvt.common.version import MVT_VERSION
@@ -81,7 +82,7 @@ class Command:
os.path.join(self.results_path, "command.log")
)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - " "%(levelname)s - %(message)s"
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
@@ -100,15 +101,25 @@ class Command:
if not self.results_path:
return
# We use local timestamps in the timeline on Android as many
# logs do not contain timezone information.
if type(self).__name__.startswith("CmdAndroid"):
is_utc = False
else:
is_utc = True
if len(self.timeline) > 0:
save_timeline(
self.timeline, os.path.join(self.results_path, "timeline.csv")
self.timeline,
os.path.join(self.results_path, "timeline.csv"),
is_utc=is_utc,
)
if len(self.timeline_detected) > 0:
save_timeline(
self.timeline_detected,
os.path.join(self.results_path, "timeline_detected.csv"),
is_utc=is_utc,
)
def _store_info(self) -> None:
@@ -132,7 +143,7 @@ class Command:
if ioc_file_path and ioc_file_path not in info["ioc_files"]:
info["ioc_files"].append(ioc_file_path)
if self.target_path and (os.environ.get("MVT_HASH_FILES") or self.hashes):
if self.target_path and (settings.HASH_FILES or self.hashes):
self.generate_hashes()
info["hashes"] = self.hash_values
@@ -141,7 +152,7 @@ class Command:
with open(info_path, "w+", encoding="utf-8") as handle:
json.dump(info, handle, indent=4)
if self.target_path and (os.environ.get("MVT_HASH_FILES") or self.hashes):
if self.target_path and (settings.HASH_FILES or self.hashes):
info_hash = get_sha256_from_file_path(info_path)
self.log.info('Reference hash of the info.json file: "%s"', info_hash)

105
src/mvt/common/config.py Normal file
View File

@@ -0,0 +1,105 @@
import os
import yaml
import json
from typing import Tuple, Type, Optional
from appdirs import user_config_dir
from pydantic import AnyHttpUrl, Field
from pydantic_settings import (
BaseSettings,
InitSettingsSource,
PydanticBaseSettingsSource,
SettingsConfigDict,
YamlConfigSettingsSource,
)
MVT_CONFIG_FOLDER = user_config_dir("mvt")
MVT_CONFIG_PATH = os.path.join(MVT_CONFIG_FOLDER, "config.yaml")
class MVTSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="MVT_",
env_nested_delimiter="_",
extra="ignore",
nested_model_default_partial_updates=True,
)
# Allow to decided if want to load environment variables
load_env: bool = Field(True, exclude=True)
# General settings
PYPI_UPDATE_URL: AnyHttpUrl = Field(
"https://pypi.org/pypi/mvt/json",
validate_default=False,
)
NETWORK_ACCESS_ALLOWED: bool = True
NETWORK_TIMEOUT: int = 15
# Command default settings, all can be specified by MVT_ prefixed environment variables too.
IOS_BACKUP_PASSWORD: Optional[str] = Field(
None, description="Default password to use to decrypt iOS backups"
)
ANDROID_BACKUP_PASSWORD: Optional[str] = Field(
None, description="Default password to use to decrypt Android backups"
)
STIX2: Optional[str] = Field(
None, description="List of directories where STIX2 files are stored"
)
VT_API_KEY: Optional[str] = Field(
None, description="API key to use for VirusTotal lookups"
)
PROFILE: bool = Field(False, description="Profile the execution of MVT modules")
HASH_FILES: bool = Field(False, description="Should MVT hash output files")
@classmethod
def settings_customise_sources(
cls,
settings_cls: Type[BaseSettings],
init_settings: InitSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> Tuple[PydanticBaseSettingsSource, ...]:
sources = (
YamlConfigSettingsSource(settings_cls, MVT_CONFIG_PATH),
init_settings,
)
# Load env variables if enabled
if init_settings.init_kwargs.get("load_env", True):
sources = (env_settings,) + sources
return sources
def save_settings(
self,
) -> None:
"""
Save the current settings to a file.
"""
if not os.path.isdir(MVT_CONFIG_FOLDER):
os.makedirs(MVT_CONFIG_FOLDER)
# Dump the settings to the YAML file
model_serializable = json.loads(self.model_dump_json(exclude_defaults=True))
with open(MVT_CONFIG_PATH, "w") as config_file:
config_file.write(yaml.dump(model_serializable, default_flow_style=False))
@classmethod
def initialise(cls) -> "MVTSettings":
"""
Initialise the settings file.
We first initialise the settings (without env variable) and then persist
them to file. This way we can update the config file with the default values.
Afterwards we load the settings again, this time including the env variables.
"""
# Set invalid env prefix to avoid loading env variables.
settings = MVTSettings(load_env=False)
settings.save_settings()
# Load the settings again with any ENV variables.
settings = MVTSettings(load_env=True)
return settings
settings = MVTSettings.initialise()

View File

@@ -14,6 +14,7 @@ import ahocorasick
from appdirs import user_data_dir
from .url import URL
from .config import settings
MVT_DATA_FOLDER = user_data_dir("mvt")
MVT_INDICATORS_FOLDER = os.path.join(MVT_DATA_FOLDER, "indicators")
@@ -41,12 +42,12 @@ class Indicators:
def _check_stix2_env_variable(self) -> None:
"""
Checks if a variable MVT_STIX2 contains path to a STIX file. Also recursively searches through dirs in MVT_STIX2
Checks if MVT_STIX2 setting or environment variable contains path to a STIX file. Also recursively searches through dirs in MVT_STIX2
"""
if "MVT_STIX2" not in os.environ:
if not settings.STIX2:
return
paths = os.environ["MVT_STIX2"].split(":")
paths = settings.STIX2.split(":")
for path in paths:
if os.path.isfile(path) and path.lower().endswith(".stix2"):
self.parse_stix2(path)
@@ -383,8 +384,7 @@ class Indicators:
for ioc in self.get_iocs("urls"):
if ioc["value"] == url:
self.log.warning(
"Found a known suspicious URL %s "
'matching indicator "%s" from "%s"',
'Found a known suspicious URL %s matching indicator "%s" from "%s"',
url,
ioc["value"],
ioc["name"],

View File

@@ -227,7 +227,7 @@ def run_module(module: MVTModule) -> None:
module.save_to_json()
def save_timeline(timeline: list, timeline_path: str) -> None:
def save_timeline(timeline: list, timeline_path: str, is_utc: bool = True) -> None:
"""Save the timeline in a csv file.
:param timeline: List of records to order and store
@@ -238,7 +238,12 @@ def save_timeline(timeline: list, timeline_path: str) -> None:
csvoutput = csv.writer(
handle, delimiter=",", quotechar='"', quoting=csv.QUOTE_ALL, escapechar="\\"
)
csvoutput.writerow(["UTC Timestamp", "Plugin", "Event", "Description"])
if is_utc:
timestamp_header = "UTC Timestamp"
else:
timestamp_header = "Device Local Timestamp"
csvoutput.writerow([timestamp_header, "Plugin", "Event", "Description"])
for event in sorted(
timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""

View File

@@ -14,6 +14,7 @@ from packaging import version
from .indicators import MVT_DATA_FOLDER, MVT_INDICATORS_FOLDER
from .version import MVT_VERSION
from .config import settings
log = logging.getLogger(__name__)
@@ -23,7 +24,7 @@ INDICATORS_CHECK_FREQUENCY = 12
class MVTUpdates:
def check(self) -> str:
res = requests.get("https://pypi.org/pypi/mvt/json", timeout=15)
res = requests.get(settings.PYPI_UPDATE_URL, timeout=15)
data = res.json()
latest_version = data.get("info", {}).get("version", "")

View File

@@ -13,6 +13,7 @@ import re
from typing import Any, Iterator, Union
from rich.logging import RichHandler
from mvt.common.config import settings
class CustomJSONEncoder(json.JSONEncoder):
@@ -256,7 +257,7 @@ def set_verbose_logging(verbose: bool = False):
def exec_or_profile(module, globals, locals):
"""Hook for profiling MVT modules"""
if int(os.environ.get("MVT_PROFILE", False)):
if settings.PROFILE:
cProfile.runctx(module, globals, locals)
else:
exec(module, globals, locals)

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.6.0"

View File

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

View File

@@ -1083,5 +1083,29 @@
{
"version": "18.0.1",
"build": "22A3370"
},
{
"version": "18.1",
"build": "22B83"
},
{
"version": "18.1.1",
"build": "22B91"
},
{
"version": "18.2",
"build": "22C152"
},
{
"version": "18.2.1",
"build": "22C161"
},
{
"version": "18.3",
"build": "22D63"
},
{
"version": "18.3.1",
"build": "22D72"
}
]

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,12 @@ from mvt.ios.modules.base import IOSExtraction
APPLICATIONS_DB_PATH = [
"private/var/containers/Bundle/Application/*/iTunesMetadata.plist"
]
KNOWN_APP_INSTALLERS = [
"com.apple.AppStore",
"com.apple.AppStore.ProductPageExtension",
"com.apple.dmd",
"dmd",
]
class Applications(IOSExtraction):
@@ -80,12 +86,10 @@ class Applications(IOSExtraction):
self.detected.append(result)
continue
# Some apps installed from apple store with sourceApp "com.apple.AppStore.ProductPageExtension"
if result.get("sourceApp", "com.apple.AppStore") not in [
"com.apple.AppStore",
"com.apple.AppStore.ProductPageExtension",
"com.apple.dmd",
"dmd",
]:
if (
result.get("sourceApp", "com.apple.AppStore")
not in KNOWN_APP_INSTALLERS
):
self.log.warning(
"Suspicious app not installed from the App Store or MDM: %s",
result["softwareVersionBundleId"],

View File

@@ -43,7 +43,7 @@ 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']})"
sms_data = f'{record["service"]}: {record["guid"]} "{text}" from {record["phone_number"]} ({record["account"]})'
records = [
{
"timestamp": record["isodate"],

View File

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

View File

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

View File

@@ -29,3 +29,28 @@ class TestDumpsysADBArtifact:
user_key["fingerprint"] == "F0:A1:3D:8C:B3:F4:7B:09:9F:EE:8B:D8:38:2E:BD:C6"
)
assert user_key["user"] == "user@linux"
def test_parsing_adb_xml(self):
da_adb = DumpsysADBArtifact()
file = get_artifact("android_data/dumpsys_adb_xml.txt")
with open(file, "rb") as f:
data = f.read()
da_adb.parse(data)
assert len(da_adb.results) == 1
adb_data = da_adb.results[0]
assert "user_keys" in adb_data
assert len(adb_data["user_keys"]) == 1
# Check key and fingerprint parsed successfully.
expected_fingerprint = "F0:0B:27:08:E3:68:7B:FA:4C:79:A2:B4:BF:0E:CF:70"
user_key = adb_data["user_keys"][0]
user_key["fingerprint"] == expected_fingerprint
assert user_key["user"] == "user@laptop"
key_store_entry = adb_data["keystore"][0]
assert key_store_entry["user"] == "user@laptop"
assert key_store_entry["fingerprint"] == expected_fingerprint
assert key_store_entry["last_connected"] == "1628501829898"

View File

@@ -43,5 +43,21 @@ class TestDumpsysAppopsArtifact:
ind.ioc_collections[0]["app_ids"].append("com.facebook.katana")
da.indicators = ind
assert len(da.detected) == 0
da.check_indicators()
assert len(da.detected) == 1
detected_by_ioc = [
detected for detected in da.detected if detected.get("matched_indicator")
]
detected_by_permission_heuristic = [
detected
for detected in da.detected
if all(
[
perm["name"] == "REQUEST_INSTALL_PACKAGES"
for perm in detected["permissions"]
]
)
]
assert len(da.detected) == 3
assert len(detected_by_ioc) == 1
assert len(detected_by_permission_heuristic) == 2

View File

@@ -0,0 +1,40 @@
# 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_platform_compat import DumpsysPlatformCompatArtifact
from mvt.common.indicators import Indicators
from ..utils import get_artifact
class TestDumpsysPlatformCompatArtifact:
def test_parsing(self):
dbi = DumpsysPlatformCompatArtifact()
file = get_artifact("android_data/dumpsys_platform_compat.txt")
with open(file) as f:
data = f.read()
assert len(dbi.results) == 0
dbi.parse(data)
assert len(dbi.results) == 2
assert dbi.results[0]["package_name"] == "org.torproject.torbrowser"
assert dbi.results[1]["package_name"] == "org.article19.circulo.next"
def test_ioc_check(self, indicator_file):
dbi = DumpsysPlatformCompatArtifact()
file = get_artifact("android_data/dumpsys_platform_compat.txt")
with open(file) as f:
data = f.read()
dbi.parse(data)
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)
ind.ioc_collections[0]["app_ids"].append("org.torproject.torbrowser")
ind.ioc_collections[0]["app_ids"].append("org.article19.circulo.next")
dbi.indicators = ind
assert len(dbi.detected) == 0
dbi.check_indicators()
assert len(dbi.detected) == 2

View File

@@ -2,39 +2,66 @@
# 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 os
import datetime
import pytest
from mvt.android.artifacts.tombstone_crashes import TombstoneCrashArtifact
from mvt.android.parsers.proto.tombstone import Tombstone
from ..utils import get_artifact
class TestTombstoneCrashArtifact:
# def test_tombtone_process_parsing(self):
# tombstone_artifact = TombstoneCrashArtifact()
# file = get_artifact("android_data/tombstone_process.txt")
# with open(file, "rb") as f:
# data = f.read()
# tombstone_artifact.parse_text(data)
# assert len(tombstone_artifact.results) == 1
# def test_tombtone_kernel_parsing(self):
# tombstone_artifact = TombstoneCrashArtifact()
# file = get_artifact("android_data/tombstone_kernel.txt")
# with open(file, "rb") as f:
# data = f.read()
# tombstone_artifact.parse_text(data)
# assert len(tombstone_artifact.results) == 1
def test_tombstone_pb_process_parsing(self):
file = get_artifact("android_data/tombstone_process.pb")
def test_tombtone_process_parsing(self):
tombstone_artifact = TombstoneCrashArtifact()
artifact_path = "android_data/tombstone_process.txt"
file = get_artifact(artifact_path)
with open(file, "rb") as f:
data = f.read()
parsed_tombstone = Tombstone().parse(data)
assert parsed_tombstone
assert parsed_tombstone.command_line == ["/vendor/bin/hw/android.hardware.media.c2@1.2-mediatek"]
assert parsed_tombstone.uid == 1046
assert parsed_tombstone.timestamp == "2023-04-12 12:32:40.518290770+0200"
# Pass the file name and timestamp to the parse method
file_name = os.path.basename(artifact_path)
file_timestamp = datetime.datetime(2023, 4, 12, 12, 32, 40, 518290)
tombstone_artifact.parse(file_name, file_timestamp, data)
assert len(tombstone_artifact.results) == 1
self.validate_tombstone_result(tombstone_artifact.results[0])
def test_tombstone_pb_process_parsing(self):
tombstone_artifact = TombstoneCrashArtifact()
artifact_path = "android_data/tombstone_process.pb"
file = get_artifact(artifact_path)
with open(file, "rb") as f:
data = f.read()
file_name = os.path.basename(artifact_path)
file_timestamp = datetime.datetime(2023, 4, 12, 12, 32, 40, 518290)
tombstone_artifact.parse_protobuf(file_name, file_timestamp, data)
assert len(tombstone_artifact.results) == 1
self.validate_tombstone_result(tombstone_artifact.results[0])
@pytest.mark.skip(reason="Not implemented yet")
def test_tombtone_kernel_parsing(self):
tombstone_artifact = TombstoneCrashArtifact()
file = get_artifact("android_data/tombstone_kernel.txt")
with open(file, "rb") as f:
data = f.read()
tombstone_artifact.parse_text(data)
assert len(tombstone_artifact.results) == 1
def validate_tombstone_result(self, tombstone_result: dict):
assert tombstone_result.get("command_line") == [
"/vendor/bin/hw/android.hardware.media.c2@1.2-mediatek"
]
assert tombstone_result.get("uid") == 1046
assert tombstone_result.get("pid") == 25541
assert tombstone_result.get("process_name") == "mtk.ape.decoder"
# With Android logs we want to keep timestamps as device local time for consistency.
# We often don't know the time offset for a log entry and so can't convert everything to UTC.
# MVT should output the local time only:
# So original 2023-04-12 12:32:40.518290770+0200 -> 2023-04-12 12:32:40.000000
assert tombstone_result.get("timestamp") == "2023-04-12 12:32:40.000000"

View File

@@ -0,0 +1,23 @@
# 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 pathlib import Path
from mvt.android.modules.androidqf.dumpsys_platform_compat import DumpsysPlatformCompat
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysPlatformCompatModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysPlatformCompat(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 2
assert len(m.detected) == 0

View File

@@ -21,4 +21,9 @@ class TestDumpsysAppOpsModule:
run_module(m)
assert len(m.results) == 12
assert len(m.timeline) == 16
assert len(m.detected) == 0
detected_by_ioc = [
detected for detected in m.detected if detected.get("matched_indicator")
]
assert len(m.detected) == 1
assert len(detected_by_ioc) == 0

View File

@@ -33,7 +33,12 @@ class TestBugreportAnalysis:
m = self.launch_bug_report_module(Appops)
assert len(m.results) == 12
assert len(m.timeline) == 16
assert len(m.detected) == 0
detected_by_ioc = [
detected for detected in m.detected if detected.get("matched_indicator")
]
assert len(m.detected) == 1 # Hueristic detection for suspicious permissions
assert len(detected_by_ioc) == 0
def test_packages_module(self):
m = self.launch_bug_report_module(Packages)

View File

@@ -246,6 +246,23 @@ Packages:
com.instagram.direct.share.handler.DirectMultipleExternalMediaShareActivity
com.instagram.share.handleractivity.ClipsShareHandlerActivity
com.instagram.direct.share.handler.DirectMultipleExternalMediaShareActivityInterop
--------- 0.053s was the duration of dumpsys appops, ending at: 2022-03-29 23:14:27
-------------------------------------------------------------------------------
DUMP OF SERVICE platform_compat:
ChangeId(180326845; name=OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; disabled; overridable)
ChangeId(189969744; name=DOWNSCALE_65; disabled; overridable)
ChangeId(183372781; name=ENABLE_RAW_SYSTEM_GALLERY_ACCESS; enableSinceTargetSdk=30)
ChangeId(150939131; name=ADD_CONTENT_OBSERVER_FLAGS; enableSinceTargetSdk=30)
ChangeId(226439802; name=SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT; disabled)
ChangeId(270674727; name=ENABLE_STRICT_FORMATTER_VALIDATION; enableSinceTargetSdk=35)
ChangeId(183155436; name=ALWAYS_USE_CONTEXT_USER; enableSinceTargetSdk=33)
ChangeId(303742236; name=ROLE_MANAGER_USER_HANDLE_AWARE; enableSinceTargetSdk=35)
ChangeId(203800354; name=MEDIA_CONTROL_SESSION_ACTIONS; enableSinceTargetSdk=33)
ChangeId(144027538; name=BLOCK_GPS_STATUS_USAGE; enableSinceTargetSdk=31)
ChangeId(189969749; name=DOWNSCALE_35; disabled; overridable)
ChangeId(143539591; name=SELINUX_LATEST_CHANGES; disabled)
ChangeId(247079863; name=DISALLOW_INVALID_GROUP_REFERENCE; enableSinceTargetSdk=34)
ChangeId(174227820; name=FORCE_DISABLE_HEVC_SUPPORT; disabled)
ChangeId(168419799; name=DOWNSCALED; disabled; packageOverrides={com.google.android.apps.tachyon=false, org.torproject.torbrowser=false}; rawOverrides={org.torproject.torbrowser=false, org.article19.circulo.next=false}; overridable)

View File

@@ -0,0 +1,16 @@
-------------------------------------------------------------------------------
DUMP OF SERVICE adb:
ADB MANAGER STATE (dumpsys adb):
{
debugging_manager={
connected_to_adb=true
user_keys=QAAAAAcgbytJst31DsaSP7hn8QcBXKR9NPVPK9MZssFVSNIP user@laptop
keystore=<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<keyStore version="1">
<adbKey key="QAAAAAcgbytJst31DsaSP7hn8QcBXKR9NPVPK9MZssFVSNIP user@laptop" lastConnection="1628501829898" />
</keyStore>
}
}
--------- 0.012s was the duration of dumpsys adb, ending at: 2025-02-04 20:25:58

View File

@@ -0,0 +1,16 @@
DUMP OF SERVICE platform_compat:
ChangeId(180326845; name=OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; disabled; overridable)
ChangeId(189969744; name=DOWNSCALE_65; disabled; overridable)
ChangeId(183372781; name=ENABLE_RAW_SYSTEM_GALLERY_ACCESS; enableSinceTargetSdk=30)
ChangeId(150939131; name=ADD_CONTENT_OBSERVER_FLAGS; enableSinceTargetSdk=30)
ChangeId(226439802; name=SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT; disabled)
ChangeId(270674727; name=ENABLE_STRICT_FORMATTER_VALIDATION; enableSinceTargetSdk=35)
ChangeId(183155436; name=ALWAYS_USE_CONTEXT_USER; enableSinceTargetSdk=33)
ChangeId(303742236; name=ROLE_MANAGER_USER_HANDLE_AWARE; enableSinceTargetSdk=35)
ChangeId(203800354; name=MEDIA_CONTROL_SESSION_ACTIONS; enableSinceTargetSdk=33)
ChangeId(144027538; name=BLOCK_GPS_STATUS_USAGE; enableSinceTargetSdk=31)
ChangeId(189969749; name=DOWNSCALE_35; disabled; overridable)
ChangeId(143539591; name=SELINUX_LATEST_CHANGES; disabled)
ChangeId(247079863; name=DISALLOW_INVALID_GROUP_REFERENCE; enableSinceTargetSdk=34)
ChangeId(174227820; name=FORCE_DISABLE_HEVC_SUPPORT; disabled)
ChangeId(168419799; name=DOWNSCALED; disabled; packageOverrides={com.google.android.apps.tachyon=false, org.torproject.torbrowser=false}; rawOverrides={org.torproject.torbrowser=false, org.article19.circulo.next=false}; overridable)

View File

@@ -379,4 +379,22 @@ Daily stats:
Update com.google.android.projection.gearhead vers=99632623
Update com.google.android.projection.gearhead vers=99632623
Update com.google.android.projection.gearhead vers=99632623
--------- 0.053s was the duration of dumpsys batterystats, ending at: 2024-03-21 11:07:22
-------------------------------------------------------------------------------
DUMP OF SERVICE platform_compat:
ChangeId(180326845; name=OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; disabled; overridable)
ChangeId(189969744; name=DOWNSCALE_65; disabled; overridable)
ChangeId(183372781; name=ENABLE_RAW_SYSTEM_GALLERY_ACCESS; enableSinceTargetSdk=30)
ChangeId(150939131; name=ADD_CONTENT_OBSERVER_FLAGS; enableSinceTargetSdk=30)
ChangeId(226439802; name=SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT; disabled)
ChangeId(270674727; name=ENABLE_STRICT_FORMATTER_VALIDATION; enableSinceTargetSdk=35)
ChangeId(183155436; name=ALWAYS_USE_CONTEXT_USER; enableSinceTargetSdk=33)
ChangeId(303742236; name=ROLE_MANAGER_USER_HANDLE_AWARE; enableSinceTargetSdk=35)
ChangeId(203800354; name=MEDIA_CONTROL_SESSION_ACTIONS; enableSinceTargetSdk=33)
ChangeId(144027538; name=BLOCK_GPS_STATUS_USAGE; enableSinceTargetSdk=31)
ChangeId(189969749; name=DOWNSCALE_35; disabled; overridable)
ChangeId(143539591; name=SELINUX_LATEST_CHANGES; disabled)
ChangeId(247079863; name=DISALLOW_INVALID_GROUP_REFERENCE; enableSinceTargetSdk=34)
ChangeId(174227820; name=FORCE_DISABLE_HEVC_SUPPORT; disabled)
ChangeId(168419799; name=DOWNSCALED; disabled; packageOverrides={com.google.android.apps.tachyon=false, org.torproject.torbrowser=false}; rawOverrides={org.torproject.torbrowser=false, org.article19.circulo.next=false}; overridable)

View File

@@ -6,6 +6,8 @@
import logging
import os
from mvt.common.config import settings
from mvt.common.indicators import Indicators
from ..utils import get_artifact_folder
@@ -100,6 +102,8 @@ class TestIndicators:
def test_env_stix(self, indicator_file):
os.environ["MVT_STIX2"] = indicator_file
settings.__init__() # Reset settings
ind = Indicators(log=logging)
ind.load_indicators_files([], load_default=False)
assert ind.total_ioc_count == 9

View File

@@ -75,7 +75,7 @@ class TestHashes:
# This needs to be updated when we add or edit files in AndroidQF folder
assert (
hashes[1]["sha256"]
== "1bd255f656a7f9d5647a730f0f0cc47053115576f11532d41bf28c16635b193d"
== "9fb6396b64cfff30e2a459a64496d3c1386926d09edd68be2d878de45fa7b3a9"
)

View File

@@ -8,6 +8,7 @@ import os
from click.testing import CliRunner
from mvt.android.cli import check_androidqf
from mvt.common.config import settings
from .utils import get_artifact_folder
@@ -56,6 +57,8 @@ class TestCheckAndroidqfCommand:
)
os.environ["MVT_ANDROID_BACKUP_PASSWORD"] = TEST_BACKUP_PASSWORD
settings.__init__() # Reset settings
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted")
result = runner.invoke(check_androidqf, [path])
@@ -63,3 +66,4 @@ class TestCheckAndroidqfCommand:
assert prompt_mock.call_count == 0
assert result.exit_code == 0
del os.environ["MVT_ANDROID_BACKUP_PASSWORD"]
settings.__init__() # Reset settings

View File

@@ -9,6 +9,7 @@ import os
from click.testing import CliRunner
from mvt.android.cli import check_backup
from mvt.common.config import settings
from .utils import get_artifact_folder
@@ -63,6 +64,8 @@ class TestCheckAndroidBackupCommand:
)
os.environ["MVT_ANDROID_BACKUP_PASSWORD"] = TEST_BACKUP_PASSWORD
settings.__init__() # Reset settings
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted/backup.ab")
result = runner.invoke(check_backup, [path])
@@ -70,3 +73,4 @@ class TestCheckAndroidBackupCommand:
assert prompt_mock.call_count == 0
assert result.exit_code == 0
del os.environ["MVT_ANDROID_BACKUP_PASSWORD"]
settings.__init__() # Reset settings