Compare commits

...

186 Commits

Author SHA1 Message Date
Nex
a30d7b2871 Adding support for iOS lockdown management 2022-07-05 18:12:10 +02:00
Nex
459ff8c51c Adding some more checks to bugreport packages module 2022-07-05 18:10:48 +02:00
Nex
88665cf7dd Merge pull request #289 from lorenzo-reho/main
Fixed cmd_download_apks serial connection bug
2022-07-02 18:22:59 +02:00
lorenzo-reho
0a749da85f Fixed cmd_download_apks serial connection bug 2022-07-02 16:14:27 +02:00
Nex
f81604133a Fixed Prompt imports 2022-06-30 11:06:37 +02:00
Nex
cdd9b74cbc Replaced getpass with Prompt 2022-06-30 10:58:50 +02:00
Nex
3fb37b4f30 Added finish() method to Command class 2022-06-30 10:26:33 +02:00
Nex
2fe8b58c09 Removed space 2022-06-30 10:26:30 +02:00
tek
61d0c4134d Fixes a bug in mvt-android download-apks 2022-06-29 23:06:49 +02:00
Nex
6b36fe5fca Re-adding again empty spacing that went missing 2022-06-29 10:35:30 +02:00
Nex
c9f54947e3 Small language and style changes 2022-06-29 01:11:30 +02:00
Nex
ae6fec5ac5 Merge branch 'Te-k-feature/ios-check-usb' 2022-06-29 00:57:32 +02:00
Nex
298726ab2b Minor style fixes 2022-06-29 00:57:25 +02:00
Nex
7222bc82e1 Sorting imports and removing unused ones 2022-06-29 00:05:36 +02:00
Nex
4a568835d2 Merge branch 'main' into feature/ios-check-usb 2022-06-28 23:58:38 +02:00
tek
f98282d6c5 Adds applications and device info iOS USB modules 2022-06-28 23:37:57 +02:00
tek
f864adf97e First structure for mvt-ios check-usb 2022-06-28 20:35:52 +02:00
Nex
8f6882b0ff Merge pull request #287 from mvt-project/ioc_updates
Added process to automatically check for indicators updates
2022-06-28 16:04:08 +02:00
Nex
b6531e3e70 Forgot closing bold tags 2022-06-28 15:55:52 +02:00
Nex
ef662c1145 Added new indicators update to mvt-android 2022-06-28 15:03:52 +02:00
Nex
b8e5346660 Updating last check time when forcefully updating iocs 2022-06-28 13:12:09 +02:00
Nex
aedef123c9 Added frequency of indicators updates check 2022-06-28 12:54:33 +02:00
Nex
8ff8e599d8 Fixed flake8 and minor code style 2022-06-28 12:00:30 +02:00
Nex
815cdc0a88 Adding system to check for updates of indicators files and notify if any are available 2022-06-27 14:41:40 +02:00
Nex
b420d828ee Reintroduced public_indicators.json file to be available for older versions 2022-06-25 00:49:16 +02:00
Nex
7b92903536 Moved indicators file to dedicated repository 2022-06-25 00:41:58 +02:00
Nex
2bde693c35 Removed empty spaces 2022-06-24 15:20:09 +02:00
Nex
7daea737c6 Merge branch 'main' of github.com:mvt-project/mvt 2022-06-24 15:14:47 +02:00
Nex
0d75dc3ba0 Optionally loading indicators description 2022-06-24 15:14:33 +02:00
tek
0622357a64 Adds support for MMS parsing in android backups 2022-06-23 11:05:04 +02:00
tek
c4f91ba28b Merge branch 'main' of github.com:mvt-project/mvt 2022-06-23 10:02:53 +02:00
tek
5ade0657ac Fixes an issue in Android backup parsing 2022-06-23 10:02:37 +02:00
Nex
cca9083dff Reintroduced is_backup and is_fs_dump 2022-06-22 17:54:03 +02:00
Nex
3f4ddaaa0c Minor code style fixes 2022-06-22 17:53:53 +02:00
Nex
7024909e05 Adding more type hints 2022-06-22 16:53:29 +02:00
Nex
3899dce353 Hashing files only when MVT_HASH_FILES env is set 2022-06-20 23:41:59 +02:00
Nex
4830aa5a6c Improved analytics iOS versions module, checking dates, and sorting results 2022-06-20 23:35:46 +02:00
Nex
3608576417 Added new AnalyticsIOSVersions to collect a timeline of iOS versions 2022-06-20 20:26:18 +02:00
Nex
043c234401 Moved logging and sorting of Analytics results 2022-06-20 19:06:48 +02:00
Nex
8663c78b63 Actually using self.log 2022-06-20 18:29:39 +02:00
Nex
b847683717 Catching PermissionError 2022-06-20 18:28:05 +02:00
Nex
09400a2847 Added some notes in documentation about using VirusTotal 2022-06-20 11:32:57 +02:00
Nex
2bc6fbef2f Starting to add type hints 2022-06-17 22:30:46 +02:00
Nex
b77749e6ba Storing information about analysis in info.json (closes: #274) 2022-06-17 17:48:07 +02:00
Nex
1643454190 Ordered commands arguments 2022-06-17 17:16:20 +02:00
Nex
c2f1fe718d Fixed bug in store timeline logic 2022-06-17 17:16:00 +02:00
Nex
444ecf032d Fixing newlines 2022-06-17 17:07:36 +02:00
Nex
dd230c2407 Added optional file logging 2022-06-17 14:56:39 +02:00
Nex
cd87b6ed31 Using proper logger in WhatsApp module 2022-06-17 13:40:30 +02:00
Nex
6f50af479d Bumped version 2022-06-17 10:36:27 +02:00
Nex
36a67911b3 Merge pull request #282 from mvt-project/cli_refactor
CLI refactor
2022-06-17 10:27:47 +02:00
Nex
2dbfef322a Some marginal code style fix 2022-06-16 17:08:42 +02:00
Nex
fba4e27757 Refactored check-iocs command for Android as well 2022-06-16 17:02:38 +02:00
Nex
abc0f2768b Fixed tests 2022-06-16 15:24:43 +02:00
Nex
e7fe30e201 Refactoring cli commands for iOS too 2022-06-16 15:18:50 +02:00
Nex
c54a01ca59 Fixing exceeding lines length 2022-06-16 15:01:07 +02:00
Nex
a12c4e6b93 First commit to refactor of command definitions 2022-06-15 17:41:19 +02:00
Nex
a9be771f79 Using remote picture so to not break pypi etc. 2022-06-14 18:13:21 +02:00
Nex
a7d35dba4a Refactoring support for VirusTotal lookups, and removed Koodous lookups (ref: #273) 2022-06-14 15:46:01 +02:00
Nex
3a6e4a7001 Temporarily disabled Koodous lookup 2022-06-13 20:06:35 +02:00
Nex
bb0e41e949 Bumped version 2022-06-03 11:44:44 +02:00
Nex
6844f0b90b Added new iOS version 2022-06-03 11:43:42 +02:00
Tek
fb2a0ba668 Merge pull request #280 from Niek/patch-1
Fix stalkerware STIX URL
2022-06-02 10:29:53 +02:00
Niek van der Maas
e34f8f3660 Fix stalkerware STIX URL 2022-06-02 09:57:56 +02:00
tek
fd3f9dba8f Adds automated flake8 check in github workflow 2022-05-25 14:38:51 +02:00
Nex
27f0364c1d Removed static android data files 2022-05-09 11:09:07 +02:00
Nex
8dac714214 Added support for multiprocessing when decrypting an iOS backup 2022-05-08 17:25:27 +02:00
Nex
732a712e3d Changed path where to seek packages 2022-05-08 17:06:34 +02:00
Nex
6d278d4bec Actually, can also move version to setup.cfg 2022-05-08 15:17:19 +02:00
Nex
c39b4d2179 Minimized setup.py and moved all possible to setup.cfg 2022-05-08 15:15:00 +02:00
Nex
a653fd5253 Moved dependencies to setup.cfg 2022-05-08 15:07:17 +02:00
Nex
f754bf274d Unfortunately until #233 is resolved, we have to enforce Python >= 3.8 as well 2022-05-08 14:59:34 +02:00
Nex
fcac8a8c7d Updated README 2022-05-08 14:57:33 +02:00
Nex
d82c788a18 Removed AUTHORS file in favor of explicit copyright notice 2022-05-08 14:53:50 +02:00
Nex
946a9ef02b Added missing import 2022-05-08 14:51:31 +02:00
Nex
c343eed5a0 Moved flake8 config to setup.cfg 2022-05-08 14:50:31 +02:00
Nex
6162a1e1f2 Sorted imports 2022-05-08 14:47:54 +02:00
Nex
f61729deed Starting to move declarative information to setup.cfg 2022-05-08 14:45:14 +02:00
Nex
7a00e88f1f Explicit license version in setup.py 2022-05-08 14:26:43 +02:00
Nex
ff41efba72 Bumped version 2022-04-05 21:46:38 +02:00
Nex
26e6a00bf5 Added new iOS version 2022-04-04 13:25:13 +02:00
Nex
9d61b9048c Fixed variable names mismatch and styling 2022-03-30 08:49:22 +02:00
tek
9950b3d6c2 Add appops dumpsys parser and modules 2022-03-30 01:16:22 +02:00
tek
e0d30ea990 Removes check for a deprecated Android setting 2022-03-29 18:37:56 +02:00
tek
293752f90a Merge branch 'main' of github.com:mvt-project/mvt 2022-03-28 20:12:17 +02:00
tek
ac1e5c29d3 Clarifies the backup path needed in the documentation 2022-03-28 15:38:20 +02:00
Nex
d868e6d9f0 Merge pull request #259 from mlowdi/configuration_profiles_fix
base64 encoding fixes in ConfigurationProfiles module
2022-03-28 14:08:22 +02:00
Martin L. Fällman
f5cb7f06e1 Fix for missing base64 encoding of MDM certificate data in JSON output 2022-03-25 20:36:30 +01:00
Martin L. Fällman
5ce8035820 Add Sublime Text project files to .gitignore 2022-03-25 20:16:20 +01:00
Donncha Ó Cearbhaill
e3a8bde150 Fix path error when relative '.' used as backup source directory 2022-03-20 15:56:13 +01:00
Nex
d6af7c8cca Updating flake8 config and fixed some violations 2022-03-18 11:10:06 +01:00
Nex
6584d8232c Fixed bug in bugreport packages parser 2022-03-16 10:20:53 +01:00
Nex
3487078c03 Added flake8 configuration file 2022-03-15 13:36:03 +01:00
Nex
bc5d386be7 Bumped version 2022-03-15 11:19:22 +01:00
Nex
03efc8494b Added new iOS version 2022-03-15 11:19:05 +01:00
Nex
0b3f529cfa Bumped version 2022-03-14 10:22:29 +01:00
Nex
9bdef6ede4 Fixing spacing 2022-03-10 11:35:49 +01:00
Nex
fc9a27d030 Sorted imports 2022-03-10 11:33:54 +01:00
tek
f5f3660d82 Updates the documentation 2022-03-08 14:17:41 +01:00
Tek
712f5bcb9b Merge pull request #251 from mvt-project/feature/read-sms-adb-backup
Add initial implementation of SMS extraction using ADB
2022-03-05 23:27:55 +01:00
Donncha Ó Cearbhaill
ac26aa964a Fix exception with bad password 2022-03-04 17:24:26 +01:00
Donncha Ó Cearbhaill
be511dcb51 Refactor SMS ADB code to use backup functions 2022-03-04 17:06:10 +01:00
Donncha Ó Cearbhaill
b44c67e699 Refactor some of the decryption code 2022-03-04 17:04:32 +01:00
tek
a4d08f8f35 Replaces pyaes with cryptography and reorganize backup parser code 2022-03-04 15:05:10 +01:00
tek
6cc67f3c1d Fixes testing issue 2022-03-04 12:34:54 +01:00
tek
0d5377597f Merge branch 'main' into feature/read-sms-adb-backup 2022-03-04 12:30:45 +01:00
tek
86c79075ff Reorganise code for backup modules 2022-03-04 10:10:56 +01:00
tek
9940b1d145 Adds test of the check-backup command 2022-03-01 18:54:34 +01:00
tek
b07fb092aa Adds tests for SMS module 2022-03-01 13:11:50 +01:00
tek
639c163297 Adds partial compression support in Android Backup parsing 2022-02-23 16:18:45 +01:00
tek
8eb30e3a02 Improves android backup parsing for check-backup and check-adb 2022-02-23 15:07:13 +01:00
Donncha Ó Cearbhaill
cd0e7d9879 Fix syntax error with broken comment 2022-02-18 15:09:08 +01:00
Donncha Ó Cearbhaill
bdaaf15434 Add initial implementation of SMS extraction using ADB 2022-02-17 18:17:38 +01:00
tek
699824d9ff Adds iOS version 15.3.1 2022-02-11 12:25:53 +01:00
Nex
8cca78d222 Missing newline 2022-02-09 13:31:27 +01:00
Nex
57cbb0ed56 Fixed typo 2022-02-09 13:30:31 +01:00
Nex
e9cc6b3928 Fixed code styling and added missing check in adb getprop 2022-02-09 13:20:09 +01:00
tek
6d47d4d416 Adds warning for outdated iOS systems 2022-02-08 15:49:10 +01:00
tek
ed54761747 Adds warning if phone is outdated in getprop module 2022-02-07 17:28:01 +01:00
Nex
71c4ba799f Fixed help message for download-apks 2022-02-04 13:42:32 +01:00
Nex
09a6f291c0 Bumped version 2022-02-04 13:39:37 +01:00
Nex
b50be69dd4 Bumped version 2022-02-04 13:38:04 +01:00
Nex
6fc6102b73 Improved parsing of bugreports by finding dumpstate file name from main_entry.txt 2022-02-04 13:34:40 +01:00
Nex
3fe5d8dc8d Fixing battery stats history parsing 2022-02-03 22:18:37 +01:00
Nex
fec6210d1b Fixed parsing of dbinfo to support multiple formats 2022-02-03 20:36:47 +01:00
Nex
6a723e533f Fixed logging in adb modules 2022-02-03 20:19:07 +01:00
Nex
ed8a5a3845 Fixed dumpstate parsing for different formats and added logging 2022-02-03 19:55:18 +01:00
Nex
04225a4455 Ignoring decoding errors 2022-02-03 19:40:36 +01:00
Nex
5987f218be Supporting multiple file names 2022-02-03 19:26:45 +01:00
Nex
748780476e Fixed a typo and catching exception 2022-02-03 19:20:26 +01:00
Nex
c522b54326 Supporting searching files by multiple patterns 2022-02-03 17:21:29 +01:00
Nex
0e0e346916 Fixed issue in parsing batterystats daily 2022-02-03 13:36:08 +01:00
Nex
69daf3c3cd Added module checking SELinux enforcement status 2022-02-03 11:34:02 +01:00
Nex
998d87900d Merge pull request #247 from mvt-project/android-split-parsers
Android split parsers
2022-02-03 00:06:53 +01:00
Nex
230f81879a Added check for indicators to Processes 2022-02-03 00:06:15 +01:00
Nex
df42efb7cb Added getprop parser 2022-02-02 22:07:47 +01:00
Nex
0922e569b0 Sorted imports 2022-02-02 22:00:48 +01:00
Nex
03092cf3b7 Attempting split of parsers 2022-02-02 21:58:11 +01:00
Nex
ab63a02c9f Code clean-ups 2022-02-02 19:18:47 +01:00
Nex
a833dda581 Added getprop bugreport module 2022-02-02 19:00:20 +01:00
Nex
189b1d7fc6 Fixed tests 2022-02-02 18:14:10 +01:00
Nex
b1b282ac20 Merge pull request #246 from mvt-project/check-bugreport
Check bugreport
2022-02-02 18:12:24 +01:00
Nex
512c349c2c Sorted imports 2022-02-02 16:10:24 +01:00
Nex
b94ba28873 Supporting loading from extracted folder 2022-02-02 16:10:12 +01:00
Nex
564efc3629 Sorted imports 2022-02-02 15:49:24 +01:00
Nex
9c62e6e4d6 Added Packages module 2022-02-02 15:47:55 +01:00
Nex
153f6cce02 Returning stix2 file name with iocs as well 2022-02-02 14:57:32 +01:00
Nex
47f9a0104c Added a break for speed 2022-02-02 14:54:40 +01:00
Nex
bdad23feee Refactored indicators to support multiple malware/collections per stix2 file 2022-02-02 14:53:26 +01:00
Donncha Ó Cearbhaill
5416b66915 Add CI and downloads page 2022-02-02 12:45:06 +01:00
Nex
e2936c3d33 Added new check-bugreport command and modules 2022-02-02 00:09:53 +01:00
Nex
3483ca1584 Package dumpsys parsing as static method 2022-02-01 21:45:26 +01:00
Nex
7b107edf1f Bumped version 2022-02-01 17:54:01 +01:00
Nex
b97ce7651a Fixed missing checks for indicators instance (ref: #245) 2022-02-01 17:48:19 +01:00
Nex
52a204cab6 Obtaining permissions for installed packages 2022-02-01 15:33:19 +01:00
Nex
1b335fda1d Renamed function argument to more descriptive 2022-02-01 15:07:43 +01:00
Nex
2ad175eae2 Renamed package to package_name for consistency 2022-02-01 14:27:00 +01:00
Nex
2d00dca5bd Bumped version 2022-02-01 12:46:31 +01:00
Nex
c8e50eb958 Merge pull request #244 from dangaffey/patch-1
Update docker.md
2022-02-01 11:54:04 +01:00
Dan Gaffey
1f049fc8ba Update docker.md
Had to run an additional Docker flag to get it building on the new M1 chip from Apple. Figured it would be helpful to point that out in the Docs for the less initiated users.
2022-01-31 20:22:54 -05:00
Nex
434738a306 Better regexp formatting 2022-01-31 13:05:03 +01:00
Nex
06cd640c5e Using static methods 2022-01-31 12:58:33 +01:00
Nex
fb8a7ca104 Enforce consistency in Android modules 2022-01-31 11:30:49 +01:00
Nex
8d15ff58dd Renamed matched field name to singular 2022-01-30 20:29:09 +01:00
Nex
eb5f07a75d Updated copyright notice 2022-01-30 20:15:01 +01:00
Nex
ececf1a6b2 Added module to extract db queries 2022-01-30 19:43:09 +01:00
Nex
851cd52602 Ordering and clean-up 2022-01-30 16:41:32 +01:00
Nex
8db04fc991 Added module to parse battery daily stats package updates 2022-01-30 16:02:24 +01:00
Nex
3d0ba56e1f Fixed parsing of wake events 2022-01-30 15:20:03 +01:00
Nex
c48a4e8f50 Fixed variable name 2022-01-30 04:12:19 +01:00
Nex
001c2998a5 Removed unnecessary newlines 2022-01-30 04:11:46 +01:00
Nex
5e7c5727af Added check for indicators to dumpsys modules 2022-01-30 04:08:48 +01:00
Nex
883fbaeb88 Parsing records from accessibility and battery history 2022-01-30 03:44:41 +01:00
Nex
6f0012cede Removed modules which are only duplicated outputs from dumpsys full 2022-01-30 03:39:26 +01:00
Nex
458e80ccbb Adding module to process battery history 2022-01-30 03:34:16 +01:00
Nex
c8185fdbd8 Small code clean-ups 2022-01-29 15:13:35 +01:00
Nex
67eea3edec Merge pull request #241 from yallxe/main
Make utf-8 as a default for open()
2022-01-29 14:44:16 +01:00
Yallxe
bc86d159b8 Clear 'debugging' things 2022-01-29 12:28:22 +01:00
Yallxe
43b1612dfe Set utf-8 as an encoding for open()
Not every system uses 'utf-8' as a default encoding for opening files in Python.

Before you say that there must be a way to set default encoding in one line, no, there is not. At least, I didn't found a way to do this.
2022-01-29 12:18:18 +01:00
Yallxe
156f1084f1 Add IDEA to gitignore 2022-01-29 12:03:00 +01:00
Nex
49e34f6299 Better parsing of dumpsys package and added parsing of Activities too 2022-01-29 03:50:33 +01:00
Nex
d88a66dd54 Fixed typo 2022-01-29 01:13:52 +01:00
Nex
d3ed778ae4 Fixed comment stylling 2022-01-29 01:13:29 +01:00
tek
4c3306c272 Separate receivers parsing in DumpsysReceivers 2022-01-29 01:06:32 +01:00
Nex
1c912f68fe Bumped version 2022-01-28 22:25:41 +01:00
Nex
10a640d3f7 Temporary disabing VirusTotal lookup because of API issues 2022-01-28 22:25:21 +01:00
171 changed files with 5113 additions and 1674 deletions

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,7 +1,7 @@
# 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: Python package
name: CI
on:
push:
@@ -28,7 +28,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest safety stix2
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

6
.gitignore vendored
View File

@@ -131,3 +131,9 @@ dmypy.json
# Temporal files
*~
# IDEA Dev Environment
.idea
# Sublime Text project files
*.sublime*

View File

@@ -1,7 +0,0 @@
MVT was originally authored by Claudio Guarnieri <nex@nex.sx>.
For an up-to-date list of all contributors visit:
https://github.com/mvt-project/mvt/graphs/contributors
Or run:
git shortlog -s -n

View File

@@ -1,11 +1,13 @@
<p align="center">
<img src="./docs/mvt.png" width="200" />
<img src="https://docs.mvt.re/en/latest/mvt.png" width="200" />
</p>
# Mobile Verification Toolkit
[![](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/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.
@@ -13,6 +15,7 @@ It has been developed and released by the [Amnesty International Security Lab](h
*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.
## 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/)):
@@ -21,14 +24,14 @@ MVT can be installed from sources or from [PyPi](https://pypi.org/project/mvt/)
pip3 install mvt
```
Alternatively, you can decide to run MVT and all relevant tools through a [Docker container](https://docs.mvt.re/en/latest/docker/).
For alternative installation options and known issues, please refer to the [documentation](https://docs.mvt.re/en/latest/install/) as well as [GitHub Issues](https://github.com/mvt-project/mvt/issues).
**Please note:** MVT is best run on Linux or Mac systems. [It does not currently support running natively on Windows.](https://docs.mvt.re/en/latest/install/#mvt-on-windows)
## Usage
MVT provides two commands `mvt-ios` and `mvt-android`. [Check out the documentation to learn how to use them!](https://docs.mvt.re/)
## License
The purpose of MVT is to facilitate the ***consensual forensic analysis*** of devices of those who might be targets of sophisticated mobile spyware attacks, especially members of civil society and marginalized communities. We do not want MVT to enable privacy violations of non-consenting individuals. In order to achieve this, MVT is released under its own license. [Read more here.](https://docs.mvt.re/en/latest/license/)

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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,6 +1,6 @@
#!/usr/bin/env python3
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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

@@ -11,18 +11,37 @@ That said, most versions of Android should still allow to locally backup SMS mes
Because `mvt-android check-backup` currently only supports checking SMS messages, you can indicate to backup only those:
```bash
adb backup com.android.providers.telephony
adb backup -nocompress com.android.providers.telephony
```
In case you nonetheless wish to take a full backup, you can do so with
```bash
adb backup -all
adb backup -nocompress -all
```
## Unpack the backup
Some recent phones will enforce the utilisation of a password to encrypt the backup archive. In that case, the password will obviously be needed to extract and analyse the data later on.
In order to unpack the backup, use [Android Backup Extractor (ABE)](https://github.com/nelenkov/android-backup-extractor) to convert it to a readable file format. Make sure that java is installed on your system and use the following command:
## 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 containing links from the backup directly with MVT:
```bash
$ mvt-android check-backup --output /path/to/results/ /path/to/backup.ab
14:09:45 INFO [mvt.android.cli] Checking ADB backup located at: 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 containing links
```
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.
## Alternative ways to unpack and check the backup
If you encounter an issue during the analysis of the backup, you can alternatively use [Android Backup Extractor (ABE)](https://github.com/nelenkov/android-backup-extractor) to convert it to a readable file format. Make sure that java is installed on your system and use the following command:
```bash
java -jar ~/path/to/abe.jar unpack backup.ab backup.tar
@@ -33,17 +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.
## Check the backup
You can then extract SMSs containing links with MVT:
```bash
$ mvt-android check-backup --output /path/to/results/ /path/to/backup/
16:18:38 INFO [mvt.android.cli] Checking ADB backup located at: .
INFO [mvt.android.modules.backup.sms] Running module SMS...
INFO [mvt.android.modules.backup.sms] Processing SMS backup file at /path/to/backup/apps/com.android.providers.telephony/d_f/000000_sms_backup
16:18:39 INFO [mvt.android.modules.backup.sms] Extracted a total of
64 SMS messages containing links
```
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.
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

@@ -13,22 +13,16 @@ It might take several minutes to complete.
!!! info
MVT will likely warn you it was unable to download certain installed packages. There is no reason to be alarmed: this is typically expected behavior when MVT attempts to download a system package it has no privileges to access.
Optionally, you can decide to enable lookups of the SHA256 hash of all the extracted APKs on [VirusTotal](https://www.virustotal.com) and/or [Koodous](https://koodous.com). While these lookups do not provide any conclusive assessment on all of the extracted APKs, they might highlight any known malicious ones:
Optionally, you can decide to enable lookups of the SHA256 hash of all the extracted APKs on [VirusTotal](https://www.virustotal.com). While these lookups do not provide any conclusive assessment on all of the extracted APKs, they might highlight any known malicious ones:
```bash
mvt-android download-apks --output /path/to/folder --virustotal
mvt-android download-apks --output /path/to/folder --koodous
MVT_VT_API_KEY=<key> mvt-android download-apks --output /path/to/folder --virustotal
```
Or, to launch all available lookups:
Please note that in order to use VirusTotal lookups you are required to provide your own API key through the `MVT_VT_API_KEY` environment variable. You should also note that VirusTotal enforces strict API usage. Be mindful that MVT might consume your hourly search quota.
In case you have a previous extraction of APKs you want to later check against VirusTotal, you can do so with the following arguments:
```bash
mvt-android download-apks --output /path/to/folder --all-checks
MVT_VT_API_KEY=<key> mvt-android download-apks --from-file /path/to/folder/apks.json --virustotal
```
In case you have a previous extraction of APKs you want to later check against VirusTotal and Koodous, you can do so with the following arguments:
```bash
mvt-android download-apks --from-file /path/to/folder/apks.json --all-checks
```

View File

@@ -8,8 +8,10 @@ However, not all is lost.
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.
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 lookup services such as [VirusTotal](https://www.virustotal.com) or [Koodous](https://koodous.com) which might quickly indicate known bad apps.
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).
!!! 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

View File

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

View File

@@ -39,8 +39,8 @@ export MVT_STIX2="/home/user/IOC1.stix2:/home/user/IOC2.stix2"
- 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))
- [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/stalkerware.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).
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.
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

@@ -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.
@@ -18,8 +18,8 @@ If indicators are provided through the command-line, processes and domains are c
### `backup_info.json`
!!! info "Availabiliy"
Backup: :material-check:
!!! info "Availability"
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.
@@ -29,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.
@@ -41,7 +41,7 @@ If indicators are provided through the command-line, they are checked against th
### `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.
@@ -51,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.
@@ -63,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.
@@ -75,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.
@@ -87,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.
@@ -97,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.
@@ -109,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.
@@ -121,7 +121,7 @@ If indicators are provided through the command-line, they are checked against th
### `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.
@@ -133,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.
@@ -143,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.
@@ -153,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.
@@ -163,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.
@@ -175,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.
@@ -187,10 +187,10 @@ 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 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.
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.
If indicators are provided through the command-line, they are checked against the process names. Any matches are stored in *datausage_detected.json*. If running on a full filesystem dump and if the `--fast` flag was not enabled by command-line, mvt-ios will highlight processes which look suspicious and check the presence of a binary file of the same name in the dump.
@@ -199,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.
@@ -211,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.
@@ -221,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.
@@ -233,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.
@@ -245,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.
@@ -257,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.
@@ -269,7 +269,7 @@ 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 containing HTTP links from the SQLite database located at */private/var/mobile/Library/SMS/sms.db*.
@@ -281,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.
@@ -291,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.
@@ -301,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*.
@@ -311,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.
@@ -323,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.
@@ -335,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.
@@ -347,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.
@@ -359,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.
@@ -371,7 +371,7 @@ 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 containing HTTP links from the SQLite database located at *private/var/mobile/Containers/Shared/AppGroup/\*/ChatStorage.sqlite*.

View File

@@ -1,7 +1,7 @@
site_name: Mobile Verification Toolkit
repo_url: https://github.com/mvt-project/mvt
edit_uri: edit/main/docs/
copyright: Copyright &copy; 2021 MVT Project Developers
copyright: Copyright &copy; 2021-2022 MVT Project Developers
site_description: Mobile Verification Toolkit Documentation
markdown_extensions:
- attr_list

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 The MVT Project 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 The MVT Project 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/
@@ -9,18 +9,21 @@ 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.indicators import Indicators, download_indicators_files
from mvt.common.logo import logo
from mvt.common.module import run_module, save_timeline
from mvt.common.updates import IndicatorsUpdates
from .download_apks import DownloadAPKs
from .lookups.koodous import koodous_lookup
from .lookups.virustotal import virustotal_lookup
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"
@@ -46,21 +49,19 @@ def version():
#==============================================================================
# Download APKs
# Command: download-apks
#==============================================================================
@cli.command("download-apks", help="Download all or non-safelisted installed APKs installed on the device")
@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("--koodous", "-k", is_flag=True, help="Check packages on Koodous")
@click.option("--all-checks", "-A", is_flag=True, help="Run all available checks")
@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, koodous, all_checks, output, from_file, serial):
def download_apks(ctx, all_apks, virustotal, output, from_file, serial):
try:
if from_file:
download = DownloadAPKs.from_json(from_file)
@@ -83,23 +84,27 @@ def download_apks(ctx, all_apks, virustotal, koodous, all_checks, output, from_f
download.serial = serial
download.run()
packages = download.packages
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) == 0:
return
if len(packages_to_lookup) == 0:
return
if virustotal or all_checks:
virustotal_lookup(packages)
if koodous or all_checks:
koodous_lookup(packages)
if virustotal:
m = Packages()
m.check_virustotal(packages_to_lookup)
except KeyboardInterrupt:
print("")
ctx.exit(1)
#==============================================================================
# Checks through ADB
# Command: check-adb
#==============================================================================
@cli.command("check-adb", help="Check an Android device over adb")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@@ -111,92 +116,77 @@ def download_apks(ctx, all_apks, virustotal, koodous, all_checks, output, from_f
@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, iocs, output, fast, list_modules, module, serial):
if list_modules:
log.info("Following is the list of available check-adb modules:")
for adb_module in ADB_MODULES:
log.info(" - %s", adb_module.__name__)
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 through adb bridge")
log.info("Checking Android device over debug bridge")
if output and 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)
cmd.run()
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
timeline = []
timeline_detected = []
for adb_module in ADB_MODULES:
if module and adb_module.__name__ != module:
continue
m = adb_module(output_folder=output, fast_mode=fast,
log=logging.getLogger(adb_module.__module__))
if indicators.total_ioc_count:
m.indicators = indicators
m.indicators.log = m.log
if serial:
m.serial = serial
run_module(m)
timeline.extend(m.timeline)
timeline_detected.extend(m.timeline_detected)
if output:
if len(timeline) > 0:
save_timeline(timeline, os.path.join(output, "timeline.csv"))
if len(timeline_detected) > 0:
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the Android device produced %d detections!",
len(cmd.timeline_detected))
#==============================================================================
# Check ADB backup
# 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, iocs, output, backup_path, serial):
log.info("Checking ADB backup located at: %s", backup_path)
def check_backup(ctx, serial, iocs, output, list_modules, backup_path):
cmd = CmdAndroidCheckBackup(target_path=backup_path, results_path=output,
ioc_files=iocs)
if output and 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)
if list_modules:
cmd.list_modules()
return
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
log.info("Checking Android backup at path: %s", backup_path)
if os.path.isfile(backup_path):
log.critical("The path you specified is a not a folder!")
cmd.run()
if os.path.basename(backup_path) == "backup.ab":
log.info("You can use ABE (https://github.com/nelenkov/android-backup-extractor) "
"to extract 'backup.ab' files!")
ctx.exit(1)
for module in BACKUP_MODULES:
m = module(base_folder=backup_path, output_folder=output,
log=logging.getLogger(module.__module__))
if indicators.total_ioc_count:
m.indicators = indicators
m.indicators.log = m.log
if serial:
m.serial = serial
run_module(m)
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the Android backup produced %d detections!",
len(cmd.timeline_detected))
#==============================================================================
@@ -210,59 +200,14 @@ def check_backup(ctx, iocs, output, backup_path, serial):
@click.argument("FOLDER", type=click.Path(exists=True))
@click.pass_context
def check_iocs(ctx, iocs, list_modules, module, folder):
all_modules = []
for entry in BACKUP_MODULES + ADB_MODULES:
if entry not in all_modules:
all_modules.append(entry)
cmd = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module)
cmd.modules = BACKUP_MODULES + ADB_MODULES + BUGREPORT_MODULES
if list_modules:
log.info("Following is the list of available check-iocs modules:")
for iocs_module in all_modules:
log.info(" - %s", iocs_module.__name__)
cmd.list_modules()
return
log.info("Checking stored results against provided indicators...")
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
total_detections = 0
for file_name in os.listdir(folder):
name_only, ext = os.path.splitext(file_name)
file_path = os.path.join(folder, file_name)
# TODO: Skipping processing of result files that are not json.
# We might want to revisit this eventually.
if ext != ".json":
continue
for iocs_module in all_modules:
if module and iocs_module.__name__ != module:
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 indicators.total_ioc_count > 0:
m.indicators = indicators
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)
cmd.run()
#==============================================================================
@@ -270,4 +215,5 @@ def check_iocs(ctx, iocs, list_modules, module, folder):
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
def download_indicators():
download_indicators_files(log)
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,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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/
@@ -44,11 +44,12 @@ class DownloadAPKs(AndroidExtraction):
or filter known-goods
:param packages: Provided list of packages, typically for JSON checks
"""
super().__init__(output_folder=output_folder, log=log)
super().__init__(log=log)
self.packages = packages
self.all_apks = all_apks
self.output_folder_apk = None
self.output_folder = output_folder
@classmethod
def from_json(cls, json_path):
@@ -57,7 +58,7 @@ class DownloadAPKs(AndroidExtraction):
:param json_path: Path to the apks.json file to parse.
"""
with open(json_path, "r") as handle:
with open(json_path, "r", encoding="utf-8") as handle:
packages = json.load(handle)
return cls(packages=packages)
@@ -114,6 +115,7 @@ class DownloadAPKs(AndroidExtraction):
m = Packages()
m.log = self.log
m.serial = self.serial
m.run()
self.packages = m.results
@@ -173,10 +175,10 @@ class DownloadAPKs(AndroidExtraction):
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") as handle:
with open(json_path, "w", encoding="utf-8") as handle:
json.dump(self.packages, handle, indent=4)
def run(self):
def run(self) -> None:
"""Run all steps of fetch-apk."""
self.get_packages()
self._adb_connect()

