Compare commits

..

1 Commits

Author SHA1 Message Date
Nex
a30d7b2871 Adding support for iOS lockdown management 2022-07-05 18:12:10 +02:00
350 changed files with 8125 additions and 26765 deletions

View File

@@ -1,19 +0,0 @@
name: Add issue to project
on:
issues:
types:
- opened
- reopened
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.5.0
with:
# You can target a project in a different organization
# to the issue
project-url: https://github.com/orgs/mvt-project/projects/1
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}

26
.github/workflows/flake8.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Flake8
on:
push:
paths:
- '*.py'
jobs:
flake8_py3:
runs-on: ubuntu-latest
steps:
- name: Setup Python
uses: actions/setup-python@v1
with:
python-version: 3.9
architecture: x64
- name: Checkout
uses: actions/checkout@master
- name: Install flake8
run: pip install flake8
- name: Run flake8
uses: suo/flake8-github-action@releases/v1
with:
checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

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

43
.github/workflows/python-package.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
# 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.7, 3.8, 3.9]
python-version: [3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest safety stix2 pytest-mock
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
run: pytest

View File

@@ -1,27 +0,0 @@
name: Ruff
on:
push:
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
- name: ruff
run: |
make ruff

View File

@@ -1,96 +0,0 @@
"""
Python script to download the Apple RSS feed and parse it.
"""
import json
import os
import urllib.request
from xml.dom.minidom import parseString
from packaging import version
def download_apple_rss(feed_url):
with urllib.request.urlopen(feed_url) as f:
rss_feed = f.read().decode("utf-8")
print("Downloaded RSS feed from Apple.")
return rss_feed
def parse_latest_ios_versions(rss_feed_text):
latest_ios_versions = []
parsed_feed = parseString(rss_feed_text)
for item in parsed_feed.getElementsByTagName("item"):
title = item.getElementsByTagName("title")[0].firstChild.data
if not title.startswith("iOS"):
continue
import re
build_match = re.match(
r"iOS (?P<version>[\d\.]+) (?P<beta>beta )?(\S*)?\((?P<build>.*)\)", title
)
if not build_match:
print("Could not parse iOS build:", title)
continue
# Handle iOS beta releases
release_info = build_match.groupdict()
release_beta = release_info.pop("beta")
if release_beta:
print("Skipping beta release:", title)
continue
# Some iOS releases have multiple build number for different hardware models.
# We will split these into separate entries and record each build number.
build_list = release_info.pop("build")
build_variants = build_list.split(" | ")
for build_number in build_variants:
release_info["build"] = build_number
latest_ios_versions.append(release_info)
return latest_ios_versions
def update_mvt(mvt_checkout_path, latest_ios_versions):
version_path = os.path.join(mvt_checkout_path, "src/mvt/ios/data/ios_versions.json")
with open(version_path, "r") as version_file:
current_versions = json.load(version_file)
new_entry_count = 0
for new_version in latest_ios_versions:
for current_version in current_versions:
if new_version["build"] == current_version["build"]:
break
else:
# New version that does not exist in current data
current_versions.append(new_version)
new_entry_count += 1
if not new_entry_count:
print("No new iOS versions found.")
else:
print("Found {} new iOS versions.".format(new_entry_count))
new_version_list = sorted(
current_versions, key=lambda x: version.Version(x["version"])
)
with open(version_path, "w") as version_file:
json.dump(new_version_list, version_file, indent=4)
def main():
print("Downloading RSS feed...")
mvt_checkout_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../../")
)
rss_feed = download_apple_rss(
"https://developer.apple.com/news/releases/rss/releases.rss"
)
latest_ios_version = parse_latest_ios_versions(rss_feed)
update_mvt(mvt_checkout_path, latest_ios_version)
if __name__ == "__main__":
main()

View File

@@ -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.8', '3.9', '3.10'] # , '3.11']
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

View File

@@ -1,29 +0,0 @@
name: Update iOS releases and version numbers
run-name: ${{ github.actor }} is finding the latest iOS release version and build numbers
on:
workflow_dispatch:
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '0 */6 * * *'
jobs:
update-ios-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- name: Run script to fetch latest iOS releases from Apple RSS feed.
run: python3 .github/workflows/scripts/update-ios-releases.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
title: '[auto] Update iOS releases and versions'
commit-message: Add new iOS versions and build numbers
branch: auto/add-new-ios-releases
body: |
This is an automated pull request to update the iOS releases and version numbers.
add-paths: |
*.json
labels: |
automated pr

2
.gitignore vendored
View File

@@ -50,8 +50,6 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
pytest-coverage.txt
pytest.xml
# Translations
*.mo

View File

@@ -5,15 +5,11 @@
# Required
version: 2
build:
os: "ubuntu-22.04"
tools:
python: "3.11"
mkdocs:
configuration: mkdocs.yml
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: docs/requirements.txt

View File

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

View File

@@ -1,19 +0,0 @@
# Contributing
Thank you for your interest in contributing to Mobile Verification Toolkit (MVT)! Your help is very much appreciated.
## Where to start
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.
## Code style
When contributing code to
- **Indentation**: we use 4-spaces tabs.
- **Quotes**: we use double quotes (`"`) as a default. Single quotes (`'`) can be favored with nested strings instead of escaping (`\"`), or when using f-formatting.
- **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.

View File

@@ -1,159 +1,89 @@
# Base image for building libraries
# ---------------------------------
FROM ubuntu:22.04 as build-base
FROM ubuntu:20.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
# Fixing major OS dependencies
# ----------------------------
RUN apt update \
&& apt install -y python3 python3-pip libusb-1.0-0-dev \
&& apt install -y wget unzip\
&& DEBIAN_FRONTEND=noninteractive apt-get -y install default-jre-headless \
# 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:22.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 --upgrade pip \
&& PIP_NO_CACHE_DIR=1 pip3 install ./mvt \
&& apt-get remove -y python3-pip git && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf mvt
# Installing MVT
# --------------
RUN pip3 install 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
# --------------
RUN mkdir /opt/abe \
&& wget https://github.com/nelenkov/android-backup-extractor/releases/download/20210709062403-4c55371/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
# Install Android Platform Tools
# ------------------------------
RUN mkdir /opt/android \
&& wget -q https://dl.google.com/android/repository/platform-tools-latest-linux.zip \
&& unzip platform-tools-latest-linux.zip -d /opt/android \
# Create alias for adb
&& echo 'alias adb="/opt/android/platform-tools/adb"' >> ~/.bashrc
# Generate adb key folder
# ------------------------------
RUN mkdir /root/.android && /opt/android/platform-tools/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

View File

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

View File

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

View File

@@ -1,39 +1,10 @@
PWD = $(shell pwd)
autofix:
ruff format .
ruff check --fix .
check: ruff mypy
ruff:
ruff format --check .
ruff check -q .
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 -r test-requirements.txt
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/*

View File

@@ -6,33 +6,19 @@
[![](https://img.shields.io/pypi/v/mvt)](https://pypi.org/project/mvt/)
[![Documentation Status](https://readthedocs.org/projects/mvt/badge/?version=latest)](https://docs.mvt.re/en/latest/?badge=latest)
[![CI](https://github.com/mvt-project/mvt/actions/workflows/tests.yml/badge.svg)](https://github.com/mvt-project/mvt/actions/workflows/tests.yml)
[![CI](https://github.com/mvt-project/mvt/actions/workflows/python-package.yml/badge.svg)](https://github.com/mvt-project/mvt/actions/workflows/python-package.yml)
[![Downloads](https://pepy.tech/badge/mvt)](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.
It has been developed and released by the [Amnesty International Security Lab](https://securitylab.amnesty.org) in July 2021 in the context of the [Pegasus Project](https://forbiddenstories.org/about-the-pegasus-project/) along with [a technical forensic methodology](https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/). It continues to be maintained by Amnesty International and other contributors.
It has been developed and released by the [Amnesty International Security Lab](https://www.amnesty.org/en/tech/) in July 2021 in the context of the [Pegasus project](https://forbiddenstories.org/about-the-pegasus-project/) along with [a technical forensic methodology and forensic evidence](https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/).
> **Note**
> MVT is a forensic research tool intended for technologists and investigators. It requires understanding digital forensics and using command-line tools. This is not intended for end-user self-assessment. If you are concerned with the security of your device please seek reputable expert assistance.
>
*Warning*: MVT is a forensic research tool intended for technologists and investigators. Using it requires understanding the basics of forensic analysis and using command-line tools. This is not intended for end-user self-assessment. If you are concerned with the security of your device please seek expert assistance.
### Indicators of Compromise
MVT supports using public [indicators of compromise (IOCs)](https://github.com/mvt-project/mvt-indicators) to scan mobile devices for potential traces of targeting or infection by known spyware campaigns. This includes IOCs published by [Amnesty International](https://github.com/AmnestyTech/investigations/) and other research groups.
> **Warning**
> Public indicators of compromise are insufficient to determine that a device is "clean", and not targeted with a particular spyware tool. Reliance on public indicators alone can miss recent forensic traces and give a false sense of security.
>
> Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
>
>Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/get-help/?c=mvt_docs) or through our forensic partnership with [Access Nows 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/).
## Installation
MVT can be installed from sources or from [PyPI](https://pypi.org/project/mvt/) (you will need some dependencies, check the [documentation](https://docs.mvt.re/en/latest/install/)):
MVT can be installed from sources or from [PyPi](https://pypi.org/project/mvt/) (you will need some dependencies, check the [documentation](https://docs.mvt.re/en/latest/install/)):
```
pip3 install mvt

View File

@@ -1,5 +0,0 @@
# Reporting security issues
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 *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
View 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
View 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()

View File

@@ -24,7 +24,7 @@ Some recent phones will enforce the utilisation of a password to encrypt the bac
## Unpack and check the backup
MVT includes a partial implementation of the Android Backup parsing, because of the implementation difference in the compression algorithm between Java and Python. The `-nocompress` option passed to adb in the section above allows to avoid this issue. You can analyse and extract SMSs from the backup directly with MVT:
MVT includes a partial implementation of the Android Backup parsing, because of the implementation difference in the compression algorithm between Java and Python. The `-nocompress` option passed to adb in the section above allows to avoid this issue. You can analyse and extract SMSs containing links from the backup directly with MVT:
```bash
$ mvt-android check-backup --output /path/to/results/ /path/to/backup.ab
@@ -32,14 +32,10 @@ $ mvt-android check-backup --output /path/to/results/ /path/to/backup.ab
INFO [mvt.android.modules.backup.sms] Running module SMS...
INFO [mvt.android.modules.backup.sms] Processing SMS backup file at
apps/com.android.providers.telephony/d_f/000000_sms_backup
INFO [mvt.android.modules.backup.sms] Extracted a total of 64 SMS messages
INFO [mvt.android.modules.backup.sms] Extracted a total of 64 SMS messages containing links
```
If the backup is encrypted, MVT will prompt you to enter the password. A backup password can also be provided with the `--backup-password` command line option or through the `MVT_ANDROID_BACKUP_PASSWORD` environment variable. The same options can also be used to when analysing an encrypted backup collected through AndroidQF in the `mvt-android check-androidqf` command:
```bash
$ mvt-android check-backup --backup-password "password123" --output /path/to/results/ /path/to/backup.ab
```
If the backup is encrypted, MVT will prompt you to enter the password.
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by MVT. Any matches will be highlighted in the terminal output.
@@ -56,4 +52,4 @@ If the backup is encrypted, ABE will prompt you to enter the password.
Alternatively, [ab-decrypt](https://github.com/joernheissler/ab-decrypt) can be used for that purpose.
You can then extract SMSs with MVT by passing the folder path as parameter instead of the `.ab` file: `mvt-android check-backup --output /path/to/results/ /path/to/backup/` (the path to backup given should be the folder containing the `apps` folder).
You can then extract SMSs containing links with MVT by passing the folder path as parameter instead of the `.ab` file: `mvt-android check-backup --output /path/to/results/ /path/to/backup/` (the path to backup given should be the folder containing the `apps` folder).

View File

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

View File

@@ -1,27 +0,0 @@
# Development
The Mobile Verification Toolkit team welcomes contributions of new forensic modules or other contributions which help improve the software.
## Testing
MVT uses `pytest` for unit and integration tests. Code style consistency is maintained with `flake8`, `ruff` and `black`. All can
be run automatically with:
```bash
make check
```
Run these tests before making new commits or opening pull requests.
## Profiling
Some MVT modules extract and process significant amounts of data during the analysis process or while checking results against known indicators. Care must be
take to avoid inefficient code paths as we add new modules.
MVT modules can be profiled with Python built-in `cProfile` by setting the `MVT_PROFILE` environment variable.
```bash
MVT_PROFILE=1 dev/mvt-ios check-backup test_backup
```
Open an issue or PR if you are encountering significant performance issues when analyzing a device with MVT.

View File

@@ -1,23 +1,8 @@
Using Docker simplifies having all the required dependencies and tools (including most recent versions of [libimobiledevice](https://libimobiledevice.org)) readily installed. Note that this requires a Linux host, as Docker for Windows and Mac [doesn't support passing through USB devices](https://docs.docker.com/desktop/faqs/#can-i-pass-through-a-usb-device-to-a-container).
Using Docker simplifies having all the required dependencies and tools (including most recent versions of [libimobiledevice](https://libimobiledevice.org)) readily installed.
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
@@ -25,6 +10,11 @@ cd mvt
docker build -t mvt .
```
Optionally, you may need to specify your platform to Docker in order to build successfully (Apple M1)
```bash
docker build --platform amd64 -t mvt .
```
Test if the image was created successfully:
```bash
@@ -33,9 +23,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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

View File

@@ -6,9 +6,6 @@
Mobile Verification Toolkit (MVT) is a tool to facilitate the [consensual forensic analysis](introduction.md#consensual-forensics) of Android and iOS devices, for the purpose of identifying traces of compromise.
It has been developed and released by the [Amnesty International Security Lab](https://securitylab.amnesty.org) in July 2021 in the context of the [Pegasus Project](https://forbiddenstories.org/about-the-pegasus-project/) along with [a technical forensic methodology](https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/). It continues to be maintained by Amnesty International and other contributors.
In this documentation you will find instructions on how to install and run the `mvt-ios` and `mvt-android` commands, and guidance on how to interpret the extracted results.
## Resources

View File

@@ -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,43 +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.
Or 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.

View File

@@ -12,20 +12,6 @@ Mobile Verification Toolkit (MVT) is a collection of utilities designed to facil
MVT is a forensic research tool intended for technologists and investigators. Using it requires understanding the basics of forensic analysis and using command-line tools. MVT is not intended for end-user self-assessment. If you are concerned with the security of your device please seek expert assistance.
## Indicators of Compromise
MVT supports using [indicators of compromise (IOCs)](https://github.com/mvt-project/mvt-indicators) to scan mobile devices for potential traces of targeting or infection by known spyware campaigns. This includes IOCs published by [Amnesty International](https://github.com/AmnestyTech/investigations/) and other research groups.
!!! warning
Public indicators of compromise are insufficient to determine that a device is "clean", and not targeted with a particular spyware tool. Reliance on public indicators alone can miss recent forensic traces and give a false sense of security.
Reliable and comprehensive digital forensic support and triage requires access to non-public indicators, research and threat intelligence.
Such support is available to civil society through [Amnesty International's Security Lab](https://securitylab.amnesty.org/get-help/?c=mvt_docs) or [Access Nows Digital Security Helpline](https://www.accessnow.org/help/).
More information about using indicators of compromise with MVT is available in the [documentation](iocs.md).
## Consensual Forensics
While MVT is capable of extracting and processing various types of very personal records typically found on a mobile phone (such as calls history, SMS and WhatsApp messages, etc.), this is intended to help identify potential attack vectors such as malicious SMS messages leading to exploitation.

View File

@@ -34,25 +34,13 @@ 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:
- [Pegasus](https://en.wikipedia.org/wiki/Pegasus_(spyware)) ([STIX2](https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-07-18_nso/pegasus.stix2))
- [Predator from Cytrox](https://citizenlab.ca/2021/12/pegasus-vs-predator-dissidents-doubly-infected-iphone-reveals-cytrox-mercenary-spyware/) ([STIX2](https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-12-16_cytrox/cytrox.stix2))
- [An Android Spyware Campaign Linked to a Mercenary Company](https://github.com/AmnestyTech/investigations/tree/master/2023-03-29_android_campaign) ([STIX2](https://github.com/AmnestyTech/investigations/blob/master/2023-03-29_android_campaign/malware.stix2))
- [This repository](https://github.com/Te-k/stalkerware-indicators) contains IOCs for Android stalkerware including [a STIX MVT-compatible file](https://raw.githubusercontent.com/Te-k/stalkerware-indicators/master/generated/stalkerware.stix2).
- We are also maintaining [a list of IOCs](https://github.com/mvt-project/mvt-indicators) in STIX format from public spyware campaigns.
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.
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 listed [here](https://github.com/mvt-project/mvt/blob/main/public_indicators.json) 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.

View File

@@ -1,41 +1,16 @@
# Backup with iTunes app
It is possible to do an iPhone backup by using iTunes on Windows or macOS computers (in most recent versions of macOS, this feature is included in Finder, see below).
It is possible to do an iPhone backup by using iTunes on Windows or macOS computers (in most recent versions of macOS, this feature is included in Finder).
To do that:
1. Make sure iTunes is installed.
2. Connect your iPhone to your computer using a Lightning/USB cable.
3. Open the device in iTunes (or Finder on macOS).
4. If you want to have a more accurate detection, ensure that the encrypted backup option is activated and choose a secure password for the backup.
5. Start the backup and wait for it to finish (this may take up to 30 minutes).
* Make sure iTunes is installed.
* Connect your iPhone to your computer using a Lightning/USB cable.
* Open the device in iTunes (or Finder on macOS).
* If you want to have a more accurate detection, ensure that the encrypted backup option is activated and choose a secure password for the backup.
* Start the backup and wait for it to finish (this may take up to 30 minutes).
![](../../img/macos-backup.jpg)
![](../../../img/macos-backup.jpg)
_Source: [Apple Support](https://support.apple.com/en-us/HT211229)_
Once the backup is done, find its location and copy it to a place where it can be analyzed by MVT. On Windows, the backup can be stored either in `%USERPROFILE%\Apple\MobileSync\` or `%USERPROFILE%\AppData\Roaming\Apple Computer\MobileSync\`. On macOS, the backup is stored in `~/Library/Application Support/MobileSync/`.
# Backup with Finder
On more recent MacOS versions, this feature is included in Finder. To do a backup:
1. Launch Finder on your Mac.
2. Connect your iPhone to your Mac using a Lightning/USB cable.
3. Select your device from the list of devices located at the bottom of the left side bar labeled "locations".
4. In the General tab, select `Back up all the data on your iPhone to this Mac` from the options under the Backups section.
5. Check the box that says `Encrypt local backup`. If it is your first time selecting this option, you may need to enter a password to encrypt the backup.
![](../../img/macos-backup2.png)
_Source: [Apple Support](https://support.apple.com/en-us/HT211229)_
6. Click `Back Up Now` to start the back-up process.
7. The encrypted backup for your iPhone should now start. Once the process finishes, you can check the backup by opening `Finder`, clicking on the `General` tab, then click on `Manage Backups`. Now you should see a list of your backups like the image below:
![](../../img/macos-backups.png)
_Source: [Apple Support](https://support.apple.com/en-us/HT211229)_
If your backup has a lock next to it like in the image above, then the backup is encrypted. You should also see the date and time when the encrypted backup was created. The backup files are stored in `~/Library/Application Support/MobileSync/`.
## Notes:
- Remember to keep the backup encryption password that you created safe, since without it you will not be able to access/modify/decrypt the backup file.
* Once the backup is done, find its location and copy it to a place where it can be analyzed by MVT. On Windows, the backup can be stored either in `%USERPROFILE%\Apple\MobileSync\` or `%USERPROFILE%\AppData\Roaming\Apple Computer\MobileSync\`. On macOS, the backup is stored in `~/Library/Application Support/MobileSync/`.

View File

@@ -3,10 +3,10 @@
If you have correctly [installed libimobiledevice](../install.md) you can easily generate an iTunes backup using the `idevicebackup2` tool included in the suite. First, you might want to ensure that backup encryption is enabled (**note: encrypted backup contain more data than unencrypted backups**):
```bash
idevicebackup2 -i encryption on
idevicebackup2 -i backup encryption on
```
Note that if a backup password was previously set on this device, you might need to use the same or change it. You can try changing password using `idevicebackup2 -i changepw`, or by turning off encryption (`idevicebackup2 -i encryption off`) and turning it back on again.
Note that if a backup password was previously set on this device, you might need to use the same or change it. You can try changing password using `idevicebackup2 -i backup changepw`, or by turning off encryption (`idevicebackup2 -i backup encryption off`) and turning it back on again.
If you are not able to recover or change the password, you should try to disable encryption and obtain an unencrypted backup.

View File

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

View File

@@ -7,7 +7,7 @@ In this page you can find a (reasonably) up-to-date breakdown of the files creat
### `analytics.json`
!!! info "Availability"
Backup (if encrypted): :material-close:
Backup (if encrypted): :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Analytics` module. The module extracts records from the plists inside the SQLite databases located at *private/var/Keychains/Analytics/\*.db*, which contain various analytics information regarding networking, certificate-pinning, TLS, etc. failures.
@@ -16,22 +16,10 @@ If indicators are provided through the command-line, processes and domains are c
---
### `applications.json`
!!! info "Availability"
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Applications` module. The module extracts the list of applications installed on the device from the `Info.plist` file in backup, or from the `iTunesMetadata.plist` files in a file system dump. These records contains detailed information on the source and installation of the app.
If indicators are provided through the command-line, processes and application ids are checked against the app name of each application. It also flags any applications not installed from the AppStore. Any matches are stored in *applications_detected.json*.
---
### `backup_info.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-close:
This JSON file is created by mvt-ios' `BackupInfo` module. The module extracts some details about the backup and the device, such as name, phone number, IMEI, product type and version.
@@ -41,7 +29,7 @@ This JSON file is created by mvt-ios' `BackupInfo` module. The module extracts s
### `cache_files.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `CacheFiles` module. The module extracts records from all SQLite database files stored on disk with the name *Cache.db*. These databases typically contain data from iOS' [internal URL caching](https://developer.apple.com/documentation/foundation/nsurlcache). Through this module you might be able to recover records of HTTP requests and responses performed my applications as well as system services, that would otherwise be unavailable. For example, you might see HTTP requests part of an exploitation chain performed by an iOS service attempting to download a first stage malicious payload.
@@ -50,22 +38,10 @@ If indicators are provided through the command-line, they are checked against th
---
### `calendar.json`
!!! info "Availability"
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Calendar` module. This module extracts all CalendarItems from the `Calendar.sqlitedb` database. This database contains all calendar entries from the different calendars installed on the phone.
If indicators are provided through the command-line, email addresses are checked against the inviter's email of the different events. Any matches are stored in *calendar_detected.json*.
---
### `calls.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Calls` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/CallHistoryDB/CallHistory.storedata*, which contains records of incoming and outgoing calls, including from messaging apps such as WhatsApp or Skype.
@@ -75,7 +51,7 @@ This JSON file is created by mvt-ios' `Calls` module. The module extracts record
### `chrome_favicon.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `ChromeFavicon` module. The module extracts records from a SQLite database located at */private/var/mobile/Containers/Data/Application/\*/Library/Application Support/Google/Chrome/Default/Favicons*, which contains a mapping of favicons' URLs and the visited URLs which loaded them.
@@ -87,7 +63,7 @@ If indicators are provided through the command-line, they are checked against bo
### `chrome_history.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `ChromeHistory` module. The module extracts records from a SQLite database located at */private/var/mobile/Containers/Data/Application/\*/Library/Application Support/Google/Chrome/Default/History*, which contains a history of URL visits.
@@ -99,7 +75,7 @@ If indicators are provided through the command-line, they are checked against th
### `configuration_profiles.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-close:
This JSON file is created by mvt-ios' `ConfigurationProfiles` module. The module extracts details about iOS configuration profiles that have been installed on the device. These should include both default iOS as well as third-party profiles.
@@ -111,7 +87,7 @@ If indicators are provided through the command-line, they are checked against th
### `contacts.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Contacts` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/AddressBook/AddressBook.sqlitedb*, which contains records from the phone's address book. While this database obviously would not contain any malicious indicators per se, you might want to use it to compare records from other apps (such as iMessage, SMS, etc.) to filter those originating from unknown origins.
@@ -121,7 +97,7 @@ This JSON file is created by mvt-ios' `Contacts` module. The module extracts rec
### `firefox_favicon.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `FirefoxFavicon` module. The module extracts records from a SQLite database located at */private/var/mobile/profile.profile/browser.db*, which contains a mapping of favicons' URLs and the visited URLs which loaded them.
@@ -133,7 +109,7 @@ If indicators are provided through the command-line, they are checked against bo
### `firefox_history.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `FirefoxHistory` module. The module extracts records from a SQLite database located at */private/var/mobile/profile.profile/browser.db*, which contains a history of URL visits.
@@ -142,20 +118,10 @@ If indicators are provided through the command-line, they are checked against th
---
### `global_preferences.json`
!!! info "Availability"
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `GlobalPreferences` module. The module extracts records from a Plist file located at */private/var/mobile/Library/Preferences/.GlobalPreferences.plist*, which contains a system preferences including if Lockdown Mode is enabled.
---
### `id_status_cache.json`
!!! info "Availability"
Backup (before iOS 14.7): :material-check:
Backup (before iOS 14.7): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `IDStatusCache` module. The module extracts records from a plist file located at */private/var/mobile/Library/Preferences/com.apple.identityservices.idstatuscache.plist*, which contains a cache of Apple user ID authentication. This chance will indicate when apps like Facetime and iMessage first established contacts with other registered Apple IDs. This is significant because it might contain traces of malicious accounts involved in exploitation of those apps.
@@ -167,7 +133,7 @@ Starting from iOS 14.7.0, this file is empty or absent.
### `shortcuts.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Shortcuts` module. The module extracts records from an SQLite database located at */private/var/mobile/Library/Shortcuts/Shortcuts.sqlite*, which contains records about the Shortcuts application. Shortcuts are a built-in iOS feature which allows users to automation certain actions on their device. In some cases the legitimate Shortcuts app may be abused by spyware to maintain persistence on an infected devices.
@@ -177,7 +143,7 @@ This JSON file is created by mvt-ios' `Shortcuts` module. The module extracts re
### `interaction_c.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `InteractionC` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/CoreDuet/People/interactionC.db*, which contains details about user interactions with installed apps.
@@ -187,7 +153,7 @@ This JSON file is created by mvt-ios' `InteractionC` module. The module extracts
### `locationd_clients.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `LocationdClients` module. The module extracts records from a plist file located at */private/var/mobile/Library/Caches/locationd/clients.plist*, which contains a cache of apps which requested access to location services.
@@ -197,7 +163,7 @@ This JSON file is created by mvt-ios' `LocationdClients` module. The module extr
### `manifest.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-close:
This JSON file is created by mvt-ios' `Manifest` module. The module extracts records from the SQLite database *Manifest.db* contained in iTunes backups, and which indexes the locally backed-up files to the original paths on the iOS device.
@@ -209,7 +175,7 @@ If indicators are provided through the command-line, they are checked against th
### `os_analytics_ad_daily.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `OSAnalyticsADDaily` module. The module extracts records from a plist located *private/var/mobile/Library/Preferences/com.apple.osanalytics.addaily.plist*, which contains a history of data usage by processes running on the system. Besides the network statistics, these records are particularly important because they might show traces of malicious process executions and the relevant timeframe.
@@ -221,7 +187,7 @@ If indicators are provided through the command-line, they are checked against th
### `datausage.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Datausage` module. The module extracts records from a SQLite database located */private/var/wireless/Library/Databases/DataUsage.sqlite*, which contains a history of network data usage by processes running on the system. It does not log network traffic through WiFi (the fields `WIFI_IN` and `WIFI_OUT` are always empty), and the `WWAN_IN` and `WWAN_OUT` fields are stored in bytes. Besides the network statistics, these records are particularly important because they might show traces of malicious process executions and the relevant timeframe. In particular, processes which do not have a valid bundle ID might require particular attention.
@@ -233,7 +199,7 @@ If indicators are provided through the command-line, they are checked against th
### `netusage.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Netusage` module. The module extracts records from a SQLite database located */private/var/networkd/netusage.sqlite*, which contains a history of data usage by processes running on the system. Besides the network statistics, these records are particularly important because they might show traces of malicious process executions and the relevant timeframe. In particular, processes which do not have a valid bundle ID might require particular attention.
@@ -245,7 +211,7 @@ If indicators are provided through the command-line, they are checked against th
### `profile_events.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-close:
This JSON file is created by mvt-ios' `ProfileEvents` module. The module extracts a timeline of configuration profile operations. For example, it should indicate when a new profile was installed from the Settings app, or when one was removed.
@@ -255,7 +221,7 @@ This JSON file is created by mvt-ios' `ProfileEvents` module. The module extract
### `safari_browser_state.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SafariBrowserState` module. The module extracts records from the SQLite databases located at */private/var/mobile/Library/Safari/BrowserState.db* or */private/var/mobile/Containers/Data/Application/\*/Library/Safari/BrowserState.db*, which contain records of opened tabs.
@@ -267,7 +233,7 @@ If indicators are provided through the command-line, they are checked against th
### `safari_favicon.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SafariFavicon` module. The module extracts records from the SQLite databases located at */private/var/mobile/Library/Image Cache/Favicons/Favicons.db* or */private/var/mobile/Containers/Data/Application/\*/Library/Image Cache/Favicons/Favicons.db*, which contain mappings of favicons' URLs and the visited URLs which loaded them.
@@ -279,7 +245,7 @@ If indicators are provided through the command-line, they are checked against bo
### `safari_history.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SafariHistory` module. The module extracts records from the SQLite databases located at */private/var/mobile/Library/Safari/History.db* or */private/var/mobile/Containers/Data/Application/\*/Library/Safari/History.db*, which contain a history of URL visits.
@@ -291,7 +257,7 @@ If indicators are provided through the command-line, they are checked against th
### `shutdown_log.json`
!!! info "Availability"
Backup (if encrypted): :material-close:
Backup (if encrypted): :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `ShutdownLog` module. The module extracts records from the shutdown log located at *private/var/db/diagnostics/shutdown.log*. When shutting down an iPhone, a SIGTERM will be sent to all processes runnning. The `shutdown.log` file will log any process (with its pid and path) that did not shut down after the SIGTERM was sent.
@@ -303,10 +269,10 @@ If indicators are provided through the command-line, they are checked against th
### `sms.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SMS` module. The module extracts a list of SMS messages from the SQLite database located at */private/var/mobile/Library/SMS/sms.db*.
This JSON file is created by mvt-ios' `SMS` module. The module extracts a list of SMS messages containing HTTP links from the SQLite database located at */private/var/mobile/Library/SMS/sms.db*.
If indicators are provided through the command-line, they are checked against the extracted HTTP links. Any matches are stored in *sms_detected.json*.
@@ -315,7 +281,7 @@ If indicators are provided through the command-line, they are checked against th
### `sms_attachments.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SMSAttachments` module. The module extracts details about attachments sent via SMS or iMessage from the same database used by the `SMS` module. These records might be useful to indicate unique patterns that might be indicative of exploitation attempts leveraging potential vulnerabilities in file format parsers or other forms of file handling by the Messages app.
@@ -325,7 +291,7 @@ This JSON file is created by mvt-ios' `SMSAttachments` module. The module extrac
### `tcc.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `TCC` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/TCC/TCC.db*, which contains a list of which services such as microphone, camera, or location, apps have been granted or denied access to.
@@ -335,7 +301,7 @@ This JSON file is created by mvt-ios' `TCC` module. The module extracts records
### `version_history.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `IOSVersionHistory` module. The module extracts records of iOS software updates from analytics plist files located at */private/var/db/analyticsd/Analytics-Journal-\*.ips*.
@@ -345,7 +311,7 @@ This JSON file is created by mvt-ios' `IOSVersionHistory` module. The module ext
### `webkit_indexeddb.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitIndexedDB` module. The module extracts a list of file and folder names located at the following path */private/var/mobile/Containers/Data/Application/\*/Library/WebKit/WebsiteData/IndexedDB*, which contains IndexedDB files created by any app installed on the device.
@@ -357,7 +323,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_local_storage.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitLocalStorage` module. The module extracts a list of file and folder names located at the following path */private/var/mobile/Containers/Data/Application/\*/Library/WebKit/WebsiteData/LocalStorage/*, which contains local storage files created by any app installed on the device.
@@ -369,7 +335,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_resource_load_statistics.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios `WebkitResourceLoadStatistics` module. The module extracts records from available WebKit ResourceLoadStatistics *observations.db* SQLite3 databases. These records should indicate domain names contacted by apps, including a timestamp.
@@ -381,7 +347,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_safari_view_service.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitSafariViewService` module. The module extracts a list of file and folder names located at the following path */private/var/mobile/Containers/Data/Application/\*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/*, which contains files cached by SafariVewService.
@@ -393,7 +359,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_session_resource_log.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitSessionResourceLog` module. The module extracts records from plist files with the name *full_browsing_session_resourceLog.plist*, which contain records of resources loaded by different domains visited.
@@ -405,10 +371,10 @@ If indicators are provided through the command-line, they are checked against th
### `whatsapp.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WhatsApp` module. The module extracts a list of WhatsApp messages from the SQLite database located at *private/var/mobile/Containers/Shared/AppGroup/\*/ChatStorage.sqlite*.
This JSON file is created by mvt-ios' `WhatsApp` module. The module extracts a list of WhatsApp messages containing HTTP links from the SQLite database located at *private/var/mobile/Containers/Shared/AppGroup/\*/ChatStorage.sqlite*.
If indicators are provided through the command-line, they are checked against the extracted HTTP links. Any matches are stored in *whatsapp_detected.json*.

