mirror of
https://github.com/mvt-project/mvt.git
synced 2026-02-15 18:02:44 +00:00
Compare commits
3 Commits
mounts
...
feature/an
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82f5e5c627 | ||
|
|
2bb613fe09 | ||
|
|
355850bd5c |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
|||||||
# 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"
|
|
||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.10', '3.11', '3.12', '3.13']
|
python-version: ['3.8', '3.9', '3.10'] # , '3.11']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
with:
|
with:
|
||||||
pytest-coverage-path: ./pytest-coverage.txt
|
pytest-coverage-path: ./pytest-coverage.txt
|
||||||
junitxml-path: ./pytest.xml
|
junitxml-path: ./pytest.xml
|
||||||
1
.github/workflows/update-ios-data.yml
vendored
1
.github/workflows/update-ios-data.yml
vendored
@@ -21,7 +21,6 @@ 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:24.04 as main
|
FROM ubuntu:22.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,7 +135,8 @@ 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 --break-system-packages ./mvt \
|
&& PIP_NO_CACHE_DIR=1 pip3 install --upgrade pip \
|
||||||
|
&& 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
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -23,7 +23,7 @@ install:
|
|||||||
python3 -m pip install --upgrade -e .
|
python3 -m pip install --upgrade -e .
|
||||||
|
|
||||||
test-requirements:
|
test-requirements:
|
||||||
python3 -m pip install --upgrade --group dev
|
python3 -m pip install --upgrade -r test-requirements.txt
|
||||||
|
|
||||||
generate-proto-parsers:
|
generate-proto-parsers:
|
||||||
# Generate python parsers for protobuf files
|
# Generate python parsers for protobuf files
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-autorefs==1.4.3
|
mkdocs-autorefs==1.2.0
|
||||||
mkdocs-material==9.6.20
|
mkdocs-material==9.5.42
|
||||||
mkdocs-material-extensions==1.3.1
|
mkdocs-material-extensions==1.3.1
|
||||||
mkdocstrings==0.30.1
|
mkdocstrings==0.23.0
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mvt"
|
name = "mvt"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
authors = [{ name = "Claudio Guarnieri", email = "nex@nex.sx" }]
|
authors = [
|
||||||
|
{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"
|
||||||
@@ -14,61 +16,48 @@ 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.2.1",
|
"click >=8.1.3",
|
||||||
"rich==14.1.0",
|
"rich >=12.6.0",
|
||||||
"tld==0.13.1",
|
"tld >=0.12.6",
|
||||||
"requests==2.32.4",
|
"requests >=2.28.1",
|
||||||
"simplejson==3.20.1",
|
"simplejson >=3.17.6",
|
||||||
"packaging==25.0",
|
"packaging >=21.3",
|
||||||
"appdirs==1.4.4",
|
"appdirs >=1.4.4",
|
||||||
"iOSbackup==0.9.925",
|
"iOSbackup >=0.9.923",
|
||||||
"adb-shell[usb]==0.4.4",
|
"adb-shell[usb] >=0.4.3",
|
||||||
"libusb1==3.3.1",
|
"libusb1 >=3.0.0",
|
||||||
"cryptography==45.0.6",
|
"cryptography >=42.0.5",
|
||||||
"PyYAML>=6.0.2",
|
"pyyaml >=6.0",
|
||||||
"pyahocorasick==2.2.0",
|
"pyahocorasick >= 2.0.0",
|
||||||
"betterproto==1.2.5",
|
"betterproto >=1.2.0",
|
||||||
"pydantic==2.11.7",
|
"pydantic >= 2.10.0",
|
||||||
"pydantic-settings==2.10.1",
|
"pydantic-settings >= 2.7.0",
|
||||||
"NSKeyedUnArchiver==1.5.2",
|
'backports.zoneinfo; python_version < "3.9"',
|
||||||
"python-dateutil==2.9.0.post0",
|
|
||||||
"tzdata==2025.2",
|
|
||||||
]
|
]
|
||||||
requires-python = ">= 3.10"
|
requires-python = ">= 3.8"
|
||||||
|
|
||||||
[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 = ["tests/*"]
|
omit = [
|
||||||
|
"tests/*",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.coverage.html]
|
[tool.coverage.html]
|
||||||
directory = "htmlcov"
|
directory= "htmlcov"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
install_types = true
|
install_types = true
|
||||||
@@ -78,13 +67,15 @@ 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 = ["tests"]
|
testpaths = [
|
||||||
|
"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
|
||||||
@@ -96,14 +87,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"]
|
||||||
@@ -112,4 +103,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,14 +4,13 @@
|
|||||||
# 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", "keystore"]
|
multiline_fields = ["user_keys"]
|
||||||
|
|
||||||
def indented_dump_parser(self, dump_data):
|
def indented_dump_parser(self, dump_data):
|
||||||
"""
|
"""
|
||||||
@@ -68,38 +67,14 @@ 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:
|
||||||
if b" " in user_key:
|
key_base64, user = user_key.split(b" ", 1)
|
||||||
key_base64, user = user_key.split(b" ", 1)
|
key_raw = base64.b64decode(key_base64)
|
||||||
else:
|
key_fingerprint = hashlib.md5(key_raw).hexdigest().upper()
|
||||||
key_base64, user = user_key, b""
|
key_fingerprint_colon = ":".join(
|
||||||
|
[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,
|
||||||
@@ -140,24 +115,8 @@ 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:
|
||||||
keystore_data = None
|
parsed = parsed["debugging_manager"]
|
||||||
|
|
||||||
# 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 = []
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
from 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)
|
|
||||||
@@ -51,6 +51,11 @@ 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",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from typing import List, Optional, Union
|
|||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
import betterproto
|
import betterproto
|
||||||
from dateutil import parser
|
|
||||||
|
|
||||||
from mvt.common.utils import convert_datetime_to_iso
|
from mvt.common.utils import convert_datetime_to_iso
|
||||||
from mvt.android.parsers.proto.tombstone import Tombstone
|
from mvt.android.parsers.proto.tombstone import Tombstone
|
||||||
@@ -53,7 +52,7 @@ class TombstoneCrashResult(pydantic.BaseModel):
|
|||||||
file_name: str
|
file_name: str
|
||||||
file_timestamp: str # We store the timestamp as a string to avoid timezone issues
|
file_timestamp: str # We store the timestamp as a string to avoid timezone issues
|
||||||
build_fingerprint: str
|
build_fingerprint: str
|
||||||
revision: str
|
revision: int
|
||||||
arch: Optional[str] = None
|
arch: Optional[str] = None
|
||||||
timestamp: str # We store the timestamp as a string to avoid timezone issues
|
timestamp: str # We store the timestamp as a string to avoid timezone issues
|
||||||
process_uptime: Optional[int] = None
|
process_uptime: Optional[int] = None
|
||||||
@@ -63,14 +62,14 @@ class TombstoneCrashResult(pydantic.BaseModel):
|
|||||||
process_name: Optional[str] = None
|
process_name: Optional[str] = None
|
||||||
binary_path: Optional[str] = None
|
binary_path: Optional[str] = None
|
||||||
selinux_label: Optional[str] = None
|
selinux_label: Optional[str] = None
|
||||||
uid: int
|
uid: Optional[int] = None
|
||||||
signal_info: SignalInfo
|
signal_info: SignalInfo
|
||||||
cause: Optional[str] = None
|
cause: Optional[str] = None
|
||||||
extra: Optional[str] = None
|
extra: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class TombstoneCrashArtifact(AndroidArtifact):
|
class TombstoneCrashArtifact(AndroidArtifact):
|
||||||
"""
|
""" "
|
||||||
Parser for Android tombstone crash files.
|
Parser for Android tombstone crash files.
|
||||||
|
|
||||||
This parser can parse both text and protobuf tombstone crash files.
|
This parser can parse both text and protobuf tombstone crash files.
|
||||||
@@ -121,11 +120,11 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
|||||||
def parse_protobuf(
|
def parse_protobuf(
|
||||||
self, file_name: str, file_timestamp: datetime.datetime, data: bytes
|
self, file_name: str, file_timestamp: datetime.datetime, data: bytes
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Parse Android tombstone crash files from a protobuf object."""
|
"""
|
||||||
|
Parse Android tombstone crash files from a protobuf object.
|
||||||
|
"""
|
||||||
tombstone_pb = Tombstone().parse(data)
|
tombstone_pb = Tombstone().parse(data)
|
||||||
tombstone_dict = tombstone_pb.to_dict(
|
tombstone_dict = tombstone_pb.to_dict(betterproto.Casing.SNAKE)
|
||||||
betterproto.Casing.SNAKE, include_default_values=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add some extra metadata
|
# Add some extra metadata
|
||||||
tombstone_dict["timestamp"] = self._parse_timestamp_string(
|
tombstone_dict["timestamp"] = self._parse_timestamp_string(
|
||||||
@@ -142,23 +141,21 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
|||||||
def parse(
|
def parse(
|
||||||
self, file_name: str, file_timestamp: datetime.datetime, content: bytes
|
self, file_name: str, file_timestamp: datetime.datetime, content: bytes
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Parse text Android tombstone crash files."""
|
"""
|
||||||
|
Parse text Android tombstone crash files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Split the tombstone file into a dictonary
|
||||||
tombstone_dict = {
|
tombstone_dict = {
|
||||||
"file_name": file_name,
|
"file_name": file_name,
|
||||||
"file_timestamp": convert_datetime_to_iso(file_timestamp),
|
"file_timestamp": convert_datetime_to_iso(file_timestamp),
|
||||||
}
|
}
|
||||||
lines = content.decode("utf-8").splitlines()
|
lines = content.decode("utf-8").splitlines()
|
||||||
for line_num, line in enumerate(lines, 1):
|
for line in lines:
|
||||||
if not line.strip() or TOMBSTONE_DELIMITER in line:
|
if not line.strip() or TOMBSTONE_DELIMITER in line:
|
||||||
continue
|
continue
|
||||||
try:
|
for key, destination_key in TOMBSTONE_TEXT_KEY_MAPPINGS.items():
|
||||||
for key, destination_key in TOMBSTONE_TEXT_KEY_MAPPINGS.items():
|
self._parse_tombstone_line(line, key, destination_key, tombstone_dict)
|
||||||
if self._parse_tombstone_line(
|
|
||||||
line, key, destination_key, tombstone_dict
|
|
||||||
):
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"Error parsing line {line_num}: {str(e)}")
|
|
||||||
|
|
||||||
# Validate the tombstone and add it to the results
|
# Validate the tombstone and add it to the results
|
||||||
tombstone = TombstoneCrashResult.model_validate(tombstone_dict)
|
tombstone = TombstoneCrashResult.model_validate(tombstone_dict)
|
||||||
@@ -168,7 +165,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
|||||||
self, line: str, key: str, destination_key: str, tombstone: dict
|
self, line: str, key: str, destination_key: str, tombstone: dict
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not line.startswith(f"{key}"):
|
if not line.startswith(f"{key}"):
|
||||||
return False
|
return None
|
||||||
|
|
||||||
if key == "pid":
|
if key == "pid":
|
||||||
return self._load_pid_line(line, tombstone)
|
return self._load_pid_line(line, tombstone)
|
||||||
@@ -187,7 +184,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
|||||||
raise ValueError(f"Expected key {key}, got {line_key}")
|
raise ValueError(f"Expected key {key}, got {line_key}")
|
||||||
|
|
||||||
value_clean = value.strip().strip("'")
|
value_clean = value.strip().strip("'")
|
||||||
if destination_key == "uid":
|
if destination_key in ["uid", "revision"]:
|
||||||
tombstone[destination_key] = int(value_clean)
|
tombstone[destination_key] = int(value_clean)
|
||||||
elif destination_key == "process_uptime":
|
elif destination_key == "process_uptime":
|
||||||
# eg. "Process uptime: 40s"
|
# eg. "Process uptime: 40s"
|
||||||
@@ -200,50 +197,51 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _load_pid_line(self, line: str, tombstone: dict) -> bool:
|
def _load_pid_line(self, line: str, tombstone: dict) -> bool:
|
||||||
try:
|
pid_part, tid_part, name_part = [part.strip() for part in line.split(",")]
|
||||||
parts = line.split(" >>> ") if " >>> " in line else line.split(">>>")
|
|
||||||
process_info = parts[0]
|
|
||||||
|
|
||||||
# Parse pid, tid, name from process info
|
pid_key, pid_value = pid_part.split(":", 1)
|
||||||
info_parts = [p.strip() for p in process_info.split(",")]
|
if pid_key != "pid":
|
||||||
for info in info_parts:
|
raise ValueError(f"Expected key pid, got {pid_key}")
|
||||||
key, value = info.split(":", 1)
|
pid_value = int(pid_value.strip())
|
||||||
key = key.strip()
|
|
||||||
value = value.strip()
|
|
||||||
|
|
||||||
if key == "pid":
|
tid_key, tid_value = tid_part.split(":", 1)
|
||||||
tombstone["pid"] = int(value)
|
if tid_key != "tid":
|
||||||
elif key == "tid":
|
raise ValueError(f"Expected key tid, got {tid_key}")
|
||||||
tombstone["tid"] = int(value)
|
tid_value = int(tid_value.strip())
|
||||||
elif key == "name":
|
|
||||||
tombstone["process_name"] = value
|
|
||||||
|
|
||||||
# Extract binary path if it exists
|
name_key, name_value = name_part.split(":", 1)
|
||||||
if len(parts) > 1:
|
if name_key != "name":
|
||||||
tombstone["binary_path"] = parts[1].strip().rstrip(" <")
|
raise ValueError(f"Expected key name, got {name_key}")
|
||||||
|
name_value = name_value.strip()
|
||||||
|
process_name, binary_path = self._parse_process_name(name_value, tombstone)
|
||||||
|
|
||||||
return True
|
tombstone["pid"] = pid_value
|
||||||
|
tombstone["tid"] = tid_value
|
||||||
|
tombstone["process_name"] = process_name
|
||||||
|
tombstone["binary_path"] = binary_path
|
||||||
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
def _parse_process_name(self, process_name_part, tombstone: dict) -> bool:
|
||||||
raise ValueError(f"Failed to parse PID line: {str(e)}")
|
process_name, process_path = process_name_part.split(">>>")
|
||||||
|
process_name = process_name.strip()
|
||||||
|
binary_path = process_path.strip().split(" ")[0]
|
||||||
|
return process_name, binary_path
|
||||||
|
|
||||||
def _load_signal_line(self, line: str, tombstone: dict) -> bool:
|
def _load_signal_line(self, line: str, tombstone: dict) -> bool:
|
||||||
signal_part, code_part = map(str.strip, line.split(",")[:2])
|
signal, code, _ = [part.strip() for part in line.split(",", 2)]
|
||||||
|
signal = signal.split("signal ")[1]
|
||||||
|
signal_code, signal_name = signal.split(" ")
|
||||||
|
signal_name = signal_name.strip("()")
|
||||||
|
|
||||||
def parse_part(part: str, prefix: str) -> tuple[int, str]:
|
code_part = code.split("code ")[1]
|
||||||
match = part.split(prefix)[1]
|
code_number, code_name = code_part.split(" ")
|
||||||
number = int(match.split()[0])
|
code_name = code_name.strip("()")
|
||||||
name = match.split("(")[1].split(")")[0] if "(" in match else "UNKNOWN"
|
|
||||||
return number, name
|
|
||||||
|
|
||||||
signal_number, signal_name = parse_part(signal_part, "signal ")
|
|
||||||
code_number, code_name = parse_part(code_part, "code ")
|
|
||||||
|
|
||||||
tombstone["signal_info"] = {
|
tombstone["signal_info"] = {
|
||||||
"code": code_number,
|
"code": int(code_number),
|
||||||
"code_name": code_name,
|
"code_name": code_name,
|
||||||
"name": signal_name,
|
"name": signal_name,
|
||||||
"number": signal_number,
|
"number": int(signal_code),
|
||||||
}
|
}
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -254,7 +252,13 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_timestamp_string(timestamp: str) -> str:
|
def _parse_timestamp_string(timestamp: str) -> str:
|
||||||
timestamp_parsed = parser.parse(timestamp)
|
timestamp_date, timezone = timestamp.split("+")
|
||||||
|
# Truncate microseconds before parsing
|
||||||
|
timestamp_without_micro = timestamp_date.split(".")[0] + "+" + timezone
|
||||||
|
timestamp_parsed = datetime.datetime.strptime(
|
||||||
|
timestamp_without_micro, "%Y-%m-%d %H:%M:%S%z"
|
||||||
|
)
|
||||||
|
|
||||||
# HACK: Swap the local timestamp to UTC, so keep the original time and avoid timezone conversion.
|
# HACK: Swap the local timestamp to UTC, so keep the original time and avoid timezone conversion.
|
||||||
local_timestamp = timestamp_parsed.replace(tzinfo=datetime.timezone.utc)
|
local_timestamp = timestamp_parsed.replace(tzinfo=datetime.timezone.utc)
|
||||||
return convert_datetime_to_iso(local_timestamp)
|
return convert_datetime_to_iso(local_timestamp)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from typing import List, Optional
|
|||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
|
|
||||||
from .modules.androidqf import ANDROIDQF_MODULES
|
from .modules.androidqf import ANDROIDQF_MODULES
|
||||||
|
from .modules.bugreport import BUGREPORT_MODULES
|
||||||
|
from .modules.bugreport.base import BugReportModule
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -39,7 +41,11 @@ class CmdAndroidCheckAndroidQF(Command):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.name = "check-androidqf"
|
self.name = "check-androidqf"
|
||||||
self.modules = ANDROIDQF_MODULES
|
|
||||||
|
# We can load AndroidQF and bugreport modules here, as
|
||||||
|
# AndroidQF dump will contain a bugreport.
|
||||||
|
self.modules = ANDROIDQF_MODULES + BUGREPORT_MODULES
|
||||||
|
# TODO: Check how to namespace and deduplicate modules.
|
||||||
|
|
||||||
self.format: Optional[str] = None
|
self.format: Optional[str] = None
|
||||||
self.archive: Optional[zipfile.ZipFile] = None
|
self.archive: Optional[zipfile.ZipFile] = None
|
||||||
@@ -54,12 +60,44 @@ class CmdAndroidCheckAndroidQF(Command):
|
|||||||
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.archive = zipfile.ZipFile(self.target_path)
|
||||||
self.files = self.archive.namelist()
|
self.files = self.archive.namelist()
|
||||||
|
|
||||||
|
def load_bugreport(self):
|
||||||
|
# Refactor this file list loading
|
||||||
|
# First we need to find the bugreport file location
|
||||||
|
bugreport_zip_path = None
|
||||||
|
for file_name in self.files:
|
||||||
|
if file_name.endswith("bugreport.zip"):
|
||||||
|
bugreport_zip_path = file_name
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.log.warning("No bugreport.zip found in the AndroidQF dump")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.format == "zip":
|
||||||
|
# Create handle to the bugreport.zip file inside the AndroidQF dump
|
||||||
|
handle = self.archive.open(bugreport_zip_path)
|
||||||
|
bugreport_zip = zipfile.ZipFile(handle)
|
||||||
|
else:
|
||||||
|
# Load the bugreport.zip file from the extracted AndroidQF dump on disk.
|
||||||
|
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
||||||
|
bug_report_path = os.path.join(parent_path, bugreport_zip_path)
|
||||||
|
bugreport_zip = zipfile.ZipFile(bug_report_path)
|
||||||
|
|
||||||
|
return bugreport_zip
|
||||||
|
|
||||||
def module_init(self, module):
|
def module_init(self, module):
|
||||||
|
if isinstance(module, BugReportModule):
|
||||||
|
bugreport_archive = self.load_bugreport()
|
||||||
|
if not bugreport_archive:
|
||||||
|
return
|
||||||
|
module.from_zip(bugreport_archive, bugreport_archive.namelist())
|
||||||
|
return
|
||||||
|
|
||||||
if self.format == "zip":
|
if self.format == "zip":
|
||||||
module.from_zip_file(self.archive, self.files)
|
module.from_zip_file(self.archive, self.files)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ class Packages(AndroidExtraction):
|
|||||||
result["matched_indicator"] = ioc
|
result["matched_indicator"] = ioc
|
||||||
self.detected.append(result)
|
self.detected.append(result)
|
||||||
|
|
||||||
def check_virustotal(self, packages: list) -> None:
|
@staticmethod
|
||||||
|
def check_virustotal(packages: list) -> None:
|
||||||
hashes = []
|
hashes = []
|
||||||
for package in packages:
|
for package in packages:
|
||||||
for file in package.get("files", []):
|
for file in package.get("files", []):
|
||||||
@@ -142,15 +143,8 @@ class Packages(AndroidExtraction):
|
|||||||
|
|
||||||
for package in packages:
|
for package in packages:
|
||||||
for file in package.get("files", []):
|
for file in package.get("files", []):
|
||||||
if "package_name" in package:
|
row = [package["package_name"], file["path"]]
|
||||||
row = [package["package_name"], file["path"]]
|
|
||||||
elif "name" in package:
|
|
||||||
row = [package["name"], file["path"]]
|
|
||||||
else:
|
|
||||||
self.log.error(
|
|
||||||
f"Package {package} has no name or package_name. packages.json or apks.json is malformed"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if file["sha256"] in detections:
|
if file["sha256"] in detections:
|
||||||
detection = detections[file["sha256"]]
|
detection = detections[file["sha256"]]
|
||||||
positives = detection.split("/")[0]
|
positives = detection.split("/")[0]
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ from .processes import Processes
|
|||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
from .sms import SMS
|
from .sms import SMS
|
||||||
from .files import Files
|
from .files import Files
|
||||||
from .mounts import Mounts
|
|
||||||
|
|
||||||
ANDROIDQF_MODULES = [
|
ANDROIDQF_MODULES = [
|
||||||
DumpsysActivities,
|
DumpsysActivities,
|
||||||
@@ -38,5 +37,4 @@ ANDROIDQF_MODULES = [
|
|||||||
SMS,
|
SMS,
|
||||||
DumpsysPackages,
|
DumpsysPackages,
|
||||||
Files,
|
Files,
|
||||||
Mounts,
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from mvt.android.artifacts.mounts import Mounts as MountsArtifact
|
|
||||||
|
|
||||||
from .base import AndroidQFModule
|
|
||||||
|
|
||||||
|
|
||||||
class Mounts(MountsArtifact, AndroidQFModule):
|
|
||||||
"""This module extracts and analyzes mount information from AndroidQF acquisitions."""
|
|
||||||
|
|
||||||
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 = []
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
"""
|
|
||||||
Run the mounts analysis module.
|
|
||||||
|
|
||||||
This module looks for mount information files collected by androidqf
|
|
||||||
and analyzes them for suspicious configurations, particularly focusing
|
|
||||||
on detecting root access indicators like /system mounted as read-write.
|
|
||||||
"""
|
|
||||||
mount_files = self._get_files_by_pattern("*/mounts.json")
|
|
||||||
|
|
||||||
if not mount_files:
|
|
||||||
self.log.info("No mount information file found")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.log.info("Found mount information file: %s", mount_files[0])
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = self._get_file_content(mount_files[0]).decode(
|
|
||||||
"utf-8", errors="replace"
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
self.log.error("Failed to read mount information file: %s", exc)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Parse the mount data
|
|
||||||
try:
|
|
||||||
json_data = json.loads(data)
|
|
||||||
|
|
||||||
if isinstance(json_data, list):
|
|
||||||
# AndroidQF format: array of strings like
|
|
||||||
# "/dev/block/dm-12 on / type ext4 (ro,seclabel,noatime)"
|
|
||||||
mount_content = "\n".join(json_data)
|
|
||||||
self.parse(mount_content)
|
|
||||||
except Exception as exc:
|
|
||||||
self.log.error("Failed to parse mount information: %s", exc)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.log.info("Extracted a total of %d mount entries", len(self.results))
|
|
||||||
@@ -231,7 +231,6 @@ def parse_sms_file(data):
|
|||||||
entry.pop("mms_body")
|
entry.pop("mms_body")
|
||||||
|
|
||||||
body = entry.get("body", None)
|
body = entry.get("body", None)
|
||||||
message_links = None
|
|
||||||
if body:
|
if body:
|
||||||
message_links = check_for_links(entry["body"])
|
message_links = check_for_links(entry["body"])
|
||||||
|
|
||||||
|
|||||||
@@ -65,10 +65,6 @@ class CmdCheckIOCS(Command):
|
|||||||
m = iocs_module.from_json(
|
m = iocs_module.from_json(
|
||||||
file_path, log=logging.getLogger(iocs_module.__module__)
|
file_path, log=logging.getLogger(iocs_module.__module__)
|
||||||
)
|
)
|
||||||
if not m:
|
|
||||||
log.warning("No result from this module, skipping it")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.iocs.total_ioc_count > 0:
|
if self.iocs.total_ioc_count > 0:
|
||||||
m.indicators = self.iocs
|
m.indicators = self.iocs
|
||||||
m.indicators.log = m.log
|
m.indicators.log = m.log
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def check_updates() -> None:
|
|||||||
if latest_version:
|
if latest_version:
|
||||||
rich_print(
|
rich_print(
|
||||||
f"\t\t[bold]Version {latest_version} is available! "
|
f"\t\t[bold]Version {latest_version} is available! "
|
||||||
"Upgrade mvt with `pip3 install -U mvt` or with `pipx upgrade mvt`[/bold]"
|
"Upgrade mvt with `pip3 install -U mvt`[/bold]"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Then we check for indicators files updates.
|
# Then we check for indicators files updates.
|
||||||
|
|||||||
@@ -69,14 +69,10 @@ class MVTModule:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, json_path: str, log: logging.Logger):
|
def from_json(cls, json_path: str, log: logging.Logger):
|
||||||
with open(json_path, "r", encoding="utf-8") as handle:
|
with open(json_path, "r", encoding="utf-8") as handle:
|
||||||
try:
|
results = json.load(handle)
|
||||||
results = json.load(handle)
|
if log:
|
||||||
if log:
|
log.info('Loaded %d results from "%s"', len(results), json_path)
|
||||||
log.info('Loaded %d results from "%s"', len(results), json_path)
|
return cls(results=results, log=log)
|
||||||
return cls(results=results, log=log)
|
|
||||||
except json.decoder.JSONDecodeError as err:
|
|
||||||
log.error('Error to decode the json "%s" file: "%s"', json_path, err)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_slug(cls) -> str:
|
def get_slug(cls) -> str:
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
MVT_VERSION = "2.6.1"
|
MVT_VERSION = "2.6.0"
|
||||||
|
|||||||
@@ -891,14 +891,6 @@
|
|||||||
"version": "15.8.2",
|
"version": "15.8.2",
|
||||||
"build": "19H384"
|
"build": "19H384"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"version": "15.8.4",
|
|
||||||
"build": "19H390"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "15.8.5",
|
|
||||||
"build": "19H394"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"build": "20A362",
|
"build": "20A362",
|
||||||
"version": "16.0"
|
"version": "16.0"
|
||||||
@@ -1000,14 +992,6 @@
|
|||||||
"version": "16.7.8",
|
"version": "16.7.8",
|
||||||
"build": "20H343"
|
"build": "20H343"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"version": "16.7.11",
|
|
||||||
"build": "20H360"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "16.7.12",
|
|
||||||
"build": "20H364"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "17.0",
|
"version": "17.0",
|
||||||
"build": "21A327"
|
"build": "21A327"
|
||||||
@@ -1092,10 +1076,6 @@
|
|||||||
"version": "17.6.1",
|
"version": "17.6.1",
|
||||||
"build": "21G101"
|
"build": "21G101"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"version": "17.7.7",
|
|
||||||
"build": "21H433"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "18",
|
"version": "18",
|
||||||
"build": "22A3354"
|
"build": "22A3354"
|
||||||
@@ -1123,45 +1103,5 @@
|
|||||||
{
|
{
|
||||||
"version": "18.3",
|
"version": "18.3",
|
||||||
"build": "22D63"
|
"build": "22D63"
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.3.1",
|
|
||||||
"build": "22D72"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.4",
|
|
||||||
"build": "22E240"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.4.1",
|
|
||||||
"build": "22E252"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.5",
|
|
||||||
"build": "22F76"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.6",
|
|
||||||
"build": "22G86"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.6.1",
|
|
||||||
"build": "22G90"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.6.2",
|
|
||||||
"build": "22G100"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "18.7",
|
|
||||||
"build": "22H20"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "26",
|
|
||||||
"build": "23A341"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "26.0.1",
|
|
||||||
"build": "23A355"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -43,8 +43,6 @@ class GlobalPreferences(IOSExtraction):
|
|||||||
self.log.warning("Lockdown mode enabled")
|
self.log.warning("Lockdown mode enabled")
|
||||||
else:
|
else:
|
||||||
self.log.warning("Lockdown mode disabled")
|
self.log.warning("Lockdown mode disabled")
|
||||||
return
|
|
||||||
self.log.warning("Lockdown mode disabled")
|
|
||||||
|
|
||||||
def process_file(self, file_path: str) -> None:
|
def process_file(self, file_path: str) -> None:
|
||||||
with open(file_path, "rb") as handle:
|
with open(file_path, "rb") as handle:
|
||||||
|
|||||||
@@ -95,17 +95,14 @@ class SafariBrowserState(IOSExtraction):
|
|||||||
)
|
)
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
# Old version iOS <12 likely
|
# Old version iOS <12 likely
|
||||||
try:
|
cur.execute(
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
title, url, user_visible_url, last_viewed_time, session_data
|
|
||||||
FROM tabs
|
|
||||||
ORDER BY last_viewed_time;
|
|
||||||
"""
|
"""
|
||||||
)
|
SELECT
|
||||||
except sqlite3.OperationalError as e:
|
title, url, user_visible_url, last_viewed_time, session_data
|
||||||
self.log.error(f"Error executing query: {e}")
|
FROM tabs
|
||||||
|
ORDER BY last_viewed_time;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
for row in cur:
|
for row in cur:
|
||||||
session_entries = []
|
session_entries = []
|
||||||
|
|||||||
@@ -116,16 +116,13 @@ class TCC(IOSExtraction):
|
|||||||
)
|
)
|
||||||
db_version = "v2"
|
db_version = "v2"
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
try:
|
cur.execute(
|
||||||
cur.execute(
|
"""SELECT
|
||||||
"""SELECT
|
service, client, client_type, allowed,
|
||||||
service, client, client_type, allowed,
|
prompt_count
|
||||||
prompt_count
|
FROM access;"""
|
||||||
FROM access;"""
|
)
|
||||||
)
|
db_version = "v1"
|
||||||
db_version = "v1"
|
|
||||||
except sqlite3.OperationalError as e:
|
|
||||||
self.log.error(f"Error parsing TCC database: {e}")
|
|
||||||
|
|
||||||
for row in cur:
|
for row in cur:
|
||||||
service = row[0]
|
service = row[0]
|
||||||
|
|||||||
9
test-requirements.txt
Normal file
9
test-requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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]
|
||||||
@@ -29,28 +29,3 @@ class TestDumpsysADBArtifact:
|
|||||||
user_key["fingerprint"] == "F0:A1:3D:8C:B3:F4:7B:09:9F:EE:8B:D8:38:2E:BD:C6"
|
user_key["fingerprint"] == "F0:A1:3D:8C:B3:F4:7B:09:9F:EE:8B:D8:38:2E:BD:C6"
|
||||||
)
|
)
|
||||||
assert user_key["user"] == "user@linux"
|
assert user_key["user"] == "user@linux"
|
||||||
|
|
||||||
def test_parsing_adb_xml(self):
|
|
||||||
da_adb = DumpsysADBArtifact()
|
|
||||||
file = get_artifact("android_data/dumpsys_adb_xml.txt")
|
|
||||||
with open(file, "rb") as f:
|
|
||||||
data = f.read()
|
|
||||||
|
|
||||||
da_adb.parse(data)
|
|
||||||
|
|
||||||
assert len(da_adb.results) == 1
|
|
||||||
|
|
||||||
adb_data = da_adb.results[0]
|
|
||||||
assert "user_keys" in adb_data
|
|
||||||
assert len(adb_data["user_keys"]) == 1
|
|
||||||
|
|
||||||
# Check key and fingerprint parsed successfully.
|
|
||||||
expected_fingerprint = "F0:0B:27:08:E3:68:7B:FA:4C:79:A2:B4:BF:0E:CF:70"
|
|
||||||
user_key = adb_data["user_keys"][0]
|
|
||||||
user_key["fingerprint"] == expected_fingerprint
|
|
||||||
assert user_key["user"] == "user@laptop"
|
|
||||||
|
|
||||||
key_store_entry = adb_data["keystore"][0]
|
|
||||||
assert key_store_entry["user"] == "user@laptop"
|
|
||||||
assert key_store_entry["fingerprint"] == expected_fingerprint
|
|
||||||
assert key_store_entry["last_connected"] == "1628501829898"
|
|
||||||
|
|||||||
@@ -64,4 +64,4 @@ class TestTombstoneCrashArtifact:
|
|||||||
# We often don't know the time offset for a log entry and so can't convert everything to UTC.
|
# We often don't know the time offset for a log entry and so can't convert everything to UTC.
|
||||||
# MVT should output the local time only:
|
# MVT should output the local time only:
|
||||||
# So original 2023-04-12 12:32:40.518290770+0200 -> 2023-04-12 12:32:40.000000
|
# So original 2023-04-12 12:32:40.518290770+0200 -> 2023-04-12 12:32:40.000000
|
||||||
assert tombstone_result.get("timestamp") == "2023-04-12 12:32:40.518290"
|
assert tombstone_result.get("timestamp") == "2023-04-12 12:32:40.000000"
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 The MVT Authors.
|
|
||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
||||||
# https://license.mvt.re/1.1/
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from mvt.common.module import run_module
|
|
||||||
|
|
||||||
from ..utils import get_android_androidqf, list_files
|
|
||||||
|
|
||||||
|
|
||||||
class TestAndroidqfMountsArtifact:
|
|
||||||
def test_parse_mounts_token_checks(self):
|
|
||||||
"""
|
|
||||||
Test the artifact-level `parse` method using tolerant token checks.
|
|
||||||
|
|
||||||
Different parser variants may place mount tokens into different dict
|
|
||||||
keys (for example `mount_options`, `pass_num`, `dump_freq`, etc.). To
|
|
||||||
avoid brittle assertions we concatenate each parsed entry's values and
|
|
||||||
look for expected tokens (device names, mount points, options) somewhere
|
|
||||||
in the combined representation.
|
|
||||||
"""
|
|
||||||
from mvt.android.artifacts.mounts import Mounts as MountsArtifact
|
|
||||||
|
|
||||||
m = MountsArtifact()
|
|
||||||
|
|
||||||
mount_lines = [
|
|
||||||
"/dev/block/dm-12 on / type ext4 (ro,seclabel,noatime)",
|
|
||||||
"/dev/block/by-name/system on /system type ext4 (rw,seclabel,noatime)",
|
|
||||||
"/dev/block/by-name/data on /data type f2fs (rw,nosuid,nodev,noatime)",
|
|
||||||
]
|
|
||||||
mount_content = "\n".join(mount_lines)
|
|
||||||
|
|
||||||
# Parse the mount lines (artifact-level)
|
|
||||||
m.parse(mount_content)
|
|
||||||
|
|
||||||
# Basic sanity: parser should return one entry per input line
|
|
||||||
assert len(m.results) == 3, f"Expected 3 parsed mounts, got: {m.results}"
|
|
||||||
|
|
||||||
# Concatenate each entry's values into a single string so token checks
|
|
||||||
# are tolerant to which dict keys were used by the parser.
|
|
||||||
def concat_values(entry):
|
|
||||||
parts = []
|
|
||||||
for v in entry.values():
|
|
||||||
try:
|
|
||||||
parts.append(str(v))
|
|
||||||
except Exception:
|
|
||||||
# Skip values that can't be stringified
|
|
||||||
continue
|
|
||||||
return " ".join(parts)
|
|
||||||
|
|
||||||
concatenated = [concat_values(e) for e in m.results]
|
|
||||||
|
|
||||||
# Token expectations (tolerant):
|
|
||||||
# - Root line should include 'dm-12' and 'noatime' (and typically 'ro')
|
|
||||||
assert any("dm-12" in s and "noatime" in s for s in concatenated), (
|
|
||||||
f"No root-like tokens (dm-12 + noatime) found in parsed results: {concatenated}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# - System line should include '/system' or 'by-name/system' and 'rw'
|
|
||||||
assert any(
|
|
||||||
(("by-name/system" in s or "/system" in s) and "rw" in s)
|
|
||||||
for s in concatenated
|
|
||||||
), (
|
|
||||||
f"No system-like tokens (system + rw) found in parsed results: {concatenated}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# - Data line should include '/data' or 'by-name/data' and 'rw'
|
|
||||||
assert any(
|
|
||||||
(("by-name/data" in s or "/data" in s) and "rw" in s) for s in concatenated
|
|
||||||
), f"No data-like tokens (data + rw) found in parsed results: {concatenated}"
|
|
||||||
|
|
||||||
|
|
||||||
class TestAndroidqfMountsModule:
|
|
||||||
def test_androidqf_module_no_mounts_file(self):
|
|
||||||
"""
|
|
||||||
When no `mounts.json` is present in the androidqf dataset, the module
|
|
||||||
should not produce results nor detections.
|
|
||||||
"""
|
|
||||||
from mvt.android.modules.androidqf.mounts import Mounts
|
|
||||||
|
|
||||||
data_path = get_android_androidqf()
|
|
||||||
m = Mounts(target_path=data_path, log=logging)
|
|
||||||
files = list_files(data_path)
|
|
||||||
parent_path = Path(data_path).absolute().parent.as_posix()
|
|
||||||
m.from_folder(parent_path, files)
|
|
||||||
|
|
||||||
run_module(m)
|
|
||||||
|
|
||||||
# The provided androidqf test dataset does not include mounts.json, so
|
|
||||||
# results should remain empty.
|
|
||||||
assert len(m.results) == 0, (
|
|
||||||
f"Expected no results when mounts.json is absent, got: {m.results}"
|
|
||||||
)
|
|
||||||
assert len(m.detected) == 0, f"Expected no detections, got: {m.detected}"
|
|
||||||
@@ -9,7 +9,6 @@ from pathlib import Path
|
|||||||
from mvt.android.modules.bugreport.appops import Appops
|
from mvt.android.modules.bugreport.appops import Appops
|
||||||
from mvt.android.modules.bugreport.getprop import Getprop
|
from mvt.android.modules.bugreport.getprop import Getprop
|
||||||
from mvt.android.modules.bugreport.packages import Packages
|
from mvt.android.modules.bugreport.packages import Packages
|
||||||
from mvt.android.modules.bugreport.tombstones import Tombstones
|
|
||||||
from mvt.common.module import run_module
|
from mvt.common.module import run_module
|
||||||
|
|
||||||
from ..utils import get_artifact_folder
|
from ..utils import get_artifact_folder
|
||||||
@@ -55,8 +54,3 @@ class TestBugreportAnalysis:
|
|||||||
def test_getprop_module(self):
|
def test_getprop_module(self):
|
||||||
m = self.launch_bug_report_module(Getprop)
|
m = self.launch_bug_report_module(Getprop)
|
||||||
assert len(m.results) == 0
|
assert len(m.results) == 0
|
||||||
|
|
||||||
def test_tombstones_modules(self):
|
|
||||||
m = self.launch_bug_report_module(Tombstones)
|
|
||||||
assert len(m.results) == 2
|
|
||||||
assert m.results[1]["pid"] == 3559
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
|
|
||||||
Build fingerprint: 'samsung/a10eea/a10:10/.190711.020/A105:user/release-keys'
|
|
||||||
Revision: '5'
|
|
||||||
ABI: 'arm'
|
|
||||||
Timestamp: 2021-09-29 17:43:49+0200
|
|
||||||
pid: 9850, tid: 9893, name: UsbFfs-worker >>> /system/bin/adbd <<<
|
|
||||||
uid: 2000
|
|
||||||
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
|
|
||||||
Abort message: 'Check failed: payload.size() <= bytes_left (payload.size()=99, bytes_left=51) '
|
|
||||||
r0 00000000 r1 000026a5 r2 00000006 r3 f11fad98
|
|
||||||
r4 f11fadac r5 f11fad90 r6 0000267a r7 0000016b
|
|
||||||
r8 f11fada8 r9 f11fad98 r10 f11fadc8 r11 f11fadb8
|
|
||||||
ip 000026a5 sp f11fad68 lr f20c23b7 pc f20c23ca
|
|
||||||
|
|
||||||
backtrace:
|
|
||||||
#00 pc 000603ca /apex/com.android.runtime/lib/bionic/libc.so (abort+166) (BuildId: 320fbdc2a1289fadd7dacae7f2eb77a3)
|
|
||||||
#01 pc 00007e23 /system/lib/libbase.so (android::base::DefaultAborter(char const*)+6) (BuildId: a28585ee446ea17e3e6fcf9c907fff2a)
|
|
||||||
#02 pc 0000855f /system/lib/libbase.so (android::base::LogMessage::~LogMessage()+406) (BuildId: a28585ee446ea17e3e6fcf9c907fff2a)
|
|
||||||
#03 pc 000309cf /system/lib/libadbd.so (UsbFfsConnection::ProcessRead(IoBlock*)+814) (BuildId: 3645b175977ae210c156a57b25dfa599)
|
|
||||||
#04 pc 00030459 /system/lib/libadbd.so (UsbFfsConnection::HandleRead(TransferId, long long)+84) (BuildId: 3645b175977ae210c156a57b25dfa599)
|
|
||||||
#05 pc 00030349 /system/lib/libadbd.so (UsbFfsConnection::ReadEvents()+92) (BuildId: 3645b175977ae210c156a57b25dfa599)
|
|
||||||
#06 pc 00030169 /system/lib/libadbd.so (_ZZN16UsbFfsConnection11StartWorkerEvENKUlvE_clEv+504) (BuildId: 3645b175977ae210c156a57b25dfa599)
|
|
||||||
#07 pc 0002ff53 /system/lib/libadbd.so (_ZNSt3__114__thread_proxyINS_5tupleIJNS_10unique_ptrINS_15__thread_structENS_14default_deleteIS3_EEEEZN16UsbFfsConnection11StartWorkerEvEUlvE_EEEEEPvSA_+26) (BuildId: 3645b175977ae210c156a57b25dfa599)
|
|
||||||
#08 pc 000a75b3 /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+20) (BuildId: 320fbdc2a1289fadd7dacae7f2eb77a3)
|
|
||||||
#09 pc 00061b33 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+30) (BuildId: 320fbdc2a1289fadd7dacae7f2eb77a3)
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
|
|
||||||
Build fingerprint: 'samsung/a10eea/a10:11/RP1A.200720.012/A105:user/release-keys'
|
|
||||||
Revision: '5'
|
|
||||||
ABI: 'arm'
|
|
||||||
Timestamp: 2023-08-21 23:28:59-0400
|
|
||||||
pid: 3559, tid: 3568, name: tzts_daemon >>> /vendor/bin/tzts_daemon <<<
|
|
||||||
uid: 1000
|
|
||||||
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xe8b4d14c
|
|
||||||
r0 e8b4d14c r1 e8b4d14c r2 0000002b r3 00000004
|
|
||||||
r4 00000000 r5 e8b4d14c r6 00000000 r7 00000000
|
|
||||||
r8 e7ef78b0 r9 0000002b r10 e7ef7dad r11 e7ef7400
|
|
||||||
ip 00000000 sp e7ef7208 lr e89f4b01 pc e89c273a
|
|
||||||
|
|
||||||
backtrace:
|
|
||||||
#00 pc 0005f73a /apex/com.android.runtime/lib/bionic/libc.so (strlen_a15+54) (BuildId: fef5b751123147ea65bf3f4f798c9518)
|
|
||||||
#01 pc 00091afd /apex/com.android.runtime/lib/bionic/libc.so (__vfprintf+3364) (BuildId: fef5b751123147ea65bf3f4f798c9518)
|
|
||||||
#02 pc 000a68e5 /apex/com.android.runtime/lib/bionic/libc.so (vsnprintf+152) (BuildId: fef5b751123147ea65bf3f4f798c9518)
|
|
||||||
#03 pc 000051cf /system/lib/liblog.so (__android_log_vprint+74) (BuildId: 3fcead474cd0ecbdafb529ff176b0d13)
|
|
||||||
#04 pc 000012e8 /vendor/bin/tzts_daemon
|
|
||||||
|
|
||||||
memory near r0:
|
|
||||||
e8b4d12c -------- -------- -------- -------- ................
|
|
||||||
e8b4d13c -------- -------- -------- -------- ................
|
|
||||||
e8b4d14c -------- -------- -------- -------- ................
|
|
||||||
e8b4d15c -------- -------- -------- -------- ................
|
|
||||||
e8b4d16c -------- -------- -------- -------- ................
|
|
||||||
e8b4d17c -------- -------- -------- -------- ................
|
|
||||||
e8b4d18c -------- -------- -------- -------- ................
|
|
||||||
e8b4d19c -------- -------- -------- -------- ................
|
|
||||||
e8b4d1ac -------- -------- -------- -------- ................
|
|
||||||
e8b4d1bc -------- -------- -------- -------- ................
|
|
||||||
e8b4d1cc -------- -------- -------- -------- ................
|
|
||||||
e8b4d1dc -------- -------- -------- -------- ................
|
|
||||||
e8b4d1ec -------- -------- -------- -------- ................
|
|
||||||
e8b4d1fc -------- -------- -------- -------- ................
|
|
||||||
e8b4d20c -------- -------- -------- -------- ................
|
|
||||||
e8b4d21c -------- -------- -------- -------- ................
|
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
-------------------------------------------------------------------------------
|
|
||||||
DUMP OF SERVICE adb:
|
|
||||||
ADB MANAGER STATE (dumpsys adb):
|
|
||||||
{
|
|
||||||
debugging_manager={
|
|
||||||
connected_to_adb=true
|
|
||||||
user_keys=QAAAAAcgbytJst31DsaSP7hn8QcBXKR9NPVPK9MZssFVSNIP user@laptop
|
|
||||||
|
|
||||||
keystore=<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
|
|
||||||
<keyStore version="1">
|
|
||||||
<adbKey key="QAAAAAcgbytJst31DsaSP7hn8QcBXKR9NPVPK9MZssFVSNIP user@laptop" lastConnection="1628501829898" />
|
|
||||||
</keyStore>
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
--------- 0.012s was the duration of dumpsys adb, ending at: 2025-02-04 20:25:58
|
|
||||||
Reference in New Issue
Block a user