1
mirror of https://github.com/mvt-project/mvt synced 2025-11-13 01:37:36 +01:00

Compare commits

..

53 Commits

Author SHA1 Message Date
dependabot[bot]
b637a115df Bump requests from 2.32.4 to 2.32.5
Bumps [requests](https://github.com/psf/requests) from 2.32.4 to 2.32.5.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.4...v2.32.5)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 17:41:40 +00:00
dependabot[bot]
b4a8dd226a Bump mkdocs-material from 9.6.18 to 9.6.20 (#691)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.18 to 9.6.20.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.18...9.6.20)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-version: 9.6.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 19:40:29 +02:00
dependabot[bot]
88213e12c9 Bump mkdocs-autorefs from 1.4.2 to 1.4.3 (#686)
Bumps [mkdocs-autorefs](https://github.com/mkdocstrings/autorefs) from 1.4.2 to 1.4.3.
- [Release notes](https://github.com/mkdocstrings/autorefs/releases)
- [Changelog](https://github.com/mkdocstrings/autorefs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/autorefs/compare/1.4.2...1.4.3)

---
updated-dependencies:
- dependency-name: mkdocs-autorefs
  dependency-version: 1.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-05 18:30:36 +02:00
r-tx
f75b8e186a add iOS 18.6.2 (#682)
* iOS 18.6.2

* iOS 18.6.2

---------

Co-authored-by: r-tx <r-tx@users.noreply.github.com>
Co-authored-by: Tek <tek@randhome.io>
2025-08-26 13:52:55 +02:00
dependabot[bot]
5babc1fcf3 Bump mkdocs-material from 9.6.17 to 9.6.18 (#683)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.17 to 9.6.18.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.17...9.6.18)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-version: 9.6.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 11:25:22 +02:00
besendorf
b723ebf28e move test dependencies to dev dependency group (#679) 2025-08-21 16:10:03 +02:00
dependabot[bot]
616e870212 Bump mkdocs-material from 9.6.16 to 9.6.17 (#678)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.16 to 9.6.17.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.16...9.6.17)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-version: 9.6.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tek <tek@randhome.io>
2025-08-20 11:13:59 +02:00
Tek
847b0e087b Adds iOS 18.6.1 (#681) 2025-08-20 11:10:20 +02:00
dependabot[bot]
86a0772eb2 Bump cryptography from 45.0.5 to 45.0.6 (#675)
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.5 to 45.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.5...45.0.6)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 45.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 10:38:19 +02:00
github-actions[bot]
7d0be9db4f Add new iOS versions and build numbers (#673)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-07-31 13:20:34 +02:00
dependabot[bot]
4e120b2640 Bump pydantic-settings from 2.9.1 to 2.10.1 (#655)
Bumps [pydantic-settings](https://github.com/pydantic/pydantic-settings) from 2.9.1 to 2.10.1.
- [Release notes](https://github.com/pydantic/pydantic-settings/releases)
- [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.9.1...2.10.1)

---
updated-dependencies:
- dependency-name: pydantic-settings
  dependency-version: 2.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 22:58:12 +02:00
dependabot[bot]
dbe9e5db9b Bump mkdocstrings from 0.29.1 to 0.30.0 (#671)
Bumps [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) from 0.29.1 to 0.30.0.
- [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases)
- [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.29.1...0.30.0)

---
updated-dependencies:
- dependency-name: mkdocstrings
  dependency-version: 0.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tek <tek@randhome.io>
2025-07-28 22:42:37 +02:00
dependabot[bot]
0b00398729 Bump rich from 14.0.0 to 14.1.0 (#670)
Bumps [rich](https://github.com/Textualize/rich) from 14.0.0 to 14.1.0.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.0.0...v14.1.0)

---
updated-dependencies:
- dependency-name: rich
  dependency-version: 14.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 22:37:42 +02:00
dependabot[bot]
87034d2c7a Bump mkdocs-material from 9.6.14 to 9.6.16 (#672)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.14 to 9.6.16.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.14...9.6.16)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-version: 9.6.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 22:29:04 +02:00
besendorf
595a2f6536 Merge pull request #656 from mvt-project/fix/install_non_market_apps
remove deprecated install_non_market_apps permission check
2025-07-22 19:32:05 +02:00
besendorf
8ead44a31e Merge branch 'main' into fix/install_non_market_apps 2025-07-22 19:12:44 +02:00
besendorf
5c19d02a73 Merge pull request #659 from mvt-project/fix/tcc
fix #579 TCC: no such table: access
2025-07-22 19:02:32 +02:00
besendorf
14ebc9ee4e Merge branch 'main' into fix/tcc 2025-07-22 18:56:10 +02:00
besendorf
de53cc07f8 Merge pull request #660 from mvt-project/fix/safari_browserstate
catch sqlite exception in safari_browserstate.py
2025-07-22 18:33:39 +02:00
besendorf
22e066fc4a Merge branch 'main' into fix/safari_browserstate 2025-07-22 18:20:07 +02:00
besendorf
242052b8ec Merge branch 'main' into fix/install_non_market_apps 2025-07-17 11:45:34 +02:00
dependabot[bot]
1df61b5bbf Bump cryptography from 45.0.4 to 45.0.5 (#661)
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.4 to 45.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.4...45.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 45.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 21:03:08 +02:00
besendorf
b691de2cc0 catch sqlite exception in safari_browserstate.py 2025-07-04 17:52:05 +02:00
besendorf
10915f250c catch tcc error 2025-07-04 17:46:50 +02:00
besendorf
c60cef4009 Merge branch 'main' into fix/install_non_market_apps 2025-07-04 17:04:13 +02:00
besendorf
dda798df8e Merge pull request #658 from mvt-project/fix-mms
initialise message_links in backup parser to fix sms module bug
2025-07-04 15:32:47 +02:00
besendorf
ffe6ad2014 initialise message_links in backup parser to fix sms module bug 2025-07-04 15:29:36 +02:00
dependabot[bot]
a125b20fc5 Bump pydantic from 2.11.5 to 2.11.7 (#651)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.5 to 2.11.7.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.5...v2.11.7)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.11.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 20:59:41 +02:00
besendorf
49108e67e2 remove deprecated install_non_market_apps permission check 2025-07-02 10:11:35 +02:00
dependabot[bot]
883b450601 Bump mkdocstrings from 0.23.0 to 0.29.1 (#649)
Bumps [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) from 0.23.0 to 0.29.1.
- [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases)
- [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.23.0...0.29.1)

---
updated-dependencies:
- dependency-name: mkdocstrings
  dependency-version: 0.29.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tek <tek@randhome.io>
2025-06-20 11:29:34 +02:00
dependabot[bot]
ce813568ff Bump mkdocs-autorefs from 1.2.0 to 1.4.2 (#648)
Bumps [mkdocs-autorefs](https://github.com/mkdocstrings/autorefs) from 1.2.0 to 1.4.2.
- [Release notes](https://github.com/mkdocstrings/autorefs/releases)
- [Changelog](https://github.com/mkdocstrings/autorefs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/autorefs/compare/1.2.0...1.4.2)

---
updated-dependencies:
- dependency-name: mkdocs-autorefs
  dependency-version: 1.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-20 11:28:24 +02:00
dependabot[bot]
93303f181a Bump mkdocs-material from 9.5.42 to 9.6.14 (#647)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.42 to 9.6.14.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.42...9.6.14)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-version: 9.6.14
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tek <tek@randhome.io>
2025-06-20 11:24:12 +02:00
dependabot[bot]
bee453a090 Bump cryptography from 45.0.3 to 45.0.4 (#645)
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.3 to 45.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.3...45.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 45.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-20 11:22:26 +02:00
dependabot[bot]
42106aa4d6 Bump pyahocorasick from 2.1.0 to 2.2.0 (#646)
Bumps [pyahocorasick](https://github.com/WojciechMula/pyahocorasick) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/WojciechMula/pyahocorasick/releases)
- [Changelog](https://github.com/WojciechMula/pyahocorasick/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/WojciechMula/pyahocorasick/compare/2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: pyahocorasick
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-20 11:19:57 +02:00
Tek
95076c8f71 Create dependabot.yml (#644) 2025-06-20 11:17:40 +02:00
dependabot[bot]
c9ac12f336 Bump requests from 2.32.2 to 2.32.4 (#642)
Bumps [requests](https://github.com/psf/requests) from 2.32.2 to 2.32.4.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-19 23:55:42 +02:00
Tek
486e3e7e9b Update global_preferences.py (#641)
Added a warning for lockdown mode when the property is not present
---------

Co-authored-by: makitos666 <makitos666@gmail.com>
2025-06-19 23:51:59 +02:00
besendorf
be1fc3bd8b Update NSKeyedUnarchiver (#636) 2025-06-12 22:42:02 +02:00
Tek
4757cff262 Fixes date parsing issue in tombstones (#635) 2025-06-12 20:49:31 +02:00
Tek
61f51caf31 Freeze versions and bump version (#632)
* Freeze versions and bump version
* Drops support for python below 3.10
2025-06-12 16:33:15 +02:00
besendorf
511063fd0e Update pyproject.toml (#630) 2025-06-04 13:00:04 +02:00
scribblemaniac
88bc5672cb Upgrade main dockerfile runtime to ubuntu:24.04 (#619)
Co-authored-by: Tek <tek@randhome.io>
2025-05-14 11:34:40 +02:00
github-actions[bot]
0fce0acf7a Add new iOS versions and build numbers (#626)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-05-14 11:12:13 +02:00
github-actions[bot]
61f95d07d3 Add new iOS versions and build numbers (#625)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-05-12 22:37:46 +02:00
ping2A
3dedd169c4 Fix issue #574 for a module without IOCs output (#620)
* Fix issue #574 for a module without IOCs output
2025-04-30 10:30:39 +02:00
Tek
e34e03d3a3 Fixes Android Dumpsys ADB parsing issue 2025-04-18 17:43:08 +02:00
github-actions[bot]
34374699ce Add new iOS versions and build numbers (#622)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-04-17 09:46:17 +02:00
github-actions[bot]
cf5aa7c89f Add new iOS versions and build numbers (#618)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-04-01 16:04:06 +02:00
Donncha Ó Cearbhaill
2766739512 Fix bug where default values were dropped when parsing protobuf tombstones (#617) 2025-03-11 14:10:34 +01:00
cacu
9c84afb4b0 Update logo.py (#615)
add instructions to update mvt via pipx
2025-03-11 13:46:59 +01:00
Donncha Ó Cearbhaill
80fc8bd879 Fix YAML format (#611) 2025-02-21 15:48:00 +01:00
Donncha Ó Cearbhaill
ca41f7f106 Always open automatic PRs as drafts (#609) 2025-02-21 15:35:06 +01:00
github-actions[bot]
55ddd86ad5 Add new iOS versions and build numbers (#607)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-02-21 15:24:27 +01:00
194 changed files with 3458 additions and 2290 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.9', '3.10'] # , '3.11']
python-version: ['3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
@@ -35,4 +35,4 @@ jobs:
if: github.event_name == 'pull_request'
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
junitxml-path: ./pytest.xml

View File

@@ -21,6 +21,7 @@ jobs:
title: '[auto] Update iOS releases and versions'
commit-message: Add new iOS versions and build numbers
branch: auto/add-new-ios-releases
draft: true
body: |
This is an automated pull request to update the iOS releases and version numbers.
add-paths: |

View File

@@ -103,7 +103,7 @@ RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
# Create main image
FROM ubuntu:22.04 as main
FROM ubuntu:24.04 as main
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
@@ -135,8 +135,7 @@ COPY --from=build-usbmuxd /build /
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 \
&& PIP_NO_CACHE_DIR=1 pip3 install --break-system-packages ./mvt \
&& apt-get remove -y python3-pip git && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf mvt

View File

@@ -23,7 +23,7 @@ install:
python3 -m pip install --upgrade -e .
test-requirements:
python3 -m pip install --upgrade -r test-requirements.txt
python3 -m pip install --upgrade --group dev
generate-proto-parsers:
# Generate python parsers for protobuf files

View File

@@ -1,26 +1,42 @@
# Deprecation of ADB command in MVT
# Check over ADB
In order to check an Android device over the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) you will first need to install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you have installed [Android Studio](https://developer.android.com/studio/) you should already have access to `adb` and other utilities.
While many Linux distributions already package Android Platform Tools (for example `android-platform-tools-base` on Debian), it is preferable to install the most recent version from the official website. Packaged versions might be outdated and incompatible with most recent Android handsets.
Next you will need to enable debugging on the Android device you are testing. [Please follow the official instructions on how to do so.](https://developer.android.com/studio/command-line/adb)
## Connecting over USB
The easiest way to check the device is over a USB transport. You will need to have USB debugging enabled and the device plugged into your computer. If everything is configured appropriately you should see your device when launching the command `adb devices`.
Now you can try launching MVT with:
```bash
mvt-android check-adb --output /path/to/results
```
If you have previously started an adb daemon MVT will alert you and require you to kill it with `adb kill-server` and relaunch the command.
!!! warning
MVT relies on the Python library [adb-shell](https://pypi.org/project/adb-shell/) to connect to an Android device, which relies on libusb for the USB transport. Because of known driver issues, Windows users [are recommended](https://github.com/JeffLIrion/adb_shell/issues/118) to install appropriate drivers using [Zadig](https://zadig.akeo.ie/). Alternatively, an easier option might be to use the TCP transport and connect over Wi-Fi as describe next.
The `mvt-android check-adb` command has been deprecated and removed from MVT.
## Connecting over Wi-FI
The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT due to several technical and forensic limitations.
When connecting to the device over USB is not possible or not working properly, an alternative option is to connect over the network. In order to do so, first launch an adb daemon at a fixed port number:
## Reasons for Deprecation
```bash
adb tcpip 5555
```
1. **Inconsistent Data Collection Across Devices**
Android devices vary significantly in their system architecture, security policies, and available diagnostic logs. This inconsistency makes it difficult to ensure that MVT can reliably collect necessary forensic data across all devices.
Then you can specify the IP address of the phone with the adb port number to MVT like so:
2. **Incomplete Forensic Data Acquisition**
The `check-adb` command did not retrieve a full forensic snapshot of all available data on the device. For example, critical logs such as the **full bugreport** were not systematically collected, leading to potential gaps in forensic analysis. This can be a serious problem in scenarios where the analyst only had one time access to the Android device.
```bash
mvt-android check-adb --serial 192.168.1.20:5555 --output /path/to/results
```
4. **Code Duplication and Difficulty Ensuring Consistent Behavior Across Sources**
Similar forensic data such as "dumpsys" logs were being loaded and parsed by MVT's ADB, AndroidQF and Bugreport commands. Multiple modules were needed to handle each source format which created duplication leading to inconsistent
behavior and difficulties in maintaining the code base.
Where `192.168.1.20` is the correct IP address of your device.
5. **Alignment with iOS Workflow**
MVTs forensic workflow for iOS relies on pre-extracted artifacts, such as iTunes backups or filesystem dumps, rather than preforming commands or interactions directly on a live device. Removing the ADB functionality ensures a more consistent methodology across both Android and iOS mobile forensic.
## MVT modules requiring root privileges
## Alternative: Using AndroidQF for Forensic Data Collection
To replace the deprecated ADB-based approach, forensic analysts should use [AndroidQF](https://github.com/mvt-project/androidqf) for comprehensive data collection, followed by MVT for forensic analysis. The workflow is outlined in the MVT [Android methodology](./methodology.md)
Of the currently available `mvt-android check-adb` modules a handful require root privileges to function correctly. This is because certain files, such as browser history and SMS messages databases are not accessible with user privileges through adb. These modules are to be considered OPTIONALLY available in case the device was already jailbroken. **Do NOT jailbreak your own device unless you are sure of what you are doing!** Jailbreaking your phone exposes it to considerable security risks!

View File

@@ -1,53 +1,23 @@
# Methodology for Android forensic
Unfortunately Android devices provide fewer complete forensically useful datasources than their iOS cousins. Unlike iOS, the Android backup feature only provides a limited about of relevant data.
Android diagnostic logs such as *bugreport files* can be inconsistent in format and structure across different Android versions and device vendors. The limited diagnostic information available makes it difficult to triage potential compromises, and because of this `mvt-android` capabilities are limited as well.
Unfortunately Android devices provide much less observability than their iOS cousins. Android stores very little diagnostic information useful to triage potential compromises, and because of this `mvt-android` capabilities are limited as well.
However, not all is lost.
## Check Android devices with AndroidQF and MVT
## Check installed Apps
The [AndroidQF](https://github.com/mvt-project/androidqf) tool can be used to collect a wide range of forensic artifacts from an Android device including an Android backup, a bugreport file, and a range of system logs. MVT natively supports analyzing the generated AndroidQF output for signs of device compromise.
Because malware attacks over Android typically take the form of malicious or backdoored apps, the very first thing you might want to do is to extract and verify all installed Android packages and triage quickly if there are any which stand out as malicious or which might be atypical.
### Why Use AndroidQF?
While it is out of the scope of this documentation to dwell into details on how to analyze Android apps, MVT does allow to easily and automatically extract information about installed apps, download copies of them, and quickly look them up on services such as [VirusTotal](https://www.virustotal.com).
- **Complete and raw data extraction**
AndroidQF collects full forensic artifacts using an on-device forensic collection agent, ensuring that no crucial data is overlooked. The data collection does not depended on the shell environment or utilities available on the device.
- **Consistent and standardized output**
By collecting a predefined and complete set of forensic files, AndroidQF ensures consistency in data acquisition across different Android devices.
- **Future-proof analysis**
Since the full forensic artifacts are preserved, analysts can extract new evidence or apply updated analysis techniques without requiring access to the original device.
- **Cross-platform tool without dependencies**
AndroidQF is a standalone Go binary which can be used to remotely collect data from an Android device without the device owner needing to install MVT or a Python environment.
### Workflow for Android Forensic Analysis with AndroidQF
With AndroidQF the analysis process is split into a separate data collection and data analysis stages.
1. **Extract Data Using AndroidQF**
Deploy the AndroidQF forensic collector to acquire all relevant forensic artifacts from the Android device.
2. **Analyze Extracted Data with MVT**
Use the `mvt-android check-androidqf` command to perform forensic analysis on the extracted artifacts.
By separating artifact collection from forensic analysis, this approach ensures a more reliable and scalable methodology for Android forensic investigations.
For more information, refer to the [AndroidQF project documentation](https://github.com/mvt-project/androidqf).
!!! info "Using VirusTotal"
Please note that in order to use VirusTotal lookups you are required to provide your own API key through the `MVT_VT_API_KEY` environment variable. You should also note that VirusTotal enforces strict API usage. Be mindful that MVT might consume your hourly search quota.
## Check the device over Android Debug Bridge
The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT.
Some additional diagnostic information can be extracted from the phone using the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb). `mvt-android` allows to automatically extract information including [dumpsys](https://developer.android.com/studio/command-line/dumpsys) results, details on installed packages (without download), running processes, presence of root binaries and packages, and more.
See the [Android ADB documentation](./adb.md) for more information.
## Check an Android Backup (SMS messages)
Although Android backups are becoming deprecated, it is still possible to generate one. Unfortunately, because apps these days typically favor backup over the cloud, the amount of data available is limited.
The `mvt-android check-androidqf` command will automatically check an Android backup and SMS messages if an SMS backup is included in the AndroidQF extraction.
The `mvt-android check-backup` command can also be used directly with an Android backup file.
Although Android backups are becoming deprecated, it is still possible to generate one. Unfortunately, because apps these days typically favor backup over the cloud, the amount of data available is limited. Currently, `mvt-android check-backup` only supports checking SMS messages containing links.

View File

@@ -31,4 +31,21 @@ Test if the image was created successfully:
docker run -it mvt
```
If a prompt is spawned successfully, you can close it with `exit`.
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
docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt
```
**Please note:** the `--privileged` parameter is generally regarded as a security risk. If you want to learn more about this check out [this explainer on container escapes](https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/) as it gives access to the whole system.
Recent versions of Docker provide a `--device` parameter allowing to specify a precise USB device without enabling `--privileged`:
```bash
docker run -it --device=/dev/<your_usb_port> mvt
```

View File

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

View File

@@ -1,13 +1,11 @@
[project]
name = "mvt"
dynamic = ["version"]
authors = [
{name = "Claudio Guarnieri", email = "nex@nex.sx"}
]
authors = [{ name = "Claudio Guarnieri", email = "nex@nex.sx" }]
maintainers = [
{name = "Etienne Maynier", email = "tek@randhome.io"},
{name = "Donncha Ó Cearbhaill", email = "donncha.ocearbhaill@amnesty.org"},
{name = "Rory Flynn", email = "rory.flynn@amnesty.org"}
{ name = "Etienne Maynier", email = "tek@randhome.io" },
{ name = "Donncha Ó Cearbhaill", email = "donncha.ocearbhaill@amnesty.org" },
{ name = "Rory Flynn", email = "rory.flynn@amnesty.org" },
]
description = "Mobile Verification Toolkit"
readme = "README.md"
@@ -16,46 +14,60 @@ classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Information Technology",
"Operating System :: OS Independent",
"Programming Language :: Python"
"Programming Language :: Python",
]
dependencies = [
"click >=8.1.3",
"rich >=12.6.0",
"tld >=0.12.6",
"requests >=2.28.1",
"simplejson >=3.17.6",
"packaging >=21.3",
"appdirs >=1.4.4",
"iOSbackup >=0.9.923",
"cryptography >=42.0.5",
"pyyaml >=6.0",
"pyahocorasick >= 2.0.0",
"betterproto >=1.2.0",
"pydantic >= 2.10.0",
"pydantic-settings >= 2.7.0",
'backports.zoneinfo; python_version < "3.9"',
"click==8.2.1",
"rich==14.1.0",
"tld==0.13.1",
"requests==2.32.5",
"simplejson==3.20.1",
"packaging==25.0",
"appdirs==1.4.4",
"iOSbackup==0.9.925",
"adb-shell[usb]==0.4.4",
"libusb1==3.3.1",
"cryptography==45.0.6",
"PyYAML>=6.0.2",
"pyahocorasick==2.2.0",
"betterproto==1.2.5",
"pydantic==2.11.7",
"pydantic-settings==2.10.1",
"NSKeyedUnArchiver==1.5.2",
"python-dateutil==2.9.0.post0",
]
requires-python = ">= 3.8"
requires-python = ">= 3.10"
[project.urls]
homepage = "https://docs.mvt.re/en/latest/"
repository = "https://github.com/mvt-project/mvt"
[project.scripts]
mvt-ios = "mvt.ios:cli"
mvt-android = "mvt.android:cli"
mvt-ios = "mvt.ios:cli"
mvt-android = "mvt.android:cli"
[dependency-groups]
dev = [
"requests>=2.31.0",
"pytest>=7.4.3",
"pytest-cov>=4.1.0",
"pytest-github-actions-annotate-failures>=0.2.0",
"pytest-mock>=3.14.0",
"stix2>=3.0.1",
"ruff>=0.1.6",
"mypy>=1.7.1",
"betterproto[compiler]",
]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[tool.coverage.run]
omit = [
"tests/*",
]
omit = ["tests/*"]
[tool.coverage.html]
directory= "htmlcov"
directory = "htmlcov"
[tool.mypy]
install_types = true
@@ -65,15 +77,13 @@ packages = "src"
[tool.pytest.ini_options]
addopts = "-ra -q --cov=mvt --cov-report html --junitxml=pytest.xml --cov-report=term-missing:skip-covered"
testpaths = [
"tests"
]
testpaths = ["tests"]
[tool.ruff.lint]
select = ["C90", "E", "F", "W"] # flake8 default set
select = ["C90", "E", "F", "W"] # flake8 default set
ignore = [
"E501", # don't enforce line length violations
"C901", # complex-structure
"E501", # don't enforce line length violations
"C901", # complex-structure
# These were previously ignored but don't seem to be required:
# "E265", # no-space-after-block-comment
@@ -85,14 +95,14 @@ ignore = [
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] # unused-import
"__init__.py" = ["F401"] # unused-import
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.setuptools]
include-package-data = true
package-dir = {"" = "src"}
package-dir = { "" = "src" }
[tool.setuptools.packages.find]
where = ["src"]
@@ -101,4 +111,4 @@ where = ["src"]
mvt = ["ios/data/*.json"]
[tool.setuptools.dynamic]
version = {attr = "mvt.common.version.MVT_VERSION"}
version = { attr = "mvt.common.version.MVT_VERSION" }

View File

@@ -14,10 +14,10 @@ class DumpsysAccessibilityArtifact(AndroidArtifact):
return
for result in self.results:
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def parse(self, content: str) -> None:

View File

@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import base64
import binascii
import hashlib
from .artifact import AndroidArtifact
@@ -89,11 +90,16 @@ class DumpsysADBArtifact(AndroidArtifact):
else:
key_base64, user = user_key, b""
key_raw = base64.b64decode(key_base64)
key_fingerprint = hashlib.md5(key_raw).hexdigest().upper()
key_fingerprint_colon = ":".join(
[key_fingerprint[i : i + 2] for i in range(0, len(key_fingerprint), 2)]
)
try:
key_raw = base64.b64decode(key_base64)
key_fingerprint = hashlib.md5(key_raw).hexdigest().upper()
key_fingerprint_colon = ":".join(
[key_fingerprint[i : i + 2] for i in range(0, len(key_fingerprint), 2)]
)
except binascii.Error:
# Impossible to parse base64
key_fingerprint_colon = ""
return {
"user": user.decode("utf-8"),
"fingerprint": key_fingerprint_colon,

View File

@@ -4,9 +4,9 @@
# https://license.mvt.re/1.1/
from datetime import datetime
from typing import Any, Dict, List, Union
from mvt.common.utils import convert_datetime_to_iso
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
from .artifact import AndroidArtifact
@@ -20,9 +20,9 @@ class DumpsysAppopsArtifact(AndroidArtifact):
Parser for dumpsys app ops info
"""
def serialize(self, result: ModuleAtomicResult) -> ModuleSerializedResult:
def serialize(self, record: dict) -> Union[dict, list]:
records = []
for perm in result["permissions"]:
for perm in record["permissions"]:
if "entries" not in perm:
continue
@@ -33,7 +33,7 @@ class DumpsysAppopsArtifact(AndroidArtifact):
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{result['package_name']} access to "
"data": f"{record['package_name']} access to "
f"{perm['name']}: {entry['access']}",
}
)
@@ -43,51 +43,48 @@ class DumpsysAppopsArtifact(AndroidArtifact):
def check_indicators(self) -> None:
for result in self.results:
if self.indicators:
ioc_match = self.indicators.check_app_id(result.get("package_name"))
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
self.get_slug(), ioc_match.message, "", result
)
ioc = self.indicators.check_app_id(result.get("package_name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
# We use a placeholder entry to create a basic alert even without permission entries.
placeholder_entry = {"access": "Unknown", "timestamp": ""}
detected_permissions = []
for perm in result["permissions"]:
if (
perm["name"] in RISKY_PERMISSIONS
# and perm["access"] == "allow"
):
for entry in sorted(
perm["entries"] or [placeholder_entry],
key=lambda x: x["timestamp"],
):
cleaned_result = result.copy()
cleaned_result["permissions"] = [perm]
self.alertstore.medium(
self.get_slug(),
f"Package '{result['package_name']}' had risky permission '{perm['name']}' set to '{entry['access']}' at {entry['timestamp']}",
detected_permissions.append(perm)
for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]):
self.log.warning(
"Package '%s' had risky permission '%s' set to '%s' at %s",
result["package_name"],
perm["name"],
entry["access"],
entry["timestamp"],
cleaned_result,
)
elif result["package_name"] in RISKY_PACKAGES:
for entry in sorted(
perm["entries"] or [placeholder_entry],
key=lambda x: x["timestamp"],
):
cleaned_result = result.copy()
cleaned_result["permissions"] = [perm]
self.alertstore.medium(
self.get_slug(),
f"Risky package '{result['package_name']}' had '{perm['name']}' permission set to '{entry['access']}' at {entry['timestamp']}",
detected_permissions.append(perm)
for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]):
self.log.warning(
"Risky package '%s' had '%s' permission set to '%s' at %s",
result["package_name"],
perm["name"],
entry["access"],
entry["timestamp"],
cleaned_result,
)
if detected_permissions:
# We clean the result to only include the risky permission, otherwise the timeline
# will be polluted with all the other irrelevant permissions
cleaned_result = result.copy()
cleaned_result["permissions"] = detected_permissions
self.detected.append(cleaned_result)
def parse(self, output: str) -> None:
# self.results: List[Dict[str, Any]] = []
self.results: List[Dict[str, Any]] = []
perm = {}
package = {}
entry = {}

View File

@@ -3,9 +3,9 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from typing import Union
from .artifact import AndroidArtifact
from mvt.common.module_types import ModuleSerializedResult, ModuleAtomicResult
class DumpsysBatteryDailyArtifact(AndroidArtifact):
@@ -13,7 +13,7 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
Parser for dumpsys dattery daily updates.
"""
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
def serialize(self, record: dict) -> Union[dict, list]:
return {
"timestamp": record["from"],
"module": self.__class__.__name__,
@@ -27,10 +27,10 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
return
for result in self.results:
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def parse(self, output: str) -> None:

View File

@@ -16,10 +16,10 @@ class DumpsysBatteryHistoryArtifact(AndroidArtifact):
return
for result in self.results:
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def parse(self, data: str) -> None:

View File

@@ -20,12 +20,10 @@ class DumpsysDBInfoArtifact(AndroidArtifact):
for result in self.results:
path = result.get("path", "")
for part in path.split("/"):
ioc_match = self.indicators.check_app_id(part)
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
self.get_slug(), ioc_match.message, "", result
)
ioc = self.indicators.check_app_id(part)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def parse(self, output: str) -> None:

View File

@@ -12,12 +12,10 @@ class DumpsysPackageActivitiesArtifact(AndroidArtifact):
return
for activity in self.results:
ioc_match = self.indicators.check_app_id(activity["package_name"])
if ioc_match:
activity["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
self.get_slug(), ioc_match.message, "", activity
)
ioc = self.indicators.check_app_id(activity["package_name"])
if ioc:
activity["matched_indicator"] = ioc
self.detected.append(activity)
continue
def parse(self, content: str):

View File

@@ -4,39 +4,35 @@
# https://license.mvt.re/1.1/
import re
from typing import Any, Dict, List
from typing import Any, Dict, List, Union
from mvt.android.utils import ROOT_PACKAGES
from .artifact import AndroidArtifact
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
class DumpsysPackagesArtifact(AndroidArtifact):
def check_indicators(self) -> None:
for result in self.results:
# XXX: De-duplication Package detections
if result["package_name"] in ROOT_PACKAGES:
self.alertstore.medium(
self.get_slug(),
f'Found an installed package related to rooting/jailbreaking: "{result["package_name"]}"',
"",
result,
self.log.warning(
'Found an installed package related to rooting/jailbreaking: "%s"',
result["package_name"],
)
self.alertstore.log_latest()
self.detected.append(result)
continue
if not self.indicators:
continue
ioc_match = self.indicators.check_app_id(result.get("package_name", ""))
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
self.alertstore.log_latest()
ioc = self.indicators.check_app_id(result.get("package_name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
def serialize(self, record: dict) -> Union[dict, list]:
records = []
timestamps = [
{"event": "package_install", "timestamp": record["timestamp"]},
{

View File

@@ -16,10 +16,10 @@ class DumpsysPlatformCompatArtifact(AndroidArtifact):
return
for result in self.results:
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def parse(self, data: str) -> None:

View File

@@ -50,12 +50,10 @@ class DumpsysReceiversArtifact(AndroidArtifact):
if not self.indicators:
continue
ioc_match = self.indicators.check_app_id(receiver["package_name"])
if ioc_match:
receiver["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
self.get_slug(), ioc_match.message, "", {intent: receiver}
)
ioc = self.indicators.check_app_id(receiver["package_name"])
if ioc:
receiver["matched_indicator"] = ioc
self.detected.append({intent: receiver})
continue
def parse(self, output: str) -> None:

View File

@@ -2,13 +2,13 @@
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from typing import Union
from .artifact import AndroidArtifact
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
class FileTimestampsArtifact(AndroidArtifact):
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
def serialize(self, record: dict) -> Union[dict, list]:
records = []
for ts in set(

View File

@@ -59,16 +59,13 @@ class GetProp(AndroidArtifact):
self.log.info("%s: %s", entry["name"], entry["value"])
if entry["name"] == "ro.build.version.security_patch":
warning_message = warn_android_patch_level(entry["value"], self.log)
self.alertstore.medium(self.get_slug(), warning_message, "", entry)
warn_android_patch_level(entry["value"], self.log)
if not self.indicators:
return
for result in self.results:
ioc_match = self.indicators.check_android_property_name(
result.get("name", "")
)
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
ioc = self.indicators.check_android_property_name(result.get("name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)

View File

@@ -58,13 +58,13 @@ class Processes(AndroidArtifact):
if result["proc_name"] == "gatekeeperd":
continue
ioc_match = self.indicators.check_app_id(proc_name)
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
ioc = self.indicators.check_app_id(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
ioc_match = self.indicators.check_process(proc_name)
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
ioc = self.indicators.check_process(proc_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)

View File

@@ -51,11 +51,6 @@ 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",

View File

@@ -4,13 +4,13 @@
# https://license.mvt.re/1.1/
import datetime
from typing import List, Optional
from typing import List, Optional, Union
import pydantic
import betterproto
from dateutil import parser
from mvt.common.utils import convert_datetime_to_iso
from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult
from mvt.android.parsers.proto.tombstone import Tombstone
from .artifact import AndroidArtifact
@@ -63,7 +63,7 @@ class TombstoneCrashResult(pydantic.BaseModel):
process_name: Optional[str] = None
binary_path: Optional[str] = None
selinux_label: Optional[str] = None
uid: Optional[int] = None
uid: int
signal_info: SignalInfo
cause: Optional[str] = None
extra: Optional[str] = None
@@ -76,7 +76,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
This parser can parse both text and protobuf tombstone crash files.
"""
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
def serialize(self, record: dict) -> Union[dict, list]:
return {
"timestamp": record["timestamp"],
"module": self.__class__.__name__,
@@ -92,20 +92,18 @@ class TombstoneCrashArtifact(AndroidArtifact):
return
for result in self.results:
ioc_match = self.indicators.check_process(result["process_name"])
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(self.get_slug(), ioc_match.message, "", result)
ioc = self.indicators.check_process(result["process_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
if result.get("command_line", []):
command_name = result.get("command_line")[0].split("/")[-1]
ioc_match = self.indicators.check_process(command_name)
if ioc_match:
result["matched_indicator"] = ioc_match.ioc
self.alertstore.critical(
self.get_slug(), ioc_match.message, "", result
)
ioc = self.indicators.check_process(command_name)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
SUSPICIOUS_UIDS = [
@@ -114,15 +112,11 @@ class TombstoneCrashArtifact(AndroidArtifact):
2000, # shell
]
if result["uid"] in SUSPICIOUS_UIDS:
self.alertstore.medium(
self.get_slug(),
(
f"Potentially suspicious crash in process '{result['process_name']}' "
f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}"
),
"",
result,
self.log.warning(
f"Potentially suspicious crash in process '{result['process_name']}' "
f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}"
)
self.detected.append(result)
def parse_protobuf(
self, file_name: str, file_timestamp: datetime.datetime, data: bytes
@@ -131,7 +125,9 @@ class TombstoneCrashArtifact(AndroidArtifact):
Parse Android tombstone crash files from a protobuf object.
"""
tombstone_pb = Tombstone().parse(data)
tombstone_dict = tombstone_pb.to_dict(betterproto.Casing.SNAKE)
tombstone_dict = tombstone_pb.to_dict(
betterproto.Casing.SNAKE, include_default_values=True
)
# Add some extra metadata
tombstone_dict["timestamp"] = self._parse_timestamp_string(
@@ -259,12 +255,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
@staticmethod
def _parse_timestamp_string(timestamp: str) -> str:
timestamp_date, timezone = timestamp.split("+")
# Truncate microseconds before parsing
timestamp_without_micro = timestamp_date.split(".")[0] + "+" + timezone
timestamp_parsed = datetime.datetime.strptime(
timestamp_without_micro, "%Y-%m-%d %H:%M:%S%z"
)
timestamp_parsed = parser.parse(timestamp)
# HACK: Swap the local timestamp to UTC, so keep the original time and avoid timezone conversion.
local_timestamp = timestamp_parsed.replace(tzinfo=datetime.timezone.utc)

View File

@@ -11,14 +11,20 @@ from mvt.common.cmd_check_iocs import CmdCheckIOCS
from mvt.common.help import (
HELP_MSG_VERSION,
HELP_MSG_OUTPUT,
HELP_MSG_SERIAL,
HELP_MSG_DOWNLOAD_APKS,
HELP_MSG_DOWNLOAD_ALL_APKS,
HELP_MSG_VIRUS_TOTAL,
HELP_MSG_APK_OUTPUT,
HELP_MSG_APKS_FROM_FILE,
HELP_MSG_VERBOSE,
HELP_MSG_CHECK_ADB,
HELP_MSG_IOC,
HELP_MSG_FAST,
HELP_MSG_LIST_MODULES,
HELP_MSG_MODULE,
HELP_MSG_NONINTERACTIVE,
HELP_MSG_ANDROID_BACKUP_PASSWORD,
HELP_MSG_CHECK_ADB_REMOVED,
HELP_MSG_CHECK_ADB_REMOVED_DESCRIPTION,
HELP_MSG_CHECK_BUGREPORT,
HELP_MSG_CHECK_ANDROID_BACKUP,
HELP_MSG_CHECK_ANDROIDQF,
@@ -30,14 +36,16 @@ from mvt.common.logo import logo
from mvt.common.updates import IndicatorsUpdates
from mvt.common.utils import init_logging, set_verbose_logging
from .cmd_check_adb import CmdAndroidCheckADB
from .cmd_check_androidqf import CmdAndroidCheckAndroidQF
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.backup.helpers import cli_load_android_backup_password
from .modules.bugreport import BUGREPORT_MODULES
from .modules.androidqf import ANDROIDQF_MODULES
init_logging()
log = logging.getLogger("mvt")
@@ -62,14 +70,117 @@ def version():
# ==============================================================================
# Command: check-adb (removed)
# Command: download-apks
# ==============================================================================
@cli.command(
"check-adb", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_CHECK_ADB_REMOVED
"download-apks", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_DOWNLOAD_APKS
)
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--all-apks", "-a", is_flag=True, help=HELP_MSG_DOWNLOAD_ALL_APKS)
@click.option("--virustotal", "-V", is_flag=True, help=HELP_MSG_VIRUS_TOTAL)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_APK_OUTPUT)
@click.option(
"--from-file", "-f", type=click.Path(exists=True), help=HELP_MSG_APKS_FROM_FILE
)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.pass_context
def check_adb(ctx):
log.error(HELP_MSG_CHECK_ADB_REMOVED_DESCRIPTION)
def download_apks(ctx, all_apks, virustotal, output, from_file, serial, verbose):
set_verbose_logging(verbose)
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)
download = DownloadAPKs(results_path=output, all_apks=all_apks)
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", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_CHECK_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.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE)
@click.pass_context
def check_adb(
ctx,
serial,
iocs,
output,
fast,
list_modules,
module,
non_interactive,
backup_password,
verbose,
):
set_verbose_logging(verbose)
module_options = {
"fast_mode": fast,
"interactive": not non_interactive,
"backup_password": cli_load_android_backup_password(log, backup_password),
}
cmd = CmdAndroidCheckADB(
results_path=output,
ioc_files=iocs,
module_name=module,
serial=serial,
module_options=module_options,
)
if list_modules:
cmd.list_modules()
return
log.info("Checking Android device over debug bridge")
cmd.run()
if cmd.detected_count > 0:
log.warning(
"The analysis of the Android device produced %d detections!",
cmd.detected_count,
)
# ==============================================================================
@@ -110,8 +221,12 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_
log.info("Checking Android bug report at path: %s", bugreport_path)
cmd.run()
cmd.show_alerts_brief()
cmd.show_support_message()
if cmd.detected_count > 0:
log.warning(
"The analysis of the Android bug report produced %d detections!",
cmd.detected_count,
)
# ==============================================================================
@@ -168,8 +283,12 @@ def check_backup(
log.info("Checking Android backup at path: %s", backup_path)
cmd.run()
cmd.show_alerts_brief()
cmd.show_support_message()
if cmd.detected_count > 0:
log.warning(
"The analysis of the Android backup produced %d detections!",
cmd.detected_count,
)
# ==============================================================================
@@ -228,9 +347,12 @@ def check_androidqf(
log.info("Checking AndroidQF acquisition at path: %s", androidqf_path)
cmd.run()
cmd.show_alerts_brief()
cmd.show_disable_adb_warning()
cmd.show_support_message()
if cmd.detected_count > 0:
log.warning(
"The analysis of the AndroidQF acquisition produced %d detections!",
cmd.detected_count,
)
# ==============================================================================
@@ -251,15 +373,13 @@ def check_androidqf(
@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 + BUGREPORT_MODULES + ANDROIDQF_MODULES
cmd.modules = BACKUP_MODULES + ADB_MODULES + BUGREPORT_MODULES
if list_modules:
cmd.list_modules()
return
cmd.run()
cmd.show_alerts_brief()
cmd.show_support_message()
# ==============================================================================

View File

@@ -0,0 +1,37 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from typing import Optional
from mvt.common.command import Command
from .modules.adb import ADB_MODULES
log = logging.getLogger(__name__)
class CmdAndroidCheckADB(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
module_name=module_name,
serial=serial,
module_options=module_options,
log=log,
)
self.name = "check-adb"
self.modules = ADB_MODULES

View File

@@ -10,190 +10,58 @@ from pathlib import Path
from typing import List, Optional
from mvt.common.command import Command
from mvt.common.indicators import Indicators
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
from mvt.android.cmd_check_backup import CmdAndroidCheckBackup
from .modules.androidqf import ANDROIDQF_MODULES
from .modules.androidqf.base import AndroidQFModule
log = logging.getLogger(__name__)
class NoAndroidQFTargetPath(Exception):
pass
class NoAndroidQFBugReport(Exception):
pass
class NoAndroidQFBackup(Exception):
pass
class CmdAndroidCheckAndroidQF(Command):
def __init__(
self,
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
hashes: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
)
self.name = "check-androidqf"
self.modules = ANDROIDQF_MODULES
self.__format: Optional[str] = None
self.__zip: Optional[zipfile.ZipFile] = None
self.__files: List[str] = []
self.format: Optional[str] = None
self.archive: Optional[zipfile.ZipFile] = None
self.files: List[str] = []
def init(self):
if not self.target_path:
raise NoAndroidQFTargetPath
if os.path.isdir(self.target_path):
self.__format = "dir"
self.format = "dir"
parent_path = Path(self.target_path).absolute().parent.as_posix()
target_abs_path = os.path.abspath(self.target_path)
for root, subdirs, subfiles in os.walk(target_abs_path):
for fname in subfiles:
file_path = os.path.relpath(os.path.join(root, fname), parent_path)
self.__files.append(file_path)
self.files.append(file_path)
elif os.path.isfile(self.target_path):
self.__format = "zip"
self.__zip = zipfile.ZipFile(self.target_path)
self.__files = self.__zip.namelist()
self.format = "zip"
self.archive = zipfile.ZipFile(self.target_path)
self.files = self.archive.namelist()
def module_init(self, module: AndroidQFModule) -> None: # type: ignore[override]
if self.__format == "zip" and self.__zip:
module.from_zip(self.__zip, self.__files)
return
if not self.target_path:
raise NoAndroidQFTargetPath
parent_path = Path(self.target_path).absolute().parent.as_posix()
module.from_dir(parent_path, self.__files)
def load_bugreport(self) -> zipfile.ZipFile:
bugreport_zip_path = None
for file_name in self.__files:
if file_name.endswith("bugreport.zip"):
bugreport_zip_path = file_name
break
def module_init(self, module):
if self.format == "zip":
module.from_zip_file(self.archive, self.files)
else:
raise NoAndroidQFBugReport
if self.__format == "zip" and self.__zip:
handle = self.__zip.open(bugreport_zip_path)
return zipfile.ZipFile(handle)
if self.__format == "dir" and self.target_path:
parent_path = Path(self.target_path).absolute().parent.as_posix()
bug_report_path = os.path.join(parent_path, bugreport_zip_path)
return zipfile.ZipFile(bug_report_path)
raise NoAndroidQFBugReport
def load_backup(self) -> bytes:
backup_ab_path = None
for file_name in self.__files:
if file_name.endswith("backup.ab"):
backup_ab_path = file_name
break
else:
raise NoAndroidQFBackup
if self.__format == "zip" and self.__zip:
backup_file_handle = self.__zip.open(backup_ab_path)
return backup_file_handle.read()
if self.__format == "dir" and self.target_path:
parent_path = Path(self.target_path).absolute().parent.as_posix()
backup_path = os.path.join(parent_path, backup_ab_path)
with open(backup_path, "rb") as backup_file:
backup_ab_data = backup_file.read()
return backup_ab_data
raise NoAndroidQFBackup
def run_bugreport_cmd(self) -> bool:
try:
bugreport = self.load_bugreport()
except NoAndroidQFBugReport:
self.log.warning(
"Skipping bugreport modules as no bugreport.zip found in AndroidQF data."
)
return False
else:
cmd = CmdAndroidCheckBugreport(
target_path=None,
results_path=self.results_path,
ioc_files=self.ioc_files,
iocs=self.iocs,
module_options=self.module_options,
hashes=self.hashes,
sub_command=True,
)
cmd.from_zip(bugreport)
cmd.run()
self.timeline.extend(cmd.timeline)
self.alertstore.extend(cmd.alertstore.alerts)
finally:
if bugreport:
bugreport.close()
def run_backup_cmd(self) -> bool:
try:
backup = self.load_backup()
except NoAndroidQFBackup:
self.log.warning(
"Skipping backup modules as no backup.ab found in AndroidQF data."
)
return False
else:
cmd = CmdAndroidCheckBackup(
target_path=None,
results_path=self.results_path,
ioc_files=self.ioc_files,
iocs=self.iocs,
module_options=self.module_options,
hashes=self.hashes,
sub_command=True,
)
cmd.from_ab(backup)
cmd.run()
self.timeline.extend(cmd.timeline)
self.alertstore.extend(cmd.alertstore.alerts)
finally:
if backup:
backup.close()
def finish(self) -> None:
"""
Run the bugreport and backup modules if the respective files are found in the AndroidQF data.
"""
self.run_bugreport_cmd()
self.run_backup_cmd()
module.from_folder(parent_path, self.files)

View File

@@ -11,7 +11,7 @@ import tarfile
from pathlib import Path
from typing import List, Optional
from mvt.android.modules.backup.base import BackupModule
from mvt.android.modules.backup.base import BackupExtraction
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.android.parsers.backup import (
AndroidBackupParsingError,
@@ -20,7 +20,6 @@ from mvt.android.parsers.backup import (
parse_backup_file,
)
from mvt.common.command import Command
from mvt.common.indicators import Indicators
from .modules.backup import BACKUP_MODULES
@@ -33,77 +32,72 @@ class CmdAndroidCheckBackup(Command):
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
hashes: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
)
self.name = "check-backup"
self.modules = BACKUP_MODULES
self.__type: str = ""
self.__tar: Optional[tarfile.TarFile] = None
self.__files: List[str] = []
def from_ab(self, ab_file_bytes: bytes) -> None:
self.__type = "ab"
header = parse_ab_header(ab_file_bytes)
if not header["backup"]:
log.critical("Invalid backup format, file should be in .ab format")
sys.exit(1)
password = None
if header["encryption"] != "none":
password = prompt_or_load_android_backup_password(log, self.module_options)
if not password:
log.critical("No backup password provided.")
sys.exit(1)
try:
tardata = parse_backup_file(ab_file_bytes, password=password)
except InvalidBackupPassword:
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as exc:
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
dbytes = io.BytesIO(tardata)
self.__tar = tarfile.open(fileobj=dbytes)
for member in self.__tar:
self.__files.append(member.name)
self.backup_type: str = ""
self.backup_archive: Optional[tarfile.TarFile] = None
self.backup_files: List[str] = []
def init(self) -> None:
if not self.target_path:
return
if os.path.isfile(self.target_path):
self.__type = "ab"
self.backup_type = "ab"
with open(self.target_path, "rb") as handle:
ab_file_bytes = handle.read()
self.from_ab(ab_file_bytes)
data = handle.read()
header = parse_ab_header(data)
if not header["backup"]:
log.critical("Invalid backup format, file should be in .ab format")
sys.exit(1)
password = None
if header["encryption"] != "none":
password = prompt_or_load_android_backup_password(
log, self.module_options
)
if not password:
log.critical("No backup password provided.")
sys.exit(1)
try:
tardata = parse_backup_file(data, password=password)
except InvalidBackupPassword:
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as exc:
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
dbytes = io.BytesIO(tardata)
self.backup_archive = tarfile.open(fileobj=dbytes)
for member in self.backup_archive:
self.backup_files.append(member.name)
elif os.path.isdir(self.target_path):
self.__type = "folder"
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.__files.append(
self.backup_files.append(
os.path.relpath(os.path.join(root, fname), self.target_path)
)
else:
@@ -113,12 +107,8 @@ class CmdAndroidCheckBackup(Command):
)
sys.exit(1)
def module_init(self, module: BackupModule) -> None: # type: ignore[override]
if self.__type == "folder":
module.from_dir(self.target_path, self.__files)
def module_init(self, module: BackupExtraction) -> None: # type: ignore[override]
if self.backup_type == "folder":
module.from_folder(self.target_path, self.backup_files)
else:
module.from_ab(self.target_path, self.__tar, self.__files)
def finish(self) -> None:
if self.__tar:
self.__tar.close()
module.from_ab(self.target_path, self.backup_archive, self.backup_files)

View File

@@ -11,7 +11,6 @@ from zipfile import ZipFile
from mvt.android.modules.bugreport.base import BugReportModule
from mvt.common.command import Command
from mvt.common.indicators import Indicators
from .modules.bugreport import BUGREPORT_MODULES
@@ -24,76 +23,54 @@ class CmdAndroidCheckBugreport(Command):
target_path: Optional[str] = None,
results_path: Optional[str] = None,
ioc_files: Optional[list] = None,
iocs: Optional[Indicators] = None,
module_name: Optional[str] = None,
serial: Optional[str] = None,
module_options: Optional[dict] = None,
hashes: Optional[bool] = False,
sub_command: Optional[bool] = False,
hashes: bool = False,
) -> None:
super().__init__(
target_path=target_path,
results_path=results_path,
ioc_files=ioc_files,
iocs=iocs,
module_name=module_name,
serial=serial,
module_options=module_options,
hashes=hashes,
sub_command=sub_command,
log=log,
)
self.name = "check-bugreport"
self.modules = BUGREPORT_MODULES
self.__format: str = ""
self.__zip: Optional[ZipFile] = None
self.__files: List[str] = []
def from_dir(self, dir_path: str) -> None:
"""This method is used to initialize the bug report analysis from an
uncompressed directory.
"""
self.__format = "dir"
self.target_path = dir_path
parent_path = Path(dir_path).absolute().as_posix()
for root, _, subfiles in os.walk(os.path.abspath(dir_path)):
for file_name in subfiles:
file_path = os.path.relpath(os.path.join(root, file_name), parent_path)
self.__files.append(file_path)
def from_zip(self, bugreport_zip: ZipFile) -> None:
"""This method is used to initialize the bug report analysis from a
compressed archive.
"""
# NOTE: This will be invoked either by the CLI directly,or by the
# check-androidqf command. We need this because we want to support
# check-androidqf to analyse compressed archives itself too.
# So, we'll need to extract bugreport.zip from a 'androidqf.zip', and
# since nothing is written on disk, we need to be able to pass this
# command a ZipFile instance in memory.
self.__format = "zip"
self.__zip = bugreport_zip
for file_name in self.__zip.namelist():
self.__files.append(file_name)
self.bugreport_format: str = ""
self.bugreport_archive: Optional[ZipFile] = None
self.bugreport_files: List[str] = []
def init(self) -> None:
if not self.target_path:
return
if os.path.isfile(self.target_path):
self.from_zip(ZipFile(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.from_dir(self.target_path)
self.bugreport_format = "dir"
parent_path = Path(self.target_path).absolute().as_posix()
for root, _, subfiles in os.walk(os.path.abspath(self.target_path)):
for file_name in subfiles:
file_path = os.path.relpath(
os.path.join(root, file_name), parent_path
)
self.bugreport_files.append(file_path)
def module_init(self, module: BugReportModule) -> None: # type: ignore[override]
if self.__format == "zip":
module.from_zip(self.__zip, self.__files)
if self.bugreport_format == "zip":
module.from_zip(self.bugreport_archive, self.bugreport_files)
else:
module.from_dir(self.target_path, self.__files)
module.from_folder(self.target_path, self.bugreport_files)
def finish(self) -> None:
if self.__zip:
self.__zip.close()
if self.bugreport_archive:
self.bugreport_archive.close()

View File

@@ -0,0 +1,184 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import json
import logging
import os
from typing import Callable, Optional, Union
from rich.progress import track
from mvt.common.module import InsufficientPrivileges
from .modules.adb.base import AndroidExtraction
from .modules.adb.packages import Packages
log = logging.getLogger(__name__)
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:
"""Initialize module.
:param results_path: 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)
self.packages = packages
self.all_apks = all_apks
self.results_path_apks = None
@classmethod
def from_json(cls, json_path: str) -> Callable:
"""Initialize this class from an existing apks.json file.
:param json_path: Path to the apks.json file to parse.
"""
with open(json_path, "r", encoding="utf-8") as handle:
packages = json.load(handle)
return cls(packages=packages)
def pull_package_file(
self, package_name: str, remote_path: str
) -> Union[str, None]:
"""Pull files related to specific package from the device.
:param package_name: Name of the package to download
:param remote_path: Path to the file to download
:returns: Path to the local copy
"""
log.info("Downloading %s ...", remote_path)
file_name = ""
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"
)
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"
)
try:
self._adb_download(remote_path, local_path)
except InsufficientPrivileges:
log.error(
"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)
self._adb_reconnect()
return None
return local_path
def get_packages(self) -> None:
"""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...")
m = Packages()
m.log = self.log
m.serial = self.serial
m.run()
self.packages = m.results
def pull_packages(self) -> None:
"""Download all files of all selected packages from the device."""
log.info(
"Starting extraction of installed APKs at folder %s", self.results_path
)
# If the user provided the flag --all-apks we select all packages.
packages_selection = []
if self.all_apks:
log.info("Selected all %d available packages", len(self.packages))
packages_selection = self.packages
else:
# Otherwise we loop through the packages and get only those that
# are not marked as system.
for package in self.packages:
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),
)
if len(packages_selection) == 0:
log.info("No packages were selected for download")
return
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)
for i in track(
range(len(packages_selection)),
description=f"Downloading {len(packages_selection)} packages...",
):
package = packages_selection[i]
log.info(
"[%d/%d] Package: %s",
i,
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.
for package_file in package["files"]:
device_path = package_file["path"]
local_path = self.pull_package_file(
package["package_name"], device_path
)
if not local_path:
continue
package_file["local_path"] = local_path
log.info("Download of selected packages completed")
def save_json(self) -> None:
json_path = os.path.join(self.results_path, "apks.json")
with open(json_path, "w", encoding="utf-8") as handle:
json.dump(self.packages, handle, indent=4)
def run(self) -> None:
self.get_packages()
self._adb_connect()
self.pull_packages()
self.save_json()
self._adb_disconnect()

View File

@@ -4,7 +4,15 @@
# https://license.mvt.re/1.1/
from .chrome_history import ChromeHistory
from .dumpsys_accessibility import DumpsysAccessibility
from .dumpsys_activities import DumpsysActivities
from .dumpsys_appops import DumpsysAppOps
from .dumpsys_battery_daily import DumpsysBatteryDaily
from .dumpsys_battery_history import DumpsysBatteryHistory
from .dumpsys_dbinfo import DumpsysDBInfo
from .dumpsys_adbstate import DumpsysADBState
from .dumpsys_full import DumpsysFull
from .dumpsys_receivers import DumpsysReceivers
from .files import Files
from .getprop import Getprop
from .logcat import Logcat
@@ -24,7 +32,15 @@ ADB_MODULES = [
Getprop,
Settings,
SELinuxStatus,
DumpsysBatteryHistory,
DumpsysBatteryDaily,
DumpsysReceivers,
DumpsysActivities,
DumpsysAccessibility,
DumpsysDBInfo,
DumpsysADBState,
DumpsysFull,
DumpsysAppOps,
Packages,
Logcat,
RootBinaries,

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