View File

@@ -1,10 +0,0 @@
su
busybox
supersu
Superuser.apk
KingoUser.apk
SuperSu.apk
magisk
magiskhide
magiskinit
magiskpolicy

View File

@@ -1,25 +0,0 @@
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

View File

@@ -1,4 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# 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,58 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import requests
from rich.console import Console
from rich.progress import track
from rich.table import Table
from rich.text import Text
log = logging.getLogger(__name__)
def koodous_lookup(packages):
log.info("Looking up all extracted files on Koodous (www.koodous.com)")
log.info("This might take a while...")
table = Table(title="Koodous Packages Detections")
table.add_column("Package name")
table.add_column("File name")
table.add_column("Trusted")
table.add_column("Detected")
table.add_column("Rating")
total_packages = len(packages)
for i in track(range(total_packages), description=f"Looking up {total_packages} packages..."):
package = packages[i]
for file in package.get("files", []):
url = f"https://api.koodous.com/apks/{file['sha256']}"
res = requests.get(url)
report = res.json()
row = [package["package_name"], file["path"]]
if "package_name" in report:
trusted = "no"
if report["trusted"]:
trusted = Text("yes", "green bold")
detected = "no"
if report["detected"]:
detected = Text("yes", "red bold")
rating = "0"
if int(report["rating"]) < 0:
rating = Text(str(report["rating"]), "red bold")
row.extend([trusted, detected, rating])
else:
row.extend(["n/a", "n/a", "n/a"])
table.add_row(*row)
console = Console()
console.print(table)

