Compare commits

..
Author SHA1 Message Date
Donncha Ó Cearbhaill e9e621640b Close open archive (zip/tar) file handles 2025-10-06 10:07:16 +02:00
Donncha Ó Cearbhaill 05ad7d274c Fix profile events log line 2025-10-06 09:50:43 +02:00
Donncha Ó Cearbhaill 70d646af78 Quote STIX path in log line 2025-10-06 09:50:24 +02:00
Donncha Ó Cearbhaill 2d547662f8 Rework old detections tracking into stuctured alert levels 2025-02-19 23:46:03 +01:00
Donncha Ó Cearbhaill ca0bc46f11 Fix up, remove ADB module base 2025-02-16 00:30:45 +01:00
Donncha Ó Cearbhaill 1b03002a00 Major refactor to add structured alerting and typed indicators
This commit makes a structural change to MVT by changing binary
detected/not detected logic into a structured multi-level system
of alerts. This gives far more power to extend MVT and manage
alerts.

This commit also begins the process of adding proper typing for
key objects used in MVT including Indicators, IndicatorMatches,
and ModuleResults. This will also be keep to programmatically using
the output of MVT.
2025-02-16 00:16:34 +01:00
Donncha Ó Cearbhaill 6bac787cb5 Remove check-apk code and old dependencies 2025-02-16 00:00:09 +01:00
Donncha Ó Cearbhaill 064b9fbeb9 Remove check-adb command and update docs 2025-02-15 22:47:42 +01:00
Donncha Ó Cearbhaill 4c1cdf5129 Raise the proper NoAndroidQFBackup exception when a back-up isn't found 2025-02-11 15:04:48 +01:00
Donncha Ó Cearbhaill a08c24b02a Deduplicate modules which are run by the sub-commands. 2025-02-10 20:32:51 +01:00
Donncha Ó Cearbhaill 5d696350dc Run bugreport and backup modules during check-androidqf
Adding support to automatically run ADB backup and bugreport modules
automatically when running the check-androidqf command. This is a first
step to deduplicate the code for Android modules.
2025-02-10 19:28:20 +01:00
149 changed files with 2452 additions and 4709 deletions

No files matched your search