View File

@@ -1,5 +1,5 @@
mkdocs==1.6.1
mkdocs-autorefs==1.2.0
mkdocs-material==9.5.42
mkdocs-material-extensions==1.3.1
mkdocstrings==0.23.0
mkdocs==1.2.3
mkdocs-autorefs
mkdocs-material
mkdocs-material-extensions
mkdocstrings

View File

@@ -1,14 +1,14 @@
site_name: Mobile Verification Toolkit
repo_url: https://github.com/mvt-project/mvt
edit_uri: edit/main/docs/
copyright: Copyright &copy; 2021-2023 MVT Project Developers
copyright: Copyright &copy; 2021-2022 MVT Project Developers
site_description: Mobile Verification Toolkit Documentation
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:
@@ -46,5 +46,4 @@ nav:
- Check an Android Backup (SMS messages): "android/backup.md"
- Download APKs: "android/download_apks.md"
- Indicators of Compromise: "iocs.md"
- Development: "development.md"
- License: "license.md"

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/

219
mvt/android/cli.py Normal file
View File

@@ -0,0 +1,219 @@
# 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 logging
import os
import click
from rich.logging import RichHandler
from mvt.common.cmd_check_iocs import CmdCheckIOCS
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_IOC,
HELP_MSG_LIST_MODULES, HELP_MSG_MODULE,
HELP_MSG_OUTPUT, HELP_MSG_SERIAL)
from mvt.common.logo import logo
from mvt.common.updates import IndicatorsUpdates
from .cmd_check_adb import CmdAndroidCheckADB
from .cmd_check_backup import CmdAndroidCheckBackup
from .cmd_check_bugreport import CmdAndroidCheckBugreport
from .cmd_download_apks import DownloadAPKs
from .modules.adb import ADB_MODULES
from .modules.adb.packages import Packages
from .modules.backup import BACKUP_MODULES
from .modules.bugreport import BUGREPORT_MODULES
# Setup logging using Rich.
LOG_FORMAT = "[%(name)s] %(message)s"
logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__)
#==============================================================================
# Main
#==============================================================================
@click.group(invoke_without_command=False)
def cli():
logo()
#==============================================================================
# Command: version
#==============================================================================
@cli.command("version", help="Show the currently installed version of MVT")
def version():
return
#==============================================================================
# Command: download-apks
#==============================================================================
@cli.command("download-apks", help="Download all or only non-system installed APKs")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--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.pass_context
def download_apks(ctx, all_apks, virustotal, output, from_file, serial):
try:
if from_file:
download = DownloadAPKs.from_json(from_file)
else:
# TODO: Do we actually want to be able to run without storing any file?
if not output:
log.critical("You need to specify an output folder with --output!")
ctx.exit(1)
if not os.path.exists(output):
try:
os.makedirs(output)
except Exception as e:
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
download = DownloadAPKs(output_folder=output, all_apks=all_apks,
log=logging.getLogger(DownloadAPKs.__module__))
if serial:
download.serial = serial
download.run()
packages_to_lookup = []
if all_apks:
packages_to_lookup = download.packages
else:
for package in download.packages:
if not package.get("system", False):
packages_to_lookup.append(package)
if len(packages_to_lookup) == 0:
return
if virustotal:
m = Packages()
m.check_virustotal(packages_to_lookup)
except KeyboardInterrupt:
print("")
ctx.exit(1)
#==============================================================================
# Command: check-adb
#==============================================================================
@cli.command("check-adb", help="Check an Android device over adb")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False),
help=HELP_MSG_OUTPUT)
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.pass_context
def check_adb(ctx, serial, iocs, output, fast, list_modules, module):
cmd = CmdAndroidCheckADB(results_path=output, ioc_files=iocs,
module_name=module, serial=serial, fast_mode=fast)
if list_modules:
cmd.list_modules()
return
log.info("Checking Android device over debug bridge")
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the Android device produced %d detections!",
len(cmd.timeline_detected))
#==============================================================================
# Command: check-bugreport
#==============================================================================
@cli.command("check-bugreport", help="Check an Android Bug Report")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
@click.pass_context
def check_bugreport(ctx, iocs, output, list_modules, module, bugreport_path):
cmd = CmdAndroidCheckBugreport(target_path=bugreport_path, results_path=output,
ioc_files=iocs, module_name=module)
if list_modules:
cmd.list_modules()
return
log.info("Checking Android bug report at path: %s", bugreport_path)
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the Android bug report produced %d detections!",
len(cmd.timeline_detected))
#==============================================================================
# Command: check-backup
#==============================================================================
@cli.command("check-backup", help="Check an Android Backup")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(ctx, serial, iocs, output, list_modules, backup_path):
cmd = CmdAndroidCheckBackup(target_path=backup_path, results_path=output,
ioc_files=iocs)
if list_modules:
cmd.list_modules()
return
log.info("Checking Android backup at path: %s", backup_path)
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the Android backup produced %d detections!",
len(cmd.timeline_detected))
#==============================================================================
# Command: check-iocs
#==============================================================================
@cli.command("check-iocs", help="Compare stored JSON results to provided indicators")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@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)
cmd.modules = BACKUP_MODULES + ADB_MODULES + BUGREPORT_MODULES
if list_modules:
cmd.list_modules()
return
cmd.run()
#==============================================================================
# Command: download-iocs
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
def download_indicators():
ioc_updates = IndicatorsUpdates()
ioc_updates.update()

View File

@@ -0,0 +1,25 @@
# 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 logging
from mvt.common.command import Command
from .modules.adb import ADB_MODULES
log = logging.getLogger(__name__)
class CmdAndroidCheckADB(Command):
name = "check-adb"
modules = ADB_MODULES
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)

View File

@@ -0,0 +1,84 @@
# 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 io
import logging
import os
import sys
import tarfile
from pathlib import Path
from rich.prompt import Prompt
from mvt.android.parsers.backup import (AndroidBackupParsingError,
InvalidBackupPassword, parse_ab_header,
parse_backup_file)
from mvt.common.command import Command
from .modules.backup import BACKUP_MODULES
log = logging.getLogger(__name__)
class CmdAndroidCheckBackup(Command):
name = "check-backup"
modules = BACKUP_MODULES
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
self.backup_type = None
self.backup_archive = None
self.backup_files = []
def init(self):
if os.path.isfile(self.target_path):
self.backup_type = "ab"
with open(self.target_path, "rb") as handle:
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.ask("Enter backup password", password=True)
try:
tardata = parse_backup_file(data, password=password)
except InvalidBackupPassword:
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as e:
log.critical("Impossible to parse this backup file: %s", e)
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"
self.target_path = Path(self.target_path).absolute().as_posix()
for root, subdirs, subfiles in os.walk(os.path.abspath(self.target_path)):
for fname in subfiles:
self.backup_files.append(os.path.relpath(os.path.join(root, fname), self.target_path))
else:
log.critical("Invalid backup path, path should be a folder or an Android Backup (.ab) file")
sys.exit(1)
def module_init(self, module):
if self.backup_type == "folder":
module.from_folder(self.target_path, self.backup_files)
else:
module.from_ab(self.target_path, self.backup_archive, self.backup_files)

View File

@@ -0,0 +1,51 @@
# 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 logging
import os
from pathlib import Path
from zipfile import ZipFile
from mvt.common.command import Command
from .modules.bugreport import BUGREPORT_MODULES
log = logging.getLogger(__name__)
class CmdAndroidCheckBugreport(Command):
name = "check-bugreport"
modules = BUGREPORT_MODULES
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
self.bugreport_format = None
self.bugreport_archive = None
self.bugreport_files = []
def init(self):
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, subdirs, subfiles in os.walk(os.path.abspath(self.target_path)):
for file_name in subfiles:
self.bugreport_files.append(os.path.relpath(os.path.join(root, file_name), parent_path))
def module_init(self, module):
if self.bugreport_format == "zip":
module.from_zip(self.bugreport_archive, self.bugreport_files)
else:
module.from_folder(self.target_path, self.bugreport_files)

View File