View File

@@ -1,96 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import requests
from rich.console import Console
from rich.progress import track
from rich.table import Table
from rich.text import Text
log = logging.getLogger(__name__)
def get_virustotal_report(hashes):
apikey = "233f22e200ca5822bd91103043ccac138b910db79f29af5616a9afe8b6f215ad"
url = f"https://www.virustotal.com/partners/sysinternals/file-reports?apikey={apikey}"
items = []
for sha256 in hashes:
items.append({
"autostart_location": "",
"autostart_entry": "",
"hash": sha256,
"local_name": "",
"creation_datetime": "",
})
headers = {"User-Agent": "VirusTotal", "Content-Type": "application/json"}
res = requests.post(url, headers=headers, json=items)
if res.status_code == 200:
report = res.json()
return report["data"]
else:
log.error("Unexpected response from VirusTotal: %s", res.status_code)
return None
def virustotal_lookup(packages):
log.info("Looking up all extracted files on VirusTotal (www.virustotal.com)")
unique_hashes = []
for package in packages:
for file in package.get("files", []):
if file["sha256"] not in unique_hashes:
unique_hashes.append(file["sha256"])
total_unique_hashes = len(unique_hashes)
detections = {}
def virustotal_query(batch):
report = get_virustotal_report(batch)
if not report:
return
for entry in report:
if entry["hash"] not in detections and entry["found"] is True:
detections[entry["hash"]] = entry["detection_ratio"]
batch = []
for i in track(range(total_unique_hashes), description=f"Looking up {total_unique_hashes} files..."):
file_hash = unique_hashes[i]
batch.append(file_hash)
if len(batch) == 25:
virustotal_query(batch)
batch = []
if batch:
virustotal_query(batch)
table = Table(title="VirusTotal Packages Detections")
table.add_column("Package name")
table.add_column("File path")
table.add_column("Detections")
for package in packages:
for file in package.get("files", []):
row = [package["package_name"], file["path"]]
if file["sha256"] in detections:
detection = detections[file["sha256"]]
positives = detection.split("/")[0]
if int(positives) > 0:
row.append(Text(detection, "red bold"))
else:
row.append(detection)
else:
row.append("not found")
table.add_row(*row)
console = Console()
console.print(table)

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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,14 +1,16 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 .chrome_history import ChromeHistory
from .dumpsys_accessibility import DumpsysAccessibility
from .dumpsys_batterystats import DumpsysBatterystats
from .dumpsys_activities import DumpsysActivities
from .dumpsys_appops import DumpsysAppOps
from .dumpsys_battery_daily import DumpsysBatteryDaily
from .dumpsys_battery_history import DumpsysBatteryHistory
from .dumpsys_dbinfo import DumpsysDBInfo
from .dumpsys_full import DumpsysFull
from .dumpsys_packages import DumpsysPackages
from .dumpsys_procstats import DumpsysProcstats
from .dumpsys_receivers import DumpsysReceivers
from .files import Files
from .getprop import Getprop
@@ -16,11 +18,13 @@ from .logcat import Logcat
from .packages import Packages
from .processes import Processes
from .root_binaries import RootBinaries
from .selinux_status import SELinuxStatus
from .settings import Settings
from .sms import SMS
from .whatsapp import Whatsapp
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes, Getprop, Settings,
DumpsysAccessibility, DumpsysBatterystats, DumpsysProcstats,
DumpsysPackages, DumpsysReceivers, DumpsysFull,
Packages, RootBinaries, Logcat, Files]
SELinuxStatus, DumpsysBatteryHistory, DumpsysBatteryDaily,
DumpsysReceivers, DumpsysActivities, DumpsysAccessibility,
DumpsysDBInfo, DumpsysFull, DumpsysAppOps, Packages, Logcat,
RootBinaries, Files]

View File