-13
View File
@@ -1,13 +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: "uv" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
cooldown:
default-days: 7
+7 -9
View File
@@ -7,19 +7,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
python-version: 3.9
cache: 'pip'
- name: Checkout
uses: actions/checkout@master
- name: Install Dependencies
run: |
uv sync --locked --group dev
pip install mypy
- name: mypy
run: |
make mypy
+11 -29
View File
@@ -4,8 +4,6 @@ name: Create and publish a Docker image
# Configures this workflow to run every time a release is published.
on:
workflow_dispatch:
push:
branches: [main]
release:
types: [published]
@@ -25,18 +23,9 @@ jobs:
attestations: write
id-token: write
#
strategy:
matrix:
platform:
- dockerfile: "Dockerfile"
tag-suffix: ""
- dockerfile: "Dockerfile.ios"
tag-suffix: "-ios"
- dockerfile: "Dockerfile.android"
tag-suffix: "-android"
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
@@ -47,33 +36,26 @@ jobs:
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
tags: |
type=raw,value=latest,enable={{ is_default_branch }},suffix=${{ matrix.platform.tag-suffix }}
type=raw,enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }},value=stable,suffix=${{ matrix.platform.tag-suffix }}
type=raw,enable=${{ github.event_name == 'release' }},value=${{ github.event.release.tag_name }},suffix=${{ matrix.platform.tag-suffix }}
type=raw,enable=${{ github.ref_type == 'tag' }},value=${{ github.ref_name }},suffix=${{ matrix.platform.tag-suffix }}
type=sha,suffix=${{ matrix.platform.tag-suffix }}
type=sha,format=long,suffix=${{ matrix.platform.tag-suffix }}
# This step sets up some additional capabilities to generate the provenance and sbom attestations
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
file: ${{ matrix.platform.dockerfile }}
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: mode=max
sbom: true
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
+7 -9
View File
@@ -11,19 +11,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
python-version: 3.9
cache: 'pip'
- name: Checkout
uses: actions/checkout@master
- name: Install Dependencies
run: |
uv sync --locked --group dev
pip install ruff
- name: ruff
run: |
make ruff
+6 -9
View File
@@ -12,21 +12,18 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
python-version: ['3.8', '3.9', '3.10'] # , '3.11']
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Python dependencies
run: |
uv sync --locked --group dev --python ${{ matrix.python-version }}
make install
make test-requirements
- name: Test with pytest
run: |
set -o pipefail
@@ -38,4 +35,4 @@ jobs:
if: github.event_name == 'pull_request'
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
junitxml-path: ./pytest.xml
+2 -3
View File
@@ -16,15 +16,14 @@ jobs:
- name: Run script to fetch latest iOS releases from Apple RSS feed.
run: python3 .github/workflows/scripts/update-ios-releases.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
uses: peter-evans/create-pull-request@v5
with:
title: '[auto] Update iOS releases and versions'
commit-message: Add new iOS versions and build numbers
branch: auto/add-new-ios-releases
draft: true
body: |
This is an automated pull request to update the iOS releases and version numbers.
add-paths: |
*.json
labels: |
automated pr
automated pr
+1 -4
View File
@@ -86,9 +86,6 @@ ipython_config.py
# pyenv
.python-version
# uv project Python version
!.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
@@ -141,4 +138,4 @@ dmypy.json
.idea
# Sublime Text project files
*.sublime*
*.sublime*
-1
View File
@@ -1 +0,0 @@
3.10
+4 -6
View File
@@ -6,16 +6,14 @@
version: 2
build:
os: "ubuntu-24.04"
os: "ubuntu-22.04"
tools:
python: "3.11"
mkdocs:
configuration: mkdocs.yml
# Optionally set the version of Python and requirements required to build your docs
python:
install:
- method: uv
command: sync
groups:
- docs
install:
- requirements: docs/requirements.txt
+15 -11
View File
@@ -1,6 +1,6 @@
# Base image for building libraries
# ---------------------------------
FROM ubuntu:22.04 AS build-base
FROM ubuntu:22.04 as build-base
ARG DEBIAN_FRONTEND=noninteractive
@@ -22,7 +22,7 @@ RUN apt-get update \
# libplist
# --------
FROM build-base AS build-libplist
FROM build-base as build-libplist
# Build
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
@@ -32,7 +32,7 @@ RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
# libimobiledevice-glue
# ---------------------
FROM build-base AS build-libimobiledevice-glue
FROM build-base as build-libimobiledevice-glue
# Install dependencies
COPY --from=build-libplist /build /
@@ -45,7 +45,7 @@ RUN git clone https://github.com/libimobiledevice/libimobiledevice-glue && cd li
# libtatsu
# --------
FROM build-base AS build-libtatsu
FROM build-base as build-libtatsu
# Install dependencies
COPY --from=build-libplist /build /
@@ -58,7 +58,7 @@ RUN git clone https://github.com/libimobiledevice/libtatsu && cd libtatsu \
# libusbmuxd
# ----------
FROM build-base AS build-libusbmuxd
FROM build-base as build-libusbmuxd
# Install dependencies
COPY --from=build-libplist /build /
@@ -72,7 +72,7 @@ RUN git clone https://github.com/libimobiledevice/libusbmuxd && cd libusbmuxd \
# libimobiledevice
# ----------------
FROM build-base AS build-libimobiledevice
FROM build-base as build-libimobiledevice
# Install dependencies
COPY --from=build-libplist /build /
@@ -88,7 +88,7 @@ RUN git clone https://github.com/libimobiledevice/libimobiledevice && cd libimob
# usbmuxd
# -------
FROM build-base AS build-usbmuxd
FROM build-base as build-usbmuxd
# Install dependencies
COPY --from=build-libplist /build /
@@ -103,8 +103,7 @@ RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
# Create main image
FROM ubuntu:24.04 AS main
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /uvx /usr/local/bin/
FROM ubuntu:22.04 as main
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
@@ -134,8 +133,13 @@ COPY --from=build-usbmuxd /build /
# Install mvt using the locally checked out source
COPY . mvt/
RUN uv pip install --system --break-system-packages --no-cache ./mvt \
&& rm -rf mvt
RUN apt-get update \
&& apt-get install -y git python3-pip \
&& 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 \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf mvt
# Installing ABE
ADD --checksum=sha256:a20e07f8b2ea47620aff0267f230c3f1f495f097081fd709eec51cf2a2e11632 \
+2 -3
View File
@@ -1,6 +1,5 @@
# Create main image
FROM python:3.10.14-alpine3.20 AS main
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /uvx /usr/local/bin/
FROM python:3.10.14-alpine3.20 as main
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
@@ -21,7 +20,7 @@ RUN apk add --no-cache \
# Install mvt
COPY ./ mvt
RUN apk add --no-cache --virtual .build-deps gcc musl-dev \
&& uv pip install --system --no-cache ./mvt \
&& PIP_NO_CACHE_DIR=1 pip3 install ./mvt \
&& apk del .build-deps gcc musl-dev && rm -rf ./mvt
# Installing ABE
+9 -10
View File
@@ -1,6 +1,6 @@
# Base image for building libraries
# ---------------------------------
FROM ubuntu:22.04 AS build-base
FROM ubuntu:22.04 as build-base
ARG DEBIAN_FRONTEND=noninteractive
@@ -22,7 +22,7 @@ RUN apt-get update \
# libplist
# --------
FROM build-base AS build-libplist
FROM build-base as build-libplist
# Build
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
@@ -32,7 +32,7 @@ RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
# libimobiledevice-glue
# ---------------------
FROM build-base AS build-libimobiledevice-glue
FROM build-base as build-libimobiledevice-glue
# Install dependencies
COPY --from=build-libplist /build /
@@ -45,7 +45,7 @@ RUN git clone https://github.com/libimobiledevice/libimobiledevice-glue && cd li
# libtatsu
# --------
FROM build-base AS build-libtatsu
FROM build-base as build-libtatsu
# Install dependencies
COPY --from=build-libplist /build /
@@ -58,7 +58,7 @@ RUN git clone https://github.com/libimobiledevice/libtatsu && cd libtatsu \
# libusbmuxd
# ----------
FROM build-base AS build-libusbmuxd
FROM build-base as build-libusbmuxd
# Install dependencies
COPY --from=build-libplist /build /
@@ -72,7 +72,7 @@ RUN git clone https://github.com/libimobiledevice/libusbmuxd && cd libusbmuxd \
# libimobiledevice
# ----------------
FROM build-base AS build-libimobiledevice
FROM build-base as build-libimobiledevice
# Install dependencies
COPY --from=build-libplist /build /
@@ -88,7 +88,7 @@ RUN git clone https://github.com/libimobiledevice/libimobiledevice && cd libimob
# usbmuxd
# -------
FROM build-base AS build-usbmuxd
FROM build-base as build-usbmuxd
# Install dependencies
COPY --from=build-libplist /build /
@@ -104,8 +104,7 @@ RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
# Main image
# ----------
FROM python:3.10.14-alpine3.20 AS main
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /uvx /usr/local/bin/
FROM python:3.10.14-alpine3.20 as main
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
@@ -132,7 +131,7 @@ COPY --from=build-usbmuxd /build /
# Install mvt using the locally checked out source
COPY ./ mvt
RUN apk add --no-cache --virtual .build-deps git gcc musl-dev \
&& uv pip install --system --no-cache ./mvt \
&& PIP_NO_CACHE_DIR=1 pip3 install ./mvt \
&& apk del .build-deps git gcc musl-dev && rm -rf ./mvt
ENTRYPOINT [ "/usr/local/bin/mvt-ios" ]
+16 -11
View File
@@ -1,39 +1,44 @@
PWD = $(shell pwd)
UV ?= uv
autofix:
ruff format .
ruff check --fix .
check: ruff mypy
ruff:
$(UV) run ruff check .
ruff format --check .
ruff check -q .
mypy:
$(UV) run mypy
mypy
test:
$(UV) run pytest
python3 -m pytest
test-ci:
$(UV) run pytest -v
python3 -m pytest -v
install:
$(UV) sync
python3 -m pip install --upgrade -e .
test-requirements:
$(UV) sync --group dev
python3 -m pip install --upgrade -r test-requirements.txt
generate-proto-parsers:
# Generate python parsers for protobuf files
PROTO_FILES=$$(find src/mvt/android/parsers/proto/ -iname "*.proto"); \
$(UV) run protoc -Isrc/mvt/android/parsers/proto/ --python_betterproto2_out=src/mvt/android/parsers/proto/ $$PROTO_FILES
protoc -Isrc/mvt/android/parsers/proto/ --python_betterproto_out=src/mvt/android/parsers/proto/ $$PROTO_FILES
clean:
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/src/mvt.egg-info
dist:
$(UV) build
python3 -m pip install --upgrade build
python3 -m build
upload:
$(UV) tool run twine upload dist/*
python3 -m twine upload dist/*
test-upload:
$(UV) tool run twine upload --repository testpypi dist/*
python3 -m twine upload --repository testpypi dist/*
-15
View File
@@ -4,9 +4,6 @@
# Mobile Verification Toolkit
> [!IMPORTANT]
> We recently merged the "v3" branch. This introduced breaking changes. If you relied on mvt output in other scripts They might have broken. More details: https://github.com/mvt-project/mvt/issues/757
[![](https://img.shields.io/pypi/v/mvt)](https://pypi.org/project/mvt/)
[![Documentation Status](https://readthedocs.org/projects/mvt/badge/?version=latest)](https://docs.mvt.re/en/latest/?badge=latest)
[![CI](https://github.com/mvt-project/mvt/actions/workflows/tests.yml/badge.svg)](https://github.com/mvt-project/mvt/actions/workflows/tests.yml)
@@ -41,18 +38,6 @@ MVT can be installed from sources or from [PyPI](https://pypi.org/project/mvt/)
pip3 install mvt
```
You can also install MVT from PyPI with [uv](https://docs.astral.sh/uv/). First, install uv:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
Then install MVT as a command-line tool:
```bash
uv tool install mvt
```
For alternative installation options and known issues, please refer to the [documentation](https://docs.mvt.re/en/latest/install/) as well as [GitHub Issues](https://github.com/mvt-project/mvt/issues).
+1 -58
View File
@@ -2,61 +2,4 @@
Thank you for your interest in reporting security issues and vulnerabilities! Security research is of utmost importance and we take all reports seriously. If you discover an issue please report it to us right away!
Please DO NOT file a public issue, instead send your report privately to the MVT maintainers at Amnesty International via `security [at] amnesty [dot] tech`.
You can also write PGP-encrypted emails to key `CFBF9698DCA8EB2A80F48ADEA035A030FA04ED13`. The corresponding PGP public key is lited below.
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGlFPwsBEADQ+d7SeHrFPYv3wPOjWs2oMpp0DPdfIyGbg+iYWOC36FegZhKY
+WeK96GqJWt8wD6kwFUVwQI795WZrjSd1q4a7wR+kj/h7xlRB6ZfVICA6O5DOOm6
GNMvqy7ESm8g1XZDpb2u1BXmSS9X8f6rjB0e86kYsF1mB5/2USTM63jgDs0GGTkZ
Q1z4Mq4gYyqH32b3gvXkbb68LeQmONUIM3cgmec9q8/pNc1l7fcoLWhOVADRj17Q
plisa/EUf/SYqdtk9w7EHGggNenKNwVM235mkPcMqmE72bTpjT6XCxvZY3ByG5yi
7L+tHJU45ZuXtt62EvX03azxThVfSmH/WbRk8lH8+CW8XMmiWZphG4ydPWqgVKCB
2UOXm+6CQnKA+7Dt1AeK2t5ciATrv9LvwgSxk5WKc3288XFLA6eGMrTdQygYlLjJ
+42RSdK/7fCt/qk4q13oUw8ZTVcCia98uZFi704XuuYTH6NrntIB7j/0oucIS4Y9
cTWNO5LBerez4v8VI4YHcYESPeIWGFkXhvJzo0VMg1zidBLtiPoGF2JKZGwaK7/p
yY1xALskLp4H+5OY4eB1kf8kl4vGsEK8xA/NNzOiapVmwBXpvVvmXIQJE2k+olNf
sAuyB8+aO1Ws7tFYt3D+olC7iaprOdK7uA4GCgmYYhq6QQPg+cxfczgHfwARAQAB
tD1TZWN1cml0eSBMYWIgYXQgQW1uZXN0eSBJbnRlcm5hdGlvbmFsIDxzZWN1cml0
eUBhbW5lc3R5LnRlY2g+iQJRBBMBCAA7FiEEz7+WmNyo6yqA9IreoDWgMPoE7RMF
AmlFPwsCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQoDWgMPoE7RNr
2w//a88uP90uSN6lgeIwKsHr1ri27QIBbzCV6hLN/gZBFR2uaiOn/xfFDbnR0Cjo
5nMCJCT1k4nrPbMTlfmWLCD+YKELBzVqWlw4J2SOg3nznPl2JrL8QBKjwts0sF+h
QbRWDsT54wBZnl6ZJJ79eLShNTokBbKnQ7071dMrENr5e2P2sClQXyiIc51ga4FM
fHyhsx+GsrdiZNd2AH8912ljW1GuEi3epTO7KMZprmr37mjpZSUToiV59Yhl1Gbo
2pixkYJqi62DG02/gTpCjq9NH3cEMxcxjh4E7yCA8ggLG6+IN6woIvPIdOsnQ+Yj
d3H4rMNBjPSKoL+bdHILkCnp5HokcbVjNY3QAyOAF4qWhk4GtgpTshwxUmb4Tbay
tWLJC2bzjuUBxLkGzMVFfU3B96sVS4Fi0sBaEMBtHskl2f45X8LJhSq//Lw/2L/8
34uP/RxDSn+DPvj/yqMpekdCcmeFSTX1A19xkPcc0rVhMRde4VL338R86vzh0gMI
1LySDAhXZyVWzrQ5s3n6N3EvCaHCn3qu7ieyFJifCSR7gZqevCEznMQRVpkMTzUt
rk13Z6NOOb4IlTW7HFoY3omJG8Z5jV4kMIE7n6nb0qpNYQiG+YvjenQ3VrMoISyh
lpS2De8+oOtwrxBVX3+qKWvQqzufeE3416kw2Z+5mxH7bx25Ag0EaUU/CwEQALyZ
b+kwLN1yHObTm2yDBEn5HbCT3H1GremvPNmbAaTnfrjUngoKa8MuWWzbX5ptgmZR
UpYY/ylOYcgGydz58vUNrPlhIZT9UhmiifPgZLEXyd0uFpr/NsbRajHMkK10iEZf
h5bHNobiB7pGCu4Uj9e1cMiIZ4yEaYeyXYUoNHf6ISP39mJhHy6ov5yIpm9q0wzm
tGUQPupxGXmEZlOPr3lxqXQ3Ekdv6cWDY5r/oOq71QJ/HUQ13QUuGFIbhnMbT8zd
zaS6f/v772YKsWPc4NNUhtlf25VnQ4FuUtjCe3p6iYP4OVD8gJm0GvXyvyTuiQbL
CSk/378JiNT7nZzYXxrWchMwvEoMIU55+/UaBc50HI5xvDQ858CX7PYGiimcdsO1
EkQzhVxRfjlILfWrC2lgt+H5qhTn4Fah250Xe1PnLjXGHVUQnY/f3MFeiWQgf92b
02+MfvOeC5OKttP1z5lcx6RFWCIa1E/u8Nj7YrH9hk0ZBRAnBaeAncDFY8dfX2zX
VMoc0dV16gM7RrZ6i7D3CG3eLLkQlX0jbW9dzTuG/3f098EWB1p8vOfS/RbNCBRX
jqGiqacL/aFF3Ci3nQ4O5tSv1XipbgrUhvXnwm9pxrLPS/45iaO59WN4RRGWLLQ7
LHmeBxoa9avv0SdBYUL+eBxY46GXb/j5VLzHYhSnABEBAAGJAjYEGAEIACAWIQTP
v5aY3KjrKoD0it6gNaAw+gTtEwUCaUU/CwIbDAAKCRCgNaAw+gTtEyvsEACnyFFD
alOZTrrJTXNnUejuiExLh+qTO3T91p5bte597jpwCZnYGwkxEfffsqqhlY6ftEOf
d5tNWE5isai4v8XCbplWomz4KBpepxcn2b+9o5dSyr1vohEFuCJziZDsta1J2DX5
IE9U48kTgLDfdIBhuOyHNRkvXRHP2OVLCaiw4d9q+hlrraR8pehHt2BJSxh+QZoe
n0iHvIZCBIUA45zLEGmXFpNTGeEf2dKPp3xOkAXOhAMPptE0V1itkF3R7kEW4aFO
SZo8L3C1aWSz/gQ4/vvW5t1IJxirNMUgTMQFvqEkAwX3fm6GCxlgRSvTTRXdcrS8
6qyFdH1nkCNsavPahN3N2RGGIlWtODEMTO1Hjy0kZtTYdW+JH9sendliCoJES+yN
DjM125SgdAgrqlSYm/g8n9knWpxZv1QM6jU/sVz1J+l6/ixugL2i+CAL2d6uv4tT
QmXnu7Ei4/2kHBUu3Lf59MNgmLHm6F7AhOWErszSeoJKsp+3yA1oTT/npz67sRzY
VVyxz4NBIollna59a1lz0RhlWzNKqNB27jhylyM4ltdzHB7r4VMAVJyttozmIIOC
35ucYxl5BHLuapaRSaYHdUId1LOccYyaOOFF/PSyCu9dKzXk7zEz2HNcIboWSkAE
8ZDExMYM4WVpVCOj+frdsaBvzItHacRWuijtkw==
=JAXX
-----END PGP PUBLIC KEY BLOCK-----
```
Please DO NOT file a public issue, instead send your report privately to *nex [at] nex [dot] sx*. You can also write PGP-encrypted emails to [this key](https://keybase.io/nex/pgp_keys.asc?fingerprint=05216f3b86848a303c2fe37dd166f1667359d880).
+16 -19
View File
@@ -1,29 +1,26 @@
# ADB Analysis Removed
# Deprecation of ADB command in MVT
The ability to analyze Android devices directly over the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) has been removed from MVT.
!!! warning
Use [AndroidQF](https://github.com/mvt-project/androidqf) to collect forensic artifacts from an Android device, then analyze the collected output with:
The `mvt-android check-adb` command has been deprecated and removed from MVT.
```bash
mvt-android check-androidqf /path/to/androidqf-output
```
The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT due to several technical and forensic limitations.
For a standalone Android bug report, use:
```bash
mvt-android check-bugreport /path/to/bugreport.zip
```
## Reasons for Removal
## Reasons for Deprecation
1. **Inconsistent Data Collection Across Devices**
Android devices vary significantly in system architecture, security policy, and diagnostic log availability. This made it difficult to collect reliable forensic data across devices.
Android devices vary significantly in their system architecture, security policies, and available diagnostic logs. This inconsistency makes it difficult to ensure that MVT can reliably collect necessary forensic data across all devices.
2. **Incomplete Forensic Data Acquisition**
Direct ADB analysis did not retrieve a complete forensic snapshot. Critical artifacts such as full bug reports could be missing.
The `check-adb` command did not retrieve a full forensic snapshot of all available data on the device. For example, critical logs such as the **full bugreport** were not systematically collected, leading to potential gaps in forensic analysis. This can be a serious problem in scenarios where the analyst only had one time access to the Android device.
3. **Duplicated Analysis Paths**
Similar artifacts were parsed through separate ADB, AndroidQF, and bugreport modules, which made behavior harder to keep consistent.
4. **Code Duplication and Difficulty Ensuring Consistent Behavior Across Sources**
Similar forensic data such as "dumpsys" logs were being loaded and parsed by MVT's ADB, AndroidQF and Bugreport commands. Multiple modules were needed to handle each source format which created duplication leading to inconsistent
behavior and difficulties in maintaining the code base.
4. **Workflow Consistency**
MVT now focuses on analyzing acquired artifacts rather than interacting with live devices directly.
5. **Alignment with iOS Workflow**
MVT’s forensic workflow for iOS relies on pre-extracted artifacts, such as iTunes backups or filesystem dumps, rather than preforming commands or interactions directly on a live device. Removing the ADB functionality ensures a more consistent methodology across both Android and iOS mobile forensic.
## Alternative: Using AndroidQF for Forensic Data Collection
To replace the deprecated ADB-based approach, forensic analysts should use [AndroidQF](https://github.com/mvt-project/androidqf) for comprehensive data collection, followed by MVT for forensic analysis. The workflow is outlined in the MVT [Android methodology](./methodology.md)
+28
View File
@@ -0,0 +1,28 @@
# Downloading APKs from an Android phone
MVT allows you to attempt to download all available installed packages (APKs) from a device in order to further inspect them and potentially identify any which might be malicious in nature.
You can do so by launching the following command:
```bash
mvt-android download-apks --output /path/to/folder
```
It might take several minutes to complete.
!!! info
MVT will likely warn you it was unable to download certain installed packages. There is no reason to be alarmed: this is typically expected behavior when MVT attempts to download a system package it has no privileges to access.
Optionally, you can decide to enable lookups of the SHA256 hash of all the extracted APKs on [VirusTotal](https://www.virustotal.com). While these lookups do not provide any conclusive assessment on all of the extracted APKs, they might highlight any known malicious ones:
```bash
MVT_VT_API_KEY=<key> mvt-android download-apks --output /path/to/folder --virustotal
```
Please note that in order to use VirusTotal lookups you are required to provide your own API key through the `MVT_VT_API_KEY` environment variable. You should also note that VirusTotal enforces strict API usage. Be mindful that MVT might consume your hourly search quota.
In case you have a previous extraction of APKs you want to later check against VirusTotal, you can do so with the following arguments:
```bash
MVT_VT_API_KEY=<key> mvt-android download-apks --from-file /path/to/folder/apks.json --virustotal
```
+4 -2
View File
@@ -38,9 +38,11 @@ By separating artifact collection from forensic analysis, this approach ensures
For more information, refer to the [AndroidQF project documentation](https://github.com/mvt-project/androidqf).
## Android Debug Bridge analysis removed
## Check the device over Android Debug Bridge
The ability to analyze Android devices directly over ADB has been removed from MVT. Use AndroidQF for device acquisition and `mvt-android check-androidqf` for analysis.
The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT.
See the [Android ADB documentation](./adb.md) for more information.
## Check an Android Backup (SMS messages)
+2 -10
View File
@@ -2,17 +2,9 @@
The Mobile Verification Toolkit team welcomes contributions of new forensic modules or other contributions which help improve the software.
## Local environment
MVT uses `uv` for dependency management. To install the project and development dependencies from the locked environment, run:
```bash
make install
```
## Testing
MVT uses `pytest` for unit and integration tests. Code style consistency is maintained with `ruff` and `mypy`. All can
MVT uses `pytest` for unit and integration tests. Code style consistency is maintained with `flake8`, `ruff` and `black`. All can
be run automatically with:
```bash
@@ -32,4 +24,4 @@ MVT modules can be profiled with Python built-in `cProfile` by setting the `MVT_
MVT_PROFILE=1 dev/mvt-ios check-backup test_backup
```
Open an issue or PR if you are encountering significant performance issues when analyzing a device with MVT.
Open an issue or PR if you are encountering significant performance issues when analyzing a device with MVT.
+5
View File
@@ -0,0 +1,5 @@
mkdocs==1.6.1
mkdocs-autorefs==1.2.0
mkdocs-material==9.5.42
mkdocs-material-extensions==1.3.1
mkdocstrings==0.23.0
+2
View File
@@ -42,7 +42,9 @@ nav:
- Records extracted by mvt-ios: "ios/records.md"
- MVT for Android:
- Android Forensic Methodology: "android/methodology.md"
- Check over ADB: "android/adb.md"
- Check an Android Backup (SMS messages): "android/backup.md"
- Download APKs: "android/download_apks.md"
- Indicators of Compromise: "iocs.md"
- Development: "development.md"
- License: "license.md"
+42 -62
View File
@@ -1,11 +1,13 @@
[project]
name = "mvt"
dynamic = ["version"]
authors = [{ name = "Claudio Guarnieri", email = "nex@nex.sx" }]
authors = [
{name = "Claudio Guarnieri", email = "nex@nex.sx"}
]
maintainers = [
{ name = "Etienne Maynier", email = "tek@randhome.io" },
{ name = "Donncha Ó Cearbhaill", email = "donncha.ocearbhaill@amnesty.org" },
{ name = "Rory Flynn", email = "rory.flynn@amnesty.org" },
{name = "Etienne Maynier", email = "tek@randhome.io"},
{name = "Donncha Ó Cearbhaill", email = "donncha.ocearbhaill@amnesty.org"},
{name = "Rory Flynn", email = "rory.flynn@amnesty.org"}
]
description = "Mobile Verification Toolkit"
readme = "README.md"
@@ -14,86 +16,64 @@ classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Information Technology",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python"
]
dependencies = [
"click==8.3.2",
"rich==14.3.3",
"tld==0.13.1",
"requests==2.33.1",
"simplejson==3.20.2",
"packaging==26.0",
"appdirs==1.4.4",
"iOSbackup==0.9.925",
"adb-shell[usb]==0.4.4",
"libusb1==3.3.1",
"cryptography==47.0.0",
"PyYAML>=6.0.2",
"pyahocorasick==2.2.0",
"betterproto2==0.9.1",
"pydantic==2.13.3",
"pydantic-settings==2.13.1",
"NSKeyedUnArchiver==1.5.2",
"python-dateutil==2.9.0.post0",
"tzdata==2026.1",
"click >=8.1.3",
"rich >=12.6.0",
"tld >=0.12.6",
"requests >=2.28.1",
"simplejson >=3.17.6",
"packaging >=21.3",
"appdirs >=1.4.4",
"iOSbackup >=0.9.923",
"cryptography >=42.0.5",
"pyyaml >=6.0",
"pyahocorasick >= 2.0.0",
"betterproto >=1.2.0",
"pydantic >= 2.10.0",
"pydantic-settings >= 2.7.0",
'backports.zoneinfo; python_version < "3.9"',
]
requires-python = ">= 3.10"
requires-python = ">= 3.8"
[project.urls]
homepage = "https://docs.mvt.re/en/latest/"
repository = "https://github.com/mvt-project/mvt"
[project.scripts]
mvt-ios = "mvt.ios: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",
"betterproto2-compiler",
"types-PyYAML>=6.0.12.20250516",
"types-python-dateutil>=2.9.0.20250822",
"types-requests>=2.32.4.20250913",
]
docs = [
"mkdocs==1.6.1",
"mkdocs-autorefs==1.4.4",
"mkdocs-material==9.7.6",
"mkdocs-material-extensions==1.3.1",
"mkdocstrings==1.0.4",
]
mvt-ios = "mvt.ios:cli"
mvt-android = "mvt.android:cli"
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[tool.coverage.run]
omit = ["tests/*"]
omit = [
"tests/*",
]
[tool.coverage.html]
directory = "htmlcov"
directory= "htmlcov"
[tool.mypy]
install_types = false
install_types = true
non_interactive = true
ignore_missing_imports = true
packages = "src"
[tool.pytest.ini_options]
addopts = "-ra -q --cov=mvt --cov-report html --junitxml=pytest.xml --cov-report=term-missing:skip-covered"
testpaths = ["tests"]
testpaths = [
"tests"
]
[tool.ruff]
lint.select = ["C90", "E", "F", "W"] # flake8 default set
lint.ignore = [
"E501", # don't enforce line length violations
"C901", # complex-structure
[tool.ruff.lint]
select = ["C90", "E", "F", "W"] # flake8 default set
ignore = [
"E501", # don't enforce line length violations
"C901", # complex-structure
# These were previously ignored but don't seem to be required:
# "E265", # no-space-after-block-comment
@@ -105,14 +85,14 @@ lint.ignore = [
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] # unused-import
"__init__.py" = ["F401"] # unused-import
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.setuptools]
include-package-data = true
package-dir = { "" = "src" }
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
@@ -121,4 +101,4 @@ where = ["src"]
mvt = ["ios/data/*.json"]
[tool.setuptools.dynamic]
version = { attr = "mvt.common.version.MVT_VERSION" }
version = {attr = "mvt.common.version.MVT_VERSION"}
+13 -29
View File
@@ -20,39 +20,23 @@ class AndroidArtifact(Artifact):
:param binary: whether the dumpsys should be pared as binary or not (bool)
:return: section extracted (string or bytes)
"""
lines = []
in_section = False
delimiter_str = "------------------------------------------------------------------------------"
delimiter_bytes = b"------------------------------------------------------------------------------"
delimiter = "------------------------------------------------------------------------------"
if binary:
lines_bytes = []
for line in dumpsys.splitlines(): # type: ignore[union-attr]
if line.strip() == separator: # type: ignore[arg-type]
in_section = True
continue
delimiter = delimiter.encode("utf-8")
if not in_section:
continue
for line in dumpsys.splitlines():
if line.strip() == separator:
in_section = True
continue
if line.strip().startswith(delimiter_bytes): # type: ignore[arg-type]
break
if not in_section:
continue
lines_bytes.append(line) # type: ignore[arg-type]
if line.strip().startswith(delimiter):
break
return b"\n".join(lines_bytes) # type: ignore[return-value,arg-type]
else:
lines_str = []
for line in dumpsys.splitlines(): # type: ignore[union-attr]
if line.strip() == separator: # type: ignore[arg-type]
in_section = True
continue
lines.append(line)
if not in_section:
continue
if line.strip().startswith(delimiter_str): # type: ignore[arg-type]
break
lines_str.append(line) # type: ignore[arg-type]
return "\n".join(lines_str) # type: ignore[return-value,arg-type]
return b"\n".join(lines) if binary else "\n".join(lines)
@@ -16,9 +16,8 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
for result in self.results:
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
continue
def parse(self, content: str) -> None:
+6 -12
View File
@@ -4,7 +4,6 @@
# https://license.mvt.re/1.1/
import base64
import binascii
import hashlib
from .artifact import AndroidArtifact
@@ -84,22 +83,17 @@ class DumpsysADBArtifact(AndroidArtifact):
return keystore
@staticmethod
def calculate_key_info(user_key: bytes) -> dict:
def calculate_key_info(user_key: bytes) -> str:
if b" " in user_key:
key_base64, user = user_key.split(b" ", 1)
else:
key_base64, user = user_key, b""
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 = ""
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)]
)
return {
"user": user.decode("utf-8"),
"fingerprint": key_fingerprint_colon,
+9 -6
View File
@@ -4,13 +4,13 @@
# https://license.mvt.re/1.1/
from datetime import datetime
from typing import Any
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
from mvt.common.utils import convert_datetime_to_iso
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
from .artifact import AndroidArtifact
RISKY_PERMISSIONS = ["REQUEST_INSTALL_PACKAGES"]
RISKY_PACKAGES = ["com.android.shell"]
@@ -45,8 +45,9 @@ class DumpsysAppopsArtifact(AndroidArtifact):
if self.indicators:
ioc_match = self.indicators.check_app_id(result.get("package_name"))
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
self.get_slug(), ioc_match.message, "", result
)
continue
@@ -65,6 +66,7 @@ class DumpsysAppopsArtifact(AndroidArtifact):
cleaned_result = result.copy()
cleaned_result["permissions"] = [perm]
self.alertstore.medium(
self.get_slug(),
f"Package '{result['package_name']}' had risky permission '{perm['name']}' set to '{entry['access']}' at {entry['timestamp']}",
entry["timestamp"],
cleaned_result,
@@ -78,6 +80,7 @@ class DumpsysAppopsArtifact(AndroidArtifact):
cleaned_result = result.copy()
cleaned_result["permissions"] = [perm]
self.alertstore.medium(
self.get_slug(),
f"Risky package '{result['package_name']}' had '{perm['name']}' permission set to '{entry['access']}' at {entry['timestamp']}",
entry["timestamp"],
cleaned_result,
@@ -85,9 +88,9 @@ class DumpsysAppopsArtifact(AndroidArtifact):
def parse(self, output: str) -> None:
# self.results: List[Dict[str, Any]] = []
perm: dict[str, Any] = {}
package: dict[str, Any] = {}
entry: dict[str, Any] = {}
perm = {}
package = {}
entry = {}
uid = None
in_packages = False
@@ -3,11 +3,9 @@
# 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 mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
from .artifact import AndroidArtifact
from mvt.common.module_types import ModuleSerializedResult, ModuleAtomicResult
class DumpsysBatteryDailyArtifact(AndroidArtifact):
@@ -16,23 +14,12 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
"""
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
action = record.get("action", "update")
package_name = record["package_name"]
vers = record["vers"]
if vers == "0":
data = f"Recorded uninstall of package {package_name} (vers 0)"
elif action == "downgrade":
prev_vers = record.get("previous_vers", "unknown")
data = f"Recorded downgrade of package {package_name} from vers {prev_vers} to vers {vers}"
else:
data = f"Recorded update of package {package_name} with vers {vers}"
return {
"timestamp": record["from"],
"module": self.__class__.__name__,
"event": "battery_daily",
"data": data,
"data": f"Recorded update of package {record['package_name']} "
f"with vers {record['vers']}",
}
def check_indicators(self) -> None:
@@ -42,17 +29,13 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
for result in self.results:
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
continue
def parse(self, output: str) -> None:
daily = None
daily_updates: list[dict[str, Any]] = []
package_versions: dict[
str, str
] = {} # Track package versions to detect downgrades
daily_updates = []
for line in output.splitlines():
if line.startswith(" Daily from "):
if len(daily_updates) > 0:
@@ -81,43 +64,15 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
break
if not already_seen:
update_record: dict[str, Any] = {
"action": "update",
"from": daily["from"],
"to": daily["to"],
"package_name": package_name,
"vers": vers_nr,
}
# Check for uninstall (version 0)
if vers_nr == "0":
self.alertstore.medium(
f"Detected uninstall of package {package_name} (vers 0)",
daily["from"],
update_record,
)
# Check for downgrade
elif package_name in package_versions:
try:
current_vers = int(vers_nr)
previous_vers = int(package_versions[package_name])
if current_vers < previous_vers:
update_record["action"] = "downgrade"
update_record["previous_vers"] = str(previous_vers)
self.alertstore.medium(
f"Detected downgrade of package {package_name} "
f"from vers {previous_vers} to vers {current_vers}",
daily["from"],
update_record,
)
except ValueError:
# If version numbers aren't integers, skip comparison
pass
# Update tracking dictionary
package_versions[package_name] = vers_nr
daily_updates.append(update_record)
daily_updates.append(
{
"action": "update",
"from": daily["from"],
"to": daily["to"],
"package_name": package_name,
"vers": vers_nr,
}
)
if len(daily_updates) > 0:
self.results.extend(daily_updates)
@@ -18,9 +18,8 @@ class DumpsysBatteryHistoryArtifact(AndroidArtifact):
for result in self.results:
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
continue
def parse(self, data: str) -> None:
+2 -1
View File
@@ -22,8 +22,9 @@ class DumpsysDBInfoArtifact(AndroidArtifact):
for part in path.split("/"):
ioc_match = self.indicators.check_app_id(part)
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
self.get_slug(), ioc_match.message, "", result
)
continue
@@ -14,8 +14,9 @@ class DumpsysPackageActivitiesArtifact(AndroidArtifact):
for activity in self.results:
ioc_match = self.indicators.check_app_id(activity["package_name"])
if ioc_match:
activity["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message, "", activity, matched_indicator=ioc_match.ioc
self.get_slug(), ioc_match.message, "", activity
)
continue
+10 -8
View File
@@ -7,9 +7,9 @@ import re
from typing import Any, Dict, List
from mvt.android.utils import ROOT_PACKAGES
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
from .artifact import AndroidArtifact
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
class DumpsysPackagesArtifact(AndroidArtifact):
@@ -18,10 +18,12 @@ class DumpsysPackagesArtifact(AndroidArtifact):
# XXX: De-duplication Package detections
if result["package_name"] in ROOT_PACKAGES:
self.alertstore.medium(
self.get_slug(),
f'Found an installed package related to rooting/jailbreaking: "{result["package_name"]}"',
"",
result,
)
self.alertstore.log_latest()
continue
if not self.indicators:
@@ -29,9 +31,9 @@ class DumpsysPackagesArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(result.get("package_name", ""))
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
self.alertstore.log_latest()
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
records = []
@@ -61,15 +63,15 @@ class DumpsysPackagesArtifact(AndroidArtifact):
"""
Parse one entry of a dumpsys package information
"""
details: Dict[str, Any] = {
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"permissions": list(),
"requested_permissions": list(),
"permissions": [],
"requested_permissions": [],
}
in_install_permissions = False
in_runtime_permissions = False
@@ -147,7 +149,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
results = []
package_name = None
package = {}
lines: list[str] = []
lines = []
for line in output.splitlines():
if line.startswith(" Package ["):
if len(lines) > 0:
@@ -18,9 +18,8 @@ class DumpsysPlatformCompatArtifact(AndroidArtifact):
for result in self.results:
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
continue
def parse(self, data: str) -> None:
@@ -52,16 +52,14 @@ class DumpsysReceiversArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(receiver["package_name"])
if ioc_match:
receiver["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message,
"",
{intent: receiver},
matched_indicator=ioc_match.ioc,
self.get_slug(), ioc_match.message, "", {intent: receiver}
)
continue
def parse(self, output: str) -> None:
self.results: dict[str, list[dict[str, str]]] = {}
self.results = {}
in_receiver_resolver_table = False
in_non_data_actions = False
+6 -8
View File
@@ -39,10 +39,10 @@ class GetProp(AndroidArtifact):
if not matches or len(matches[0]) != 2:
continue
prop_entry = {"name": matches[0][0], "value": matches[0][1]}
self.results.append(prop_entry)
entry = {"name": matches[0][0], "value": matches[0][1]}
self.results.append(entry)
def get_device_timezone(self) -> str | None:
def get_device_timezone(self) -> str:
"""
Get the device timezone from the getprop results
@@ -60,8 +60,7 @@ class GetProp(AndroidArtifact):
if entry["name"] == "ro.build.version.security_patch":
warning_message = warn_android_patch_level(entry["value"], self.log)
if isinstance(warning_message, str):
self.alertstore.medium(warning_message, "", entry)
self.alertstore.medium(self.get_slug(), warning_message, "", entry)
if not self.indicators:
return
@@ -71,6 +70,5 @@ class GetProp(AndroidArtifact):
result.get("name", "")
)
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
-197
View File
@@ -1,197 +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.alertstore.high(
"Root detected /system partition is mounted as read-write (rw)",
"",
mount,
)
else:
self.alertstore.high(
f"System partition {mount_point} is mounted as read-write (rw). This may indicate system modifications.",
"",
mount,
)
# 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.alertstore.medium(
f"Suspicious mount options found for {mount_point}: {', '.join(suspicious_opts)}",
"",
mount,
)
# 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:
self.alertstore.critical(
ioc.message,
"",
mount,
matched_indicator=ioc.ioc,
)
# Check device paths for indicators
ioc = self.indicators.check_file_path(mount.get("device", ""))
if ioc:
self.alertstore.critical(
ioc.message,
"",
mount,
matched_indicator=ioc.ioc,
)
+4 -6
View File
@@ -60,13 +60,11 @@ class Processes(AndroidArtifact):
ioc_match = self.indicators.check_app_id(proc_name)
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
continue
ioc_match = self.indicators.check_process(proc_name)
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
+5
View File
@@ -51,6 +51,11 @@ ANDROID_DANGEROUS_SETTINGS = [
"key": "send_action_app_error",
"safe_value": "1",
},
{
"description": "enabled installation of non Google Play apps",
"key": "install_non_market_apps",
"safe_value": "0",
},
{
"description": "enabled accessibility services",
"key": "accessibility_enabled",
+66 -62
View File
@@ -7,15 +7,14 @@ import datetime
from typing import List, Optional
import pydantic
import betterproto2
from dateutil import parser
import betterproto
from mvt.android.parsers.proto.tombstone import Tombstone
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
from mvt.common.utils import convert_datetime_to_iso
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
from mvt.android.parsers.proto.tombstone import Tombstone
from .artifact import AndroidArtifact
TOMBSTONE_DELIMITER = "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***"
# Map the legacy crash file keys to the new format.
@@ -54,7 +53,7 @@ class TombstoneCrashResult(pydantic.BaseModel):
file_name: str
file_timestamp: str # We store the timestamp as a string to avoid timezone issues
build_fingerprint: str
revision: str
revision: int
arch: Optional[str] = None
timestamp: str # We store the timestamp as a string to avoid timezone issues
process_uptime: Optional[int] = None
@@ -64,14 +63,14 @@ class TombstoneCrashResult(pydantic.BaseModel):
process_name: Optional[str] = None
binary_path: Optional[str] = None
selinux_label: Optional[str] = None
uid: int
uid: Optional[int] = None
signal_info: SignalInfo
cause: Optional[str] = None
extra: Optional[str] = None
class TombstoneCrashArtifact(AndroidArtifact):
"""
""" "
Parser for Android tombstone crash files.
This parser can parse both text and protobuf tombstone crash files.
@@ -95,18 +94,17 @@ class TombstoneCrashArtifact(AndroidArtifact):
for result in self.results:
ioc_match = self.indicators.check_process(result["process_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
continue
if result.get("command_line", []):
command_name = result.get("command_line")[0].split("/")[-1]
command_name = result["command_line"][0]
ioc_match = self.indicators.check_process(command_name)
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
self.get_slug(), ioc_match.message, "", result
)
continue
@@ -117,6 +115,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
]
if result["uid"] in SUSPICIOUS_UIDS:
self.alertstore.medium(
self.get_slug(),
(
f"Potentially suspicious crash in process '{result['process_name']}' "
f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}"
@@ -128,11 +127,11 @@ class TombstoneCrashArtifact(AndroidArtifact):
def parse_protobuf(
self, file_name: str, file_timestamp: datetime.datetime, data: bytes
) -> 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_dict = tombstone_pb.to_dict(
casing=betterproto2.Casing.SNAKE, include_default_values=True
)
tombstone_dict = tombstone_pb.to_dict(betterproto.Casing.SNAKE)
# Add some extra metadata
tombstone_dict["timestamp"] = self._parse_timestamp_string(
@@ -149,23 +148,21 @@ class TombstoneCrashArtifact(AndroidArtifact):
def parse(
self, file_name: str, file_timestamp: datetime.datetime, content: bytes
) -> None:
"""Parse text Android tombstone crash files."""
"""
Parse text Android tombstone crash files.
"""
# Split the tombstone file into a dictonary
tombstone_dict = {
"file_name": file_name,
"file_timestamp": convert_datetime_to_iso(file_timestamp),
}
lines = content.decode("utf-8").splitlines()
for line_num, line in enumerate(lines, 1):
for line in lines:
if not line.strip() or TOMBSTONE_DELIMITER in line:
continue
try:
for key, destination_key in TOMBSTONE_TEXT_KEY_MAPPINGS.items():
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)}")
for key, destination_key in TOMBSTONE_TEXT_KEY_MAPPINGS.items():
self._parse_tombstone_line(line, key, destination_key, tombstone_dict)
# Validate the tombstone and add it to the results
tombstone = TombstoneCrashResult.model_validate(tombstone_dict)
@@ -175,7 +172,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
self, line: str, key: str, destination_key: str, tombstone: dict
) -> bool:
if not line.startswith(f"{key}"):
return False
return None
if key == "pid":
return self._load_pid_line(line, tombstone)
@@ -194,7 +191,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
raise ValueError(f"Expected key {key}, got {line_key}")
value_clean = value.strip().strip("'")
if destination_key == "uid":
if destination_key in ["uid", "revision"]:
tombstone[destination_key] = int(value_clean)
elif destination_key == "process_uptime":
# eg. "Process uptime: 40s"
@@ -207,50 +204,51 @@ class TombstoneCrashArtifact(AndroidArtifact):
return True
def _load_pid_line(self, line: str, tombstone: dict) -> bool:
try:
parts = line.split(" >>> ") if " >>> " in line else line.split(">>>")
process_info = parts[0]
pid_part, tid_part, name_part = [part.strip() for part in line.split(",")]
# Parse pid, tid, name from process info
info_parts = [p.strip() for p in process_info.split(",")]
for info in info_parts:
key, value = info.split(":", 1)
key = key.strip()
value = value.strip()
pid_key, pid_value = pid_part.split(":", 1)
if pid_key != "pid":
raise ValueError(f"Expected key pid, got {pid_key}")
pid_value = int(pid_value.strip())
if key == "pid":
tombstone["pid"] = int(value)
elif key == "tid":
tombstone["tid"] = int(value)
elif key == "name":
tombstone["process_name"] = value
tid_key, tid_value = tid_part.split(":", 1)
if tid_key != "tid":
raise ValueError(f"Expected key tid, got {tid_key}")
tid_value = int(tid_value.strip())
# Extract binary path if it exists
if len(parts) > 1:
tombstone["binary_path"] = parts[1].strip().rstrip(" <")
name_key, name_value = name_part.split(":", 1)
if name_key != "name":
raise ValueError(f"Expected key name, got {name_key}")
name_value = name_value.strip()
process_name, binary_path = self._parse_process_name(name_value, tombstone)
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:
raise ValueError(f"Failed to parse PID line: {str(e)}")
def _parse_process_name(self, process_name_part, tombstone: dict) -> bool:
process_name, process_path = process_name_part.split(">>>")
process_name = process_name.strip()
binary_path = process_path.strip().split(" ")[0]
return process_name, binary_path
def _load_signal_line(self, line: str, tombstone: dict) -> bool:
signal_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]:
match = part.split(prefix)[1]
number = int(match.split()[0])
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 ")
code_part = code.split("code ")[1]
code_number, code_name = code_part.split(" ")
code_name = code_name.strip("()")
tombstone["signal_info"] = {
"code": code_number,
"code": int(code_number),
"code_name": code_name,
"name": signal_name,
"number": signal_number,
"number": int(signal_code),
}
return True
@@ -261,7 +259,13 @@ class TombstoneCrashArtifact(AndroidArtifact):
@staticmethod
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.
local_timestamp = timestamp_parsed.replace(tzinfo=datetime.timezone.utc)
return convert_datetime_to_iso(local_timestamp)
+15 -48
View File
@@ -9,36 +9,35 @@ import click
from mvt.common.cmd_check_iocs import CmdCheckIOCS
from mvt.common.help import (
HELP_MSG_ANDROID_BACKUP_PASSWORD,
HELP_MSG_CHECK_ADB_REMOVED,
HELP_MSG_CHECK_ADB_REMOVED_DESCRIPTION,
HELP_MSG_CHECK_ANDROID_BACKUP,
HELP_MSG_CHECK_ANDROIDQF,
HELP_MSG_CHECK_BUGREPORT,
HELP_MSG_CHECK_IOCS,
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
HELP_MSG_DISABLE_UPDATE_CHECK,
HELP_MSG_HASHES,
HELP_MSG_VERSION,
HELP_MSG_OUTPUT,
HELP_MSG_VERBOSE,
HELP_MSG_IOC,
HELP_MSG_LIST_MODULES,
HELP_MSG_MODULE,
HELP_MSG_NONINTERACTIVE,
HELP_MSG_OUTPUT,
HELP_MSG_ANDROID_BACKUP_PASSWORD,
HELP_MSG_CHECK_ADB_REMOVED,
HELP_MSG_CHECK_ADB_REMOVED_DESCRIPTION,
HELP_MSG_CHECK_BUGREPORT,
HELP_MSG_CHECK_ANDROID_BACKUP,
HELP_MSG_CHECK_ANDROIDQF,
HELP_MSG_HASHES,
HELP_MSG_CHECK_IOCS,
HELP_MSG_STIX2,
HELP_MSG_VERBOSE,
HELP_MSG_VERSION,
)
from mvt.common.logo import logo
from mvt.common.updates import IndicatorsUpdates
from mvt.common.utils import init_logging, set_verbose_logging
from .cmd_check_androidqf import CmdAndroidCheckAndroidQF
from .cmd_check_backup import CmdAndroidCheckBackup
from .cmd_check_bugreport import CmdAndroidCheckBugreport
from .modules.androidqf import ANDROIDQF_MODULES
from .modules.backup import BACKUP_MODULES
from .modules.backup.helpers import cli_load_android_backup_password
from .modules.bugreport import BUGREPORT_MODULES
from .modules.androidqf import ANDROIDQF_MODULES
init_logging()
log = logging.getLogger("mvt")
@@ -46,37 +45,12 @@ log = logging.getLogger("mvt")
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
# ==============================================================================
@click.group(invoke_without_command=False)
@click.option(
"--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,
)
def cli():
logo()
# ==============================================================================
@@ -96,7 +70,6 @@ def version():
@click.pass_context
def check_adb(ctx):
log.error(HELP_MSG_CHECK_ADB_REMOVED_DESCRIPTION)
ctx.exit(1)
# ==============================================================================
@@ -128,8 +101,6 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
ioc_files=iocs,
module_name=module,
hashes=True,
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
if list_modules:
@@ -188,8 +159,6 @@ def check_backup(
"interactive": not non_interactive,
"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:
@@ -250,8 +219,6 @@ def check_androidqf(
"interactive": not non_interactive,
"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:
+20 -23
View File
@@ -9,11 +9,12 @@ import zipfile
from pathlib import Path
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.indicators import Indicators
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
from mvt.android.cmd_check_backup import CmdAndroidCheckBackup
from .modules.androidqf import ANDROIDQF_MODULES
from .modules.androidqf.base import AndroidQFModule
@@ -44,8 +45,6 @@ class CmdAndroidCheckAndroidQF(Command):
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:
super().__init__(
target_path=target_path,
@@ -58,8 +57,6 @@ class CmdAndroidCheckAndroidQF(Command):
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-androidqf"
@@ -140,7 +137,6 @@ class CmdAndroidCheckAndroidQF(Command):
raise NoAndroidQFBackup
def run_bugreport_cmd(self) -> bool:
bugreport = None
try:
bugreport = self.load_bugreport()
except NoAndroidQFBugReport:
@@ -167,8 +163,6 @@ class CmdAndroidCheckAndroidQF(Command):
if bugreport:
bugreport.close()
return True
def run_backup_cmd(self) -> bool:
try:
backup = self.load_backup()
@@ -177,22 +171,25 @@ class CmdAndroidCheckAndroidQF(Command):
"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()
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.timeline.extend(cmd.timeline)
self.alertstore.extend(cmd.alertstore.alerts)
finally:
if backup:
backup.close()
self.timeline.extend(cmd.timeline)
self.alertstore.extend(cmd.alertstore.alerts)
return True
def finish(self) -> None:
"""
+7 -17
View File
@@ -39,8 +39,6 @@ class CmdAndroidCheckBackup(Command):
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:
super().__init__(
target_path=target_path,
@@ -53,8 +51,6 @@ class CmdAndroidCheckBackup(Command):
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-backup"
@@ -93,28 +89,22 @@ class CmdAndroidCheckBackup(Command):
self.__files.append(member.name)
def init(self) -> None:
if not self.target_path: # type: ignore[has-type]
if not self.target_path:
return
# Type guard: we know it's not None here after the check above
assert self.target_path is not None # type: ignore[has-type]
# Use a different local variable name to avoid any scoping issues
backup_path: str = self.target_path # type: ignore[has-type]
if os.path.isfile(backup_path):
if os.path.isfile(self.target_path):
self.__type = "ab"
with open(backup_path, "rb") as handle:
with open(self.target_path, "rb") as handle:
ab_file_bytes = handle.read()
self.from_ab(ab_file_bytes)
elif os.path.isdir(backup_path):
elif os.path.isdir(self.target_path):
self.__type = "folder"
backup_path = Path(backup_path).absolute().as_posix()
self.target_path = backup_path
for root, subdirs, subfiles in os.walk(os.path.abspath(backup_path)):
self.target_path = Path(self.target_path).absolute().as_posix()
for root, subdirs, subfiles in os.walk(os.path.abspath(self.target_path)):
for fname in subfiles:
self.__files.append(
os.path.relpath(os.path.join(root, fname), backup_path)
os.path.relpath(os.path.join(root, fname), self.target_path)
)
else:
log.critical(
-6
View File
@@ -30,8 +30,6 @@ class CmdAndroidCheckBugreport(Command):
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:
super().__init__(
target_path=target_path,
@@ -44,8 +42,6 @@ class CmdAndroidCheckBugreport(Command):
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-bugreport"
@@ -96,8 +92,6 @@ class CmdAndroidCheckBugreport(Command):
if self.__format == "zip":
module.from_zip(self.__zip, self.__files)
else:
if not self.target_path:
raise ValueError("target_path is not set")
module.from_dir(self.target_path, self.__files)
def finish(self) -> None:
+32
View File
@@ -0,0 +1,32 @@
# 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 .chrome_history import ChromeHistory
from .dumpsys_full import DumpsysFull
from .files import Files
from .getprop import Getprop
from .logcat import Logcat
from .packages import Packages
from .processes import Processes
from .root_binaries import RootBinaries
from .selinux_status import SELinuxStatus
from .settings import Settings
from .sms import SMS
from .whatsapp import Whatsapp
ADB_MODULES = [
ChromeHistory,
SMS,
Whatsapp,
Processes,
Getprop,
Settings,
SELinuxStatus,
DumpsysFull,
Packages,
Logcat,
RootBinaries,
Files,
]
@@ -0,0 +1,116 @@
# 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 os
import sqlite3
from typing import Optional
from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleSerializedResult,
ModuleResults,
)
from .base import AndroidExtraction
CHROME_HISTORY_PATH = "data/data/com.android.chrome/app_chrome/Default/History"
class ChromeHistory(AndroidExtraction):
"""This module extracts records from Android's Chrome browsing history."""
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: ModuleResults = [],
) -> 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 serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "visit",
"data": f"{record['id']} - {record['url']} (visit ID: {record['visit_id']}, "
f"redirect source: {record['redirect_source']})",
}
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc_match = self.indicators.check_url(result["url"])
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
def _parse_db(self, db_path: str) -> None:
"""Parse a Chrome History database file.
:param db_path: Path to the History database to process.
"""
assert isinstance(self.results, list) # assert results type for mypy
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute(
"""
SELECT
urls.id,
urls.url,
visits.id,
visits.visit_time,
visits.from_visit
FROM urls
JOIN visits ON visits.url = urls.id
ORDER BY visits.visit_time;
"""
)
for item in cur:
self.results.append(
{
"id": item[0],
"url": item[1],
"visit_id": item[2],
"timestamp": item[3],
"isodate": convert_datetime_to_iso(
convert_chrometime_to_datetime(item[3])
),
"redirect_source": item[4],
}
)
cur.close()
conn.close()
self.log.info("Extracted a total of %d history items", len(self.results))
def run(self) -> None:
self._adb_connect()
try:
self._adb_process_file(
os.path.join("/", CHROME_HISTORY_PATH), self._parse_db
)
except Exception as exc:
self.log.error(exc)
self._adb_disconnect()
@@ -0,0 +1,46 @@
# 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 os
from typing import Optional
from .base import AndroidExtraction
from mvt.common.module_types import ModuleResults
class DumpsysFull(AndroidExtraction):
"""This module extracts stats on battery consumption by processes."""
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: ModuleResults = [],
) -> 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")
if self.results_path:
output_path = os.path.join(self.results_path, "dumpsys.txt")
with open(output_path, "w", encoding="utf-8") as handle:
handle.write(output)
self.log.info("Full dumpsys output stored at %s", output_path)
self._adb_disconnect()
+156
View File
@@ -0,0 +1,156 @@
# 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 os
import stat
from typing import Optional, Union
from mvt.common.utils import convert_unix_to_iso
from mvt.common.module_types import ModuleResults
from .base import AndroidExtraction
ANDROID_TMP_FOLDERS = [
"/tmp/",
"/data/local/tmp/",
]
ANDROID_MEDIA_FOLDERS = [
"/data/media/0",
"/sdcard/",
]
class Files(AndroidExtraction):
"""This module extracts the list of files on the device."""
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: ModuleResults = [],
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.full_find = False
def serialize(self, record: dict) -> Union[dict, list, None]:
if "modified_time" in record:
return {
"timestamp": record["modified_time"],
"module": self.__class__.__name__,
"event": "file_modified",
"data": record["path"],
}
return None
def check_indicators(self) -> None:
for result in self.results:
if result.get("is_suid"):
self.log.warning(
'Found an SUID file in a non-standard directory "%s".',
result["path"],
)
if self.indicators and self.indicators.check_file_path(result["path"]):
self.log.warning(
'Found a known suspicous file at path: "%s"', result["path"]
)
self.detected.append(result)
def backup_file(self, file_path: str) -> None:
if not self.results_path:
return
local_file_name = file_path.replace("/", "_").replace(" ", "-")
local_files_folder = os.path.join(self.results_path, "files")
if not os.path.exists(local_files_folder):
os.mkdir(local_files_folder)
local_file_path = os.path.join(local_files_folder, local_file_name)
try:
self._adb_download(remote_path=file_path, local_path=local_file_path)
except Exception:
pass
else:
self.log.info(
"Downloaded file %s to local copy at %s", file_path, local_file_path
)
def find_files(self, folder: str) -> None:
assert isinstance(self.results, list)
if self.full_find:
cmd = f"find '{folder}' -type f -printf '%T@ %m %s %u %g %p\n' 2> /dev/null"
output = self._adb_command(cmd)
for file_line in output.splitlines():
file_info = file_line.rstrip().split(" ", 5)
if len(file_line) < 6:
self.log.info("Skipping invalid file info - %s", file_line.rstrip())
continue
[unix_timestamp, mode, size, owner, group, full_path] = file_info
mod_time = convert_unix_to_iso(unix_timestamp)
self.results.append(
{
"path": full_path,
"modified_time": mod_time,
"mode": mode,
"is_suid": (int(mode, 8) & stat.S_ISUID) == 2048,
"is_sgid": (int(mode, 8) & stat.S_ISGID) == 1024,
"size": size,
"owner": owner,
"group": group,
}
)
else:
output = self._adb_command(f"find '{folder}' -type f 2> /dev/null")
for file_line in output.splitlines():
self.results.append({"path": file_line.rstrip()})
def run(self) -> None:
self._adb_connect()
cmd = "find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null"
output = self._adb_command(cmd)
if output or output.strip().splitlines():
self.full_find = True
for tmp_folder in ANDROID_TMP_FOLDERS:
self.find_files(tmp_folder)
for entry in self.results:
self.log.info("Found file in tmp folder at path %s", entry.get("path"))
self.backup_file(entry.get("path"))
for media_folder in ANDROID_MEDIA_FOLDERS:
self.find_files(media_folder)
self.log.info(
"Found %s files in primary Android tmp and media folders", len(self.results)
)
if self.module_options.get("fast_mode", None):
self.log.info(
"The `fast_mode` option was enabled: skipping full file listing"
)
else:
self.log.info("Processing full file listing. This may take a while...")
self.find_files("/")
self.log.info("Found %s total files", len(self.results))
self._adb_disconnect()
+44
View File
@@ -0,0 +1,44 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
from .base import AndroidExtraction
from mvt.common.module_types import ModuleResults
class Getprop(GetPropArtifact, AndroidExtraction):
"""This module extracts device properties from getprop command."""
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: ModuleResults = [],
) -> 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 = {} if not results else results
def run(self) -> None:
self._adb_connect()
output = self._adb_command("getprop")
self._adb_disconnect()
self.parse(output)
self.log.info("Extracted %d Android system properties", len(self.results))
+58
View File
@@ -0,0 +1,58 @@
# 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 os
from typing import Optional
from .base import AndroidExtraction
from mvt.common.module_types import ModuleResults
class Logcat(AndroidExtraction):
"""This module extracts details on installed packages."""
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: ModuleResults = [],
) -> 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()
# Get the current logcat.
output = self._adb_command('logcat -d -b all "*:V"')
# Get the locat prior to last reboot.
last_output = self._adb_command('logcat -L -b all "*:V"')
if self.results_path:
logcat_path = os.path.join(self.results_path, "logcat.txt")
with open(logcat_path, "w", encoding="utf-8") as handle:
handle.write(output)
self.log.info("Current logcat logs stored at %s", logcat_path)
logcat_last_path = os.path.join(self.results_path, "logcat_last.txt")
with open(logcat_last_path, "w", encoding="utf-8") as handle:
handle.write(last_output)
self.log.info(
"Logcat logs prior to last reboot stored at %s", logcat_last_path
)
self._adb_disconnect()
+311
View File
@@ -0,0 +1,311 @@
# 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_packages import DumpsysPackagesArtifact
from mvt.android.utils import (
DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
ROOT_PACKAGES,
SECURITY_PACKAGES,
SYSTEM_UPDATE_PACKAGES,
)
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleResults,
ModuleSerializedResult,
)
from .base import AndroidExtraction
class Packages(AndroidExtraction):
"""This module extracts the list of installed packages."""
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: ModuleResults = [],
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self._user_needed = False
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
records = []
timestamps = [
{"event": "package_install", "timestamp": record["timestamp"]},
{
"event": "package_first_install",
"timestamp": record["first_install_time"],
},
{"event": "package_last_update", "timestamp": record["last_update_time"]},
]
for timestamp in timestamps:
records.append(
{
"timestamp": timestamp["timestamp"],
"module": self.__class__.__name__,
"event": timestamp["event"],
"data": f"{record['package_name']} (system: {record['system']},"
f" third party: {record['third_party']})",
}
)
return records
def check_indicators(self) -> None:
for result in self.results:
if result["package_name"] in ROOT_PACKAGES:
self.log.warning(
'Found an installed package related to rooting/jailbreaking: "%s"',
result["package_name"],
)
self.detected.append(result)
continue
if result["package_name"] in SECURITY_PACKAGES and result["disabled"]:
self.log.warning(
'Found a security package disabled: "%s"', result["package_name"]
)
if result["package_name"] in SYSTEM_UPDATE_PACKAGES and result["disabled"]:
self.log.warning(
'System OTA update package "%s" disabled on the phone',
result["package_name"],
)
if not self.indicators:
continue
ioc_match = self.indicators.check_app_id(result.get("package_name"))
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
for package_file in result.get("files", []):
ioc_match = self.indicators.check_file_hash(package_file["sha256"])
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
self.get_slug(), ioc_match.message, "", result
)
# @staticmethod
# def check_virustotal(packages: list) -> None:
# hashes = []
# for package in packages:
# for file in package.get("files", []):
# if file["sha256"] not in hashes:
# hashes.append(file["sha256"])
# total_hashes = len(hashes)
# detections = {}
# progress_desc = f"Looking up {total_hashes} files..."
# for i in track(range(total_hashes), description=progress_desc):
# try:
# results = virustotal_lookup(hashes[i])
# except VTNoKey:
# return
# except VTQuotaExceeded as exc:
# print("Unable to continue: %s", exc)
# break
# if not results:
# continue
# positives = results["attributes"]["last_analysis_stats"]["malicious"]
# total = len(results["attributes"]["last_analysis_results"])
# detections[hashes[i]] = f"{positives}/{total}"
# table = Table(title="VirusTotal Packages Detections")
# table.add_column("Package name")
# table.add_column("File path")
# table.add_column("Detections")
# for package in packages:
# for file in package.get("files", []):
# row = [package["package_name"], file["path"]]
# if file["sha256"] in detections:
# detection = detections[file["sha256"]]
# positives = detection.split("/")[0]
# if int(positives) > 0:
# row.append(Text(detection, "red bold"))
# else:
# row.append(detection)
# else:
# row.append("not found")
# table.add_row(*row)
# console = Console()
# console.print(table)
@staticmethod
def parse_package_for_details(output: str) -> dict:
lines = []
in_packages = False
for line in output.splitlines():
if in_packages:
if line.strip() == "":
break
lines.append(line)
if line.strip() == "Packages:":
in_packages = True
return DumpsysPackagesArtifact.parse_dumpsys_package_for_details(
"\n".join(lines)
)
def _get_files_for_package(self, package_name: str) -> list:
command = f"pm path {package_name}"
if self._user_needed:
command += " --user 0"
output = self._adb_command(command)
output = output.strip().replace("package:", "")
if not output:
return []
package_files = []
for file_path in output.splitlines():
file_path = file_path.strip()
md5 = self._adb_command(f"md5sum {file_path}").split(" ", maxsplit=1)[0]
sha1 = self._adb_command(f"sha1sum {file_path}").split(" ", maxsplit=1)[0]
sha256 = self._adb_command(f"sha256sum {file_path}").split(" ", maxsplit=1)[
0
]
sha512 = self._adb_command(f"sha512sum {file_path}").split(" ", maxsplit=1)[
0
]
package_files.append(
{
"path": file_path,
"md5": md5,
"sha1": sha1,
"sha256": sha256,
"sha512": sha512,
}
)
return package_files
def run(self) -> None:
self._adb_connect()
packages = self._adb_command("pm list packages -u -i -f")
if "java.lang.SecurityException" in packages or packages.strip() == "":
self._user_needed = True
packages = self._adb_command("pm list packages -u -i -f --user 0")
for line in packages.splitlines():
line = line.strip()
if not line.startswith("package:"):
continue
fields = line.split()
file_name, package_name = fields[0].split(":")[1].rsplit("=", 1)
try:
installer = fields[1].split("=")[1].strip()
except IndexError:
installer = None
else:
if installer == "null":
installer = None
package_files = self._get_files_for_package(package_name)
new_package = {
"package_name": package_name,
"file_name": file_name,
"installer": installer,
"disabled": False,
"system": False,
"third_party": False,
"files": package_files,
}
dumpsys_package = self._adb_command(f"dumpsys package {package_name}")
package_details = self.parse_package_for_details(dumpsys_package)
new_package.update(package_details)
self.results.append(new_package)
cmds = [
{"field": "disabled", "arg": "-d"},
{"field": "system", "arg": "-s"},
{"field": "third_party", "arg": "-3"},
]
for cmd in cmds:
command = f"pm list packages {cmd['arg']}"
if self._user_needed:
command += " --user 0"
output = self._adb_command(command)
for line in output.splitlines():
line = line.strip()
if not line.startswith("package:"):
continue
package_name = line.split(":", 1)[1]
for i, result in enumerate(self.results):
if result["package_name"] == package_name:
self.results[i][cmd["field"]] = True
for result in self.results:
if not result["third_party"]:
continue
dangerous_permissions_count = 0
for perm in result["requested_permissions"]:
if perm in DANGEROUS_PERMISSIONS:
dangerous_permissions_count += 1
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
self.log.info(
'Third-party package "%s" requested %d '
"potentially dangerous permissions",
result["package_name"],
dangerous_permissions_count,
)
packages_to_lookup = []
for result in self.results:
if result["system"]:
continue
packages_to_lookup.append(result)
self.log.info(
'Found non-system package with name "%s" installed by "%s" on %s',
result["package_name"],
result["installer"],
result["timestamp"],
)
if not self.module_options.get("fast_mode", None):
self.check_virustotal(packages_to_lookup)
self.log.info(
"Extracted at total of %d installed package names", len(self.results)
)
self._adb_disconnect()
+43
View 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/
import logging
from typing import Optional
from mvt.android.artifacts.processes import Processes as ProcessesArtifact
from .base import AndroidExtraction
from mvt.common.module_types import ModuleResults
class Processes(ProcessesArtifact, AndroidExtraction):
"""This module extracts details on running processes."""
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: ModuleResults = [],
) -> 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("ps -A")
self.parse(output)
self._adb_disconnect()
self.log.info("Extracted records on a total of %d processes", len(self.results))
@@ -0,0 +1,71 @@
# 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 .base import AndroidExtraction
from mvt.common.module_types import ModuleResults
class RootBinaries(AndroidExtraction):
"""This module extracts the list of installed packages."""
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: ModuleResults = [],
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def check_indicators(self) -> None:
for root_binary in self.results:
self.detected.append(root_binary)
self.log.warning('Found root binary "%s"', root_binary)
def run(self) -> None:
root_binaries = [
"su",
"busybox",
"supersu",
"Superuser.apk",
"KingoUser.apk",
"SuperSu.apk",
"magisk",
"magiskhide",
"magiskinit",
"magiskpolicy",
]
self._adb_connect()
for root_binary in root_binaries:
root_binary = root_binary.strip()
if not root_binary:
continue
output = self._adb_command(f"which -a {root_binary}")
output = output.strip()
if not output:
continue
if "which: not found" in output:
continue
self.results.append(root_binary)
self._adb_disconnect()
@@ -0,0 +1,49 @@
# 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 .base import AndroidExtraction
from mvt.common.module_types import ModuleResults
class SELinuxStatus(AndroidExtraction):
"""This module checks if SELinux is being enforced."""
slug = "selinux_status"
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: ModuleResults = [],
) -> 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 = {} if not results else results
def run(self) -> None:
self._adb_connect()
output = self._adb_command("getenforce")
self._adb_disconnect()
status = output.lower().strip()
self.results["status"] = status
if status == "enforcing":
self.log.info("SELinux is being regularly enforced")
else:
self.log.warning('SELinux status is "%s"!', status)
+59
View File
@@ -0,0 +1,59 @@
# 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.settings import Settings as SettingsArtifact
from mvt.common.module_types import ModuleResults
from .base import AndroidExtraction
class Settings(SettingsArtifact, AndroidExtraction):
"""This module extracts Android system settings."""
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: ModuleResults = [],
) -> 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 = {} if not results else results
def run(self) -> None:
self._adb_connect()
for namespace in ["system", "secure", "global"]:
out = self._adb_command(f"cmd settings list {namespace}")
if not out:
continue
self.results[namespace] = {}
for line in out.splitlines():
line = line.strip()
if line == "":
continue
fields = line.split("=", 1)
try:
self.results[namespace][fields[0]] = fields[1]
except IndexError:
continue
self._adb_disconnect()
+187
View File
@@ -0,0 +1,187 @@
# 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 os
import sqlite3
from typing import Optional
from mvt.android.parsers.backup import AndroidBackupParsingError, parse_tar_for_sms
from mvt.common.module import InsufficientPrivileges
from mvt.common.utils import check_for_links, convert_unix_to_iso
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleResults,
ModuleSerializedResult,
)
from .base import AndroidExtraction
SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
SMS_BUGLE_QUERY = """
SELECT
ppl.normalized_destination AS address,
p.timestamp AS timestamp,
CASE WHEN m.sender_id IN
(SELECT _id FROM participants WHERE contact_id=-1)
THEN 2 ELSE 1 END incoming, p.text AS body
FROM messages m, conversations c, parts p,
participants ppl, conversation_participants cp
WHERE (m.conversation_id = c._id)
AND (m._id = p.message_id)
AND (cp.conversation_id = c._id)
AND (cp.participant_id = ppl._id);
"""
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
SMS_MMSMS_QUERY = """
SELECT
address AS address,
date_sent AS timestamp,
type as incoming,
body AS body
FROM sms;
"""
class SMS(AndroidExtraction):
"""This module extracts all SMS messages."""
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: ModuleResults = [],
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
self.sms_db_type = 0
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
body = record["body"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": f"sms_{record['direction']}",
"data": f'{record.get("address", "unknown source")}: "{body}"',
}
def check_indicators(self) -> None:
if not self.indicators:
return
for message in self.results:
if "body" not in message:
continue
message_links = message.get("links", [])
if message_links == []:
message_links = check_for_links(message["body"])
ioc_match = self.indicators.check_urls(message_links)
if ioc_match:
message["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
self.get_slug(), ioc_match.message, "", message
)
def _parse_db(self, db_path: str) -> None:
"""Parse an Android bugle_db SMS database file.
:param db_path: Path to the Android SMS database file to process
"""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
if self.sms_db_type == 1:
cur.execute(SMS_BUGLE_QUERY)
elif self.sms_db_type == 2:
cur.execute(SMS_MMSMS_QUERY)
names = [description[0] for description in cur.description]
for item in cur:
message = {}
for index, value in enumerate(item):
message[names[index]] = value
message["direction"] = "received" if message["incoming"] == 1 else "sent"
message["isodate"] = convert_unix_to_iso(message["timestamp"])
# Extract links in the message body
body = message.get("body", None)
if body:
links = check_for_links(message["body"])
message["links"] = links
self.results.append(message)
cur.close()
conn.close()
self.log.info("Extracted a total of %d SMS messages", len(self.results))
def _extract_sms_adb(self) -> None:
"""Use the Android backup command to extract SMS data from the native
SMS app.
It is crucial to use the under-documented "-nocompress" flag to disable
the non-standard Java compression algorithm. This module only supports
an unencrypted ADB backup.
"""
backup_tar = self._generate_backup("com.android.providers.telephony")
if not backup_tar:
return
try:
self.results = parse_tar_for_sms(backup_tar)
except AndroidBackupParsingError:
self.log.info(
"Impossible to read SMS from the Android Backup, "
"please extract the SMS and try extracting it with "
"Android Backup Extractor"
)
return
self.log.info("Extracted a total of %d SMS messages", len(self.results))
def run(self) -> None:
self._adb_connect()
try:
if self._adb_check_file_exists(os.path.join("/", SMS_BUGLE_PATH)):
self.sms_db_type = 1
self._adb_process_file(
os.path.join("/", SMS_BUGLE_PATH), self._parse_db
)
elif self._adb_check_file_exists(os.path.join("/", SMS_MMSSMS_PATH)):
self.sms_db_type = 2
self._adb_process_file(
os.path.join("/", SMS_MMSSMS_PATH), self._parse_db
)
self._adb_disconnect()
return
except InsufficientPrivileges:
pass
self.log.info(
"No SMS database found. Trying extraction of SMS data "
"using Android backup feature."
)
self._extract_sms_adb()
self._adb_disconnect()
+118
View File
@@ -0,0 +1,118 @@
# 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 base64
import logging
import os
import sqlite3
from typing import Optional
from mvt.common.utils import check_for_links, convert_unix_to_iso
from .base import AndroidExtraction
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleSerializedResult,
ModuleResults,
)
WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db"
class Whatsapp(AndroidExtraction):
"""This module extracts all WhatsApp messages containing links."""
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: ModuleResults = [],
) -> None:
super().__init__(
file_path=file_path,
target_path=target_path,
results_path=results_path,
module_options=module_options,
log=log,
results=results,
)
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
text = record["data"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": f"whatsapp_msg_{record['direction']}",
"data": f'"{text}"',
}
def check_indicators(self) -> None:
if not self.indicators:
return
for message in self.results:
if "data" not in message:
continue
message_links = check_for_links(message["data"])
if self.indicators.check_urls(message_links):
self.detected.append(message)
continue
def _parse_db(self, db_path: str) -> None:
"""Parse an Android msgstore.db WhatsApp database file.
:param db_path: Path to the Android WhatsApp database file to process
"""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute(
"""
SELECT * FROM messages;
"""
)
names = [description[0] for description in cur.description]
messages = []
for item in cur:
message = {}
for index, value in enumerate(item):
message[names[index]] = value
if not message["data"]:
continue
message["direction"] = "send" if message["key_from_me"] == 1 else "received"
message["isodate"] = convert_unix_to_iso(message["timestamp"])
# If we find links in the messages or if they are empty we add them
# to the list.
if check_for_links(message["data"]) or message["data"].strip() == "":
if message.get("thumb_image"):
message["thumb_image"] = base64.b64encode(message["thumb_image"])
messages.append(message)
cur.close()
conn.close()
self.log.info(
"Extracted a total of %d WhatsApp messages containing links", len(messages)
)
self.results = messages
def run(self) -> None:
self._adb_connect()
try:
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
except Exception as exc:
self.log.error(exc)
self._adb_disconnect()
@@ -3,13 +3,11 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .aqf_files import AQFFiles
from .aqf_getprop import AQFGetProp
from .aqf_packages import AQFPackages
from .aqf_processes import AQFProcesses
from .aqf_settings import AQFSettings
from .mounts import Mounts
from .root_binaries import RootBinaries
from .aqf_files import AQFFiles
from .sms import SMS
ANDROIDQF_MODULES = [
@@ -19,6 +17,4 @@ ANDROIDQF_MODULES = [
AQFSettings,
AQFFiles,
SMS,
RootBinaries,
Mounts,
]
@@ -15,8 +15,8 @@ from typing import Optional
from mvt.android.modules.androidqf.base import AndroidQFModule
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleResults,
ModuleAtomicResult,
ModuleSerializedResult,
)
from mvt.common.utils import convert_datetime_to_iso
@@ -89,9 +89,9 @@ class AQFFiles(AndroidQFModule):
for result in self.results:
ioc_match = self.indicators.check_file_path(result["path"])
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
self.alertstore.log_latest()
continue
# NOTE: Update with final path used for Android collector.
@@ -105,16 +105,16 @@ class AQFFiles(AndroidQFModule):
file_type = "executable "
msg = f'Found {file_type}file at suspicious path "{result["path"]}"'
self.alertstore.high(msg, "", result)
self.alertstore.high(self.get_slug(), msg, "", result)
self.alertstore.log_latest()
if result.get("sha256", "") == "":
continue
ioc_match = self.indicators.check_file_hash(result.get("sha256") or "")
ioc_match = self.indicators.check_file_hash(result["sha256"])
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
# TODO: adds SHA1 and MD5 when available in MVT
@@ -7,9 +7,9 @@ import logging
from typing import Optional
from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
from mvt.common.module_types import ModuleResults
from .base import AndroidQFModule
from mvt.common.module_types import ModuleResults
class AQFGetProp(GetPropArtifact, AndroidQFModule):
@@ -32,7 +32,7 @@ class AQFGetProp(GetPropArtifact, AndroidQFModule):
log=log,
results=results,
)
self.results: list = []
self.results = []
def run(self) -> None:
getprop_files = self._get_files_by_pattern("*/getprop.txt")
@@ -3,16 +3,15 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import datetime
import logging
import os
from typing import Optional
from mvt.android.artifacts.file_timestamps import FileTimestampsArtifact
from mvt.common.module_types import ModuleResults
from mvt.common.utils import convert_datetime_to_iso
from mvt.common.module_types import ModuleResults
from .base import AndroidQFModule
from mvt.android.artifacts.file_timestamps import FileTimestampsArtifact
class AQFLogTimestamps(FileTimestampsArtifact, AndroidQFModule):
@@ -38,13 +37,11 @@ class AQFLogTimestamps(FileTimestampsArtifact, AndroidQFModule):
results=results,
)
def _get_file_modification_time(self, file_path: str) -> datetime.datetime:
def _get_file_modification_time(self, file_path: str) -> dict:
if self.archive:
file_timetuple = self.archive.getinfo(file_path).date_time
return datetime.datetime(*file_timetuple)
else:
if not self.parent_path:
raise ValueError("parent_path is not set")
file_stat = os.stat(os.path.join(self.parent_path, file_path))
return datetime.datetime.fromtimestamp(file_stat.st_mtime)
@@ -11,13 +11,13 @@ from mvt.android.utils import (
BROWSER_INSTALLERS,
PLAY_STORE_INSTALLERS,
ROOT_PACKAGES,
THIRD_PARTY_STORE_INSTALLERS,
SECURITY_PACKAGES,
SYSTEM_UPDATE_PACKAGES,
THIRD_PARTY_STORE_INSTALLERS,
)
from mvt.common.module_types import ModuleResults
from .base import AndroidQFModule
from mvt.common.module_types import ModuleResults
class AQFPackages(AndroidQFModule):
@@ -45,67 +45,79 @@ class AQFPackages(AndroidQFModule):
for result in self.results:
if result["name"] in ROOT_PACKAGES:
self.alertstore.medium(
self.get_slug(),
f'Found an installed package related to rooting/jailbreaking: "{result["name"]}"',
"",
result,
)
self.alertstore.log_latest()
continue
# Detections for apps installed via unusual methods.
if result["installer"] in THIRD_PARTY_STORE_INSTALLERS:
self.alertstore.info(
self.get_slug(),
f'Found a package installed via a third party store (installer="{result["installer"]}"): "{result["name"]}"',
"",
result,
)
self.alertstore.log_latest()
elif result["installer"] in BROWSER_INSTALLERS:
self.alertstore.medium(
self.get_slug(),
f'Found a package installed via a browser (installer="{result["installer"]}"): "{result["name"]}"',
"",
result,
)
self.alertstore.log_latest()
elif result["installer"] == "null" and result["system"] is False:
self.alertstore.medium(
self.alertstore.high(
self.get_slug(),
f'Found a non-system package installed via adb or another method: "{result["name"]}"',
"",
result,
)
self.alertstore.log_latest()
elif result["installer"] in PLAY_STORE_INSTALLERS:
pass
# Check for disabled security or software update packages.
package_disabled = result.get("disabled", None)
if result["name"] in SECURITY_PACKAGES and package_disabled:
self.alertstore.medium(
self.alertstore.high(
self.get_slug(),
f'Security package "{result["name"]}" disabled on the phone',
"",
result,
)
self.alertstore.log_latest()
if result["name"] in SYSTEM_UPDATE_PACKAGES and package_disabled:
self.alertstore.medium(
self.alertstore.high(
self.get_slug(),
f'System OTA update package "{result["name"]}" disabled on the phone',
"",
result,
)
self.alertstore.log_latest()
if not self.indicators:
continue
ioc_match = self.indicators.check_app_id(result.get("name") or "")
ioc_match = self.indicators.check_app_id(result.get("name"))
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
self.alertstore.log_latest()
for package_file in result.get("files", []):
ioc_match = self.indicators.check_file_hash(
package_file.get("sha256") or ""
)
ioc_match = self.indicators.check_file_hash(package_file["sha256"])
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
self.get_slug(), ioc_match.message, "", result
)
self.alertstore.log_latest()
if "certificate" not in package_file:
continue
@@ -117,12 +129,11 @@ class AQFPackages(AndroidQFModule):
certificate_hash
)
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message,
"",
result,
matched_indicator=ioc_match.ioc,
self.get_slug(), ioc_match.message, "", result
)
self.alertstore.log_latest()
break
def run(self) -> None:
@@ -7,9 +7,9 @@ import logging
from typing import Optional
from mvt.android.artifacts.settings import Settings as SettingsArtifact
from mvt.common.module_types import ModuleResults
from .base import AndroidQFModule
from mvt.common.module_types import ModuleResults
class AQFSettings(SettingsArtifact, AndroidQFModule):
@@ -32,7 +32,7 @@ class AQFSettings(SettingsArtifact, AndroidQFModule):
log=log,
results=results,
)
self.results: dict = {}
self.results = {}
def run(self) -> None:
for setting_file in self._get_files_by_pattern("*/settings_*.txt"):
+2 -2
View File
@@ -33,8 +33,8 @@ class AndroidQFModule(MVTModule):
log=log,
results=results,
)
self.parent_path: Optional[str] = None
self._path: Optional[str] = target_path
self.parent_path = None
self._path: str = target_path
self.files: List[str] = []
self.archive: Optional[zipfile.ZipFile] = None
@@ -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 json
import logging
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: list = []
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))
@@ -1,120 +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 json
import logging
from typing import Optional
from .base import AndroidQFModule
class RootBinaries(AndroidQFModule):
"""This module analyzes root_binaries.json for root binaries found by androidqf."""
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 serialize(self, record: dict) -> dict:
return {
"timestamp": record.get("timestamp"),
"module": self.__class__.__name__,
"event": "root_binary_found",
"data": f"Root binary found: {record['path']} (binary: {record['binary_name']})",
}
def check_indicators(self) -> None:
"""Check for indicators of device rooting."""
if not self.results:
return
# All found root binaries are considered indicators of rooting
for result in self.results:
self.alertstore.high(
f'Found root binary "{result["binary_name"]}" at path "{result["path"]}"',
"",
result,
)
if self.results:
self.log.warning(
"Device shows signs of rooting with %d root binaries found",
len(self.results),
)
def run(self) -> None:
"""Run the root binaries analysis."""
root_binaries_files = self._get_files_by_pattern("*/root_binaries.json")
if not root_binaries_files:
self.log.info("No root_binaries.json file found")
return
rawdata = self._get_file_content(root_binaries_files[0]).decode(
"utf-8", errors="ignore"
)
try:
root_binary_paths = json.loads(rawdata)
except json.JSONDecodeError as e:
self.log.error("Failed to parse root_binaries.json: %s", e)
return
if not isinstance(root_binary_paths, list):
self.log.error("Expected root_binaries.json to contain a list of paths")
return
# Known root binary names that might be found and their descriptions
# This maps the binary name to a human-readable description
known_root_binaries = {
"su": "SuperUser binary",
"busybox": "BusyBox utilities",
"supersu": "SuperSU root management",
"Superuser.apk": "Superuser app",
"KingoUser.apk": "KingRoot app",
"SuperSu.apk": "SuperSU app",
"magisk": "Magisk root framework",
"magiskhide": "Magisk hide utility",
"magiskinit": "Magisk init binary",
"magiskpolicy": "Magisk policy binary",
}
for path in root_binary_paths:
if not path or not isinstance(path, str):
continue
# Extract binary name from path
binary_name = path.split("/")[-1].lower()
# Check if this matches a known root binary by exact name match
description = "Unknown root binary"
for known_binary in known_root_binaries:
if binary_name == known_binary.lower():
description = known_root_binaries[known_binary]
break
result = {
"path": path.strip(),
"binary_name": binary_name,
"description": description,
}
self.results.append(result)
self.log.info("Found %d root binaries", len(self.results))
+2 -1
View File
@@ -55,8 +55,9 @@ class SMS(AndroidQFModule):
ioc_match = self.indicators.check_domains(message.get("links", []))
if ioc_match:
message["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message, "", message, matched_indicator=ioc_match.ioc
self.get_slug(), ioc_match.message, "", message
)
def parse_backup(self, data):
+7 -10
View File
@@ -9,7 +9,7 @@ import os
from tarfile import TarFile
from typing import List, Optional
from mvt.common.module import ModuleResults, MVTModule
from mvt.common.module import MVTModule, ModuleResults
class BackupModule(MVTModule):
@@ -32,10 +32,10 @@ class BackupModule(MVTModule):
log=log,
results=results,
)
self.ab: Optional[str] = None
self.backup_path: Optional[str] = None
self.tar: Optional[TarFile] = None
self.files: list = []
self.ab = None
self.backup_path = None
self.tar = None
self.files = []
def from_dir(self, backup_path: Optional[str], files: List[str]) -> None:
self.backup_path = backup_path
@@ -55,15 +55,12 @@ class BackupModule(MVTModule):
return fnmatch.filter(self.files, pattern)
def _get_file_content(self, file_path: str) -> bytes:
handle = None
if self.tar:
try:
member = self.tar.getmember(file_path)
handle = self.tar.extractfile(member)
if not handle:
raise ValueError(f"Could not extract file: {file_path}")
except KeyError:
raise FileNotFoundError(f"File not found in tar: {file_path}")
return None
handle = self.tar.extractfile(member)
elif self.backup_path:
handle = open(os.path.join(self.backup_path, file_path), "rb")
else:
+5 -4
View File
@@ -4,12 +4,12 @@
# https://license.mvt.re/1.1/
import logging
from typing import Any, Optional
from typing import Optional
from mvt.android.modules.backup.base import BackupModule
from mvt.android.parsers.backup import parse_sms_file
from mvt.common.module_types import ModuleResults
from mvt.common.utils import check_for_links
from mvt.common.module_types import ModuleResults
class SMS(BackupModule):
@@ -30,7 +30,7 @@ class SMS(BackupModule):
log=log,
results=results,
)
self.results: list[dict[str, Any]] = []
self.results = []
def check_indicators(self) -> None:
if not self.indicators:
@@ -46,8 +46,9 @@ class SMS(BackupModule):
ioc_match = self.indicators.check_urls(message_links)
if ioc_match:
message["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message, "", message, matched_indicator=ioc_match.ioc
self.get_slug(), ioc_match.message, "", message
)
continue
+8 -15
View File
@@ -6,10 +6,11 @@ import datetime
import fnmatch
import logging
import os
from typing import List, Optional
from zipfile import ZipFile
from mvt.common.module import ModuleResults, MVTModule
from mvt.common.module import MVTModule, ModuleResults
class BugReportModule(MVTModule):
@@ -68,8 +69,6 @@ class BugReportModule(MVTModule):
if self.zip_archive:
handle = self.zip_archive.open(file_path)
else:
if not self.extract_path:
raise ValueError("extract_path is not set")
handle = open(os.path.join(self.extract_path, file_path), "rb")
data = handle.read()
@@ -77,7 +76,7 @@ class BugReportModule(MVTModule):
return data
def _get_dumpstate_file(self) -> Optional[bytes]:
def _get_dumpstate_file(self) -> bytes:
main = self._get_files_by_pattern("main_entry.txt")
if main:
main_content = self._get_file_content(main[0])
@@ -85,23 +84,17 @@ class BugReportModule(MVTModule):
return self._get_file_content(main_content.decode().strip())
except KeyError:
return None
else:
dumpstate_logs = self._get_files_by_pattern("dumpState_*.log")
if not dumpstate_logs:
return None
dumpstate_logs = self._get_files_by_pattern("dumpState_*.log")
if dumpstate_logs:
return self._get_file_content(dumpstate_logs[0])
dumpsys_files = self._get_files_by_pattern("*/dumpsys.txt")
if dumpsys_files:
return self._get_file_content(dumpsys_files[0])
return None
def _get_file_modification_time(self, file_path: str) -> datetime.datetime:
def _get_file_modification_time(self, file_path: str) -> dict:
if self.zip_archive:
file_timetuple = self.zip_archive.getinfo(file_path).date_time
return datetime.datetime(*file_timetuple)
else:
if not self.extract_path:
raise ValueError("extract_path is not set")
file_stat = os.stat(os.path.join(self.extract_path, file_path))
return datetime.datetime.fromtimestamp(file_stat.st_mtime)
@@ -6,9 +6,9 @@
import logging
from typing import Optional
from mvt.common.module_types import ModuleResults
from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact
from mvt.android.utils import DANGEROUS_PERMISSIONS, DANGEROUS_PERMISSIONS_THRESHOLD
from mvt.common.module_types import ModuleResults
from .base import BugReportModule
@@ -43,9 +43,8 @@ class DumpsysPackages(DumpsysPackagesArtifact, BugReportModule):
)
return
content = self.extract_dumpsys_section(
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE package:"
)
data = data.decode("utf-8", errors="replace")
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:")
self.parse(content)
for result in self.results:
@@ -35,24 +35,6 @@ class DumpsysReceivers(DumpsysReceiversArtifact, BugReportModule):
self.results = results if results else {}
def check_indicators(self) -> None:
for result in self.results:
if self.indicators:
receiver_name = self.results[result][0]["receiver"]
# return IoC if the stix2 process name a substring of the receiver name
ioc_match = self.indicators.check_receiver_prefix(receiver_name)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
"",
self.results[result][0],
matched_indicator=ioc_match.ioc,
)
continue
def run(self) -> None:
content = self._get_dumpstate_file()
if not content:
-1
View File
@@ -231,7 +231,6 @@ def parse_sms_file(data):
entry.pop("mms_body")
body = entry.get("body", None)
message_links = None
if body:
message_links = check_for_links(entry["body"])
+139 -138
View File
@@ -1,12 +1,13 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# sources: tombstone.proto
# plugin: python-betterproto2
# plugin: python-betterproto
from dataclasses import dataclass
from typing import Dict, List
import betterproto2
import betterproto
class Architecture(betterproto2.Enum):
class Architecture(betterproto.Enum):
ARM32 = 0
ARM64 = 1
X86 = 2
@@ -15,12 +16,12 @@ class Architecture(betterproto2.Enum):
NONE = 5
class MemoryErrorTool(betterproto2.Enum):
class MemoryErrorTool(betterproto.Enum):
GWP_ASAN = 0
SCUDO = 1
class MemoryErrorType(betterproto2.Enum):
class MemoryErrorType(betterproto.Enum):
UNKNOWN = 0
USE_AFTER_FREE = 1
DOUBLE_FREE = 2
@@ -29,179 +30,179 @@ class MemoryErrorType(betterproto2.Enum):
BUFFER_UNDERFLOW = 5
@dataclass(eq=False, repr=False)
class CrashDetail(betterproto2.Message):
@dataclass
class CrashDetail(betterproto.Message):
"""
NOTE TO OEMS: If you add custom fields to this proto, do not use numbers in
the reserved range.
"""
name: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES)
data: "bytes" = betterproto2.field(2, betterproto2.TYPE_BYTES)
name: bytes = betterproto.bytes_field(1)
data: bytes = betterproto.bytes_field(2)
@dataclass(eq=False, repr=False)
class StackHistoryBufferEntry(betterproto2.Message):
addr: "BacktraceFrame | None" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, optional=True)
fp: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
tag: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
@dataclass
class StackHistoryBufferEntry(betterproto.Message):
addr: "BacktraceFrame" = betterproto.message_field(1)
fp: int = betterproto.uint64_field(2)
tag: int = betterproto.uint64_field(3)
@dataclass(eq=False, repr=False)
class StackHistoryBuffer(betterproto2.Message):
tid: "int" = betterproto2.field(1, betterproto2.TYPE_UINT64)
entries: "list[StackHistoryBufferEntry]" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, repeated=True)
@dataclass
class StackHistoryBuffer(betterproto.Message):
tid: int = betterproto.uint64_field(1)
entries: List["StackHistoryBufferEntry"] = betterproto.message_field(2)
@dataclass(eq=False, repr=False)
class Tombstone(betterproto2.Message):
arch: "Architecture" = betterproto2.field(1, betterproto2.TYPE_ENUM, default_factory=lambda: Architecture(0))
guest_arch: "Architecture" = betterproto2.field(24, betterproto2.TYPE_ENUM, default_factory=lambda: Architecture(0))
build_fingerprint: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
revision: "str" = betterproto2.field(3, betterproto2.TYPE_STRING)
timestamp: "str" = betterproto2.field(4, betterproto2.TYPE_STRING)
pid: "int" = betterproto2.field(5, betterproto2.TYPE_UINT32)
tid: "int" = betterproto2.field(6, betterproto2.TYPE_UINT32)
uid: "int" = betterproto2.field(7, betterproto2.TYPE_UINT32)
selinux_label: "str" = betterproto2.field(8, betterproto2.TYPE_STRING)
command_line: "list[str]" = betterproto2.field(9, betterproto2.TYPE_STRING, repeated=True)
@dataclass
class Tombstone(betterproto.Message):
arch: "Architecture" = betterproto.enum_field(1)
guest_arch: "Architecture" = betterproto.enum_field(24)
build_fingerprint: str = betterproto.string_field(2)
revision: str = betterproto.string_field(3)
timestamp: str = betterproto.string_field(4)
pid: int = betterproto.uint32_field(5)
tid: int = betterproto.uint32_field(6)
uid: int = betterproto.uint32_field(7)
selinux_label: str = betterproto.string_field(8)
command_line: List[str] = betterproto.string_field(9)
# Process uptime in seconds.
process_uptime: "int" = betterproto2.field(20, betterproto2.TYPE_UINT32)
signal_info: "Signal | None" = betterproto2.field(10, betterproto2.TYPE_MESSAGE, optional=True)
abort_message: "str" = betterproto2.field(14, betterproto2.TYPE_STRING)
crash_details: "list[CrashDetail]" = betterproto2.field(21, betterproto2.TYPE_MESSAGE, repeated=True)
causes: "list[Cause]" = betterproto2.field(15, betterproto2.TYPE_MESSAGE, repeated=True)
threads: "dict[int, Thread]" = betterproto2.field(
16, betterproto2.TYPE_MAP, map_meta=betterproto2.map_meta(betterproto2.TYPE_UINT32, betterproto2.TYPE_MESSAGE)
process_uptime: int = betterproto.uint32_field(20)
signal_info: "Signal" = betterproto.message_field(10)
abort_message: str = betterproto.string_field(14)
crash_details: List["CrashDetail"] = betterproto.message_field(21)
causes: List["Cause"] = betterproto.message_field(15)
threads: Dict[int, "Thread"] = betterproto.map_field(
16, betterproto.TYPE_UINT32, betterproto.TYPE_MESSAGE
)
guest_threads: "dict[int, Thread]" = betterproto2.field(
25, betterproto2.TYPE_MAP, map_meta=betterproto2.map_meta(betterproto2.TYPE_UINT32, betterproto2.TYPE_MESSAGE)
guest_threads: Dict[int, "Thread"] = betterproto.map_field(
25, betterproto.TYPE_UINT32, betterproto.TYPE_MESSAGE
)
memory_mappings: "list[MemoryMapping]" = betterproto2.field(17, betterproto2.TYPE_MESSAGE, repeated=True)
log_buffers: "list[LogBuffer]" = betterproto2.field(18, betterproto2.TYPE_MESSAGE, repeated=True)
open_fds: "list[FD]" = betterproto2.field(19, betterproto2.TYPE_MESSAGE, repeated=True)
page_size: "int" = betterproto2.field(22, betterproto2.TYPE_UINT32)
has_been_16kb_mode: "bool" = betterproto2.field(23, betterproto2.TYPE_BOOL)
stack_history_buffer: "StackHistoryBuffer | None" = betterproto2.field(26, betterproto2.TYPE_MESSAGE, optional=True)
memory_mappings: List["MemoryMapping"] = betterproto.message_field(17)
log_buffers: List["LogBuffer"] = betterproto.message_field(18)
open_fds: List["FD"] = betterproto.message_field(19)
page_size: int = betterproto.uint32_field(22)
has_been_16kb_mode: bool = betterproto.bool_field(23)
stack_history_buffer: "StackHistoryBuffer" = betterproto.message_field(26)
@dataclass(eq=False, repr=False)
class Signal(betterproto2.Message):
number: "int" = betterproto2.field(1, betterproto2.TYPE_INT32)
name: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
code: "int" = betterproto2.field(3, betterproto2.TYPE_INT32)
code_name: "str" = betterproto2.field(4, betterproto2.TYPE_STRING)
has_sender: "bool" = betterproto2.field(5, betterproto2.TYPE_BOOL)
sender_uid: "int" = betterproto2.field(6, betterproto2.TYPE_INT32)
sender_pid: "int" = betterproto2.field(7, betterproto2.TYPE_INT32)
has_fault_address: "bool" = betterproto2.field(8, betterproto2.TYPE_BOOL)
fault_address: "int" = betterproto2.field(9, betterproto2.TYPE_UINT64)
@dataclass
class Signal(betterproto.Message):
number: int = betterproto.int32_field(1)
name: str = betterproto.string_field(2)
code: int = betterproto.int32_field(3)
code_name: str = betterproto.string_field(4)
has_sender: bool = betterproto.bool_field(5)
sender_uid: int = betterproto.int32_field(6)
sender_pid: int = betterproto.int32_field(7)
has_fault_address: bool = betterproto.bool_field(8)
fault_address: int = betterproto.uint64_field(9)
# Note, may or may not contain the dump of the actual memory contents.
# Currently, on arm64, we only include metadata, and not the contents.
fault_adjacent_metadata: "MemoryDump | None" = betterproto2.field(10, betterproto2.TYPE_MESSAGE, optional=True)
fault_adjacent_metadata: "MemoryDump" = betterproto.message_field(10)
@dataclass(eq=False, repr=False)
class HeapObject(betterproto2.Message):
address: "int" = betterproto2.field(1, betterproto2.TYPE_UINT64)
size: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
allocation_tid: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
allocation_backtrace: "list[BacktraceFrame]" = betterproto2.field(4, betterproto2.TYPE_MESSAGE, repeated=True)
deallocation_tid: "int" = betterproto2.field(5, betterproto2.TYPE_UINT64)
deallocation_backtrace: "list[BacktraceFrame]" = betterproto2.field(6, betterproto2.TYPE_MESSAGE, repeated=True)
@dataclass
class HeapObject(betterproto.Message):
address: int = betterproto.uint64_field(1)
size: int = betterproto.uint64_field(2)
allocation_tid: int = betterproto.uint64_field(3)
allocation_backtrace: List["BacktraceFrame"] = betterproto.message_field(4)
deallocation_tid: int = betterproto.uint64_field(5)
deallocation_backtrace: List["BacktraceFrame"] = betterproto.message_field(6)
@dataclass(eq=False, repr=False)
class MemoryError(betterproto2.Message):
tool: "MemoryErrorTool" = betterproto2.field(1, betterproto2.TYPE_ENUM, default_factory=lambda: MemoryErrorTool(0))
type: "MemoryErrorType" = betterproto2.field(2, betterproto2.TYPE_ENUM, default_factory=lambda: MemoryErrorType(0))
heap: "HeapObject | None" = betterproto2.field(3, betterproto2.TYPE_MESSAGE, optional=True, group="location")
@dataclass
class MemoryError(betterproto.Message):
tool: "MemoryErrorTool" = betterproto.enum_field(1)
type: "MemoryErrorType" = betterproto.enum_field(2)
heap: "HeapObject" = betterproto.message_field(3, group="location")
@dataclass(eq=False, repr=False)
class Cause(betterproto2.Message):
human_readable: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
memory_error: "MemoryError | None" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, optional=True, group="details")
@dataclass
class Cause(betterproto.Message):
human_readable: str = betterproto.string_field(1)
memory_error: "MemoryError" = betterproto.message_field(2, group="details")
@dataclass(eq=False, repr=False)
class Register(betterproto2.Message):
name: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
u64: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
@dataclass
class Register(betterproto.Message):
name: str = betterproto.string_field(1)
u64: int = betterproto.uint64_field(2)
@dataclass(eq=False, repr=False)
class Thread(betterproto2.Message):
id: "int" = betterproto2.field(1, betterproto2.TYPE_INT32)
name: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
registers: "list[Register]" = betterproto2.field(3, betterproto2.TYPE_MESSAGE, repeated=True)
backtrace_note: "list[str]" = betterproto2.field(7, betterproto2.TYPE_STRING, repeated=True)
unreadable_elf_files: "list[str]" = betterproto2.field(9, betterproto2.TYPE_STRING, repeated=True)
current_backtrace: "list[BacktraceFrame]" = betterproto2.field(4, betterproto2.TYPE_MESSAGE, repeated=True)
memory_dump: "list[MemoryDump]" = betterproto2.field(5, betterproto2.TYPE_MESSAGE, repeated=True)
tagged_addr_ctrl: "int" = betterproto2.field(6, betterproto2.TYPE_INT64)
pac_enabled_keys: "int" = betterproto2.field(8, betterproto2.TYPE_INT64)
@dataclass
class Thread(betterproto.Message):
id: int = betterproto.int32_field(1)
name: str = betterproto.string_field(2)
registers: List["Register"] = betterproto.message_field(3)
backtrace_note: List[str] = betterproto.string_field(7)
unreadable_elf_files: List[str] = betterproto.string_field(9)
current_backtrace: List["BacktraceFrame"] = betterproto.message_field(4)
memory_dump: List["MemoryDump"] = betterproto.message_field(5)
tagged_addr_ctrl: int = betterproto.int64_field(6)
pac_enabled_keys: int = betterproto.int64_field(8)
@dataclass(eq=False, repr=False)
class BacktraceFrame(betterproto2.Message):
rel_pc: "int" = betterproto2.field(1, betterproto2.TYPE_UINT64)
pc: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
sp: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
function_name: "str" = betterproto2.field(4, betterproto2.TYPE_STRING)
function_offset: "int" = betterproto2.field(5, betterproto2.TYPE_UINT64)
file_name: "str" = betterproto2.field(6, betterproto2.TYPE_STRING)
file_map_offset: "int" = betterproto2.field(7, betterproto2.TYPE_UINT64)
build_id: "str" = betterproto2.field(8, betterproto2.TYPE_STRING)
@dataclass
class BacktraceFrame(betterproto.Message):
rel_pc: int = betterproto.uint64_field(1)
pc: int = betterproto.uint64_field(2)
sp: int = betterproto.uint64_field(3)
function_name: str = betterproto.string_field(4)
function_offset: int = betterproto.uint64_field(5)
file_name: str = betterproto.string_field(6)
file_map_offset: int = betterproto.uint64_field(7)
build_id: str = betterproto.string_field(8)
@dataclass(eq=False, repr=False)
class ArmMTEMetadata(betterproto2.Message):
@dataclass
class ArmMTEMetadata(betterproto.Message):
# One memory tag per granule (e.g. every 16 bytes) of regular memory.
memory_tags: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES)
memory_tags: bytes = betterproto.bytes_field(1)
@dataclass(eq=False, repr=False)
class MemoryDump(betterproto2.Message):
register_name: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
mapping_name: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
begin_address: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
memory: "bytes" = betterproto2.field(4, betterproto2.TYPE_BYTES)
arm_mte_metadata: "ArmMTEMetadata | None" = betterproto2.field(6, betterproto2.TYPE_MESSAGE, optional=True, group="metadata")
@dataclass
class MemoryDump(betterproto.Message):
register_name: str = betterproto.string_field(1)
mapping_name: str = betterproto.string_field(2)
begin_address: int = betterproto.uint64_field(3)
memory: bytes = betterproto.bytes_field(4)
arm_mte_metadata: "ArmMTEMetadata" = betterproto.message_field(6, group="metadata")
@dataclass(eq=False, repr=False)
class MemoryMapping(betterproto2.Message):
begin_address: "int" = betterproto2.field(1, betterproto2.TYPE_UINT64)
end_address: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64)
offset: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64)
read: "bool" = betterproto2.field(4, betterproto2.TYPE_BOOL)
write: "bool" = betterproto2.field(5, betterproto2.TYPE_BOOL)
execute: "bool" = betterproto2.field(6, betterproto2.TYPE_BOOL)
mapping_name: "str" = betterproto2.field(7, betterproto2.TYPE_STRING)
build_id: "str" = betterproto2.field(8, betterproto2.TYPE_STRING)
load_bias: "int" = betterproto2.field(9, betterproto2.TYPE_UINT64)
@dataclass
class MemoryMapping(betterproto.Message):
begin_address: int = betterproto.uint64_field(1)
end_address: int = betterproto.uint64_field(2)
offset: int = betterproto.uint64_field(3)
read: bool = betterproto.bool_field(4)
write: bool = betterproto.bool_field(5)
execute: bool = betterproto.bool_field(6)
mapping_name: str = betterproto.string_field(7)
build_id: str = betterproto.string_field(8)
load_bias: int = betterproto.uint64_field(9)
@dataclass(eq=False, repr=False)
class FD(betterproto2.Message):
fd: "int" = betterproto2.field(1, betterproto2.TYPE_INT32)
path: "str" = betterproto2.field(2, betterproto2.TYPE_STRING)
owner: "str" = betterproto2.field(3, betterproto2.TYPE_STRING)
tag: "int" = betterproto2.field(4, betterproto2.TYPE_UINT64)
@dataclass
class FD(betterproto.Message):
fd: int = betterproto.int32_field(1)
path: str = betterproto.string_field(2)
owner: str = betterproto.string_field(3)
tag: int = betterproto.uint64_field(4)
@dataclass(eq=False, repr=False)
class LogBuffer(betterproto2.Message):
name: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
logs: "list[LogMessage]" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, repeated=True)
@dataclass
class LogBuffer(betterproto.Message):
name: str = betterproto.string_field(1)
logs: List["LogMessage"] = betterproto.message_field(2)
@dataclass(eq=False, repr=False)
class LogMessage(betterproto2.Message):
timestamp: "str" = betterproto2.field(1, betterproto2.TYPE_STRING)
pid: "int" = betterproto2.field(2, betterproto2.TYPE_UINT32)
tid: "int" = betterproto2.field(3, betterproto2.TYPE_UINT32)
priority: "int" = betterproto2.field(4, betterproto2.TYPE_UINT32)
tag: "str" = betterproto2.field(5, betterproto2.TYPE_STRING)
message: "str" = betterproto2.field(6, betterproto2.TYPE_STRING)
@dataclass
class LogMessage(betterproto.Message):
timestamp: str = betterproto.string_field(1)
pid: int = betterproto.uint32_field(2)
tid: int = betterproto.uint32_field(3)
priority: int = betterproto.uint32_field(4)
tag: str = betterproto.string_field(5)
message: str = betterproto.string_field(6)
+3 -2
View File
@@ -6,13 +6,14 @@ from datetime import datetime, timedelta
from typing import List
def warn_android_patch_level(patch_level: str, log) -> str | bool:
def warn_android_patch_level(patch_level: str, log) -> str:
"""Alert if Android patch level out-of-date"""
patch_date = datetime.strptime(patch_level, "%Y-%m-%d")
if (datetime.now() - patch_date) > timedelta(days=6 * 31):
warning_message = (
f"This phone has not received security updates "
f"for more than six months (last update: {patch_level})."
f"for more than six months (last update: {patch_level}).",
patch_level,
)
return warning_message
+14 -83
View File
@@ -4,13 +4,12 @@
# https://license.mvt.re/1.1/
import csv
import inspect
import logging
from dataclasses import asdict, dataclass
from enum import Enum
from typing import Any, Dict, List, Optional
from dataclasses import dataclass, asdict
from typing import List, Dict, Any, Optional
from .log import CRITICAL_ALERT, HIGH_ALERT, INFO_ALERT, LOW_ALERT, MEDIUM_ALERT
from .log import INFO_ALERT, LOW_ALERT, HIGH_ALERT, CRITICAL_ALERT, MEDIUM_ALERT
from .module_types import ModuleAtomicResult
@@ -29,7 +28,6 @@ class Alert:
message: str
event_time: str
event: ModuleAtomicResult
matched_indicator: Optional[Any] = None
class AlertStore:
@@ -37,138 +35,78 @@ class AlertStore:
self.__alerts: List[Alert] = []
self.__log = log
def _get_calling_module(self) -> str:
"""
Automatically detect the calling MVT module and return its slug.
Walks up the call stack to find the first frame that belongs to an MVT module
(artifact or extraction module) and extracts its slug.
:return: Module slug string
"""
frame = inspect.currentframe()
try:
# Walk up the call stack
while frame is not None:
frame = frame.f_back
if frame is None:
break
# Get the 'self' object from the frame's local variables
frame_locals = frame.f_locals
if "self" in frame_locals:
obj = frame_locals["self"]
# Check if it has a get_slug method (MVT modules have this)
if hasattr(obj, "get_slug") and callable(obj.get_slug):
try:
return str(obj.get_slug())
except Exception:
pass
# Fallback: return "unknown" if we can't find the module
return "unknown"
finally:
del frame
@property
def alerts(self) -> List[Alert]:
return self.__alerts
def add(self, alert: Alert) -> None:
self.__alerts.append(alert)
self.log(alert)
def extend(self, alerts: List[Alert]) -> None:
for alert in alerts:
self.add(alert)
self.__alerts.extend(alerts)
def info(
self,
message: str,
event_time: str,
event: ModuleAtomicResult,
matched_indicator: Optional[Any] = None,
self, module: str, message: str, event_time: str, event: ModuleAtomicResult
):
self.add(
Alert(
level=AlertLevel.INFORMATIONAL,
module=self._get_calling_module(),
module=module,
message=message,
event_time=event_time,
event=event,
matched_indicator=matched_indicator,
)
)
def low(
self,
message: str,
event_time: str,
event: ModuleAtomicResult,
matched_indicator: Optional[Any] = None,
self, module: str, message: str, event_time: str, event: ModuleAtomicResult
):
self.add(
Alert(
level=AlertLevel.LOW,
module=self._get_calling_module(),
module=module,
message=message,
event_time=event_time,
event=event,
matched_indicator=matched_indicator,
)
)
def medium(
self,
message: str,
event_time: str,
event: ModuleAtomicResult,
matched_indicator: Optional[Any] = None,
self, module: str, message: str, event_time: str, event: ModuleAtomicResult
):
self.add(
Alert(
level=AlertLevel.MEDIUM,
module=self._get_calling_module(),
module=module,
message=message,
event_time=event_time,
event=event,
matched_indicator=matched_indicator,
)
)
def high(
self,
message: str,
event_time: str,
event: ModuleAtomicResult,
matched_indicator: Optional[Any] = None,
self, module: str, message: str, event_time: str, event: ModuleAtomicResult
):
self.add(
Alert(
level=AlertLevel.HIGH,
module=self._get_calling_module(),
module=module,
message=message,
event_time=event_time,
event=event,
matched_indicator=matched_indicator,
)
)
def critical(
self,
message: str,
event_time: str,
event: ModuleAtomicResult,
matched_indicator: Optional[Any] = None,
self, module: str, message: str, event_time: str, event: ModuleAtomicResult
):
self.add(
Alert(
level=AlertLevel.CRITICAL,
module=self._get_calling_module(),
module=module,
message=message,
event_time=event_time,
event=event,
matched_indicator=matched_indicator,
)
)
@@ -207,13 +145,6 @@ class AlertStore:
alert_dict = asdict(alert)
# This is required because an Enum is not JSON serializable.
alert_dict["level"] = alert.level.name
if isinstance(alert_dict.get("event"), dict):
event_matched_indicator = alert_dict["event"].pop(
"matched_indicator", None
)
if alert_dict["matched_indicator"] is None:
alert_dict["matched_indicator"] = event_matched_indicator
alerts.append(alert_dict)
return alerts
-8
View File
@@ -24,8 +24,6 @@ class CmdCheckIOCS(Command):
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:
super().__init__(
target_path=target_path,
@@ -37,8 +35,6 @@ class CmdCheckIOCS(Command):
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-iocs"
@@ -73,10 +69,6 @@ class CmdCheckIOCS(Command):
m = iocs_module.from_json(
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:
m.indicators = self.iocs
m.indicators.log = m.log
+12 -25
View File
@@ -8,22 +8,20 @@ import logging
import os
import sys
from datetime import datetime
from typing import Any, Optional
from typing import Optional
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from .alerts import AlertLevel, AlertStore
from .config import settings
from .indicators import Indicators
from .module import EncryptedBackupError, MVTModule, run_module, save_timeline
from .module import MVTModule, run_module, save_timeline
from .utils import (
CustomJSONEncoder,
convert_datetime_to_iso,
generate_hashes_from_path,
get_sha256_from_file_path,
)
from .config import settings
from .alerts import AlertStore, AlertLevel
from .version import MVT_VERSION
@@ -40,11 +38,9 @@ class Command:
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
log: logging.Logger = logging.getLogger(__name__),
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
self.name = ""
self.modules: list[Any] = []
self.modules = []
self.target_path = target_path
self.results_path = results_path
@@ -53,8 +49,6 @@ class Command:
self.serial = serial
self.log = log
self.sub_command = sub_command
self.disable_version_check = disable_version_check
self.disable_indicator_check = disable_indicator_check
# This dictionary can contain options that will be passed down from
# the Command to all modules. This can for example be used to pass
@@ -63,10 +57,10 @@ class Command:
# This list will contain all executed modules.
# We can use this to reference e.g. self.executed[0].results.
self.executed: list[Any] = []
self.executed = []
self.hashes = hashes
self.hash_values: list[dict[str, Any]] = []
self.timeline: list[dict[str, Any]] = []
self.hash_values = []
self.timeline = []
# Load IOCs
self._create_storage()
@@ -142,7 +136,7 @@ class Command:
alerts_path = os.path.join(self.results_path, "alerts.json")
with open(alerts_path, "w+", encoding="utf-8") as handle:
json.dump(alerts, handle, indent=4, cls=CustomJSONEncoder)
json.dump(alerts, handle, indent=4)
def _store_alerts_timeline(self) -> None:
if not self.results_path:
@@ -155,11 +149,11 @@ class Command:
if not self.results_path:
return
target_path: Optional[str] = None
target_path = None
if self.target_path:
target_path = os.path.abspath(self.target_path)
info: dict[str, Any] = {
info = {
"target_path": target_path,
"mvt_version": MVT_VERSION,
"date": convert_datetime_to_iso(datetime.now()),
@@ -295,14 +289,7 @@ class Command:
except NotImplementedError:
pass
try:
run_module(m)
except EncryptedBackupError:
self.log.critical(
"The backup appears to be encrypted. "
"Please decrypt it first using `mvt-ios decrypt-backup`."
)
return
run_module(m)
self.executed.append(m)
self.timeline.extend(m.timeline)
+23 -22
View File
@@ -1,12 +1,13 @@
import json
import os
from typing import Optional, Tuple, Type
import yaml
import json
from typing import Tuple, Type, Optional
from appdirs import user_config_dir
from pydantic import Field
from pydantic import AnyHttpUrl, Field
from pydantic_settings import (
BaseSettings,
InitSettingsSource,
PydanticBaseSettingsSource,
SettingsConfigDict,
YamlConfigSettingsSource,
@@ -21,51 +22,51 @@ class MVTSettings(BaseSettings):
env_prefix="MVT_",
env_nested_delimiter="_",
extra="ignore",
nested_model_default_partial_updates=True,
)
# Allow to decided if want to load environment variables
load_env: bool = Field(True, exclude=True)
# General settings
PYPI_UPDATE_URL: str = Field(
default="https://pypi.org/pypi/mvt/json",
PYPI_UPDATE_URL: AnyHttpUrl = Field(
"https://pypi.org/pypi/mvt/json",
validate_default=False,
)
NETWORK_ACCESS_ALLOWED: bool = True
NETWORK_TIMEOUT: int = 15
# Command default settings, all can be specified by MVT_ prefixed environment variables too.
IOS_BACKUP_PASSWORD: Optional[str] = Field(
default=None, description="Default password to use to decrypt iOS backups"
None, description="Default password to use to decrypt iOS backups"
)
ANDROID_BACKUP_PASSWORD: Optional[str] = Field(
default=None, description="Default password to use to decrypt Android backups"
None, description="Default password to use to decrypt Android backups"
)
STIX2: Optional[str] = Field(
default=None, description="List of directories where STIX2 files are stored"
None, description="List of directories where STIX2 files are stored"
)
VT_API_KEY: Optional[str] = Field(
default=None, description="API key to use for VirusTotal lookups"
None, description="API key to use for VirusTotal lookups"
)
PROFILE: bool = Field(
default=False, description="Profile the execution of MVT modules"
)
HASH_FILES: bool = Field(default=False, description="Should MVT hash output files")
PROFILE: bool = Field(False, description="Profile the execution of MVT modules")
HASH_FILES: bool = Field(False, description="Should MVT hash output files")
@classmethod
def settings_customise_sources(
cls,
settings_cls: Type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
init_settings: InitSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> Tuple[PydanticBaseSettingsSource, ...]:
yaml_source = YamlConfigSettingsSource(settings_cls, MVT_CONFIG_PATH)
sources: Tuple[PydanticBaseSettingsSource, ...] = (
yaml_source,
sources = (
YamlConfigSettingsSource(settings_cls, MVT_CONFIG_PATH),
init_settings,
)
# Always load env variables by default
sources = (env_settings,) + sources
# Load env variables if enabled
if init_settings.init_kwargs.get("load_env", True):
sources = (env_settings,) + sources
return sources
def save_settings(
@@ -93,11 +94,11 @@ class MVTSettings(BaseSettings):
Afterwards we load the settings again, this time including the env variables.
"""
# Set invalid env prefix to avoid loading env variables.
settings = cls(load_env=False)
settings = MVTSettings(load_env=False)
settings.save_settings()
# Load the settings again with any ENV variables.
settings = cls(load_env=True)
settings = MVTSettings(load_env=True)
return settings
-2
View File
@@ -15,8 +15,6 @@ HELP_MSG_HASHES = "Generate hashes of all the files analyzed"
HELP_MSG_VERBOSE = "Verbose mode"
HELP_MSG_CHECK_IOCS = "Compare stored JSON results to provided indicators"
HELP_MSG_STIX2 = "Download public STIX2 indicators"
HELP_MSG_DISABLE_UPDATE_CHECK = "Disable MVT version update check"
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK = "Disable indicators update check"
# IOS Specific
HELP_MSG_DECRYPT_BACKUP = "Decrypt an encrypted iTunes backup"
+8 -40
View File
@@ -7,15 +7,15 @@ import glob
import json
import logging
import os
from dataclasses import dataclass
from functools import lru_cache
from typing import Any, Dict, Iterator, List, Optional
from dataclasses import dataclass
import ahocorasick
from appdirs import user_data_dir
from .config import settings
from .url import URL
from .config import settings
MVT_DATA_FOLDER = user_data_dir("mvt")
MVT_INDICATORS_FOLDER = os.path.join(MVT_DATA_FOLDER, "indicators")
@@ -67,7 +67,9 @@ class Indicators:
if os.path.isfile(path) and path.lower().endswith(".stix2"):
self.parse_stix2(path)
elif os.path.isdir(path):
for file in glob.glob(os.path.join(path, "**", "*.stix2"), recursive=True):
for file in glob.glob(
os.path.join(path, "**", "*.stix2", recursive=True)
):
self.parse_stix2(file)
else:
self.log.error(
@@ -115,17 +117,6 @@ class Indicators:
key, value = indicator.get("pattern", "").strip("[]").split("=")
key = key.strip()
# Normalize hash algorithm keys so that both the STIX2-spec-compliant
# form (e.g. file:hashes.'SHA-256', which requires quotes around
# algorithm names that contain hyphens) and the non-standard lowercase
# form (e.g. file:hashes.sha256) are accepted. Strip single quotes and
# hyphens from the algorithm name only, then lowercase it.
for sep in ("hashes.", "cert."):
if sep in key:
prefix, _, algo = key.partition(sep)
key = prefix + sep + algo.replace("'", "").replace("-", "").lower()
break
if key == "domain-name:value":
# We force domain names to lower case.
self._add_indicator(
@@ -359,7 +350,7 @@ class Indicators:
@lru_cache()
def get_ioc_matcher(
self, ioc_type: Optional[str] = None, ioc_list: Optional[List[Indicator]] = None
self, ioc_type: Optional[str] = None, ioc_list: Optional[list] = None
) -> ahocorasick.Automaton:
"""
Build an Aho-Corasick automaton from a list of iocs (i.e indicators)
@@ -379,9 +370,9 @@ class Indicators:
"""
automaton = ahocorasick.Automaton()
if ioc_type:
iocs: Iterator[Indicator] = self.get_iocs(ioc_type)
iocs = self.get_iocs(ioc_type)
elif ioc_list:
iocs = iter(ioc_list)
iocs = ioc_list
else:
raise ValueError("Must provide either ioc_type or ioc_list")
@@ -727,29 +718,6 @@ class Indicators:
return None
def check_receiver_prefix(
self, receiver_name: str
) -> Optional[IndicatorMatch]:
"""Check the provided receiver name against the list of indicators.
An IoC match is detected when a substring of the receiver matches the indicator.
:param receiver_name: Receiver name to check against app ID indicators
:type receiver_name: str
:returns: IndicatorMatch if matched, otherwise None
"""
if not receiver_name:
return None
for ioc in self.get_iocs("app_ids"):
if ioc.value.lower() in receiver_name.lower():
return IndicatorMatch(
ioc=ioc,
message=f'Found a known suspicious receiver with name "{receiver_name}" matching indicators from "{ioc.name}"',
)
return None
def check_android_property_name(
self, property_name: str
) -> Optional[IndicatorMatch]:
+50 -61
View File
@@ -12,85 +12,74 @@ from .updates import IndicatorsUpdates, MVTUpdates
from .version import MVT_VERSION
def check_updates(
disable_version_check: bool = False, disable_indicator_check: bool = False
) -> None:
def check_updates() -> None:
log = logging.getLogger("mvt")
# First we check for MVT version updates.
if not disable_version_check:
try:
mvt_updates = MVTUpdates()
latest_version = mvt_updates.check()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
try:
mvt_updates = MVTUpdates()
latest_version = mvt_updates.check()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
rich_print(
"\t\t[bold]Note: Could not check for MVT updates.[/bold] "
"You may be working offline. Please update MVT regularly."
)
except Exception as e:
log.error("Error encountered when trying to check latest MVT version: %s", e)
else:
if latest_version:
rich_print(
"\t\t[bold]Note: Could not check for MVT updates.[/bold] "
"You may be working offline. Please update MVT regularly."
f"\t\t[bold]Version {latest_version} is available! "
"Upgrade mvt with `pip3 install -U mvt`[/bold]"
)
except Exception as e:
log.error(
"Error encountered when trying to check latest MVT version: %s", e
)
else:
if latest_version:
rich_print(
f"\t\t[bold]Version {latest_version} is available! "
"Upgrade mvt with `pip3 install -U mvt` or with `pipx upgrade mvt`[/bold]"
)
# Then we check for indicators files updates.
if not disable_indicator_check:
ioc_updates = IndicatorsUpdates()
ioc_updates = IndicatorsUpdates()
# Before proceeding, we check if we have downloaded an indicators index.
# If not, there's no point in proceeding with the updates check.
if ioc_updates.get_latest_update() == 0:
rich_print(
"\t\t[bold]You have not yet downloaded any indicators, check "
"the `download-iocs` command![/bold]"
)
return
# Before proceeding, we check if we have downloaded an indicators index.
# If not, there's no point in proceeding with the updates check.
if ioc_updates.get_latest_update() == 0:
rich_print(
"\t\t[bold]You have not yet downloaded any indicators, check "
"the `download-iocs` command![/bold]"
)
return
# We only perform this check at a fixed frequency, in order to not
# overburden the user with too many lookups if the command is being run
# multiple times.
should_check, hours = ioc_updates.should_check()
if not should_check:
rich_print(
f"\t\tIndicators updates checked recently, next automatic check "
f"in {int(hours)} hours"
)
return
# We only perform this check at a fixed frequency, in order to not
# overburden the user with too many lookups if the command is being run
# multiple times.
should_check, hours = ioc_updates.should_check()
if not should_check:
rich_print(
f"\t\tIndicators updates checked recently, next automatic check "
f"in {int(hours)} hours"
)
return
try:
ioc_to_update = ioc_updates.check()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
try:
ioc_to_update = ioc_updates.check()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
rich_print(
"\t\t[bold]Note: Could not check for indicator updates.[/bold] "
"You may be working offline. Please update MVT indicators regularly."
)
except Exception as e:
log.error("Error encountered when trying to check latest MVT indicators: %s", e)
else:
if ioc_to_update:
rich_print(
"\t\t[bold]Note: Could not check for indicator updates.[/bold] "
"You may be working offline. Please update MVT indicators regularly."
)
except Exception as e:
log.error(
"Error encountered when trying to check latest MVT indicators: %s", e
"\t\t[bold]There are updates to your indicators files! "
"Run the `download-iocs` command to update![/bold]"
)
else:
if ioc_to_update:
rich_print(
"\t\t[bold]There are updates to your indicators files! "
"Run the `download-iocs` command to update![/bold]"
)
else:
rich_print("\t\tYour indicators files seem to be up to date.")
rich_print("\t\tYour indicators files seem to be up to date.")
def logo(
disable_version_check: bool = False, disable_indicator_check: bool = False
) -> None:
def logo() -> None:
rich_print("\n")
rich_print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
rich_print("\t\thttps://mvt.re")
rich_print(f"\t\tVersion: {MVT_VERSION}")
check_updates(disable_version_check, disable_indicator_check)
check_updates()
rich_print("\n")
+26 -28
View File
@@ -11,15 +11,15 @@ import re
from dataclasses import asdict, is_dataclass
from typing import Any, Dict, Optional
from .alerts import AlertStore
from .indicators import Indicators
from .module_types import (
ModuleAtomicResult,
ModuleResults,
ModuleSerializedResult,
ModuleTimeline,
)
from .utils import CustomJSONEncoder, exec_or_profile
from .indicators import Indicators
from .alerts import AlertStore
from .module_types import (
ModuleResults,
ModuleTimeline,
ModuleSerializedResult,
ModuleAtomicResult,
)
class DatabaseNotFoundError(Exception):
@@ -30,10 +30,6 @@ class DatabaseCorruptedError(Exception):
pass
class EncryptedBackupError(Exception):
pass
class InsufficientPrivileges(Exception):
pass
@@ -81,6 +77,7 @@ class MVTModule:
self.results: ModuleResults = results if results else []
self.timeline: ModuleTimeline = []
self.timeline_detected: ModuleTimeline = []
@classmethod
def from_json(cls, json_path: str, log: logging.Logger):
@@ -109,13 +106,10 @@ class MVTModule:
name = self.get_slug()
if self.results:
if isinstance(self.results, dict):
converted_results = self.results
else:
converted_results = [
asdict(result) if is_dataclass(result) else result
for result in self.results
]
converted_results = [
asdict(result) if is_dataclass(result) else result
for result in self.results
]
results_file_name = f"{name}.json"
results_json_path = os.path.join(self.results_path, results_file_name)
with open(results_json_path, "w", encoding="utf-8") as handle:
@@ -136,7 +130,7 @@ class MVTModule:
detected_json_path = os.path.join(self.results_path, detected_file_name)
with open(detected_json_path, "w", encoding="utf-8") as handle:
json.dump(
self.alertstore.as_json(), handle, indent=4, cls=CustomJSONEncoder
self.alertstore.alerts, handle, indent=4, cls=CustomJSONEncoder
)
def serialize(self, result: ModuleAtomicResult) -> ModuleSerializedResult:
@@ -153,10 +147,7 @@ class MVTModule:
for record in timeline:
timeline_set.add(
json.dumps(
asdict(record)
if is_dataclass(record) and not isinstance(record, type)
else record,
sort_keys=True,
asdict(record) if is_dataclass(record) else record, sort_keys=True
)
)
@@ -171,12 +162,21 @@ class MVTModule:
record: ModuleSerializedResult = self.serialize(result)
if record:
if isinstance(record, list):
self.timeline.extend(record) # type: ignore[arg-type]
self.timeline.extend(record)
else:
self.timeline.append(record) # type: ignore[arg-type]
self.timeline.append(record)
# for detected in self.alertstore.alerts:
# record = self.serialize(detected)
# if record:
# if isinstance(record, list):
# self.timeline_detected.extend(record)
# else:
# self.timeline_detected.append(record)
# De-duplicate timeline entries.
self.timeline = self._deduplicate_timeline(self.timeline)
# self.timeline_detected = self._deduplicate_timeline(self.timeline_detected)
def run(self) -> None:
"""Run the main module procedure."""
@@ -188,8 +188,6 @@ def run_module(module: MVTModule) -> None:
try:
exec_or_profile("module.run()", globals(), locals())
except EncryptedBackupError:
raise
except NotImplementedError:
module.log.exception(
"The run() procedure of module %s was not implemented yet!",
+7 -12
View File
@@ -3,17 +3,15 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .indicators import Indicator
from dataclasses import dataclass
from typing import Any, Dict, List, Union
from typing import List, Union, Optional
# ModuleAtomicResult is a flexible dictionary that can contain any data.
# Common fields include:
# - timestamp: Optional[str] - timestamp string
# - isodate: Optional[str] - ISO formatted date string
# - matched_indicator: Optional[Indicator] - indicator that matched this result
# - Any other module-specific fields
ModuleAtomicResult = Dict[str, Any]
@dataclass
class ModuleAtomicResult:
timestamp: Optional[str]
matched_indicator: Optional[Indicator]
ModuleResults = List[ModuleAtomicResult]
@@ -28,7 +26,4 @@ class ModuleAtomicTimeline:
ModuleTimeline = List[ModuleAtomicTimeline]
# ModuleSerializedResult can be a proper timeline object or a plain dict for compatibility
ModuleSerializedResult = Union[
ModuleAtomicTimeline, ModuleTimeline, Dict[str, Any], List[Dict[str, Any]]
]
ModuleSerializedResult = Union[ModuleAtomicTimeline, ModuleTimeline]
+5 -24
View File
@@ -12,9 +12,9 @@ import requests
import yaml
from packaging import version
from .config import settings
from .indicators import MVT_DATA_FOLDER, MVT_INDICATORS_FOLDER
from .version import MVT_VERSION
from .config import settings
log = logging.getLogger(__name__)
@@ -24,11 +24,7 @@ INDICATORS_CHECK_FREQUENCY = 12
class MVTUpdates:
def check(self) -> str:
try:
res = requests.get(str(settings.PYPI_UPDATE_URL), timeout=5)
except requests.exceptions.RequestException as e:
log.error("Failed to check for updates, skipping updates: %s", e)
return ""
res = requests.get(settings.PYPI_UPDATE_URL, timeout=15)
data = res.json()
latest_version = data.get("info", {}).get("version", "")
@@ -97,12 +93,7 @@ class IndicatorsUpdates:
url = self.github_raw_url.format(
self.index_owner, self.index_repo, self.index_branch, self.index_path
)
try:
res = requests.get(url, timeout=5)
except requests.exceptions.RequestException as e:
log.error("Failed to retrieve indicators index from %s: %s", url, e)
return None
res = requests.get(url, timeout=15)
if res.status_code != 200:
log.error(
"Failed to retrieve indicators index located at %s (error %d)",
@@ -114,12 +105,7 @@ class IndicatorsUpdates:
return yaml.safe_load(res.content)
def download_remote_ioc(self, ioc_url: str) -> Optional[str]:
try:
res = requests.get(ioc_url, timeout=15)
except requests.exceptions.RequestException as e:
log.error("Failed to download indicators file from %s: %s", ioc_url, e)
return None
res = requests.get(ioc_url, timeout=15)
if res.status_code != 200:
log.error(
"Failed to download indicators file from %s (error %d)",
@@ -185,12 +171,7 @@ class IndicatorsUpdates:
file_commit_url = (
f"https://api.github.com/repos/{owner}/{repo}/commits?path={path}"
)
try:
res = requests.get(file_commit_url, timeout=5)
except requests.exceptions.RequestException as e:
log.error("Failed to get details about file %s: %s", file_commit_url, e)
return -1
res = requests.get(file_commit_url, timeout=15)
if res.status_code != 200:
log.error(
"Failed to get details about file %s (error %d)",
+6 -12
View File
@@ -338,12 +338,11 @@ class URL:
:rtype: str
"""
tld_obj = get_tld(self.url, as_object=True, fix_protocol=True)
if isinstance(tld_obj, str):
return tld_obj
if tld_obj is None:
return ""
return tld_obj.parsed_url.netloc.lower().lstrip("www.")
return (
get_tld(self.url, as_object=True, fix_protocol=True)
.parsed_url.netloc.lower()
.lstrip("www.")
)
def get_top_level(self) -> str:
"""Get only the top-level domain from a URL.
@@ -352,12 +351,7 @@ class URL:
:rtype: str
"""
tld_obj = get_tld(self.url, as_object=True, fix_protocol=True)
if isinstance(tld_obj, str):
return tld_obj
if tld_obj is None:
return ""
return tld_obj.fld.lower()
return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower()
def check_if_shortened(self) -> bool:
"""Check if the URL is among list of shortener services.
+1 -5
View File
@@ -10,7 +10,6 @@ import json
import logging
import os
import re
from dataclasses import asdict, is_dataclass
from typing import Any, Iterator, Union
from .log import MVTLogHandler
@@ -31,9 +30,6 @@ class CustomJSONEncoder(json.JSONEncoder):
"""
def default(self, o):
# Unwrap dataclass instances (such as Indicator) to dict. Skip class itself.
if is_dataclass(o) and not isinstance(o, type):
return asdict(o)
if isinstance(o, bytes):
# Decode as utf-8, replace any invalid UTF-8 bytes with escaped hex
return o.decode("utf-8", errors="backslashreplace")
@@ -239,7 +235,7 @@ def init_logging(verbose: bool = False):
Initialise logging for the MVT module
"""
log = logging.getLogger("mvt")
log.setLevel(logging.DEBUG)
log.setLevel(logging.INFO)
consoleHandler = MVTLogHandler()
consoleHandler.setFormatter(logging.Formatter("%(message)s"))
if verbose:
+1 -1
View File
@@ -3,4 +3,4 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
MVT_VERSION = "2026.4.28"
MVT_VERSION = "3.0.0"
+3 -40
View File
@@ -37,8 +37,6 @@ from mvt.common.help import (
HELP_MSG_CHECK_IOCS,
HELP_MSG_STIX2,
HELP_MSG_CHECK_IOS_BACKUP,
HELP_MSG_DISABLE_UPDATE_CHECK,
HELP_MSG_DISABLE_INDICATOR_UPDATE_CHECK,
)
from .cmd_check_backup import CmdIOSCheckBackup
from .cmd_check_fs import CmdIOSCheckFS
@@ -55,37 +53,12 @@ MVT_IOS_BACKUP_PASSWORD = "MVT_IOS_BACKUP_PASSWORD"
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
# ==============================================================================
@click.group(invoke_without_command=False)
@click.option(
"--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,
)
def cli():
logo()
# ==============================================================================
@@ -246,8 +219,6 @@ def check_backup(
module_name=module,
module_options=module_options,
hashes=hashes,
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
if list_modules:
@@ -292,8 +263,6 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dum
module_name=module,
module_options=module_options,
hashes=hashes,
disable_version_check=_get_disable_flags(ctx)[0],
disable_indicator_check=_get_disable_flags(ctx)[1],
)
if list_modules:
@@ -324,13 +293,7 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dum
@click.argument("FOLDER", type=click.Path(exists=True))
@click.pass_context
def check_iocs(ctx, iocs, list_modules, module, folder):
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 = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module)
cmd.modules = BACKUP_MODULES + FS_MODULES + MIXED_MODULES
if list_modules:
-4
View File
@@ -27,8 +27,6 @@ class CmdIOSCheckBackup(Command):
module_options: Optional[dict] = None,
hashes: bool = False,
sub_command: bool = False,
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
super().__init__(
target_path=target_path,
@@ -41,8 +39,6 @@ class CmdIOSCheckBackup(Command):
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-backup"
+1 -4
View File
@@ -27,8 +27,6 @@ class CmdIOSCheckFS(Command):
module_options: Optional[dict] = None,
hashes: bool = False,
sub_command: bool = False,
disable_version_check: bool = False,
disable_indicator_check: bool = False,
) -> None:
super().__init__(
target_path=target_path,
@@ -36,12 +34,11 @@ class CmdIOSCheckFS(Command):
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
disable_version_check=disable_version_check,
disable_indicator_check=disable_indicator_check,
)
self.name = "check-fs"
-36
View File
@@ -194,41 +194,5 @@
{
"identifier": "iPhone16,2",
"description": "iPhone 15 Pro Max"
},
{
"identifier": "iPhone17,1",
"description": "iPhone 16 Pro"
},
{
"identifier": "iPhone17,2",
"description": "iPhone 16 Pro Max"
},
{
"identifier": "iPhone17,3",
"description": "iPhone 16"
},
{
"identifier": "iPhone17,4",
"description": "iPhone 16 Plus"
},
{
"identifier": "iPhone17,5",
"description": "iPhone 16e"
},
{
"identifier": "iPhone18,1",
"description": "iPhone 17 Pro"
},
{
"identifier": "iPhone18,2",
"description": "iPhone 17 Pro Max"
},
{
"identifier": "iPhone18,3",
"description": "iPhone 17"
},
{
"identifier": "iPhone18,4",
"description": "iPhone Air"
}
]
-136
View File
@@ -631,10 +631,6 @@
"build": "16H81",
"version": "12.5.7"
},
{
"version": "12.5.8",
"build": "16H88"
},
{
"build": "17A577",
"version": "13.0"
@@ -895,22 +891,6 @@
"version": "15.8.2",
"build": "19H384"
},
{
"version": "15.8.4",
"build": "19H390"
},
{
"version": "15.8.5",
"build": "19H394"
},
{
"version": "15.8.6",
"build": "19H402"
},
{
"version": "15.8.7",
"build": "19H411"
},
{
"build": "20A362",
"version": "16.0"
@@ -1012,22 +992,6 @@
"version": "16.7.8",
"build": "20H343"
},
{
"version": "16.7.11",
"build": "20H360"
},
{
"version": "16.7.12",
"build": "20H364"
},
{
"version": "16.7.14",
"build": "20H370"
},
{
"version": "16.7.15",
"build": "20H380"
},
{
"version": "17.0",
"build": "21A327"
@@ -1112,10 +1076,6 @@
"version": "17.6.1",
"build": "21G101"
},
{
"version": "17.7.7",
"build": "21H433"
},
{
"version": "18",
"build": "22A3354"
@@ -1143,101 +1103,5 @@
{
"version": "18.3",
"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": "18.7.2",
"build": "22H124"
},
{
"version": "18.7.3",
"build": "22H217"
},
{
"version": "18.7.4",
"build": "22H218"
},
{
"version": "18.7.5",
"build": "22H311"
},
{
"version": "18.7.6",
"build": "22H320"
},
{
"version": "18.7.7",
"build": "22H333"
},
{
"version": "18.7.8",
"build": "22H352"
},
{
"version": "26",
"build": "23A341"
},
{
"version": "26.0.1",
"build": "23A355"
},
{
"version": "26.1",
"build": "23B85"
},
{
"version": "26.2",
"build": "23C55"
},
{
"version": "26.2.1",
"build": "23C71"
},
{
"version": "26.3",
"build": "23D127"
},
{
"version": "26.3.1",
"build": "23D8133"
},
{
"version": "26.4",
"build": "23E246"
},
{
"version": "26.4.2",
"build": "23E261"
}
]
+2 -8
View File
@@ -46,21 +46,18 @@ class DecryptBackup:
"""
conn = sqlite3.connect(os.path.join(backup_path, "Manifest.db"))
cur = conn.cursor()
try:
cur = conn.cursor()
cur.execute("SELECT fileID FROM Files LIMIT 1;")
except sqlite3.DatabaseError:
return True
else:
log.critical("The backup does not seem encrypted!")
return False
finally:
conn.close()
def _process_file(
self, relative_path: str, domain: str, item, file_id: str, item_folder: str
) -> None:
assert self._backup is not None
self._backup.getFileDecryptedCopy(
manifestEntry=item, targetName=file_id, targetFolder=item_folder
)
@@ -73,9 +70,6 @@ class DecryptBackup:
)
def process_backup(self) -> None:
assert self._backup is not None
assert self.dest_path is not None
if not os.path.exists(self.dest_path):
os.makedirs(self.dest_path)
@@ -103,7 +97,7 @@ class DecryptBackup:
)
continue
item_folder = os.path.join(self.dest_path, file_id[0:2]) # type: ignore[arg-type]
item_folder = os.path.join(self.dest_path, file_id[0:2])
if not os.path.exists(item_folder):
os.makedirs(item_folder)
+1 -3
View File
@@ -36,11 +36,9 @@ class BackupInfo(IOSExtraction):
results=results,
)
self.results: dict = {}
self.results = {}
def run(self) -> None:
if not self.target_path:
raise DatabaseNotFoundError("target_path is not set")
info_path = os.path.join(self.target_path, "Info.plist")
if not os.path.exists(info_path):
raise DatabaseNotFoundError(
@@ -9,12 +9,12 @@ import plistlib
from base64 import b64encode
from typing import Optional
from mvt.common.utils import convert_datetime_to_iso
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleResults,
ModuleSerializedResult,
ModuleResults,
)
from mvt.common.utils import convert_datetime_to_iso
from ..base import IOSExtraction
@@ -72,17 +72,24 @@ class ConfigurationProfiles(IOSExtraction):
result["plist"]["PayloadUUID"]
)
if ioc_match:
warning_message = f'Found a known malicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with UUID "{result["plist"]["PayloadUUID"]}"'
self.alertstore.critical(
warning_message, "", result, matched_indicator=ioc_match.ioc
warning_message = (
f'Found a known malicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with UUID "{result["plist"]["PayloadUUID"]}"',
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
self.get_slug(), warning_message, "", result
)
self.alertstore.log_latest()
continue
# Highlight suspicious configuration profiles which may be used
# to hide notifications.
if payload_content["PayloadType"] in ["com.apple.notificationsettings"]:
warning_message = f'Found a potentially suspicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with payload type {payload_content["PayloadType"]}'
self.alertstore.medium(warning_message, "", result)
warning_message = (
f'Found a potentially suspicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with payload type {payload_content["PayloadType"]}',
)
self.alertstore.medum(self.get_slug(), warning_message, "", result)
self.alertstore.log_latest()
continue
def run(self) -> None:
+12 -22
View File
@@ -8,17 +8,16 @@ import io
import logging
import os
import plistlib
import sqlite3
from typing import Optional
from mvt.common.module import DatabaseNotFoundError, EncryptedBackupError
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleResults,
ModuleSerializedResult,
)
from mvt.common.module import DatabaseNotFoundError
from mvt.common.url import URL
from mvt.common.utils import convert_datetime_to_iso, convert_unix_to_iso
from mvt.common.module_types import (
ModuleResults,
ModuleAtomicResult,
ModuleSerializedResult,
)
from ..base import IOSExtraction
@@ -67,7 +66,7 @@ class Manifest(IOSExtraction):
return convert_unix_to_iso(timestamp_or_unix_time_int)
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
records: list = []
records = []
if "modified" not in record or "status_changed" not in record:
return records
@@ -103,9 +102,8 @@ class Manifest(IOSExtraction):
ioc_match = self.indicators.check_file_path("/" + result["relative_path"])
if ioc_match:
self.alertstore.high(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.high(self.get_slug(), ioc_match.message, "", result)
continue
rel_path = result["relative_path"].lower()
@@ -118,16 +116,15 @@ class Manifest(IOSExtraction):
ioc_match = self.indicators.check_url(part)
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.high(
self.get_slug(),
f'Found mention of domain "{ioc_match.ioc.value}" in a backup file with path: {rel_path}',
"",
result,
matched_indicator=ioc_match.ioc,
)
def run(self) -> None:
if not self.target_path:
raise DatabaseNotFoundError("target_path is not set")
manifest_db_path = os.path.join(self.target_path, "Manifest.db")
if not os.path.isfile(manifest_db_path):
raise DatabaseNotFoundError("unable to find backup's Manifest.db")
@@ -137,14 +134,7 @@ class Manifest(IOSExtraction):
conn = self._open_sqlite_db(manifest_db_path)
cur = conn.cursor()
try:
cur.execute("SELECT * FROM Files;")
except sqlite3.DatabaseError:
conn.close()
raise EncryptedBackupError(
"Manifest.db is not a valid SQLite database. "
"The backup may be encrypted."
)
cur.execute("SELECT * FROM Files;")
names = [description[0] for description in cur.description]
for file_entry in cur:
+12 -11
View File
@@ -7,12 +7,12 @@ import logging
import plistlib
from typing import Optional
from mvt.common.utils import convert_datetime_to_iso
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleResults,
ModuleSerializedResult,
)
from mvt.common.utils import convert_datetime_to_iso
from ..base import IOSExtraction
@@ -58,28 +58,29 @@ class ProfileEvents(IOSExtraction):
def check_indicators(self) -> None:
for result in self.results:
message = f'On {result.get("timestamp")} process "{result.get("process")}" started operation "{result.get("operation")}" of profile "{result.get("profile_id")}"'
self.alertstore.low(message, result.get("timestamp") or "", result)
self.alertstore.low(
self.get_slug(), message, result.get("timestamp"), result
)
self.alertstore.log_latest()
if not self.indicators:
return
for result in self.results:
ioc_match = self.indicators.check_process(result.get("process") or "")
ioc_match = self.indicators.check_process(result.get("process"))
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
continue
ioc_match = self.indicators.check_profile(result.get("profile_id") or "")
ioc_match = self.indicators.check_profile(result.get("profile_id"))
if ioc_match:
self.alertstore.critical(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
@staticmethod
def parse_profile_events(file_data: bytes) -> list:
results: list = []
results = []
events_plist = plistlib.loads(file_data)
+10 -32
View File
@@ -11,12 +11,8 @@ import sqlite3
import subprocess
from typing import Iterator, Optional, Union
from mvt.common.module import (
DatabaseCorruptedError,
DatabaseNotFoundError,
ModuleResults,
MVTModule,
)
from mvt.common.module import DatabaseCorruptedError, DatabaseNotFoundError
from mvt.common.module import MVTModule, ModuleResults
class IOSExtraction(MVTModule):
@@ -114,16 +110,12 @@ class IOSExtraction(MVTModule):
(Default value = None)
"""
if not self.target_path:
raise DatabaseNotFoundError("target_path is not set")
manifest_db_path = os.path.join(self.target_path, "Manifest.db")
if not os.path.exists(manifest_db_path):
raise DatabaseNotFoundError("unable to find backup's Manifest.db")
base_sql = "SELECT fileID, domain, relativePath FROM Files WHERE "
conn: Optional[sqlite3.Connection] = None
cur: Optional[sqlite3.Cursor] = None
try:
conn = self._open_sqlite_db(manifest_db_path)
cur = conn.cursor()
@@ -143,27 +135,17 @@ class IOSExtraction(MVTModule):
cur.execute(f"{base_sql} relativePath = ?;", (relative_path,))
elif domain:
cur.execute(f"{base_sql} domain = ?;", (domain,))
records = [
{
"file_id": row[0],
"domain": row[1],
"relative_path": row[2],
}
for row in cur
]
except Exception as exc:
raise DatabaseCorruptedError(f"failed to query Manifest.db: {exc}") from exc
finally:
if cur:
cur.close()
if conn:
conn.close()
return iter(records)
for row in cur:
yield {
"file_id": row[0],
"domain": row[1],
"relative_path": row[2],
}
def _get_backup_file_from_id(self, file_id: str) -> Union[str, None]:
if not self.target_path:
return None
file_path = os.path.join(self.target_path, file_id[0:2], file_id)
if os.path.exists(file_path):
return file_path
@@ -171,8 +153,6 @@ class IOSExtraction(MVTModule):
return None
def _get_fs_files_from_patterns(self, root_paths: list) -> Iterator[str]:
if not self.target_path:
return
for root_path in root_paths:
for found_path in glob.glob(os.path.join(self.target_path, root_path)):
if not os.path.exists(found_path):
@@ -194,10 +174,9 @@ class IOSExtraction(MVTModule):
:param backup_ids: Default value = None)
"""
file_path: Optional[str] = None
file_path = None
# First we check if the was an explicit file path specified.
if not self.file_path:
# Type narrowing: we know self.file_path is None here, work with local file_path
# If not, we first try with backups.
# We construct the path to the file according to the iTunes backup
# folder structure, if we have a valid ID.
@@ -219,9 +198,8 @@ class IOSExtraction(MVTModule):
# If we do not find any, we fail.
if file_path:
self.file_path = file_path # type: str
self.file_path = file_path
else:
raise DatabaseNotFoundError("unable to find the module's database file")
assert self.file_path is not None
self._recover_sqlite_db_if_needed(self.file_path)
+12 -11
View File
@@ -9,12 +9,12 @@ import plistlib
import sqlite3
from typing import Optional
from mvt.common.utils import convert_mactime_to_iso
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleResults,
ModuleSerializedResult,
ModuleAtomicResult,
)
from mvt.common.utils import convert_mactime_to_iso
from ..base import IOSExtraction
@@ -44,7 +44,6 @@ class Analytics(IOSExtraction):
log=log,
results=results,
)
self.results: list = []
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
return {
@@ -65,21 +64,23 @@ class Analytics(IOSExtraction):
ioc_match = self.indicators.check_process(value)
if ioc_match:
warning_message = f'Found mention of a malicious process "{value}" in {result["artifact"]} file at {result["isodate"]}'
new_result = copy.copy(result)
self.alertstore.critical(
warning_message, "", new_result, matched_indicator=ioc_match.ioc
warning_message = (
f'Found mention of a malicious process "{value}" in {result["artifact"]} file at {result["isodate"]}',
)
new_result = copy.copy(result)
new_result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
self.get_slug(), warning_message, "", new_result
)
self.alertstore.log_latest()
continue
ioc_match = self.indicators.check_url(value)
if ioc_match:
new_result = copy.copy(result)
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
ioc_match.message,
"",
new_result,
matched_indicator=ioc_match.ioc,
self.get_slug(), ioc_match.message, "", new_result
)
def _extract_analytics_data(self):
+28 -31
View File
@@ -10,10 +10,9 @@ from typing import Optional
from mvt.common.module_types import (
ModuleAtomicResult,
ModuleResults,
ModuleSerializedResult,
ModuleResults,
)
from ..base import IOSExtraction
@@ -54,17 +53,19 @@ class CacheFiles(IOSExtraction):
if not self.indicators:
return
self.alertstore.alerts = {}
for key, values in self.results.items():
for value in values:
ioc_match = self.indicators.check_url(value["url"])
if ioc_match:
value["cache_file"] = key
self.alertstore.critical(
ioc_match.message,
value.get("isodate", ""),
value,
matched_indicator=ioc_match.ioc,
)
value["matched_indicator"] = ioc_match.ioc
# XXX: Finish converting this method
if key not in self.alertstore.alerts:
self.alertstore.alerts[key] = [
value,
]
else:
self.alertstore.alerts[key].append(value)
def _process_cache_file(self, file_path):
self.log.info("Processing cache file at path: %s", file_path)
@@ -73,32 +74,28 @@ class CacheFiles(IOSExtraction):
cur = conn.cursor()
try:
try:
cur.execute("SELECT * FROM cfurl_cache_response;")
except sqlite3.OperationalError:
return
cur.execute("SELECT * FROM cfurl_cache_response;")
except sqlite3.OperationalError:
return
key_name = os.path.relpath(file_path, self.target_path)
if key_name not in self.results:
self.results[key_name] = []
key_name = os.path.relpath(file_path, self.target_path)
if key_name not in self.results:
self.results[key_name] = []
for row in cur:
self.results[key_name].append(
{
"entry_id": row[0],
"version": row[1],
"hash_value": row[2],
"storage_policy": row[3],
"url": row[4],
"isodate": row[5],
}
)
finally:
cur.close()
conn.close()
for row in cur:
self.results[key_name].append(
{
"entry_id": row[0],
"version": row[1],
"hash_value": row[2],
"storage_policy": row[3],
"url": row[4],
"isodate": row[5],
}
)
def run(self) -> None:
self.results: dict = {}
self.results = {}
for root, _, files in os.walk(self.target_path):
for file_name in files:
if file_name != "Cache.db":
+4 -6
View File
@@ -58,9 +58,8 @@ class Filesystem(IOSExtraction):
ioc_match = self.indicators.check_file_path(result["path"])
if ioc_match:
self.alertstore.high(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
self.alertstore.high(self.get_slug(), ioc_match.message, "", result)
self.alertstore.log_latest()
# If we are instructed to run fast, we skip the rest.
if self.module_options.get("fast_mode", None):
@@ -68,9 +67,8 @@ class Filesystem(IOSExtraction):
ioc_match = self.indicators.check_file_path_process(result["path"])
if ioc_match:
self.alertstore.high(
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
self.alertstore.high(self.get_slug(), ioc_match.message, "", result)
self.alertstore.log_latest()
def run(self) -> None:
for root, dirs, files in os.walk(self.target_path):
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff. Show more