mirror of
https://github.com/mvt-project/mvt
synced 2025-10-21 22:42:15 +02:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c719c4da1e | ||
|
|
0f3e93c152 | ||
|
|
a2ee46b8f8 | ||
|
|
e60e5fdc6e | ||
|
|
7e0e071c5d | ||
|
|
b259db30f8 | ||
|
|
26f981244d | ||
|
|
2069e2b760 | ||
|
|
355480414f | ||
|
|
9a831b5930 | ||
|
|
a103b50759 | ||
|
|
84dc13144d | ||
|
|
6356a4ff87 | ||
|
|
f96f2fe34a | ||
|
|
ae0e470c56 | ||
|
|
4c175530a8 | ||
|
|
ecf75447aa | ||
|
|
0389d335ed | ||
|
|
7f9acec108 | ||
|
|
3ec3b86a45 | ||
|
|
57d4aca72e | ||
|
|
1d740ad802 | ||
|
|
15ce1b7e64 | ||
|
|
d6fca2f8ae | ||
|
|
cabb679ff1 | ||
|
|
829a9f0cf6 | ||
|
|
52e0176d5d | ||
|
|
8d8bdf26de | ||
|
|
34fa77ae4d | ||
|
|
ed7d6fb847 | ||
|
|
a2386dbdf7 | ||
|
|
019cfbb84e | ||
|
|
3d924e22ec | ||
|
|
ca3c1bade4 | ||
|
|
85877fd3eb | ||
|
|
8015ff78e8 | ||
|
|
1a07b9a78f | ||
|
|
0b88de9867 | ||
|
|
0edc9d7b81 | ||
|
|
76d7534b05 | ||
|
|
ae2ab02347 | ||
|
|
e2c623c40f | ||
|
|
a6e1a3de12 | ||
|
|
e7270d6a07 | ||
|
|
1968a0fca2 | ||
|
|
46cc54df74 | ||
|
|
7046ff80d1 |
18
.github/workflows/python-package.yml
vendored
18
.github/workflows/python-package.yml
vendored
@@ -16,19 +16,19 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.8', '3.9', '3.10']
|
python-version: ['3.8', '3.9', '3.10'] # , '3.11']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade setuptools
|
python -m pip install --upgrade setuptools
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
python -m pip install flake8 pytest safety stix2 pytest-mock
|
python -m pip install flake8 pytest safety stix2 pytest-mock pytest-cov
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
python -m pip install .
|
python -m pip install .
|
||||||
- name: Lint with flake8
|
- name: Lint with flake8
|
||||||
@@ -39,5 +39,11 @@ jobs:
|
|||||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
- name: Safety checks
|
- name: Safety checks
|
||||||
run: safety check
|
run: safety check
|
||||||
- name: Test with pytest
|
- name: Test with pytest and coverage
|
||||||
run: pytest
|
run: pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=mvt tests/ | tee pytest-coverage.txt
|
||||||
|
- name: Pytest coverage comment
|
||||||
|
uses: MishaKav/pytest-coverage-comment@main
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
pytest-coverage-path: ./pytest-coverage.txt
|
||||||
|
junitxml-path: ./pytest.xml
|
||||||
16
.github/workflows/ruff.yml
vendored
16
.github/workflows/ruff.yml
vendored
@@ -1,21 +1,19 @@
|
|||||||
name: Ruff
|
name: Ruff
|
||||||
on: [push]
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
jobs:
|
jobs:
|
||||||
ruff_py3:
|
ruff_py3:
|
||||||
name: Ruff syntax check
|
name: Ruff syntax check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
architecture: x64
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@master
|
uses: actions/checkout@master
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install ruff
|
pip install --user ruff
|
||||||
- name: ruff
|
- name: ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check .
|
ruff --format=github .
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -50,6 +50,8 @@ coverage.xml
|
|||||||
*.py,cover
|
*.py,cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
pytest-coverage.txt
|
||||||
|
pytest.xml
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -2,9 +2,10 @@ PWD = $(shell pwd)
|
|||||||
|
|
||||||
check:
|
check:
|
||||||
flake8
|
flake8
|
||||||
pytest -q
|
|
||||||
ruff check -q .
|
ruff check -q .
|
||||||
black --check .
|
black --check .
|
||||||
|
pytest -q
|
||||||
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/mvt.egg-info
|
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/mvt.egg-info
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ $ mvt-android check-backup --output /path/to/results/ /path/to/backup.ab
|
|||||||
INFO [mvt.android.modules.backup.sms] Extracted a total of 64 SMS messages
|
INFO [mvt.android.modules.backup.sms] Extracted a total of 64 SMS messages
|
||||||
```
|
```
|
||||||
|
|
||||||
If the backup is encrypted, MVT will prompt you to enter the password.
|
If the backup is encrypted, MVT will prompt you to enter the password. A backup password can also be provided with the `--backup-password` command line option or through the `MVT_ANDROID_BACKUP_PASSWORD` environment variable. The same options can also be used to when analysing an encrypted backup collected through AndroidQF in the `mvt-android check-androidqf` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mvt-android check-backup --backup-password "password123" --output /path/to/results/ /path/to/backup.ab
|
||||||
|
```
|
||||||
|
|
||||||
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by MVT. Any matches will be highlighted in the terminal output.
|
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by MVT. Any matches will be highlighted in the terminal output.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
Mobile Verification Toolkit (MVT) is a tool to facilitate the [consensual forensic analysis](introduction.md#consensual-forensics) of Android and iOS devices, for the purpose of identifying traces of compromise.
|
Mobile Verification Toolkit (MVT) is a tool to facilitate the [consensual forensic analysis](introduction.md#consensual-forensics) of Android and iOS devices, for the purpose of identifying traces of compromise.
|
||||||
|
|
||||||
|
It has been developed and released by the [Amnesty International Security Lab](https://www.amnesty.org/en/tech/) in July 2021 in the context of the [Pegasus Project](https://forbiddenstories.org/about-the-pegasus-project/) along with [a technical forensic methodology](https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/). It continues to be maintained by Amnesty International and other contributors.
|
||||||
|
|
||||||
|
|
||||||
In this documentation you will find instructions on how to install and run the `mvt-ios` and `mvt-android` commands, and guidance on how to interpret the extracted results.
|
In this documentation you will find instructions on how to install and run the `mvt-ios` and `mvt-android` commands, and guidance on how to interpret the extracted results.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|||||||
@@ -12,6 +12,20 @@ Mobile Verification Toolkit (MVT) is a collection of utilities designed to facil
|
|||||||
|
|
||||||
MVT is a forensic research tool intended for technologists and investigators. Using it requires understanding the basics of forensic analysis and using command-line tools. MVT is not intended for end-user self-assessment. If you are concerned with the security of your device please seek expert assistance.
|
MVT is a forensic research tool intended for technologists and investigators. Using it requires understanding the basics of forensic analysis and using command-line tools. MVT is not intended for end-user self-assessment. If you are concerned with the security of your device please seek expert assistance.
|
||||||
|
|
||||||
|
## Indicators of Compromise
|
||||||
|
|
||||||
|
MVT supports using [indicators of compromise (IOCs)](https://github.com/mvt-project/mvt-indicators) to scan mobile devices for potential traces of targeting or infection by known spyware campaigns. This includes IOCs published by [Amnesty International](https://github.com/AmnestyTech/investigations/) and other research groups.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Public indicators of compromise are insufficient to determine that a device is "clean", and not targeted with a particular spyware tool. Reliance on public indicators alone can miss recent forensic traces and give a false sense of security.
|
||||||
|
|
||||||
|
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://www.amnesty.org/en/tech/) or [Access Now’s Digital Security Helpline](https://www.accessnow.org/help/).
|
||||||
|
|
||||||
|
More information about using indicators of compromise with MVT is available in the [documentation](iocs.md).
|
||||||
|
|
||||||
|
|
||||||
## Consensual Forensics
|
## Consensual Forensics
|
||||||
|
|
||||||
While MVT is capable of extracting and processing various types of very personal records typically found on a mobile phone (such as calls history, SMS and WhatsApp messages, etc.), this is intended to help identify potential attack vectors such as malicious SMS messages leading to exploitation.
|
While MVT is capable of extracting and processing various types of very personal records typically found on a mobile phone (such as calls history, SMS and WhatsApp messages, etc.), this is intended to help identify potential attack vectors such as malicious SMS messages leading to exploitation.
|
||||||
|
|||||||
@@ -142,6 +142,16 @@ If indicators are provided through the command-line, they are checked against th
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### `global_preferences.json`
|
||||||
|
|
||||||
|
!!! info "Availability"
|
||||||
|
Backup: :material-check:
|
||||||
|
Full filesystem dump: :material-check:
|
||||||
|
|
||||||
|
This JSON file is created by mvt-ios' `GlobalPreferences` module. The module extracts records from a Plist file located at */private/var/mobile/Library/Preferences/.GlobalPreferences.plist*, which contains a system preferences including if Lockdown Mode is enabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `id_status_cache.json`
|
### `id_status_cache.json`
|
||||||
|
|
||||||
!!! info "Availability"
|
!!! info "Availability"
|
||||||
|
|||||||
4
mvt/android/artifacts/__init__.py
Normal file
4
mvt/android/artifacts/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
36
mvt/android/artifacts/artifact.py
Normal file
36
mvt/android/artifacts/artifact.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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.common.artifact import Artifact
|
||||||
|
|
||||||
|
|
||||||
|
class AndroidArtifact(Artifact):
|
||||||
|
@staticmethod
|
||||||
|
def extract_dumpsys_section(dumpsys: str, separator: str) -> str:
|
||||||
|
"""
|
||||||
|
Extract a section from a full dumpsys file.
|
||||||
|
|
||||||
|
:param dumpsys: content of the full dumpsys file (string)
|
||||||
|
:param separator: content of the first line separator (string)
|
||||||
|
:return: section extracted (string)
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
in_section = False
|
||||||
|
for line in dumpsys.splitlines():
|
||||||
|
if line.strip() == separator:
|
||||||
|
in_section = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_section:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.strip().startswith(
|
||||||
|
"------------------------------------------------------------------------------"
|
||||||
|
):
|
||||||
|
break
|
||||||
|
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
47
mvt/android/artifacts/dumpsys_accessibility.py
Normal file
47
mvt/android/artifacts/dumpsys_accessibility.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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 DumpsysAccessibilityArtifact(AndroidArtifact):
|
||||||
|
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, content: str) -> None:
|
||||||
|
"""
|
||||||
|
Parse the Dumpsys Accessibility section/
|
||||||
|
Adds results to self.results (List[Dict[str, str]])
|
||||||
|
|
||||||
|
:param content: content of the accessibility section (string)
|
||||||
|
"""
|
||||||
|
in_services = False
|
||||||
|
for line in content.splitlines():
|
||||||
|
if line.strip().startswith("installed services:"):
|
||||||
|
in_services = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_services:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.strip() == "}":
|
||||||
|
break
|
||||||
|
|
||||||
|
service = line.split(":")[1].strip()
|
||||||
|
|
||||||
|
self.results.append(
|
||||||
|
{
|
||||||
|
"package_name": service.split("/")[0],
|
||||||
|
"service": service,
|
||||||
|
}
|
||||||
|
)
|
||||||
150
mvt/android/artifacts/dumpsys_appops.py
Normal file
150
mvt/android/artifacts/dumpsys_appops.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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
|
||||||
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
|
from mvt.common.utils import convert_datetime_to_iso
|
||||||
|
|
||||||
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
|
||||||
|
class DumpsysAppopsArtifact(AndroidArtifact):
|
||||||
|
"""
|
||||||
|
Parser for dumpsys app ops info
|
||||||
|
"""
|
||||||
|
|
||||||
|
def serialize(self, record: dict) -> Union[dict, list]:
|
||||||
|
records = []
|
||||||
|
for perm in record["permissions"]:
|
||||||
|
if "entries" not in perm:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for entry in perm["entries"]:
|
||||||
|
if "timestamp" in entry:
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
"timestamp": entry["timestamp"],
|
||||||
|
"module": self.__class__.__name__,
|
||||||
|
"event": entry["access"],
|
||||||
|
"data": f"{record['package_name']} access to "
|
||||||
|
f"{perm['name']}: {entry['access']}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return records
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
for result in self.results:
|
||||||
|
if self.indicators:
|
||||||
|
ioc = self.indicators.check_app_id(result.get("package_name"))
|
||||||
|
if ioc:
|
||||||
|
result["matched_indicator"] = ioc
|
||||||
|
self.detected.append(result)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for perm in result["permissions"]:
|
||||||
|
if (
|
||||||
|
perm["name"] == "REQUEST_INSTALL_PACKAGES"
|
||||||
|
and perm["access"] == "allow"
|
||||||
|
):
|
||||||
|
self.log.info(
|
||||||
|
"Package %s with REQUEST_INSTALL_PACKAGES " "permission",
|
||||||
|
result["package_name"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse(self, output: str) -> None:
|
||||||
|
self.results: List[Dict[str, Any]] = []
|
||||||
|
perm = {}
|
||||||
|
package = {}
|
||||||
|
entry = {}
|
||||||
|
uid = None
|
||||||
|
in_packages = False
|
||||||
|
|
||||||
|
for line in output.splitlines():
|
||||||
|
if line.startswith(" Uid 0:"):
|
||||||
|
in_packages = True
|
||||||
|
|
||||||
|
if not in_packages:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(" Uid "):
|
||||||
|
uid = line[6:-1]
|
||||||
|
if entry:
|
||||||
|
perm["entries"].append(entry)
|
||||||
|
entry = {}
|
||||||
|
if package:
|
||||||
|
if perm:
|
||||||
|
package["permissions"].append(perm)
|
||||||
|
|
||||||
|
perm = {}
|
||||||
|
self.results.append(package)
|
||||||
|
package = {}
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(" Package "):
|
||||||
|
if entry:
|
||||||
|
perm["entries"].append(entry)
|
||||||
|
entry = {}
|
||||||
|
|
||||||
|
if package:
|
||||||
|
if perm:
|
||||||
|
package["permissions"].append(perm)
|
||||||
|
|
||||||
|
perm = {}
|
||||||
|
self.results.append(package)
|
||||||
|
|
||||||
|
package = {
|
||||||
|
"package_name": line[12:-1],
|
||||||
|
"permissions": [],
|
||||||
|
"uid": uid,
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
|
if package and line.startswith(" ") and line[6] != " ":
|
||||||
|
if entry:
|
||||||
|
perm["entries"].append(entry)
|
||||||
|
entry = {}
|
||||||
|
if perm:
|
||||||
|
package["permissions"].append(perm)
|
||||||
|
perm = {}
|
||||||
|
|
||||||
|
perm["name"] = line.split()[0]
|
||||||
|
perm["entries"] = []
|
||||||
|
if len(line.split()) > 1:
|
||||||
|
perm["access"] = line.split()[1][1:-2]
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(" "):
|
||||||
|
# Permission entry like:
|
||||||
|
# Reject: [fg-s]2021-05-19 22:02:52.054 (-314d1h25m2s33ms)
|
||||||
|
if entry:
|
||||||
|
perm["entries"].append(entry)
|
||||||
|
entry = {}
|
||||||
|
|
||||||
|
entry["access"] = line.split(":")[0].strip()
|
||||||
|
entry["type"] = line[line.find("[") + 1 : line.find("]")]
|
||||||
|
|
||||||
|
try:
|
||||||
|
entry["timestamp"] = convert_datetime_to_iso(
|
||||||
|
datetime.strptime(
|
||||||
|
line[line.find("]") + 1 : line.find("(")].strip(),
|
||||||
|
"%Y-%m-%d %H:%M:%S.%f",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
# Invalid date format
|
||||||
|
pass
|
||||||
|
|
||||||
|
if line.strip() == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
if entry:
|
||||||
|
perm["entries"].append(entry)
|
||||||
|
if perm:
|
||||||
|
package["permissions"].append(perm)
|
||||||
|
if package:
|
||||||
|
self.results.append(package)
|
||||||
78
mvt/android/artifacts/dumpsys_battery_daily.py
Normal file
78
mvt/android/artifacts/dumpsys_battery_daily.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
|
||||||
|
class DumpsysBatteryDailyArtifact(AndroidArtifact):
|
||||||
|
"""
|
||||||
|
Parser for dumpsys dattery daily updates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def serialize(self, record: dict) -> Union[dict, list]:
|
||||||
|
return {
|
||||||
|
"timestamp": record["from"],
|
||||||
|
"module": self.__class__.__name__,
|
||||||
|
"event": "battery_daily",
|
||||||
|
"data": f"Recorded update of package {record['package_name']} "
|
||||||
|
f"with vers {record['vers']}",
|
||||||
|
}
|
||||||
|
|
||||||
|
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, output: str) -> None:
|
||||||
|
daily = None
|
||||||
|
daily_updates = []
|
||||||
|
for line in output.splitlines():
|
||||||
|
if line.startswith(" Daily from "):
|
||||||
|
if len(daily_updates) > 0:
|
||||||
|
self.results.extend(daily_updates)
|
||||||
|
daily_updates = []
|
||||||
|
|
||||||
|
timeframe = line[13:].strip()
|
||||||
|
date_from, date_to = timeframe.strip(":").split(" to ", 1)
|
||||||
|
daily = {"from": date_from[0:10], "to": date_to[0:10]}
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not daily:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not line.strip().startswith("Update "):
|
||||||
|
continue
|
||||||
|
|
||||||
|
line = line.strip().replace("Update ", "")
|
||||||
|
package_name, vers = line.split(" ", 1)
|
||||||
|
vers_nr = vers.split("=", 1)[1]
|
||||||
|
|
||||||
|
already_seen = False
|
||||||
|
for update in daily_updates:
|
||||||
|
if package_name == update["package_name"] and vers_nr == update["vers"]:
|
||||||
|
already_seen = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not already_seen:
|
||||||
|
daily_updates.append(
|
||||||
|
{
|
||||||
|
"action": "update",
|
||||||
|
"from": daily["from"],
|
||||||
|
"to": daily["to"],
|
||||||
|
"package_name": package_name,
|
||||||
|
"vers": vers_nr,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(daily_updates) > 0:
|
||||||
|
self.results.extend(daily_updates)
|
||||||
78
mvt/android/artifacts/dumpsys_battery_history.py
Normal file
78
mvt/android/artifacts/dumpsys_battery_history.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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 DumpsysBatteryHistoryArtifact(AndroidArtifact):
|
||||||
|
"""
|
||||||
|
Parser for dumpsys dattery history events.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 line.startswith("Battery History "):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.strip() == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
time_elapsed = line.strip().split(" ", 1)[0]
|
||||||
|
|
||||||
|
event = ""
|
||||||
|
if line.find("+job") > 0:
|
||||||
|
event = "start_job"
|
||||||
|
uid = line[line.find("+job") + 5 : line.find(":")]
|
||||||
|
service = line[line.find(":") + 1 :].strip('"')
|
||||||
|
package_name = service.split("/")[0]
|
||||||
|
elif line.find("-job") > 0:
|
||||||
|
event = "end_job"
|
||||||
|
uid = line[line.find("-job") + 5 : line.find(":")]
|
||||||
|
service = line[line.find(":") + 1 :].strip('"')
|
||||||
|
package_name = service.split("/")[0]
|
||||||
|
elif line.find("+running +wake_lock=") > 0:
|
||||||
|
uid = line[line.find("+running +wake_lock=") + 21 : line.find(":")]
|
||||||
|
event = "wake"
|
||||||
|
service = (
|
||||||
|
line[line.find("*walarm*:") + 9 :].split(" ")[0].strip('"').strip()
|
||||||
|
)
|
||||||
|
if service == "" or "/" not in service:
|
||||||
|
continue
|
||||||
|
|
||||||
|
package_name = service.split("/")[0]
|
||||||
|
elif (line.find("+top=") > 0) or (line.find("-top") > 0):
|
||||||
|
if line.find("+top=") > 0:
|
||||||
|
event = "start_top"
|
||||||
|
top_pos = line.find("+top=")
|
||||||
|
else:
|
||||||
|
event = "end_top"
|
||||||
|
top_pos = line.find("-top=")
|
||||||
|
colon_pos = top_pos + line[top_pos:].find(":")
|
||||||
|
uid = line[top_pos + 5 : colon_pos]
|
||||||
|
service = ""
|
||||||
|
package_name = line[colon_pos + 1 :].strip('"')
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.results.append(
|
||||||
|
{
|
||||||
|
"time_elapsed": time_elapsed,
|
||||||
|
"event": event,
|
||||||
|
"uid": uid,
|
||||||
|
"package_name": package_name,
|
||||||
|
"service": service,
|
||||||
|
}
|
||||||
|
)
|
||||||
83
mvt/android/artifacts/dumpsys_dbinfo.py
Normal file
83
mvt/android/artifacts/dumpsys_dbinfo.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
class DumpsysDBInfoArtifact(AndroidArtifact):
|
||||||
|
"""
|
||||||
|
Parser for dumpsys DBInfo service
|
||||||
|
"""
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
if not self.indicators:
|
||||||
|
return
|
||||||
|
|
||||||
|
for result in self.results:
|
||||||
|
path = result.get("path", "")
|
||||||
|
for part in path.split("/"):
|
||||||
|
ioc = self.indicators.check_app_id(part)
|
||||||
|
if ioc:
|
||||||
|
result["matched_indicator"] = ioc
|
||||||
|
self.detected.append(result)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def parse(self, output: str) -> None:
|
||||||
|
rxp = re.compile(
|
||||||
|
r".*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\].*\[Pid:\((\d+)\)\](\w+).*sql\=\"(.+?)\""
|
||||||
|
) # pylint: disable=line-too-long
|
||||||
|
rxp_no_pid = re.compile(
|
||||||
|
r".*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\][ ]{1}(\w+).*sql\=\"(.+?)\""
|
||||||
|
) # pylint: disable=line-too-long
|
||||||
|
|
||||||
|
pool = None
|
||||||
|
in_operations = False
|
||||||
|
for line in output.splitlines():
|
||||||
|
if line.startswith("Connection pool for "):
|
||||||
|
pool = line.replace("Connection pool for ", "").rstrip(":")
|
||||||
|
|
||||||
|
if not pool:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.strip() == "Most recently executed operations:":
|
||||||
|
in_operations = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_operations:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not line.startswith(" "):
|
||||||
|
in_operations = False
|
||||||
|
pool = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
matches = rxp.findall(line)
|
||||||
|
if not matches:
|
||||||
|
matches = rxp_no_pid.findall(line)
|
||||||
|
if not matches:
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = matches[0]
|
||||||
|
self.results.append(
|
||||||
|
{
|
||||||
|
"isodate": match[0],
|
||||||
|
"action": match[1],
|
||||||
|
"sql": match[2],
|
||||||
|
"path": pool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
match = matches[0]
|
||||||
|
self.results.append(
|
||||||
|
{
|
||||||
|
"isodate": match[0],
|
||||||
|
"pid": match[1],
|
||||||
|
"action": match[2],
|
||||||
|
"sql": match[3],
|
||||||
|
"path": pool,
|
||||||
|
}
|
||||||
|
)
|
||||||
84
mvt/android/artifacts/dumpsys_package_activities.py
Normal file
84
mvt/android/artifacts/dumpsys_package_activities.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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 DumpsysPackageActivitiesArtifact(AndroidArtifact):
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
if not self.indicators:
|
||||||
|
return
|
||||||
|
|
||||||
|
for activity in self.results:
|
||||||
|
ioc = self.indicators.check_app_id(activity["package_name"])
|
||||||
|
if ioc:
|
||||||
|
activity["matched_indicator"] = ioc
|
||||||
|
self.detected.append(activity)
|
||||||
|
continue
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
in_activity_resolver_table = False
|
||||||
|
in_non_data_actions = False
|
||||||
|
intent = None
|
||||||
|
for line in content.splitlines():
|
||||||
|
if line.startswith("Activity Resolver Table:"):
|
||||||
|
in_activity_resolver_table = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_activity_resolver_table:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(" Non-Data Actions:"):
|
||||||
|
in_non_data_actions = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_non_data_actions:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we hit an empty line, the Non-Data Actions section should be
|
||||||
|
# finished.
|
||||||
|
if line.strip() == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
# We detect the action name.
|
||||||
|
if (
|
||||||
|
line.startswith(" " * 6)
|
||||||
|
and not line.startswith(" " * 8)
|
||||||
|
and ":" in line
|
||||||
|
):
|
||||||
|
intent = line.strip().replace(":", "")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we are not in an intent block yet, skip.
|
||||||
|
if not intent:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we are in a block but the line does not start with 8 spaces
|
||||||
|
# it means the block ended a new one started, so we reset and
|
||||||
|
# continue.
|
||||||
|
if not line.startswith(" " * 8):
|
||||||
|
intent = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we got this far, we are processing receivers for the
|
||||||
|
# activities we are interested in.
|
||||||
|
activity = line.strip().split(" ")[1]
|
||||||
|
package_name = activity.split("/")[0]
|
||||||
|
|
||||||
|
self.results.append(
|
||||||
|
{
|
||||||
|
"intent": intent,
|
||||||
|
"package_name": package_name,
|
||||||
|
"activity": activity,
|
||||||
|
}
|
||||||
|
)
|
||||||
116
mvt/android/artifacts/dumpsys_receivers.py
Normal file
116
mvt/android/artifacts/dumpsys_receivers.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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
|
||||||
|
|
||||||
|
INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
|
||||||
|
INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
|
||||||
|
INTENT_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
|
||||||
|
INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE"
|
||||||
|
INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
|
||||||
|
|
||||||
|
|
||||||
|
class DumpsysReceiversArtifact(AndroidArtifact):
|
||||||
|
"""
|
||||||
|
Parser for dumpsys receivers in the package section
|
||||||
|
"""
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
for intent, receivers in self.results.items():
|
||||||
|
for receiver in receivers:
|
||||||
|
if intent == INTENT_NEW_OUTGOING_SMS:
|
||||||
|
self.log.info(
|
||||||
|
'Found a receiver to intercept outgoing SMS messages: "%s"',
|
||||||
|
receiver["receiver"],
|
||||||
|
)
|
||||||
|
elif intent == INTENT_SMS_RECEIVED:
|
||||||
|
self.log.info(
|
||||||
|
'Found a receiver to intercept incoming SMS messages: "%s"',
|
||||||
|
receiver["receiver"],
|
||||||
|
)
|
||||||
|
elif intent == INTENT_DATA_SMS_RECEIVED:
|
||||||
|
self.log.info(
|
||||||
|
'Found a receiver to intercept incoming data SMS message: "%s"',
|
||||||
|
receiver["receiver"],
|
||||||
|
)
|
||||||
|
elif intent == INTENT_PHONE_STATE:
|
||||||
|
self.log.info(
|
||||||
|
"Found a receiver monitoring "
|
||||||
|
'telephony state/incoming calls: "%s"',
|
||||||
|
receiver["receiver"],
|
||||||
|
)
|
||||||
|
elif intent == INTENT_NEW_OUTGOING_CALL:
|
||||||
|
self.log.info(
|
||||||
|
'Found a receiver monitoring outgoing calls: "%s"',
|
||||||
|
receiver["receiver"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.indicators:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ioc = self.indicators.check_app_id(receiver["package_name"])
|
||||||
|
if ioc:
|
||||||
|
receiver["matched_indicator"] = ioc
|
||||||
|
self.detected.append({intent: receiver})
|
||||||
|
continue
|
||||||
|
|
||||||
|
def parse(self, output: str) -> None:
|
||||||
|
self.results = {}
|
||||||
|
|
||||||
|
in_receiver_resolver_table = False
|
||||||
|
in_non_data_actions = False
|
||||||
|
intent = None
|
||||||
|
for line in output.splitlines():
|
||||||
|
if line.startswith("Receiver Resolver Table:"):
|
||||||
|
in_receiver_resolver_table = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_receiver_resolver_table:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(" Non-Data Actions:"):
|
||||||
|
in_non_data_actions = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_non_data_actions:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we hit an empty line, the Non-Data Actions section should be
|
||||||
|
# finished.
|
||||||
|
if line.strip() == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
# We detect the action name.
|
||||||
|
if (
|
||||||
|
line.startswith(" " * 6)
|
||||||
|
and not line.startswith(" " * 8)
|
||||||
|
and ":" in line
|
||||||
|
):
|
||||||
|
intent = line.strip().replace(":", "")
|
||||||
|
self.results[intent] = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we are not in an intent block yet, skip.
|
||||||
|
if not intent:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we are in a block but the line does not start with 8 spaces
|
||||||
|
# it means the block ended a new one started, so we reset and
|
||||||
|
# continue.
|
||||||
|
if not line.startswith(" " * 8):
|
||||||
|
intent = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we got this far, we are processing receivers for the
|
||||||
|
# activities we are interested in.
|
||||||
|
receiver = line.strip().split(" ")[1]
|
||||||
|
package_name = receiver.split("/")[0]
|
||||||
|
|
||||||
|
self.results[intent].append(
|
||||||
|
{
|
||||||
|
"package_name": package_name,
|
||||||
|
"receiver": receiver,
|
||||||
|
}
|
||||||
|
)
|
||||||
60
mvt/android/artifacts/getprop.py
Normal file
60
mvt/android/artifacts/getprop.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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 Dict, List
|
||||||
|
|
||||||
|
from mvt.android.utils import warn_android_patch_level
|
||||||
|
|
||||||
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
INTERESTING_PROPERTIES = [
|
||||||
|
"gsm.sim.operator.alpha",
|
||||||
|
"gsm.sim.operator.iso-country",
|
||||||
|
"persist.sys.timezone",
|
||||||
|
"ro.boot.serialno",
|
||||||
|
"ro.build.version.sdk",
|
||||||
|
"ro.build.version.security_patch",
|
||||||
|
"ro.product.cpu.abi",
|
||||||
|
"ro.product.locale",
|
||||||
|
"ro.product.vendor.manufacturer",
|
||||||
|
"ro.product.vendor.model",
|
||||||
|
"ro.product.vendor.name",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GetProp(AndroidArtifact):
|
||||||
|
def parse(self, entry: str) -> None:
|
||||||
|
self.results: List[Dict[str, str]] = []
|
||||||
|
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
|
||||||
|
|
||||||
|
for line in entry.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
matches = re.findall(rxp, line)
|
||||||
|
if not matches or len(matches[0]) != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry = {"name": matches[0][0], "value": matches[0][1]}
|
||||||
|
self.results.append(entry)
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
for entry in self.results:
|
||||||
|
if entry["name"] in INTERESTING_PROPERTIES:
|
||||||
|
self.log.info("%s: %s", entry["name"], entry["value"])
|
||||||
|
|
||||||
|
if entry["name"] == "ro.build.version.security_patch":
|
||||||
|
warn_android_patch_level(entry["value"], self.log)
|
||||||
|
|
||||||
|
if not self.indicators:
|
||||||
|
return
|
||||||
|
|
||||||
|
for result in self.results:
|
||||||
|
ioc = self.indicators.check_android_property_name(result.get("name", ""))
|
||||||
|
if ioc:
|
||||||
|
result["matched_indicator"] = ioc
|
||||||
|
self.detected.append(result)
|
||||||
70
mvt/android/artifacts/processes.py
Normal file
70
mvt/android/artifacts/processes.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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 Processes(AndroidArtifact):
|
||||||
|
def parse(self, entry: str) -> None:
|
||||||
|
for line in entry.split("\n")[1:]:
|
||||||
|
proc = line.split()
|
||||||
|
|
||||||
|
# Skip empty lines
|
||||||
|
if len(proc) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sometimes WCHAN is empty.
|
||||||
|
if len(proc) == 8:
|
||||||
|
proc = proc[:5] + [""] + proc[5:]
|
||||||
|
|
||||||
|
# Sometimes there is the security label.
|
||||||
|
if proc[0].startswith("u:r"):
|
||||||
|
label = proc[0]
|
||||||
|
proc = proc[1:]
|
||||||
|
else:
|
||||||
|
label = ""
|
||||||
|
|
||||||
|
# Sometimes there is no WCHAN.
|
||||||
|
if len(proc) < 9:
|
||||||
|
proc = proc[:5] + [""] + proc[5:]
|
||||||
|
|
||||||
|
self.results.append(
|
||||||
|
{
|
||||||
|
"user": proc[0],
|
||||||
|
"pid": int(proc[1]),
|
||||||
|
"ppid": int(proc[2]),
|
||||||
|
"virtual_memory_size": int(proc[3]),
|
||||||
|
"resident_set_size": int(proc[4]),
|
||||||
|
"wchan": proc[5],
|
||||||
|
"aprocress": proc[6],
|
||||||
|
"stat": proc[7],
|
||||||
|
"proc_name": proc[8].strip("[]"),
|
||||||
|
"label": label,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
if not self.indicators:
|
||||||
|
return
|
||||||
|
|
||||||
|
for result in self.results:
|
||||||
|
proc_name = result.get("proc_name", "")
|
||||||
|
if not proc_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skipping this process because of false positives.
|
||||||
|
if result["proc_name"] == "gatekeeperd":
|
||||||
|
continue
|
||||||
|
|
||||||
|
ioc = self.indicators.check_app_id(proc_name)
|
||||||
|
if ioc:
|
||||||
|
result["matched_indicator"] = ioc
|
||||||
|
self.detected.append(result)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ioc = self.indicators.check_process(proc_name)
|
||||||
|
if ioc:
|
||||||
|
result["matched_indicator"] = ioc
|
||||||
|
self.detected.append(result)
|
||||||
72
mvt/android/artifacts/settings.py
Normal file
72
mvt/android/artifacts/settings.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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
|
||||||
|
|
||||||
|
ANDROID_DANGEROUS_SETTINGS = [
|
||||||
|
{
|
||||||
|
"description": "disabled Google Play Services apps verification",
|
||||||
|
"key": "verifier_verify_adb_installs",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled Google Play Protect",
|
||||||
|
"key": "package_verifier_enable",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled Google Play Protect",
|
||||||
|
"key": "package_verifier_user_consent",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled Google Play Protect",
|
||||||
|
"key": "upload_apk_enable",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled confirmation of adb apps installation",
|
||||||
|
"key": "adb_install_need_confirm",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled sharing of security reports",
|
||||||
|
"key": "send_security_reports",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled sharing of crash logs with manufacturer",
|
||||||
|
"key": "samsung_errorlog_agree",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled applications errors reports",
|
||||||
|
"key": "send_action_app_error",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "enabled installation of non Google Play apps",
|
||||||
|
"key": "install_non_market_apps",
|
||||||
|
"safe_value": "0",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(AndroidArtifact):
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
for namespace, settings in self.results.items():
|
||||||
|
for key, value in settings.items():
|
||||||
|
for danger in ANDROID_DANGEROUS_SETTINGS:
|
||||||
|
# Check if one of the dangerous settings is using an unsafe
|
||||||
|
# value (different than the one specified).
|
||||||
|
if danger["key"] == key and danger["safe_value"] != value:
|
||||||
|
self.log.warning(
|
||||||
|
'Found suspicious "%s" setting "%s = %s" (%s)',
|
||||||
|
namespace,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
danger["description"],
|
||||||
|
)
|
||||||
|
break
|
||||||
@@ -9,11 +9,13 @@ import click
|
|||||||
|
|
||||||
from mvt.common.cmd_check_iocs import CmdCheckIOCS
|
from mvt.common.cmd_check_iocs import CmdCheckIOCS
|
||||||
from mvt.common.help import (
|
from mvt.common.help import (
|
||||||
|
HELP_MSG_ANDROID_BACKUP_PASSWORD,
|
||||||
HELP_MSG_FAST,
|
HELP_MSG_FAST,
|
||||||
HELP_MSG_HASHES,
|
HELP_MSG_HASHES,
|
||||||
HELP_MSG_IOC,
|
HELP_MSG_IOC,
|
||||||
HELP_MSG_LIST_MODULES,
|
HELP_MSG_LIST_MODULES,
|
||||||
HELP_MSG_MODULE,
|
HELP_MSG_MODULE,
|
||||||
|
HELP_MSG_NONINTERACTIVE,
|
||||||
HELP_MSG_OUTPUT,
|
HELP_MSG_OUTPUT,
|
||||||
HELP_MSG_SERIAL,
|
HELP_MSG_SERIAL,
|
||||||
HELP_MSG_VERBOSE,
|
HELP_MSG_VERBOSE,
|
||||||
@@ -30,10 +32,12 @@ from .cmd_download_apks import DownloadAPKs
|
|||||||
from .modules.adb import ADB_MODULES
|
from .modules.adb import ADB_MODULES
|
||||||
from .modules.adb.packages import Packages
|
from .modules.adb.packages import Packages
|
||||||
from .modules.backup import BACKUP_MODULES
|
from .modules.backup import BACKUP_MODULES
|
||||||
|
from .modules.backup.helpers import cli_load_android_backup_password
|
||||||
from .modules.bugreport import BUGREPORT_MODULES
|
from .modules.bugreport import BUGREPORT_MODULES
|
||||||
|
|
||||||
init_logging()
|
init_logging()
|
||||||
log = logging.getLogger("mvt")
|
log = logging.getLogger("mvt")
|
||||||
|
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|
||||||
|
|
||||||
@@ -125,7 +129,7 @@ def download_apks(ctx, all_apks, virustotal, output, from_file, serial, verbose)
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@cli.command(
|
@cli.command(
|
||||||
"check-adb",
|
"check-adb",
|
||||||
help="Check an Android device over adb",
|
help="Check an Android device over ADB",
|
||||||
context_settings=CONTEXT_SETTINGS,
|
context_settings=CONTEXT_SETTINGS,
|
||||||
)
|
)
|
||||||
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
|
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
|
||||||
@@ -141,16 +145,35 @@ def download_apks(ctx, all_apks, virustotal, output, from_file, serial, verbose)
|
|||||||
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
|
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||||
|
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
|
||||||
|
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_adb(ctx, serial, iocs, output, fast, list_modules, module, verbose):
|
def check_adb(
|
||||||
|
ctx,
|
||||||
|
serial,
|
||||||
|
iocs,
|
||||||
|
output,
|
||||||
|
fast,
|
||||||
|
list_modules,
|
||||||
|
module,
|
||||||
|
non_interactive,
|
||||||
|
backup_password,
|
||||||
|
verbose,
|
||||||
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose)
|
||||||
|
module_options = {
|
||||||
|
"fast_mode": fast,
|
||||||
|
"interactive": not non_interactive,
|
||||||
|
"backup_password": cli_load_android_backup_password(log, backup_password),
|
||||||
|
}
|
||||||
|
|
||||||
cmd = CmdAndroidCheckADB(
|
cmd = CmdAndroidCheckADB(
|
||||||
results_path=output,
|
results_path=output,
|
||||||
ioc_files=iocs,
|
ioc_files=iocs,
|
||||||
module_name=module,
|
module_name=module,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
fast_mode=fast,
|
module_options=module_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -232,14 +255,33 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
|
|||||||
)
|
)
|
||||||
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
|
||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
|
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
|
||||||
|
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
||||||
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
|
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_backup(ctx, iocs, output, list_modules, verbose, backup_path):
|
def check_backup(
|
||||||
|
ctx,
|
||||||
|
iocs,
|
||||||
|
output,
|
||||||
|
list_modules,
|
||||||
|
non_interactive,
|
||||||
|
backup_password,
|
||||||
|
verbose,
|
||||||
|
backup_path,
|
||||||
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose)
|
||||||
|
|
||||||
# Always generate hashes as backups are generally small.
|
# Always generate hashes as backups are generally small.
|
||||||
cmd = CmdAndroidCheckBackup(
|
cmd = CmdAndroidCheckBackup(
|
||||||
target_path=backup_path, results_path=output, ioc_files=iocs, hashes=True
|
target_path=backup_path,
|
||||||
|
results_path=output,
|
||||||
|
ioc_files=iocs,
|
||||||
|
hashes=True,
|
||||||
|
module_options={
|
||||||
|
"interactive": not non_interactive,
|
||||||
|
"backup_password": cli_load_android_backup_password(log, backup_password),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -277,19 +319,35 @@ def check_backup(ctx, iocs, output, list_modules, verbose, backup_path):
|
|||||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||||
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
|
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
|
||||||
|
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
|
||||||
|
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
|
||||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
||||||
@click.argument("ANDROIDQF_PATH", type=click.Path(exists=True))
|
@click.argument("ANDROIDQF_PATH", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_androidqf(
|
def check_androidqf(
|
||||||
ctx, iocs, output, list_modules, module, hashes, verbose, androidqf_path
|
ctx,
|
||||||
|
iocs,
|
||||||
|
output,
|
||||||
|
list_modules,
|
||||||
|
module,
|
||||||
|
hashes,
|
||||||
|
non_interactive,
|
||||||
|
backup_password,
|
||||||
|
verbose,
|
||||||
|
androidqf_path,
|
||||||
):
|
):
|
||||||
set_verbose_logging(verbose)
|
set_verbose_logging(verbose)
|
||||||
|
|
||||||
cmd = CmdAndroidCheckAndroidQF(
|
cmd = CmdAndroidCheckAndroidQF(
|
||||||
target_path=androidqf_path,
|
target_path=androidqf_path,
|
||||||
results_path=output,
|
results_path=output,
|
||||||
ioc_files=iocs,
|
ioc_files=iocs,
|
||||||
module_name=module,
|
module_name=module,
|
||||||
hashes=hashes,
|
hashes=hashes,
|
||||||
|
module_options={
|
||||||
|
"interactive": not non_interactive,
|
||||||
|
"backup_password": cli_load_android_backup_password(log, backup_password),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class CmdAndroidCheckADB(Command):
|
|||||||
ioc_files: Optional[list] = None,
|
ioc_files: Optional[list] = None,
|
||||||
module_name: Optional[str] = None,
|
module_name: Optional[str] = None,
|
||||||
serial: Optional[str] = None,
|
serial: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
@@ -29,7 +29,7 @@ class CmdAndroidCheckADB(Command):
|
|||||||
ioc_files=ioc_files,
|
ioc_files=ioc_files,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
log=log,
|
log=log,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
import os
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
|
|
||||||
@@ -21,7 +24,7 @@ class CmdAndroidCheckAndroidQF(Command):
|
|||||||
ioc_files: Optional[list] = None,
|
ioc_files: Optional[list] = None,
|
||||||
module_name: Optional[str] = None,
|
module_name: Optional[str] = None,
|
||||||
serial: Optional[str] = None,
|
serial: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
hashes: bool = False,
|
hashes: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -30,10 +33,35 @@ class CmdAndroidCheckAndroidQF(Command):
|
|||||||
ioc_files=ioc_files,
|
ioc_files=ioc_files,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
hashes=hashes,
|
hashes=hashes,
|
||||||
log=log,
|
log=log,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.name = "check-androidqf"
|
self.name = "check-androidqf"
|
||||||
self.modules = ANDROIDQF_MODULES
|
self.modules = ANDROIDQF_MODULES
|
||||||
|
|
||||||
|
self.format: Optional[str] = None
|
||||||
|
self.archive: Optional[zipfile.ZipFile] = None
|
||||||
|
self.files: List[str] = []
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
if os.path.isdir(self.target_path):
|
||||||
|
self.format = "dir"
|
||||||
|
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
||||||
|
target_abs_path = os.path.abspath(self.target_path)
|
||||||
|
for root, subdirs, subfiles in os.walk(target_abs_path):
|
||||||
|
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 module_init(self, module):
|
||||||
|
if self.format == "zip":
|
||||||
|
module.from_zip_file(self.archive, self.files)
|
||||||
|
else:
|
||||||
|
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
||||||
|
module.from_folder(parent_path, self.files)
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ import tarfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from rich.prompt import Prompt
|
|
||||||
|
|
||||||
from mvt.android.modules.backup.base import BackupExtraction
|
from mvt.android.modules.backup.base import BackupExtraction
|
||||||
|
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
|
||||||
from mvt.android.parsers.backup import (
|
from mvt.android.parsers.backup import (
|
||||||
AndroidBackupParsingError,
|
AndroidBackupParsingError,
|
||||||
InvalidBackupPassword,
|
InvalidBackupPassword,
|
||||||
@@ -35,7 +34,7 @@ class CmdAndroidCheckBackup(Command):
|
|||||||
ioc_files: Optional[list] = None,
|
ioc_files: Optional[list] = None,
|
||||||
module_name: Optional[str] = None,
|
module_name: Optional[str] = None,
|
||||||
serial: Optional[str] = None,
|
serial: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
hashes: bool = False,
|
hashes: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -44,7 +43,7 @@ class CmdAndroidCheckBackup(Command):
|
|||||||
ioc_files=ioc_files,
|
ioc_files=ioc_files,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
hashes=hashes,
|
hashes=hashes,
|
||||||
log=log,
|
log=log,
|
||||||
)
|
)
|
||||||
@@ -72,7 +71,12 @@ class CmdAndroidCheckBackup(Command):
|
|||||||
|
|
||||||
password = None
|
password = None
|
||||||
if header["encryption"] != "none":
|
if header["encryption"] != "none":
|
||||||
password = Prompt.ask("Enter backup password", password=True)
|
password = prompt_or_load_android_backup_password(
|
||||||
|
log, self.module_options
|
||||||
|
)
|
||||||
|
if not password:
|
||||||
|
log.critical("No backup password provided.")
|
||||||
|
sys.exit(1)
|
||||||
try:
|
try:
|
||||||
tardata = parse_backup_file(data, password=password)
|
tardata = parse_backup_file(data, password=password)
|
||||||
except InvalidBackupPassword:
|
except InvalidBackupPassword:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class CmdAndroidCheckBugreport(Command):
|
|||||||
ioc_files: Optional[list] = None,
|
ioc_files: Optional[list] = None,
|
||||||
module_name: Optional[str] = None,
|
module_name: Optional[str] = None,
|
||||||
serial: Optional[str] = None,
|
serial: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
hashes: bool = False,
|
hashes: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -34,7 +34,7 @@ class CmdAndroidCheckBugreport(Command):
|
|||||||
ioc_files=ioc_files,
|
ioc_files=ioc_files,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
hashes=hashes,
|
hashes=hashes,
|
||||||
log=log,
|
log=log,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ from adb_shell.exceptions import (
|
|||||||
UsbDeviceNotFoundError,
|
UsbDeviceNotFoundError,
|
||||||
UsbReadFailedError,
|
UsbReadFailedError,
|
||||||
)
|
)
|
||||||
from rich.prompt import Prompt
|
|
||||||
from usb1 import USBErrorAccess, USBErrorBusy
|
from usb1 import USBErrorAccess, USBErrorBusy
|
||||||
|
|
||||||
|
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
|
||||||
from mvt.android.parsers.backup import (
|
from mvt.android.parsers.backup import (
|
||||||
InvalidBackupPassword,
|
InvalidBackupPassword,
|
||||||
parse_ab_header,
|
parse_ab_header,
|
||||||
@@ -44,7 +44,7 @@ class AndroidExtraction(MVTModule):
|
|||||||
file_path: Optional[str] = None,
|
file_path: Optional[str] = None,
|
||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
log: logging.Logger = logging.getLogger(__name__),
|
||||||
results: Optional[list] = None,
|
results: Optional[list] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -52,7 +52,7 @@ class AndroidExtraction(MVTModule):
|
|||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
log=log,
|
log=log,
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
@@ -311,6 +311,12 @@ class AndroidExtraction(MVTModule):
|
|||||||
"You may need to set a backup password. \a"
|
"You may need to set a backup password. \a"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.module_options.get("backup_password", None):
|
||||||
|
self.log.warning(
|
||||||
|
"Backup password already set from command line or environment "
|
||||||
|
"variable. You should use the same password if enabling encryption!"
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over
|
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over
|
||||||
# the shell transport...
|
# the shell transport...
|
||||||
cmd = f"/system/bin/bu backup -nocompress '{package_name}' | base64"
|
cmd = f"/system/bin/bu backup -nocompress '{package_name}' | base64"
|
||||||
@@ -329,7 +335,12 @@ class AndroidExtraction(MVTModule):
|
|||||||
return parse_backup_file(backup_output, password=None)
|
return parse_backup_file(backup_output, password=None)
|
||||||
|
|
||||||
for _ in range(0, 3):
|
for _ in range(0, 3):
|
||||||
backup_password = Prompt.ask("Enter backup password", password=True)
|
backup_password = prompt_or_load_android_backup_password(
|
||||||
|
self.log, self.module_options
|
||||||
|
)
|
||||||
|
if not backup_password:
|
||||||
|
# Fail as no backup password loaded for this encrypted backup
|
||||||
|
self.log.critical("No backup password provided.")
|
||||||
try:
|
try:
|
||||||
decrypted_backup_tar = parse_backup_file(backup_output, backup_password)
|
decrypted_backup_tar = parse_backup_file(backup_output, backup_password)
|
||||||
return decrypted_backup_tar
|
return decrypted_backup_tar
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ChromeHistory(AndroidExtraction):
|
|||||||
file_path: Optional[str] = None,
|
file_path: Optional[str] = None,
|
||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
log: logging.Logger = logging.getLogger(__name__),
|
||||||
results: Optional[list] = None,
|
results: Optional[list] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -31,7 +31,7 @@ class ChromeHistory(AndroidExtraction):
|
|||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
log=log,
|
log=log,
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.android.parsers import parse_dumpsys_accessibility
|
from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
from .base import AndroidExtraction
|
||||||
|
|
||||||
|
|
||||||
class DumpsysAccessibility(AndroidExtraction):
|
class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidExtraction):
|
||||||
"""This module extracts stats on accessibility."""
|
"""This module extracts stats on accessibility."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -19,7 +19,7 @@ class DumpsysAccessibility(AndroidExtraction):
|
|||||||
file_path: Optional[str] = None,
|
file_path: Optional[str] = None,
|
||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
log: logging.Logger = logging.getLogger(__name__),
|
||||||
results: Optional[list] = None,
|
results: Optional[list] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -27,28 +27,17 @@ class DumpsysAccessibility(AndroidExtraction):
|
|||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
log=log,
|
log=log,
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
|
|
||||||
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 run(self) -> None:
|
def run(self) -> None:
|
||||||
self._adb_connect()
|
self._adb_connect()
|
||||||
output = self._adb_command("dumpsys accessibility")
|
output = self._adb_command("dumpsys accessibility")
|
||||||
self._adb_disconnect()
|
self._adb_disconnect()
|
||||||
|
|
||||||
self.results = parse_dumpsys_accessibility(output)
|
self.parse(output)
|
||||||
|
|
||||||
for result in self.results:
|
for result in self.results:
|
||||||
self.log.info(
|
self.log.info(
|
||||||
|
|||||||
@@ -6,12 +6,14 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.android.parsers import parse_dumpsys_activity_resolver_table
|
from mvt.android.artifacts.dumpsys_package_activities import (
|
||||||
|
DumpsysPackageActivitiesArtifact,
|
||||||
|
)
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
from .base import AndroidExtraction
|
||||||
|
|
||||||
|
|
||||||
class DumpsysActivities(AndroidExtraction):
|
class DumpsysActivities(DumpsysPackageActivitiesArtifact, AndroidExtraction):
|
||||||
"""This module extracts details on receivers for risky activities."""
|
"""This module extracts details on receivers for risky activities."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -19,7 +21,7 @@ class DumpsysActivities(AndroidExtraction):
|
|||||||
file_path: Optional[str] = None,
|
file_path: Optional[str] = None,
|
||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
log: logging.Logger = logging.getLogger(__name__),
|
||||||
results: Optional[list] = None,
|
results: Optional[list] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -27,30 +29,17 @@ class DumpsysActivities(AndroidExtraction):
|
|||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
log=log,
|
log=log,
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.results = results if results else {}
|
self.results = results if results else []
|
||||||
|
|
||||||
def check_indicators(self) -> None:
|
|
||||||
if not self.indicators:
|
|
||||||
return
|
|
||||||
|
|
||||||
for intent, activities in self.results.items():
|
|
||||||
for activity in activities:
|
|
||||||
ioc = self.indicators.check_app_id(activity["package_name"])
|
|
||||||
if ioc:
|
|
||||||
activity["matched_indicator"] = ioc
|
|
||||||
self.detected.append({intent: activity})
|
|
||||||
continue
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
self._adb_connect()
|
self._adb_connect()
|
||||||
output = self._adb_command("dumpsys package")
|
output = self._adb_command("dumpsys package")
|
||||||
self._adb_disconnect()
|
self._adb_disconnect()
|
||||||
|
self.parse(output)
|
||||||
|
|
||||||
self.results = parse_dumpsys_activity_resolver_table(output)
|
self.log.info("Extracted %d package activities", len(self.results))
|
||||||
|
|
||||||
self.log.info("Extracted activities for %d intents", len(self.results))
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Union
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.android.parsers.dumpsys import parse_dumpsys_appops
|
from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
from .base import AndroidExtraction
|
||||||
|
|
||||||
|
|
||||||
class DumpsysAppOps(AndroidExtraction):
|
class DumpsysAppOps(DumpsysAppopsArtifact, AndroidExtraction):
|
||||||
"""This module extracts records from App-op Manager."""
|
"""This module extracts records from App-op Manager."""
|
||||||
|
|
||||||
slug = "dumpsys_appops"
|
slug = "dumpsys_appops"
|
||||||
@@ -21,7 +21,7 @@ class DumpsysAppOps(AndroidExtraction):
|
|||||||
file_path: Optional[str] = None,
|
file_path: Optional[str] = None,
|
||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
log: logging.Logger = logging.getLogger(__name__),
|
||||||
results: Optional[list] = None,
|
results: Optional[list] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -29,56 +29,17 @@ class DumpsysAppOps(AndroidExtraction):
|
|||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
log=log,
|
log=log,
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
|
|
||||||
def serialize(self, record: dict) -> Union[dict, list]:
|
|
||||||
records = []
|
|
||||||
for perm in record["permissions"]:
|
|
||||||
if "entries" not in perm:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for entry in perm["entries"]:
|
|
||||||
if "timestamp" in entry:
|
|
||||||
records.append(
|
|
||||||
{
|
|
||||||
"timestamp": entry["timestamp"],
|
|
||||||
"module": self.__class__.__name__,
|
|
||||||
"event": entry["access"],
|
|
||||||
"data": f"{record['package_name']} access to "
|
|
||||||
f"{perm['name']}: {entry['access']}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return records
|
|
||||||
|
|
||||||
def check_indicators(self) -> None:
|
|
||||||
for result in self.results:
|
|
||||||
if self.indicators:
|
|
||||||
ioc = self.indicators.check_app_id(result.get("package_name"))
|
|
||||||
if ioc:
|
|
||||||
result["matched_indicator"] = ioc
|
|
||||||
self.detected.append(result)
|
|
||||||
continue
|
|
||||||
|
|
||||||
for perm in result["permissions"]:
|
|
||||||
if (
|
|
||||||
perm["name"] == "REQUEST_INSTALL_PACKAGES"
|
|
||||||
and perm["access"] == "allow"
|
|
||||||
):
|
|
||||||
self.log.info(
|
|
||||||
"Package %s with REQUEST_INSTALL_PACKAGES " "permission",
|
|
||||||
result["package_name"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
self._adb_connect()
|
self._adb_connect()
|
||||||
output = self._adb_command("dumpsys appops")
|
output = self._adb_command("dumpsys appops")
|
||||||
self._adb_disconnect()
|
self._adb_disconnect()
|
||||||
|
|
||||||
self.results = parse_dumpsys_appops(output)
|
self.parse(output)
|
||||||
|
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"Extracted a total of %d records from app-ops manager", len(self.results)
|
"Extracted a total of %d records from app-ops manager", len(self.results)
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Union
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.android.parsers import parse_dumpsys_battery_daily
|
from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
from .base import AndroidExtraction
|
||||||
|
|
||||||
|
|
||||||
class DumpsysBatteryDaily(AndroidExtraction):
|
class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, AndroidExtraction):
|
||||||
"""This module extracts records from battery daily updates."""
|
"""This module extracts records from battery daily updates."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -19,7 +19,7 @@ class DumpsysBatteryDaily(AndroidExtraction):
|
|||||||
file_path: Optional[str] = None,
|
file_path: Optional[str] = None,
|
||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
log: logging.Logger = logging.getLogger(__name__),
|
||||||
results: Optional[list] = None,
|
results: Optional[list] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -27,37 +27,17 @@ class DumpsysBatteryDaily(AndroidExtraction):
|
|||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
log=log,
|
log=log,
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
|
|
||||||
def serialize(self, record: dict) -> Union[dict, list]:
|
|
||||||
return {
|
|
||||||
"timestamp": record["from"],
|
|
||||||
"module": self.__class__.__name__,
|
|
||||||
"event": "battery_daily",
|
|
||||||
"data": f"Recorded update of package {record['package_name']} "
|
|
||||||
f"with vers {record['vers']}",
|
|
||||||
}
|
|
||||||
|
|
||||||
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 run(self) -> None:
|
def run(self) -> None:
|
||||||
self._adb_connect()
|
self._adb_connect()
|
||||||
output = self._adb_command("dumpsys batterystats --daily")
|
output = self._adb_command("dumpsys batterystats --daily")
|
||||||
self._adb_disconnect()
|
self._adb_disconnect()
|
||||||
|
|
||||||
self.results = parse_dumpsys_battery_daily(output)
|
self.parse(output)
|
||||||
|
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"Extracted %d records from battery daily stats", len(self.results)
|
"Extracted %d records from battery daily stats", len(self.results)
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.android.parsers import parse_dumpsys_battery_history
|
from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
from .base import AndroidExtraction
|
||||||
|
|
||||||
|
|
||||||
class DumpsysBatteryHistory(AndroidExtraction):
|
class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, AndroidExtraction):
|
||||||
"""This module extracts records from battery history events."""
|
"""This module extracts records from battery history events."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -19,7 +19,7 @@ class DumpsysBatteryHistory(AndroidExtraction):
|
|||||||
file_path: Optional[str] = None,
|
file_path: Optional[str] = None,
|
||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
fast_mode: bool = False,
|
module_options: Optional[dict] = None,
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
log: logging.Logger = logging.getLogger(__name__),
|
||||||
results: Optional[list] = None,
|
results: Optional[list] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -27,27 +27,16 @@ class DumpsysBatteryHistory(AndroidExtraction):
|
|||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
fast_mode=fast_mode,
|
module_options=module_options,
|
||||||
log=log,
|
log=log,
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
|
|
||||||
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 run(self) -> None:
|
def run(self) -> None:
|
||||||
self._adb_connect()
|
self._adb_connect()
|
||||||
output = self._adb_command("dumpsys batterystats --history")
|
output = self._adb_command("dumpsys batterystats --history")
|
||||||
self._adb_disconnect()
|
self._adb_disconnect()
|
||||||
|
|
||||||
self.results = parse_dumpsys_battery_history(output)
|
self.parse(output)
|
||||||
|
|
||||||
self.log.info("Extracted %d records from battery history", len(self.results))
|
self.log.info("Extracted %d records from battery history", len(self.results))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user