@@ -1,8 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 base64
import logging
import os
import random
@@ -10,14 +11,18 @@ import string
import sys
import tempfile
import time
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 rich.prompt import Prompt
from usb1 import USBErrorAccess, USBErrorBusy
from mvt.android.parsers.backup import (InvalidBackupPassword, parse_ab_header,
parse_backup_file)
from mvt.common.module import InsufficientPrivileges, MVTModule
log = logging.getLogger(__name__)
@@ -29,17 +34,18 @@ 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=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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
@staticmethod
def _adb_check_keys():
def _adb_check_keys() -> None:
"""Make sure Android adb keys exist."""
if not os.path.isdir(os.path.dirname(ADB_KEY_PATH)):
os.makedirs(os.path.dirname(ADB_KEY_PATH))
@@ -50,7 +56,7 @@ class AndroidExtraction(MVTModule):
if not os.path.exists(ADB_PUB_KEY_PATH):
write_public_keyfile(ADB_KEY_PATH, ADB_PUB_KEY_PATH)
def _adb_connect(self):
def _adb_connect(self) -> None:
"""Connect to the device over adb."""
self._adb_check_keys()
@@ -99,17 +105,17 @@ class AndroidExtraction(MVTModule):
else:
break
def _adb_disconnect(self):
def _adb_disconnect(self) -> None:
"""Close adb connection to the device."""
self.device.close()
def _adb_reconnect(self):
def _adb_reconnect(self) -> None:
"""Reconnect to device using adb."""
log.info("Reconnecting ...")
self._adb_disconnect()
self._adb_connect()
def _adb_command(self, command):
def _adb_command(self, command: str) -> str:
"""Execute an adb shell command.
:param command: Shell command to execute
@@ -118,7 +124,7 @@ class AndroidExtraction(MVTModule):
"""
return self.device.shell(command, read_timeout_s=200.0)
def _adb_check_if_root(self):
def _adb_check_if_root(self) -> bool:
"""Check if we have a `su` binary on the Android device.
@@ -127,7 +133,7 @@ class AndroidExtraction(MVTModule):
"""
return bool(self._adb_command("command -v su"))
def _adb_root_or_die(self):
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!")
@@ -141,7 +147,7 @@ class AndroidExtraction(MVTModule):
"""
return self._adb_command(f"su -c {command}")
def _adb_check_file_exists(self, file):
def _adb_check_file_exists(self, file: str) -> bool:
"""Verify that a file exists.
:param file: Path of the file
@@ -158,7 +164,9 @@ class AndroidExtraction(MVTModule):
return bool(self._adb_command_as_root(f"[ ! -f {file} ] || echo 1"))
def _adb_download(self, remote_path, local_path, progress_callback=None, retry_root=True):
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
@@ -175,7 +183,8 @@ class AndroidExtraction(MVTModule):
else:
raise Exception(f"Unable to download file {remote_path}: {e}")
def _adb_download_root(self, remote_path, local_path, progress_callback=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()
@@ -203,7 +212,8 @@ class AndroidExtraction(MVTModule):
except AdbCommandFailureException as e:
raise Exception(f"Unable to download file {remote_path}: {e}")
def _adb_process_file(self, remote_path, process_routine):
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.
@@ -243,6 +253,32 @@ class AndroidExtraction(MVTModule):
# Disconnect from the device.
self._adb_disconnect()
def run(self):
def _generate_backup(self, package_name: str) -> bytes:
self.log.warning("Please check phone and accept Android backup prompt. You may need to set a backup password. \a")
# 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
if header["encryption"] == "none":
return parse_backup_file(backup_output, password=None)
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.warn("All attempts to decrypt backup with password failed!")
def run(self) -> None:
"""Run the main procedure."""
raise NotImplementedError

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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/
@@ -20,13 +20,14 @@ 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=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def serialize(self, record: dict) -> None:
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
@@ -34,7 +35,7 @@ class ChromeHistory(AndroidExtraction):
"data": f"{record['id']} - {record['url']} (visit ID: {record['visit_id']}, redirect source: {record['redirect_source']})"
}
def check_indicators(self):
def check_indicators(self) -> None:
if not self.indicators:
return
@@ -42,7 +43,7 @@ class ChromeHistory(AndroidExtraction):
if self.indicators.check_domain(result["url"]):
self.detected.append(result)
def _parse_db(self, db_path):
def _parse_db(self, db_path: str) -> None:
"""Parse a Chrome History database file.
:param db_path: Path to the History database to process.
@@ -68,7 +69,7 @@ class ChromeHistory(AndroidExtraction):
"url": item[1],
"visit_id": item[2],
"timestamp": item[3],
"isodate": convert_timestamp_to_iso(convert_chrometime_to_unix[item[3]]),
"isodate": convert_timestamp_to_iso(convert_chrometime_to_unix(item[3])),
"redirect_source": item[4],
})
@@ -77,6 +78,9 @@ class ChromeHistory(AndroidExtraction):
log.info("Extracted a total of %d history items", len(self.results))
def run(self):
self._adb_process_file(os.path.join("/", CHROME_HISTORY_PATH),
self._parse_db)
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

@@ -1,11 +1,11 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 io
import logging
import os
from mvt.android.parsers import parse_dumpsys_accessibility
from .base import AndroidExtraction
@@ -15,39 +15,32 @@ log = logging.getLogger(__name__)
class DumpsysAccessibility(AndroidExtraction):
"""This module extracts stats on accessibility."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
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()
stats = self._adb_command("dumpsys accessibility")
in_services = False
for line in stats.split("\n"):
if line.strip().startswith("installed services:"):
in_services = True
continue
if not in_services:
continue
if line.strip() == "}":
break
service = line.split(":")[1].strip()
log.info("Found installed accessibility service \"%s\"", service)
if self.output_folder:
acc_path = os.path.join(self.output_folder,
"dumpsys_accessibility.txt")
with io.open(acc_path, "w", encoding="utf-8") as handle:
handle.write(stats)
log.info("Records from dumpsys accessibility stored at %s",
acc_path)
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

@@ -1,46 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import os
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysBatterystats(AndroidExtraction):
"""This module extracts stats on battery consumption by processes."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def run(self):
self._adb_connect()
stats = self._adb_command("dumpsys batterystats")
if self.output_folder:
stats_path = os.path.join(self.output_folder,
"dumpsys_batterystats.txt")
with open(stats_path, "w") as handle:
handle.write(stats)
log.info("Records from dumpsys batterystats stored at %s",
stats_path)
history = self._adb_command("dumpsys batterystats --history")
if self.output_folder:
history_path = os.path.join(self.output_folder,
"dumpsys_batterystats_history.txt")
with open(history_path, "w") as handle:
handle.write(history)
log.info("History records from dumpsys batterystats stored at %s",
history_path)
self._adb_disconnect()

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

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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/
@@ -14,23 +14,22 @@ log = logging.getLogger(__name__)
class DumpsysFull(AndroidExtraction):
"""This module extracts stats on battery consumption by processes."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def run(self) -> None:
self._adb_connect()
stats = self._adb_command("dumpsys")
if self.output_folder:
stats_path = os.path.join(self.output_folder,
"dumpsys.txt")
with open(stats_path, "w") as handle:
handle.write(stats)
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",
stats_path)
log.info("Full dumpsys output stored at %s", output_path)
self._adb_disconnect()

View File

@@ -1,37 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import os
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysPackages(AndroidExtraction):
"""This module extracts details on installed packages."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys package")
if self.output_folder:
packages_path = os.path.join(self.output_folder,
"dumpsys_packages.txt")
with open(packages_path, "w") as handle:
handle.write(output)
log.info("Records from dumpsys package stored at %s",
packages_path)
self._adb_disconnect()

View File

@@ -1,36 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import os
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysProcstats(AndroidExtraction):
"""This module extracts stats on memory consumption by processes."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys procstats")
if self.output_folder:
procstats_path = os.path.join(self.output_folder,
"dumpsys_procstats.txt")
with open(procstats_path, "w") as handle:
handle.write(output)
log.info("Records from dumpsys procstats stored at %s",
procstats_path)
self._adb_disconnect()

View File

@@ -1,10 +1,12 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 mvt.android.parsers import parse_dumpsys_receiver_resolver_table
from .base import AndroidExtraction
log = logging.getLogger(__name__)
@@ -19,78 +21,47 @@ 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=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
for result in self.results:
if result["activity"] == INTENT_NEW_OUTGOING_SMS:
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
result["receiver"])
elif result["activity"] == INTENT_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
result["receiver"])
elif result["activity"] == INTENT_DATA_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
result["receiver"])
elif result["activity"] == INTENT_PHONE_STATE:
self.log.info("Found a receiver monitoring telephony state/incoming calls: \"%s\"",
result["receiver"])
elif result["activity"] == INTENT_NEW_OUTGOING_CALL:
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
result["receiver"])
self.results = results if results else {}
def run(self):
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")
if not output:
return
activity = None
for line in output.split("\n"):
# Find activity block markers.
if line.strip().startswith(INTENT_NEW_OUTGOING_SMS):
activity = INTENT_NEW_OUTGOING_SMS
continue
elif line.strip().startswith(INTENT_SMS_RECEIVED):
activity = INTENT_SMS_RECEIVED
continue
elif line.strip().startswith(INTENT_PHONE_STATE):
activity = INTENT_PHONE_STATE
continue
elif line.strip().startswith(INTENT_DATA_SMS_RECEIVED):
activity = INTENT_DATA_SMS_RECEIVED
continue
elif line.strip().startswith(INTENT_NEW_OUTGOING_CALL):
activity = INTENT_NEW_OUTGOING_CALL
continue
# If we are not in an activity block yet, skip.
if not activity:
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):
activity = 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]
if package_name == "com.google.android.gms":
continue
self.results.append({
"activity": activity,
"package_name": package_name,
"receiver": receiver,
})
self.results = parse_dumpsys_receiver_resolver_table(output)
self._adb_disconnect()

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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/
@@ -17,16 +17,17 @@ log = logging.getLogger(__name__)
class Files(AndroidExtraction):
"""This module extracts the list of files on the device."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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, file_path):
def find_files(self, folder: str) -> None:
if self.full_find:
output = self._adb_command(f"find '{file_path}' -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
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)
@@ -42,11 +43,11 @@ class Files(AndroidExtraction):
"group": group,
})
else:
output = self._adb_command(f"find '{file_path}' 2> /dev/null")
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):
def serialize(self, record: dict) -> None:
if "modified_time" in record:
return {
"timestamp": record["modified_time"],
@@ -55,7 +56,7 @@ class Files(AndroidExtraction):
"data": record["path"],
}
def check_suspicious(self):
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"):
@@ -63,7 +64,7 @@ class Files(AndroidExtraction):
result["path"])
self.detected.append(result)
def check_indicators(self):
def check_indicators(self) -> None:
"""Check file list for known suspicious files or suspicious properties"""
self.check_suspicious()
@@ -75,7 +76,7 @@ class Files(AndroidExtraction):
self.log.warning("Found a known suspicous file at path: \"%s\"", result["path"])
self.detected.append(result)
def run(self):
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")

View File

@@ -1,11 +1,12 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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
import re
from datetime import datetime, timedelta
from mvt.android.parsers import parse_getprop
from .base import AndroidExtraction
@@ -15,32 +16,28 @@ log = logging.getLogger(__name__)
class Getprop(AndroidExtraction):
"""This module extracts device properties from getprop command."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def run(self) -> None:
self._adb_connect()
rxp = re.compile("\\[(.+?)\\]: \\[(.+?)\\]")
out = self._adb_command("getprop")
for line in out.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]
self.results[key] = value
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

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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/
@@ -14,13 +14,14 @@ log = logging.getLogger(__name__)
class Logcat(AndroidExtraction):
"""This module extracts details on installed packages."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def run(self) -> None:
self._adb_connect()
# Get the current logcat.
@@ -28,18 +29,18 @@ class Logcat(AndroidExtraction):
# Get the locat prior to last reboot.
last_output = self._adb_command("logcat -L")
if self.output_folder:
logcat_path = os.path.join(self.output_folder,
if self.results_path:
logcat_path = os.path.join(self.results_path,
"logcat.txt")
with open(logcat_path, "w") as handle:
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.output_folder,
logcat_last_path = os.path.join(self.results_path,
"logcat_last.txt")
with open(logcat_last_path, "w") as handle:
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",

View File

@@ -1,31 +1,85 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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
import pkg_resources
from rich.console import Console
from rich.progress import track
from rich.table import Table
from rich.text import Text
from mvt.android.lookups.koodous import koodous_lookup
from mvt.android.lookups.virustotal import virustotal_lookup
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=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def serialize(self, record: dict) -> None:
records = []
timestamps = [
@@ -44,39 +98,130 @@ class Packages(AndroidExtraction):
return records
def check_indicators(self):
root_packages_path = os.path.join("..", "..", "data", "root_packages.txt")
root_packages_string = pkg_resources.resource_string(__name__, root_packages_path)
root_packages = root_packages_string.decode("utf-8").split("\n")
root_packages = [rp.strip() for rp in root_packages]
def check_indicators(self) -> None:
for result in self.results:
if result["package_name"] in root_packages:
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_indicators"] = ioc
result["matched_indicator"] = ioc
self.detected.append(result)
continue
for package_file in result["files"]:
for package_file in result.get("files", []):
ioc = self.indicators.check_file_hash(package_file["sha256"])
if ioc:
result["matched_indicators"] = ioc
result["matched_indicator"] = ioc
self.detected.append(result)
def _get_files_for_package(self, package_name):
@staticmethod
def check_virustotal(packages: list) -> None:
hashes = []
for package in packages:
for file in package.get("files", []):
if file["sha256"] not in hashes:
hashes.append(file["sha256"])
total_hashes = len(hashes)
detections = {}
for i in track(range(total_hashes), description=f"Looking up {total_hashes} files..."):
try:
results = virustotal_lookup(hashes[i])
except VTNoKey as e:
log.info(e)
return
except VTQuotaExceeded as e:
log.error("Unable to continue: %s", e)
break
if not results:
continue
positives = results["attributes"]["last_analysis_stats"]["malicious"]
total = len(results["attributes"]["last_analysis_results"])
detections[hashes[i]] = f"{positives}/{total}"
table = Table(title="VirusTotal Packages Detections")
table.add_column("Package name")
table.add_column("File path")
table.add_column("Detections")
for package in packages:
for file in package.get("files", []):
row = [package["package_name"], file["path"]]
if file["sha256"] in detections:
detection = detections[file["sha256"]]
positives = detection.split("/")[0]
if int(positives) > 0:
row.append(Text(detection, "red bold"))
else:
row.append(detection)
else:
row.append("not found")
table.add_row(*row)
console = Console()
console.print(table)
@staticmethod
def parse_package_for_details(output: str) -> dict:
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"requested_permissions": [],
}
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:
output = self._adb_command(f"pm path {package_name}")
output = output.strip().replace("package:", "")
if not output:
return []
package_files = []
for file_path in output.split("\n"):
for file_path in output.splitlines():
file_path = file_path.strip()
md5 = self._adb_command(f"md5sum {file_path}").split(" ")[0]
@@ -94,14 +239,12 @@ class Packages(AndroidExtraction):
return package_files
def run(self):
def run(self) -> None:
self._adb_connect()
packages = self._adb_command("pm list packages -U -u -i -f")
if packages.strip() == "Error: Unknown option: -U":
packages = self._adb_command("pm list packages -u -i -f")
packages = self._adb_command("pm list packages -u -i -f")
for line in packages.split("\n"):
for line in packages.splitlines():
line = line.strip()
if not line.startswith("package:"):
continue
@@ -117,31 +260,22 @@ class Packages(AndroidExtraction):
if installer == "null":
installer = None
try:
uid = fields[2].split(":")[1].strip()
except IndexError:
uid = None
dumpsys = self._adb_command(f"dumpsys package {package_name} | grep -A2 timeStamp").split("\n")
timestamp = dumpsys[0].split("=")[1].strip()
first_install = dumpsys[1].split("=")[1].strip()
last_update = dumpsys[2].split("=")[1].strip()
package_files = self._get_files_for_package(package_name)
self.results.append({
new_package = {
"package_name": package_name,
"file_name": file_name,
"installer": installer,
"timestamp": timestamp,
"first_install_time": first_install,
"last_update_time": last_update,
"uid": uid,
"disabled": False,
"system": False,
"third_party": False,
"files": package_files,
})
}
dumpsys_package = self._adb_command(f"dumpsys package {package_name}")
package_details = self.parse_package_for_details(dumpsys_package)
new_package.update(package_details)
self.results.append(new_package)
cmds = [
{"field": "disabled", "arg": "-d"},
@@ -150,7 +284,7 @@ class Packages(AndroidExtraction):
]
for cmd in cmds:
output = self._adb_command(f"pm list packages {cmd['arg']}")
for line in output.split("\n"):
for line in output.splitlines():
line = line.strip()
if not line.startswith("package:"):
continue
@@ -160,6 +294,19 @@ class Packages(AndroidExtraction):
if result["package_name"] == package_name:
self.results[i][cmd["field"]] = True
for result in self.results:
if not result["third_party"]:
continue
dangerous_permissions_count = 0
for perm in result["requested_permissions"]:
if perm in DANGEROUS_PERMISSIONS:
dangerous_permissions_count += 1
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
self.log.info("Third-party package \"%s\" requested %d potentially dangerous permissions",
result["package_name"], dangerous_permissions_count)
packages_to_lookup = []
for result in self.results:
if result["system"]:
@@ -170,8 +317,7 @@ class Packages(AndroidExtraction):
result["package_name"], result["installer"], result["timestamp"])
if not self.fast_mode:
virustotal_lookup(packages_to_lookup)
koodous_lookup(packages_to_lookup)
self.check_virustotal(packages_to_lookup)
self.log.info("Extracted at total of %d installed package names",
len(self.results))

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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,18 +13,29 @@ log = logging.getLogger(__name__)
class Processes(AndroidExtraction):
"""This module extracts details on running processes."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
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.split("\n")[1:]:
for line in output.splitlines()[1:]:
line = line.strip()
if line == "":
continue