@@ -1,14 +1,13 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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 json
import logging
import os
from typing import Callable, Optional, Union
from rich.progress import track
from tqdm import tqdm
from mvt.common.module import InsufficientPrivileges
@@ -18,31 +17,42 @@ from .modules.adb.packages import Packages
log = logging.getLogger(__name__)
# TODO: Would be better to replace tqdm with rich.progress to reduce
# the number of dependencies. Need to investigate whether
# it's possible to have a similar callback system.
class PullProgress(tqdm):
"""PullProgress is a tqdm update system for APK downloads."""
def update_to(self, file_name, current, total):
if total is not None:
self.total = total
self.update(current - self.n)
class DownloadAPKs(AndroidExtraction):
"""DownloadAPKs is the main class operating the download of APKs
from the device.
"""
def __init__(
self,
results_path: Optional[str] = None,
all_apks: bool = False,
packages: Optional[list] = None,
) -> None:
def __init__(self, output_folder=None, all_apks=False, log=None,
packages=None):
"""Initialize module.
:param results_path: Path to the folder where data should be stored
:param output_folder: Path to the folder where data should be stored
:param all_apks: Boolean indicating whether to download all packages
or filter known-goods
:param packages: Provided list of packages, typically for JSON checks
"""
super().__init__(results_path=results_path, log=log)
super().__init__(log=log)
self.packages = packages
self.all_apks = all_apks
self.results_path_apks = None
self.output_folder_apk = None
self.output_folder = output_folder
@classmethod
def from_json(cls, json_path: str) -> Callable:
def from_json(cls, json_path):
"""Initialize this class from an existing apks.json file.
:param json_path: Path to the apks.json file to parse.
@@ -52,9 +62,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, remote_path):
"""Pull files related to specific package from the device.
:param package_name: Name of the package to download
@@ -68,39 +76,40 @@ class DownloadAPKs(AndroidExtraction):
if "==/" in remote_path:
file_name = "_" + remote_path.split("==/")[1].replace(".apk", "")
local_path = os.path.join(
self.results_path_apks, f"{package_name}{file_name}.apk"
)
local_path = os.path.join(self.output_folder_apk,
f"{package_name}{file_name}.apk")
name_counter = 0
while True:
if not os.path.exists(local_path):
break
name_counter += 1
local_path = os.path.join(
self.results_path_apks, f"{package_name}{file_name}_{name_counter}.apk"
)
local_path = os.path.join(self.output_folder_apk,
f"{package_name}{file_name}_{name_counter}.apk")
try:
self._adb_download(remote_path, local_path)
with PullProgress(unit='B', unit_divisor=1024, unit_scale=True,
miniters=1) as pp:
self._adb_download(remote_path, local_path,
progress_callback=pp.update_to)
except InsufficientPrivileges:
log.error(
"Unable to pull package file from %s: insufficient privileges, "
"it might be a system app",
remote_path,
)
log.warn("Unable to pull package file from %s: insufficient privileges, it might be a system app",
remote_path)
self._adb_reconnect()
return None
except Exception as exc:
log.exception("Failed to pull package file from %s: %s", remote_path, exc)
except Exception as e:
log.exception("Failed to pull package file from %s: %s",
remote_path, e)
self._adb_reconnect()
return None
return local_path
def get_packages(self) -> None:
def get_packages(self):
"""Use the Packages adb module to retrieve the list of packages.
We reuse the same extraction logic to then download the APKs.
"""
self.log.info("Retrieving list of installed packages...")
@@ -111,11 +120,12 @@ class DownloadAPKs(AndroidExtraction):
self.packages = m.results
def pull_packages(self) -> None:
def pull_packages(self):
"""Download all files of all selected packages from the device."""
log.info(
"Starting extraction of installed APKs at folder %s", self.results_path
)
log.info("Starting extraction of installed APKs at folder %s", self.output_folder)
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
# If the user provided the flag --all-apks we select all packages.
packages_selection = []
@@ -129,10 +139,8 @@ class DownloadAPKs(AndroidExtraction):
if not package.get("system", False):
packages_selection.append(package)
log.info(
'Selected only %d packages which are not marked as "system"',
len(packages_selection),
)
log.info("Selected only %d packages which are not marked as system",
len(packages_selection))
if len(packages_selection) == 0:
log.info("No packages were selected for download")
@@ -140,30 +148,23 @@ class DownloadAPKs(AndroidExtraction):
log.info("Downloading packages from device. This might take some time ...")
self.results_path_apks = os.path.join(self.results_path, "apks")
if not os.path.exists(self.results_path_apks):
os.makedirs(self.results_path_apks, exist_ok=True)
self.output_folder_apk = os.path.join(self.output_folder, "apks")
if not os.path.exists(self.output_folder_apk):
os.mkdir(self.output_folder_apk)
for i in track(
range(len(packages_selection)),
description=f"Downloading {len(packages_selection)} packages...",
):
package = packages_selection[i]
counter = 0
for package in packages_selection:
counter += 1
log.info(
"[%d/%d] Package: %s",
i,
len(packages_selection),
package["package_name"],
)
log.info("[%d/%d] Package: %s", counter, len(packages_selection),
package["package_name"])
# Sometimes the package path contains multiple lines for multiple
# apks. We loop through each line and download each file.
# Sometimes the package path contains multiple lines for multiple apks.
# We loop through each line and download each file.
for package_file in package["files"]:
device_path = package_file["path"]
local_path = self.pull_package_file(
package["package_name"], device_path
)
local_path = self.pull_package_file(package["package_name"],
device_path)
if not local_path:
continue
@@ -171,12 +172,14 @@ class DownloadAPKs(AndroidExtraction):
log.info("Download of selected packages completed")
def save_json(self) -> None:
json_path = os.path.join(self.results_path, "apks.json")
def save_json(self):
"""Save the results to the package.json file."""
json_path = os.path.join(self.output_folder, "apks.json")
with open(json_path, "w", encoding="utf-8") as handle:
json.dump(self.packages, handle, indent=4)
def run(self) -> None:
"""Run all steps of fetch-apk."""
self.get_packages()
self._adb_connect()
self.pull_packages()

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/
@@ -10,7 +10,6 @@ from .dumpsys_appops import DumpsysAppOps
from .dumpsys_battery_daily import DumpsysBatteryDaily
from .dumpsys_battery_history import DumpsysBatteryHistory
from .dumpsys_dbinfo import DumpsysDBInfo
from .dumpsys_adbstate import DumpsysADBState
from .dumpsys_full import DumpsysFull
from .dumpsys_receivers import DumpsysReceivers
from .files import Files
@@ -24,25 +23,8 @@ from .settings import Settings
from .sms import SMS
from .whatsapp import Whatsapp
ADB_MODULES = [
ChromeHistory,
SMS,
Whatsapp,
Processes,
Getprop,
Settings,
SELinuxStatus,
DumpsysBatteryHistory,
DumpsysBatteryDaily,
DumpsysReceivers,
DumpsysActivities,
DumpsysAccessibility,
DumpsysDBInfo,
DumpsysADBState,
DumpsysFull,
DumpsysAppOps,
Packages,
Logcat,
RootBinaries,
Files,
]
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes, Getprop, Settings,
SELinuxStatus, DumpsysBatteryHistory, DumpsysBatteryDaily,
DumpsysReceivers, DumpsysActivities, DumpsysAccessibility,
DumpsysDBInfo, DumpsysFull, DumpsysAppOps, Packages, Logcat,
RootBinaries, Files]

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/
@@ -11,27 +11,22 @@ import string
import sys
import tempfile
import time
from typing import Callable, Optional
from typing import Callable
from adb_shell.adb_device import AdbDeviceTcp, AdbDeviceUsb
from adb_shell.auth.keygen import keygen, write_public_keyfile
from adb_shell.auth.sign_pythonrsa import PythonRSASigner
from adb_shell.exceptions import (
AdbCommandFailureException,
DeviceAuthError,
UsbDeviceNotFoundError,
UsbReadFailedError,
)
from adb_shell.exceptions import (AdbCommandFailureException, DeviceAuthError,
UsbDeviceNotFoundError, UsbReadFailedError)
from rich.prompt import Prompt
from usb1 import USBErrorAccess, USBErrorBusy
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.android.parsers.backup import (
InvalidBackupPassword,
parse_ab_header,
parse_backup_file,
)
from mvt.android.parsers.backup import (InvalidBackupPassword, parse_ab_header,
parse_backup_file)
from mvt.common.module import InsufficientPrivileges, MVTModule
log = logging.getLogger(__name__)
ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey")
ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub")
@@ -39,23 +34,12 @@ ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub")
class AndroidExtraction(MVTModule):
"""This class provides a base for all Android extraction modules."""
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 __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.device = None
self.serial = None
@@ -90,49 +74,33 @@ class AndroidExtraction(MVTModule):
try:
self.device = AdbDeviceUsb(serial=self.serial)
except UsbDeviceNotFoundError:
self.log.critical(
"No device found. Make sure it is connected and unlocked."
)
log.critical("No device found. Make sure it is connected and unlocked.")
sys.exit(-1)
# Otherwise we try to use the TCP transport.
else:
addr = self.serial.split(":")
if len(addr) < 2:
raise ValueError(
"TCP serial number must follow the format: `address:port`"
)
raise ValueError("TCP serial number must follow the format: `address:port`")
self.device = AdbDeviceTcp(
addr[0], int(addr[1]), default_transport_timeout_s=30.0
)
self.device = AdbDeviceTcp(addr[0], int(addr[1]),
default_transport_timeout_s=30.)
while True:
try:
self.device.connect(rsa_keys=[signer], auth_timeout_s=5)
except (USBErrorBusy, USBErrorAccess):
self.log.critical(
"Device is busy, maybe run `adb kill-server` and try again."
)
log.critical("Device is busy, maybe run `adb kill-server` and try again.")
sys.exit(-1)
except DeviceAuthError:
self.log.error(
"You need to authorize this computer on the Android device. "
"Retrying in 5 seconds..."
)
log.error("You need to authorize this computer on the Android device. Retrying in 5 seconds...")
time.sleep(5)
except UsbReadFailedError:
self.log.error(
"Unable to connect to the device over USB. "
"Try to unplug, plug the device and start again."
)
log.error("Unable to connect to the device over USB. Try to unplug, plug the device and start again.")
sys.exit(-1)
except OSError as exc:
if exc.errno == 113 and self.serial:
self.log.critical(
"Unable to connect to the device %s: "
"did you specify the correct IP address?",
self.serial,
)
except OSError as e:
if e.errno == 113 and self.serial:
log.critical("Unable to connect to the device %s: did you specify the correct IP addres?",
self.serial)
sys.exit(-1)
else:
break
@@ -143,18 +111,18 @@ class AndroidExtraction(MVTModule):
def _adb_reconnect(self) -> None:
"""Reconnect to device using adb."""
self.log.info("Reconnecting ...")
log.info("Reconnecting ...")
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.
@@ -163,17 +131,12 @@ class AndroidExtraction(MVTModule):
:returns: Boolean indicating whether a `su` binary is present or not
"""
result = self._adb_command("command -v su && su -c true")
return bool(result) and "Permission denied" not in result
return bool(self._adb_command("command -v su"))
def _adb_root_or_die(self) -> None:
"""Check if we have a `su` binary, otherwise raise an Exception."""
if not self._adb_check_if_root():
raise InsufficientPrivileges(
"This module is optionally available "
"in case the device is already rooted."
" Do NOT root your own device!"
)
raise InsufficientPrivileges("This module is optionally available in case the device is already rooted. Do NOT root your own device!")
def _adb_command_as_root(self, command):
"""Execute an adb shell command.
@@ -194,81 +157,63 @@ class AndroidExtraction(MVTModule):
# TODO: Need to support checking files without root privileges as well.
# Connect to the device over adb.
self._adb_connect()
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
return bool(self._adb_command_as_root(f"[ ! -f {file} ] || echo 1"))
def _adb_download(
self,
remote_path: str,
local_path: str,
progress_callback: Optional[Callable] = None,
retry_root: Optional[bool] = True,
) -> None:
def _adb_download(self, remote_path: str, local_path: str,
progress_callback: Callable = None,
retry_root: bool = True) -> None:
"""Download a file form the device.
:param remote_path: Path to download from the device
:param local_path: Path to where to locally store the copy of the file
:param progress_callback: Callback for download progress bar
(Default value = None)
:param progress_callback: Callback for download progress bar (Default value = None)
:param retry_root: Default value = True)
"""
try:
self.device.pull(remote_path, local_path, progress_callback)
except AdbCommandFailureException as exc:
except AdbCommandFailureException as e:
if retry_root:
self._adb_download_root(remote_path, local_path, progress_callback)
else:
raise Exception(
f"Unable to download file {remote_path}: {exc}"
) from exc
raise Exception(f"Unable to download file {remote_path}: {e}")
def _adb_download_root(
self,
remote_path: str,
local_path: str,
progress_callback: Optional[Callable] = None,
) -> None:
def _adb_download_root(self, remote_path: str, local_path: str,
progress_callback: Callable = None) -> None:
try:
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
# We generate a random temporary filename.
allowed_chars = (
string.ascii_uppercase + string.ascii_lowercase + string.digits
)
tmp_filename = "tmp_" + "".join(random.choices(allowed_chars, k=10))
tmp_filename = "tmp_" + ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=10))
# We create a temporary local file.
new_remote_path = f"/sdcard/{tmp_filename}"
# We copy the file from the data folder to /sdcard/.
cp_output = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if (
cp_output.startswith("cp: ")
and "No such file or directory" in cp_output
):
cp = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if cp.startswith("cp: ") and "No such file or directory" in cp:
raise Exception(f"Unable to process file {remote_path}: File not found")
if cp_output.startswith("cp: ") and "Permission denied" in cp_output:
raise Exception(
f"Unable to process file {remote_path}: Permission denied"
)
elif cp.startswith("cp: ") and "Permission denied" in cp:
raise Exception(f"Unable to process file {remote_path}: Permission denied")
# We download from /sdcard/ to the local temporary file.
# If it doesn't work now, don't try again (retry_root=False)
self._adb_download(
new_remote_path, local_path, progress_callback, retry_root=False
)
self._adb_download(new_remote_path, local_path, retry_root=False)
# Delete the copy on /sdcard/.
self._adb_command(f"rm -rf {new_remote_path}")
except AdbCommandFailureException as exc:
raise Exception(f"Unable to download file {remote_path}: {exc}") from exc
except AdbCommandFailureException as e:
raise Exception(f"Unable to download file {remote_path}: {e}")
def _adb_process_file(self, remote_path: str, process_routine: Callable) -> None:
def _adb_process_file(self, remote_path: str,
process_routine: Callable) -> None:
"""Download a local copy of a file which is only accessible as root.
This is a wrapper around process_routine.
@@ -278,6 +223,7 @@ class AndroidExtraction(MVTModule):
"""
# Connect to the device over adb.
self._adb_connect()
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
@@ -288,10 +234,10 @@ class AndroidExtraction(MVTModule):
new_remote_path = f"/sdcard/Download/{local_name}"
# We copy the file from the data folder to /sdcard/.
cp_output = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if cp_output.startswith("cp: ") and "No such file or directory" in cp_output:
cp = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if cp.startswith("cp: ") and "No such file or directory" in cp:
raise Exception(f"Unable to process file {remote_path}: File not found")
if cp_output.startswith("cp: ") and "Permission denied" in cp_output:
elif cp.startswith("cp: ") and "Permission denied" in cp:
raise Exception(f"Unable to process file {remote_path}: Permission denied")
# We download from /sdcard/ to the local temporary file.
@@ -304,52 +250,34 @@ class AndroidExtraction(MVTModule):
tmp.close()
# Delete the copy on /sdcard/.
self._adb_command(f"rm -f {new_remote_path}")
# Disconnect from the device.
self._adb_disconnect()
def _generate_backup(self, package_name: str) -> bytes:
self.log.info(
"Please check phone and accept Android backup prompt. "
"You may need to set a backup password. \a"
)
self.log.warning("Please check phone and accept Android backup prompt. You may need to set a backup password. \a")
if self.module_options.get("backup_password", None):
self.log.warning(
"Backup password already set from command line or environment "
"variable. You should use the same password if enabling encryption!"
)
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over
# the shell transport...
cmd = f"/system/bin/bu backup -nocompress '{package_name}' | base64"
backup_output_b64 = self._adb_command(cmd)
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over the shell transport...
backup_output_b64 = self._adb_command("/system/bin/bu backup -nocompress '{}' | base64".format(
package_name))
backup_output = base64.b64decode(backup_output_b64)
header = parse_ab_header(backup_output)
if not header["backup"]:
self.log.error(
"Extracting SMS via Android backup failed. "
"No valid backup data found."
)
return None
self.log.error("Extracting SMS via Android backup failed. No valid backup data found.")
return
if header["encryption"] == "none":
return parse_backup_file(backup_output, password=None)
for _ in range(0, 3):
backup_password = prompt_or_load_android_backup_password(
self.log, self.module_options
)
if not backup_password:
# Fail as no backup password loaded for this encrypted backup
self.log.critical("No backup password provided.")
for password_retry in range(0, 3):
backup_password = Prompt.ask("Enter backup password", password=True)
try:
decrypted_backup_tar = parse_backup_file(backup_output, backup_password)
return decrypted_backup_tar
except InvalidBackupPassword:
self.log.error("You provided the wrong password! Please try again...")
self.log.error("All attempts to decrypt backup with password failed!")
return None
self.log.warn("All attempts to decrypt backup with password failed!")
def run(self) -> None:
"""Run the main procedure."""

View File

@@ -0,0 +1,86 @@
# 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 logging
import os
import sqlite3
from mvt.common.utils import (convert_chrometime_to_unix,
convert_timestamp_to_iso)
from .base import AndroidExtraction
log = logging.getLogger(__name__)
CHROME_HISTORY_PATH = "data/data/com.android.chrome/app_chrome/Default/History"
class ChromeHistory(AndroidExtraction):
"""This module extracts records from Android's Chrome browsing history."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record: dict) -> None:
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "visit",
"data": f"{record['id']} - {record['url']} (visit ID: {record['visit_id']}, redirect source: {record['redirect_source']})"
}
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
if self.indicators.check_domain(result["url"]):
self.detected.append(result)
def _parse_db(self, db_path: str) -> None:
"""Parse a Chrome History database file.
:param db_path: Path to the History database to process.
"""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("""
SELECT
urls.id,
urls.url,
visits.id,
visits.visit_time,
visits.from_visit
FROM urls
JOIN visits ON visits.url = urls.id
ORDER BY visits.visit_time;
""")
for item in cur:
self.results.append({
"id": item[0],
"url": item[1],
"visit_id": item[2],
"timestamp": item[3],
"isodate": convert_timestamp_to_iso(convert_chrometime_to_unix(item[3])),
"redirect_source": item[4],
})
cur.close()
conn.close()
log.info("Extracted a total of %d history items", len(self.results))
def run(self) -> None:
try:
self._adb_process_file(os.path.join("/", CHROME_HISTORY_PATH),
self._parse_db)
except Exception as e:
self.log.error(e)

View File

@@ -0,0 +1,46 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_accessibility
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysAccessibility(AndroidExtraction):
"""This module extracts stats on accessibility."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys accessibility")
self._adb_disconnect()
self.results = parse_dumpsys_accessibility(output)
for result in self.results:
log.info("Found installed accessibility service \"%s\"", result.get("service"))
self.log.info("Identified a total of %d accessibility services", len(self.results))

View File

@@ -0,0 +1,46 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_activity_resolver_table
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysActivities(AndroidExtraction):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = results if results else {}
def check_indicators(self) -> None:
if not self.indicators:
return
for intent, activities in self.results.items():
for activity in activities:
ioc = self.indicators.check_app_id(activity["package_name"])
if ioc:
activity["matched_indicator"] = ioc
self.detected.append({intent: activity})
continue
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys package")
self._adb_disconnect()
self.results = parse_dumpsys_activity_resolver_table(output)
self.log.info("Extracted activities for %d intents", len(self.results))

View File

