1
mirror of https://github.com/mvt-project/mvt synced 2025-10-21 22:42:15 +02:00

Compare commits

..

47 Commits

Author SHA1 Message Date
tek
c719c4da1e Bumps version 2023-09-05 13:01:20 +02:00
tek
0f3e93c152 Adds missing iphone models 2023-09-05 12:53:19 +02:00
tek
a2ee46b8f8 Refactors dumpsys receiver parsing into an artifact 2023-08-08 20:23:09 +02:00
tek
e60e5fdc6e Refactors DumpsysBatteryHistory and adds related androidqf module 2023-08-04 19:20:14 +02:00
tek
7e0e071c5d Refactor DumpsysBatteryDaily module and add related artifact 2023-08-04 16:17:52 +02:00
Nex
b259db30f8 Added missing empty lines 2023-08-03 08:06:59 +02:00
Donncha Ó Cearbhaill
26f981244d Merge pull request #380 from a-sdi/patch-1
Update applications.py to add extra valid source
2023-08-02 20:03:30 +02:00
Donncha Ó Cearbhaill
2069e2b760 Fix style error (need space after # in comment) 2023-08-02 19:57:26 +02:00
a-sdi
355480414f Update applications.py
Some apps installed from apple store with sourceApp "com.apple.AppStore.ProductPageExtension"
2023-08-02 19:26:06 +03:00
tek
9a831b5930 Adds GlobalPreferences iOS module 2023-08-02 15:28:16 +02:00
tek
a103b50759 Rename artifacts to avoid name collisions 2023-08-02 13:32:58 +02:00
tek
84dc13144d Refactor DumpsysAppOps 2023-08-01 11:58:20 +02:00
tek
6356a4ff87 Refactor code of DumpsysDBInfo 2023-07-31 23:43:20 +02:00
tek
f96f2fe34a refactor dumpsys package activity code 2023-07-31 18:38:41 +02:00
Donncha Ó Cearbhaill
ae0e470c56 Fix inconsisent filesytem tests on some platforms 2023-07-31 11:45:53 +02:00
tek
4c175530a8 Refactor dumpsys accessibility in an artifact 2023-07-27 19:42:06 +02:00
Donncha Ó Cearbhaill
ecf75447aa Only add coverage comment to pull requests 2023-07-27 17:44:18 +02:00
tek
0389d335ed Bumps version 2023-07-26 18:20:25 +02:00
tek
7f9acec108 Move verbose indicator information to debug 2023-07-26 15:12:58 +02:00
Tek
3ec3b86a45 Adds support for zip files in check-androidqf command (#372) 2023-07-26 13:53:54 +02:00
Donncha Ó Cearbhaill
57d4aca72e Refactor Android modules to remove duplication (#368)
* Remove duplicated detection logic from GetProp modules
* Deduplicate settings and processes
* Refactor detection in artifacts
* Improves Artifact class
---------

Co-authored-by: tek <tek@randhome.io>
2023-07-26 13:42:17 +02:00
github-actions[bot]
1d740ad802 Add new iOS versions and build numbers (#373)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-07-25 10:21:08 +02:00
Donncha Ó Cearbhaill
15ce1b7e64 Merge pull request #370 from mvt-project/android-backup-refactor
Refactor Android backup password handling and add tests
2023-07-22 20:17:47 +02:00
Donncha Ó Cearbhaill
d6fca2f8ae Fix bugs with running ADB commands 2023-07-22 20:16:23 +02:00
Donncha Ó Cearbhaill
cabb679ff1 Merge branch 'main' into android-backup-refactor 2023-07-22 19:59:42 +02:00
Donncha Ó Cearbhaill
829a9f0cf6 Merge pull request #371 from mvt-project/add-coverage
Add code test coverage reporting using pytest-cov
2023-07-22 19:56:04 +02:00
Donncha Ó Cearbhaill
52e0176d5d Add code test coverage reporting 2023-07-22 19:54:01 +02:00
Donncha Ó Cearbhaill
8d8bdf26de Fix black style checks 2023-07-22 19:52:25 +02:00
Donncha Ó Cearbhaill
34fa77ae4d Add documentation for new options 2023-07-22 19:49:59 +02:00
Donncha Ó Cearbhaill
ed7d6fb847 Add integration tests for 'mvt-android check-backup' 2023-07-22 19:26:05 +02:00
Donncha Ó Cearbhaill
a2386dbdf7 Refactor Android backup password handling and add tests 2023-07-22 19:17:27 +02:00
Donncha Ó Cearbhaill
019cfbb84e Merge pull request #363 from aticu/main
Add option to disable interactivity and pass Android backup password on CLI
2023-07-22 16:44:35 +02:00
Donncha Ó Cearbhaill
3d924e22ec Merge branch 'release/v2.4.0' 2023-07-21 12:17:32 +02:00
Donncha Ó Cearbhaill
ca3c1bade4 Bump version to v2.4.0
Bumping the minor version as we introduce some backwards-incompatible
API changes to module definition in #367.
2023-07-21 12:14:31 +02:00
Donncha Ó Cearbhaill
85877fd3eb Merge pull request #369 from mvt-project/move-indicator-checking
Move detection and alerts from run() to check_indicators()
2023-07-21 12:12:36 +02:00
Donncha Ó Cearbhaill
8015ff78e8 Fix black error 2023-07-21 12:10:45 +02:00
Donncha Ó Cearbhaill
1a07b9a78f Move syntax checking before unit tests 2023-07-21 11:30:59 +02:00
Donncha Ó Cearbhaill
0b88de9867 Move detection and alerts from run() to check_indicators() 2023-07-21 11:29:12 +02:00
Niclas Schwarzlose
0edc9d7b81 Add option to disable interactivity 2023-07-19 11:29:51 +02:00
Donncha Ó Cearbhaill
76d7534b05 Fix bug recording detections in WebkitResourceLoadStatistics module 2023-07-18 18:02:42 +02:00
Donncha Ó Cearbhaill
ae2ab02347 Merge pull request #367 from mvt-project/refactor-module-options
Add a module_options parameter to pass data from CLI to modules
2023-07-17 19:07:41 +02:00
Donncha Ó Cearbhaill
e2c623c40f Move --fast flag from being a top-level MVT module parameter to an option in a new module_options parameter 2023-07-17 18:52:35 +02:00
Christian Clauss
a6e1a3de12 Add GitHub Annotions to ruff output (#364)
* Add GitHub Annotions to ruff output
* Upgrade GitHub Actions
* No Py3.11
2023-07-15 14:42:13 +02:00
tek
e7270d6a07 Fixes import and adds test for PR 361 2023-07-10 22:55:22 +02:00
Niclas Schwarzlose
1968a0fca2 Improve appops parsing in dumpsys (#361)
Without this change the package doesn't get properly reset when a new
user starts.

See for example in this excerpt:

```
 1 |    Package com.android.bluetooth:
 2 |      READ_CONTACTS (allow):
 3 |        null=[
 4 |          Access: [pers-s] 2022-04-22 13:24:17.577 (-277d5h22m53s447ms)
 5 |        ]
 6 |      WAKE_LOCK (allow):
 7 |        null=[
 8 |          Access: [pers-s] 2023-01-24 17:45:49.712 (-1m21s312ms) duration=+3ms
 9 |        ]
10 |      GET_USAGE_STATS (default):
11 |        null=[
12 |          Reject: [pers-s]2022-04-22 13:23:53.964 (-277d5h23m17s60ms)
13 |        ]
14 |      BLUETOOTH_CONNECT (allow):
15 |        null=[
16 |          Access: [pers-s] 2022-04-22 13:23:53.988 (-277d5h23m17s36ms)
17 |        ]
18 |  Uid 1027:
19 |    state=pers
20 |    capability=LCMN
21 |    appWidgetVisible=false
22 |      LEGACY_STORAGE: mode=ignore
23 |    Package com.android.nfc:
24 |      WAKE_LOCK (allow):
25 |        null=[
26 |          Access: [pers-s] 2022-04-22 13:23:54.633 (-277d5h23m16s391ms) duration=+1s73ms
27 |        ]
```

Here the package "com.android.bluetooth" is not reset when in line 18,
so when "LEGACY_STORAGE:" in line 22 is encountered, it's added as
another permission to "com.android.bluetooth" with "access" set to
"ode=igno".

This PR fixes that by resetting the package whenever a new Uid is
encountered.

Co-authored-by: Niclas Schwarzlose <niclas.schwarzlose@reporter-ohne-grenzen.de>
2023-07-10 22:53:58 +02:00
Donncha Ó Cearbhaill
46cc54df74 Add information about public indicators and support avenues to documentation 2023-06-30 19:43:30 +02:00
Donncha Ó Cearbhaill
7046ff80d1 Add SMS read time in the MVT logs 2023-06-30 19:30:50 +02:00
172 changed files with 2945 additions and 1780 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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 Nows Digital Security Helpline](https://www.accessnow.org/help/).
More information about using indicators of compromise with MVT is available in the [documentation](iocs.md).
## 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.

View File

@@ -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"

View 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/

View 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)

View 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,
}
)

View 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)

View 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)

View 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,
}
)

View 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,
}
)

View 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,
}
)

View 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,
}
)

View 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)

View 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)

View 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

View File

@@ -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:

View File

@@ -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,
) )

View File

@@ -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)

View File

@@ -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:

View File

@@ -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,
) )

View File

@@ -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

View File

@@ -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,
) )

View File

@@ -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(

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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