View File

@@ -1,12 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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
import pkg_resources
from .base import AndroidExtraction
@@ -16,16 +13,26 @@ log = logging.getLogger(__name__)
class RootBinaries(AndroidExtraction):
"""This module extracts the list of installed packages."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
root_binaries_path = os.path.join("..", "..", "data", "root_binaries.txt")
root_binaries_string = pkg_resources.resource_string(__name__, root_binaries_path)
root_binaries = root_binaries_string.decode("utf-8").split("\n")
def run(self) -> None:
root_binaries = [
"su",
"busybox",
"supersu",
"Superuser.apk",
"KingoUser.apk",
"SuperSu.apk",
"magisk",
"magiskhide",
"magiskinit",
"magiskpolicy",
]
self._adb_connect()

View File

@@ -0,0 +1,38 @@
# 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 SELinuxStatus(AndroidExtraction):
"""This module checks if SELinux is being enforced."""
slug = "selinux_status"
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("getenforce")
self._adb_disconnect()
status = output.lower().strip()
self.results["status"] = status
if status == "enforcing":
self.log.info("SELinux is being regularly enforced")
else:
self.log.warning("SELinux status is \"%s\"!", status)

View File

@@ -1,11 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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
import re
from .base import AndroidExtraction
@@ -33,11 +31,6 @@ ANDROID_DANGEROUS_SETTINGS = [
"key": "upload_apk_enable",
"safe_value": "1",
},
{
"description": "enabled installation of non-market apps",
"key": "install_non_market_apps",
"safe_value": "0",
},
{
"description": "disabled confirmation of adb apps installation",
"key": "adb_install_need_confirm",
@@ -60,18 +53,20 @@ ANDROID_DANGEROUS_SETTINGS = [
},
]
class Settings(AndroidExtraction):
"""This module extracts Android system settings."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def check_indicators(self) -> None:
for namespace, settings in self.results.items():
for key, value in settings.items():
for danger in ANDROID_DANGEROUS_SETTINGS:
@@ -82,7 +77,7 @@ class Settings(AndroidExtraction):
key, value, danger["description"])
break
def run(self):
def run(self) -> None:
self._adb_connect()
for namespace in ["system", "secure", "global"]:

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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,6 +7,9 @@ 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
@@ -16,11 +19,11 @@ 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 number,
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 text
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)
@@ -32,10 +35,10 @@ WHERE (m.conversation_id = c._id)
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
SMS_MMSMS_QUERY = """
SELECT
address AS number,
address AS address,
date_sent AS timestamp,
type as incoming,
body AS text
body AS body
FROM sms;
"""
@@ -43,34 +46,36 @@ FROM sms;
class SMS(AndroidExtraction):
"""This module extracts all SMS messages containing links."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
text = record["text"].replace("\n", "\\n")
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['number']}: \"{text}\""
"data": f"{record['address']}: \"{body}\""
}
def check_indicators(self):
def check_indicators(self) -> None:
if not self.indicators:
return
for message in self.results:
if "text" not in message:
if "body" not in message:
continue
message_links = check_for_links(message["text"])
# 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):
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
@@ -79,9 +84,9 @@ class SMS(AndroidExtraction):
conn = sqlite3.connect(db_path)
cur = conn.cursor()
if (self.SMS_DB_TYPE == 1):
if self.SMS_DB_TYPE == 1:
cur.execute(SMS_BUGLE_QUERY)
elif (self.SMS_DB_TYPE == 2):
elif self.SMS_DB_TYPE == 2:
cur.execute(SMS_MMSMS_QUERY)
names = [description[0] for description in cur.description]
@@ -96,7 +101,7 @@ class SMS(AndroidExtraction):
# 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["text"]) or message["text"].strip() == "":
if check_for_links(message["body"]) or message["body"].strip() == "":
self.results.append(message)
cur.close()
@@ -104,12 +109,35 @@ class SMS(AndroidExtraction):
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
def run(self):
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)
else:
self.log.error("No SMS database found")
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 The MVT Project 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/
@@ -20,13 +20,14 @@ 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=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def serialize(self, record: dict) -> None:
text = record["data"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
@@ -35,7 +36,7 @@ class Whatsapp(AndroidExtraction):
"data": f"\"{text}\""
}
def check_indicators(self):
def check_indicators(self) -> None:
if not self.indicators:
return
@@ -47,7 +48,7 @@ class Whatsapp(AndroidExtraction):
if self.indicators.check_domains(message_links):
self.detected.append(message)
def _parse_db(self, db_path):
def _parse_db(self, db_path: str) -> None:
"""Parse an Android msgstore.db WhatsApp database file.
:param db_path: Path to the Android WhatsApp database file to process
@@ -84,5 +85,8 @@ class Whatsapp(AndroidExtraction):
log.info("Extracted a total of %d WhatsApp messages containing links", len(messages))
self.results = messages
def run(self):
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
def run(self) -> None:
try:
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
except Exception as e:
self.log.error(e)

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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,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 fnmatch
import os
from tarfile import TarFile
from mvt.common.module import MVTModule
class BackupExtraction(MVTModule):
"""This class provides a base for all backup extractios modules"""
ab = 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: str, tar: TarFile, files: list) -> None:
"""
Extract the files
"""
self.ab = file_path
self.tar = tar
self.files = files
def _get_files_by_pattern(self, pattern: str) -> list:
return fnmatch.filter(self.files, pattern)
def _get_file_content(self, file_path: str) -> bytes:
if self.ab:
try:
member = self.tar.getmember(file_path)
except KeyError:
return None
handle = self.tar.extractfile(member)
else:
handle = open(os.path.join(self.backup_path, file_path), "rb")
data = handle.read()
handle.close()
return data

View File