@@ -0,0 +1,66 @@
# 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 logging
from mvt.android.parsers.dumpsys import parse_dumpsys_appops
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysAppOps(AndroidExtraction):
"""This module extracts records from App-op Manager."""
slug = "dumpsys_appops"
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record: dict) -> None:
records = []
for perm in record["permissions"]:
if "entries" not in perm:
continue
for entry in perm["entries"]:
if "timestamp" in entry:
records.append({
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to {perm['name']}: {entry['access']}",
})
return records
def check_indicators(self) -> None:
for result in self.results:
if self.indicators:
ioc = self.indicators.check_app_id(result.get("package_name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
for perm in result["permissions"]:
if perm["name"] == "REQUEST_INSTALL_PACKAGES" and perm["access"] == "allow":
self.log.info("Package %s with REQUEST_INSTALL_PACKAGES permission",
result["package_name"])
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys appops")
self._adb_disconnect()
self.results = parse_dumpsys_appops(output)
self.log.info("Extracted a total of %d records from app-ops manager",
len(self.results))

View File

@@ -0,0 +1,51 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_battery_daily
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysBatteryDaily(AndroidExtraction):
"""This module extracts records from battery daily updates."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record: dict) -> None:
return {
"timestamp": record["from"],
"module": self.__class__.__name__,
"event": "battery_daily",
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
}
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys batterystats --daily")
self._adb_disconnect()
self.results = parse_dumpsys_battery_daily(output)
self.log.info("Extracted %d records from battery daily stats", len(self.results))

View File

@@ -0,0 +1,43 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_battery_history
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysBatteryHistory(AndroidExtraction):
"""This module extracts records from battery history events."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys batterystats --history")
self._adb_disconnect()
self.results = parse_dumpsys_battery_history(output)
self.log.info("Extracted %d records from battery history", len(self.results))

View File

@@ -0,0 +1,48 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_dbinfo
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysDBInfo(AndroidExtraction):
"""This module extracts records from battery daily updates."""
slug = "dumpsys_dbinfo"
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
path = result.get("path", "")
for part in path.split("/"):
ioc = self.indicators.check_app_id(part)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys dbinfo")
self._adb_disconnect()
self.results = parse_dumpsys_dbinfo(output)
self.log.info("Extracted a total of %d records from database information",
len(self.results))

View File

@@ -0,0 +1,35 @@
# 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 logging
import os
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysFull(AndroidExtraction):
"""This module extracts stats on battery consumption by processes."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys")
if self.results_path:
output_path = os.path.join(self.results_path, "dumpsys.txt")
with open(output_path, "w", encoding="utf-8") as handle:
handle.write(output)
log.info("Full dumpsys output stored at %s", output_path)
self._adb_disconnect()

View File

@@ -0,0 +1,67 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_receiver_resolver_table
from .base import AndroidExtraction
log = logging.getLogger(__name__)
INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
INTENT_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE"
INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
class DumpsysReceivers(AndroidExtraction):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = results if results else {}
def check_indicators(self) -> None:
if not self.indicators:
return
for intent, receivers in self.results.items():
for receiver in receivers:
if intent == INTENT_NEW_OUTGOING_SMS:
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
receiver["receiver"])
elif intent == INTENT_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
receiver["receiver"])
elif intent == INTENT_DATA_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
receiver["receiver"])
elif intent == INTENT_PHONE_STATE:
self.log.info("Found a receiver monitoring telephony state/incoming calls: \"%s\"",
receiver["receiver"])
elif intent == INTENT_NEW_OUTGOING_CALL:
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
receiver["receiver"])
ioc = self.indicators.check_app_id(receiver["package_name"])
if ioc:
receiver["matched_indicator"] = ioc
self.detected.append({intent: receiver})
continue
def run(self) -> None:
self._adb_connect()
output = self._adb_command("dumpsys package")
self.results = parse_dumpsys_receiver_resolver_table(output)
self._adb_disconnect()

View File

@@ -0,0 +1,98 @@
# 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 datetime
import logging
import stat
from mvt.common.utils import convert_timestamp_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Files(AndroidExtraction):
"""This module extracts the list of files on the device."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.full_find = False
def find_files(self, folder: str) -> None:
if self.full_find:
output = self._adb_command(f"find '{folder}' -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
for file_line in output.splitlines():
[unix_timestamp, mode, size, owner, group, full_path] = file_line.rstrip().split(" ", 5)
mod_time = convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(int(float(unix_timestamp))))
self.results.append({
"path": full_path,
"modified_time": mod_time,
"mode": mode,
"is_suid": (int(mode, 8) & stat.S_ISUID) == 2048,
"is_sgid": (int(mode, 8) & stat.S_ISGID) == 1024,
"size": size,
"owner": owner,
"group": group,
})
else:
output = self._adb_command(f"find '{folder}' 2> /dev/null")
for file_line in output.splitlines():
self.results.append({"path": file_line.rstrip()})
def serialize(self, record: dict) -> None:
if "modified_time" in record:
return {
"timestamp": record["modified_time"],
"module": self.__class__.__name__,
"event": "file_modified",
"data": record["path"],
}
def check_suspicious(self) -> None:
"""Check for files with suspicious permissions"""
for result in sorted(self.results, key=lambda item: item["path"]):
if result.get("is_suid"):
self.log.warning("Found an SUID file in a non-standard directory \"%s\".",
result["path"])
self.detected.append(result)
def check_indicators(self) -> None:
"""Check file list for known suspicious files or suspicious properties"""
self.check_suspicious()
if not self.indicators:
return
for result in self.results:
if self.indicators.check_file_path(result["path"]):
self.log.warning("Found a known suspicous file at path: \"%s\"", result["path"])
self.detected.append(result)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
if output or output.strip().splitlines():
self.full_find = True
for data_path in ["/data/local/tmp/", "/sdcard/", "/tmp/"]:
self.find_files(data_path)
self.log.info("Found %s files in primary Android data directories", len(self.results))
if self.fast_mode:
self.log.info("Flag --fast was enabled: skipping full file listing")
else:
self.log.info("Processing full file listing. This may take a while...")
self.find_files("/")
self.log.info("Found %s total files", len(self.results))
self._adb_disconnect()

View File

@@ -0,0 +1,43 @@
# 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 logging
from datetime import datetime, timedelta
from mvt.android.parsers import parse_getprop
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Getprop(AndroidExtraction):
"""This module extracts device properties from getprop command."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = {} if not results else results
def run(self) -> None:
self._adb_connect()
output = self._adb_command("getprop")
self._adb_disconnect()
self.results = parse_getprop(output)
# Alert if phone is outdated.
security_patch = self.results.get("ro.build.version.security_patch", "")
if security_patch:
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
if (datetime.now() - patch_date) > timedelta(days=6*30):
self.log.warning("This phone has not received security updates for more than "
"six months (last update: %s)", security_patch)
self.log.info("Extracted %d Android system properties", len(self.results))

View File

@@ -0,0 +1,49 @@
# 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 logging
import os
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Logcat(AndroidExtraction):
"""This module extracts details on installed packages."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def run(self) -> None:
self._adb_connect()
# Get the current logcat.
output = self._adb_command("logcat -d")
# Get the locat prior to last reboot.
last_output = self._adb_command("logcat -L")
if self.results_path:
logcat_path = os.path.join(self.results_path,
"logcat.txt")
with open(logcat_path, "w", encoding="utf-8") as handle:
handle.write(output)
log.info("Current logcat logs stored at %s",
logcat_path)
logcat_last_path = os.path.join(self.results_path,
"logcat_last.txt")
with open(logcat_last_path, "w", encoding="utf-8") as handle:
handle.write(last_output)
log.info("Logcat logs prior to last reboot stored at %s",
logcat_last_path)
self._adb_disconnect()

View File

@@ -1,98 +1,111 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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 logging
from typing import 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.common.virustotal import VTNoKey, VTQuotaExceeded, virustotal_lookup
from .base import AndroidExtraction
log = logging.getLogger(__name__)
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 = [
"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",
]
class Packages(AndroidExtraction):
"""This module extracts the list of installed packages."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: 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._user_needed = False
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record: dict) -> Union[dict, list]:
def serialize(self, record: dict) -> None:
records = []
timestamps = [
{"event": "package_install", "timestamp": record["timestamp"]},
{
"event": "package_first_install",
"timestamp": record["first_install_time"],
},
{"event": "package_first_install", "timestamp": record["first_install_time"]},
{"event": "package_last_update", "timestamp": record["last_update_time"]},
]
for timestamp in timestamps:
records.append(
{
"timestamp": timestamp["timestamp"],
"module": self.__class__.__name__,
"event": timestamp["event"],
"data": f"{record['package_name']} (system: {record['system']},"
f" third party: {record['third_party']})",
}
)
for ts in timestamps:
records.append({
"timestamp": ts["timestamp"],
"module": self.__class__.__name__,
"event": ts["event"],
"data": f"{record['package_name']} (system: {record['system']}, third party: {record['third_party']})",
})
return records
def check_indicators(self) -> None:
for result in self.results:
if result["package_name"] in ROOT_PACKAGES:
self.log.warning(
"Found an installed package related to "
'rooting/jailbreaking: "%s"',
result["package_name"],
)
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
result["package_name"])
self.detected.append(result)
continue
if result["package_name"] in SECURITY_PACKAGES and result["disabled"]:
self.log.warning(
'Found a security package disabled: "%s"', result["package_name"]
)
if result["package_name"] in SYSTEM_UPDATE_PACKAGES and result["disabled"]:
self.log.warning(
'System OTA update package "%s" disabled on the phone',
result["package_name"],
)
if not self.indicators:
continue
@@ -119,14 +132,14 @@ class Packages(AndroidExtraction):
total_hashes = len(hashes)
detections = {}
progress_desc = f"Looking up {total_hashes} files..."
for i in track(range(total_hashes), description=progress_desc):
for i in track(range(total_hashes), description=f"Looking up {total_hashes} files..."):
try:
results = virustotal_lookup(hashes[i])
except VTNoKey:
except VTNoKey as e:
log.info(e)
return
except VTQuotaExceeded as exc:
print("Unable to continue: %s", exc)
except VTQuotaExceeded as e:
log.error("Unable to continue: %s", e)
break
if not results:
@@ -163,25 +176,46 @@ class Packages(AndroidExtraction):
@staticmethod
def parse_package_for_details(output: str) -> dict:
lines = []
in_packages = False
for line in output.splitlines():
if in_packages:
if line.strip() == "":
break
lines.append(line)
if line.strip() == "Packages:":
in_packages = True
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"requested_permissions": [],
}
return DumpsysPackagesArtifact.parse_dumpsys_package_for_details(
"\n".join(lines)
)
in_permissions = False
for line in output.splitlines():
if in_permissions:
if line.startswith(" " * 4) and not line.startswith(" " * 6):
in_permissions = False
continue
permission = line.strip().split(":")[0]
details["requested_permissions"].append(permission)
if line.strip().startswith("userId="):
details["uid"] = line.split("=")[1].strip()
elif line.strip().startswith("versionName="):
details["version_name"] = line.split("=")[1].strip()
elif line.strip().startswith("versionCode="):
details["version_code"] = line.split("=", 1)[1].strip()
elif line.strip().startswith("timeStamp="):
details["timestamp"] = line.split("=")[1].strip()
elif line.strip().startswith("firstInstallTime="):
details["first_install_time"] = line.split("=")[1].strip()
elif line.strip().startswith("lastUpdateTime="):
details["last_update_time"] = line.split("=")[1].strip()
elif line.strip() == "requested permissions:":
in_permissions = True
continue
return details
def _get_files_for_package(self, package_name: str) -> list:
command = f"pm path {package_name}"
if self._user_needed:
command += " --user 0"
output = self._adb_command(command)
output = self._adb_command(f"pm path {package_name}")
output = output.strip().replace("package:", "")
if not output:
return []
@@ -190,24 +224,18 @@ class Packages(AndroidExtraction):
for file_path in output.splitlines():
file_path = file_path.strip()
md5 = self._adb_command(f"md5sum {file_path}").split(" ", maxsplit=1)[0]
sha1 = self._adb_command(f"sha1sum {file_path}").split(" ", maxsplit=1)[0]
sha256 = self._adb_command(f"sha256sum {file_path}").split(" ", maxsplit=1)[
0
]
sha512 = self._adb_command(f"sha512sum {file_path}").split(" ", maxsplit=1)[
0
]
md5 = self._adb_command(f"md5sum {file_path}").split(" ")[0]
sha1 = self._adb_command(f"sha1sum {file_path}").split(" ")[0]
sha256 = self._adb_command(f"sha256sum {file_path}").split(" ")[0]
sha512 = self._adb_command(f"sha512sum {file_path}").split(" ")[0]
package_files.append(
{
"path": file_path,
"md5": md5,
"sha1": sha1,
"sha256": sha256,
"sha512": sha512,
}
)
package_files.append({
"path": file_path,
"md5": md5,
"sha1": sha1,
"sha256": sha256,
"sha512": sha512,
})
return package_files
@@ -215,9 +243,6 @@ class Packages(AndroidExtraction):
self._adb_connect()
packages = self._adb_command("pm list packages -u -i -f")
if "java.lang.SecurityException" in packages or packages.strip() == "":
self._user_needed = True
packages = self._adb_command("pm list packages -u -i -f --user 0")
for line in packages.splitlines():
line = line.strip()
@@ -258,10 +283,7 @@ class Packages(AndroidExtraction):
{"field": "third_party", "arg": "-3"},
]
for cmd in cmds:
command = f"pm list packages {cmd['arg']}"
if self._user_needed:
command += " --user 0"
output = self._adb_command(command)
output = self._adb_command(f"pm list packages {cmd['arg']}")
for line in output.splitlines():
line = line.strip()
if not line.startswith("package:"):
@@ -282,12 +304,8 @@ class Packages(AndroidExtraction):
dangerous_permissions_count += 1
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
self.log.info(
'Third-party package "%s" requested %d '
"potentially dangerous permissions",
result["package_name"],
dangerous_permissions_count,
)
self.log.info("Third-party package \"%s\" requested %d potentially dangerous permissions",
result["package_name"], dangerous_permissions_count)
packages_to_lookup = []
for result in self.results:
@@ -295,18 +313,13 @@ class Packages(AndroidExtraction):
continue
packages_to_lookup.append(result)
self.log.info(
'Found non-system package with name "%s" installed by "%s" on %s',
result["package_name"],
result["installer"],
result["timestamp"],
)
self.log.info("Found non-system package with name \"%s\" installed by \"%s\" on %s",
result["package_name"], result["installer"], result["timestamp"])
if not self.module_options.get("fast_mode", None):
if not self.fast_mode:
self.check_virustotal(packages_to_lookup)
self.log.info(
"Extracted at total of %d installed package names", len(self.results)
)
self.log.info("Extracted at total of %d installed package names",
len(self.results))
self._adb_disconnect()

View File

@@ -0,0 +1,66 @@
# 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 logging
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Processes(AndroidExtraction):
"""This module extracts details on running processes."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result.get("name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self) -> None:
self._adb_connect()
output = self._adb_command("ps -e")
for line in output.splitlines()[1:]:
line = line.strip()
if line == "":
continue
fields = line.split()
proc = {
"user": fields[0],
"pid": fields[1],
"parent_pid": fields[2],
"vsize": fields[3],
"rss": fields[4],
}
# Sometimes WCHAN is empty, so we need to re-align output fields.
if len(fields) == 8:
proc["wchan"] = ""
proc["pc"] = fields[5]
proc["name"] = fields[7]
elif len(fields) == 9:
proc["wchan"] = fields[5]
proc["pc"] = fields[6]
proc["name"] = fields[8]
self.results.append(proc)
self._adb_disconnect()
log.info("Extracted records on a total of %d processes", len(self.results))

View File

@@ -1,39 +1,24 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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 logging
from typing import Optional
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class RootBinaries(AndroidExtraction):
"""This module extracts the list of installed packages."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: 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 check_indicators(self) -> None:
for root_binary in self.results:
self.detected.append(root_binary)
self.log.warning('Found root binary "%s"', root_binary)
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def run(self) -> None:
root_binaries = [
@@ -65,6 +50,7 @@ class RootBinaries(AndroidExtraction):
if "which: not found" in output:
continue
self.results.append(root_binary)
self.detected.append(root_binary)
self.log.warning("Found root binary \"%s\"", root_binary)
self._adb_disconnect()

View File

@@ -1,36 +1,26 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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 logging
from typing import Optional
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class SELinuxStatus(AndroidExtraction):
"""This module checks if SELinux is being enforced."""
slug = "selinux_status"
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: 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 __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = {} if not results else results
@@ -45,4 +35,4 @@ class SELinuxStatus(AndroidExtraction):
if status == "enforcing":
self.log.info("SELinux is being regularly enforced")
else:
self.log.warning('SELinux status is "%s"!', status)
self.log.warning("SELinux status is \"%s\"!", status)

View File

@@ -1,9 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/
from .artifact import AndroidArtifact
import logging
from .base import AndroidExtraction
log = logging.getLogger(__name__)
ANDROID_DANGEROUS_SETTINGS = [
{
@@ -46,20 +51,21 @@ ANDROID_DANGEROUS_SETTINGS = [
"key": "send_action_app_error",
"safe_value": "1",
},
{
"description": "enabled installation of non Google Play apps",
"key": "install_non_market_apps",
"safe_value": "0",
},
{
"description": "enabled accessibility services",
"key": "accessibility_enabled",
"safe_value": "0",
},
]
class Settings(AndroidArtifact):
class Settings(AndroidExtraction):
"""This module extracts Android system settings."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = {} if not results else results
def check_indicators(self) -> None:
for namespace, settings in self.results.items():
for key, value in settings.items():
@@ -67,11 +73,29 @@ class Settings(AndroidArtifact):
# Check if one of the dangerous settings is using an unsafe
# value (different than the one specified).
if danger["key"] == key and danger["safe_value"] != value:
self.log.warning(
'Found suspicious "%s" setting "%s = %s" (%s)',
namespace,
key,
value,
danger["description"],
)
self.log.warning("Found suspicious setting \"%s = %s\" (%s)",
key, value, danger["description"])
break
def run(self) -> None:
self._adb_connect()
for namespace in ["system", "secure", "global"]:
out = self._adb_command(f"cmd settings list {namespace}")
if not out:
continue
self.results[namespace] = {}
for line in out.splitlines():
line = line.strip()
if line == "":
continue
fields = line.split("=", 1)
try:
self.results[namespace][fields[0]] = fields[1]
except IndexError:
continue
self._adb_disconnect()

View File

@@ -0,0 +1,143 @@
# 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 logging
import os
import sqlite3
from mvt.android.parsers.backup import (AndroidBackupParsingError,
parse_tar_for_sms)
from mvt.common.module import InsufficientPrivileges
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
SMS_BUGLE_QUERY = """
SELECT
ppl.normalized_destination AS address,
p.timestamp AS timestamp,
CASE WHEN m.sender_id IN
(SELECT _id FROM participants WHERE contact_id=-1)
THEN 2 ELSE 1 END incoming, p.text AS body
FROM messages m, conversations c, parts p,
participants ppl, conversation_participants cp
WHERE (m.conversation_id = c._id)
AND (m._id = p.message_id)
AND (cp.conversation_id = c._id)
AND (cp.participant_id = ppl._id);
"""
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
SMS_MMSMS_QUERY = """
SELECT
address AS address,
date_sent AS timestamp,
type as incoming,
body AS body
FROM sms;
"""
class SMS(AndroidExtraction):
"""This module extracts all SMS messages containing links."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record: dict) -> None:
body = record["body"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": f"sms_{record['direction']}",
"data": f"{record['address']}: \"{body}\""
}
def check_indicators(self) -> None:
if not self.indicators:
return
for message in self.results:
if "body" not in message:
continue
# FIXME: check links exported from the body previously
message_links = check_for_links(message["body"])
if self.indicators.check_domains(message_links):
self.detected.append(message)
def _parse_db(self, db_path: str) -> None:
"""Parse an Android bugle_db SMS database file.
:param db_path: Path to the Android SMS database file to process
"""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
if self.SMS_DB_TYPE == 1:
cur.execute(SMS_BUGLE_QUERY)
elif self.SMS_DB_TYPE == 2:
cur.execute(SMS_MMSMS_QUERY)
names = [description[0] for description in cur.description]
for item in cur:
message = {}
for index, value in enumerate(item):
message[names[index]] = value
message["direction"] = ("received" if message["incoming"] == 1 else "sent")
message["isodate"] = convert_timestamp_to_iso(message["timestamp"])
# If we find links in the messages or if they are empty we add
# them to the list of results.
if check_for_links(message["body"]) or message["body"].strip() == "":
self.results.append(message)
cur.close()
conn.close()
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
def _extract_sms_adb(self) -> None:
"""Use the Android backup command to extract SMS data from the native SMS app
It is crucial to use the under-documented "-nocompress" flag to disable the non-standard Java compression
algorithim. This module only supports an unencrypted ADB backup.
"""
backup_tar = self._generate_backup("com.android.providers.telephony")
if not backup_tar:
return
try:
self.results = parse_tar_for_sms(backup_tar)
except AndroidBackupParsingError:
self.log.info("Impossible to read SMS from the Android Backup, please extract the SMS and try extracting it with Android Backup Extractor")
return
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
def run(self) -> None:
try:
if (self._adb_check_file_exists(os.path.join("/", SMS_BUGLE_PATH))):
self.SMS_DB_TYPE = 1
self._adb_process_file(os.path.join("/", SMS_BUGLE_PATH), self._parse_db)
elif (self._adb_check_file_exists(os.path.join("/", SMS_MMSSMS_PATH))):
self.SMS_DB_TYPE = 2
self._adb_process_file(os.path.join("/", SMS_MMSSMS_PATH), self._parse_db)
return
except InsufficientPrivileges:
pass
self.log.warn("No SMS database found. Trying extraction of SMS data using Android backup feature.")
self._extract_sms_adb()

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/
@@ -7,43 +7,33 @@ import base64
import logging
import os
import sqlite3
from typing import Optional, Union
from mvt.common.utils import check_for_links, convert_unix_to_iso
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db"
class Whatsapp(AndroidExtraction):
"""This module extracts all WhatsApp messages containing links."""
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[dict] = None,
log: logging.Logger = logging.getLogger(__name__),
results: 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 __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record: dict) -> Union[dict, list]:
def serialize(self, record: dict) -> None:
text = record["data"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": f"whatsapp_msg_{record['direction']}",
"data": f'"{text}"',
"data": f"\"{text}\""
}
def check_indicators(self) -> None:
@@ -55,9 +45,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.
@@ -67,11 +56,9 @@ class Whatsapp(AndroidExtraction):
"""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute(
"""
cur.execute("""
SELECT * FROM messages;
"""
)
""")
names = [description[0] for description in cur.description]
messages = []
@@ -83,31 +70,23 @@ class Whatsapp(AndroidExtraction):
if not message["data"]:
continue
message["direction"] = "send" if message["key_from_me"] == 1 else "received"
message["isodate"] = convert_unix_to_iso(message["timestamp"])
message["direction"] = ("send" if message["key_from_me"] == 1 else "received")
message["isodate"] = convert_timestamp_to_iso(message["timestamp"])
# If we find links in the messages or if they are empty we add them
# to the list.
# If we find links in the messages or if they are empty we add them to the list.
if check_for_links(message["data"]) or message["data"].strip() == "":
if message.get("thumb_image"):
message["thumb_image"] = base64.b64encode(message["thumb_image"])
if (message.get('thumb_image') is not None):
message['thumb_image'] = base64.b64encode(message['thumb_image'])
messages.append(message)
cur.close()
conn.close()
self.log.info(
"Extracted a total of %d WhatsApp messages containing links", len(messages)
)
log.info("Extracted a total of %d WhatsApp messages containing links", len(messages))
self.results = messages
def run(self) -> None:
self._adb_connect()
try:
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
except Exception as exc:
self.log.error(exc)
self._adb_disconnect()
except Exception as e:
self.log.error(e)

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/

