mirror of
https://github.com/mvt-project/mvt.git
synced 2026-02-14 17:42:46 +00:00
Compare commits
1 Commits
dependabot
...
feature/st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac157a4421 |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
11
.github/workflows/black.yml
vendored
Normal file
11
.github/workflows/black.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
name: Black
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
black:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
options: "--check"
|
||||
23
.github/workflows/mypy.yml
vendored
23
.github/workflows/mypy.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Mypy
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
mypy_py3:
|
||||
name: Mypy check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
cache: 'pip'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@master
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install mypy
|
||||
- name: mypy
|
||||
run: |
|
||||
make mypy
|
||||
61
.github/workflows/publish-release-docker.yml
vendored
61
.github/workflows/publish-release-docker.yml
vendored
@@ -1,61 +0,0 @@
|
||||
#
|
||||
name: Create and publish a Docker image
|
||||
|
||||
# Configures this workflow to run every time a release is published.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
#
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
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
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 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@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# 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@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# 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
|
||||
|
||||
50
.github/workflows/python-package.yml
vendored
Normal file
50
.github/workflows/python-package.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.8', '3.9', '3.10'] # , '3.11']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade setuptools
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest safety stix2 pytest-mock pytest-cov
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
python -m pip install .
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Safety checks
|
||||
run: safety check
|
||||
- name: Test with pytest and coverage
|
||||
run: pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=mvt tests/ | tee pytest-coverage.txt
|
||||
- name: Pytest coverage comment
|
||||
continue-on-error: true # Workflows running on a fork can't post comments
|
||||
uses: MishaKav/pytest-coverage-comment@main
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
pytest-coverage-path: ./pytest-coverage.txt
|
||||
junitxml-path: ./pytest.xml
|
||||
12
.github/workflows/ruff.yml
vendored
12
.github/workflows/ruff.yml
vendored
@@ -4,24 +4,16 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
ruff_py3:
|
||||
name: Ruff syntax check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
cache: 'pip'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@master
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install ruff
|
||||
pip install --user ruff
|
||||
- name: ruff
|
||||
run: |
|
||||
make ruff
|
||||
ruff check --output-format github .
|
||||
|
||||
@@ -54,7 +54,7 @@ def parse_latest_ios_versions(rss_feed_text):
|
||||
|
||||
|
||||
def update_mvt(mvt_checkout_path, latest_ios_versions):
|
||||
version_path = os.path.join(mvt_checkout_path, "src/mvt/ios/data/ios_versions.json")
|
||||
version_path = os.path.join(mvt_checkout_path, "mvt/ios/data/ios_versions.json")
|
||||
with open(version_path, "r") as version_file:
|
||||
current_versions = json.load(version_file)
|
||||
|
||||
|
||||
38
.github/workflows/tests.yml
vendored
38
.github/workflows/tests.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Run Python Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
make install
|
||||
make test-requirements
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
set -o pipefail
|
||||
make test-ci | tee pytest-coverage.txt
|
||||
|
||||
- name: Pytest coverage comment
|
||||
continue-on-error: true # Workflows running on a fork can't post comments
|
||||
uses: MishaKav/pytest-coverage-comment@main
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
pytest-coverage-path: ./pytest-coverage.txt
|
||||
junitxml-path: ./pytest.xml
|
||||
1
.github/workflows/update-ios-data.yml
vendored
1
.github/workflows/update-ios-data.yml
vendored
@@ -21,7 +21,6 @@ jobs:
|
||||
title: '[auto] Update iOS releases and versions'
|
||||
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: |
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Safety Security and License Configuration file
|
||||
# We recommend checking this file into your source control in the root of your Python project
|
||||
# If this file is named .safety-policy.yml and is in the same directory where you run `safety check` it will be used by default.
|
||||
# Otherwise, you can use the flag `safety check --policy-file <path-to-this-file>` to specify a custom location and name for the file.
|
||||
# To validate and review your policy file, run the validate command: `safety validate policy_file --path <path-to-this-file>`
|
||||
security: # configuration for the `safety check` command
|
||||
ignore-vulnerabilities: # Here you can list multiple specific vulnerabilities you want to ignore (optionally for a time period)
|
||||
67599: # Example vulnerability ID
|
||||
reason: disputed, inapplicable
|
||||
70612:
|
||||
reason: disputed, inapplicable
|
||||
@@ -1,65 +1,19 @@
|
||||
# Contributing to Mobile Verification Toolkit (MVT)
|
||||
# Contributing
|
||||
|
||||
We greatly appreciate contributions to MVT!
|
||||
|
||||
Your involvement, whether through identifying issues, improving functionality, or enhancing documentation, is very much appreciated. To ensure smooth collaboration and a welcoming environment, we've outlined some key guidelines for contributing below.
|
||||
|
||||
## Getting started
|
||||
|
||||
Contributing to an open-source project like MVT might seem overwhelming at first, but we're here to support you!
|
||||
|
||||
Whether you're a technologist, a frontline human rights defender, a field researcher, or someone new to consensual spyware forensics, there are many ways to make meaningful contributions.
|
||||
|
||||
Here's how you can get started:
|
||||
|
||||
1. **Explore the codebase:**
|
||||
- Browse the repository to get familar with MVT. Many MVT modules are simple in functionality and easy to understand.
|
||||
- Look for `TODO:` or `FIXME:` comments in the code for areas that need attention.
|
||||
|
||||
2. **Check Github issues:**
|
||||
- Look for issues tagged with ["help wanted"](https://github.com/mvt-project/mvt/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or ["good first issue"](https://github.com/mvt-project/mvt/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to find tasks that are beginner-friendly or where input from the community would be helpful.
|
||||
|
||||
3. **Ask for guidance:**
|
||||
|
||||
- If you're unsure where to start, feel free to open a [discussion](https://github.com/mvt-project/mvt/discussions) or comment on an issue.
|
||||
|
||||
## How to contribute:
|
||||
|
||||
1. **Report issues:**
|
||||
|
||||
- Found a bug? Please check existing issues to see if it's already reported. If not, open a new issue. Mobile operating systems and databases are constantly evolving, an new errors may appear spontaniously in new app versions.
|
||||
|
||||
**Please provide as much information as possible about the prodblem including: any error messages, steps to reproduce the problem, and any logs or screenshots that can help.**
|
||||
Thank you for your interest in contributing to Mobile Verification Toolkit (MVT)! Your help is very much appreciated.
|
||||
|
||||
|
||||
2. **Suggest features:**
|
||||
- If you have an idea for new functionality, create a feature request issue and describe your proposal.
|
||||
## Where to start
|
||||
|
||||
3. **Submit code:**
|
||||
- Fork the repository and create a new branch for your changes.
|
||||
- Ensure your changes align with the code style guidelines (see below).
|
||||
- Open a pull request (PR) with a clear description of your changes and link it to any relevant issues.
|
||||
Starting to contribute to a somewhat complex project like MVT might seem intimidating. Unless you have specific ideas of new functionality you would like to submit, some good starting points are searching for `TODO:` and `FIXME:` comments throughout the code. Alternatively you can check if any GitHub issues existed marked with the ["help wanted"](https://github.com/mvt-project/mvt/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) tag.
|
||||
|
||||
4. **Documentation contributions:**
|
||||
- Improving documentation is just as valuable as contributing code! If you notice gaps or inaccuracies in the documentation, feel free to submit changes or suggest updates.
|
||||
|
||||
## Code style
|
||||
Please follow these code style guidelines for consistency and readability:
|
||||
|
||||
- **Indentation**: use 4 spaces per tab.
|
||||
- **Quotes**: Use double quotes (`"`) by default. Use single quotes (`'`) for nested strings instead of escaping (`\"`), or when using f-formatting.
|
||||
- **Maximum line length**:
|
||||
- Aim for lines no longer than 80 characters.
|
||||
- Exceptions are allowed for long log lines or strings, which may extend up to 100 characters.
|
||||
- Wrap lines that exceed 100 characters.
|
||||
When contributing code to
|
||||
|
||||
Follow [PEP 8 guidelines](https://peps.python.org/pep-0008/) for indentation and overall Python code style. All MVT code is automatically linted with [Ruff](https://github.com/astral-sh/ruff) before merging.
|
||||
- **Indentation**: we use 4-spaces tabs.
|
||||
|
||||
Please check your code before opening a pull request by running `make ruff`
|
||||
- **Quotes**: we use double quotes (`"`) as a default. Single quotes (`'`) can be favored with nested strings instead of escaping (`\"`), or when using f-formatting.
|
||||
|
||||
|
||||
## Community and support
|
||||
|
||||
We aim to create a supportive and collaborative environment for all contributors. If you run into any challenges, feel free to reach out through the discussions or issues section of the repository.
|
||||
|
||||
Your contributions, big or small, help improve MVT and are always appreciated.
|
||||
- **Maximum line length**: we strongly encourage to respect a 80 characters long lines and to follow [PEP8 indentation guidelines](https://peps.python.org/pep-0008/#indentation) when having to wrap. However, if breaking at 80 is not possible or is detrimental to the readability of the code, exceptions are tolerated. For example, long log lines, or long strings can be extended to 100 characters long. Please hard wrap anything beyond 100 characters.
|
||||
|
||||
179
Dockerfile
179
Dockerfile
@@ -1,158 +1,79 @@
|
||||
# Base image for building libraries
|
||||
# ---------------------------------
|
||||
FROM ubuntu:22.04 as build-base
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
# Ref. https://github.com/mvt-project/mvt
|
||||
|
||||
# Install build tools and dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
LABEL url="https://mvt.re"
|
||||
LABEL vcs-url="https://github.com/mvt-project/mvt"
|
||||
LABEL description="MVT is a forensic tool to look for signs of infection in smartphone devices."
|
||||
|
||||
ENV PIP_NO_CACHE_DIR=1
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Fixing major OS dependencies
|
||||
# ----------------------------
|
||||
RUN apt update \
|
||||
&& apt install -y python3 python3-pip libusb-1.0-0-dev wget unzip default-jre-headless adb \
|
||||
|
||||
# Install build tools for libimobiledevice
|
||||
# ----------------------------------------
|
||||
build-essential \
|
||||
checkinstall \
|
||||
git \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool-bin \
|
||||
pkg-config \
|
||||
libcurl4-openssl-dev \
|
||||
libusb-1.0-0-dev \
|
||||
libplist-dev \
|
||||
libusbmuxd-dev \
|
||||
libssl-dev \
|
||||
udev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
sqlite3 \
|
||||
pkg-config \
|
||||
|
||||
|
||||
# libplist
|
||||
# Clean up
|
||||
# --------
|
||||
FROM build-base as build-libplist
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
|
||||
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libplist
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt
|
||||
|
||||
|
||||
# libimobiledevice-glue
|
||||
# ---------------------
|
||||
FROM build-base as build-libimobiledevice-glue
|
||||
# Build libimobiledevice
|
||||
# ----------------------
|
||||
RUN git clone https://github.com/libimobiledevice/libplist \
|
||||
&& git clone https://github.com/libimobiledevice/libimobiledevice-glue \
|
||||
&& git clone https://github.com/libimobiledevice/libusbmuxd \
|
||||
&& git clone https://github.com/libimobiledevice/libimobiledevice \
|
||||
&& git clone https://github.com/libimobiledevice/usbmuxd \
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
&& cd libplist && ./autogen.sh && make && make install && ldconfig \
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libimobiledevice-glue && cd libimobiledevice-glue \
|
||||
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libimobiledevice-glue
|
||||
&& cd ../libimobiledevice-glue && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr && make && make install && ldconfig \
|
||||
|
||||
&& cd ../libusbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh && make && make install && ldconfig \
|
||||
|
||||
# libtatsu
|
||||
# --------
|
||||
FROM build-base as build-libtatsu
|
||||
&& cd ../libimobiledevice && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --enable-debug && make && make install && ldconfig \
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
&& cd ../usbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make && make install \
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libtatsu && cd libtatsu \
|
||||
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libtatsu
|
||||
# Clean up.
|
||||
&& cd .. && rm -rf libplist libimobiledevice-glue libusbmuxd libimobiledevice usbmuxd
|
||||
|
||||
|
||||
# libusbmuxd
|
||||
# ----------
|
||||
FROM build-base as build-libusbmuxd
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
COPY --from=build-libimobiledevice-glue /build /
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libusbmuxd && cd libusbmuxd \
|
||||
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libusbmuxd
|
||||
|
||||
|
||||
# libimobiledevice
|
||||
# ----------------
|
||||
FROM build-base as build-libimobiledevice
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
COPY --from=build-libtatsu /build /
|
||||
COPY --from=build-libimobiledevice-glue /build /
|
||||
COPY --from=build-libusbmuxd /build /
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libimobiledevice && cd libimobiledevice \
|
||||
&& ./autogen.sh --enable-debug && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libimobiledevice
|
||||
|
||||
|
||||
# usbmuxd
|
||||
# -------
|
||||
FROM build-base as build-usbmuxd
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
COPY --from=build-libimobiledevice-glue /build /
|
||||
COPY --from=build-libusbmuxd /build /
|
||||
COPY --from=build-libimobiledevice /build /
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
|
||||
&& ./autogen.sh --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf usbmuxd && mv /build/lib /build/usr/lib
|
||||
|
||||
|
||||
# Create main image
|
||||
FROM ubuntu:24.04 as main
|
||||
|
||||
LABEL org.opencontainers.image.url="https://mvt.re"
|
||||
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
|
||||
LABEL org.opencontainers.image.source="https://github.com/mvt-project/mvt"
|
||||
LABEL org.opencontainers.image.title="Mobile Verification Toolkit"
|
||||
LABEL org.opencontainers.image.description="MVT is a forensic tool to look for signs of infection in smartphone devices."
|
||||
LABEL org.opencontainers.image.licenses="MVT License 1.1"
|
||||
LABEL org.opencontainers.image.base.name=docker.io/library/ubuntu:22.04
|
||||
|
||||
# Install runtime dependencies
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
adb \
|
||||
default-jre-headless \
|
||||
libcurl4 \
|
||||
libssl3 \
|
||||
libusb-1.0-0 \
|
||||
python3 \
|
||||
sqlite3
|
||||
COPY --from=build-libplist /build /
|
||||
COPY --from=build-libimobiledevice-glue /build /
|
||||
COPY --from=build-libtatsu /build /
|
||||
COPY --from=build-libusbmuxd /build /
|
||||
COPY --from=build-libimobiledevice /build /
|
||||
COPY --from=build-usbmuxd /build /
|
||||
|
||||
# Install mvt using the locally checked out source
|
||||
COPY . mvt/
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y git python3-pip \
|
||||
&& PIP_NO_CACHE_DIR=1 pip3 install --break-system-packages ./mvt \
|
||||
&& apt-get remove -y python3-pip git && apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf mvt
|
||||
# Installing MVT
|
||||
# --------------
|
||||
RUN pip3 install git+https://github.com/mvt-project/mvt.git@main
|
||||
|
||||
# Installing ABE
|
||||
ADD --checksum=sha256:a20e07f8b2ea47620aff0267f230c3f1f495f097081fd709eec51cf2a2e11632 \
|
||||
https://github.com/nelenkov/android-backup-extractor/releases/download/master-20221109063121-8fdfc5e/abe.jar /opt/abe/abe.jar
|
||||
# --------------
|
||||
RUN mkdir /opt/abe \
|
||||
&& wget https://github.com/nelenkov/android-backup-extractor/releases/download/master-20221109063121-8fdfc5e/abe.jar -O /opt/abe/abe.jar \
|
||||
# Create alias for abe
|
||||
RUN echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
|
||||
&& echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
|
||||
|
||||
# Generate adb key folder
|
||||
RUN echo 'if [ ! -f /root/.android/adbkey ]; then adb keygen /root/.android/adbkey 2&>1 > /dev/null; fi' >> ~/.bashrc
|
||||
RUN mkdir /root/.android
|
||||
# Generate adb key folder
|
||||
# ------------------------------
|
||||
RUN mkdir /root/.android && adb keygen /root/.android/adbkey
|
||||
|
||||
# Setup investigations environment
|
||||
# --------------------------------
|
||||
RUN mkdir /home/cases
|
||||
WORKDIR /home/cases
|
||||
WORKDIR /home/cases
|
||||
RUN echo 'echo "Mobile Verification Toolkit @ Docker\n------------------------------------\n\nYou can find information about how to use this image for Android (https://github.com/mvt-project/mvt/tree/master/docs/android) and iOS (https://github.com/mvt-project/mvt/tree/master/docs/ios) in the official docs of the project.\n"' >> ~/.bashrc \
|
||||
&& echo 'echo "Note that to perform the debug via USB you might need to give the Docker image access to the USB using \"docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt\" or, preferably, the \"--device=\" parameter.\n"' >> ~/.bashrc
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# Create main image
|
||||
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"
|
||||
LABEL org.opencontainers.image.source="https://github.com/mvt-project/mvt"
|
||||
LABEL org.opencontainers.image.title="Mobile Verification Toolkit (Android)"
|
||||
LABEL org.opencontainers.image.description="MVT is a forensic tool to look for signs of infection in smartphone devices."
|
||||
LABEL org.opencontainers.image.licenses="MVT License 1.1"
|
||||
LABEL org.opencontainers.image.base.name=docker.io/library/python:3.10.14-alpine3.20
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache \
|
||||
android-tools \
|
||||
git \
|
||||
libusb \
|
||||
openjdk11-jre-headless \
|
||||
sqlite
|
||||
|
||||
# Install mvt
|
||||
COPY ./ mvt
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev \
|
||||
&& PIP_NO_CACHE_DIR=1 pip3 install ./mvt \
|
||||
&& apk del .build-deps gcc musl-dev && rm -rf ./mvt
|
||||
|
||||
# Installing ABE
|
||||
ADD --checksum=sha256:a20e07f8b2ea47620aff0267f230c3f1f495f097081fd709eec51cf2a2e11632 \
|
||||
https://github.com/nelenkov/android-backup-extractor/releases/download/master-20221109063121-8fdfc5e/abe.jar /opt/abe/abe.jar
|
||||
# Create alias for abe
|
||||
RUN echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
|
||||
|
||||
# Generate adb key folder
|
||||
RUN echo 'if [ ! -f /root/.android/adbkey ]; then adb keygen /root/.android/adbkey 2&>1 > /dev/null; fi' >> ~/.bashrc
|
||||
RUN mkdir /root/.android
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/mvt-android" ]
|
||||
137
Dockerfile.ios
137
Dockerfile.ios
@@ -1,137 +0,0 @@
|
||||
# Base image for building libraries
|
||||
# ---------------------------------
|
||||
FROM ubuntu:22.04 as build-base
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install build tools and dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
build-essential \
|
||||
git \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool-bin \
|
||||
pkg-config \
|
||||
libcurl4-openssl-dev \
|
||||
libusb-1.0-0-dev \
|
||||
libssl-dev \
|
||||
udev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# libplist
|
||||
# --------
|
||||
FROM build-base as build-libplist
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
|
||||
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libplist
|
||||
|
||||
|
||||
# libimobiledevice-glue
|
||||
# ---------------------
|
||||
FROM build-base as build-libimobiledevice-glue
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libimobiledevice-glue && cd libimobiledevice-glue \
|
||||
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libimobiledevice-glue
|
||||
|
||||
|
||||
# libtatsu
|
||||
# --------
|
||||
FROM build-base as build-libtatsu
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libtatsu && cd libtatsu \
|
||||
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libtatsu
|
||||
|
||||
|
||||
# libusbmuxd
|
||||
# ----------
|
||||
FROM build-base as build-libusbmuxd
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
COPY --from=build-libimobiledevice-glue /build /
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libusbmuxd && cd libusbmuxd \
|
||||
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libusbmuxd
|
||||
|
||||
|
||||
# libimobiledevice
|
||||
# ----------------
|
||||
FROM build-base as build-libimobiledevice
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
COPY --from=build-libtatsu /build /
|
||||
COPY --from=build-libimobiledevice-glue /build /
|
||||
COPY --from=build-libusbmuxd /build /
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/libimobiledevice && cd libimobiledevice \
|
||||
&& ./autogen.sh --enable-debug && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf libimobiledevice
|
||||
|
||||
|
||||
# usbmuxd
|
||||
# -------
|
||||
FROM build-base as build-usbmuxd
|
||||
|
||||
# Install dependencies
|
||||
COPY --from=build-libplist /build /
|
||||
COPY --from=build-libimobiledevice-glue /build /
|
||||
COPY --from=build-libusbmuxd /build /
|
||||
COPY --from=build-libimobiledevice /build /
|
||||
|
||||
# Build
|
||||
RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
|
||||
&& ./autogen.sh --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make -j "$(nproc)" && make install DESTDIR=/build \
|
||||
&& cd .. && rm -rf usbmuxd && mv /build/lib /build/usr/lib
|
||||
|
||||
|
||||
# Main image
|
||||
# ----------
|
||||
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"
|
||||
LABEL org.opencontainers.image.source="https://github.com/mvt-project/mvt"
|
||||
LABEL org.opencontainers.image.title="Mobile Verification Toolkit (iOS)"
|
||||
LABEL org.opencontainers.image.description="MVT is a forensic tool to look for signs of infection in smartphone devices."
|
||||
LABEL org.opencontainers.image.licenses="MVT License 1.1"
|
||||
LABEL org.opencontainers.image.base.name=docker.io/library/python:3.10.14-alpine3.20
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache \
|
||||
gcompat \
|
||||
libcurl \
|
||||
libssl3 \
|
||||
libusb \
|
||||
sqlite
|
||||
COPY --from=build-libplist /build /
|
||||
COPY --from=build-libimobiledevice-glue /build /
|
||||
COPY --from=build-libtatsu /build /
|
||||
COPY --from=build-libusbmuxd /build /
|
||||
COPY --from=build-libimobiledevice /build /
|
||||
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 \
|
||||
&& 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" ]
|
||||
36
Makefile
36
Makefile
@@ -1,39 +1,23 @@
|
||||
PWD = $(shell pwd)
|
||||
|
||||
check: ruff mypy
|
||||
check:
|
||||
flake8
|
||||
ruff check -q .
|
||||
black --check .
|
||||
pytest -q
|
||||
|
||||
ruff:
|
||||
ruff check .
|
||||
|
||||
mypy:
|
||||
mypy
|
||||
|
||||
test:
|
||||
python3 -m pytest
|
||||
|
||||
test-ci:
|
||||
python3 -m pytest -v
|
||||
|
||||
install:
|
||||
python3 -m pip install --upgrade -e .
|
||||
|
||||
test-requirements:
|
||||
python3 -m pip install --upgrade --group dev
|
||||
|
||||
generate-proto-parsers:
|
||||
# Generate python parsers for protobuf files
|
||||
PROTO_FILES=$$(find src/mvt/android/parsers/proto/ -iname "*.proto"); \
|
||||
protoc -Isrc/mvt/android/parsers/proto/ --python_betterproto_out=src/mvt/android/parsers/proto/ $$PROTO_FILES
|
||||
|
||||
clean:
|
||||
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/src/mvt.egg-info
|
||||
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/mvt.egg-info
|
||||
|
||||
dist:
|
||||
python3 -m pip install --upgrade build
|
||||
python3 -m build
|
||||
python3 setup.py sdist bdist_wheel
|
||||
|
||||
upload:
|
||||
python3 -m twine upload dist/*
|
||||
|
||||
test-upload:
|
||||
python3 -m twine upload --repository testpypi dist/*
|
||||
|
||||
pylint:
|
||||
pylint --rcfile=setup.cfg mvt
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
[](https://pypi.org/project/mvt/)
|
||||
[](https://docs.mvt.re/en/latest/?badge=latest)
|
||||
[](https://github.com/mvt-project/mvt/actions/workflows/tests.yml)
|
||||
[](https://github.com/mvt-project/mvt/actions/workflows/python-package.yml)
|
||||
[](https://pepy.tech/project/mvt)
|
||||
|
||||
Mobile Verification Toolkit (MVT) is a collection of utilities to simplify and automate the process of gathering forensic traces helpful to identify a potential compromise of Android and iOS devices.
|
||||
@@ -26,7 +26,7 @@ MVT supports using public [indicators of compromise (IOCs)](https://github.com/m
|
||||
>
|
||||
> Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
|
||||
>
|
||||
>Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/get-help/?c=mvt_docs) or through our forensic partnership with [Access Now’s Digital Security Helpline](https://www.accessnow.org/help/).
|
||||
>Such support is available to civil society through [Amnesty International's Security Lab](https://www.amnesty.org/en/tech/) or through our forensic partnership with [Access Now’s Digital Security Helpline](https://www.accessnow.org/help/).
|
||||
|
||||
More information about using indicators of compromise with MVT is available in the [documentation](https://docs.mvt.re/en/latest/iocs/).
|
||||
|
||||
|
||||
59
SECURITY.md
59
SECURITY.md
@@ -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).
|
||||
|
||||
14
dev/mvt-android
Executable file
14
dev/mvt-android
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from mvt import android
|
||||
|
||||
android.cli()
|
||||
14
dev/mvt-ios
Executable file
14
dev/mvt-ios
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from mvt import ios
|
||||
|
||||
ios.cli()
|
||||
@@ -16,12 +16,6 @@ Now you can try launching MVT with:
|
||||
mvt-android check-adb --output /path/to/results
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The `check-adb` command is deprecated and will be removed in a future release.
|
||||
Whenever possible, prefer acquiring device data using the AndroidQF project (https://github.com/mvt-project/androidqf/) and then analyze those acquisitions with MVT.
|
||||
|
||||
Running `mvt-android check-adb` will also emit a runtime deprecation warning advising you to migrate to AndroidQF.
|
||||
|
||||
If you have previously started an adb daemon MVT will alert you and require you to kill it with `adb kill-server` and relaunch the command.
|
||||
|
||||
!!! warning
|
||||
@@ -43,14 +37,6 @@ mvt-android check-adb --serial 192.168.1.20:5555 --output /path/to/results
|
||||
|
||||
Where `192.168.1.20` is the correct IP address of your device.
|
||||
|
||||
!!! warning
|
||||
The `check-adb` workflow shown above is deprecated. If you can acquire an AndroidQF acquisition from the device (recommended), use the AndroidQF project to create that acquisition: https://github.com/mvt-project/androidqf/
|
||||
|
||||
AndroidQF acquisitions provide a more stable, reproducible analysis surface and are the preferred workflow going forward.
|
||||
|
||||
## MVT modules requiring root privileges
|
||||
|
||||
!!! warning
|
||||
Deprecated: many `mvt-android check-adb` workflows are deprecated and will be removed in a future release. Whenever possible, prefer acquiring an AndroidQF acquisition using the AndroidQF project (https://github.com/mvt-project/androidqf/).
|
||||
|
||||
Of the currently available `mvt-android check-adb` modules a handful require root privileges to function correctly. This is because certain files, such as browser history and SMS messages databases are not accessible with user privileges through adb. These modules are to be considered OPTIONALLY available in case the device was already jailbroken. **Do NOT jailbreak your own device unless you are sure of what you are doing!** Jailbreaking your phone exposes it to considerable security risks!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
MVT allows to attempt to download all available installed packages (APKs) 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:
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# Command Completion
|
||||
|
||||
MVT utilizes the [Click](https://click.palletsprojects.com/en/stable/) library for creating its command line interface.
|
||||
|
||||
Click provides tab completion support for Bash (version 4.4 and up), Zsh, and Fish.
|
||||
|
||||
To enable it, you need to manually register a special function with your shell, which varies depending on the shell you are using.
|
||||
|
||||
The following describes how to generate the command completion scripts and add them to your shell configuration.
|
||||
|
||||
> **Note: You will need to start a new shell for the changes to take effect.**
|
||||
|
||||
### For Bash
|
||||
|
||||
```bash
|
||||
# Generates bash completion scripts
|
||||
echo "$(_MVT_IOS_COMPLETE=bash_source mvt-ios)" > ~/.mvt-ios-complete.bash &&
|
||||
echo "$(_MVT_ANDROID_COMPLETE=bash_source mvt-android)" > ~/.mvt-android-complete.bash
|
||||
```
|
||||
|
||||
Add the following to `~/.bashrc`:
|
||||
```bash
|
||||
# source mvt completion scripts
|
||||
. ~/.mvt-ios-complete.bash && . ~/.mvt-android-complete.bash
|
||||
```
|
||||
|
||||
### For Zsh
|
||||
|
||||
```bash
|
||||
# Generates zsh completion scripts
|
||||
echo "$(_MVT_IOS_COMPLETE=zsh_source mvt-ios)" > ~/.mvt-ios-complete.zsh &&
|
||||
echo "$(_MVT_ANDROID_COMPLETE=zsh_source mvt-android)" > ~/.mvt-android-complete.zsh
|
||||
```
|
||||
|
||||
Add the following to `~/.zshrc`:
|
||||
```bash
|
||||
# source mvt completion scripts
|
||||
. ~/.mvt-ios-complete.zsh && . ~/.mvt-android-complete.zsh
|
||||
```
|
||||
|
||||
For more information, visit the official [Click Docs](https://click.palletsprojects.com/en/stable/shell-completion/#enabling-completion).
|
||||
|
||||
|
||||
@@ -2,22 +2,7 @@ Using Docker simplifies having all the required dependencies and tools (includin
|
||||
|
||||
Install Docker following the [official documentation](https://docs.docker.com/get-docker/).
|
||||
|
||||
Once Docker is installed, you can run MVT by downloading a prebuilt MVT Docker image, or by building a Docker image yourself from the MVT source repo.
|
||||
|
||||
### Using the prebuilt Docker image
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/mvt-project/mvt
|
||||
```
|
||||
|
||||
You can then run the Docker container with:
|
||||
|
||||
```
|
||||
docker run -it ghcr.io/mvt-project/mvt
|
||||
```
|
||||
|
||||
|
||||
### Build and run Docker image from source
|
||||
Once installed, you can clone MVT's repository and build its Docker image:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mvt-project/mvt.git
|
||||
@@ -33,9 +18,6 @@ docker run -it mvt
|
||||
|
||||
If a prompt is spawned successfully, you can close it with `exit`.
|
||||
|
||||
|
||||
## Docker usage with Android devices
|
||||
|
||||
If you wish to use MVT to test an Android device you will need to enable the container's access to the host's USB devices. You can do so by enabling the `--privileged` flag and mounting the USB bus device as a volume:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -7,27 +7,11 @@ Before proceeding, please note that MVT requires Python 3.6+ to run. While it sh
|
||||
First install some basic dependencies that will be necessary to build all required tools:
|
||||
|
||||
```bash
|
||||
sudo apt install python3 python3-venv python3-pip sqlite3 libusb-1.0-0
|
||||
sudo apt install python3 python3-pip libusb-1.0-0 sqlite3
|
||||
```
|
||||
|
||||
*libusb-1.0-0* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
|
||||
|
||||
(Recommended) Set up `pipx`
|
||||
|
||||
For Ubuntu 23.04 or above:
|
||||
```bash
|
||||
sudo apt install pipx
|
||||
pipx ensurepath
|
||||
```
|
||||
|
||||
For Ubuntu 22.04 or below:
|
||||
```
|
||||
python3 -m pip install --user pipx
|
||||
python3 -m pipx ensurepath
|
||||
```
|
||||
|
||||
Other distributions: check for a `pipx` or `python-pipx` via your package manager.
|
||||
|
||||
When working with Android devices you should additionally install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you prefer to install a package made available by your distribution of choice, please make sure the version is recent to ensure compatibility with modern Android devices.
|
||||
|
||||
## Dependencies on macOS
|
||||
@@ -37,7 +21,7 @@ Running MVT on macOS requires Xcode and [homebrew](https://brew.sh) to be instal
|
||||
In order to install dependencies use:
|
||||
|
||||
```bash
|
||||
brew install python3 pipx libusb sqlite3
|
||||
brew install python3 libusb sqlite3
|
||||
```
|
||||
|
||||
*libusb* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
|
||||
@@ -58,47 +42,24 @@ It is recommended to try installing and running MVT from [Windows Subsystem Linu
|
||||
|
||||
## Installing MVT
|
||||
|
||||
### Installing from PyPI with pipx (recommended)
|
||||
1. Install `pipx` following the instructions above for your OS/distribution. Make sure to run `pipx ensurepath` and open a new terminal window.
|
||||
2. ```bash
|
||||
pipx install mvt
|
||||
```
|
||||
If you haven't done so, you can add this to your `.bashrc` or `.zshrc` file in order to add locally installed PyPI binaries to your `$PATH`:
|
||||
|
||||
You now should have the `mvt-ios` and `mvt-android` utilities installed. If you run into problems with these commands not being found, ensure you have run `pipx ensurepath` and opened a new terminal window.
|
||||
|
||||
### Installing from PyPI directly into a virtual environment
|
||||
You can use `pipenv`, `poetry` etc. for your virtual environment, but the provided example is with the built-in `venv` tool:
|
||||
|
||||
1. Create the virtual environment in a folder in the current directory named `env`:
|
||||
```bash
|
||||
python3 -m venv env
|
||||
export PATH=$PATH:~/.local/bin
|
||||
```
|
||||
|
||||
2. Activate the virtual environment:
|
||||
Then you can install MVT directly from [PyPI](https://pypi.org/project/mvt/)
|
||||
|
||||
```bash
|
||||
source env/bin/activate
|
||||
pip3 install mvt
|
||||
```
|
||||
|
||||
3. Install `mvt` into the virtual environment:
|
||||
```bash
|
||||
pip install mvt
|
||||
```
|
||||
|
||||
The `mvt-ios` and `mvt-android` utilities should now be available as commands whenever the virtual environment is active.
|
||||
|
||||
### Installing from git source with pipx
|
||||
If you want to have the latest features in development, you can install MVT directly from the source code in git.
|
||||
If you want to have the latest features in development, you can install MVT directly from the source code. If you installed MVT previously from pypi, you should first uninstall it using `pip3 uninstall mvt` and then install from the source code:
|
||||
|
||||
```bash
|
||||
pipx install --force git+https://github.com/mvt-project/mvt.git
|
||||
git clone https://github.com/mvt-project/mvt.git
|
||||
cd mvt
|
||||
pip3 install .
|
||||
```
|
||||
|
||||
You now should have the `mvt-ios` and `mvt-android` utilities installed.
|
||||
|
||||
**Notes:**
|
||||
1. The `--force` flag is necessary to force the reinstallation of the package.
|
||||
2. To revert to using a PyPI version, it will be necessary to `pipx uninstall mvt` first.
|
||||
|
||||
## Setting up command completions
|
||||
|
||||
See ["Command completions"](command_completion.md)
|
||||
|
||||
@@ -21,7 +21,7 @@ MVT supports using [indicators of compromise (IOCs)](https://github.com/mvt-proj
|
||||
|
||||
Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
|
||||
|
||||
Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/get-help/?c=mvt_docs) or [Access Now’s Digital Security Helpline](https://www.accessnow.org/help/).
|
||||
Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/contact-us/) or [Access Now’s Digital Security Helpline](https://www.accessnow.org/help/).
|
||||
|
||||
More information about using indicators of compromise with MVT is available in the [documentation](iocs.md).
|
||||
|
||||
|
||||
10
docs/iocs.md
10
docs/iocs.md
@@ -34,13 +34,6 @@ It is also possible to load STIX2 files automatically from the environment varia
|
||||
export MVT_STIX2="/home/user/IOC1.stix2:/home/user/IOC2.stix2"
|
||||
```
|
||||
|
||||
## STIX2 Support
|
||||
|
||||
So far MVT implements only a subset of [STIX2 specifications](https://docs.oasis-open.org/cti/stix/v2.1/csprd01/stix-v2.1-csprd01.html):
|
||||
|
||||
* It only supports checks for one value (such as `[domain-name:value='DOMAIN']`) and not boolean expressions over multiple comparisons
|
||||
* It only supports the following types: `domain-name:value`, `process:name`, `email-addr:value`, `file:name`, `file:path`, `file:hashes.md5`, `file:hashes.sha1`, `file:hashes.sha256`, `app:id`, `configuration-profile:id`, `android-property:name`, `url:value` (but each type will only be checked by a module if it is relevant to the type of data obtained)
|
||||
|
||||
## Known repositories of STIX2 IOCs
|
||||
|
||||
- The [Amnesty International investigations repository](https://github.com/AmnestyTech/investigations) contains STIX-formatted IOCs for:
|
||||
@@ -53,6 +46,3 @@ So far MVT implements only a subset of [STIX2 specifications](https://docs.oasis
|
||||
You can automaticallly download the latest public indicator files with the command `mvt-ios download-iocs` or `mvt-android download-iocs`. These commands download the list of indicators from the [mvt-indicators](https://github.com/mvt-project/mvt-indicators/blob/main/indicators.yaml) repository and store them in the [appdir](https://pypi.org/project/appdirs/) folder. They are then loaded automatically by MVT.
|
||||
|
||||
Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ Once the idevice tools are available you can check if everything works fine by c
|
||||
ideviceinfo
|
||||
```
|
||||
|
||||
This should show many details on the connected iOS device. If you are connecting the device to your laptop for the first time, it will require to unlock and enter the PIN code on the mobile device. If it complains that no device is connected and the mobile device is indeed plugged in through the USB cable, you might need to do this first, although typically the pairing is automatically done when connecting the device:
|
||||
This should some many details on the connected iOS device. If you are connecting the device to your laptop for the first time, it will require to unlock and enter the PIN code on the mobile device. If it complains that no device is connected and the mobile device is indeed plugged in through the USB cable, you might need to do this first, although typically the pairing is automatically done when connecting the device:
|
||||
|
||||
```bash
|
||||
sudo usbmuxd -f -v
|
||||
sudo usbmuxd -f -d
|
||||
idevicepair pair
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mkdocs==1.6.1
|
||||
mkdocs-autorefs==1.4.3
|
||||
mkdocs-material==9.6.20
|
||||
mkdocs-material-extensions==1.3.1
|
||||
mkdocstrings==0.30.1
|
||||
mkdocs==1.2.3
|
||||
mkdocs-autorefs
|
||||
mkdocs-material
|
||||
mkdocs-material-extensions
|
||||
mkdocstrings
|
||||
|
||||
@@ -7,8 +7,8 @@ markdown_extensions:
|
||||
- attr_list
|
||||
- admonition
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||
- pymdownx.superfences
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.highlight:
|
||||
|
||||
36
mvt/android/artifacts/artifact.py
Normal file
36
mvt/android/artifacts/artifact.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 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 mvt.common.artifact import Artifact
|
||||
|
||||
|
||||
class AndroidArtifact(Artifact):
|
||||
@staticmethod
|
||||
def extract_dumpsys_section(dumpsys: str, separator: str) -> str:
|
||||
"""
|
||||
Extract a section from a full dumpsys file.
|
||||
|
||||
:param dumpsys: content of the full dumpsys file (string)
|
||||
:param separator: content of the first line separator (string)
|
||||
:return: section extracted (string)
|
||||
"""
|
||||
lines = []
|
||||
in_section = False
|
||||
for line in dumpsys.splitlines():
|
||||
if line.strip() == separator:
|
||||
in_section = True
|
||||
continue
|
||||
|
||||
if not in_section:
|
||||
continue
|
||||
|
||||
if line.strip().startswith(
|
||||
"------------------------------------------------------------------------------"
|
||||
):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
return "\n".join(lines)
|
||||
@@ -3,8 +3,6 @@
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import re
|
||||
|
||||
from .artifact import AndroidArtifact
|
||||
|
||||
|
||||
@@ -27,8 +25,6 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
|
||||
|
||||
:param content: content of the accessibility section (string)
|
||||
"""
|
||||
|
||||
# "Old" syntax
|
||||
in_services = False
|
||||
for line in content.splitlines():
|
||||
if line.strip().startswith("installed services:"):
|
||||
@@ -39,7 +35,6 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
|
||||
continue
|
||||
|
||||
if line.strip() == "}":
|
||||
# At end of installed services
|
||||
break
|
||||
|
||||
service = line.split(":")[1].strip()
|
||||
@@ -50,19 +45,3 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
|
||||
"service": service,
|
||||
}
|
||||
)
|
||||
|
||||
# "New" syntax - AOSP >= 14 (?)
|
||||
# Looks like:
|
||||
# Enabled services:{{com.azure.authenticator/com.microsoft.brooklyn.module.accessibility.BrooklynAccessibilityService}, {com.agilebits.onepassword/com.agilebits.onepassword.filling.accessibility.FillingAccessibilityService}}
|
||||
|
||||
for line in content.splitlines():
|
||||
if line.strip().startswith("Enabled services:"):
|
||||
matches = re.finditer(r"{([^{]+?)}", line)
|
||||
|
||||
for match in matches:
|
||||
# Each match is in format: <package_name>/<service>
|
||||
package_name, _, service = match.group(1).partition("/")
|
||||
|
||||
self.results.append(
|
||||
{"package_name": package_name, "service": service}
|
||||
)
|
||||
@@ -11,10 +11,6 @@ from mvt.common.utils import convert_datetime_to_iso
|
||||
from .artifact import AndroidArtifact
|
||||
|
||||
|
||||
RISKY_PERMISSIONS = ["REQUEST_INSTALL_PACKAGES"]
|
||||
RISKY_PACKAGES = ["com.android.shell"]
|
||||
|
||||
|
||||
class DumpsysAppopsArtifact(AndroidArtifact):
|
||||
"""
|
||||
Parser for dumpsys app ops info
|
||||
@@ -49,39 +45,15 @@ class DumpsysAppopsArtifact(AndroidArtifact):
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
detected_permissions = []
|
||||
for perm in result["permissions"]:
|
||||
if (
|
||||
perm["name"] in RISKY_PERMISSIONS
|
||||
# and perm["access"] == "allow"
|
||||
perm["name"] == "REQUEST_INSTALL_PACKAGES"
|
||||
and perm["access"] == "allow"
|
||||
):
|
||||
detected_permissions.append(perm)
|
||||
for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]):
|
||||
self.log.warning(
|
||||
"Package '%s' had risky permission '%s' set to '%s' at %s",
|
||||
result["package_name"],
|
||||
perm["name"],
|
||||
entry["access"],
|
||||
entry["timestamp"],
|
||||
)
|
||||
|
||||
elif result["package_name"] in RISKY_PACKAGES:
|
||||
detected_permissions.append(perm)
|
||||
for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]):
|
||||
self.log.warning(
|
||||
"Risky package '%s' had '%s' permission set to '%s' at %s",
|
||||
result["package_name"],
|
||||
perm["name"],
|
||||
entry["access"],
|
||||
entry["timestamp"],
|
||||
)
|
||||
|
||||
if detected_permissions:
|
||||
# We clean the result to only include the risky permission, otherwise the timeline
|
||||
# will be polluted with all the other irrelevant permissions
|
||||
cleaned_result = result.copy()
|
||||
cleaned_result["permissions"] = detected_permissions
|
||||
self.detected.append(cleaned_result)
|
||||
self.log.info(
|
||||
"Package %s with REQUEST_INSTALL_PACKAGES " "permission",
|
||||
result["package_name"],
|
||||
)
|
||||
|
||||
def parse(self, output: str) -> None:
|
||||
self.results: List[Dict[str, Any]] = []
|
||||
@@ -149,16 +121,11 @@ class DumpsysAppopsArtifact(AndroidArtifact):
|
||||
if line.startswith(" "):
|
||||
# Permission entry like:
|
||||
# Reject: [fg-s]2021-05-19 22:02:52.054 (-314d1h25m2s33ms)
|
||||
access_type = line.split(":")[0].strip()
|
||||
if access_type not in ["Access", "Reject"]:
|
||||
# Skipping invalid access type. Some entries are not in the format we expect
|
||||
continue
|
||||
|
||||
if entry:
|
||||
perm["entries"].append(entry)
|
||||
entry = {}
|
||||
|
||||
entry["access"] = access_type
|
||||
entry["access"] = line.split(":")[0].strip()
|
||||
entry["type"] = line[line.find("[") + 1 : line.find("]")]
|
||||
|
||||
try:
|
||||
@@ -42,17 +42,6 @@ class GetProp(AndroidArtifact):
|
||||
entry = {"name": matches[0][0], "value": matches[0][1]}
|
||||
self.results.append(entry)
|
||||
|
||||
def get_device_timezone(self) -> str:
|
||||
"""
|
||||
Get the device timezone from the getprop results
|
||||
|
||||
Used in other moduels to calculate the timezone offset
|
||||
"""
|
||||
for entry in self.results:
|
||||
if entry["name"] == "persist.sys.timezone":
|
||||
return entry["value"]
|
||||
return None
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for entry in self.results:
|
||||
if entry["name"] in INTERESTING_PROPERTIES:
|
||||
@@ -16,11 +16,6 @@ ANDROID_DANGEROUS_SETTINGS = [
|
||||
"key": "package_verifier_enable",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled APK package verification",
|
||||
"key": "package_verifier_state",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "disabled Google Play Protect",
|
||||
"key": "package_verifier_user_consent",
|
||||
@@ -52,8 +47,8 @@ ANDROID_DANGEROUS_SETTINGS = [
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "enabled accessibility services",
|
||||
"key": "accessibility_enabled",
|
||||
"description": "enabled installation of non Google Play apps",
|
||||
"key": "install_non_market_apps",
|
||||
"safe_value": "0",
|
||||
},
|
||||
]
|
||||
@@ -10,17 +10,6 @@ import click
|
||||
from mvt.common.cmd_check_iocs import CmdCheckIOCS
|
||||
from mvt.common.help import (
|
||||
HELP_MSG_ANDROID_BACKUP_PASSWORD,
|
||||
HELP_MSG_APK_OUTPUT,
|
||||
HELP_MSG_APKS_FROM_FILE,
|
||||
HELP_MSG_CHECK_ADB,
|
||||
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_DOWNLOAD_ALL_APKS,
|
||||
HELP_MSG_DOWNLOAD_APKS,
|
||||
HELP_MSG_FAST,
|
||||
HELP_MSG_HASHES,
|
||||
HELP_MSG_IOC,
|
||||
@@ -29,10 +18,7 @@ from mvt.common.help import (
|
||||
HELP_MSG_NONINTERACTIVE,
|
||||
HELP_MSG_OUTPUT,
|
||||
HELP_MSG_SERIAL,
|
||||
HELP_MSG_STIX2,
|
||||
HELP_MSG_VERBOSE,
|
||||
HELP_MSG_VERSION,
|
||||
HELP_MSG_VIRUS_TOTAL,
|
||||
)
|
||||
from mvt.common.logo import logo
|
||||
from mvt.common.updates import IndicatorsUpdates
|
||||
@@ -55,43 +41,18 @@ 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()
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Command: version
|
||||
# ==============================================================================
|
||||
@cli.command("version", help=HELP_MSG_VERSION)
|
||||
@cli.command("version", help="Show the currently installed version of MVT")
|
||||
def version():
|
||||
return
|
||||
|
||||
@@ -100,14 +61,30 @@ def version():
|
||||
# Command: download-apks
|
||||
# ==============================================================================
|
||||
@cli.command(
|
||||
"download-apks", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_DOWNLOAD_APKS
|
||||
"download-apks",
|
||||
help="Download all or only non-system installed APKs",
|
||||
context_settings=CONTEXT_SETTINGS,
|
||||
)
|
||||
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
|
||||
@click.option("--all-apks", "-a", is_flag=True, help=HELP_MSG_DOWNLOAD_ALL_APKS)
|
||||
@click.option("--virustotal", "-V", is_flag=True, help=HELP_MSG_VIRUS_TOTAL)
|
||||
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_APK_OUTPUT)
|
||||
@click.option(
|
||||
"--from-file", "-f", type=click.Path(exists=True), help=HELP_MSG_APKS_FROM_FILE
|
||||
"--all-apks",
|
||||
"-a",
|
||||
is_flag=True,
|
||||
help="Extract all packages installed on the phone, including system packages",
|
||||
)
|
||||
@click.option("--virustotal", "-v", is_flag=True, help="Check packages on VirusTotal")
|
||||
@click.option(
|
||||
"--output",
|
||||
"-o",
|
||||
type=click.Path(exists=False),
|
||||
help="Specify a path to a folder where you want to store the APKs",
|
||||
)
|
||||
@click.option(
|
||||
"--from-file",
|
||||
"-f",
|
||||
type=click.Path(exists=True),
|
||||
help="Instead of acquiring from phone, load an existing packages.json file for "
|
||||
"lookups (mainly for debug purposes)",
|
||||
)
|
||||
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
|
||||
@click.pass_context
|
||||
@@ -150,7 +127,11 @@ def download_apks(ctx, all_apks, virustotal, output, from_file, serial, verbose)
|
||||
# ==============================================================================
|
||||
# Command: check-adb
|
||||
# ==============================================================================
|
||||
@cli.command("check-adb", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_CHECK_ADB)
|
||||
@cli.command(
|
||||
"check-adb",
|
||||
help="Check an Android device over ADB",
|
||||
context_settings=CONTEXT_SETTINGS,
|
||||
)
|
||||
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
|
||||
@click.option(
|
||||
"--iocs",
|
||||
@@ -193,19 +174,12 @@ def check_adb(
|
||||
module_name=module,
|
||||
serial=serial,
|
||||
module_options=module_options,
|
||||
disable_version_check=_get_disable_flags(ctx)[0],
|
||||
disable_indicator_check=_get_disable_flags(ctx)[1],
|
||||
)
|
||||
|
||||
if list_modules:
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
log.warning(
|
||||
"DEPRECATION: The 'check-adb' command is deprecated and may be removed in a future release. "
|
||||
"Prefer acquiring device data using the AndroidQF project (https://github.com/mvt-project/androidqf/) and analyzing that acquisition with MVT."
|
||||
)
|
||||
|
||||
log.info("Checking Android device over debug bridge")
|
||||
|
||||
cmd.run()
|
||||
@@ -221,7 +195,9 @@ def check_adb(
|
||||
# Command: check-bugreport
|
||||
# ==============================================================================
|
||||
@cli.command(
|
||||
"check-bugreport", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_CHECK_BUGREPORT
|
||||
"check-bugreport",
|
||||
help="Check an Android Bug Report",
|
||||
context_settings=CONTEXT_SETTINGS,
|
||||
)
|
||||
@click.option(
|
||||
"--iocs",
|
||||
@@ -246,8 +222,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:
|
||||
@@ -269,9 +243,7 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
|
||||
# Command: check-backup
|
||||
# ==============================================================================
|
||||
@cli.command(
|
||||
"check-backup",
|
||||
context_settings=CONTEXT_SETTINGS,
|
||||
help=HELP_MSG_CHECK_ANDROID_BACKUP,
|
||||
"check-backup", help="Check an Android Backup", context_settings=CONTEXT_SETTINGS
|
||||
)
|
||||
@click.option(
|
||||
"--iocs",
|
||||
@@ -310,8 +282,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:
|
||||
@@ -333,7 +303,9 @@ def check_backup(
|
||||
# Command: check-androidqf
|
||||
# ==============================================================================
|
||||
@cli.command(
|
||||
"check-androidqf", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_CHECK_ANDROIDQF
|
||||
"check-androidqf",
|
||||
help="Check data collected with AndroidQF",
|
||||
context_settings=CONTEXT_SETTINGS,
|
||||
)
|
||||
@click.option(
|
||||
"--iocs",
|
||||
@@ -376,8 +348,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:
|
||||
@@ -398,7 +368,11 @@ def check_androidqf(
|
||||
# ==============================================================================
|
||||
# Command: check-iocs
|
||||
# ==============================================================================
|
||||
@cli.command("check-iocs", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_CHECK_IOCS)
|
||||
@cli.command(
|
||||
"check-iocs",
|
||||
help="Compare stored JSON results to provided indicators",
|
||||
context_settings=CONTEXT_SETTINGS,
|
||||
)
|
||||
@click.option(
|
||||
"--iocs",
|
||||
"-i",
|
||||
@@ -412,13 +386,7 @@ def check_androidqf(
|
||||
@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 + ADB_MODULES + BUGREPORT_MODULES
|
||||
|
||||
if list_modules:
|
||||
@@ -431,7 +399,11 @@ def check_iocs(ctx, iocs, list_modules, module, folder):
|
||||
# ==============================================================================
|
||||
# Command: download-iocs
|
||||
# ==============================================================================
|
||||
@cli.command("download-iocs", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_STIX2)
|
||||
@cli.command(
|
||||
"download-iocs",
|
||||
help="Download public STIX2 indicators",
|
||||
context_settings=CONTEXT_SETTINGS,
|
||||
)
|
||||
def download_indicators():
|
||||
ioc_updates = IndicatorsUpdates()
|
||||
ioc_updates.update()
|
||||
@@ -7,7 +7,6 @@ import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.command import Command
|
||||
from mvt.common.indicators import Indicators
|
||||
|
||||
from .modules.adb import ADB_MODULES
|
||||
|
||||
@@ -20,28 +19,18 @@ class CmdAndroidCheckADB(Command):
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
iocs: Optional[Indicators] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
hashes: Optional[bool] = False,
|
||||
sub_command: Optional[bool] = False,
|
||||
disable_version_check: bool = False,
|
||||
disable_indicator_check: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
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-adb"
|
||||
67
mvt/android/cmd_check_androidqf.py
Normal file
67
mvt/android/cmd_check_androidqf.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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 zipfile
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from mvt.common.command import Command
|
||||
|
||||
from .modules.androidqf import ANDROIDQF_MODULES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdAndroidCheckAndroidQF(Command):
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
hashes: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
ioc_files=ioc_files,
|
||||
module_name=module_name,
|
||||
serial=serial,
|
||||
module_options=module_options,
|
||||
hashes=hashes,
|
||||
log=log,
|
||||
)
|
||||
|
||||
self.name = "check-androidqf"
|
||||
self.modules = ANDROIDQF_MODULES
|
||||
|
||||
self.format: Optional[str] = None
|
||||
self.archive: Optional[zipfile.ZipFile] = None
|
||||
self.files: List[str] = []
|
||||
|
||||
def init(self):
|
||||
if os.path.isdir(self.target_path):
|
||||
self.format = "dir"
|
||||
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
||||
target_abs_path = os.path.abspath(self.target_path)
|
||||
for root, subdirs, subfiles in os.walk(target_abs_path):
|
||||
for fname in subfiles:
|
||||
file_path = os.path.relpath(os.path.join(root, fname), parent_path)
|
||||
self.files.append(file_path)
|
||||
elif os.path.isfile(self.target_path):
|
||||
self.format = "zip"
|
||||
self.archive = zipfile.ZipFile(self.target_path)
|
||||
self.files = self.archive.namelist()
|
||||
|
||||
def module_init(self, module):
|
||||
if self.format == "zip":
|
||||
module.from_zip_file(self.archive, self.files)
|
||||
else:
|
||||
parent_path = Path(self.target_path).absolute().parent.as_posix()
|
||||
module.from_folder(parent_path, self.files)
|
||||
@@ -20,7 +20,6 @@ from mvt.android.parsers.backup import (
|
||||
parse_backup_file,
|
||||
)
|
||||
from mvt.common.command import Command
|
||||
from mvt.common.indicators import Indicators
|
||||
|
||||
from .modules.backup import BACKUP_MODULES
|
||||
|
||||
@@ -33,28 +32,20 @@ class CmdAndroidCheckBackup(Command):
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
iocs: Optional[Indicators] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
hashes: Optional[bool] = False,
|
||||
sub_command: Optional[bool] = False,
|
||||
disable_version_check: bool = False,
|
||||
disable_indicator_check: bool = False,
|
||||
hashes: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
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-backup"
|
||||
@@ -64,34 +55,6 @@ class CmdAndroidCheckBackup(Command):
|
||||
self.backup_archive: Optional[tarfile.TarFile] = None
|
||||
self.backup_files: List[str] = []
|
||||
|
||||
def from_ab(self, ab_file_bytes: bytes) -> None:
|
||||
self.backup_type = "ab"
|
||||
header = parse_ab_header(ab_file_bytes)
|
||||
if not header["backup"]:
|
||||
log.critical("Invalid backup format, file should be in .ab format")
|
||||
sys.exit(1)
|
||||
|
||||
password = None
|
||||
if header["encryption"] != "none":
|
||||
password = prompt_or_load_android_backup_password(log, self.module_options)
|
||||
if not password:
|
||||
log.critical("No backup password provided.")
|
||||
sys.exit(1)
|
||||
try:
|
||||
tardata = parse_backup_file(ab_file_bytes, password=password)
|
||||
except InvalidBackupPassword:
|
||||
log.critical("Invalid backup password")
|
||||
sys.exit(1)
|
||||
except AndroidBackupParsingError as exc:
|
||||
log.critical("Impossible to parse this backup file: %s", exc)
|
||||
log.critical("Please use Android Backup Extractor (ABE) instead")
|
||||
sys.exit(1)
|
||||
|
||||
dbytes = io.BytesIO(tardata)
|
||||
self.backup_archive = tarfile.open(fileobj=dbytes)
|
||||
for member in self.backup_archive:
|
||||
self.backup_files.append(member.name)
|
||||
|
||||
def init(self) -> None:
|
||||
if not self.target_path:
|
||||
return
|
||||
@@ -99,8 +62,35 @@ class CmdAndroidCheckBackup(Command):
|
||||
if os.path.isfile(self.target_path):
|
||||
self.backup_type = "ab"
|
||||
with open(self.target_path, "rb") as handle:
|
||||
ab_file_bytes = handle.read()
|
||||
self.from_ab(ab_file_bytes)
|
||||
data = handle.read()
|
||||
|
||||
header = parse_ab_header(data)
|
||||
if not header["backup"]:
|
||||
log.critical("Invalid backup format, file should be in .ab format")
|
||||
sys.exit(1)
|
||||
|
||||
password = None
|
||||
if header["encryption"] != "none":
|
||||
password = prompt_or_load_android_backup_password(
|
||||
log, self.module_options
|
||||
)
|
||||
if not password:
|
||||
log.critical("No backup password provided.")
|
||||
sys.exit(1)
|
||||
try:
|
||||
tardata = parse_backup_file(data, password=password)
|
||||
except InvalidBackupPassword:
|
||||
log.critical("Invalid backup password")
|
||||
sys.exit(1)
|
||||
except AndroidBackupParsingError as exc:
|
||||
log.critical("Impossible to parse this backup file: %s", exc)
|
||||
log.critical("Please use Android Backup Extractor (ABE) instead")
|
||||
sys.exit(1)
|
||||
|
||||
dbytes = io.BytesIO(tardata)
|
||||
self.backup_archive = tarfile.open(fileobj=dbytes)
|
||||
for member in self.backup_archive:
|
||||
self.backup_files.append(member.name)
|
||||
|
||||
elif os.path.isdir(self.target_path):
|
||||
self.backup_type = "folder"
|
||||
@@ -119,6 +109,6 @@ class CmdAndroidCheckBackup(Command):
|
||||
|
||||
def module_init(self, module: BackupExtraction) -> None: # type: ignore[override]
|
||||
if self.backup_type == "folder":
|
||||
module.from_dir(self.target_path, self.backup_files)
|
||||
module.from_folder(self.target_path, self.backup_files)
|
||||
else:
|
||||
module.from_ab(self.target_path, self.backup_archive, self.backup_files)
|
||||
76
mvt/android/cmd_check_bugreport.py
Normal file
76
mvt/android/cmd_check_bugreport.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# 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 pathlib import Path
|
||||
from typing import List, Optional
|
||||
from zipfile import ZipFile
|
||||
|
||||
from mvt.android.modules.bugreport.base import BugReportModule
|
||||
from mvt.common.command import Command
|
||||
|
||||
from .modules.bugreport import BUGREPORT_MODULES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdAndroidCheckBugreport(Command):
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
hashes: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
ioc_files=ioc_files,
|
||||
module_name=module_name,
|
||||
serial=serial,
|
||||
module_options=module_options,
|
||||
hashes=hashes,
|
||||
log=log,
|
||||
)
|
||||
|
||||
self.name = "check-bugreport"
|
||||
self.modules = BUGREPORT_MODULES
|
||||
|
||||
self.bugreport_format: str = ""
|
||||
self.bugreport_archive: Optional[ZipFile] = None
|
||||
self.bugreport_files: List[str] = []
|
||||
|
||||
def init(self) -> None:
|
||||
if not self.target_path:
|
||||
return
|
||||
|
||||
if os.path.isfile(self.target_path):
|
||||
self.bugreport_format = "zip"
|
||||
self.bugreport_archive = ZipFile(self.target_path)
|
||||
for file_name in self.bugreport_archive.namelist():
|
||||
self.bugreport_files.append(file_name)
|
||||
elif os.path.isdir(self.target_path):
|
||||
self.bugreport_format = "dir"
|
||||
parent_path = Path(self.target_path).absolute().as_posix()
|
||||
for root, _, subfiles in os.walk(os.path.abspath(self.target_path)):
|
||||
for file_name in subfiles:
|
||||
file_path = os.path.relpath(
|
||||
os.path.join(root, file_name), parent_path
|
||||
)
|
||||
self.bugreport_files.append(file_path)
|
||||
|
||||
def module_init(self, module: BugReportModule) -> None: # type: ignore[override]
|
||||
if self.bugreport_format == "zip":
|
||||
module.from_zip(self.bugreport_archive, self.bugreport_files)
|
||||
else:
|
||||
module.from_folder(self.target_path, self.bugreport_files)
|
||||
|
||||
def finish(self) -> None:
|
||||
if self.bugreport_archive:
|
||||
self.bugreport_archive.close()
|
||||
@@ -6,7 +6,7 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Callable, Optional, Union
|
||||
from typing import Callable, Optional
|
||||
|
||||
from rich.progress import track
|
||||
|
||||
@@ -52,9 +52,7 @@ class DownloadAPKs(AndroidExtraction):
|
||||
packages = json.load(handle)
|
||||
return cls(packages=packages)
|
||||
|
||||
def pull_package_file(
|
||||
self, package_name: str, remote_path: str
|
||||
) -> Union[str, None]:
|
||||
def pull_package_file(self, package_name: str, remote_path: str) -> None:
|
||||
"""Pull files related to specific package from the device.
|
||||
|
||||
:param package_name: Name of the package to download
|
||||
@@ -4,7 +4,14 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
from .chrome_history import ChromeHistory
|
||||
from .dumpsys_accessibility import DumpsysAccessibility
|
||||
from .dumpsys_activities import DumpsysActivities
|
||||
from .dumpsys_appops import DumpsysAppOps
|
||||
from .dumpsys_battery_daily import DumpsysBatteryDaily
|
||||
from .dumpsys_battery_history import DumpsysBatteryHistory
|
||||
from .dumpsys_dbinfo import DumpsysDBInfo
|
||||
from .dumpsys_full import DumpsysFull
|
||||
from .dumpsys_receivers import DumpsysReceivers
|
||||
from .files import Files
|
||||
from .getprop import Getprop
|
||||
from .logcat import Logcat
|
||||
@@ -24,7 +31,14 @@ ADB_MODULES = [
|
||||
Getprop,
|
||||
Settings,
|
||||
SELinuxStatus,
|
||||
DumpsysBatteryHistory,
|
||||
DumpsysBatteryDaily,
|
||||
DumpsysReceivers,
|
||||
DumpsysActivities,
|
||||
DumpsysAccessibility,
|
||||
DumpsysDBInfo,
|
||||
DumpsysFull,
|
||||
DumpsysAppOps,
|
||||
Packages,
|
||||
Logcat,
|
||||
RootBinaries,
|
||||
@@ -147,14 +147,14 @@ class AndroidExtraction(MVTModule):
|
||||
self._adb_disconnect()
|
||||
self._adb_connect()
|
||||
|
||||
def _adb_command(self, command: str, decode: bool = True) -> str:
|
||||
def _adb_command(self, command: str) -> str:
|
||||
"""Execute an adb shell command.
|
||||
|
||||
:param command: Shell command to execute
|
||||
:returns: Output of command
|
||||
|
||||
"""
|
||||
return self.device.shell(command, read_timeout_s=200.0, decode=decode)
|
||||
return self.device.shell(command, read_timeout_s=200.0)
|
||||
|
||||
def _adb_check_if_root(self) -> bool:
|
||||
"""Check if we have a `su` binary on the Android device.
|
||||
@@ -326,7 +326,8 @@ class AndroidExtraction(MVTModule):
|
||||
|
||||
if not header["backup"]:
|
||||
self.log.error(
|
||||
"Extracting SMS via Android backup failed. No valid backup data found."
|
||||
"Extracting SMS via Android backup failed. "
|
||||
"No valid backup data found."
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -51,9 +51,8 @@ class ChromeHistory(AndroidExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if self.indicators.check_url(result["url"]):
|
||||
if self.indicators.check_domain(result["url"]):
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def _parse_db(self, db_path: str) -> None:
|
||||
"""Parse a Chrome History database file.
|
||||
49
mvt/android/modules/adb/dumpsys_accessibility.py
Normal file
49
mvt/android/modules/adb/dumpsys_accessibility.py
Normal file
@@ -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 mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
|
||||
class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidExtraction):
|
||||
"""This module extracts stats on accessibility."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys accessibility")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.parse(output)
|
||||
|
||||
for result in self.results:
|
||||
self.log.info(
|
||||
'Found installed accessibility service "%s"', result.get("service")
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"Identified a total of %d accessibility services", len(self.results)
|
||||
)
|
||||
45
mvt/android/modules/adb/dumpsys_activities.py
Normal file
45
mvt/android/modules/adb/dumpsys_activities.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_package_activities import (
|
||||
DumpsysPackageActivitiesArtifact,
|
||||
)
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
|
||||
class DumpsysActivities(DumpsysPackageActivitiesArtifact, AndroidExtraction):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
self.results = results if results else []
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys package")
|
||||
self._adb_disconnect()
|
||||
self.parse(output)
|
||||
|
||||
self.log.info("Extracted %d package activities", len(self.results))
|
||||
46
mvt/android/modules/adb/dumpsys_appops.py
Normal file
46
mvt/android/modules/adb/dumpsys_appops.py
Normal file
@@ -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
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
|
||||
class DumpsysAppOps(DumpsysAppopsArtifact, AndroidExtraction):
|
||||
"""This module extracts records from App-op Manager."""
|
||||
|
||||
slug = "dumpsys_appops"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys appops")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.parse(output)
|
||||
|
||||
self.log.info(
|
||||
"Extracted a total of %d records from app-ops manager", len(self.results)
|
||||
)
|
||||
44
mvt/android/modules/adb/dumpsys_battery_daily.py
Normal file
44
mvt/android/modules/adb/dumpsys_battery_daily.py
Normal 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.dumpsys_battery_daily import DumpsysBatteryDailyArtifact
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
|
||||
class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, AndroidExtraction):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys batterystats --daily")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.parse(output)
|
||||
|
||||
self.log.info(
|
||||
"Extracted %d records from battery daily stats", len(self.results)
|
||||
)
|
||||
42
mvt/android/modules/adb/dumpsys_battery_history.py
Normal file
42
mvt/android/modules/adb/dumpsys_battery_history.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
|
||||
class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, AndroidExtraction):
|
||||
"""This module extracts records from battery history events."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys batterystats --history")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.parse(output)
|
||||
|
||||
self.log.info("Extracted %d records from battery history", len(self.results))
|
||||
47
mvt/android/modules/adb/dumpsys_dbinfo.py
Normal file
47
mvt/android/modules/adb/dumpsys_dbinfo.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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_dbinfo import DumpsysDBInfoArtifact
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
|
||||
class DumpsysDBInfo(DumpsysDBInfoArtifact, AndroidExtraction):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
slug = "dumpsys_dbinfo"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys dbinfo")
|
||||
self._adb_disconnect()
|
||||
|
||||
self.parse(output)
|
||||
|
||||
self.log.info(
|
||||
"Extracted a total of %d records from database information",
|
||||
len(self.results),
|
||||
)
|
||||
44
mvt/android/modules/adb/dumpsys_receivers.py
Normal file
44
mvt/android/modules/adb/dumpsys_receivers.py
Normal 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.dumpsys_receivers import DumpsysReceiversArtifact
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
|
||||
class DumpsysReceivers(DumpsysReceiversArtifact, AndroidExtraction):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
self.results = results if results else {}
|
||||
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("dumpsys package")
|
||||
self.parse(output)
|
||||
|
||||
self._adb_disconnect()
|
||||
self.log.info("Extracted receivers for %d intents", len(self.results))
|
||||
@@ -4,25 +4,86 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from rich.console import Console
|
||||
from rich.progress import track
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
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.android.parsers.dumpsys import parse_dumpsys_package_for_details
|
||||
from mvt.common.virustotal import VTNoKey, VTQuotaExceeded, virustotal_lookup
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD = 10
|
||||
DANGEROUS_PERMISSIONS = [
|
||||
"android.permission.ACCESS_COARSE_LOCATION",
|
||||
"android.permission.ACCESS_FINE_LOCATION",
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.DISABLE_KEYGUARD",
|
||||
"android.permission.PROCESS_OUTGOING_CALLS",
|
||||
"android.permission.READ_CALENDAR",
|
||||
"android.permission.READ_CALL_LOG",
|
||||
"android.permission.READ_CONTACTS",
|
||||
"android.permission.READ_PHONE_STATE",
|
||||
"android.permission.READ_SMS",
|
||||
"android.permission.RECEIVE_MMS",
|
||||
"android.permission.RECEIVE_SMS",
|
||||
"android.permission.RECEIVE_WAP_PUSH",
|
||||
"android.permission.RECORD_AUDIO",
|
||||
"android.permission.SEND_SMS",
|
||||
"android.permission.SYSTEM_ALERT_WINDOW",
|
||||
"android.permission.USE_CREDENTIALS",
|
||||
"android.permission.USE_SIP",
|
||||
"com.android.browser.permission.READ_HISTORY_BOOKMARKS",
|
||||
]
|
||||
ROOT_PACKAGES: List[str] = [
|
||||
"com.noshufou.android.su",
|
||||
"com.noshufou.android.su.elite",
|
||||
"eu.chainfire.supersu",
|
||||
"com.koushikdutta.superuser",
|
||||
"com.thirdparty.superuser",
|
||||
"com.yellowes.su",
|
||||
"com.koushikdutta.rommanager",
|
||||
"com.koushikdutta.rommanager.license",
|
||||
"com.dimonvideo.luckypatcher",
|
||||
"com.chelpus.lackypatch",
|
||||
"com.ramdroid.appquarantine",
|
||||
"com.ramdroid.appquarantinepro",
|
||||
"com.devadvance.rootcloak",
|
||||
"com.devadvance.rootcloakplus",
|
||||
"de.robv.android.xposed.installer",
|
||||
"com.saurik.substrate",
|
||||
"com.zachspong.temprootremovejb",
|
||||
"com.amphoras.hidemyroot",
|
||||
"com.amphoras.hidemyrootadfree",
|
||||
"com.formyhm.hiderootPremium",
|
||||
"com.formyhm.hideroot",
|
||||
"me.phh.superuser",
|
||||
"eu.chainfire.supersu.pro",
|
||||
"com.kingouser.com",
|
||||
"com.topjohnwu.magisk",
|
||||
]
|
||||
SECURITY_PACKAGES = [
|
||||
"com.policydm",
|
||||
"com.samsung.android.app.omcagent",
|
||||
"com.samsung.android.securitylogagent",
|
||||
"com.sec.android.soagent",
|
||||
]
|
||||
SYSTEM_UPDATE_PACKAGES = [
|
||||
"com.android.updater",
|
||||
"com.google.android.gms",
|
||||
"com.huawei.android.hwouc",
|
||||
"com.lge.lgdmsclient",
|
||||
"com.motorola.ccc.ota",
|
||||
"com.oneplus.opbackup",
|
||||
"com.oppo.ota",
|
||||
"com.transsion.systemupdate",
|
||||
"com.wssyncmldm",
|
||||
]
|
||||
|
||||
|
||||
class Packages(AndroidExtraction):
|
||||
"""This module extracts the list of installed packages."""
|
||||
@@ -75,7 +136,8 @@ class Packages(AndroidExtraction):
|
||||
for result in self.results:
|
||||
if result["package_name"] in ROOT_PACKAGES:
|
||||
self.log.warning(
|
||||
'Found an installed package related to rooting/jailbreaking: "%s"',
|
||||
"Found an installed package related to "
|
||||
'rooting/jailbreaking: "%s"',
|
||||
result["package_name"],
|
||||
)
|
||||
self.detected.append(result)
|
||||
@@ -107,7 +169,8 @@ class Packages(AndroidExtraction):
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def check_virustotal(self, packages: list) -> None:
|
||||
@staticmethod
|
||||
def check_virustotal(packages: list) -> None:
|
||||
hashes = []
|
||||
for package in packages:
|
||||
for file in package.get("files", []):
|
||||
@@ -142,15 +205,8 @@ class Packages(AndroidExtraction):
|
||||
|
||||
for package in packages:
|
||||
for file in package.get("files", []):
|
||||
if "package_name" in package:
|
||||
row = [package["package_name"], file["path"]]
|
||||
elif "name" in package:
|
||||
row = [package["name"], file["path"]]
|
||||
else:
|
||||
self.log.error(
|
||||
f"Package {package} has no name or package_name. packages.json or apks.json is malformed"
|
||||
)
|
||||
continue
|
||||
row = [package["package_name"], file["path"]]
|
||||
|
||||
if file["sha256"] in detections:
|
||||
detection = detections[file["sha256"]]
|
||||
positives = detection.split("/")[0]
|
||||
@@ -178,9 +234,7 @@ class Packages(AndroidExtraction):
|
||||
if line.strip() == "Packages:":
|
||||
in_packages = True
|
||||
|
||||
return DumpsysPackagesArtifact.parse_dumpsys_package_for_details(
|
||||
"\n".join(lines)
|
||||
)
|
||||
return parse_dumpsys_package_for_details("\n".join(lines))
|
||||
|
||||
def _get_files_for_package(self, package_name: str) -> list:
|
||||
command = f"pm path {package_name}"
|
||||
@@ -70,7 +70,7 @@ class SMS(AndroidExtraction):
|
||||
"timestamp": record["isodate"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": f"sms_{record['direction']}",
|
||||
"data": f'{record.get("address", "unknown source")}: "{body}"',
|
||||
"data": f"{record.get('address', 'unknown source')}: \"{body}\"",
|
||||
}
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
@@ -85,9 +85,8 @@ class SMS(AndroidExtraction):
|
||||
if message_links == []:
|
||||
message_links = check_for_links(message["body"])
|
||||
|
||||
if self.indicators.check_urls(message_links):
|
||||
if self.indicators.check_domains(message_links):
|
||||
self.detected.append(message)
|
||||
continue
|
||||
|
||||
def _parse_db(self, db_path: str) -> None:
|
||||
"""Parse an Android bugle_db SMS database file.
|
||||
@@ -114,10 +113,8 @@ class SMS(AndroidExtraction):
|
||||
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
|
||||
links = check_for_links(message["body"])
|
||||
message["links"] = links
|
||||
|
||||
self.results.append(message)
|
||||
|
||||
@@ -55,9 +55,8 @@ class Whatsapp(AndroidExtraction):
|
||||
continue
|
||||
|
||||
message_links = check_for_links(message["data"])
|
||||
if self.indicators.check_urls(message_links):
|
||||
if self.indicators.check_domains(message_links):
|
||||
self.detected.append(message)
|
||||
continue
|
||||
|
||||
def _parse_db(self, db_path: str) -> None:
|
||||
"""Parse an Android msgstore.db WhatsApp database file.
|
||||
@@ -9,26 +9,24 @@ from .dumpsys_appops import DumpsysAppops
|
||||
from .dumpsys_battery_daily import DumpsysBatteryDaily
|
||||
from .dumpsys_battery_history import DumpsysBatteryHistory
|
||||
from .dumpsys_dbinfo import DumpsysDBInfo
|
||||
from .dumpsys_getprop import DumpsysGetProp
|
||||
from .dumpsys_packages import DumpsysPackages
|
||||
from .dumpsys_platform_compat import DumpsysPlatformCompat
|
||||
from .dumpsys_receivers import DumpsysReceivers
|
||||
from .dumpsys_adb_state import DumpsysADBState
|
||||
from .fs_timestamps import BugReportTimestamps
|
||||
from .tombstones import Tombstones
|
||||
from .getprop import Getprop
|
||||
from .processes import Processes
|
||||
from .settings import Settings
|
||||
from .sms import SMS
|
||||
|
||||
BUGREPORT_MODULES = [
|
||||
DumpsysAccessibility,
|
||||
ANDROIDQF_MODULES = [
|
||||
DumpsysActivities,
|
||||
DumpsysReceivers,
|
||||
DumpsysAccessibility,
|
||||
DumpsysAppops,
|
||||
DumpsysDBInfo,
|
||||
DumpsysBatteryDaily,
|
||||
DumpsysBatteryHistory,
|
||||
DumpsysDBInfo,
|
||||
DumpsysGetProp,
|
||||
Processes,
|
||||
Getprop,
|
||||
Settings,
|
||||
SMS,
|
||||
DumpsysPackages,
|
||||
DumpsysPlatformCompat,
|
||||
DumpsysReceivers,
|
||||
DumpsysADBState,
|
||||
BugReportTimestamps,
|
||||
Tombstones,
|
||||
]
|
||||
@@ -32,53 +32,21 @@ class AndroidQFModule(MVTModule):
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
self.parent_path = None
|
||||
self._path: str = target_path
|
||||
self.files: List[str] = []
|
||||
self.archive: Optional[zipfile.ZipFile] = None
|
||||
|
||||
def from_dir(self, parent_path: str, files: List[str]) -> None:
|
||||
def from_folder(self, parent_path: str, files: List[str]):
|
||||
self.parent_path = parent_path
|
||||
self.files = files
|
||||
|
||||
def from_zip(self, archive: zipfile.ZipFile, files: List[str]) -> None:
|
||||
def from_zip_file(self, archive: zipfile.ZipFile, files: List[str]):
|
||||
self.archive = archive
|
||||
self.files = files
|
||||
|
||||
def _get_files_by_pattern(self, pattern: str):
|
||||
return fnmatch.filter(self.files, pattern)
|
||||
|
||||
def _get_device_timezone(self):
|
||||
"""
|
||||
Get the device timezone from the getprop.txt file.
|
||||
|
||||
This is needed to map local timestamps stored in some
|
||||
Android log files to UTC/timezone-aware timestamps.
|
||||
"""
|
||||
get_prop_files = self._get_files_by_pattern("*/getprop.txt")
|
||||
if not get_prop_files:
|
||||
self.log.warning(
|
||||
"Could not find getprop.txt file. "
|
||||
"Some timestamps and timeline data may be incorrect."
|
||||
)
|
||||
return None
|
||||
|
||||
from mvt.android.artifacts.getprop import GetProp
|
||||
|
||||
properties_artifact = GetProp()
|
||||
prop_data = self._get_file_content(get_prop_files[0]).decode("utf-8")
|
||||
properties_artifact.parse(prop_data)
|
||||
timezone = properties_artifact.get_device_timezone()
|
||||
if timezone:
|
||||
self.log.debug("Identified local phone timezone: %s", timezone)
|
||||
return timezone
|
||||
|
||||
self.log.warning(
|
||||
"Could not find or determine local device timezone. "
|
||||
"Some timestamps and timeline data may be incorrect."
|
||||
)
|
||||
return None
|
||||
|
||||
def _get_file_content(self, file_path):
|
||||
if self.archive:
|
||||
handle = self.archive.open(file_path)
|
||||
@@ -6,13 +6,13 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact
|
||||
from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact
|
||||
|
||||
from .base import BugReportModule
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysADBState(DumpsysADBArtifact, BugReportModule):
|
||||
"""This module extracts ADB key info."""
|
||||
class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidQFModule):
|
||||
"""This module analyse dumpsys accessbility"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -33,22 +33,19 @@ class DumpsysADBState(DumpsysADBArtifact, BugReportModule):
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
full_dumpsys = self._get_dumpstate_file()
|
||||
if not full_dumpsys:
|
||||
self.log.error(
|
||||
"Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?"
|
||||
)
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
content = self.extract_dumpsys_section(
|
||||
full_dumpsys,
|
||||
b"DUMP OF SERVICE adb:",
|
||||
binary=True,
|
||||
)
|
||||
data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace")
|
||||
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE accessibility:")
|
||||
self.parse(content)
|
||||
if self.results:
|
||||
|
||||
for result in self.results:
|
||||
self.log.info(
|
||||
"Identified a total of %d trusted ADB keys",
|
||||
len(self.results[0].get("user_keys", [])),
|
||||
'Found installed accessibility service "%s"', result.get("service")
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"Identified a total of %d accessibility services", len(self.results)
|
||||
)
|
||||
@@ -6,13 +6,15 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatArtifact
|
||||
from mvt.android.artifacts.dumpsys_package_activities import (
|
||||
DumpsysPackageActivitiesArtifact,
|
||||
)
|
||||
|
||||
from mvt.android.modules.bugreport.base import BugReportModule
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule):
|
||||
"""This module extracts details on uninstalled apps."""
|
||||
class DumpsysActivities(DumpsysPackageActivitiesArtifact, AndroidQFModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -32,17 +34,17 @@ class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule):
|
||||
results=results,
|
||||
)
|
||||
|
||||
self.results = results if results else []
|
||||
|
||||
def run(self) -> None:
|
||||
data = self._get_dumpstate_file()
|
||||
if not data:
|
||||
self.log.error(
|
||||
"Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?"
|
||||
)
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
data = data.decode("utf-8", errors="replace")
|
||||
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE platform_compat:")
|
||||
# Get data and extract the dumpsys section
|
||||
data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace")
|
||||
content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:")
|
||||
# Parse it
|
||||
self.parse(content)
|
||||
|
||||
self.log.info("Found %d uninstalled apps", len(self.results))
|
||||
self.log.info("Extracted %d package activities", len(self.results))
|
||||
46
mvt/android/modules/androidqf/dumpsys_appops.py
Normal file
46
mvt/android/modules/androidqf/dumpsys_appops.py
Normal file
@@ -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
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysAppops(DumpsysAppopsArtifact, AndroidQFModule):
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
# Extract section
|
||||
data = self._get_file_content(dumpsys_file[0])
|
||||
section = self.extract_dumpsys_section(
|
||||
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE appops:"
|
||||
)
|
||||
|
||||
# Parse it
|
||||
self.parse(section)
|
||||
self.log.info("Identified %d applications in AppOps Manager", len(self.results))
|
||||
46
mvt/android/modules/androidqf/dumpsys_battery_daily.py
Normal file
46
mvt/android/modules/androidqf/dumpsys_battery_daily.py
Normal file
@@ -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
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, AndroidQFModule):
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
# Extract section
|
||||
data = self._get_file_content(dumpsys_file[0])
|
||||
section = self.extract_dumpsys_section(
|
||||
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE batterystats:"
|
||||
)
|
||||
|
||||
# Parse it
|
||||
self.parse(section)
|
||||
self.log.info("Extracted a total of %d battery daily stats", len(self.results))
|
||||
46
mvt/android/modules/androidqf/dumpsys_battery_history.py
Normal file
46
mvt/android/modules/androidqf/dumpsys_battery_history.py
Normal file
@@ -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
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, AndroidQFModule):
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
# Extract section
|
||||
data = self._get_file_content(dumpsys_file[0])
|
||||
section = self.extract_dumpsys_section(
|
||||
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE batterystats:"
|
||||
)
|
||||
|
||||
# Parse it
|
||||
self.parse(section)
|
||||
self.log.info("Extracted a total of %d battery daily stats", len(self.results))
|
||||
46
mvt/android/modules/androidqf/dumpsys_dbinfo.py
Normal file
46
mvt/android/modules/androidqf/dumpsys_dbinfo.py
Normal file
@@ -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
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfoArtifact
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysDBInfo(DumpsysDBInfoArtifact, AndroidQFModule):
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
# Extract dumpsys DBInfo section
|
||||
data = self._get_file_content(dumpsys_file[0])
|
||||
section = self.extract_dumpsys_section(
|
||||
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE dbinfo:"
|
||||
)
|
||||
|
||||
# Parse it
|
||||
self.parse(section)
|
||||
self.log.info("Identified %d DB Info entries", len(self.results))
|
||||
118
mvt/android/modules/androidqf/dumpsys_packages.py
Normal file
118
mvt/android/modules/androidqf/dumpsys_packages.py
Normal 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 logging
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from mvt.android.modules.adb.packages import (
|
||||
DANGEROUS_PERMISSIONS,
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD,
|
||||
ROOT_PACKAGES,
|
||||
)
|
||||
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysPackages(AndroidQFModule):
|
||||
"""This module analyse dumpsys 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: Optional[List[Dict[str, Any]]] = 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) -> Union[dict, list]:
|
||||
entries = []
|
||||
for entry in ["timestamp", "first_install_time", "last_update_time"]:
|
||||
if entry in record:
|
||||
entries.append(
|
||||
{
|
||||
"timestamp": record[entry],
|
||||
"module": self.__class__.__name__,
|
||||
"event": entry,
|
||||
"data": f"Package {record['package_name']} "
|
||||
f"({record['uid']})",
|
||||
}
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
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 not self.indicators:
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_app_id(result.get("package_name", ""))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if len(dumpsys_file) != 1:
|
||||
self.log.info("Dumpsys file not found")
|
||||
return
|
||||
|
||||
data = self._get_file_content(dumpsys_file[0])
|
||||
|
||||
package = []
|
||||
in_service = False
|
||||
in_package_list = False
|
||||
for line in data.decode("utf-8").split("\n"):
|
||||
if line.strip().startswith("DUMP OF SERVICE package:"):
|
||||
in_service = True
|
||||
continue
|
||||
|
||||
if in_service and line.startswith("Packages:"):
|
||||
in_package_list = True
|
||||
continue
|
||||
|
||||
if not in_service or not in_package_list:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
package.append(line)
|
||||
|
||||
self.results = parse_dumpsys_packages("\n".join(package))
|
||||
|
||||
for result in self.results:
|
||||
dangerous_permissions_count = 0
|
||||
for perm in result["permissions"]:
|
||||
if perm["name"] in DANGEROUS_PERMISSIONS:
|
||||
dangerous_permissions_count += 1
|
||||
|
||||
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
|
||||
self.log.info(
|
||||
'Found package "%s" requested %d potentially dangerous permissions',
|
||||
result["package_name"],
|
||||
dangerous_permissions_count,
|
||||
)
|
||||
|
||||
self.log.info("Extracted details on %d packages", len(self.results))
|
||||
49
mvt/android/modules/androidqf/dumpsys_receivers.py
Normal file
49
mvt/android/modules/androidqf/dumpsys_receivers.py
Normal file
@@ -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 Any, Dict, List, Optional, Union
|
||||
|
||||
from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysReceivers(DumpsysReceiversArtifact, AndroidQFModule):
|
||||
"""This module analyse dumpsys receivers"""
|
||||
|
||||
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: Union[List[Any], Dict[str, Any], None] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
self.results = results if results else {}
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
data = self._get_file_content(dumpsys_file[0])
|
||||
|
||||
dumpsys_section = self.extract_dumpsys_section(
|
||||
data.decode("utf-8", errors="replace"), "DUMP OF SERVICE package:"
|
||||
)
|
||||
|
||||
self.parse(dumpsys_section)
|
||||
|
||||
self.log.info("Extracted receivers for %d intents", len(self.results))
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class AQFGetProp(GetPropArtifact, AndroidQFModule):
|
||||
class Getprop(GetPropArtifact, AndroidQFModule):
|
||||
"""This module extracts data from get properties."""
|
||||
|
||||
def __init__(
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.processes import Processes as ProcessesArtifact
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class AQFProcesses(ProcessesArtifact, AndroidQFModule):
|
||||
class Processes(ProcessesArtifact, AndroidQFModule):
|
||||
"""This module analyse running processes"""
|
||||
|
||||
def __init__(
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.settings import Settings as SettingsArtifact
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class AQFSettings(SettingsArtifact, AndroidQFModule):
|
||||
class Settings(SettingsArtifact, AndroidQFModule):
|
||||
"""This module analyse setting files"""
|
||||
|
||||
def __init__(
|
||||
@@ -19,13 +19,7 @@ from .base import AndroidQFModule
|
||||
|
||||
|
||||
class SMS(AndroidQFModule):
|
||||
"""
|
||||
This module analyse SMS file in backup
|
||||
|
||||
XXX: We should also de-duplicate this AQF module, but first we
|
||||
need to add tests for loading encrypted SMS backups through the backup
|
||||
sub-module.
|
||||
"""
|
||||
"""This module analyse SMS file in backup"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -37,7 +37,10 @@ class BackupExtraction(MVTModule):
|
||||
self.tar = None
|
||||
self.files = []
|
||||
|
||||
def from_dir(self, backup_path: Optional[str], files: List[str]) -> None:
|
||||
def from_folder(self, backup_path: Optional[str], files: List[str]) -> None:
|
||||
"""
|
||||
Get all the files and list them
|
||||
"""
|
||||
self.backup_path = backup_path
|
||||
self.files = files
|
||||
|
||||
@@ -55,16 +58,14 @@ class BackupExtraction(MVTModule):
|
||||
return fnmatch.filter(self.files, pattern)
|
||||
|
||||
def _get_file_content(self, file_path: str) -> bytes:
|
||||
if self.tar:
|
||||
if self.ab:
|
||||
try:
|
||||
member = self.tar.getmember(file_path)
|
||||
except KeyError:
|
||||
return None
|
||||
handle = self.tar.extractfile(member)
|
||||
elif self.backup_path:
|
||||
handle = open(os.path.join(self.backup_path, file_path), "rb")
|
||||
else:
|
||||
raise ValueError("No backup path or tar file provided")
|
||||
handle = open(os.path.join(self.backup_path, file_path), "rb")
|
||||
|
||||
data = handle.read()
|
||||
handle.close()
|
||||
@@ -3,11 +3,10 @@
|
||||
# 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
|
||||
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from mvt.common.config import settings
|
||||
|
||||
MVT_ANDROID_BACKUP_PASSWORD = "MVT_ANDROID_BACKUP_PASSWORD"
|
||||
|
||||
|
||||
@@ -17,24 +16,24 @@ def cli_load_android_backup_password(log, backup_password):
|
||||
|
||||
Used in MVT CLI command parsers.
|
||||
"""
|
||||
password_from_env_or_config = settings.ANDROID_BACKUP_PASSWORD
|
||||
password_from_env = os.environ.get(MVT_ANDROID_BACKUP_PASSWORD, None)
|
||||
if backup_password:
|
||||
log.info(
|
||||
"Your password may be visible in the process table because it "
|
||||
"was supplied on the command line!"
|
||||
)
|
||||
if password_from_env_or_config:
|
||||
if password_from_env:
|
||||
log.info(
|
||||
"Ignoring %s environment variable, using --backup-password argument instead",
|
||||
"MVT_ANDROID_BACKUP_PASSWORD",
|
||||
MVT_ANDROID_BACKUP_PASSWORD,
|
||||
)
|
||||
return backup_password
|
||||
elif password_from_env_or_config:
|
||||
elif password_from_env:
|
||||
log.info(
|
||||
"Using backup password from %s environment variable or config file",
|
||||
"MVT_ANDROID_BACKUP_PASSWORD",
|
||||
"Using backup password from %s environment variable",
|
||||
MVT_ANDROID_BACKUP_PASSWORD,
|
||||
)
|
||||
return password_from_env_or_config
|
||||
return password_from_env
|
||||
|
||||
|
||||
def prompt_or_load_android_backup_password(log, module_options):
|
||||
@@ -43,20 +43,19 @@ class SMS(BackupExtraction):
|
||||
if message_links == []:
|
||||
message_links = check_for_links(message.get("text", ""))
|
||||
|
||||
if self.indicators.check_urls(message_links):
|
||||
if self.indicators.check_domains(message_links):
|
||||
self.detected.append(message)
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
sms_path = "apps/com.android.providers.telephony/d_f/*_sms_backup"
|
||||
for file in self._get_files_by_pattern(sms_path):
|
||||
self.log.debug("Processing SMS backup file at %s", file)
|
||||
self.log.info("Processing SMS backup file at %s", file)
|
||||
data = self._get_file_content(file)
|
||||
self.results.extend(parse_sms_file(data))
|
||||
|
||||
mms_path = "apps/com.android.providers.telephony/d_f/*_mms_backup"
|
||||
for file in self._get_files_by_pattern(mms_path):
|
||||
self.log.debug("Processing MMS backup file at %s", file)
|
||||
self.log.info("Processing MMS backup file at %s", file)
|
||||
data = self._get_file_content(file)
|
||||
self.results.extend(parse_sms_file(data))
|
||||
|
||||
26
mvt/android/modules/bugreport/__init__.py
Normal file
26
mvt/android/modules/bugreport/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# 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 .accessibility import Accessibility
|
||||
from .activities import Activities
|
||||
from .appops import Appops
|
||||
from .battery_daily import BatteryDaily
|
||||
from .battery_history import BatteryHistory
|
||||
from .dbinfo import DBInfo
|
||||
from .getprop import Getprop
|
||||
from .packages import Packages
|
||||
from .receivers import Receivers
|
||||
|
||||
BUGREPORT_MODULES = [
|
||||
Accessibility,
|
||||
Activities,
|
||||
Appops,
|
||||
BatteryDaily,
|
||||
BatteryHistory,
|
||||
DBInfo,
|
||||
Getprop,
|
||||
Packages,
|
||||
Receivers,
|
||||
]
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArti
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule):
|
||||
class Accessibility(DumpsysAccessibilityArtifact, BugReportModule):
|
||||
"""This module extracts stats on accessibility."""
|
||||
|
||||
def __init__(
|
||||
@@ -13,7 +13,7 @@ from mvt.android.artifacts.dumpsys_package_activities import (
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class DumpsysActivities(DumpsysPackageActivitiesArtifact, BugReportModule):
|
||||
class Activities(DumpsysPackageActivitiesArtifact, BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class DumpsysAppops(DumpsysAppopsArtifact, BugReportModule):
|
||||
class Appops(DumpsysAppopsArtifact, BugReportModule):
|
||||
"""This module extracts information on package from App-Ops Manager."""
|
||||
|
||||
def __init__(
|
||||
@@ -2,11 +2,10 @@
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
|
||||
# https://github.com/mvt-project/mvt/blob/main/LICENSE
|
||||
import datetime
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
|
||||
from typing import List, Optional
|
||||
from zipfile import ZipFile
|
||||
|
||||
@@ -39,7 +38,9 @@ class BugReportModule(MVTModule):
|
||||
self.extract_files: List[str] = []
|
||||
self.zip_files: List[str] = []
|
||||
|
||||
def from_dir(self, extract_path: str, extract_files: List[str]) -> None:
|
||||
def from_folder(
|
||||
self, extract_path: Optional[str], extract_files: List[str]
|
||||
) -> None:
|
||||
self.extract_path = extract_path
|
||||
self.extract_files = extract_files
|
||||
|
||||
@@ -91,10 +92,4 @@ class BugReportModule(MVTModule):
|
||||
|
||||
return self._get_file_content(dumpstate_logs[0])
|
||||
|
||||
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:
|
||||
file_stat = os.stat(os.path.join(self.extract_path, file_path))
|
||||
return datetime.datetime.fromtimestamp(file_stat.st_mtime)
|
||||
return None
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtif
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, BugReportModule):
|
||||
class BatteryDaily(DumpsysBatteryDailyArtifact, BugReportModule):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
def __init__(
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryA
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, BugReportModule):
|
||||
class BatteryHistory(DumpsysBatteryHistoryArtifact, BugReportModule):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
def __init__(
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfoArtifact
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class DumpsysDBInfo(DumpsysDBInfoArtifact, BugReportModule):
|
||||
class DBInfo(DumpsysDBInfoArtifact, BugReportModule):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
slug = "dbinfo"
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class DumpsysGetProp(GetPropArtifact, BugReportModule):
|
||||
class Getprop(GetPropArtifact, BugReportModule):
|
||||
"""This module extracts device properties from getprop command."""
|
||||
|
||||
def __init__(
|
||||
131
mvt/android/modules/bugreport/packages.py
Normal file
131
mvt/android/modules/bugreport/packages.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# 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, Union
|
||||
|
||||
from mvt.android.modules.adb.packages import (
|
||||
DANGEROUS_PERMISSIONS,
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD,
|
||||
ROOT_PACKAGES,
|
||||
)
|
||||
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class Packages(BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
module_options: Optional[dict] = None,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
file_path=file_path,
|
||||
target_path=target_path,
|
||||
results_path=results_path,
|
||||
module_options=module_options,
|
||||
log=log,
|
||||
results=results,
|
||||
)
|
||||
|
||||
def serialize(self, record: dict) -> Union[dict, list]:
|
||||
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"Install or update of package {record['package_name']}",
|
||||
}
|
||||
)
|
||||
|
||||
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 not self.indicators:
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_app_id(result.get("package_name"))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error(
|
||||
"Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?"
|
||||
)
|
||||
return
|
||||
|
||||
in_package = False
|
||||
in_packages_list = False
|
||||
lines = []
|
||||
for line in content.decode(errors="ignore").splitlines():
|
||||
if line.strip() == "DUMP OF SERVICE package:":
|
||||
in_package = True
|
||||
continue
|
||||
|
||||
if not in_package:
|
||||
continue
|
||||
|
||||
if line.strip() == "Packages:":
|
||||
in_packages_list = True
|
||||
continue
|
||||
|
||||
if not in_packages_list:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = parse_dumpsys_packages("\n".join(lines))
|
||||
|
||||
for result in self.results:
|
||||
dangerous_permissions_count = 0
|
||||
for perm in result["permissions"]:
|
||||
if perm["name"] in DANGEROUS_PERMISSIONS:
|
||||
dangerous_permissions_count += 1
|
||||
|
||||
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
|
||||
self.log.info(
|
||||
'Found package "%s" requested %d potentially dangerous permissions',
|
||||
result["package_name"],
|
||||
dangerous_permissions_count,
|
||||
)
|
||||
|
||||
self.log.info("Extracted details on %d packages", len(self.results))
|
||||
@@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class DumpsysReceivers(DumpsysReceiversArtifact, BugReportModule):
|
||||
class Receivers(DumpsysReceiversArtifact, BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(
|
||||
@@ -34,20 +34,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 = self.indicators.check_receiver_prefix(receiver_name)
|
||||
if ioc:
|
||||
self.results[result][0]["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
|
||||
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user