@@ -1,25 +1,24 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 os
import zlib
import logging
from mvt.common.module import MVTModule
from mvt.common.utils import check_for_links
from mvt.android.modules.backup.base import BackupExtraction
from mvt.android.parsers.backup import parse_sms_file
class SMS(MVTModule):
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def check_indicators(self) -> None:
if not self.indicators:
return
@@ -27,38 +26,19 @@ class SMS(MVTModule):
if "body" not in message:
continue
message_links = check_for_links(message["body"])
if self.indicators.check_domains(message_links):
if self.indicators.check_domains(message["links"]):
self.detected.append(message)
def _process_sms_file(self, file_path):
self.log.info("Processing SMS backup file at %s", file_path)
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))
with open(file_path, "rb") as handle:
data = zlib.decompress(handle.read())
json_data = json.loads(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))
for entry in json_data:
message_links = check_for_links(entry["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() == "":
self.results.append(entry)
def run(self):
app_folder = os.path.join(self.base_folder,
"apps",
"com.android.providers.telephony",
"d_f")
if not os.path.exists(app_folder):
raise FileNotFoundError("Unable to find the SMS backup folder")
for file_name in os.listdir(app_folder):
if not file_name.endswith("_sms_backup"):
continue
file_path = os.path.join(app_folder, file_name)
self._process_sms_file(file_path)
self.log.info("Extracted a total of %d SMS messages containing links",
self.log.info("Extracted a total of %d SMS & MMS messages containing links",
len(self.results))

View File

@@ -0,0 +1,17 @@
# 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 .accessibility import Accessibility
from .activities import Activities
from .appops import Appops
from .battery_daily import BatteryDaily
from .battery_history import BatteryHistory
from .dbinfo import DBInfo
from .getprop import Getprop
from .packages import Packages
from .receivers import Receivers
BUGREPORT_MODULES = [Accessibility, Activities, Appops, BatteryDaily,
BatteryHistory, DBInfo, Getprop, Packages, Receivers]

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

@@ -0,0 +1,71 @@
# Mobile Verification Toolkit (MVT)
# 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 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."""
zip_archive = 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: ZipFile, zip_files: list) -> None:
self.zip_archive = zip_archive
self.zip_files = zip_files
def _get_files_by_pattern(self, pattern: str) -> list:
file_names = []
if self.zip_archive:
for zip_file in self.zip_files:
file_names.append(zip_file)
else:
file_names = self.extract_files
return fnmatch.filter(file_names, pattern)
def _get_files_by_patterns(self, patterns: list) -> list:
for pattern in patterns:
matches = self._get_files_by_pattern(pattern)
if matches:
return matches
def _get_file_content(self, file_path: str) -> bytes:
if self.zip_archive:
handle = self.zip_archive.open(file_path)
else:
handle = open(os.path.join(self.extract_path, file_path), "rb")
data = handle.read()
handle.close()
return data
def _get_dumpstate_file(self) -> bytes:
main = self._get_files_by_pattern("main_entry.txt")
if main:
main_content = self._get_file_content(main[0])
try:
return self._get_file_content(main_content.decode().strip())
except KeyError:
return None
else:
dumpstate_logs = self._get_files_by_pattern("dumpState_*.log")
if not dumpstate_logs:
return None
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

@@ -0,0 +1,211 @@
# 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 io
import json
import tarfile
import zlib
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_timestamp_to_iso
PBKDF2_KEY_SIZE = 32
class AndroidBackupParsingError(Exception):
"""Exception raised file parsing an android backup file"""
class AndroidBackupNotImplemented(AndroidBackupParsingError):
pass
class InvalidBackupPassword(AndroidBackupParsingError):
pass
def to_utf8_bytes(input_bytes):
output = []
for byte in input_bytes:
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')))
return bytes(output)
def parse_ab_header(data):
"""
Parse the header of an Android Backup file
Returns a dict {'backup': True, 'compression': False,
'encryption': "none", 'version': 4}
"""
if data.startswith(b"ANDROID BACKUP"):
[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")
}
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):
"""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)
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()
# Extract key and IV from decrypted blob.
key_blob = io.BytesIO(decryted_master_key_blob)
master_iv_length = ord(key_blob.read(1))
master_iv = key_blob.read(master_iv_length)
master_key_length = ord(key_blob.read(1))
master_key = key_blob.read(master_key_length)
master_key_checksum_length = ord(key_blob.read(1))
master_key_checksum = key_blob.read(master_key_checksum_length)
except TypeError:
raise InvalidBackupPassword()
# Handle quirky encoding of master key bytes in Android original Java crypto code.
if format_version > 1:
hmac_mk = to_utf8_bytes(master_key)
else:
hmac_mk = master_key
# Derive checksum to confirm successful backup decryption.
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:
raise InvalidBackupPassword()
return master_key, master_iv
def decrypt_backup_data(encrypted_backup, password, encryption_algo, format_version):
"""
Generate encryption keyffrom password and do decryption
"""
if encryption_algo != b"AES-256":
raise AndroidBackupNotImplemented("Encryption Algorithm not implemented")
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 = bytes.fromhex(user_salt.decode("utf-8"))
checksum_salt = bytes.fromhex(checksum_salt.decode("utf-8"))
pbkdf2_rounds = int(pbkdf2_rounds)
user_iv = bytes.fromhex(user_iv.decode("utf-8"))
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)
# Decrypt and unpad backup data using derivied key.
cipher = Cipher(algorithms.AES(master_key), modes.CBC(master_iv))
decryptor = cipher.decryptor()
decrypted_tar = decryptor.update(encrypted_data) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
return unpadder.update(decrypted_tar)
def parse_backup_file(data, password=None):
"""
Parse an ab file, returns a tar file
"""
if not data.startswith(b"ANDROID BACKUP"):
raise AndroidBackupParsingError("Invalid file header")
[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)
if is_compressed:
try:
tar_data = zlib.decompress(tar_data)
except zlib.error:
raise AndroidBackupParsingError("Impossible to decompress the backup file")
return tar_data
def parse_tar_for_sms(data):
"""
Extract SMS from a tar backup archive
Returns an array of SMS
"""
dbytes = io.BytesIO(data)
tar = tarfile.open(fileobj=dbytes)
res = []
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
def parse_sms_file(data):
"""
Parse an SMS or MMS file extracted from a backup
Returns a list of SMS or MMS entries
"""
res = []
data = zlib.decompress(data)
json_data = json.loads(data)
for entry in json_data:
# Adapt MMS format to SMS format
if "mms_body" in entry:
entry["body"] = entry["mms_body"]
entry.pop("mms_body")
message_links = check_for_links(entry["body"])
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")
# 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)
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 The MVT Project 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

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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,40 +1,41 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 io
import json
import logging
import os
import requests
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=None):
self.data_dir = user_data_dir("mvt")
def __init__(self, log=logging.Logger) -> None:
self.log = log
self.ioc_files = []
self.ioc_collections = []
self.total_ioc_count = 0
def _load_downloaded_indicators(self):
if not os.path.isdir(self.data_dir):
def _load_downloaded_indicators(self) -> None:
if not os.path.isdir(MVT_INDICATORS_FOLDER):
return
for f in os.listdir(self.data_dir):
if f.lower().endswith(".stix2"):
self.parse_stix2(os.path.join(self.data_dir, f))
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):
def _check_stix2_env_variable(self) -> None:
"""
Checks if a variable MVT_STIX2 contains path to STIX Files.
Checks if a variable MVT_STIX2 contains path to a STIX files.
"""
if "MVT_STIX2" not in os.environ:
return
@@ -44,14 +45,17 @@ class Indicators:
if os.path.isfile(path):
self.parse_stix2(path)
else:
self.log.info("Invalid STIX2 path %s in MVT_STIX2 environment variable", path)
self.log.error("Path specified with env MVT_STIX2 is not a valid file: %s",
path)
def _generate_indicators_file(self):
def _new_collection(self, cid: str = "", name: str = "", description: str = "",
file_name: str = "", file_path: str = "") -> dict:
return {
"name": "",
"description": "",
"file_name": "",
"file_path": "",
"id": cid,
"name": name,
"description": description,
"stix2_file_name": file_name,
"stix2_file_path": file_path,
"domains": [],
"processes": [],
"emails": [],
@@ -63,13 +67,14 @@ class Indicators:
"count": 0,
}
def _add_indicator(self, ioc, ioc_file, iocs_list):
if ioc not in iocs_list:
iocs_list.append(ioc)
ioc_file["count"] += 1
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):
def parse_stix2(self, file_path: str) -> None:
"""Extract indicators from a STIX2 file.
:param file_path: Path to the STIX2 file to parse
@@ -78,71 +83,104 @@ class Indicators:
"""
self.log.info("Parsing STIX2 indicators file at path %s", file_path)
ioc_file = self._generate_indicators_file()
ioc_file["file_path"] = file_path
ioc_file["file_name"] = os.path.basename(file_path)
with open(file_path, "r") as handle:
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 malformed or in the wrong format.")
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":
ioc_file["name"] = entry.get("name", "") or ioc_file["file_name"]
ioc_file["description"] = entry.get("description", "") or ioc_file["file_name"]
continue
malware[entry["id"]] = {
"name": entry["name"],
"description": entry.get("description", ""),
}
elif entry_type == "indicator":
indicators.append(entry)
elif entry_type == "relationship":
relationships.append(entry)
if entry_type != "indicator":
continue
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)
key, value = entry.get("pattern", "").strip("[]").split("=")
value = value.strip("'")
# We loop through all indicators.
for indicator in indicators:
malware_id = None
if key == "domain-name:value":
# We force domain names to lower case.
self._add_indicator(ioc=value.lower(),
ioc_file=ioc_file,
iocs_list=ioc_file["domains"])
elif key == "process:name":
self._add_indicator(ioc=value,
ioc_file=ioc_file,
iocs_list=ioc_file["processes"])
elif key == "email-addr:value":
# We force email addresses to lower case.
self._add_indicator(ioc=value.lower(),
ioc_file=ioc_file,
iocs_list=ioc_file["emails"])
elif key == "file:name":
self._add_indicator(ioc=value,
ioc_file=ioc_file,
iocs_list=ioc_file["file_names"])
elif key == "file:path":
self._add_indicator(ioc=value,
ioc_file=ioc_file,
iocs_list=ioc_file["file_paths"])
elif key == "file:hashes.sha256":
self._add_indicator(ioc=value,
ioc_file=ioc_file,
iocs_list=ioc_file["files_sha256"])
elif key == "app:id":
self._add_indicator(ioc=value,
ioc_file=ioc_file,
iocs_list=ioc_file["app_ids"])
elif key == "configuration-profile:id":
self._add_indicator(ioc=value,
ioc_file=ioc_file,
iocs_list=ioc_file["ios_profile_ids"])
# 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
self.log.info("Loaded %d indicators from \"%s\" indicators file",
ioc_file["count"], ioc_file["name"])
# Look for a malware definition with the correct identifier.
if relationship["target_ref"] in malware.keys():
malware_id = relationship["target_ref"]
break
self.ioc_files.append(ioc_file)
# 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
def load_indicators_files(self, files, load_default=True):
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.
"""
@@ -150,7 +188,8 @@ class Indicators:
if os.path.isfile(file_path):
self.parse_stix2(file_path)
else:
self.log.warning("This indicators file %s does not exist", file_path)
self.log.warning("No indicators file exists at path %s",
file_path)
# Load downloaded indicators and any indicators from env variable.
if load_default:
@@ -159,16 +198,17 @@ class 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):
for ioc_file in self.ioc_files:
for ioc in ioc_file.get(ioc_type, []):
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_file["name"]
"name": ioc_collection["name"],
"stix2_file_name": ioc_collection["stix2_file_name"],
}
def check_domain(self, url):
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
@@ -240,7 +280,7 @@ class Indicators:
return ioc
def check_domains(self, urls):
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
@@ -256,7 +296,7 @@ class Indicators:
if check:
return check
def check_process(self, process):
def check_process(self, process: str) -> dict:
"""Check the provided process name against the list of process
indicators.
@@ -281,7 +321,7 @@ class Indicators:
process, ioc["name"])
return ioc
def check_processes(self, processes):
def check_processes(self, processes: list) -> dict:
"""Check the provided list of processes against the list of
process indicators.
@@ -298,7 +338,7 @@ class Indicators:
if check:
return check
def check_email(self, email):
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
@@ -315,7 +355,7 @@ class Indicators:
email, ioc["name"])
return ioc
def check_file_name(self, file_name):
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
@@ -333,7 +373,7 @@ class Indicators:
file_name, ioc["name"])
return ioc
def check_file_path(self, file_path):
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
@@ -356,7 +396,7 @@ class Indicators:
file_path, ioc["name"])
return ioc
def check_profile(self, profile_uuid):
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
@@ -373,7 +413,7 @@ class Indicators:
profile_uuid, ioc["name"])
return ioc
def check_file_hash(self, file_hash):
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
@@ -390,7 +430,7 @@ class Indicators:
file_hash, ioc["name"])
return ioc
def check_app_id(self, app_id):
def check_app_id(self, app_id: str) -> dict:
"""Check the provided app identifier (typically an Android package name)
against the list of indicators.
@@ -407,36 +447,3 @@ class Indicators:
self.log.warning("Found a known suspicious app with ID \"%s\" matching indicators from \"%s\"",
app_id, ioc["name"])
return ioc
def download_indicators_files(log):
"""
Download indicators from repo into MVT app data directory.
"""
data_dir = user_data_dir("mvt")
if not os.path.isdir(data_dir):
os.makedirs(data_dir, exist_ok=True)
# Download latest list of indicators from the MVT repo.
res = requests.get("https://github.com/mvt-project/mvt/raw/main/public_indicators.json")
if res.status_code != 200:
log.warning("Unable to find retrieve list of indicators from the MVT repository.")
return
for ioc_entry in res.json():
ioc_url = ioc_entry["stix2_url"]
log.info("Downloading indicator file '%s' from '%s'", ioc_entry["name"], ioc_url)
res = requests.get(ioc_url)
if res.status_code != 200:
log.warning("Could not find indicator file '%s'", ioc_url)
continue
clean_file_name = ioc_url.lstrip("https://").replace("/", "_")
ioc_path = os.path.join(data_dir, clean_file_name)
# Write file to disk. This will overwrite any older version of the STIX2 file.
with io.open(ioc_path, "w") as f:
f.write(res.text)
log.info("Saved indicator file to '%s'", os.path.basename(ioc_path))

View File

@@ -1,25 +1,59 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 rich import print
from .version import MVT_VERSION, check_for_updates
from .updates import IndicatorsUpdates, MVTUpdates
from .version import MVT_VERSION
def logo():
print("\n")
print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
print("\t\thttps://mvt.re")
print(f"\t\tVersion: {MVT_VERSION}")
def check_updates() -> None:
# First we check for MVT version udpates.
mvt_updates = MVTUpdates()
try:
latest_version = check_for_updates()
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,12 +1,13 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 io
import logging
import os
import re
from typing import Callable
import simplejson as json
@@ -29,16 +30,17 @@ class MVTModule(object):
enabled = True
slug = None
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=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 base_folder: Path to the base folder (backup or filesystem dump)
:param target_path: Path to the target folder (backup or filesystem dump)
:type file_path: str
:param output_folder: Folder where results will be stored
:type output_folder: str
:param results_path: Folder where results will be stored
:type results_path: str
:param fast_mode: Flag to enable or disable slow modules
:type fast_mode: bool
:param log: Handle to logger
@@ -46,8 +48,8 @@ class MVTModule(object):
:type results: list
"""
self.file_path = file_path
self.base_folder = base_folder
self.output_folder = output_folder
self.target_path = target_path
self.results_path = results_path
self.fast_mode = fast_mode
self.log = log
self.indicators = None
@@ -57,15 +59,15 @@ class MVTModule(object):
self.timeline_detected = []
@classmethod
def from_json(cls, json_path, log=None):
with open(json_path, "r") as handle:
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)
return cls(results=results, log=log)
def get_slug(self):
def get_slug(self) -> str:
"""Use the module's class name to retrieve a slug"""
if self.slug:
return self.slug
@@ -73,7 +75,7 @@ class MVTModule(object):
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):
def check_indicators(self) -> None:
"""Check the results of this module against a provided list of
indicators.
@@ -81,17 +83,17 @@ class MVTModule(object):
"""
raise NotImplementedError
def save_to_json(self):
def save_to_json(self) -> None:
"""Save the collected results to a json file."""
if not self.output_folder:
if not self.results_path:
return
name = self.get_slug()
if self.results:
results_file_name = f"{name}.json"
results_json_path = os.path.join(self.output_folder, results_file_name)
with io.open(results_json_path, "w", encoding="utf-8") as handle:
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, default=str)
except Exception as e:
@@ -100,15 +102,15 @@ class MVTModule(object):
if self.detected:
detected_file_name = f"{name}_detected.json"
detected_json_path = os.path.join(self.output_folder, detected_file_name)
with io.open(detected_json_path, "w", encoding="utf-8") as handle:
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, default=str)
def serialize(self, record):
def serialize(self, record: dict) -> None:
raise NotImplementedError
@staticmethod
def _deduplicate_timeline(timeline):
def _deduplicate_timeline(timeline: list) -> list:
"""Serialize entry as JSON to deduplicate repeated entries
:param timeline: List of entries from timeline to deduplicate
@@ -119,7 +121,7 @@ class MVTModule(object):
timeline_set.add(json.dumps(record, sort_keys=True))
return [json.loads(record) for record in timeline_set]
def to_timeline(self):
def to_timeline(self) -> None:
"""Convert results into a timeline."""
for result in self.results:
record = self.serialize(result)
@@ -141,12 +143,12 @@ class MVTModule(object):
self.timeline = self._deduplicate_timeline(self.timeline)
self.timeline_detected = self._deduplicate_timeline(self.timeline_detected)
def run(self):
def run(self) -> None:
"""Run the main module procedure."""
raise NotImplementedError
def run_module(module):
def run_module(module: Callable) -> None:
module.log.info("Running module %s...", module.__class__.__name__)
try:
@@ -185,14 +187,14 @@ def run_module(module):
module.save_to_json()
def save_timeline(timeline, timeline_path):
def save_timeline(timeline: list, timeline_path: str) -> None:
"""Save the timeline in a csv file.
:param timeline: List of records to order and store
:param timeline_path: Path to the csv file to store the timeline to
"""
with io.open(timeline_path, "a+", encoding="utf-8") as handle:
with open(timeline_path, "a+", encoding="utf-8") as handle:
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 ""):

View File

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

211
mvt/common/updates.py Normal file
View File

@@ -0,0 +1,211 @@
# 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 datetime import datetime
import requests
import yaml
from packaging import version
from .indicators import MVT_DATA_FOLDER, MVT_INDICATORS_FOLDER
from .version import MVT_VERSION
log = logging.getLogger(__name__)
# In hours.
INDICATORS_CHECK_FREQUENCY = 12
class MVTUpdates:
def check(self) -> str:
res = requests.get("https://pypi.org/pypi/mvt/json")
data = res.json()
latest_version = data.get("info", {}).get("version", "")
if version.parse(latest_version) > version.parse(MVT_VERSION):
return latest_version
return ""
class IndicatorsUpdates:
def __init__(self) -> None:
self.github_raw_url = "https://raw.githubusercontent.com/{}/{}/{}/{}"
self.index_owner = "mvt-project"
self.index_repo = "mvt-indicators"
self.index_branch = "main"
self.index_path = "indicators.yaml"
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") as handle:
data = handle.read().strip()
if data:
return int(data)
return 0
def set_latest_check(self) -> None:
timestamp = int(datetime.utcnow().timestamp())
with open(self.latest_check_path, "w") as handle:
handle.write(str(timestamp))
def get_latest_update(self) -> int:
if not os.path.exists(self.latest_update_path):
return 0
with open(self.latest_update_path, "r") as handle:
data = handle.read().strip()
if data:
return int(data)
return 0
def set_latest_update(self) -> None:
timestamp = int(datetime.utcnow().timestamp())
with open(self.latest_update_path, "w") as handle:
handle.write(str(timestamp))
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)
return None
return yaml.safe_load(res.content)
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)
return None
clean_file_name = ioc_url.lstrip("https://").replace("/", "_")
ioc_path = os.path.join(MVT_INDICATORS_FOLDER, clean_file_name)
with open(ioc_path, "w", encoding="utf-8") as handle:
handle.write(res.text)
return ioc_path
def update(self) -> None:
self.set_latest_check()
if not os.path.exists(MVT_INDICATORS_FOLDER):
os.makedirs(MVT_INDICATORS_FOLDER)
index = self.get_remote_index()
for ioc in index.get("indicators", []):
ioc_type = ioc.get("type", "")
if ioc_type == "github":
github = ioc.get("github", {})
owner = github.get("owner", "")
repo = github.get("repo", "")
branch = github.get("branch", "main")
path = github.get("path", "")
ioc_url = self.github_raw_url.format(owner, repo, branch, path)
else:
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"))
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)
self.set_latest_update()
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 False
details = res.json()
if len(details) == 0:
return False
latest_commit = details[0]
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 False
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) -> (bool, int):
now = datetime.utcnow()
latest_check_ts = self.get_latest_check()
latest_check_dt = datetime.fromtimestamp(latest_check_ts)
diff = now - latest_check_dt
diff_hours = divmod(diff.total_seconds(), 3600)[0]
if diff_hours >= INDICATORS_CHECK_FREQUENCY:
return True, 0
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)
if latest_update < latest_commit_ts:
return True
index = self.get_remote_index()
for ioc in index.get("indicators", []):
if ioc.get("type", "") != "github":
continue
github = ioc.get("github", {})
owner = github.get("owner", "")
repo = github.get("repo", "")
branch = github.get("branch", "main")
path = github.get("path", "")
file_latest_commit_ts = self._get_remote_file_latest_commit(owner,
repo,
branch,
path)
if latest_update < file_latest_commit_ts:
return True
return False

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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/
@@ -253,7 +253,7 @@ SHORTENER_DOMAINS = [
class URL:
def __init__(self, url):
def __init__(self, url: str) -> None:
if type(url) == bytes:
url = url.decode()
@@ -262,7 +262,7 @@ class URL:
self.top_level = self.get_top_level()
self.is_shortened = False
def get_domain(self):
def get_domain(self) -> None:
"""Get the domain from a URL.
:param url: URL to parse
@@ -273,11 +273,13 @@ class URL:
"""
# TODO: Properly handle exception.
try:
return get_tld(self.url, as_object=True, fix_protocol=True).parsed_url.netloc.lower().lstrip("www.")
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):
def get_top_level(self) -> None:
"""Get only the top-level domain from a URL.
:param url: URL to parse
@@ -306,7 +308,7 @@ class URL:
return self.is_shortened
def unshorten(self):
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"):