View File

@@ -1,52 +1,27 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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 fnmatch
import logging
import os
from tarfile import TarFile
from typing import List, Optional
from mvt.common.module import MVTModule
class BackupExtraction(MVTModule):
"""This class provides a base for all backup extractios modules"""
ab = None
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.ab = None
self.backup_path = None
self.tar = None
self.files = []
def from_folder(self, backup_path: Optional[str], files: List[str]) -> None:
def from_folder(self, backup_path: str, files: list) -> None:
"""
Get all the files and list them
"""
self.backup_path = backup_path
self.files = files
def from_ab(
self, file_path: Optional[str], tar: Optional[TarFile], files: List[str]
) -> None:
def from_ab(self, file_path: str, tar: TarFile, files: list) -> None:
"""
Extract the files
"""

View File

@@ -0,0 +1,44 @@
# 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 logging
from mvt.android.modules.backup.base import BackupExtraction
from mvt.android.parsers.backup import parse_sms_file
class SMS(BackupExtraction):
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = []
def check_indicators(self) -> None:
if not self.indicators:
return
for message in self.results:
if "body" not in message:
continue
if self.indicators.check_domains(message["links"]):
self.detected.append(message)
def run(self) -> None:
for file in self._get_files_by_pattern("apps/com.android.providers.telephony/d_f/*_sms_backup"):
self.log.info("Processing SMS backup file at %s", file)
data = self._get_file_content(file)
self.results.extend(parse_sms_file(data))
for file in self._get_files_by_pattern("apps/com.android.providers.telephony/d_f/*_mms_backup"):
self.log.info("Processing MMS backup file at %s", file)
data = self._get_file_content(file)
self.results.extend(parse_sms_file(data))
self.log.info("Extracted a total of %d SMS & MMS messages containing links",
len(self.results))

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/
@@ -12,17 +12,6 @@ from .dbinfo import DBInfo
from .getprop import Getprop
from .packages import Packages
from .receivers import Receivers
from .adb_state import DumpsysADBState
BUGREPORT_MODULES = [
Accessibility,
Activities,
Appops,
BatteryDaily,
BatteryHistory,
DBInfo,
Getprop,
Packages,
Receivers,
DumpsysADBState,
]
BUGREPORT_MODULES = [Accessibility, Activities, Appops, BatteryDaily,
BatteryHistory, DBInfo, Getprop, Packages, Receivers]

View File

