mirror of
https://github.com/mvt-project/mvt
synced 2025-11-13 01:37:36 +01:00
Compare commits
104 Commits
feature/io
...
auto/add-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
981371bd8b | ||
|
|
c7d00978c6 | ||
|
|
339a1d0712 | ||
|
|
7009cddc8c | ||
|
|
9b4d10139c | ||
|
|
b795ea3129 | ||
|
|
5be5ffbf49 | ||
|
|
2701490501 | ||
|
|
779842567d | ||
|
|
d3cc8cf590 | ||
|
|
b8a42eaf8f | ||
|
|
62b880fbff | ||
|
|
0778d448df | ||
|
|
f020655a1a | ||
|
|
91c34e6664 | ||
|
|
b4a8dd226a | ||
|
|
88213e12c9 | ||
|
|
f75b8e186a | ||
|
|
5babc1fcf3 | ||
|
|
b723ebf28e | ||
|
|
616e870212 | ||
|
|
847b0e087b | ||
|
|
86a0772eb2 | ||
|
|
7d0be9db4f | ||
|
|
4e120b2640 | ||
|
|
dbe9e5db9b | ||
|
|
0b00398729 | ||
|
|
87034d2c7a | ||
|
|
595a2f6536 | ||
|
|
8ead44a31e | ||
|
|
5c19d02a73 | ||
|
|
14ebc9ee4e | ||
|
|
de53cc07f8 | ||
|
|
22e066fc4a | ||
|
|
242052b8ec | ||
|
|
1df61b5bbf | ||
|
|
b691de2cc0 | ||
|
|
10915f250c | ||
|
|
c60cef4009 | ||
|
|
dda798df8e | ||
|
|
ffe6ad2014 | ||
|
|
a125b20fc5 | ||
|
|
49108e67e2 | ||
|
|
883b450601 | ||
|
|
ce813568ff | ||
|
|
93303f181a | ||
|
|
bee453a090 | ||
|
|
42106aa4d6 | ||
|
|
95076c8f71 | ||
|
|
c9ac12f336 | ||
|
|
486e3e7e9b | ||
|
|
be1fc3bd8b | ||
|
|
4757cff262 | ||
|
|
61f51caf31 | ||
|
|
511063fd0e | ||
|
|
88bc5672cb | ||
|
|
0fce0acf7a | ||
|
|
61f95d07d3 | ||
|
|
3dedd169c4 | ||
|
|
e34e03d3a3 | ||
|
|
34374699ce | ||
|
|
cf5aa7c89f | ||
|
|
2766739512 | ||
|
|
9c84afb4b0 | ||
|
|
80fc8bd879 | ||
|
|
ca41f7f106 | ||
|
|
55ddd86ad5 | ||
|
|
b184eeedf4 | ||
|
|
4e97e85350 | ||
|
|
e5865b166e | ||
|
|
a2dabb4267 | ||
|
|
b7595b62eb | ||
|
|
02c02ca15c | ||
|
|
6da33394fe | ||
|
|
086871e21d | ||
|
|
f32830c649 | ||
|
|
edcad488ab | ||
|
|
43901c96a0 | ||
|
|
0962383b46 | ||
|
|
34cd08fd9a | ||
|
|
579b53f7ec | ||
|
|
dbb80d6320 | ||
|
|
0fbf24e82a | ||
|
|
a2493baead | ||
|
|
0dc6228a59 | ||
|
|
6e230bdb6a | ||
|
|
2aa76c8a1c | ||
|
|
7d6dc9e6dc | ||
|
|
458195a0ab | ||
|
|
52e854b8b7 | ||
|
|
0f1eec3971 | ||
|
|
f4425865c0 | ||
|
|
28c0c86c4e | ||
|
|
154e6dab15 | ||
|
|
0c73e3e8fa | ||
|
|
9b5f2d89d5 | ||
|
|
3da61c8da8 | ||
|
|
5b2fe3baec | ||
|
|
9d81b5bfa8 | ||
|
|
8e895d3d07 | ||
|
|
bc09e2a394 | ||
|
|
2d0de088dd | ||
|
|
8694e7a047 | ||
|
|
9b41ba99aa |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.8', '3.9', '3.10'] # , '3.11']
|
python-version: ['3.10', '3.11', '3.12', '3.13']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
1
.github/workflows/update-ios-data.yml
vendored
1
.github/workflows/update-ios-data.yml
vendored
@@ -21,6 +21,7 @@ jobs:
|
|||||||
title: '[auto] Update iOS releases and versions'
|
title: '[auto] Update iOS releases and versions'
|
||||||
commit-message: Add new iOS versions and build numbers
|
commit-message: Add new iOS versions and build numbers
|
||||||
branch: auto/add-new-ios-releases
|
branch: auto/add-new-ios-releases
|
||||||
|
draft: true
|
||||||
body: |
|
body: |
|
||||||
This is an automated pull request to update the iOS releases and version numbers.
|
This is an automated pull request to update the iOS releases and version numbers.
|
||||||
add-paths: |
|
add-paths: |
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
|
|||||||
|
|
||||||
|
|
||||||
# Create main image
|
# Create main image
|
||||||
FROM ubuntu:22.04 as main
|
FROM ubuntu:24.04 as main
|
||||||
|
|
||||||
LABEL org.opencontainers.image.url="https://mvt.re"
|
LABEL org.opencontainers.image.url="https://mvt.re"
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
|
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
|
||||||
@@ -135,8 +135,7 @@ COPY --from=build-usbmuxd /build /
|
|||||||
COPY . mvt/
|
COPY . mvt/
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y git python3-pip \
|
&& apt-get install -y git python3-pip \
|
||||||
&& PIP_NO_CACHE_DIR=1 pip3 install --upgrade pip \
|
&& PIP_NO_CACHE_DIR=1 pip3 install --break-system-packages ./mvt \
|
||||||
&& PIP_NO_CACHE_DIR=1 pip3 install ./mvt \
|
|
||||||
&& apt-get remove -y python3-pip git && apt-get autoremove -y \
|
&& apt-get remove -y python3-pip git && apt-get autoremove -y \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& rm -rf mvt
|
&& rm -rf mvt
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -23,7 +23,12 @@ install:
|
|||||||
python3 -m pip install --upgrade -e .
|
python3 -m pip install --upgrade -e .
|
||||||
|
|
||||||
test-requirements:
|
test-requirements:
|
||||||
python3 -m pip install --upgrade -r test-requirements.txt
|
python3 -m pip install --upgrade --group dev
|
||||||
|
|
||||||
|
generate-proto-parsers:
|
||||||
|
# Generate python parsers for protobuf files
|
||||||
|
PROTO_FILES=$$(find src/mvt/android/parsers/proto/ -iname "*.proto"); \
|
||||||
|
protoc -Isrc/mvt/android/parsers/proto/ --python_betterproto_out=src/mvt/android/parsers/proto/ $$PROTO_FILES
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/src/mvt.egg-info
|
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/src/mvt.egg-info
|
||||||
|
|||||||
43
docs/command_completion.md
Normal file
43
docs/command_completion.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Command Completion
|
||||||
|
|
||||||
|
MVT utilizes the [Click](https://click.palletsprojects.com/en/stable/) library for creating its command line interface.
|
||||||
|
|
||||||
|
Click provides tab completion support for Bash (version 4.4 and up), Zsh, and Fish.
|
||||||
|
|
||||||
|
To enable it, you need to manually register a special function with your shell, which varies depending on the shell you are using.
|
||||||
|
|
||||||
|
The following describes how to generate the command completion scripts and add them to your shell configuration.
|
||||||
|
|
||||||
|
> **Note: You will need to start a new shell for the changes to take effect.**
|
||||||
|
|
||||||
|
### For Bash
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generates bash completion scripts
|
||||||
|
echo "$(_MVT_IOS_COMPLETE=bash_source mvt-ios)" > ~/.mvt-ios-complete.bash &&
|
||||||
|
echo "$(_MVT_ANDROID_COMPLETE=bash_source mvt-android)" > ~/.mvt-android-complete.bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the following to `~/.bashrc`:
|
||||||
|
```bash
|
||||||
|
# source mvt completion scripts
|
||||||
|
. ~/.mvt-ios-complete.bash && . ~/.mvt-android-complete.bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Zsh
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generates zsh completion scripts
|
||||||
|
echo "$(_MVT_IOS_COMPLETE=zsh_source mvt-ios)" > ~/.mvt-ios-complete.zsh &&
|
||||||
|
echo "$(_MVT_ANDROID_COMPLETE=zsh_source mvt-android)" > ~/.mvt-android-complete.zsh
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the following to `~/.zshrc`:
|
||||||
|
```bash
|
||||||
|
# source mvt completion scripts
|
||||||
|
. ~/.mvt-ios-complete.zsh && . ~/.mvt-android-complete.zsh
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information, visit the official [Click Docs](https://click.palletsprojects.com/en/stable/shell-completion/#enabling-completion).
|
||||||
|
|
||||||
|
|
||||||
@@ -98,3 +98,7 @@ You now should have the `mvt-ios` and `mvt-android` utilities installed.
|
|||||||
**Notes:**
|
**Notes:**
|
||||||
1. The `--force` flag is necessary to force the reinstallation of the package.
|
1. The `--force` flag is necessary to force the reinstallation of the package.
|
||||||
2. To revert to using a PyPI version, it will be necessary to `pipx uninstall mvt` first.
|
2. To revert to using a PyPI version, it will be necessary to `pipx uninstall mvt` first.
|
||||||
|
|
||||||
|
## Setting up command completions
|
||||||
|
|
||||||
|
See ["Command completions"](command_completion.md)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-autorefs==1.2.0
|
mkdocs-autorefs==1.4.3
|
||||||
mkdocs-material==9.5.42
|
mkdocs-material==9.6.20
|
||||||
mkdocs-material-extensions==1.3.1
|
mkdocs-material-extensions==1.3.1
|
||||||
mkdocstrings==0.23.0
|
mkdocstrings==0.30.1
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mvt"
|
name = "mvt"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
authors = [
|
authors = [{ name = "Claudio Guarnieri", email = "nex@nex.sx" }]
|
||||||
{name = "Claudio Guarnieri", email = "nex@nex.sx"}
|
|
||||||
]
|
|
||||||
maintainers = [
|
maintainers = [
|
||||||
{name = "Etienne Maynier", email = "tek@randhome.io"},
|
{ name = "Etienne Maynier", email = "tek@randhome.io" },
|
||||||
{name = "Donncha Ó Cearbhaill", email = "donncha.ocearbhaill@amnesty.org"},
|
{ name = "Donncha Ó Cearbhaill", email = "donncha.ocearbhaill@amnesty.org" },
|
||||||
{name = "Rory Flynn", email = "rory.flynn@amnesty.org"}
|
{ name = "Rory Flynn", email = "rory.flynn@amnesty.org" },
|
||||||
]
|
]
|
||||||
description = "Mobile Verification Toolkit"
|
description = "Mobile Verification Toolkit"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -16,44 +14,61 @@ classifiers = [
|
|||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Information Technology",
|
"Intended Audience :: Information Technology",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python"
|
"Programming Language :: Python",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"click >=8.1.3",
|
"click==8.2.1",
|
||||||
"rich >=12.6.0",
|
"rich==14.1.0",
|
||||||
"tld >=0.12.6",
|
"tld==0.13.1",
|
||||||
"requests >=2.28.1",
|
"requests==2.32.4",
|
||||||
"simplejson >=3.17.6",
|
"simplejson==3.20.1",
|
||||||
"packaging >=21.3",
|
"packaging==25.0",
|
||||||
"appdirs >=1.4.4",
|
"appdirs==1.4.4",
|
||||||
"iOSbackup >=0.9.923",
|
"iOSbackup==0.9.925",
|
||||||
"adb-shell[usb] >=0.4.3",
|
"adb-shell[usb]==0.4.4",
|
||||||
"libusb1 >=3.0.0",
|
"libusb1==3.3.1",
|
||||||
"cryptography >=42.0.5",
|
"cryptography==45.0.6",
|
||||||
"pyyaml >=6.0",
|
"PyYAML>=6.0.2",
|
||||||
"pyahocorasick >= 2.0.0",
|
"pyahocorasick==2.2.0",
|
||||||
|
"betterproto==1.2.5",
|
||||||
|
"pydantic==2.11.7",
|
||||||
|
"pydantic-settings==2.10.1",
|
||||||
|
"NSKeyedUnArchiver==1.5.2",
|
||||||
|
"python-dateutil==2.9.0.post0",
|
||||||
|
"tzdata==2025.2",
|
||||||
]
|
]
|
||||||
requires-python = ">= 3.8"
|
requires-python = ">= 3.10"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
homepage = "https://docs.mvt.re/en/latest/"
|
homepage = "https://docs.mvt.re/en/latest/"
|
||||||
repository = "https://github.com/mvt-project/mvt"
|
repository = "https://github.com/mvt-project/mvt"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mvt-ios = "mvt.ios:cli"
|
mvt-ios = "mvt.ios:cli"
|
||||||
mvt-android = "mvt.android:cli"
|
mvt-android = "mvt.android:cli"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"requests>=2.31.0",
|
||||||
|
"pytest>=7.4.3",
|
||||||
|
"pytest-cov>=4.1.0",
|
||||||
|
"pytest-github-actions-annotate-failures>=0.2.0",
|
||||||
|
"pytest-mock>=3.14.0",
|
||||||
|
"stix2>=3.0.1",
|
||||||
|
"ruff>=0.1.6",
|
||||||
|
"mypy>=1.7.1",
|
||||||
|
"betterproto[compiler]",
|
||||||
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0"]
|
requires = ["setuptools>=61.0"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
omit = [
|
omit = ["tests/*"]
|
||||||
"tests/*",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.coverage.html]
|
[tool.coverage.html]
|
||||||
directory= "htmlcov"
|
directory = "htmlcov"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
install_types = true
|
install_types = true
|
||||||
@@ -63,15 +78,13 @@ packages = "src"
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-ra -q --cov=mvt --cov-report html --junitxml=pytest.xml --cov-report=term-missing:skip-covered"
|
addopts = "-ra -q --cov=mvt --cov-report html --junitxml=pytest.xml --cov-report=term-missing:skip-covered"
|
||||||
testpaths = [
|
testpaths = ["tests"]
|
||||||
"tests"
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["C90", "E", "F", "W"] # flake8 default set
|
select = ["C90", "E", "F", "W"] # flake8 default set
|
||||||
ignore = [
|
ignore = [
|
||||||
"E501", # don't enforce line length violations
|
"E501", # don't enforce line length violations
|
||||||
"C901", # complex-structure
|
"C901", # complex-structure
|
||||||
|
|
||||||
# These were previously ignored but don't seem to be required:
|
# These were previously ignored but don't seem to be required:
|
||||||
# "E265", # no-space-after-block-comment
|
# "E265", # no-space-after-block-comment
|
||||||
@@ -83,14 +96,14 @@ ignore = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"__init__.py" = ["F401"] # unused-import
|
"__init__.py" = ["F401"] # unused-import
|
||||||
|
|
||||||
[tool.ruff.lint.mccabe]
|
[tool.ruff.lint.mccabe]
|
||||||
max-complexity = 10
|
max-complexity = 10
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
include-package-data = true
|
include-package-data = true
|
||||||
package-dir = {"" = "src"}
|
package-dir = { "" = "src" }
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
@@ -99,4 +112,4 @@ where = ["src"]
|
|||||||
mvt = ["ios/data/*.json"]
|
mvt = ["ios/data/*.json"]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {attr = "mvt.common.version.MVT_VERSION"}
|
version = { attr = "mvt.common.version.MVT_VERSION" }
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import binascii
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from .artifact import AndroidArtifact
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
|
||||||
class DumpsysADBArtifact(AndroidArtifact):
|
class DumpsysADBArtifact(AndroidArtifact):
|
||||||
multiline_fields = ["user_keys"]
|
multiline_fields = ["user_keys", "keystore"]
|
||||||
|
|
||||||
def indented_dump_parser(self, dump_data):
|
def indented_dump_parser(self, dump_data):
|
||||||
"""
|
"""
|
||||||
@@ -67,14 +68,38 @@ class DumpsysADBArtifact(AndroidArtifact):
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def parse_xml(self, xml_data):
|
||||||
|
"""
|
||||||
|
Parse XML data from dumpsys ADB output
|
||||||
|
"""
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
keystore = []
|
||||||
|
keystore_root = ET.fromstring(xml_data)
|
||||||
|
for adb_key in keystore_root.findall("adbKey"):
|
||||||
|
key_info = self.calculate_key_info(adb_key.get("key").encode("utf-8"))
|
||||||
|
key_info["last_connected"] = adb_key.get("lastConnection")
|
||||||
|
keystore.append(key_info)
|
||||||
|
|
||||||
|
return keystore
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calculate_key_info(user_key: bytes) -> str:
|
def calculate_key_info(user_key: bytes) -> str:
|
||||||
key_base64, user = user_key.split(b" ", 1)
|
if b" " in user_key:
|
||||||
key_raw = base64.b64decode(key_base64)
|
key_base64, user = user_key.split(b" ", 1)
|
||||||
key_fingerprint = hashlib.md5(key_raw).hexdigest().upper()
|
else:
|
||||||
key_fingerprint_colon = ":".join(
|
key_base64, user = user_key, b""
|
||||||
[key_fingerprint[i : i + 2] for i in range(0, len(key_fingerprint), 2)]
|
|
||||||
)
|
try:
|
||||||
|
key_raw = base64.b64decode(key_base64)
|
||||||
|
key_fingerprint = hashlib.md5(key_raw).hexdigest().upper()
|
||||||
|
key_fingerprint_colon = ":".join(
|
||||||
|
[key_fingerprint[i : i + 2] for i in range(0, len(key_fingerprint), 2)]
|
||||||
|
)
|
||||||
|
except binascii.Error:
|
||||||
|
# Impossible to parse base64
|
||||||
|
key_fingerprint_colon = ""
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"user": user.decode("utf-8"),
|
"user": user.decode("utf-8"),
|
||||||
"fingerprint": key_fingerprint_colon,
|
"fingerprint": key_fingerprint_colon,
|
||||||
@@ -115,8 +140,24 @@ class DumpsysADBArtifact(AndroidArtifact):
|
|||||||
if parsed.get("debugging_manager") is None:
|
if parsed.get("debugging_manager") is None:
|
||||||
self.log.error("Unable to find expected ADB entries in dumpsys output") # noqa
|
self.log.error("Unable to find expected ADB entries in dumpsys output") # noqa
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Keystore can be in different levels, as the basic parser
|
||||||
|
# is not always consistent due to different dumpsys formats.
|
||||||
|
if parsed.get("keystore"):
|
||||||
|
keystore_data = b"\n".join(parsed["keystore"])
|
||||||
|
elif parsed["debugging_manager"].get("keystore"):
|
||||||
|
keystore_data = b"\n".join(parsed["debugging_manager"]["keystore"])
|
||||||
else:
|
else:
|
||||||
parsed = parsed["debugging_manager"]
|
keystore_data = None
|
||||||
|
|
||||||
|
# Keystore is in XML format on some devices and we need to parse it
|
||||||
|
if keystore_data and keystore_data.startswith(b"<?xml"):
|
||||||
|
parsed["debugging_manager"]["keystore"] = self.parse_xml(keystore_data)
|
||||||
|
else:
|
||||||
|
# Keystore is not XML format
|
||||||
|
parsed["debugging_manager"]["keystore"] = keystore_data
|
||||||
|
|
||||||
|
parsed = parsed["debugging_manager"]
|
||||||
|
|
||||||
# Calculate key fingerprints for better readability
|
# Calculate key fingerprints for better readability
|
||||||
key_info = []
|
key_info = []
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ from mvt.common.utils import convert_datetime_to_iso
|
|||||||
from .artifact import AndroidArtifact
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
|
||||||
|
RISKY_PERMISSIONS = ["REQUEST_INSTALL_PACKAGES"]
|
||||||
|
RISKY_PACKAGES = ["com.android.shell"]
|
||||||
|
|
||||||
|
|
||||||
class DumpsysAppopsArtifact(AndroidArtifact):
|
class DumpsysAppopsArtifact(AndroidArtifact):
|
||||||
"""
|
"""
|
||||||
Parser for dumpsys app ops info
|
Parser for dumpsys app ops info
|
||||||
@@ -45,15 +49,39 @@ class DumpsysAppopsArtifact(AndroidArtifact):
|
|||||||
self.detected.append(result)
|
self.detected.append(result)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
detected_permissions = []
|
||||||
for perm in result["permissions"]:
|
for perm in result["permissions"]:
|
||||||
if (
|
if (
|
||||||
perm["name"] == "REQUEST_INSTALL_PACKAGES"
|
perm["name"] in RISKY_PERMISSIONS
|
||||||
and perm["access"] == "allow"
|
# and perm["access"] == "allow"
|
||||||
):
|
):
|
||||||
self.log.info(
|
detected_permissions.append(perm)
|
||||||
"Package %s with REQUEST_INSTALL_PACKAGES " "permission",
|
for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]):
|
||||||
result["package_name"],
|
self.log.warning(
|
||||||
)
|
"Package '%s' had risky permission '%s' set to '%s' at %s",
|
||||||
|
result["package_name"],
|
||||||
|
perm["name"],
|
||||||
|
entry["access"],
|
||||||
|
entry["timestamp"],
|
||||||
|
)
|
||||||
|
|
||||||
|
elif result["package_name"] in RISKY_PACKAGES:
|
||||||
|
detected_permissions.append(perm)
|
||||||
|
for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]):
|
||||||
|
self.log.warning(
|
||||||
|
"Risky package '%s' had '%s' permission set to '%s' at %s",
|
||||||
|
result["package_name"],
|
||||||
|
perm["name"],
|
||||||
|
entry["access"],
|
||||||
|
entry["timestamp"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if detected_permissions:
|
||||||
|
# We clean the result to only include the risky permission, otherwise the timeline
|
||||||
|
# will be polluted with all the other irrelevant permissions
|
||||||
|
cleaned_result = result.copy()
|
||||||
|
cleaned_result["permissions"] = detected_permissions
|
||||||
|
self.detected.append(cleaned_result)
|
||||||
|
|
||||||
def parse(self, output: str) -> None:
|
def parse(self, output: str) -> None:
|
||||||
self.results: List[Dict[str, Any]] = []
|
self.results: List[Dict[str, Any]] = []
|
||||||
@@ -121,11 +149,16 @@ class DumpsysAppopsArtifact(AndroidArtifact):
|
|||||||
if line.startswith(" "):
|
if line.startswith(" "):
|
||||||
# Permission entry like:
|
# Permission entry like:
|
||||||
# Reject: [fg-s]2021-05-19 22:02:52.054 (-314d1h25m2s33ms)
|
# Reject: [fg-s]2021-05-19 22:02:52.054 (-314d1h25m2s33ms)
|
||||||
|
access_type = line.split(":")[0].strip()
|
||||||
|
if access_type not in ["Access", "Reject"]:
|
||||||
|
# Skipping invalid access type. Some entries are not in the format we expect
|
||||||
|
continue
|
||||||
|
|
||||||
if entry:
|
if entry:
|
||||||
perm["entries"].append(entry)
|
perm["entries"].append(entry)
|
||||||
entry = {}
|
entry = {}
|
||||||
|
|
||||||
entry["access"] = line.split(":")[0].strip()
|
entry["access"] = access_type
|
||||||
entry["type"] = line[line.find("[") + 1 : line.find("]")]
|
entry["type"] = line[line.find("[") + 1 : line.find("]")]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
|
|||||||
for result in self.results:
|
for result in self.results:
|
||||||
if result["package_name"] in ROOT_PACKAGES:
|
if result["package_name"] in ROOT_PACKAGES:
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
"Found an installed package related to "
|
'Found an installed package related to rooting/jailbreaking: "%s"',
|
||||||
'rooting/jailbreaking: "%s"',
|
|
||||||
result["package_name"],
|
result["package_name"],
|
||||||
)
|
)
|
||||||
self.detected.append(result)
|
self.detected.append(result)
|
||||||
|
|||||||
42
src/mvt/android/artifacts/dumpsys_platform_compat.py
Normal file
42
src/mvt/android/artifacts/dumpsys_platform_compat.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
|
||||||
|
class DumpsysPlatformCompatArtifact(AndroidArtifact):
|
||||||
|
"""
|
||||||
|
Parser for uninstalled apps listed in platform_compat section.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
if not self.indicators:
|
||||||
|
return
|
||||||
|
|
||||||
|
for result in self.results:
|
||||||
|
ioc = self.indicators.check_app_id(result["package_name"])
|
||||||
|
if ioc:
|
||||||
|
result["matched_indicator"] = ioc
|
||||||
|
self.detected.append(result)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def parse(self, data: str) -> None:
|
||||||
|
for line in data.splitlines():
|
||||||
|
if not line.startswith("ChangeId(168419799; name=DOWNSCALED;"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.strip() == "":
|
||||||
|
break
|
||||||
|
|
||||||
|
# Look for rawOverrides field
|
||||||
|
if "rawOverrides={" in line:
|
||||||
|
# Extract the content inside the braces for rawOverrides
|
||||||
|
overrides_field = line.split("rawOverrides={", 1)[1].split("};", 1)[0]
|
||||||
|
|
||||||
|
for entry in overrides_field.split(", "):
|
||||||
|
# Extract app name
|
||||||
|
uninstall_app = entry.split("=")[0].strip()
|
||||||
|
|
||||||
|
self.results.append({"package_name": uninstall_app})
|
||||||
43
src/mvt/android/artifacts/file_timestamps.py
Normal file
43
src/mvt/android/artifacts/file_timestamps.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
|
||||||
|
class FileTimestampsArtifact(AndroidArtifact):
|
||||||
|
def serialize(self, record: dict) -> Union[dict, list]:
|
||||||
|
records = []
|
||||||
|
|
||||||
|
for ts in set(
|
||||||
|
[
|
||||||
|
record.get("access_time"),
|
||||||
|
record.get("changed_time"),
|
||||||
|
record.get("modified_time"),
|
||||||
|
]
|
||||||
|
):
|
||||||
|
if not ts:
|
||||||
|
continue
|
||||||
|
|
||||||
|
macb = ""
|
||||||
|
macb += "M" if ts == record.get("modified_time") else "-"
|
||||||
|
macb += "A" if ts == record.get("access_time") else "-"
|
||||||
|
macb += "C" if ts == record.get("changed_time") else "-"
|
||||||
|
macb += "-"
|
||||||
|
|
||||||
|
msg = record["path"]
|
||||||
|
if record.get("context"):
|
||||||
|
msg += f" ({record['context']})"
|
||||||
|
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
"timestamp": ts,
|
||||||
|
"module": self.__class__.__name__,
|
||||||
|
"event": macb,
|
||||||
|
"data": msg,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return records
|
||||||
@@ -42,6 +42,17 @@ class GetProp(AndroidArtifact):
|
|||||||
entry = {"name": matches[0][0], "value": matches[0][1]}
|
entry = {"name": matches[0][0], "value": matches[0][1]}
|
||||||
self.results.append(entry)
|
self.results.append(entry)
|
||||||
|
|
||||||
|
def get_device_timezone(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the device timezone from the getprop results
|
||||||
|
|
||||||
|
Used in other moduels to calculate the timezone offset
|
||||||
|
"""
|
||||||
|
for entry in self.results:
|
||||||
|
if entry["name"] == "persist.sys.timezone":
|
||||||
|
return entry["value"]
|
||||||
|
return None
|
||||||
|
|
||||||
def check_indicators(self) -> None:
|
def check_indicators(self) -> None:
|
||||||
for entry in self.results:
|
for entry in self.results:
|
||||||
if entry["name"] in INTERESTING_PROPERTIES:
|
if entry["name"] in INTERESTING_PROPERTIES:
|
||||||
|
|||||||
186
src/mvt/android/artifacts/mounts.py
Normal file
186
src/mvt/android/artifacts/mounts.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
SUSPICIOUS_MOUNT_POINTS = [
|
||||||
|
"/system",
|
||||||
|
"/vendor",
|
||||||
|
"/product",
|
||||||
|
"/system_ext",
|
||||||
|
]
|
||||||
|
|
||||||
|
SUSPICIOUS_OPTIONS = [
|
||||||
|
"rw",
|
||||||
|
"remount",
|
||||||
|
"noatime",
|
||||||
|
"nodiratime",
|
||||||
|
]
|
||||||
|
|
||||||
|
ALLOWLIST_NOATIME = [
|
||||||
|
"/system_dlkm",
|
||||||
|
"/system_ext",
|
||||||
|
"/product",
|
||||||
|
"/vendor",
|
||||||
|
"/vendor_dlkm",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Mounts(AndroidArtifact):
|
||||||
|
"""
|
||||||
|
This artifact parses mount information from /proc/mounts or similar mount data.
|
||||||
|
It can detect potentially suspicious mount configurations that may indicate
|
||||||
|
a rooted or compromised device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def parse(self, entry: str) -> None:
|
||||||
|
"""
|
||||||
|
Parse mount information from the provided entry.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
/dev/block/bootdevice/by-name/system /system ext4 ro,seclabel,relatime 0 0
|
||||||
|
/dev/block/dm-12 on / type ext4 (ro,seclabel,noatime)
|
||||||
|
"""
|
||||||
|
self.results: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for line in entry.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
device = None
|
||||||
|
mount_point = None
|
||||||
|
filesystem_type = None
|
||||||
|
mount_options = ""
|
||||||
|
|
||||||
|
if " on " in line and " type " in line:
|
||||||
|
try:
|
||||||
|
# Format: device on mount_point type filesystem_type (options)
|
||||||
|
device_part, rest = line.split(" on ", 1)
|
||||||
|
device = device_part.strip()
|
||||||
|
|
||||||
|
# Split by 'type' to get mount_point and filesystem info
|
||||||
|
mount_part, fs_part = rest.split(" type ", 1)
|
||||||
|
mount_point = mount_part.strip()
|
||||||
|
|
||||||
|
# Parse filesystem and options
|
||||||
|
if "(" in fs_part and fs_part.endswith(")"):
|
||||||
|
# Format: filesystem_type (options)
|
||||||
|
fs_and_opts = fs_part.strip()
|
||||||
|
paren_idx = fs_and_opts.find("(")
|
||||||
|
filesystem_type = fs_and_opts[:paren_idx].strip()
|
||||||
|
mount_options = fs_and_opts[paren_idx + 1 : -1].strip()
|
||||||
|
else:
|
||||||
|
# No options in parentheses, just filesystem type
|
||||||
|
filesystem_type = fs_part.strip()
|
||||||
|
mount_options = ""
|
||||||
|
|
||||||
|
# Skip if we don't have essential info
|
||||||
|
if not device or not mount_point or not filesystem_type:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse options into list
|
||||||
|
options_list = (
|
||||||
|
[opt.strip() for opt in mount_options.split(",") if opt.strip()]
|
||||||
|
if mount_options
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if it's a system partition
|
||||||
|
is_system_partition = mount_point in SUSPICIOUS_MOUNT_POINTS or any(
|
||||||
|
mount_point.startswith(sp) for sp in SUSPICIOUS_MOUNT_POINTS
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if it's mounted read-write
|
||||||
|
is_read_write = "rw" in options_list
|
||||||
|
|
||||||
|
mount_entry = {
|
||||||
|
"device": device,
|
||||||
|
"mount_point": mount_point,
|
||||||
|
"filesystem_type": filesystem_type,
|
||||||
|
"mount_options": mount_options,
|
||||||
|
"options_list": options_list,
|
||||||
|
"is_system_partition": is_system_partition,
|
||||||
|
"is_read_write": is_read_write,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.results.append(mount_entry)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
# If parsing fails, skip this line
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Skip lines that don't match expected format
|
||||||
|
continue
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
"""
|
||||||
|
Check for suspicious mount configurations that may indicate root access
|
||||||
|
or other security concerns.
|
||||||
|
"""
|
||||||
|
system_rw_mounts = []
|
||||||
|
suspicious_mounts = []
|
||||||
|
|
||||||
|
for mount in self.results:
|
||||||
|
mount_point = mount["mount_point"]
|
||||||
|
options = mount["options_list"]
|
||||||
|
|
||||||
|
# Check for system partitions mounted as read-write
|
||||||
|
if mount["is_system_partition"] and mount["is_read_write"]:
|
||||||
|
system_rw_mounts.append(mount)
|
||||||
|
if mount_point == "/system":
|
||||||
|
self.log.warning(
|
||||||
|
"Root detected /system partition is mounted as read-write (rw). "
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log.warning(
|
||||||
|
"System partition %s is mounted as read-write (rw). This may indicate system modifications.",
|
||||||
|
mount_point,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for other suspicious mount options
|
||||||
|
suspicious_opts = [opt for opt in options if opt in SUSPICIOUS_OPTIONS]
|
||||||
|
if suspicious_opts and mount["is_system_partition"]:
|
||||||
|
if (
|
||||||
|
"noatime" in mount["mount_options"]
|
||||||
|
and mount["mount_point"] in ALLOWLIST_NOATIME
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
suspicious_mounts.append(mount)
|
||||||
|
self.log.warning(
|
||||||
|
"Suspicious mount options found for %s: %s",
|
||||||
|
mount_point,
|
||||||
|
", ".join(suspicious_opts),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log interesting mount information
|
||||||
|
if mount_point == "/data" or mount_point.startswith("/sdcard"):
|
||||||
|
self.log.info(
|
||||||
|
"Data partition: %s mounted as %s with options: %s",
|
||||||
|
mount_point,
|
||||||
|
mount["filesystem_type"],
|
||||||
|
mount["mount_options"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log.info("Parsed %d mount entries", len(self.results))
|
||||||
|
|
||||||
|
# Check indicators if available
|
||||||
|
if not self.indicators:
|
||||||
|
return
|
||||||
|
|
||||||
|
for mount in self.results:
|
||||||
|
# Check if any mount points match indicators
|
||||||
|
ioc = self.indicators.check_file_path(mount.get("mount_point", ""))
|
||||||
|
if ioc:
|
||||||
|
mount["matched_indicator"] = ioc
|
||||||
|
self.detected.append(mount)
|
||||||
|
|
||||||
|
# Check device paths for indicators
|
||||||
|
ioc = self.indicators.check_file_path(mount.get("device", ""))
|
||||||
|
if ioc:
|
||||||
|
mount["matched_indicator"] = ioc
|
||||||
|
self.detected.append(mount)
|
||||||
@@ -16,6 +16,11 @@ ANDROID_DANGEROUS_SETTINGS = [
|
|||||||
"key": "package_verifier_enable",
|
"key": "package_verifier_enable",
|
||||||
"safe_value": "1",
|
"safe_value": "1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled APK package verification",
|
||||||
|
"key": "package_verifier_state",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "disabled Google Play Protect",
|
"description": "disabled Google Play Protect",
|
||||||
"key": "package_verifier_user_consent",
|
"key": "package_verifier_user_consent",
|
||||||
@@ -46,11 +51,6 @@ ANDROID_DANGEROUS_SETTINGS = [
|
|||||||
"key": "send_action_app_error",
|
"key": "send_action_app_error",
|
||||||
"safe_value": "1",
|
"safe_value": "1",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"description": "enabled installation of non Google Play apps",
|
|
||||||
"key": "install_non_market_apps",
|
|
||||||
"safe_value": "0",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "enabled accessibility services",
|
"description": "enabled accessibility services",
|
||||||
"key": "accessibility_enabled",
|
"key": "accessibility_enabled",
|
||||||
|
|||||||
268
src/mvt/android/artifacts/tombstone_crashes.py
Normal file
268
src/mvt/android/artifacts/tombstone_crashes.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,8 @@ from mvt.common.help import (
|
|||||||
HELP_MSG_HASHES,
|
HELP_MSG_HASHES,
|
||||||
HELP_MSG_CHECK_IOCS,
|
HELP_MSG_CHECK_IOCS,
|
||||||
HELP_MSG_STIX2,
|
HELP_MSG_STIX2,
|
||||||
|
HELP_MSG_DISABLE_UPDATE_CHECK,
|
||||||
|
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
||||||
)
|
)
|
||||||
from mvt.common.logo import logo
|
from mvt.common.logo import logo
|
||||||
from mvt.common.updates import IndicatorsUpdates
|
from mvt.common.updates import IndicatorsUpdates
|
||||||
@@ -53,12 +55,37 @@ log = logging.getLogger("mvt")
|
|||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_disable_flags(ctx):
|
||||||
|
"""Helper function to safely get disable flags from context."""
|
||||||
|
if ctx.obj is None:
|
||||||
|
return False, False
|
||||||
|
return (
|
||||||
|
ctx.obj.get("disable_version_check", False),
|
||||||
|
ctx.obj.get("disable_indicator_check", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Main
|
# Main
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@click.group(invoke_without_command=False)
|
@click.group(invoke_without_command=False)
|
||||||
def cli():
|
@click.option(
|
||||||
logo()
|
"--disable-update-check", is_flag=True, help=HELP_MSG_DISABLE_UPDATE_CHECK
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--disable-indicator-update-check",
|
||||||
|
is_flag=True,
|
||||||
|
help=HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, disable_update_check, disable_indicator_update_check):
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj["disable_version_check"] = disable_update_check
|
||||||
|
ctx.obj["disable_indicator_check"] = disable_indicator_update_check
|
||||||
|
logo(
|
||||||
|
disable_version_check=disable_update_check,
|
||||||
|
disable_indicator_check=disable_indicator_update_check,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -166,6 +193,8 @@ def check_adb(
|
|||||||
module_name=module,
|
module_name=module,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
module_options=module_options,
|
module_options=module_options,
|
||||||
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -212,6 +241,8 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
|
|||||||
ioc_files=iocs,
|
ioc_files=iocs,
|
||||||
module_name=module,
|
module_name=module,
|
||||||
hashes=True,
|
hashes=True,
|
||||||
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -274,6 +305,8 @@ def check_backup(
|
|||||||
"interactive": not non_interactive,
|
"interactive": not non_interactive,
|
||||||
"backup_password": cli_load_android_backup_password(log, backup_password),
|
"backup_password": cli_load_android_backup_password(log, backup_password),
|
||||||
},
|
},
|
||||||
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -338,6 +371,8 @@ def check_androidqf(
|
|||||||
"interactive": not non_interactive,
|
"interactive": not non_interactive,
|
||||||
"backup_password": cli_load_android_backup_password(log, backup_password),
|
"backup_password": cli_load_android_backup_password(log, backup_password),
|
||||||
},
|
},
|
||||||
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
)
|
)
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
@@ -372,7 +407,13 @@ def check_androidqf(
|
|||||||
@click.argument("FOLDER", type=click.Path(exists=True))
|
@click.argument("FOLDER", type=click.Path(exists=True))
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def check_iocs(ctx, iocs, list_modules, module, folder):
|
def check_iocs(ctx, iocs, list_modules, module, folder):
|
||||||
cmd = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module)
|
cmd = CmdCheckIOCS(
|
||||||
|
target_path=folder,
|
||||||
|
ioc_files=iocs,
|
||||||
|
module_name=module,
|
||||||
|
disable_version_check=_get_disable_flags(ctx)[0],
|
||||||
|
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||||
|
)
|
||||||
cmd.modules = BACKUP_MODULES + ADB_MODULES + BUGREPORT_MODULES
|
cmd.modules = BACKUP_MODULES + ADB_MODULES + BUGREPORT_MODULES
|
||||||
|
|
||||||
if list_modules:
|
if list_modules:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import logging
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
|
from mvt.common.indicators import Indicators
|
||||||
|
|
||||||
from .modules.adb import ADB_MODULES
|
from .modules.adb import ADB_MODULES
|
||||||
|
|
||||||
@@ -19,18 +20,28 @@ class CmdAndroidCheckADB(Command):
|
|||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
ioc_files: Optional[list] = None,
|
ioc_files: Optional[list] = None,
|
||||||
|
iocs: Optional[Indicators] = None,
|
||||||
module_name: Optional[str] = None,
|
module_name: Optional[str] = None,
|
||||||
serial: Optional[str] = None,
|
serial: Optional[str] = None,
|
||||||
module_options: Optional[dict] = None,
|
module_options: Optional[dict] = None,
|
||||||
|
hashes: Optional[bool] = False,
|
||||||
|
sub_command: Optional[bool] = False,
|
||||||
|
disable_version_check: bool = False,
|
||||||
|
disable_indicator_check: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
ioc_files=ioc_files,
|
ioc_files=ioc_files,
|
||||||
|
iocs=iocs,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
module_options=module_options,
|
module_options=module_options,
|
||||||
|
hashes=hashes,
|
||||||
|
sub_command=sub_command,
|
||||||
log=log,
|
log=log,
|
||||||
|
disable_version_check=disable_version_check,
|
||||||
|
disable_indicator_check=disable_indicator_check,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.name = "check-adb"
|
self.name = "check-adb"
|
||||||
|
|||||||
@@ -9,59 +9,186 @@ import zipfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from mvt.android.cmd_check_backup import CmdAndroidCheckBackup
|
||||||
|
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
|
from mvt.common.indicators import Indicators
|
||||||
|
|
||||||
from .modules.androidqf import ANDROIDQF_MODULES
|
from .modules.androidqf import ANDROIDQF_MODULES
|
||||||
|
from .modules.androidqf.base import AndroidQFModule
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NoAndroidQFTargetPath(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoAndroidQFBugReport(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoAndroidQFBackup(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CmdAndroidCheckAndroidQF(Command):
|
class CmdAndroidCheckAndroidQF(Command):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
ioc_files: Optional[list] = None,
|
ioc_files: Optional[list] = None,
|
||||||
|
iocs: Optional[Indicators] = None,
|
||||||
module_name: Optional[str] = None,
|
module_name: Optional[str] = None,
|
||||||
serial: Optional[str] = None,
|
serial: Optional[str] = None,
|
||||||
module_options: Optional[dict] = None,
|
module_options: Optional[dict] = None,
|
||||||
hashes: bool = False,
|
hashes: Optional[bool] = False,
|
||||||
|
sub_command: Optional[bool] = False,
|
||||||
|
disable_version_check: bool = False,
|
||||||
|
disable_indicator_check: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
ioc_files=ioc_files,
|
ioc_files=ioc_files,
|
||||||
|
iocs=iocs,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
module_options=module_options,
|
module_options=module_options,
|
||||||
hashes=hashes,
|
hashes=hashes,
|
||||||
|
sub_command=sub_command,
|
||||||
log=log,
|
log=log,
|
||||||
|
disable_version_check=disable_version_check,
|
||||||
|
disable_indicator_check=disable_indicator_check,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.name = "check-androidqf"
|
self.name = "check-androidqf"
|
||||||
self.modules = ANDROIDQF_MODULES
|
self.modules = ANDROIDQF_MODULES
|
||||||
|
|
||||||
self.format: Optional[str] = None
|
self.__format: Optional[str] = None
|
||||||
self.archive: Optional[zipfile.ZipFile] = None
|
self.__zip: Optional[zipfile.ZipFile] = None
|
||||||
self.files: List[str] = []
|
self.__files: List[str] = []
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
if os.path.isdir(self.target_path):
|
if os.path.isdir(self.target_path):
|
||||||
self.format = "dir"
|
self.__format = "dir"
|
||||||
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
||||||
target_abs_path = os.path.abspath(self.target_path)
|
target_abs_path = os.path.abspath(self.target_path)
|
||||||
for root, subdirs, subfiles in os.walk(target_abs_path):
|
for root, subdirs, subfiles in os.walk(target_abs_path):
|
||||||
for fname in subfiles:
|
for fname in subfiles:
|
||||||
file_path = os.path.relpath(os.path.join(root, fname), parent_path)
|
file_path = os.path.relpath(os.path.join(root, fname), parent_path)
|
||||||
self.files.append(file_path)
|
self.__files.append(file_path)
|
||||||
elif os.path.isfile(self.target_path):
|
elif os.path.isfile(self.target_path):
|
||||||
self.format = "zip"
|
self.__format = "zip"
|
||||||
self.archive = zipfile.ZipFile(self.target_path)
|
self.__zip = zipfile.ZipFile(self.target_path)
|
||||||
self.files = self.archive.namelist()
|
self.__files = self.__zip.namelist()
|
||||||
|
|
||||||
def module_init(self, module):
|
def module_init(self, module: AndroidQFModule) -> None: # type: ignore[override]
|
||||||
if self.format == "zip":
|
if self.__format == "zip" and self.__zip:
|
||||||
module.from_zip_file(self.archive, self.files)
|
module.from_zip(self.__zip, self.__files)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.target_path:
|
||||||
|
raise NoAndroidQFTargetPath
|
||||||
|
|
||||||
|
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
||||||
|
module.from_dir(parent_path, self.__files)
|
||||||
|
|
||||||
|
def load_bugreport(self) -> zipfile.ZipFile:
|
||||||
|
bugreport_zip_path = None
|
||||||
|
for file_name in self.__files:
|
||||||
|
if file_name.endswith("bugreport.zip"):
|
||||||
|
bugreport_zip_path = file_name
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
|
raise NoAndroidQFBugReport
|
||||||
|
|
||||||
|
if self.__format == "zip" and self.__zip:
|
||||||
|
handle = self.__zip.open(bugreport_zip_path)
|
||||||
|
return zipfile.ZipFile(handle)
|
||||||
|
|
||||||
|
if self.__format == "dir" and self.target_path:
|
||||||
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
||||||
module.from_folder(parent_path, self.files)
|
bug_report_path = os.path.join(parent_path, bugreport_zip_path)
|
||||||
|
return zipfile.ZipFile(bug_report_path)
|
||||||
|
|
||||||
|
raise NoAndroidQFBugReport
|
||||||
|
|
||||||
|
def load_backup(self) -> bytes:
|
||||||
|
backup_ab_path = None
|
||||||
|
for file_name in self.__files:
|
||||||
|
if file_name.endswith("backup.ab"):
|
||||||
|
backup_ab_path = file_name
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise NoAndroidQFBackup
|
||||||
|
|
||||||
|
if self.__format == "zip" and self.__zip:
|
||||||
|
backup_file_handle = self.__zip.open(backup_ab_path)
|
||||||
|
return backup_file_handle.read()
|
||||||
|
|
||||||
|
if self.__format == "dir" and self.target_path:
|
||||||
|
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
||||||
|
backup_path = os.path.join(parent_path, backup_ab_path)
|
||||||
|
with open(backup_path, "rb") as backup_file:
|
||||||
|
backup_ab_data = backup_file.read()
|
||||||
|
return backup_ab_data
|
||||||
|
|
||||||
|
raise NoAndroidQFBackup
|
||||||
|
|
||||||
|
def run_bugreport_cmd(self) -> bool:
|
||||||
|
try:
|
||||||
|
bugreport = self.load_bugreport()
|
||||||
|
except NoAndroidQFBugReport:
|
||||||
|
self.log.warning(
|
||||||
|
"Skipping bugreport modules as no bugreport.zip found in AndroidQF data."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
cmd = CmdAndroidCheckBugreport(
|
||||||
|
target_path=None,
|
||||||
|
results_path=self.results_path,
|
||||||
|
ioc_files=self.ioc_files,
|
||||||
|
iocs=self.iocs,
|
||||||
|
module_options=self.module_options,
|
||||||
|
hashes=self.hashes,
|
||||||
|
sub_command=True,
|
||||||
|
)
|
||||||
|
cmd.from_zip(bugreport)
|
||||||
|
cmd.run()
|
||||||
|
|
||||||
|
self.detected_count += cmd.detected_count
|
||||||
|
self.timeline.extend(cmd.timeline)
|
||||||
|
self.timeline_detected.extend(cmd.timeline_detected)
|
||||||
|
|
||||||
|
def run_backup_cmd(self) -> bool:
|
||||||
|
try:
|
||||||
|
backup = self.load_backup()
|
||||||
|
except NoAndroidQFBackup:
|
||||||
|
self.log.warning(
|
||||||
|
"Skipping backup modules as no backup.ab found in AndroidQF data."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
cmd = CmdAndroidCheckBackup(
|
||||||
|
target_path=None,
|
||||||
|
results_path=self.results_path,
|
||||||
|
ioc_files=self.ioc_files,
|
||||||
|
iocs=self.iocs,
|
||||||
|
module_options=self.module_options,
|
||||||
|
hashes=self.hashes,
|
||||||
|
sub_command=True,
|
||||||
|
)
|
||||||
|
cmd.from_ab(backup)
|
||||||
|
cmd.run()
|
||||||
|
|
||||||
|
self.detected_count += cmd.detected_count
|
||||||
|
self.timeline.extend(cmd.timeline)
|
||||||
|
self.timeline_detected.extend(cmd.timeline_detected)
|
||||||
|
|
||||||
|
def finish(self) -> None:
|
||||||
|
"""
|
||||||
|
Run the bugreport and backup modules if the respective files are found in the AndroidQF data.
|
||||||
|
"""
|
||||||
|
self.run_bugreport_cmd()
|
||||||
|
self.run_backup_cmd()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from mvt.android.parsers.backup import (
|
|||||||
parse_backup_file,
|
parse_backup_file,
|
||||||
)
|
)
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
|
from mvt.common.indicators import Indicators
|
||||||
|
|
||||||
from .modules.backup import BACKUP_MODULES
|
from .modules.backup import BACKUP_MODULES
|
||||||
|
|
||||||
@@ -32,20 +33,28 @@ class CmdAndroidCheckBackup(Command):
|
|||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
ioc_files: Optional[list] = None,
|
ioc_files: Optional[list] = None,
|
||||||
|
iocs: Optional[Indicators] = None,
|
||||||
module_name: Optional[str] = None,
|
module_name: Optional[str] = None,
|
||||||
serial: Optional[str] = None,
|
serial: Optional[str] = None,
|
||||||
module_options: Optional[dict] = None,
|
module_options: Optional[dict] = None,
|
||||||
hashes: bool = False,
|
hashes: Optional[bool] = False,
|
||||||
|
sub_command: Optional[bool] = False,
|
||||||
|
disable_version_check: bool = False,
|
||||||
|
disable_indicator_check: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
ioc_files=ioc_files,
|
ioc_files=ioc_files,
|
||||||
|
iocs=iocs,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
module_options=module_options,
|
module_options=module_options,
|
||||||
hashes=hashes,
|
hashes=hashes,
|
||||||
|
sub_command=sub_command,
|
||||||
log=log,
|
log=log,
|
||||||
|
disable_version_check=disable_version_check,
|
||||||
|
disable_indicator_check=disable_indicator_check,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.name = "check-backup"
|
self.name = "check-backup"
|
||||||
@@ -55,6 +64,34 @@ class CmdAndroidCheckBackup(Command):
|
|||||||
self.backup_archive: Optional[tarfile.TarFile] = None
|
self.backup_archive: Optional[tarfile.TarFile] = None
|
||||||
self.backup_files: List[str] = []
|
self.backup_files: List[str] = []
|
||||||
|
|
||||||
|
def from_ab(self, ab_file_bytes: bytes) -> None:
|
||||||
|
self.backup_type = "ab"
|
||||||
|
header = parse_ab_header(ab_file_bytes)
|
||||||
|
if not header["backup"]:
|
||||||
|
log.critical("Invalid backup format, file should be in .ab format")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
password = None
|
||||||
|
if header["encryption"] != "none":
|
||||||
|
password = prompt_or_load_android_backup_password(log, self.module_options)
|
||||||
|
if not password:
|
||||||
|
log.critical("No backup password provided.")
|
||||||
|
sys.exit(1)
|
||||||
|
try:
|
||||||
|
tardata = parse_backup_file(ab_file_bytes, password=password)
|
||||||
|
except InvalidBackupPassword:
|
||||||
|
log.critical("Invalid backup password")
|
||||||
|
sys.exit(1)
|
||||||
|
except AndroidBackupParsingError as exc:
|
||||||
|
log.critical("Impossible to parse this backup file: %s", exc)
|
||||||
|
log.critical("Please use Android Backup Extractor (ABE) instead")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
dbytes = io.BytesIO(tardata)
|
||||||
|
self.backup_archive = tarfile.open(fileobj=dbytes)
|
||||||
|
for member in self.backup_archive:
|
||||||
|
self.backup_files.append(member.name)
|
||||||
|
|
||||||
def init(self) -> None:
|
def init(self) -> None:
|
||||||
if not self.target_path:
|
if not self.target_path:
|
||||||
return
|
return
|
||||||
@@ -62,35 +99,8 @@ class CmdAndroidCheckBackup(Command):
|
|||||||
if os.path.isfile(self.target_path):
|
if os.path.isfile(self.target_path):
|
||||||
self.backup_type = "ab"
|
self.backup_type = "ab"
|
||||||
with open(self.target_path, "rb") as handle:
|
with open(self.target_path, "rb") as handle:
|
||||||
data = handle.read()
|
ab_file_bytes = handle.read()
|
||||||
|
self.from_ab(ab_file_bytes)
|
||||||
header = parse_ab_header(data)
|
|
||||||
if not header["backup"]:
|
|
||||||
log.critical("Invalid backup format, file should be in .ab format")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
password = None
|
|
||||||
if header["encryption"] != "none":
|
|
||||||
password = prompt_or_load_android_backup_password(
|
|
||||||
log, self.module_options
|
|
||||||
)
|
|
||||||
if not password:
|
|
||||||
log.critical("No backup password provided.")
|
|
||||||
sys.exit(1)
|
|
||||||
try:
|
|
||||||
tardata = parse_backup_file(data, password=password)
|
|
||||||
except InvalidBackupPassword:
|
|
||||||
log.critical("Invalid backup password")
|
|
||||||
sys.exit(1)
|
|
||||||
except AndroidBackupParsingError as exc:
|
|
||||||
log.critical("Impossible to parse this backup file: %s", exc)
|
|
||||||
log.critical("Please use Android Backup Extractor (ABE) instead")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
dbytes = io.BytesIO(tardata)
|
|
||||||
self.backup_archive = tarfile.open(fileobj=dbytes)
|
|
||||||
for member in self.backup_archive:
|
|
||||||
self.backup_files.append(member.name)
|
|
||||||
|
|
||||||
elif os.path.isdir(self.target_path):
|
elif os.path.isdir(self.target_path):
|
||||||
self.backup_type = "folder"
|
self.backup_type = "folder"
|
||||||
@@ -109,6 +119,6 @@ class CmdAndroidCheckBackup(Command):
|
|||||||
|
|
||||||
def module_init(self, module: BackupExtraction) -> None: # type: ignore[override]
|
def module_init(self, module: BackupExtraction) -> None: # type: ignore[override]
|
||||||
if self.backup_type == "folder":
|
if self.backup_type == "folder":
|
||||||
module.from_folder(self.target_path, self.backup_files)
|
module.from_dir(self.target_path, self.backup_files)
|
||||||
else:
|
else:
|
||||||
module.from_ab(self.target_path, self.backup_archive, self.backup_files)
|
module.from_ab(self.target_path, self.backup_archive, self.backup_files)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from zipfile import ZipFile
|
|||||||
|
|
||||||
from mvt.android.modules.bugreport.base import BugReportModule
|
from mvt.android.modules.bugreport.base import BugReportModule
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
|
from mvt.common.indicators import Indicators
|
||||||
|
|
||||||
from .modules.bugreport import BUGREPORT_MODULES
|
from .modules.bugreport import BUGREPORT_MODULES
|
||||||
|
|
||||||
@@ -23,54 +24,80 @@ class CmdAndroidCheckBugreport(Command):
|
|||||||
target_path: Optional[str] = None,
|
target_path: Optional[str] = None,
|
||||||
results_path: Optional[str] = None,
|
results_path: Optional[str] = None,
|
||||||
ioc_files: Optional[list] = None,
|
ioc_files: Optional[list] = None,
|
||||||
|
iocs: Optional[Indicators] = None,
|
||||||
module_name: Optional[str] = None,
|
module_name: Optional[str] = None,
|
||||||
serial: Optional[str] = None,
|
serial: Optional[str] = None,
|
||||||
module_options: Optional[dict] = None,
|
module_options: Optional[dict] = None,
|
||||||
hashes: bool = False,
|
hashes: Optional[bool] = False,
|
||||||
|
sub_command: Optional[bool] = False,
|
||||||
|
disable_version_check: bool = False,
|
||||||
|
disable_indicator_check: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
target_path=target_path,
|
target_path=target_path,
|
||||||
results_path=results_path,
|
results_path=results_path,
|
||||||
ioc_files=ioc_files,
|
ioc_files=ioc_files,
|
||||||
|
iocs=iocs,
|
||||||
module_name=module_name,
|
module_name=module_name,
|
||||||
serial=serial,
|
serial=serial,
|
||||||
module_options=module_options,
|
module_options=module_options,
|
||||||
hashes=hashes,
|
hashes=hashes,
|
||||||
|
sub_command=sub_command,
|
||||||
log=log,
|
log=log,
|
||||||
|
disable_version_check=disable_version_check,
|
||||||
|
disable_indicator_check=disable_indicator_check,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.name = "check-bugreport"
|
self.name = "check-bugreport"
|
||||||
self.modules = BUGREPORT_MODULES
|
self.modules = BUGREPORT_MODULES
|
||||||
|
|
||||||
self.bugreport_format: str = ""
|
self.__format: str = ""
|
||||||
self.bugreport_archive: Optional[ZipFile] = None
|
self.__zip: Optional[ZipFile] = None
|
||||||
self.bugreport_files: List[str] = []
|
self.__files: List[str] = []
|
||||||
|
|
||||||
|
def from_dir(self, dir_path: str) -> None:
|
||||||
|
"""This method is used to initialize the bug report analysis from an
|
||||||
|
uncompressed directory.
|
||||||
|
"""
|
||||||
|
self.__format = "dir"
|
||||||
|
self.target_path = dir_path
|
||||||
|
parent_path = Path(dir_path).absolute().as_posix()
|
||||||
|
for root, _, subfiles in os.walk(os.path.abspath(dir_path)):
|
||||||
|
for file_name in subfiles:
|
||||||
|
file_path = os.path.relpath(os.path.join(root, file_name), parent_path)
|
||||||
|
self.__files.append(file_path)
|
||||||
|
|
||||||
|
def from_zip(self, bugreport_zip: ZipFile) -> None:
|
||||||
|
"""This method is used to initialize the bug report analysis from a
|
||||||
|
compressed archive.
|
||||||
|
"""
|
||||||
|
# NOTE: This will be invoked either by the CLI directly,or by the
|
||||||
|
# check-androidqf command. We need this because we want to support
|
||||||
|
# check-androidqf to analyse compressed archives itself too.
|
||||||
|
# So, we'll need to extract bugreport.zip from a 'androidqf.zip', and
|
||||||
|
# since nothing is written on disk, we need to be able to pass this
|
||||||
|
# command a ZipFile instance in memory.
|
||||||
|
|
||||||
|
self.__format = "zip"
|
||||||
|
self.__zip = bugreport_zip
|
||||||
|
for file_name in self.__zip.namelist():
|
||||||
|
self.__files.append(file_name)
|
||||||
|
|
||||||
def init(self) -> None:
|
def init(self) -> None:
|
||||||
if not self.target_path:
|
if not self.target_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
if os.path.isfile(self.target_path):
|
if os.path.isfile(self.target_path):
|
||||||
self.bugreport_format = "zip"
|
self.from_zip(ZipFile(self.target_path))
|
||||||
self.bugreport_archive = ZipFile(self.target_path)
|
|
||||||
for file_name in self.bugreport_archive.namelist():
|
|
||||||
self.bugreport_files.append(file_name)
|
|
||||||
elif os.path.isdir(self.target_path):
|
elif os.path.isdir(self.target_path):
|
||||||
self.bugreport_format = "dir"
|
self.from_dir(self.target_path)
|
||||||
parent_path = Path(self.target_path).absolute().as_posix()
|
|
||||||
for root, _, subfiles in os.walk(os.path.abspath(self.target_path)):
|
|
||||||
for file_name in subfiles:
|
|
||||||
file_path = os.path.relpath(
|
|
||||||
os.path.join(root, file_name), parent_path
|
|
||||||
)
|
|
||||||
self.bugreport_files.append(file_path)
|
|
||||||
|
|
||||||
def module_init(self, module: BugReportModule) -> None: # type: ignore[override]
|
def module_init(self, module: BugReportModule) -> None: # type: ignore[override]
|
||||||
if self.bugreport_format == "zip":
|
if self.__format == "zip":
|
||||||
module.from_zip(self.bugreport_archive, self.bugreport_files)
|
module.from_zip(self.__zip, self.__files)
|
||||||
else:
|
else:
|
||||||
module.from_folder(self.target_path, self.bugreport_files)
|
module.from_dir(self.target_path, self.__files)
|
||||||
|
|
||||||
def finish(self) -> None:
|
def finish(self) -> None:
|
||||||
if self.bugreport_archive:
|
if self.__zip:
|
||||||
self.bugreport_archive.close()
|
self.__zip.close()
|
||||||
|
|||||||
@@ -4,15 +4,7 @@
|
|||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
from .chrome_history import ChromeHistory
|
from .chrome_history import ChromeHistory
|
||||||
from .dumpsys_accessibility import DumpsysAccessibility
|
|
||||||
from .dumpsys_activities import DumpsysActivities
|
|
||||||
from .dumpsys_appops import DumpsysAppOps
|
|
||||||
from .dumpsys_battery_daily import DumpsysBatteryDaily
|
|
||||||
from .dumpsys_battery_history import DumpsysBatteryHistory
|
|
||||||
from .dumpsys_dbinfo import DumpsysDBInfo
|
|
||||||
from .dumpsys_adbstate import DumpsysADBState
|
|
||||||
from .dumpsys_full import DumpsysFull
|
from .dumpsys_full import DumpsysFull
|
||||||
from .dumpsys_receivers import DumpsysReceivers
|
|
||||||
from .files import Files
|
from .files import Files
|
||||||
from .getprop import Getprop
|
from .getprop import Getprop
|
||||||
from .logcat import Logcat
|
from .logcat import Logcat
|
||||||
@@ -32,15 +24,7 @@ ADB_MODULES = [
|
|||||||
Getprop,
|
Getprop,
|
||||||
Settings,
|
Settings,
|
||||||
SELinuxStatus,
|
SELinuxStatus,
|
||||||
DumpsysBatteryHistory,
|
|
||||||
DumpsysBatteryDaily,
|
|
||||||
DumpsysReceivers,
|
|
||||||
DumpsysActivities,
|
|
||||||
DumpsysAccessibility,
|
|
||||||
DumpsysDBInfo,
|
|
||||||
DumpsysADBState,
|
|
||||||
DumpsysFull,
|
DumpsysFull,
|
||||||
DumpsysAppOps,
|
|
||||||
Packages,
|
Packages,
|
||||||
Logcat,
|
Logcat,
|
||||||
RootBinaries,
|
RootBinaries,
|
||||||
|
|||||||
@@ -326,8 +326,7 @@ class AndroidExtraction(MVTModule):
|
|||||||
|
|
||||||
if not header["backup"]:
|
if not header["backup"]:
|
||||||
self.log.error(
|
self.log.error(
|
||||||
"Extracting SMS via Android backup failed. "
|
"Extracting SMS via Android backup failed. No valid backup data found."
|
||||||
"No valid backup data found."
|
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact
|
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
|
||||||
|
|
||||||
|
|
||||||
class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidExtraction):
|
|
||||||
"""This module extracts stats on accessibility."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
file_path: Optional[str] = None,
|
|
||||||
target_path: Optional[str] = None,
|
|
||||||
results_path: Optional[str] = None,
|
|
||||||
module_options: Optional[dict] = None,
|
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
|
||||||
results: Optional[list] = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
file_path=file_path,
|
|
||||||
target_path=target_path,
|
|
||||||
results_path=results_path,
|
|
||||||
module_options=module_options,
|
|
||||||
log=log,
|
|
||||||
results=results,
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self._adb_connect()
|
|
||||||
output = self._adb_command("dumpsys accessibility")
|
|
||||||
self._adb_disconnect()
|
|
||||||
|
|
||||||
self.parse(output)
|
|
||||||
|
|
||||||
for result in self.results:
|
|
||||||
self.log.info(
|
|
||||||
'Found installed accessibility service "%s"', result.get("service")
|
|
||||||
)
|
|
||||||
|
|
||||||
self.log.info(
|
|
||||||
"Identified a total of %d accessibility services", len(self.results)
|
|
||||||
)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from mvt.android.artifacts.dumpsys_package_activities import (
|
|
||||||
DumpsysPackageActivitiesArtifact,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
|
||||||
|
|
||||||
|
|
||||||
class DumpsysActivities(DumpsysPackageActivitiesArtifact, AndroidExtraction):
|
|
||||||
"""This module extracts details on receivers for risky activities."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
file_path: Optional[str] = None,
|
|
||||||
target_path: Optional[str] = None,
|
|
||||||
results_path: Optional[str] = None,
|
|
||||||
module_options: Optional[dict] = None,
|
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
|
||||||
results: Optional[list] = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
file_path=file_path,
|
|
||||||
target_path=target_path,
|
|
||||||
results_path=results_path,
|
|
||||||
module_options=module_options,
|
|
||||||
log=log,
|
|
||||||
results=results,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.results = results if results else []
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self._adb_connect()
|
|
||||||
output = self._adb_command("dumpsys package")
|
|
||||||
self._adb_disconnect()
|
|
||||||
self.parse(output)
|
|
||||||
|
|
||||||
self.log.info("Extracted %d package activities", len(self.results))
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact
|
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
|
||||||
|
|
||||||
|
|
||||||
class DumpsysADBState(DumpsysADBArtifact, AndroidExtraction):
|
|
||||||
"""This module extracts ADB keystore state."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
file_path: Optional[str] = None,
|
|
||||||
target_path: Optional[str] = None,
|
|
||||||
results_path: Optional[str] = None,
|
|
||||||
module_options: Optional[dict] = None,
|
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
|
||||||
results: Optional[list] = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
file_path=file_path,
|
|
||||||
target_path=target_path,
|
|
||||||
results_path=results_path,
|
|
||||||
module_options=module_options,
|
|
||||||
log=log,
|
|
||||||
results=results,
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self._adb_connect()
|
|
||||||
output = self._adb_command("dumpsys adb", decode=False)
|
|
||||||
self._adb_disconnect()
|
|
||||||
|
|
||||||
self.parse(output)
|
|
||||||
if self.results:
|
|
||||||
self.log.info(
|
|
||||||
"Identified a total of %d trusted ADB keys",
|
|
||||||
len(self.results[0].get("user_keys", [])),
|
|
||||||
)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact
|
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
|
||||||
|
|
||||||
|
|
||||||
class DumpsysAppOps(DumpsysAppopsArtifact, AndroidExtraction):
|
|
||||||
"""This module extracts records from App-op Manager."""
|
|
||||||
|
|
||||||
slug = "dumpsys_appops"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
file_path: Optional[str] = None,
|
|
||||||
target_path: Optional[str] = None,
|
|
||||||
results_path: Optional[str] = None,
|
|
||||||
module_options: Optional[dict] = None,
|
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
|
||||||
results: Optional[list] = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
file_path=file_path,
|
|
||||||
target_path=target_path,
|
|
||||||
results_path=results_path,
|
|
||||||
module_options=module_options,
|
|
||||||
log=log,
|
|
||||||
results=results,
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self._adb_connect()
|
|
||||||
output = self._adb_command("dumpsys appops")
|
|
||||||
self._adb_disconnect()
|
|
||||||
|
|
||||||
self.parse(output)
|
|
||||||
|
|
||||||
self.log.info(
|
|
||||||
"Extracted a total of %d records from app-ops manager", len(self.results)
|
|
||||||
)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact
|
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
|
||||||
|
|
||||||
|
|
||||||
class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, AndroidExtraction):
|
|
||||||
"""This module extracts records from battery daily updates."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
file_path: Optional[str] = None,
|
|
||||||
target_path: Optional[str] = None,
|
|
||||||
results_path: Optional[str] = None,
|
|
||||||
module_options: Optional[dict] = None,
|
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
|
||||||
results: Optional[list] = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
file_path=file_path,
|
|
||||||
target_path=target_path,
|
|
||||||
results_path=results_path,
|
|
||||||
module_options=module_options,
|
|
||||||
log=log,
|
|
||||||
results=results,
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self._adb_connect()
|
|
||||||
output = self._adb_command("dumpsys batterystats --daily")
|
|
||||||
self._adb_disconnect()
|
|
||||||
|
|
||||||
self.parse(output)
|
|
||||||
|
|
||||||
self.log.info(
|
|
||||||
"Extracted %d records from battery daily stats", len(self.results)
|
|
||||||
)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact
|
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
|
||||||
|
|
||||||
|
|
||||||
class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, AndroidExtraction):
|
|
||||||
"""This module extracts records from battery history events."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
file_path: Optional[str] = None,
|
|
||||||
target_path: Optional[str] = None,
|
|
||||||
results_path: Optional[str] = None,
|
|
||||||
module_options: Optional[dict] = None,
|
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
|
||||||
results: Optional[list] = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
file_path=file_path,
|
|
||||||
target_path=target_path,
|
|
||||||
results_path=results_path,
|
|
||||||
module_options=module_options,
|
|
||||||
log=log,
|
|
||||||
results=results,
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self._adb_connect()
|
|
||||||
output = self._adb_command("dumpsys batterystats --history")
|
|
||||||
self._adb_disconnect()
|
|
||||||
|
|
||||||
self.parse(output)
|
|
||||||
|
|
||||||
self.log.info("Extracted %d records from battery history", len(self.results))
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact
|
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
|
||||||
|
|
||||||
|
|
||||||
class DumpsysReceivers(DumpsysReceiversArtifact, AndroidExtraction):
|
|
||||||
"""This module extracts details on receivers for risky activities."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
file_path: Optional[str] = None,
|
|
||||||
target_path: Optional[str] = None,
|
|
||||||
results_path: Optional[str] = None,
|
|
||||||
module_options: Optional[dict] = None,
|
|
||||||
log: logging.Logger = logging.getLogger(__name__),
|
|
||||||
results: Optional[list] = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
file_path=file_path,
|
|
||||||
target_path=target_path,
|
|
||||||
results_path=results_path,
|
|
||||||
module_options=module_options,
|
|
||||||
log=log,
|
|
||||||
results=results,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.results = results if results else {}
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
self._adb_connect()
|
|
||||||
|
|
||||||
output = self._adb_command("dumpsys package")
|
|
||||||
self.parse(output)
|
|
||||||
|
|
||||||
self._adb_disconnect()
|
|
||||||
self.log.info("Extracted receivers for %d intents", 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