View File

@@ -1,14 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 hashlib
import os
import re
def convert_mactime_to_unix(timestamp, from_2001=True):
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).
@@ -37,7 +38,7 @@ def convert_mactime_to_unix(timestamp, from_2001=True):
return None
def convert_chrometime_to_unix(timestamp):
def convert_chrometime_to_unix(timestamp: int) -> int:
"""Converts Chrome timestamp to a Unix timestamp.
:param timestamp: Chrome timestamp as int.
@@ -50,7 +51,7 @@ def convert_chrometime_to_unix(timestamp):
return epoch_start + delta
def convert_timestamp_to_iso(timestamp):
def convert_timestamp_to_iso(timestamp: str) -> str:
"""Converts Unix timestamp to ISO string.
:param timestamp: Unix timestamp.
@@ -65,7 +66,7 @@ def convert_timestamp_to_iso(timestamp):
return None
def check_for_links(text):
def check_for_links(text: str) -> list:
"""Checks if a given text contains HTTP links.
:param text: Any provided text.
@@ -73,10 +74,10 @@ def check_for_links(text):
:returns: Search results.
"""
return re.findall("(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
return re.findall(r"(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
def get_sha256_from_file_path(file_path):
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
@@ -93,7 +94,7 @@ def get_sha256_from_file_path(file_path):
# Note: taken from here:
# https://stackoverflow.com/questions/57014259/json-dumps-on-dictionary-with-bytes-for-keys
def keys_bytes_to_string(obj):
def keys_bytes_to_string(obj) -> str:
"""Convert object keys from bytes to string.
:param obj: Object to convert from bytes to string.
@@ -119,3 +120,14 @@ def keys_bytes_to_string(obj):
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,20 +1,6 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 requests
from packaging import version
MVT_VERSION = "1.4.8"
def check_for_updates():
res = requests.get("https://pypi.org/pypi/mvt/json")
data = res.json()
latest_version = data.get("info", {}).get("version", "")
if version.parse(latest_version) > version.parse(MVT_VERSION):
return latest_version
return None
MVT_VERSION = "1.6"

45
mvt/common/virustotal.py Normal file
View File

@@ -0,0 +1,45 @@
# 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 requests
log = logging.getLogger(__name__)
MVT_VT_API_KEY = "MVT_VT_API_KEY"
class VTNoKey(Exception):
pass
class VTQuotaExceeded(Exception):
pass
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>`")
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)
if res.status_code == 200:
report = res.json()
return report["data"]
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("Unexpected response from VirusTotal: %s", res.status_code)
return None

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 The MVT Project 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/
@@ -8,17 +8,22 @@ import os
import click
from rich.logging import RichHandler
from rich.prompt import Prompt
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)
from mvt.common.indicators import Indicators, download_indicators_files
HELP_MSG_OUTPUT, HELP_MSG_SERIAL)
from mvt.common.logo import logo
from mvt.common.module import run_module, save_timeline
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
@@ -30,7 +35,7 @@ logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
log = logging.getLogger(__name__)
# Set this environment variable to a password if needed.
PASSWD_ENV = "MVT_IOS_BACKUP_PASSWORD"
MVT_IOS_BACKUP_PASSWORD = "MVT_IOS_BACKUP_PASSWORD"
#==============================================================================
@@ -56,7 +61,7 @@ def version():
@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 {PASSWD_ENV} environment variable)",
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),
@@ -68,22 +73,22 @@ def decrypt_backup(ctx, destination, password, key_file, backup_path):
backup = DecryptBackup(backup_path, destination)
if key_file:
if PASSWD_ENV in os.environ:
if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Ignoring environment variable, using --key-file '%s' instead",
PASSWD_ENV, key_file)
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 PASSWD_ENV in os.environ:
if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Ignoring %s environment variable, using --password argument instead",
PASSWD_ENV)
MVT_IOS_BACKUP_PASSWORD)
backup.decrypt_with_password(password)
elif PASSWD_ENV in os.environ:
log.info("Using password from %s environment variable", PASSWD_ENV)
backup.decrypt_with_password(os.environ[PASSWD_ENV])
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)
@@ -99,24 +104,24 @@ def decrypt_backup(ctx, destination, password, key_file, backup_path):
#==============================================================================
@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 {PASSWD_ENV} environment variable)")
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, backup_path, key_file):
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 PASSWD_ENV in os.environ:
if MVT_IOS_BACKUP_PASSWORD in os.environ:
log.info("Ignoring %s environment variable, using --password argument instead",
PASSWD_ENV)
elif PASSWD_ENV in os.environ:
log.info("Using password from %s environment variable", PASSWD_ENV)
password = os.environ[PASSWD_ENV]
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)
@@ -139,52 +144,21 @@ def extract_key(password, backup_path, key_file):
@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, backup_path, list_modules, module):
if list_modules:
log.info("Following is the list of available check-backup modules:")
for backup_module in BACKUP_MODULES + MIXED_MODULES:
log.info(" - %s", backup_module.__name__)
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)
if output and 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)
cmd.run()
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
timeline = []
timeline_detected = []
for backup_module in BACKUP_MODULES + MIXED_MODULES:
if module and backup_module.__name__ != module:
continue
m = backup_module(base_folder=backup_path, output_folder=output, fast_mode=fast,
log=logging.getLogger(backup_module.__module__))
m.is_backup = True
if indicators.total_ioc_count > 0:
m.indicators = indicators
m.indicators.log = m.log
run_module(m)
timeline.extend(m.timeline)
timeline_detected.extend(m.timeline_detected)
if output:
if len(timeline) > 0:
save_timeline(timeline, os.path.join(output, "timeline.csv"))
if len(timeline_detected) > 0:
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
if len(timeline_detected) > 0:
if len(cmd.timeline_detected) > 0:
log.warning("The analysis of the backup produced %d detections!",
len(timeline_detected))
len(cmd.timeline_detected))
#==============================================================================
@@ -199,53 +173,99 @@ def check_backup(ctx, iocs, output, fast, backup_path, list_modules, module):
@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, dump_path, list_modules, module):
if list_modules:
log.info("Following is the list of available check-fs modules:")
for fs_module in FS_MODULES + MIXED_MODULES:
log.info(" - %s", fs_module.__name__)
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 filesystem dump located at: %s", dump_path)
log.info("Checking iOS filesystem located at: %s", dump_path)
if output and not os.path.exists(output):
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:
os.makedirs(output)
except Exception as e:
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
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)
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
timeline = []
timeline_detected = []
for fs_module in FS_MODULES + MIXED_MODULES:
if module and fs_module.__name__ != module:
continue
m = fs_module(base_folder=dump_path, output_folder=output, fast_mode=fast,
log=logging.getLogger(fs_module.__module__))
m.is_fs_dump = True
if indicators.total_ioc_count > 0:
m.indicators = indicators
m.indicators.log = m.log
run_module(m)
timeline.extend(m.timeline)
timeline_detected.extend(m.timeline_detected)
if output:
if len(timeline) > 0:
save_timeline(timeline, os.path.join(output, "timeline.csv"))
if len(timeline_detected) > 0:
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
if len(timeline_detected) > 0:
log.warning("The analysis of the filesystem produced %d detections!",
len(timeline_detected))
#==============================================================================
# Command: check-iocs
@@ -258,54 +278,14 @@ def check_fs(ctx, iocs, output, fast, dump_path, list_modules, module):
@click.argument("FOLDER", type=click.Path(exists=True))
@click.pass_context
def check_iocs(ctx, iocs, list_modules, module, folder):
all_modules = []
for entry in BACKUP_MODULES + FS_MODULES + MIXED_MODULES:
if entry not in all_modules:
all_modules.append(entry)
cmd = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module)
cmd.modules = BACKUP_MODULES + FS_MODULES + MIXED_MODULES
if list_modules:
log.info("Following is the list of available check-iocs modules:")
for iocs_module in all_modules:
log.info(" - %s", iocs_module.__name__)
cmd.list_modules()
return
log.info("Checking stored results against provided indicators...")
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
total_detections = 0
for file_name in os.listdir(folder):
name_only, ext = os.path.splitext(file_name)
file_path = os.path.join(folder, file_name)
for iocs_module in all_modules:
if module and iocs_module.__name__ != module:
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 indicators.total_ioc_count > 0:
m.indicators = indicators
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)
cmd.run()
#==============================================================================
@@ -313,4 +293,5 @@ def check_iocs(ctx, iocs, list_modules, module, folder):
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
def download_iocs():
download_indicators_files(log)
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

29
mvt/ios/cmd_check_fs.py Normal file
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.fs import FS_MODULES
from .modules.mixed import MIXED_MODULES
log = logging.getLogger(__name__)
class CmdIOSCheckFS(Command):
name = "check-fs"
modules = FS_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_fs_dump = True

47
mvt/ios/cmd_check_usb.py Normal file
View File

@@ -0,0 +1,47 @@
# 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 sys
from pymobiledevice3.exceptions import (ConnectionFailedError,
FatalPairingError, NotTrustedError)
from pymobiledevice3.lockdown import LockdownClient
from mvt.common.command import Command
from .modules.usb import USB_MODULES
log = logging.getLogger(__name__)
class CmdIOSCheckUSB(Command):
name = "check-usb"
modules = USB_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.lockdown = None
def init(self):
try:
if self.serial:
self.lockdown = LockdownClient(udid=self.serial)
else:
self.lockdown = LockdownClient()
except NotTrustedError:
log.error("Trust this computer from the prompt appearing on the iOS device and try again")
sys.exit(-1)
except (ConnectionRefusedError, ConnectionFailedError, FatalPairingError):
log.error("Unable to connect to the device over USB: try to unplug, plug the device and start again")
sys.exit(-1)
def module_init(self, module):
module.lockdown = self.lockdown

View File

@@ -1,12 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 binascii
import glob
import logging
import multiprocessing
import os
import os.path
import shutil
import sqlite3
@@ -22,12 +24,12 @@ class DecryptBackup:
"""
def __init__(self, backup_path, dest_path=None):
def __init__(self, backup_path: str, dest_path: str = None) -> None:
"""Decrypts an encrypted iOS backup.
:param backup_path: Path to the encrypted backup folder
:param dest_path: Path to the folder where to store the decrypted backup
"""
self.backup_path = backup_path
self.backup_path = os.path.abspath(backup_path)
self.dest_path = dest_path
self._backup = None
self._decryption_key = None
@@ -36,7 +38,7 @@ class DecryptBackup:
return self._backup is not None
@staticmethod
def is_encrypted(backup_path) -> bool:
def is_encrypted(backup_path: str) -> bool:
"""Query Manifest.db file to see if it's encrypted or not.
:param backup_path: Path to the backup to decrypt
@@ -52,7 +54,14 @@ class DecryptBackup:
log.critical("The backup does not seem encrypted!")
return False
def process_backup(self):
def _process_file(self, relative_path: str, domain: str, item,
file_id: str, item_folder: str) -> None:
self._backup.getFileDecryptedCopy(manifestEntry=item,
targetName=file_id,
targetFolder=item_folder)
log.info("Decrypted file %s [%s] to %s/%s", relative_path, domain, item_folder, file_id)
def process_backup(self) -> None:
if not os.path.exists(self.dest_path):
os.makedirs(self.dest_path)
@@ -62,6 +71,8 @@ class DecryptBackup:
# We store it to the destination folder.
shutil.copy(self._backup.manifestDB, manifest_path)
pool = multiprocessing.Pool(multiprocessing.cpu_count())
for item in self._backup.getBackupFilesList():
try:
file_id = item["backupFile"]
@@ -84,13 +95,16 @@ class DecryptBackup:
# Add manifest plist to both keys to handle this.
item["manifest"] = item["file"]
self._backup.getFileDecryptedCopy(manifestEntry=item,
targetName=file_id,
targetFolder=item_folder)
log.info("Decrypted file %s [%s] to %s/%s", relative_path, domain, item_folder, file_id)
pool.apply_async(self._process_file, args=(relative_path,
domain, item,
file_id,
item_folder))
except Exception as e:
log.error("Failed to decrypt file %s: %s", relative_path, e)
pool.close()
pool.join()
# Copying over the root plist files as well.
for file_name in os.listdir(self.backup_path):
if file_name.endswith(".plist"):
@@ -98,7 +112,7 @@ class DecryptBackup:
shutil.copy(os.path.join(self.backup_path, file_name),
self.dest_path)
def decrypt_with_password(self, password):
def decrypt_with_password(self, password: str) -> None:
"""Decrypts an encrypted iOS backup.
:param password: Password to use to decrypt the original backup
@@ -136,7 +150,7 @@ class DecryptBackup:
log.exception(e)
log.critical("Failed to decrypt backup. Did you provide the correct password? Did you point to the right backup path?")
def decrypt_with_key_file(self, key_file):
def decrypt_with_key_file(self, key_file: str) -> None:
"""Decrypts an encrypted iOS backup using a key file.
:param key_file: File to read the key bytes to decrypt the backup
@@ -166,7 +180,7 @@ class DecryptBackup:
log.exception(e)
log.critical("Failed to decrypt backup. Did you provide the correct key file?")
def get_key(self):
def get_key(self) -> None:
"""Retrieve and prints the encryption key."""
if not self._backup:
return
@@ -175,7 +189,7 @@ class DecryptBackup:
log.info("Derived decryption key for backup at path %s is: \"%s\"",
self.backup_path, self._decryption_key)
def write_key(self, key_path):
def write_key(self, key_path: str) -> None:
"""Save extracted key to file.
:param key_path: Path to the file where to write the derived decryption key.
@@ -185,7 +199,7 @@ class DecryptBackup:
return
try:
with open(key_path, 'w') as handle:
with open(key_path, 'w', encoding="utf-8") as handle:
handle.write(self._decryption_key)
except Exception as e:
log.exception(e)

58
mvt/ios/lockdown.py Normal file
View File

@@ -0,0 +1,58 @@
# 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 platform
from mvt.common.utils import secure_delete
class Lockdown:
def __init__(self, uuids: list = []) -> None:
self.uuids = uuids
self.lockdown_folder = self._get_lockdown_folder()
@staticmethod
def _get_lockdown_folder():
system = platform.system()
if system == "Linux":
return "/var/lib/lockdown/"
elif system == "Darwin":
return "/var/db/lockdown/"
elif system == "Windows":
return os.path.join(os.environ.get("ALLUSERSPROFILE", ""),
"Apple", "Lockdown")
@staticmethod
def _get_pymobiledevice_folder():
return os.path.expanduser("~/.pymobiledevice3")
def delete_cert(self, cert_file) -> None:
if not self.lockdown_folder:
return
cert_path = os.path.join(self.lockdown_folder, cert_file)
if not os.path.exists(cert_path):
return
secure_delete(cert_path)
def find_certs(self) -> list:
if not self.lockdown_folder or not os.path.exists(self.lockdown_folder):
return []
lockdown_certs = []
for file_name in os.listdir(self.lockdown_folder):
if not file_name.endswith(".plist"):
continue
if file_name == "SystemConfiguration.plist":
continue
file_path = os.path.join(self.lockdown_folder, file_name)
lockdown_certs.append(file_path)
return sorted(lockdown_certs)

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 The MVT Project 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,12 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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
import plistlib
from mvt.common.module import DatabaseNotFoundError
from mvt.ios.versions import latest_ios_version
from ..base import IOSExtraction
@@ -14,16 +16,17 @@ from ..base import IOSExtraction
class BackupInfo(IOSExtraction):
"""This module extracts information about the device and the backup."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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 run(self):
info_path = os.path.join(self.base_folder, "Info.plist")
def run(self) -> None:
info_path = os.path.join(self.target_path, "Info.plist")
if not os.path.exists(info_path):
raise DatabaseNotFoundError("No Info.plist at backup path, unable to extract device information")
@@ -41,3 +44,9 @@ class BackupInfo(IOSExtraction):
value = info.get(field, None)
self.log.info("%s: %s", field, value)
self.results[field] = value
if "Product Version" in info:
latest = latest_ios_version()
if info["Product Version"] != latest["version"]:
self.log.warning("This phone is running an outdated iOS version: %s (latest is %s)",
info["Product Version"], latest['version'])

View File

@@ -1,8 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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
import plistlib
from base64 import b64encode
@@ -17,13 +18,14 @@ CONF_PROFILES_DOMAIN = "SysSharedContainerDomain-systemgroup.com.apple.configura
class ConfigurationProfiles(IOSExtraction):
"""This module extracts the full plist data from configuration profiles."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def serialize(self, record: dict) -> None:
if not record["install_date"]:
return
@@ -36,7 +38,7 @@ class ConfigurationProfiles(IOSExtraction):
"data": f"{record['plist']['PayloadType']} installed: {record['plist']['PayloadUUID']} - {payload_name}: {payload_description}"
}
def check_indicators(self):
def check_indicators(self) -> None:
if not self.indicators:
return
@@ -58,7 +60,7 @@ class ConfigurationProfiles(IOSExtraction):
self.detected.append(result)
continue
def run(self):
def run(self) -> None:
for conf_file in self._get_backup_files_from_manifest(domain=CONF_PROFILES_DOMAIN):
conf_rel_path = conf_file["relative_path"]
# Filter out all configuration files that are not configuration profiles.
@@ -74,12 +76,14 @@ class ConfigurationProfiles(IOSExtraction):
conf_plist = plistlib.load(handle)
except Exception:
conf_plist = {}
if "SignerCerts" in conf_plist:
conf_plist["SignerCerts"] = [b64encode(x) for x in conf_plist["SignerCerts"]]
if "OTAProfileStub" in conf_plist:
if "SignerCerts" in conf_plist["OTAProfileStub"]:
conf_plist["OTAProfileStub"]["SignerCerts"] = [b64encode(x) for x in conf_plist["OTAProfileStub"]["SignerCerts"]]
if "PayloadContent" in conf_plist["OTAProfileStub"]:
if "EnrollmentIdentityPersistentID" in conf_plist["OTAProfileStub"]["PayloadContent"]:
conf_plist["OTAProfileStub"]["PayloadContent"]["EnrollmentIdentityPersistentID"] = b64encode(conf_plist["OTAProfileStub"]["PayloadContent"]["EnrollmentIdentityPersistentID"])
if "PushTokenDataSentToServerKey" in conf_plist:
conf_plist["PushTokenDataSentToServerKey"] = b64encode(conf_plist["PushTokenDataSentToServerKey"])
if "LastPushTokenHash" in conf_plist:
@@ -88,6 +92,8 @@ class ConfigurationProfiles(IOSExtraction):
for x in range(len(conf_plist["PayloadContent"])):
if "PERSISTENT_REF" in conf_plist["PayloadContent"][x]:
conf_plist["PayloadContent"][x]["PERSISTENT_REF"] = b64encode(conf_plist["PayloadContent"][x]["PERSISTENT_REF"])
if "IdentityPersistentRef" in conf_plist["PayloadContent"][x]:
conf_plist["PayloadContent"][x]["IdentityPersistentRef"] = b64encode(conf_plist["PayloadContent"][x]["IdentityPersistentRef"])
self.results.append({
"file_id": conf_file["file_id"],

View File

@@ -1,10 +1,11 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 logging
import os
import plistlib
import sqlite3
@@ -18,10 +19,11 @@ from ..base import IOSExtraction
class Manifest(IOSExtraction):
"""This module extracts information from a backup Manifest.db file."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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 _get_key(self, dictionary, key):
@@ -47,7 +49,7 @@ class Manifest(IOSExtraction):
timestamp = datetime.datetime.utcfromtimestamp(timestamp_or_unix_time_int)
return convert_timestamp_to_iso(timestamp)
def serialize(self, record):
def serialize(self, record: dict) -> None:
records = []
if "modified" not in record or "status_changed" not in record:
return
@@ -67,7 +69,7 @@ class Manifest(IOSExtraction):
return records
def check_indicators(self):
def check_indicators(self) -> None:
if not self.indicators:
return
@@ -92,8 +94,8 @@ class Manifest(IOSExtraction):
ioc["value"], rel_path)
self.detected.append(result)
def run(self):
manifest_db_path = os.path.join(self.base_folder, "Manifest.db")
def run(self) -> None:
manifest_db_path = os.path.join(self.target_path, "Manifest.db")
if not os.path.isfile(manifest_db_path):
raise DatabaseNotFoundError("unable to find backup's Manifest.db")

View File

@@ -1,8 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 plistlib
from mvt.common.utils import convert_timestamp_to_iso
@@ -18,14 +19,14 @@ class ProfileEvents(IOSExtraction):
"""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def serialize(self, record: dict) -> None:
return {
"timestamp": record.get("timestamp"),
"module": self.__class__.__name__,
@@ -33,7 +34,7 @@ class ProfileEvents(IOSExtraction):
"data": f"Process {record.get('process')} started operation {record.get('operation')} of profile {record.get('profile_id')}"
}
def run(self):
def run(self) -> None:
for events_file in self._get_backup_files_from_manifest(relative_path=CONF_PROFILES_EVENTS_RELPATH):
events_file_path = self._get_backup_file_from_id(events_file["file_id"])
if not events_file_path:

View File

@@ -1,9 +1,10 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 glob
import logging
import os
import shutil
import sqlite3
@@ -16,10 +17,11 @@ from mvt.common.module import (DatabaseCorruptedError, DatabaseNotFoundError,
class IOSExtraction(MVTModule):
"""This class provides a base for all iOS filesystem/backup extraction modules."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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.is_backup = False
@@ -73,7 +75,7 @@ class IOSExtraction(MVTModule):
:param domain: Domain to use as filter from Manifest.db. (Default value = None)
"""
manifest_db_path = os.path.join(self.base_folder, "Manifest.db")
manifest_db_path = os.path.join(self.target_path, "Manifest.db")
if not os.path.exists(manifest_db_path):
raise DatabaseNotFoundError("unable to find backup's Manifest.db")
@@ -101,7 +103,7 @@ class IOSExtraction(MVTModule):
}
def _get_backup_file_from_id(self, file_id):
file_path = os.path.join(self.base_folder, file_id[0:2], file_id)
file_path = os.path.join(self.target_path, file_id[0:2], file_id)
if os.path.exists(file_path):
return file_path
@@ -109,7 +111,7 @@ class IOSExtraction(MVTModule):
def _get_fs_files_from_patterns(self, root_paths):
for root_path in root_paths:
for found_path in glob.glob(os.path.join(self.base_folder, root_path)):
for found_path in glob.glob(os.path.join(self.target_path, root_path)):
if not os.path.exists(found_path):
continue

View File

@@ -1,9 +1,10 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 .analytics import Analytics
from .analytics_ios_versions import AnalyticsIOSVersions
from .cache_files import CacheFiles
from .filesystem import Filesystem
from .net_netusage import Netusage
@@ -14,6 +15,6 @@ from .webkit_indexeddb import WebkitIndexedDB
from .webkit_localstorage import WebkitLocalStorage
from .webkit_safariviewservice import WebkitSafariViewService
FS_MODULES = [CacheFiles, Filesystem, Netusage, Analytics, SafariFavicon, ShutdownLog,
IOSVersionHistory, WebkitIndexedDB, WebkitLocalStorage,
WebkitSafariViewService]
FS_MODULES = [CacheFiles, Filesystem, Netusage, Analytics, AnalyticsIOSVersions,
SafariFavicon, ShutdownLog, IOSVersionHistory, WebkitIndexedDB,
WebkitLocalStorage, WebkitSafariViewService]

View File

@@ -1,8 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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 plistlib
import sqlite3
@@ -18,21 +19,22 @@ ANALYTICS_DB_PATH = [
class Analytics(IOSExtraction):
"""This module extracts information from the private/var/Keychains/Analytics/*.db files."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def serialize(self, record: dict) -> None:
return {
"timestamp": record["timestamp"],
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": record["artifact"],
"data": f"{record}",
}
def check_indicators(self):
def check_indicators(self) -> None:
if not self.indicators:
return
@@ -94,31 +96,35 @@ class Analytics(IOSExtraction):
for row in cur:
if row[0] and row[1]:
timestamp = convert_timestamp_to_iso(convert_mactime_to_unix(row[0], False))
isodate = convert_timestamp_to_iso(convert_mactime_to_unix(row[0], False))
data = plistlib.loads(row[1])
data["timestamp"] = timestamp
data["isodate"] = isodate
elif row[0]:
timestamp = convert_timestamp_to_iso(convert_mactime_to_unix(row[0], False))
isodate = convert_timestamp_to_iso(convert_mactime_to_unix(row[0], False))
data = {}
data["timestamp"] = timestamp
data["isodate"] = isodate
elif row[1]:
timestamp = ""
isodate = ""
data = plistlib.loads(row[1])
data["timestamp"] = timestamp
data["isodate"] = isodate
data["artifact"] = artifact
self.results.append(data)
self.results = sorted(self.results, key=lambda entry: entry["timestamp"])
cur.close()
conn.close()
self.log.info("Extracted information on %d analytics data from %s", len(self.results), artifact)
def run(self):
def process_analytics_dbs(self):
for file_path in self._get_fs_files_from_patterns(ANALYTICS_DB_PATH):
self.file_path = file_path
self.log.info("Found Analytics database file at path: %s", file_path)
self._extract_analytics_data()
def run(self) -> None:
self.process_analytics_dbs()
self.log.info("Extracted %d records from analytics databases",
len(self.results))
self.results = sorted(self.results, key=lambda entry: entry["isodate"])

View File

@@ -0,0 +1,73 @@
# 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
from mvt.ios.versions import find_version_by_build
from ..base import IOSExtraction
from .analytics import Analytics
class AnalyticsIOSVersions(IOSExtraction):
"""This module leverages the Analytics module in order to extract
a timeline of build numbers from the private/var/Keychains/Analytics/*.db
files."""
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": "analytics_ios_version",
"data": f"Seen iOS version {record['version']} ({record['build']})",
}
def run(self):
anl = Analytics(target_path=self.target_path, log=self.log)
anl.process_analytics_dbs()
dt_format = "%Y-%m-%d %H:%M:%S.%f"
builds = {}
for result in anl.results:
build = result.get("build")
if not build:
continue
ts = result.get("isodate", None)
if not ts:
continue
if build not in builds.keys():
builds[build] = ts
continue
result_dt = datetime.strptime(ts, dt_format)
cur_dt = datetime.strptime(builds[build], dt_format)
if result_dt < cur_dt:
builds[build] = ts
for build, ts in builds.items():
version = find_version_by_build(build)
self.results.append({
"isodate": ts,
"build": build,
"version": version,
})
self.results = sorted(self.results, key=lambda entry: entry["isodate"])
for result in self.results:
self.log.info("iOS version %s (%s) first appeared on %s",
result["version"], result["build"], result["isodate"])

View File

@@ -1,8 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project 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
import sqlite3
@@ -11,13 +12,14 @@ from ..base import IOSExtraction
class CacheFiles(IOSExtraction):
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
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):
def serialize(self, record: dict) -> None:
records = []
for item in self.results[record]:
records.append({
@@ -29,7 +31,7 @@ class CacheFiles(IOSExtraction):
return records
def check_indicators(self):
def check_indicators(self) -> None:
if not self.indicators:
return
@@ -55,7 +57,7 @@ class CacheFiles(IOSExtraction):
except sqlite3.OperationalError:
return
key_name = os.path.relpath(file_path, self.base_folder)
key_name = os.path.relpath(file_path, self.target_path)
if key_name not in self.results:
self.results[key_name] = []
@@ -69,9 +71,9 @@ class CacheFiles(IOSExtraction):
"isodate": row[5],
})
def run(self):
def run(self) -> None:
self.results = {}
for root, dirs, files in os.walk(self.base_folder):
for root, dirs, files in os.walk(self.target_path):
for file_name in files:
if file_name != "Cache.db":
continue

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