@@ -0,0 +1,61 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_accessibility
from .base import BugReportModule
log = logging.getLogger(__name__)
class Accessibility(BugReportModule):
"""This module extracts stats on accessibility."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self) -> None:
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
lines = []
in_accessibility = False
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE accessibility:":
in_accessibility = True
continue
if not in_accessibility:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_accessibility("\n".join(lines))
for result in self.results:
log.info("Found installed accessibility service \"%s\"", result.get("service"))
self.log.info("Identified a total of %d accessibility services", len(self.results))

View File

@@ -0,0 +1,62 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_activity_resolver_table
from .base import BugReportModule
log = logging.getLogger(__name__)
class Activities(BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = results if results else {}
def check_indicators(self) -> None:
if not self.indicators:
return
for intent, activities in self.results.items():
for activity in activities:
ioc = self.indicators.check_app_id(activity["package_name"])
if ioc:
activity["matched_indicator"] = ioc
self.detected.append({intent: activity})
continue
def run(self) -> None:
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
lines = []
in_package = False
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().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_activity_resolver_table("\n".join(lines))
self.log.info("Extracted activities for %d intents", len(self.results))

View File

@@ -0,0 +1,79 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_appops
from .base import BugReportModule
log = logging.getLogger(__name__)
class Appops(BugReportModule):
"""This module extracts information on package from App-Ops Manager."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record: dict) -> None:
records = []
for perm in record["permissions"]:
if "entries" not in perm:
continue
for entry in perm["entries"]:
if "timestamp" in entry:
records.append({
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to {perm['name']}: {entry['access']}",
})
return records
def check_indicators(self) -> None:
for result in self.results:
if self.indicators:
ioc = self.indicators.check_app_id(result.get("package_name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
for perm in result["permissions"]:
if perm["name"] == "REQUEST_INSTALL_PACKAGES" and perm["access"] == "allow":
self.log.info("Package %s with REQUEST_INSTALL_PACKAGES permission", result["package_name"])
def run(self) -> None:
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
lines = []
in_appops = False
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE appops:":
in_appops = True
continue
if not in_appops:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_appops("\n".join(lines))
self.log.info("Identified a total of %d packages in App-Ops Manager",
len(self.results))

View File

@@ -1,50 +1,28 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
import fnmatch
import logging
import os
from typing import List, Optional
from zipfile import ZipFile
from mvt.common.module import MVTModule
log = logging.getLogger(__name__)
class BugReportModule(MVTModule):
"""This class provides a base for all Android Bug Report modules."""
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,
)
zip_archive = None
self.zip_archive: Optional[ZipFile] = None
self.extract_path: Optional[str] = None
self.extract_files: List[str] = []
self.zip_files: List[str] = []
def from_folder(
self, extract_path: Optional[str], extract_files: List[str]
) -> None:
def from_folder(self, extract_path: str, extract_files: str) -> None:
self.extract_path = extract_path
self.extract_files = extract_files
def from_zip(self, zip_archive: Optional[ZipFile], zip_files: List[str]) -> None:
def from_zip(self, zip_archive: ZipFile, zip_files: list) -> None:
self.zip_archive = zip_archive
self.zip_files = zip_files
@@ -64,8 +42,6 @@ class BugReportModule(MVTModule):
if matches:
return matches
return []
def _get_file_content(self, file_path: str) -> bytes:
if self.zip_archive:
handle = self.zip_archive.open(file_path)
@@ -91,3 +67,5 @@ class BugReportModule(MVTModule):
return None
return self._get_file_content(dumpstate_logs[0])
return None

View File

@@ -0,0 +1,77 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_battery_daily
from .base import BugReportModule
log = logging.getLogger(__name__)
class BatteryDaily(BugReportModule):
"""This module extracts records from battery daily updates."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record: dict) -> None:
return {
"timestamp": record["from"],
"module": self.__class__.__name__,
"event": "battery_daily",
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
}
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self) -> None:
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
lines = []
in_batterystats = False
in_daily = False
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE batterystats:":
in_batterystats = True
continue
if not in_batterystats:
continue
if line.strip() == "Daily stats:":
lines.append(line)
in_daily = True
continue
if not in_daily:
continue
if line.strip() == "":
break
lines.append(line)
self.results = parse_dumpsys_battery_daily("\n".join(lines))
self.log.info("Extracted a total of %d battery daily stats",
len(self.results))

View File

@@ -0,0 +1,61 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_battery_history
from .base import BugReportModule
log = logging.getLogger(__name__)
class BatteryHistory(BugReportModule):
"""This module extracts records from battery daily updates."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self) -> None:
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
lines = []
in_history = False
for line in content.decode(errors="ignore").splitlines():
if line.strip().startswith("Battery History "):
lines.append(line)
in_history = True
continue
if not in_history:
continue
if line.strip() == "":
break
lines.append(line)
self.results = parse_dumpsys_battery_history("\n".join(lines))
self.log.info("Extracted a total of %d battery history records",
len(self.results))

View File

@@ -0,0 +1,64 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_dbinfo
from .base import BugReportModule
log = logging.getLogger(__name__)
class DBInfo(BugReportModule):
"""This module extracts records from battery daily updates."""
slug = "dbinfo"
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def check_indicators(self) -> None:
if not self.indicators:
return
for result in self.results:
path = result.get("path", "")
for part in path.split("/"):
ioc = self.indicators.check_app_id(part)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def 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_dbinfo = False
lines = []
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE dbinfo:":
in_dbinfo = True
continue
if not in_dbinfo:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_dbinfo("\n".join(lines))
self.log.info("Extracted a total of %d database connection pool records",
len(self.results))

View File

@@ -0,0 +1,59 @@
# 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 logging
from datetime import datetime, timedelta
from mvt.android.parsers import parse_getprop
from .base import BugReportModule
log = logging.getLogger(__name__)
class Getprop(BugReportModule):
"""This module extracts device properties from getprop command."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = {} if not results else results
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
lines = []
in_getprop = False
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "------ SYSTEM PROPERTIES (getprop) ------":
in_getprop = True
continue
if not in_getprop:
continue
if line.strip() == "------":
break
lines.append(line)
self.results = parse_getprop("\n".join(lines))
# Alert if phone is outdated.
security_patch = self.results.get("ro.build.version.security_patch", "")
if security_patch:
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
if (datetime.now() - patch_date) > timedelta(days=6*30):
self.log.warning("This phone has not received security updates for more than "
"six months (last update: %s)", security_patch)
self.log.info("Extracted %d Android system properties", len(self.results))

View File

@@ -0,0 +1,188 @@
# 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 logging
import re
from mvt.android.modules.adb.packages import (DANGEROUS_PERMISSIONS,
DANGEROUS_PERMISSIONS_THRESHOLD,
ROOT_PACKAGES)
from .base import BugReportModule
log = logging.getLogger(__name__)
class Packages(BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record: dict) -> None:
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 ts in timestamps:
records.append({
"timestamp": ts["timestamp"],
"module": self.__class__.__name__,
"event": ts["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
@staticmethod
def parse_package_for_details(output: str) -> dict:
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"requested_permissions": [],
}
in_install_permissions = False
in_runtime_permissions = False
for line in output.splitlines():
if in_install_permissions:
if line.startswith(" " * 4) and not line.startswith(" " * 6):
in_install_permissions = False
continue
permission = line.strip().split(":")[0]
if permission not in details["requested_permissions"]:
details["requested_permissions"].append(permission)
if in_runtime_permissions:
if not line.startswith(" " * 8):
in_runtime_permissions = False
continue
permission = line.strip().split(":")[0]
if permission not in details["requested_permissions"]:
details["requested_permissions"].append(permission)
if line.strip().startswith("userId="):
details["uid"] = line.split("=")[1].strip()
elif line.strip().startswith("versionName="):
details["version_name"] = line.split("=")[1].strip()
elif line.strip().startswith("versionCode="):
details["version_code"] = line.split("=", 1)[1].strip()
elif line.strip().startswith("timeStamp="):
details["timestamp"] = line.split("=")[1].strip()
elif line.strip().startswith("firstInstallTime="):
details["first_install_time"] = line.split("=")[1].strip()
elif line.strip().startswith("lastUpdateTime="):
details["last_update_time"] = line.split("=")[1].strip()
elif line.strip() == "install permissions:":
in_install_permissions = True
elif line.strip() == "runtime permissions:":
in_runtime_permissions = True
return details
def parse_packages_list(self, output: str) -> list:
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
results = []
package_name = None
package = {}
lines = []
for line in output.splitlines():
if line.startswith(" Package ["):
if len(lines) > 0:
details = self.parse_package_for_details("\n".join(lines))
package.update(details)
results.append(package)
lines = []
package = {}
matches = pkg_rxp.findall(line)
if not matches:
continue
package_name = matches[0]
package["package_name"] = package_name
continue
if not package_name:
continue
lines.append(line)
return results
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 = self.parse_packages_list("\n".join(lines))
for result in self.results:
dangerous_permissions_count = 0
for perm in result["requested_permissions"]:
if perm in DANGEROUS_PERMISSIONS:
dangerous_permissions_count += 1
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
self.log.info("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))

View File

@@ -0,0 +1,84 @@
# 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 logging
from mvt.android.parsers import parse_dumpsys_receiver_resolver_table
from .base import BugReportModule
log = logging.getLogger(__name__)
INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
INTENT_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE"
INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
class Receivers(BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = []) -> None:
super().__init__(file_path=file_path, target_path=target_path,
results_path=results_path, fast_mode=fast_mode,
log=log, results=results)
self.results = results if results else {}
def check_indicators(self) -> None:
if not self.indicators:
return
for intent, receivers in self.results.items():
for receiver in receivers:
if intent == INTENT_NEW_OUTGOING_SMS:
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
receiver["receiver"])
elif intent == INTENT_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
receiver["receiver"])
elif intent == INTENT_DATA_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
receiver["receiver"])
elif intent == INTENT_PHONE_STATE:
self.log.info("Found a receiver monitoring telephony state/incoming calls: \"%s\"",
receiver["receiver"])
elif intent == INTENT_NEW_OUTGOING_CALL:
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
receiver["receiver"])
ioc = self.indicators.check_app_id(receiver["package_name"])
if ioc:
receiver["matched_indicator"] = ioc
self.detected.append({intent: receiver})
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_receivers = False
lines = []
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE package:":
in_receivers = True
continue
if not in_receivers:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines))
self.log.info("Extracted receivers for %d intents", len(self.results))

View File

@@ -0,0 +1,11 @@
# 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/
from .dumpsys import (parse_dumpsys_accessibility,
parse_dumpsys_activity_resolver_table,
parse_dumpsys_appops, parse_dumpsys_battery_daily,
parse_dumpsys_battery_history, parse_dumpsys_dbinfo,
parse_dumpsys_receiver_resolver_table)
from .getprop import parse_getprop

View File

@@ -1,8 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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 datetime
import io
import json
import tarfile
@@ -12,7 +13,7 @@ from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from mvt.common.utils import check_for_links, convert_unix_to_iso
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
PBKDF2_KEY_SIZE = 32
@@ -29,18 +30,15 @@ class InvalidBackupPassword(AndroidBackupParsingError):
pass
# TODO: Need to clean all the following code and conform it to the coding style.
def to_utf8_bytes(input_bytes):
output = []
for byte in input_bytes:
if byte < ord(b"\x80"):
if byte < ord(b'\x80'):
output.append(byte)
else:
output.append(ord("\xef") | (byte >> 12))
output.append(ord("\xbc") | ((byte >> 6) & ord("\x3f")))
output.append(ord("\x80") | (byte & ord("\x3f")))
output.append(ord('\xef') | (byte >> 12))
output.append(ord('\xbc') | ((byte >> 6) & ord('\x3f')))
output.append(ord('\x80') | (byte & ord('\x3f')))
return bytes(output)
@@ -51,43 +49,36 @@ def parse_ab_header(data):
'encryption': "none", 'version': 4}
"""
if data.startswith(b"ANDROID BACKUP"):
[_, version, is_compressed, encryption, _] = data.split(b"\n", 4)
[magic_header, version, is_compressed, encryption, tar_data] = data.split(b"\n", 4)
return {
"backup": True,
"compression": (is_compressed == b"1"),
"version": int(version),
"encryption": encryption.decode("utf-8"),
"encryption": encryption.decode("utf-8")
}
return {"backup": False, "compression": None, "version": None, "encryption": None}
return {
"backup": False,
"compression": None,
"version": None,
"encryption": None
}
def decrypt_master_key(
password,
user_salt,
user_iv,
pbkdf2_rounds,
master_key_blob,
format_version,
checksum_salt,
):
def decrypt_master_key(password, user_salt, user_iv, pbkdf2_rounds, master_key_blob, format_version, checksum_salt):
"""Generate AES key from user password uisng PBKDF2
The backup master key is extracted from the master key blog after decryption.
"""
# Derive key from password using PBKDF2.
kdf = PBKDF2HMAC(
algorithm=hashes.SHA1(), length=32, salt=user_salt, iterations=pbkdf2_rounds
)
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=32, salt=user_salt, iterations=pbkdf2_rounds)
key = kdf.derive(password.encode("utf-8"))
# Decrypt master key blob.
cipher = Cipher(algorithms.AES(key), modes.CBC(user_iv))
decryptor = cipher.decryptor()
try:
decryted_master_key_blob = (
decryptor.update(master_key_blob) + decryptor.finalize()
)
decryted_master_key_blob = decryptor.update(master_key_blob) + decryptor.finalize()
# Extract key and IV from decrypted blob.
key_blob = io.BytesIO(decryted_master_key_blob)
@@ -99,8 +90,8 @@ def decrypt_master_key(
master_key_checksum_length = ord(key_blob.read(1))
master_key_checksum = key_blob.read(master_key_checksum_length)
except TypeError as exc:
raise InvalidBackupPassword() from exc
except TypeError:
raise InvalidBackupPassword()
# Handle quirky encoding of master key bytes in Android original Java crypto code.
if format_version > 1:
@@ -109,9 +100,7 @@ def decrypt_master_key(
hmac_mk = master_key
# Derive checksum to confirm successful backup decryption.
kdf = PBKDF2HMAC(
algorithm=hashes.SHA1(), length=32, salt=checksum_salt, iterations=pbkdf2_rounds
)
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=32, salt=checksum_salt, iterations=pbkdf2_rounds)
calculated_checksum = kdf.derive(hmac_mk)
if master_key_checksum != calculated_checksum:
@@ -131,15 +120,7 @@ def decrypt_backup_data(encrypted_backup, password, encryption_algo, format_vers
if password is None:
raise InvalidBackupPassword()
[
user_salt,
checksum_salt,
pbkdf2_rounds,
user_iv,
master_key_blob,
encrypted_data,
] = encrypted_backup.split(b"\n", 5)
[user_salt, checksum_salt, pbkdf2_rounds, user_iv, master_key_blob, encrypted_data] = encrypted_backup.split(b"\n", 5)
user_salt = bytes.fromhex(user_salt.decode("utf-8"))
checksum_salt = bytes.fromhex(checksum_salt.decode("utf-8"))
pbkdf2_rounds = int(pbkdf2_rounds)
@@ -147,15 +128,9 @@ def decrypt_backup_data(encrypted_backup, password, encryption_algo, format_vers
master_key_blob = bytes.fromhex(master_key_blob.decode("utf-8"))
# Derive decryption master key from password.
master_key, master_iv = decrypt_master_key(
password=password,
user_salt=user_salt,
user_iv=user_iv,
pbkdf2_rounds=pbkdf2_rounds,
master_key_blob=master_key_blob,
format_version=format_version,
checksum_salt=checksum_salt,
)
master_key, master_iv = decrypt_master_key(password=password, user_salt=user_salt, user_iv=user_iv,
pbkdf2_rounds=pbkdf2_rounds, master_key_blob=master_key_blob,
format_version=format_version, checksum_salt=checksum_salt)
# Decrypt and unpad backup data using derivied key.
cipher = Cipher(algorithms.AES(master_key), modes.CBC(master_iv))
@@ -174,23 +149,18 @@ def parse_backup_file(data, password=None):
if not data.startswith(b"ANDROID BACKUP"):
raise AndroidBackupParsingError("Invalid file header")
[_, version, is_compressed, encryption_algo, tar_data] = data.split(b"\n", 4)
[magic_header, version, is_compressed, encryption_algo, tar_data] = data.split(b"\n", 4)
version = int(version)
is_compressed = int(is_compressed)
if encryption_algo != b"none":
tar_data = decrypt_backup_data(
tar_data, password, encryption_algo, format_version=version
)
tar_data = decrypt_backup_data(tar_data, password, encryption_algo, format_version=version)
if is_compressed:
try:
tar_data = zlib.decompress(tar_data)
except zlib.error as exc:
raise AndroidBackupParsingError(
"Impossible to decompress the backup file"
) from exc
except zlib.error:
raise AndroidBackupParsingError("Impossible to decompress the backup file")
return tar_data
@@ -201,16 +171,13 @@ def parse_tar_for_sms(data):
Returns an array of SMS
"""
dbytes = io.BytesIO(data)
tar = tarfile.open(fileobj=dbytes)
res = []
with tarfile.open(fileobj=dbytes) as tar:
for member in tar.getmembers():
if member.name.startswith("apps/com.android.providers.telephony/d_f/") and (
member.name.endswith("_sms_backup")
or member.name.endswith("_mms_backup")
):
dhandler = tar.extractfile(member)
res.extend(parse_sms_file(dhandler.read()))
for member in tar.getmembers():
if member.name.startswith("apps/com.android.providers.telephony/d_f/") and \
(member.name.endswith("_sms_backup") or member.name.endswith("_mms_backup")):
dhandler = tar.extractfile(member)
res.extend(parse_sms_file(dhandler.read()))
return res
@@ -225,21 +192,20 @@ def parse_sms_file(data):
json_data = json.loads(data)
for entry in json_data:
# Adapt MMS format to SMS format.
# Adapt MMS format to SMS format
if "mms_body" in entry:
entry["body"] = entry["mms_body"]
entry.pop("mms_body")
body = entry.get("body", None)
if body:
message_links = check_for_links(entry["body"])
message_links = check_for_links(entry["body"])
entry["isodate"] = convert_unix_to_iso(int(entry["date"]) / 1000)
entry["direction"] = "sent" if int(entry["date_sent"]) else "received"
utc_timestamp = datetime.datetime.utcfromtimestamp(int(entry["date"]) / 1000)
entry["isodate"] = convert_timestamp_to_iso(utc_timestamp)
entry["direction"] = ("sent" if int(entry["date_sent"]) else "received")
# Extract links from the body
# If we find links in the messages or if they are empty we add them to the list.
if message_links or entry["body"].strip() == "":
entry["links"] = message_links
res.append(entry)
res.append(entry)
return res

View File

@@ -0,0 +1,375 @@
# 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 re
from datetime import datetime
from mvt.common.utils import convert_timestamp_to_iso
def parse_dumpsys_accessibility(output: str) -> list:
results = []
in_services = False
for line in output.splitlines():
if line.strip().startswith("installed services:"):
in_services = True
continue
if not in_services:
continue
if line.strip() == "}":
break
service = line.split(":")[1].strip()
results.append({
"package_name": service.split("/")[0],
"service": service,
})
return results
def parse_dumpsys_activity_resolver_table(output: str) -> dict:
results = {}
in_activity_resolver_table = False
in_non_data_actions = False
intent = None
for line in output.splitlines():
if line.startswith("Activity Resolver Table:"):
in_activity_resolver_table = True
continue
if not in_activity_resolver_table:
continue
if line.startswith(" Non-Data Actions:"):
in_non_data_actions = True
continue
if not in_non_data_actions:
continue
# If we hit an empty line, the Non-Data Actions section should be
# finished.
if line.strip() == "":
break
# We detect the action name.
if line.startswith(" " * 6) and not line.startswith(" " * 8) and ":" in line:
intent = line.strip().replace(":", "")
results[intent] = []
continue
# If we are not in an intent block yet, skip.
if not intent:
continue
# If we are in a block but the line does not start with 8 spaces
# it means the block ended a new one started, so we reset and
# continue.
if not line.startswith(" " * 8):
intent = None
continue
# If we got this far, we are processing receivers for the
# activities we are interested in.
activity = line.strip().split(" ")[1]
package_name = activity.split("/")[0]
results[intent].append({
"package_name": package_name,
"activity": activity,
})
return results
def parse_dumpsys_battery_daily(output: str) -> list:
results = []
daily = None
daily_updates = []
for line in output.splitlines():
if line.startswith(" Daily from "):
if len(daily_updates) > 0:
results.extend(daily_updates)
daily_updates = []
timeframe = line[13:].strip()
date_from, date_to = timeframe.strip(":").split(" to ", 1)
daily = {"from": date_from[0:10], "to": date_to[0:10]}
continue
if not daily:
continue
if not line.strip().startswith("Update "):
continue
line = line.strip().replace("Update ", "")
package_name, vers = line.split(" ", 1)
vers_nr = vers.split("=", 1)[1]
already_seen = False
for update in daily_updates:
if package_name == update["package_name"] and vers_nr == update["vers"]:
already_seen = True
break
if not already_seen:
daily_updates.append({
"action": "update",
"from": daily["from"],
"to": daily["to"],
"package_name": package_name,
"vers": vers_nr,
})
if len(daily_updates) > 0:
results.extend(daily_updates)
return results
def parse_dumpsys_battery_history(output: str) -> list:
results = []
for line in output.splitlines():
if line.startswith("Battery History "):
continue
if line.strip() == "":
break
time_elapsed = line.strip().split(" ", 1)[0]
event = ""
if line.find("+job") > 0:
event = "start_job"
uid = line[line.find("+job")+5:line.find(":")]
service = line[line.find(":")+1:].strip('"')
package_name = service.split("/")[0]
elif line.find("-job") > 0:
event = "end_job"
uid = line[line.find("-job")+5:line.find(":")]
service = line[line.find(":")+1:].strip('"')
package_name = service.split("/")[0]
elif line.find("+running +wake_lock=") > 0:
uid = line[line.find("+running +wake_lock=")+21:line.find(":")]
event = "wake"
service = line[line.find("*walarm*:")+9:].split(" ")[0].strip('"').strip()
if service == "" or "/" not in service:
continue
package_name = service.split("/")[0]
else:
continue
results.append({
"time_elapsed": time_elapsed,
"event": event,
"uid": uid,
"package_name": package_name,
"service": service,
})
return results
def parse_dumpsys_dbinfo(output: str) -> list:
results = []
rxp = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\].*\[Pid:\((\d+)\)\](\w+).*sql\=\"(.+?)\"')
rxp_no_pid = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\][ ]{1}(\w+).*sql\=\"(.+?)\"')
pool = None
in_operations = False
for line in output.splitlines():
if line.startswith("Connection pool for "):
pool = line.replace("Connection pool for ", "").rstrip(":")
if not pool:
continue
if line.strip() == "Most recently executed operations:":
in_operations = True
continue
if not in_operations:
continue
if not line.startswith(" "):
in_operations = False
pool = None
continue
matches = rxp.findall(line)
if not matches:
matches = rxp_no_pid.findall(line)
if not matches:
continue
else:
match = matches[0]
results.append({
"isodate": match[0],
"action": match[1],
"sql": match[2],
"path": pool,
})
else:
match = matches[0]
results.append({
"isodate": match[0],
"pid": match[1],
"action": match[2],
"sql": match[3],
"path": pool,
})
return results
def parse_dumpsys_receiver_resolver_table(output: str) -> dict:
results = {}
in_receiver_resolver_table = False
in_non_data_actions = False
intent = None
for line in output.splitlines():
if line.startswith("Receiver Resolver Table:"):
in_receiver_resolver_table = True
continue
if not in_receiver_resolver_table:
continue
if line.startswith(" Non-Data Actions:"):
in_non_data_actions = True
continue
if not in_non_data_actions:
continue
# If we hit an empty line, the Non-Data Actions section should be
# finished.
if line.strip() == "":
break
# We detect the action name.
if line.startswith(" " * 6) and not line.startswith(" " * 8) and ":" in line:
intent = line.strip().replace(":", "")
results[intent] = []
continue
# If we are not in an intent block yet, skip.
if not intent:
continue
# If we are in a block but the line does not start with 8 spaces
# it means the block ended a new one started, so we reset and
# continue.
if not line.startswith(" " * 8):
intent = None
continue
# If we got this far, we are processing receivers for the
# activities we are interested in.
receiver = line.strip().split(" ")[1]
package_name = receiver.split("/")[0]
results[intent].append({
"package_name": package_name,
"receiver": receiver,
})
return results
def parse_dumpsys_appops(output: str) -> list:
results = []
perm = {}
package = {}
entry = {}
uid = None
in_packages = False
for line in output.splitlines():
if line.startswith(" Uid 0:"):
in_packages = True
if not in_packages:
continue
if line.startswith(" Uid "):
uid = line[6:-1]
continue
if line.startswith(" Package "):
if entry:
perm["entries"].append(entry)
entry = {}
if package:
if perm:
package["permissions"].append(perm)
perm = {}
results.append(package)
package = {
"package_name": line[12:-1],
"permissions": [],
"uid": uid,
}
continue
if line.startswith(" ") and line[6] != " ":
if entry:
perm["entries"].append(entry)
entry = {}
if perm:
package["permissions"].append(perm)
perm = {}
perm["name"] = line.split()[0]
perm["entries"] = []
if len(line.split()) > 1:
perm["access"] = line.split()[1][1:-2]
continue
if line.startswith(" "):
# Permission entry like:
# Reject: [fg-s]2021-05-19 22:02:52.054 (-314d1h25m2s33ms)
if entry:
perm["entries"].append(entry)
entry = {}
entry["access"] = line.split(":")[0].strip()
entry["type"] = line[line.find("[")+1:line.find("]")]
try:
entry["timestamp"] = convert_timestamp_to_iso(
datetime.strptime(
line[line.find("]")+1:line.find("(")].strip(),
"%Y-%m-%d %H:%M:%S.%f"))
except ValueError:
# Invalid date format
pass
if line.strip() == "":
break
if entry:
perm["entries"].append(entry)
if perm:
package["permissions"].append(perm)
if package:
results.append(package)
return results

View File

@@ -0,0 +1,26 @@
# 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 re
def parse_getprop(output: str) -> dict:
results = {}
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
for line in output.splitlines():
line = line.strip()
if line == "":
continue
matches = re.findall(rxp, line)
if not matches or len(matches[0]) != 2:
continue
key = matches[0][0]
value = matches[0][1]
results[key] = value
return results

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/

View File

@@ -0,0 +1,64 @@
# 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 logging
import os
from mvt.common.command import Command
log = logging.getLogger(__name__)
class CmdCheckIOCS(Command):
name = "check-iocs"
modules = []
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
def run(self) -> None:
all_modules = []
for entry in self.modules:
if entry not in all_modules:
all_modules.append(entry)
log.info("Checking stored results against provided indicators...")
total_detections = 0
for file_name in os.listdir(self.target_path):
name_only, ext = os.path.splitext(file_name)
file_path = os.path.join(self.target_path, file_name)
for iocs_module in all_modules:
if self.module_name and iocs_module.__name__ != self.module_name:
continue
if iocs_module().get_slug() != name_only:
continue
log.info("Loading results from \"%s\" with module %s", file_name,
iocs_module.__name__)
m = iocs_module.from_json(file_path,
log=logging.getLogger(iocs_module.__module__))
if self.iocs.total_ioc_count > 0:
m.indicators = self.iocs
m.indicators.log = m.log
try:
m.check_indicators()
except NotImplementedError:
continue
else:
total_detections += len(m.detected)
if total_detections > 0:
log.warning("The check of the results produced %d detections!",
total_detections)

186
mvt/common/command.py Normal file
View File

@@ -0,0 +1,186 @@
# 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 hashlib
import json
import logging
import os
import sys
from datetime import datetime
from typing import Callable
from mvt.common.indicators import Indicators
from mvt.common.module import run_module, save_timeline
from mvt.common.utils import convert_timestamp_to_iso
from mvt.common.version import MVT_VERSION
class Command(object):
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False,
log: logging.Logger = logging.getLogger(__name__)):
self.name = ""
self.target_path = target_path
self.results_path = results_path
self.ioc_files = ioc_files
self.module_name = module_name
self.serial = serial
self.fast_mode = fast_mode
self.log = log
self.iocs = Indicators(log=log)
self.iocs.load_indicators_files(ioc_files)
self.timeline = []
self.timeline_detected = []
def _create_storage(self) -> None:
if self.results_path and not os.path.exists(self.results_path):
try:
os.makedirs(self.results_path)
except Exception as e:
self.log.critical("Unable to create output folder %s: %s",
self.results_path, e)
sys.exit(1)
def _add_log_file_handler(self, logger: logging.Logger) -> None:
if not self.results_path:
return
fh = logging.FileHandler(os.path.join(self.results_path, "command.log"))
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
fh.setLevel(logging.DEBUG)
fh.setFormatter(formatter)
logger.addHandler(fh)
def _store_timeline(self) -> None:
if not self.results_path:
return
if len(self.timeline) > 0:
save_timeline(self.timeline,
os.path.join(self.results_path, "timeline.csv"))
if len(self.timeline_detected) > 0:
save_timeline(self.timeline_detected,
os.path.join(self.results_path, "timeline_detected.csv"))
def _store_info(self) -> None:
if not self.results_path:
return
target_path = None
if self.target_path:
target_path = os.path.abspath(self.target_path)
info = {
"target_path": target_path,
"mvt_version": MVT_VERSION,
"date": convert_timestamp_to_iso(datetime.now()),
"ioc_files": [],
"hashes": [],
}
for coll in self.iocs.ioc_collections:
info["ioc_files"].append(coll.get("stix2_file_path", ""))
# TODO: Revisit if setting this from environment variable is good
# enough.
if self.target_path and os.environ.get("MVT_HASH_FILES"):
if os.path.isfile(self.target_path):
h = hashlib.sha256()
with open(self.target_path, "rb") as handle:
h.update(handle.read())
info["hashes"].append({
"file_path": self.target_path,
"sha256": h.hexdigest(),
})
elif os.path.isdir(self.target_path):
for (root, dirs, files) in os.walk(self.target_path):
for file in files:
file_path = os.path.join(root, file)
h = hashlib.sha256()
try:
with open(file_path, "rb") as handle:
h.update(handle.read())
except FileNotFoundError:
self.log.error("Failed to hash the file %s: might be a symlink", file_path)
continue
except PermissionError:
self.log.error("Failed to hash the file %s: permission denied", file_path)
continue
info["hashes"].append({
"file_path": file_path,
"sha256": h.hexdigest(),
})
with open(os.path.join(self.results_path, "info.json"), "w+") as handle:
json.dump(info, handle, indent=4)
def list_modules(self) -> None:
self.log.info("Following is the list of available %s modules:", self.name)
for module in self.modules:
self.log.info(" - %s", module.__name__)
def init(self) -> None:
raise NotImplementedError
def module_init(self, module: Callable) -> None:
raise NotImplementedError
def finish(self) -> None:
raise NotImplementedError
def run(self) -> None:
self._create_storage()
self._add_log_file_handler(self.log)
try:
self.init()
except NotImplementedError:
pass
for module in self.modules:
if self.module_name and module.__name__ != self.module_name:
continue
module_logger = logging.getLogger(module.__module__)
self._add_log_file_handler(module_logger)
m = module(target_path=self.target_path,
results_path=self.results_path,
fast_mode=self.fast_mode,
log=module_logger)
if self.iocs.total_ioc_count:
m.indicators = self.iocs
m.indicators.log = m.log
if self.serial:
m.serial = self.serial
try:
self.module_init(m)
except NotImplementedError:
pass
run_module(m)
self.timeline.extend(m.timeline)
self.timeline_detected.extend(m.timeline_detected)
self._store_timeline()
self._store_info()
try:
self.finish()
except NotImplementedError:
pass

14
mvt/common/help.py Normal file
View File

@@ -0,0 +1,14 @@
# 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/
# Help messages of repeating options.
HELP_MSG_OUTPUT = "Specify a path to a folder where you want to store JSON results"
HELP_MSG_IOC = "Path to indicators file (can be invoked multiple time)"
HELP_MSG_FAST = "Avoid running time/resource consuming features"
HELP_MSG_LIST_MODULES = "Print list of available modules and exit"
HELP_MSG_MODULE = "Name of a single module you would like to run instead of all"
# Android-specific.
HELP_MSG_SERIAL = "Specify a device serial number or HOST:PORT connection string"

449
mvt/common/indicators.py Normal file
View File

@@ -0,0 +1,449 @@
# 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 json
import logging
import os
from appdirs import user_data_dir
from .url import URL
MVT_DATA_FOLDER = user_data_dir("mvt")
MVT_INDICATORS_FOLDER = os.path.join(MVT_DATA_FOLDER, "indicators")
class Indicators:
"""This class is used to parse indicators from a STIX2 file and provide
functions to compare extracted artifacts to the indicators.
"""
def __init__(self, log=logging.Logger) -> None:
self.log = log
self.ioc_collections = []
self.total_ioc_count = 0
def _load_downloaded_indicators(self) -> None:
if not os.path.isdir(MVT_INDICATORS_FOLDER):
return
for ioc_file_name in os.listdir(MVT_INDICATORS_FOLDER):
if ioc_file_name.lower().endswith(".stix2"):
self.parse_stix2(os.path.join(MVT_INDICATORS_FOLDER, ioc_file_name))
def _check_stix2_env_variable(self) -> None:
"""
Checks if a variable MVT_STIX2 contains path to a STIX files.
"""
if "MVT_STIX2" not in os.environ:
return
paths = os.environ["MVT_STIX2"].split(":")
for path in paths:
if os.path.isfile(path):
self.parse_stix2(path)
else:
self.log.error("Path specified with env MVT_STIX2 is not a valid file: %s",
path)
def _new_collection(self, cid: str = "", name: str = "", description: str = "",
file_name: str = "", file_path: str = "") -> dict:
return {
"id": cid,
"name": name,
"description": description,
"stix2_file_name": file_name,
"stix2_file_path": file_path,
"domains": [],
"processes": [],
"emails": [],
"file_names": [],
"file_paths": [],
"files_sha256": [],
"app_ids": [],
"ios_profile_ids": [],
"count": 0,
}
def _add_indicator(self, ioc: str, ioc_coll: dict, ioc_coll_list: list) -> None:
ioc = ioc.strip("'")
if ioc not in ioc_coll_list:
ioc_coll_list.append(ioc)
ioc_coll["count"] += 1
self.total_ioc_count += 1
def parse_stix2(self, file_path: str) -> None:
"""Extract indicators from a STIX2 file.
:param file_path: Path to the STIX2 file to parse
:type file_path: str
"""
self.log.info("Parsing STIX2 indicators file at path %s", file_path)
with open(file_path, "r", encoding="utf-8") as handle:
try:
data = json.load(handle)
except json.decoder.JSONDecodeError:
self.log.critical("Unable to parse STIX2 indicator file. "
"The file is corrupted or in the wrong format!")
return
malware = {}
indicators = []
relationships = []
for entry in data.get("objects", []):
entry_type = entry.get("type", "")
if entry_type == "malware":
malware[entry["id"]] = {
"name": entry["name"],
"description": entry.get("description", ""),
}
elif entry_type == "indicator":
indicators.append(entry)
elif entry_type == "relationship":
relationships.append(entry)
collections = []
for mal_id, mal_values in malware.items():
collection = self._new_collection(mal_id, mal_values.get("name"),
mal_values.get("description"),
os.path.basename(file_path),
file_path)
collections.append(collection)
# We loop through all indicators.
for indicator in indicators:
malware_id = None
# We loop through all relationships and find the one pertinent to
# the current indicator.
for relationship in relationships:
if relationship["source_ref"] != indicator["id"]:
continue
# Look for a malware definition with the correct identifier.
if relationship["target_ref"] in malware.keys():
malware_id = relationship["target_ref"]
break
# Now we look for the correct collection matching the malware ID we
# got from the relationship.
for collection in collections:
if collection["id"] != malware_id:
continue
key, value = indicator.get("pattern", "").strip("[]").split("=")
if key == "domain-name:value":
# We force domain names to lower case.
self._add_indicator(ioc=value.lower(),
ioc_coll=collection,
ioc_coll_list=collection["domains"])
elif key == "process:name":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["processes"])
elif key == "email-addr:value":
# We force email addresses to lower case.
self._add_indicator(ioc=value.lower(),
ioc_coll=collection,
ioc_coll_list=collection["emails"])
elif key == "file:name":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["file_names"])
elif key == "file:path":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["file_paths"])
elif key == "file:hashes.sha256":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["files_sha256"])
elif key == "app:id":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["app_ids"])
elif key == "configuration-profile:id":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["ios_profile_ids"])
break
for coll in collections:
self.log.info("Extracted %d indicators for collection with name \"%s\"",
coll["count"], coll["name"])
self.ioc_collections.extend(collections)
def load_indicators_files(self, files: list, load_default: bool = True) -> None:
"""
Load a list of indicators files.
"""
for file_path in files:
if os.path.isfile(file_path):
self.parse_stix2(file_path)
else:
self.log.warning("No indicators file exists at path %s",
file_path)
# Load downloaded indicators and any indicators from env variable.
if load_default:
self._load_downloaded_indicators()
self._check_stix2_env_variable()
self.log.info("Loaded a total of %d unique indicators", self.total_ioc_count)
def get_iocs(self, ioc_type: str) -> dict:
for ioc_collection in self.ioc_collections:
for ioc in ioc_collection.get(ioc_type, []):
yield {
"value": ioc,
"type": ioc_type,
"name": ioc_collection["name"],
"stix2_file_name": ioc_collection["stix2_file_name"],
}
def check_domain(self, url: str) -> dict:
"""Check if a given URL matches any of the provided domain indicators.
:param url: URL to match against domain indicators
:type url: str
:returns: Indicator details if matched, otherwise None
"""
# TODO: If the IOC domain contains a subdomain, it is not currently
# being matched.
if not url:
return None
try:
# First we use the provided URL.
orig_url = URL(url)
if orig_url.check_if_shortened():
# If it is, we try to retrieve the actual URL making an
# HTTP HEAD request.
unshortened = orig_url.unshorten()
# self.log.info("Found a shortened URL %s -> %s",
# url, unshortened)
# Now we check for any nested URL shorteners.
dest_url = URL(unshortened)
if dest_url.check_if_shortened():
# self.log.info("Original URL %s appears to shorten another shortened URL %s ... checking!",
# orig_url.url, dest_url.url)
return self.check_domain(dest_url.url)
final_url = dest_url
else:
# If it's not shortened, we just use the original URL object.
final_url = orig_url
except Exception:
# If URL parsing failed, we just try to do a simple substring
# match.
for ioc in self.get_iocs("domains"):
if ioc["value"].lower() in url:
self.log.warning("Maybe found a known suspicious domain %s matching indicators from \"%s\"",
url, ioc["name"])
return ioc
# If nothing matched, we can quit here.
return None
# If all parsing worked, we start walking through available domain indicators.
for ioc in self.get_iocs("domains"):
# First we check the full domain.
if final_url.domain.lower() == ioc["value"]:
if orig_url.is_shortened and orig_url.url != final_url.url:
self.log.warning("Found a known suspicious domain %s shortened as %s matching indicators from \"%s\"",
final_url.url, orig_url.url, ioc["name"])
else:
self.log.warning("Found a known suspicious domain %s matching indicators from \"%s\"",
final_url.url, ioc["name"])
return ioc
# Then we just check the top level domain.
if final_url.top_level.lower() == ioc["value"]:
if orig_url.is_shortened and orig_url.url != final_url.url:
self.log.warning("Found a sub-domain with suspicious top level %s shortened as %s matching indicators from \"%s\"",
final_url.url, orig_url.url, ioc["name"])
else:
self.log.warning("Found a sub-domain with a suspicious top level %s matching indicators from \"%s\"",
final_url.url, ioc["name"])
return ioc
def check_domains(self, urls: list) -> dict:
"""Check a list of URLs against the provided list of domain indicators.
:param urls: List of URLs to check against domain indicators
:type urls: list
:returns: Indicator details if matched, otherwise None
"""
if not urls:
return None
for url in urls:
check = self.check_domain(url)
if check:
return check
def check_process(self, process: str) -> dict:
"""Check the provided process name against the list of process
indicators.
:param process: Process name to check against process indicators
:type process: str
:returns: Indicator details if matched, otherwise None
"""
if not process:
return None
proc_name = os.path.basename(process)
for ioc in self.get_iocs("processes"):
if proc_name == ioc["value"]:
self.log.warning("Found a known suspicious process name \"%s\" matching indicators from \"%s\"",
process, ioc["name"])
return ioc
if len(proc_name) == 16:
if ioc["value"].startswith(proc_name):
self.log.warning("Found a truncated known suspicious process name \"%s\" matching indicators from \"%s\"",
process, ioc["name"])
return ioc
def check_processes(self, processes: list) -> dict:
"""Check the provided list of processes against the list of
process indicators.
:param processes: List of processes to check against process indicators
:type processes: list
:returns: Indicator details if matched, otherwise None
"""
if not processes:
return None
for process in processes:
check = self.check_process(process)
if check:
return check
def check_email(self, email: str) -> dict:
"""Check the provided email against the list of email indicators.
:param email: Email address to check against email indicators
:type email: str
:returns: Indicator details if matched, otherwise None
"""
if not email:
return None
for ioc in self.get_iocs("emails"):
if email.lower() == ioc["value"].lower():
self.log.warning("Found a known suspicious email address \"%s\" matching indicators from \"%s\"",
email, ioc["name"])
return ioc
def check_file_name(self, file_name: str) -> dict:
"""Check the provided file name against the list of file indicators.
:param file_name: File name to check against file
indicators
:type file_name: str
:returns: Indicator details if matched, otherwise None
"""
if not file_name:
return None
for ioc in self.get_iocs("file_names"):
if ioc["value"] == file_name:
self.log.warning("Found a known suspicious file name \"%s\" matching indicators from \"%s\"",
file_name, ioc["name"])
return ioc
def check_file_path(self, file_path: str) -> dict:
"""Check the provided file path against the list of file indicators (both path and name).
:param file_path: File path or file name to check against file
indicators
:type file_path: str
:returns: Indicator details if matched, otherwise None
"""
if not file_path:
return None
ioc = self.check_file_name(os.path.basename(file_path))
if ioc:
return ioc
for ioc in self.get_iocs("file_paths"):
# Strip any trailing slash from indicator paths to match directories.
if file_path.startswith(ioc["value"].rstrip("/")):
self.log.warning("Found a known suspicious file path \"%s\" matching indicators form \"%s\"",
file_path, ioc["name"])
return ioc
def check_profile(self, profile_uuid: str) -> dict:
"""Check the provided configuration profile UUID against the list of indicators.
:param profile_uuid: Profile UUID to check against configuration profile indicators
:type profile_uuid: str
:returns: Indicator details if matched, otherwise None
"""
if not profile_uuid:
return None
for ioc in self.get_iocs("ios_profile_ids"):
if profile_uuid in ioc["value"]:
self.log.warning("Found a known suspicious profile ID \"%s\" matching indicators from \"%s\"",
profile_uuid, ioc["name"])
return ioc
def check_file_hash(self, file_hash: str) -> dict:
"""Check the provided SHA256 file hash against the list of indicators.
:param file_hash: SHA256 hash to check
:type file_hash: str
:returns: Indicator details if matched, otherwise None
"""
if not file_hash:
return None
for ioc in self.get_iocs("files_sha256"):
if file_hash.lower() == ioc["value"].lower():
self.log.warning("Found a known suspicious file with hash \"%s\" matching indicators from \"%s\"",
file_hash, ioc["name"])
return ioc
def check_app_id(self, app_id: str) -> dict:
"""Check the provided app identifier (typically an Android package name)
against the list of indicators.
:param app_id: App ID to check against the list of indicators
:type app_id: str
:returns: Indicator details if matched, otherwise None
"""
if not app_id:
return None
for ioc in self.get_iocs("app_ids"):
if app_id.lower() == ioc["value"].lower():
self.log.warning("Found a known suspicious app with ID \"%s\" matching indicators from \"%s\"",
app_id, ioc["name"])
return ioc

59
mvt/common/logo.py Normal file
View File

@@ -0,0 +1,59 @@
# 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/
from rich import print
from .updates import IndicatorsUpdates, MVTUpdates
from .version import MVT_VERSION
def check_updates() -> None:
# First we check for MVT version udpates.
mvt_updates = MVTUpdates()
try:
latest_version = mvt_updates.check()
except Exception:
pass
else:
if latest_version:
print(f"\t\t[bold]Version {latest_version} is available! Upgrade mvt![/bold]")
# Then we check for indicators files updates.
ioc_updates = IndicatorsUpdates()
# Before proceeding, we check if we have downloaded an indicators index.
# If not, there's no point in proceeding with the updates check.
if ioc_updates.get_latest_update() == 0:
print("\t\t[bold]You have not yet downloaded any indicators, check the `download-iocs` command![/bold]")
return
# We only perform this check at a fixed frequency, in order to not
# overburden the user with too many lookups if the command is being run
# multiple times.
should_check, hours = ioc_updates.should_check()
if not should_check:
print(f"\t\tIndicators updates checked recently, next automatic check in {int(hours)} hours")
return
try:
ioc_to_update = ioc_updates.check()
except Exception:
pass
else:
if ioc_to_update:
print("\t\t[bold]There are updates to your indicators files! Run the `download-iocs` command to update![/bold]")
else:
print("\t\tYour indicators files seem to be up to date.")
def logo() -> None:
print("\n")
print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
print("\t\thttps://mvt.re")
print(f"\t\tVersion: {MVT_VERSION}")
check_updates()
print("\n")

View File

@@ -1,16 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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 csv
import json
import logging
import os
import re
from typing import Any, Dict, List, Optional, Union
from typing import Callable
from .utils import CustomJSONEncoder, exec_or_profile
import simplejson as json
class DatabaseNotFoundError(Exception):
@@ -25,27 +24,20 @@ class InsufficientPrivileges(Exception):
pass
class MVTModule:
class MVTModule(object):
"""This class provides a base for all extraction modules."""
enabled = True
slug: Optional[str] = None
slug = None
def __init__(
self,
file_path: Optional[str] = None,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
module_options: Optional[Dict[str, Any]] = None,
log: logging.Logger = logging.getLogger(__name__),
results: Union[List[Dict[str, Any]], Dict[str, Any], None] = None,
) -> None:
def __init__(self, file_path: str = None, target_path: str = None,
results_path: str = None, fast_mode: bool = False,
log: logging.Logger = None, results: list = None):
"""Initialize module.
:param file_path: Path to the module's database file, if there is any
:type file_path: str
:param target_path: Path to the target folder (backup or filesystem
dump)
:param target_path: Path to the target folder (backup or filesystem dump)
:type file_path: str
:param results_path: Folder where results will be stored
:type results_path: str
@@ -58,29 +50,29 @@ class MVTModule:
self.file_path = file_path
self.target_path = target_path
self.results_path = results_path
self.module_options = module_options if module_options else {}
self.fast_mode = fast_mode
self.log = log
self.indicators = None
self.results = results if results else []
self.detected: List[Dict[str, Any]] = []
self.timeline: List[Dict[str, str]] = []
self.timeline_detected: List[Dict[str, str]] = []
self.detected = []
self.timeline = []
self.timeline_detected = []
@classmethod
def from_json(cls, json_path: str, log: logging.Logger):
def from_json(cls, json_path: str, log: logging.Logger = None):
with open(json_path, "r", encoding="utf-8") as handle:
results = json.load(handle)
if log:
log.info('Loaded %d results from "%s"', len(results), json_path)
log.info("Loaded %d results from \"%s\"",
len(results), json_path)
return cls(results=results, log=log)
@classmethod
def get_slug(cls) -> str:
def get_slug(self) -> str:
"""Use the module's class name to retrieve a slug"""
if cls.slug:
return cls.slug
if self.slug:
return self.slug
sub = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", cls.__name__)
sub = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", self.__class__.__name__)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", sub).lower()
def check_indicators(self) -> None:
@@ -103,22 +95,18 @@ class MVTModule:
results_json_path = os.path.join(self.results_path, results_file_name)
with open(results_json_path, "w", encoding="utf-8") as handle:
try:
json.dump(self.results, handle, indent=4, cls=CustomJSONEncoder)
except Exception as exc:
self.log.error(
"Unable to store results of module %s to file %s: %s",
self.__class__.__name__,
results_file_name,
exc,
)
json.dump(self.results, handle, indent=4, default=str)
except Exception as e:
self.log.error("Unable to store results of module %s to file %s: %s",
self.__class__.__name__, results_file_name, e)
if self.detected:
detected_file_name = f"{name}_detected.json"
detected_json_path = os.path.join(self.results_path, detected_file_name)
with open(detected_json_path, "w", encoding="utf-8") as handle:
json.dump(self.detected, handle, indent=4, cls=CustomJSONEncoder)
json.dump(self.detected, handle, indent=4, default=str)
def serialize(self, record: dict) -> Union[dict, list, None]:
def serialize(self, record: dict) -> None:
raise NotImplementedError
@staticmethod
@@ -138,7 +126,7 @@ class MVTModule:
for result in self.results:
record = self.serialize(result)
if record:
if isinstance(record, list):
if type(record) == list:
self.timeline.extend(record)
else:
self.timeline.append(record)
@@ -146,7 +134,7 @@ class MVTModule:
for detected in self.detected:
record = self.serialize(detected)
if record:
if isinstance(record, list):
if type(record) == list:
self.timeline_detected.extend(record)
else:
self.timeline_detected.append(record)
@@ -160,69 +148,41 @@ class MVTModule:
raise NotImplementedError
def run_module(module: MVTModule) -> None:
def run_module(module: Callable) -> None:
module.log.info("Running module %s...", module.__class__.__name__)
try:
exec_or_profile("module.run()", globals(), locals())
module.run()
except NotImplementedError:
module.log.exception(
"The run() procedure of module %s was not implemented yet!",
module.__class__.__name__,
)
except InsufficientPrivileges as exc:
module.log.info(
"Insufficient privileges for module %s: %s", module.__class__.__name__, exc
)
except DatabaseNotFoundError as exc:
module.log.info(
"There might be no data to extract by module %s: %s",
module.__class__.__name__,
exc,
)
except DatabaseCorruptedError as exc:
module.log.error(
"The %s module database seems to be corrupted: %s",
module.__class__.__name__,
exc,
)
except Exception as exc:
module.log.exception(
"Error in running extraction from module %s: %s",
module.__class__.__name__,
exc,
)
module.log.exception("The run() procedure of module %s was not implemented yet!",
module.__class__.__name__)
except InsufficientPrivileges as e:
module.log.info("Insufficient privileges for module %s: %s", module.__class__.__name__, e)
except DatabaseNotFoundError as e:
module.log.info("There might be no data to extract by module %s: %s",
module.__class__.__name__, e)
except DatabaseCorruptedError as e:
module.log.error("The %s module database seems to be corrupted: %s",
module.__class__.__name__, e)
except Exception as e:
module.log.exception("Error in running extraction from module %s: %s",
module.__class__.__name__, e)
else:
try:
exec_or_profile("module.check_indicators()", globals(), locals())
module.check_indicators()
except NotImplementedError:
module.log.info(
"The %s module does not support checking for indicators",
module.__class__.__name__,
)
except Exception as exc:
module.log.exception(
"Error when checking indicators from module %s: %s",
module.__class__.__name__,
exc,
)
module.log.info("The %s module does not support checking for indicators",
module.__class__.__name__)
pass
else:
if module.indicators and not module.detected:
module.log.info(
"The %s module produced no detections!", module.__class__.__name__
)
module.log.info("The %s module produced no detections!",
module.__class__.__name__)
try:
module.to_timeline()
except NotImplementedError:
pass
except Exception as exc:
module.log.exception(
"Error when serializing data from module %s: %s",
module.__class__.__name__,
exc,
)
module.save_to_json()
@@ -235,19 +195,12 @@ def save_timeline(timeline: list, timeline_path: str) -> None:
"""
with open(timeline_path, "a+", encoding="utf-8") as handle:
csvoutput = csv.writer(
handle, delimiter=",", quotechar='"', quoting=csv.QUOTE_ALL, escapechar="\\"
)
csvoutput = csv.writer(handle, delimiter=",", quotechar="\"")
csvoutput.writerow(["UTC Timestamp", "Plugin", "Event", "Description"])
for event in sorted(
timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""
):
csvoutput.writerow(
[
event.get("timestamp"),
event.get("module"),
event.get("event"),
event.get("data"),
]
)
for event in sorted(timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""):
csvoutput.writerow([
event.get("timestamp"),
event.get("module"),
event.get("event"),
event.get("data"),
])

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/
@@ -13,21 +13,27 @@ class MutuallyExclusiveOption(Option):
def __init__(self, *args, **kwargs):
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
help_msg = kwargs.get("help", "")
help = kwargs.get("help", "")
if self.mutually_exclusive:
ex_str = ", ".join(self.mutually_exclusive)
kwargs["help"] = (
f"{help_msg} NOTE: This argument is mutually exclusive with arguments"
f"[{ex_str}]."
kwargs["help"] = help + (
" NOTE: This argument is mutually exclusive with "
"arguments: [" + ex_str + "]."
)
super().__init__(*args, **kwargs)
super(MutuallyExclusiveOption, self).__init__(*args, **kwargs)
def handle_parse_result(self, ctx, opts, args):
if self.mutually_exclusive.intersection(opts) and self.name in opts:
raise UsageError(
f"Illegal usage: `{self.name}` is mutually exclusive "
f"with arguments `{', '.join(self.mutually_exclusive)}`."
"Illegal usage: `{}` is mutually exclusive with "
"arguments `{}`.".format(
self.name,
", ".join(self.mutually_exclusive)
)
)
return super().handle_parse_result(ctx, opts, args)
return super(MutuallyExclusiveOption, self).handle_parse_result(
ctx,
opts,
args
)

View File

@@ -1,12 +1,11 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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 logging
import os
from datetime import datetime
from typing import Optional, Tuple
import requests
import yaml
@@ -22,8 +21,9 @@ INDICATORS_CHECK_FREQUENCY = 12
class MVTUpdates:
def check(self) -> str:
res = requests.get("https://pypi.org/pypi/mvt/json", timeout=15)
res = requests.get("https://pypi.org/pypi/mvt/json")
data = res.json()
latest_version = data.get("info", {}).get("version", "")
@@ -34,6 +34,7 @@ class MVTUpdates:
class IndicatorsUpdates:
def __init__(self) -> None:
self.github_raw_url = "https://raw.githubusercontent.com/{}/{}/{}/{}"
@@ -42,21 +43,16 @@ class IndicatorsUpdates:
self.index_branch = "main"
self.index_path = "indicators.yaml"
if not os.path.exists(MVT_DATA_FOLDER):
os.makedirs(MVT_DATA_FOLDER)
self.latest_update_path = os.path.join(
MVT_DATA_FOLDER, "latest_indicators_update"
)
self.latest_check_path = os.path.join(
MVT_DATA_FOLDER, "latest_indicators_check"
)
self.latest_update_path = os.path.join(MVT_DATA_FOLDER,
"latest_indicators_update")
self.latest_check_path = os.path.join(MVT_DATA_FOLDER,
"latest_indicators_check")
def get_latest_check(self) -> int:
if not os.path.exists(self.latest_check_path):
return 0
with open(self.latest_check_path, "r", encoding="utf-8") as handle:
with open(self.latest_check_path, "r") as handle:
data = handle.read().strip()
if data:
return int(data)
@@ -64,19 +60,15 @@ class IndicatorsUpdates:
return 0
def set_latest_check(self) -> None:
timestamp = int(datetime.now().timestamp())
with open(self.latest_check_path, "w", encoding="utf-8") as handle:
timestamp = int(datetime.utcnow().timestamp())
with open(self.latest_check_path, "w") as handle:
handle.write(str(timestamp))
def get_latest_update(self) -> int:
"""
Check the time of the latest indicator update.
Returns 0 if this file doesn't exists.
"""
if not os.path.exists(self.latest_update_path):
return 0
with open(self.latest_update_path, "r", encoding="utf-8") as handle:
with open(self.latest_update_path, "r") as handle:
data = handle.read().strip()
if data:
return int(data)
@@ -84,33 +76,26 @@ class IndicatorsUpdates:
return 0
def set_latest_update(self) -> None:
timestamp = int(datetime.now().timestamp())
with open(self.latest_update_path, "w", encoding="utf-8") as handle:
timestamp = int(datetime.utcnow().timestamp())
with open(self.latest_update_path, "w") as handle:
handle.write(str(timestamp))
def get_remote_index(self) -> Optional[dict]:
url = self.github_raw_url.format(
self.index_owner, self.index_repo, self.index_branch, self.index_path
)
res = requests.get(url, timeout=15)
def get_remote_index(self) -> dict:
url = self.github_raw_url.format(self.index_owner, self.index_repo,
self.index_branch, self.index_path)
res = requests.get(url)
if res.status_code != 200:
log.error(
"Failed to retrieve indicators index located at %s (error %d)",
url,
res.status_code,
)
log.error("Failed to retrieve indicators index located at %s (error %d)",
url, res.status_code)
return None
return yaml.safe_load(res.content)
def download_remote_ioc(self, ioc_url: str) -> Optional[str]:
res = requests.get(ioc_url, timeout=15)
def download_remote_ioc(self, ioc_url: str) -> str:
res = requests.get(ioc_url)
if res.status_code != 200:
log.error(
"Failed to download indicators file from %s (error %d)",
ioc_url,
res.status_code,
)
log.error("Failed to download indicators file from %s (error %d)",
ioc_url, res.status_code)
return None
clean_file_name = ioc_url.lstrip("https://").replace("/", "_")
@@ -128,9 +113,6 @@ class IndicatorsUpdates:
os.makedirs(MVT_INDICATORS_FOLDER)
index = self.get_remote_index()
if not index:
return
for ioc in index.get("indicators", []):
ioc_type = ioc.get("type", "")
@@ -146,64 +128,45 @@ class IndicatorsUpdates:
ioc_url = ioc.get("download_url", "")
if not ioc_url:
log.error(
"Could not find a way to download indicator file for %s",
ioc.get("name"),
)
log.error("Could not find a way to download indicator file for %s",
ioc.get("name"))
continue
ioc_local_path = self.download_remote_ioc(ioc_url)
if not ioc_local_path:
continue
log.info(
'Downloaded indicators "%s" to %s', ioc.get("name"), ioc_local_path
)
log.info("Downloaded indicators \"%s\" to %s",
ioc.get("name"), ioc_local_path)
self.set_latest_update()
def _get_remote_file_latest_commit(
self, owner: str, repo: str, branch: str, path: str
) -> int:
# TODO: The branch is currently not taken into consideration.
# How do we specify which branch to look up to the API?
file_commit_url = (
f"https://api.github.com/repos/{owner}/{repo}/commits?path={path}"
)
res = requests.get(file_commit_url, timeout=15)
def _get_remote_file_latest_commit(self, owner: str, repo: str,
branch: str, path: str) -> bool:
file_commit_url = f"https://api.github.com/repos/{self.index_owner}/{self.index_repo}/commits?path={self.index_path}"
res = requests.get(file_commit_url)
if res.status_code != 200:
log.error(
"Failed to get details about file %s (error %d)",
file_commit_url,
res.status_code,
)
return -1
log.error("Failed to get details about file %s (error %d)",
file_commit_url, res.status_code)
return False
details = res.json()
if len(details) == 0:
return -1
return False
latest_commit = details[0]
latest_commit_date = (
latest_commit.get("commit", {}).get("author", {}).get("date", None)
)
latest_commit_date = latest_commit.get("commit", {}).get("author", {}).get("date", None)
if not latest_commit_date:
log.error(
"Failed to retrieve date of latest update to indicators index file"
)
return -1
log.error("Failed to retrieve date of latest update to indicators index file")
return False
latest_commit_dt = datetime.strptime(latest_commit_date, "%Y-%m-%dT%H:%M:%SZ")
latest_commit_dt = datetime.strptime(latest_commit_date, '%Y-%m-%dT%H:%M:%SZ')
latest_commit_ts = int(latest_commit_dt.timestamp())
return latest_commit_ts
def should_check(self) -> Tuple[bool, int]:
"""
Compare time of the latest indicator check with current time.
Returns bool and number of hours since the last check.
"""
now = datetime.now()
def should_check(self) -> (bool, int):
now = datetime.utcnow()
latest_check_ts = self.get_latest_check()
latest_check_dt = datetime.fromtimestamp(latest_check_ts)
@@ -213,23 +176,21 @@ class IndicatorsUpdates:
if diff_hours >= INDICATORS_CHECK_FREQUENCY:
return True, 0
return False, int(INDICATORS_CHECK_FREQUENCY - diff_hours)
return False, INDICATORS_CHECK_FREQUENCY - diff_hours
def check(self) -> bool:
self.set_latest_check()
latest_update = self.get_latest_update()
latest_commit_ts = self._get_remote_file_latest_commit(
self.index_owner, self.index_repo, self.index_branch, self.index_path
)
latest_commit_ts = self._get_remote_file_latest_commit(self.index_owner,
self.index_repo,
self.index_branch,
self.index_path)
if latest_update < latest_commit_ts:
return True
index = self.get_remote_index()
if not index:
return False
for ioc in index.get("indicators", []):
if ioc.get("type", "") != "github":
continue
@@ -240,9 +201,10 @@ class IndicatorsUpdates:
branch = github.get("branch", "main")
path = github.get("path", "")
file_latest_commit_ts = self._get_remote_file_latest_commit(
owner, repo, branch, path
)
file_latest_commit_ts = self._get_remote_file_latest_commit(owner,
repo,
branch,
path)
if latest_update < file_latest_commit_ts:
return True

View File

@@ -1,73 +1,53 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/
from typing import Optional
import requests
from tld import get_tld
SHORTENER_DOMAINS = [
"0rz.tw",
"1drv.ms",
"1link.in",
"1url.com",
"2big.at",
"2.gp",
"2pl.us",
"2tu.us",
"2ya.com",
"3.ly",
"4sq.com",
"4url.cc",
"6url.com",
"7.ly",
"a.gg",
"a.nf",
"a2a.me",
"abbrr.com",
"adf.ly",
"adjix.com",
"a.gg",
"alturl.com",
"a.nf",
"anon.to",
"apple.news",
"atu.ca",
"b23.ru",
"bacn.me",
"bc.vc",
"bfy.tw",
"binged.it",
"bit.do",
"bit.ly",
"bizj.us",
"bkite.com",
"bloat.me",
"budurl.com",
"buff.ly",
"buk.me",
"burnurl.com",
"chilp.it",
"chn.ge",
"clck.ru",
"clickmeter.com",
"cli.gs",
"c-o.in",
"chilp.it",
"clck.ru",
"cli.gs",
"clickmeter.com",
"cort.as",
"cut.ly",
"cutt.ly",
"cuturl.com",
"dai.ly",
"dailym.ai",
"db.tt",
"decenturl.com",
"decenturl.com",
"dfl8.me",
"digbig.com",
"digg.com",
"disq.us",
"dlvr.it",
"doiop.com",
"do.my",
"dwarfurl.com",
"dy.fi",
"easyuri.com",
@@ -76,35 +56,27 @@ SHORTENER_DOMAINS = [
"esyurl.com",
"ewerl.com",
"fa.b",
"fa.by",
"fb.me",
"fff.to",
"ff.im",
"fff.to",
"fhurl.com",
"fire.to",
"firsturl.de",
"firsturl.net",
"flic.kr",
"flq.us",
"fly2.ws",
"fon.gs",
"forms.gle",
"fwd4.me",
"gdurl.com",
"gg.gg",
"gl.am",
"go2cut.com",
"go2.me",
"go.9nl.com",
"go2.me",
"go2cut.com",
"goo.gl",
"goshrink.com",
"got.by",
"gowat.ch",
"gri.ms",
"gurl.es",
"hellotxt.com",
"hex.io",
"hongkiat.shorturl.com",
"hover.com",
"href.in",
"ht.ly",
@@ -113,15 +85,13 @@ SHORTENER_DOMAINS = [
"hurl.it",
"hurl.me",
"hurl.ws",
"ibb.co",
"icanhaz.com",
"idek.net",
"inreply.to",
"iscool.net",
"is.gd",
"iscool.net",
"iterasi.net",
"jijr.com",
"j.mp",
"jmp2.net",
"just.as",
"kissa.be",
@@ -129,23 +99,21 @@ SHORTENER_DOMAINS = [
"klck.me",
"korta.nu",
"krunchd.com",
"lat.ms",
"liip.to",
"liltext.com",
"lin.cr",
"linkbee.com",
"linkbun.ch",
"liurl.cn",
"lnkd.in",
"lnk.gd",
"lnk.in",
"ln-s.net",
"ln-s.ru",
"lnk.gd",
"lnk.in",
"lnkd.in",
"loopt.us",
"lru.jp",
"lt.tl",
"lurl.no",
"lyhyt.eu",
"metamark.net",
"migre.me",
"minilien.com",
@@ -153,71 +121,52 @@ SHORTENER_DOMAINS = [
"minurl.fr",
"moourl.com",
"myurl.in",
"nbcnews.to",
"ne1.net",
"njx.me",
"nn.nf",
"notlong.com",
"n.pr",
"nsfw.in",
"nyti.ms",
"om.ly",
"onforb.es",
"on.mktw.net",
"ow.ly",
"o-x.fr",
"pca.st",
"om.ly",
"ow.ly",
"pd.am",
"pic.gd",
"ping.fm",
"piurl.com",
"pnt.me",
"politi.co",
"poprl.com",
"posted.at",
"post.ly",
"posted.at",
"profile.to",
"q.gs",
"qicute.com",
"qlnk.net",
"qr.ae",
"qte.me",
"quip-art.com",
"rb6.me",
"rb.gy",
"read.bi",
"redir.ec",
"redirx.com",
"redr.me",
"reut.rs",
"rickroll.it",
"r.im",
"ri.ms",
"rickroll.it",
"riz.gd",
"rsmonkey.com",
"rubyurl.com",
"ru.ly",
"rubyurl.com",
"s7y.us",
"safe.mn",
"sharein.com",
"sharetabs.com",
"shorl.com",
"short.ie",
"short.to",
"shortlinks.co.uk",
"shortna.me",
"short.to",
"shorturl.at",
"shorturl.com",
"shoturl.us",
"shout.to",
"shrinkify.com",
"shrinkster.com",
"shrten.com",
"shrt.st",
"shrten.com",
"shrunkin.com",
"shw.me",
"simurl.com",
"smsh.me",
"sn.im",
"snipr.com",
"snipurl.com",
@@ -228,30 +177,24 @@ SHORTENER_DOMAINS = [
"starturl.com",
"sturly.com",
"su.pr",
"t.cn",
"t.co",
"tcrn.ch",
"tgr.ph",
"thrdl.es",
"tighturl.com",
"tiny.cc",
"tiny.pl",
"tiny123.com",
"tinyarro.ws",
"tiny.cc",
"tinylink.in",
"tiny.pl",
"tiny.tw",
"tinytw.it",
"tinyuri.ca",
"tinyurl.com",
"tinyvid.io",
"t.me",
"tnij.org",
"tnw.to",
"togoto.us",
"to.ly",
"traceurl.com",
"togoto.us",
"tr.im",
"tr.my",
"traceurl.com",
"turo.us",
"tweetburner.com",
"twirl.at",
@@ -261,69 +204,57 @@ SHORTENER_DOMAINS = [
"twiturl.de",
"twurl.cc",
"twurl.nl",
"u6e.de",
"ub0.cc",
"ukl.me.uk",
"u.mavrev.com",
"u.nu",
"u6e.de",
"ub0.cc",
"updating.me",
"ur1.ca",
"url.co.uk",
"url.ie",
"url4.eu",
"urlao.com",
"urlbrief.com",
"url.co.uk",
"urlcover.com",
"urlcut.com",
"urlenco.de",
"urlhawk.com",
"url.ie",
"urlkiss.com",
"urlot.com",
"urlpire.com",
"urlx.ie",
"urlx.org",
"urlzen.com",
"use.my",
"u.to",
"v.gd",
"virl.com",
"vl.am",
"vurl.com",
"vzturl.com",
"w3t.org",
"wapo.st",
"wapurl.co.uk",
"wipi.es",
"wp.me",
"xaddr.com",
"x.co",
"x.se",
"xaddr.com",
"xeeurl.com",
"xr.com",
"xrl.in",
"xrl.us",
"x.se",
"xurl.es",
"xurl.jp",
"xzb.cc",
"ye.pe",
"yep.it",
"yfrog.com",
"yhoo.it",
"ymlp.com",
"yuarel.com",
"yweb.com",
"zi.ma",
"zi.pe",
"zipmyurl.com",
"zurl.to",
"zurl.ws",
"zz.gd",
]
class URL:
def __init__(self, url: str) -> None:
if isinstance(url, bytes):
if type(url) == bytes:
url = url.decode()
self.url = url
@@ -331,27 +262,37 @@ class URL:
self.top_level = self.get_top_level()
self.is_shortened = False
def get_domain(self) -> str:
def get_domain(self) -> None:
"""Get the domain from a URL.
:param url: URL to parse
:type url: str
:returns: Domain name extracted from URL
:rtype: str
"""
return (
get_tld(self.url, as_object=True, fix_protocol=True)
.parsed_url.netloc.lower()
.lstrip("www.")
)
# TODO: Properly handle exception.
try:
return get_tld(self.url,
as_object=True,
fix_protocol=True).parsed_url.netloc.lower().lstrip("www.")
except Exception:
return None
def get_top_level(self) -> str:
def get_top_level(self) -> None:
"""Get only the top-level domain from a URL.
:param url: URL to parse
:type url: str
:returns: Top-level domain name extracted from URL
:rtype: str
"""
return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower()
# TODO: Properly handle exception.
try:
return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower()
except Exception:
return None
def check_if_shortened(self) -> bool:
"""Check if the URL is among list of shortener services.
@@ -367,10 +308,8 @@ class URL:
return self.is_shortened
def unshorten(self) -> Optional[str]:
def unshorten(self) -> None:
"""Unshorten the URL by requesting an HTTP HEAD response."""
res = requests.head(self.url)
if str(res.status_code).startswith("30"):
return res.headers["Location"]
return ""

133
mvt/common/utils.py Normal file
View File

@@ -0,0 +1,133 @@
# 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 datetime
import hashlib
import os
import re
def convert_mactime_to_unix(timestamp, from_2001: bool = True):
"""Converts Mac Standard Time to a Unix timestamp.
:param timestamp: MacTime timestamp (either int or float).
:type timestamp: int
:param from_2001: bool: Whether to (Default value = True)
:param from_2001: Default value = True)
:returns: Unix epoch timestamp.
"""
if not timestamp:
return None
# This is to fix formats in case of, for example, SMS messages database
# timestamp format.
if type(timestamp) == int and len(str(timestamp)) == 18:
timestamp = int(str(timestamp)[:9])
# MacTime counts from 2001-01-01.
if from_2001:
timestamp = timestamp + 978307200
# TODO: This is rather ugly. Happens sometimes with invalid timestamps.
try:
return datetime.datetime.utcfromtimestamp(timestamp)
except Exception:
return None
def convert_chrometime_to_unix(timestamp: int) -> int:
"""Converts Chrome timestamp to a Unix timestamp.
:param timestamp: Chrome timestamp as int.
:type timestamp: int
:returns: Unix epoch timestamp.
"""
epoch_start = datetime.datetime(1601, 1, 1)
delta = datetime.timedelta(microseconds=timestamp)
return epoch_start + delta
def convert_timestamp_to_iso(timestamp: str) -> str:
"""Converts Unix timestamp to ISO string.
:param timestamp: Unix timestamp.
:type timestamp: int
:returns: ISO timestamp string in YYYY-mm-dd HH:MM:SS.ms format.
:rtype: str
"""
try:
return timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")
except Exception:
return None
def check_for_links(text: str) -> list:
"""Checks if a given text contains HTTP links.
:param text: Any provided text.
:type text: str
:returns: Search results.
"""
return re.findall(r"(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
def get_sha256_from_file_path(file_path: str) -> str:
"""Calculate the SHA256 hash of a file from a file path.
:param file_path: Path to the file to hash
:returns: The SHA256 hash string
"""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as handle:
for byte_block in iter(lambda: handle.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
# Note: taken from here:
# https://stackoverflow.com/questions/57014259/json-dumps-on-dictionary-with-bytes-for-keys
def keys_bytes_to_string(obj) -> str:
"""Convert object keys from bytes to string.
:param obj: Object to convert from bytes to string.
:returns: Object converted to string.
:rtype: str
"""
new_obj = {}
if not isinstance(obj, dict):
if isinstance(obj, (tuple, list, set)):
value = [keys_bytes_to_string(x) for x in obj]
return value
else:
return obj
for key, value in obj.items():
if isinstance(key, bytes):
key = key.decode()
if isinstance(value, dict):
value = keys_bytes_to_string(value)
elif isinstance(value, (tuple, list, set)):
value = [keys_bytes_to_string(x) for x in value]
new_obj[key] = value
return new_obj
def secure_delete(file_path, rounds=10):
file_size = os.path.getsize(file_path)
with open(file_path, "br+", buffering=-1) as handle:
for i in range(rounds):
handle.seek(0)
handle.write(os.urandom(file_size))
os.remove(file_path)

View File

@@ -1,6 +1,6 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/
MVT_VERSION = "2.5.4"
MVT_VERSION = "1.6"

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/
@@ -23,30 +23,23 @@ class VTQuotaExceeded(Exception):
def virustotal_lookup(file_hash: str):
if MVT_VT_API_KEY not in os.environ:
raise VTNoKey(
"No VirusTotal API key provided: to use VirusTotal "
"lookups please provide your API key with "
"`export MVT_VT_API_KEY=<key>`"
)
raise VTNoKey("No VirusTotal API key provided: to use VirusTotal lookups please provide your API key with `export MVT_VT_API_KEY=<key>`")
headers = {
"User-Agent": "VirusTotal",
"Content-Type": "application/json",
"x-apikey": os.environ[MVT_VT_API_KEY],
}
res = requests.get(
f"https://www.virustotal.com/api/v3/files/{file_hash}", headers=headers
)
res = requests.get(f"https://www.virustotal.com/api/v3/files/{file_hash}", headers=headers)
if res.status_code == 200:
report = res.json()
return report["data"]
if res.status_code == 404:
elif res.status_code == 404:
log.info("Could not find results for file with hash %s", file_hash)
elif res.status_code == 429:
raise VTQuotaExceeded("You have exceeded the quota for your VirusTotal API key")
else:
raise Exception(f"Unexpected response from VirusTotal: {res.status_code}")
raise Exception("Unexpected response from VirusTotal: %s", res.status_code)
return None

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# 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/

297
mvt/ios/cli.py Normal file
View File

@@ -0,0 +1,297 @@
# 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 logging
import os
import click
from rich.logging import RichHandler
from rich.prompt import Confirm, Prompt
from simple_term_menu import TerminalMenu
from mvt.common.cmd_check_iocs import CmdCheckIOCS
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_IOC,
HELP_MSG_LIST_MODULES, HELP_MSG_MODULE,
HELP_MSG_OUTPUT, HELP_MSG_SERIAL)
from mvt.common.logo import logo
from mvt.common.options import MutuallyExclusiveOption
from mvt.common.updates import IndicatorsUpdates
from .cmd_check_backup import CmdIOSCheckBackup
from .cmd_check_fs import CmdIOSCheckFS
from .cmd_check_usb import CmdIOSCheckUSB
from .decrypt import DecryptBackup
from .lockdown import Lockdown
from .modules.backup import BACKUP_MODULES
from .modules.fs import FS_MODULES
from .modules.mixed import MIXED_MODULES
# Setup logging using Rich.
LOG_FORMAT = "[%(name)s] %(message)s"
logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__)
# Set this environment variable to a password if needed.
MVT_IOS_BACKUP_PASSWORD = "MVT_IOS_BACKUP_PASSWORD"
#==============================================================================
# Main
#==============================================================================
@click.group(invoke_without_command=False)
def cli():
logo()
#==============================================================================
# Command: version
#==============================================================================
@cli.command("version", help="Show the currently installed version of MVT")
def version():
return
#==============================================================================
# Command: decrypt-backup
#==============================================================================
@cli.command("decrypt-backup", help="Decrypt an encrypted iTunes backup")
@click.option("--destination", "-d", required=True,
help="Path to the folder where to store the decrypted backup")
@click.option("--password", "-p", cls=MutuallyExclusiveOption,
help=f"Password to use to decrypt the backup (or, set {MVT_IOS_BACKUP_PASSWORD} environment variable)",
mutually_exclusive=["key_file"])
@click.option("--key-file", "-k", cls=MutuallyExclusiveOption,
type=click.Path(exists=True),
help="File containing raw encryption key to use to decrypt the backup",
mutually_exclusive=["password"])
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def decrypt_backup(ctx, destination, password, key_file, backup_path):
backup = DecryptBackup(backup_path, destination)
if key_file:
if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Ignoring environment variable, using --key-file '%s' instead",
MVT_IOS_BACKUP_PASSWORD, key_file)
backup.decrypt_with_key_file(key_file)
elif password:
log.info("Your password may be visible in the process table because it was supplied on the command line!")
if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Ignoring %s environment variable, using --password argument instead",
MVT_IOS_BACKUP_PASSWORD)
backup.decrypt_with_password(password)
elif MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Using password from %s environment variable", MVT_IOS_BACKUP_PASSWORD)
backup.decrypt_with_password(os.environ[MVT_IOS_BACKUP_PASSWORD])
else:
sekrit = Prompt.ask("Enter backup password", password=True)
backup.decrypt_with_password(sekrit)
if not backup.can_process():
ctx.exit(1)
backup.process_backup()
#==============================================================================
# Command: extract-key
#==============================================================================
@cli.command("extract-key", help="Extract decryption key from an iTunes backup")
@click.option("--password", "-p",
help=f"Password to use to decrypt the backup (or, set {MVT_IOS_BACKUP_PASSWORD} environment variable)")
@click.option("--key-file", "-k",
help="Key file to be written (if unset, will print to STDOUT)",
required=False,
type=click.Path(exists=False, file_okay=True, dir_okay=False, writable=True))
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
def extract_key(password, key_file, backup_path):
backup = DecryptBackup(backup_path)
if password:
log.info("Your password may be visible in the process table because it was supplied on the command line!")
if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Ignoring %s environment variable, using --password argument instead",
MVT_IOS_BACKUP_PASSWORD)
elif MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Using password from %s environment variable", MVT_IOS_BACKUP_PASSWORD)
password = os.environ[MVT_IOS_BACKUP_PASSWORD]
else:
password = Prompt.ask("Enter backup password", password=True)
backup.decrypt_with_password(password)
backup.get_key()
if key_file:
backup.write_key(key_file)
#==============================================================================
# Command: check-backup
#==============================================================================
@cli.command("check-backup", help="Extract artifacts from an iTunes backup")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(ctx, iocs, output, fast, list_modules, module, backup_path):
cmd = CmdIOSCheckBackup(target_path=backup_path, results_path=output,
ioc_files=iocs, module_name=module, fast_mode=fast)
if list_modules:
cmd.list_modules()
return
log.info("Checking iTunes backup located at: %s", backup_path)
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the backup produced %d detections!",
len(cmd.timeline_detected))
#==============================================================================
# Command: check-fs
#==============================================================================
@cli.command("check-fs", help="Extract artifacts from a full filesystem dump")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.argument("DUMP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_fs(ctx, iocs, output, fast, list_modules, module, dump_path):
cmd = CmdIOSCheckFS(target_path=dump_path, results_path=output,
ioc_files=iocs, module_name=module, fast_mode=fast)
if list_modules:
cmd.list_modules()
return
log.info("Checking iOS filesystem located at: %s", dump_path)
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the iOS filesystem produced %d detections!",
len(cmd.timeline_detected))
#==============================================================================
# Command: check-usb
#==============================================================================
@cli.command("check-usb", help="Extract artifacts from a live iPhone through USB")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.pass_context
def check_usb(ctx, serial, iocs, output, fast, list_modules, module):
cmd = CmdIOSCheckUSB(results_path=output, ioc_files=iocs,
module_name=module, fast_mode=fast,
serial=serial)
if list_modules:
cmd.list_modules()
return
log.info("Checking iPhone through USB, this may take a while")
cmd.run()
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the data produced %d detections!",
len(cmd.timeline_detected))
#==============================================================================
# Command: clear-certs
#==============================================================================
@cli.command("clear-certs", help="Clear iOS lockdown certificates")
@click.pass_context
def clear_certs(ctx):
lock = Lockdown()
certs = lock.find_certs()
if not certs:
log.info("No iOS lockdown certificates found")
return
choices = []
for cert in certs:
choices.append(os.path.basename(cert))
log.info("Found lockdown certificate at %s", cert)
choices.append("Cancel")
terminal_menu = TerminalMenu(
choices,
title="Select which certificates to delete:",
multi_select=True,
show_multi_select_hint=True,
)
terminal_menu.show()
if "Cancel" in terminal_menu.chosen_menu_entries:
log.info("Cancel, not proceeding")
return
confirmed = Confirm.ask(f"You have selected {', '.join(terminal_menu.chosen_menu_entries)}. "
"Are you sure you want to proceed deleting them?")
if not confirmed:
log.info("Not proceeding")
return
for choice in terminal_menu.chosen_menu_entries:
try:
lock.delete_cert(choice)
except PermissionError:
log.error("Not enough permissions to delete certificate at \"%s\": "
"try launching this command with sudo", choice)
else:
log.info("Deleted lockdown certificate \"%s\"", choice)
#==============================================================================
# Command: check-iocs
#==============================================================================
@cli.command("check-iocs", help="Compare stored JSON results to provided indicators")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@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)
cmd.modules = BACKUP_MODULES + FS_MODULES + MIXED_MODULES
if list_modules:
cmd.list_modules()
return
cmd.run()
#==============================================================================
# Command: download-iocs
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
def download_iocs():
ioc_updates = IndicatorsUpdates()
ioc_updates.update()

View File

@@ -0,0 +1,29 @@
# 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 logging
from mvt.common.command import Command
from .modules.backup import BACKUP_MODULES
from .modules.mixed import MIXED_MODULES
log = logging.getLogger(__name__)
class CmdIOSCheckBackup(Command):
name = "check-backup"
modules = BACKUP_MODULES + MIXED_MODULES
def __init__(self, target_path: str = None, results_path: str = None,
ioc_files: list = [], module_name: str = None, serial: str = None,
fast_mode: bool = False):
super().__init__(target_path=target_path, results_path=results_path,
ioc_files=ioc_files, module_name=module_name,
serial=serial, fast_mode=fast_mode, log=log)
def module_init(self, module):
module.is_backup = True

Some files were not shown because too many files have changed in this diff Show More