Compare commits

...

632 Commits

Author SHA1 Message Date
Donncha Ó Cearbhaill
82f5e5c627 Merge branch 'main' into feature/android-sub-module-loading 2025-02-06 20:51:59 +01:00
Donncha Ó Cearbhaill
4e97e85350 Load Android device timezone info and add additional file modification logs (#567)
* Use local timestamp for Files module timeline.

Most other Android timestamps appear to be local time. The
results timeline is more useful if all the timestamps
are consistent. I would prefer to use UTC, but that would
mean converting all the other timestamps to UTC as well. We probably
do not have sufficient information to do that accurately,
especially if the device is moving between timezones..

* Add file timestamp modules to add logs into timeline

* Handle case were we cannot load device timezone

* Fix crash if prop file does not exist

* Move _get_file_modification_time to BugReportModule

* Add backport for timezone and fix Tombstone module to use local time.

* Fix import for backported Zoneinfo

* Fix ruff error
2025-02-06 20:51:15 +01:00
Donncha Ó Cearbhaill
e5865b166e Merge pull request #568 from mvt-project/feature/tombstone-parser
Add parser for Android tombstone files
2025-02-06 20:15:21 +01:00
Donncha Ó Cearbhaill
a2dabb4267 Fix generate-proto-parsers Makefile command 2025-02-06 20:11:54 +01:00
Donncha Ó Cearbhaill
b7595b62eb Add initial tombstone parser
This supports parsing tombstone files from Android bugreports. The parser
can load both the legacy text format and the new binary protobuf format.
2025-02-06 20:07:05 +01:00
Donncha Ó Cearbhaill
02c02ca15c Merge branch 'main' into feature/tombstone-parser 2025-02-03 18:44:00 +01:00
Donncha Ó Cearbhaill
6da33394fe Merge pull request #592 from mvt-project/feature/config-file
Reworking handling of config options
2025-01-30 13:32:53 +01:00
Donncha Ó Cearbhaill
086871e21d Merge branch 'main' into feature/config-file 2025-01-30 13:15:28 +01:00
Donncha Ó Cearbhaill
f32830c649 Merge pull request #603 from mvt-project/feature/add-suspicious-android-setting
Add additional Android security warnings
2025-01-30 13:12:14 +01:00
Donncha Ó Cearbhaill
edcad488ab Merge branch 'main' into feature/add-suspicious-android-setting 2025-01-30 13:10:00 +01:00
Donncha Ó Cearbhaill
43901c96a0 Add improved heuristic detections to AppOps module 2025-01-30 13:02:26 +01:00
Donncha Ó Cearbhaill
0962383b46 Alert on potentially suspicious permissions from ADB 2025-01-30 11:48:19 +01:00
Donncha Ó Cearbhaill
34cd08fd9a Add additional Android security setting to warn on 2025-01-30 11:35:18 +01:00
github-actions[bot]
579b53f7ec Add new iOS versions and build numbers (#602)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-01-28 01:27:17 +01:00
Rory Flynn
dbb80d6320 Mark release 2.6.0 (#601) 2025-01-27 15:41:41 +01:00
Donncha Ó Cearbhaill
0fbf24e82a Merge branch 'main' into feature/config-file 2025-01-14 14:33:40 +01:00
Rory Flynn
a2493baead Documentation tweaks (#599)
* Adds link in install instructions to the command completion docs added in #597
* Small visual tweaks
2025-01-14 13:12:10 +01:00
Nim
0dc6228a59 Add command completion docs (#410) (#597)
Co-authored-by: Rory Flynn <75283103+roaree@users.noreply.github.com>
2025-01-14 12:04:07 +01:00
Rory Flynn
6e230bdb6a Autofix for ruff (#598) 2025-01-14 12:02:10 +01:00
Tek
2aa76c8a1c Fixes a bug on recent phones not having WIFI column in net usage (#580)
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
Co-authored-by: Rory Flynn <75283103+roaree@users.noreply.github.com>
2025-01-07 12:48:35 +01:00
github-actions[bot]
7d6dc9e6dc Add new iOS versions and build numbers (#595)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-01-07 12:07:57 +01:00
Donncha Ó Cearbhaill
458195a0ab Fix optional typing syntax for Python 3.8 2024-12-25 00:28:02 +00:00
Donncha Ó Cearbhaill
52e854b8b7 Add missing import 2024-12-25 00:23:36 +00:00
Donncha Ó Cearbhaill
0f1eec3971 Add Pydantic dependencies 2024-12-25 00:21:42 +00:00
Donncha Ó Cearbhaill
f4425865c0 Add missed modules using updated settings module 2024-12-25 00:14:14 +00:00
Donncha Ó Cearbhaill
28c0c86c4e Update MVT code to use config file rather than raw env variables 2024-12-25 00:09:29 +00:00
Donncha Ó Cearbhaill
154e6dab15 Add config file parser for MVT 2024-12-24 23:30:18 +00:00
Donncha Ó Cearbhaill
0c73e3e8fa Merge pull request #587 from mvt-project/feature/uninstalled-apps
Add a module to parse uninstalled apps from dumpsys data
2024-12-16 00:03:23 +01:00
Donncha Ó Cearbhaill
9b5f2d89d5 Merge branch 'main' into feature/uninstalled-apps 2024-12-16 00:00:12 +01:00
Donncha Ó Cearbhaill
3da61c8da8 Fix ruff checks 2024-12-15 23:22:36 +01:00
Tek
5b2fe3baec Reorganize code in iOS app module (#586) 2024-12-14 10:04:47 +01:00
Donncha Ó Cearbhaill
a3a7789547 Merge pull request #584 from mvt-project/enhance-community-guidelines
Update MVT contributor guidelines
2024-12-13 23:01:58 +01:00
Donncha Ó Cearbhaill
d3fcc686ff Update contribution guidelines 2024-12-13 22:45:41 +01:00
github-actions[bot]
4bcc0e5f27 Add new iOS versions and build numbers (#583)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-12-12 14:43:59 +01:00
tes
9d81b5bfa8 Add a module to parse uninstalled apps from dumpsys data, for both bugreport and AndroidQF output, and match them against package name IoCs. 2024-12-11 16:47:19 -03:00
github-actions[bot]
22fce280af Add new iOS versions and build numbers (#572)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-11-20 11:02:09 +01:00
Donncha Ó Cearbhaill
4739d8853e Merge pull request #570 from mvt-project/fix/files-detection-bug
Fix error to due extra equal character in Files detection
2024-10-31 20:04:33 +01:00
Donncha Ó Cearbhaill
ace01ff7fb Merge branch 'main' into fix/files-detection-bug 2024-10-31 19:59:53 +01:00
Donncha Ó Cearbhaill
7e4f0aec4d Fix error to due extra equal character in Files detection 2024-10-31 19:59:29 +01:00
github-actions[bot]
57647583cc Add new iOS versions and build numbers (#569)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-10-29 04:17:03 +01:00
Donncha Ó Cearbhaill
8e895d3d07 Remove protobuf compiler dependency, only needed for dev 2024-10-28 13:10:37 +01:00
Donncha Ó Cearbhaill
2bb613fe09 Return after loading bugreport module 2024-10-28 11:19:45 +01:00
Donncha Ó Cearbhaill
355850bd5c WIP: Run bugreport modules against bugreport.zip in AndroidQF extraction 2024-10-28 11:12:20 +01:00
Donncha Ó Cearbhaill
bc09e2a394 Initial tests for tombstone parsing 2024-10-28 10:51:58 +01:00
Donncha Ó Cearbhaill
2d0de088dd Add generated protobuf parser 2024-10-28 10:38:19 +01:00
Donncha Ó Cearbhaill
8694e7a047 Add protobuf parser generation 2024-10-28 10:37:30 +01:00
Donncha Ó Cearbhaill
9b41ba99aa WIP: initial tombstone modules 2024-10-28 10:34:53 +01:00
Donncha Ó Cearbhaill
cd99b293ed Merge pull request #563 from mvt-project/feature/add-package-detections
Add additional detections for suspicious packages
2024-10-24 17:37:30 +02:00
Donncha Ó Cearbhaill
5fe8238ef0 Update tests to work with the new side-loading detections 2024-10-24 17:35:34 +02:00
Donncha Ó Cearbhaill
1d44ae3987 Add detections for side-loaded apps, and deduplicate results 2024-10-24 17:19:58 +02:00
Donncha Ó Cearbhaill
bb68e41c07 Add detection for disabled system packages 2024-10-24 16:48:03 +02:00
Donncha Ó Cearbhaill
787b0c1f48 Merge pull request #562 from mvt-project/fix-docker-and-docs
Improve Docker image building and add Docker info to docs
2024-10-23 15:25:52 +02:00
Donncha Ó Cearbhaill
83c1bbf714 Revert "Make multiplatform images"
This reverts commit 17b625f311.
2024-10-23 15:22:11 +02:00
Donncha Ó Cearbhaill
17b625f311 Make multiplatform images 2024-10-23 15:16:28 +02:00
Donncha Ó Cearbhaill
7772d2de72 Add build dependencies for pyahocorasick 2024-10-23 15:10:11 +02:00
Donncha Ó Cearbhaill
37705d11fa Add checksum for ABE jar 2024-10-23 14:57:03 +02:00
Donncha Ó Cearbhaill
319bc7e9cd Switch docker build to use local context rather than pulling 2024-10-23 14:56:35 +02:00
Donncha Ó Cearbhaill
62cdfa1b59 Add info to docs on using docker image 2024-10-23 13:19:34 +02:00
Donncha Ó Cearbhaill
cbb78b7ade Update pip version in image to try fix package build issue 2024-10-23 13:19:10 +02:00
Donncha Ó Cearbhaill
4598293c82 Generate ADB key on first run to avoid static key in image 2024-10-23 13:18:43 +02:00
Donncha Ó Cearbhaill
6e0cd23bbc Add license to Docker image metadata 2024-10-23 13:17:47 +02:00
Donncha Ó Cearbhaill
d6f3561995 Fix docs build dependencies 2024-10-23 12:34:47 +02:00
Donncha Ó Cearbhaill
19b3b97571 Build Docker image on release rather than on branch (#561)
* Build image on release

* Allow workflow to be trigger manually outside of releases
2024-10-23 12:04:53 +02:00
Donncha Ó Cearbhaill
2c72d80e7c Fix action which updates iOS verisons and build numbers (#560) 2024-10-23 11:55:16 +02:00
Donncha Ó Cearbhaill
720aeff6e9 Add workflow for building Docker image (#559) 2024-10-23 11:53:55 +02:00
Donncha Ó Cearbhaill
863de4f543 Fix crash Handling empty adb key list (#558) 2024-10-23 11:50:08 +02:00
Donncha Ó Cearbhaill
3afe218c7c Add support for check APK certificate hash IOCs (#557)
* Fix bug loading indicators which I introduced in 81b647b

* Add support for matching on APK certificate hash IOCs
2024-10-18 16:35:50 +02:00
Donncha Ó Cearbhaill
665806db98 Add initial parser for ADB state in Dumpsys (#547)
* Add initial parser for ADB dumpsys

* Add ADBState tests and support for AndroidQF and
check-adb

* Handle case where ADB is not available in device dumpsys
2024-10-18 15:31:25 +02:00
Tek
a03f4e55ff Adds androidqf files module (#541)
* Adds androidqf files module

* Add new files module to module list

---------

Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2024-10-17 18:32:23 +02:00
Donncha Ó Cearbhaill
81b647beac Add basic support for IP indicators in MVT (#556)
* Add prelimary ipv4-addr ioc matching support under collection domains

* Add IP addresses as a valid IOC type

This currently just supports IPv4 addresses which
are treated as domains internally in MVT.

---------

Co-authored-by: renini <renini@local>
2024-10-17 18:20:17 +02:00
Donncha Ó Cearbhaill
5ef19a327c Fix error reporting for update check failures (#555) 2024-10-17 13:26:53 +02:00
Donncha Ó Cearbhaill
f4bf3f362b Refactor CLI help messages to make the CLI code more readable and maintainable. (#554)
* - modified help message string storage and referencing for consistency
- grammar correction to docs/android/download_apks.md
- changed ios backup help message from a format string that would reference
  and explicitly print the environment variable, to printing the name of the
  environment variable itself

* Fix formatting for help message refactor

---------

Co-authored-by: jazzy0verflow <hi@ra0x1duk3.mozmail.com>
Co-authored-by: kh0rvus <50286871+kh0rvus@users.noreply.github.com>
2024-10-17 12:28:42 +02:00
Tek
7575315966 Adds timeout to update checks (#542)
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2024-10-17 11:56:05 +02:00
Tek
9678eb17e5 Fixes a minor bug in IOC import (#553) 2024-10-17 11:36:33 +02:00
Tek
7303bc06e5 Adds recovery of sqlite db when db is opened (#516)
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2024-10-17 11:28:13 +02:00
Donncha Ó Cearbhaill
477f9a7f6b Fix CI badge (#552) 2024-10-16 17:11:59 +02:00
Tek
aced1aa74d Fixes a bug in Android SMS parsing #526 (#530)
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2024-10-16 16:56:06 +02:00
Tek
052c4e207b Improves STIX2 support and testing (#523)
* Improves STIX2 support and testing

* Adds documentation on STIX2 support in MVT

---------

Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2024-10-16 16:47:10 +02:00
Donncha Ó Cearbhaill
821943a859 Merge branch 'besendorf/main' 2024-10-16 16:36:07 +02:00
Donncha Ó Cearbhaill
f4437b30b1 Fix black formatting 2024-10-16 16:35:28 +02:00
besendorf
d4946b04bf Update deprecated functions and other small changes (#533)
* also search for STIX2 files in directories in MVT_STIX2

* update datetime deprecations

* add variable declaration in __init__

* add str to return typed in cmd_download_apks.py

* change dictionary creations to dictionary literals

* replace call to set() with set literal

* fix incorrect docstrings

* remove whitespace according to PEP8: E203

* remove whitespace according to PEP8: E203

* remove unreachable return statement

* use Union[] instead of | operator for python 3.8/9 compatability

* Fix ruff formating of files

* Revert "also search for STIX2 files in directories in MVT_STIX2"

This reverts commit 287a11a2ee. We
have this change as a seperate PR in #527.

---------

Co-authored-by: Janik Besendorf <jb@reporter-ohne-grenzen.de>
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2024-10-16 16:29:02 +02:00
Donncha Ó Cearbhaill
a15d9f721d Merge pull request #544 from mvt-project/feature/use-pyproject-toml
Configure project to use pyproject.toml and consistent CI and test tooling
2024-10-16 16:06:23 +02:00
Donncha Ó Cearbhaill
10e7599c6e Merge branch 'main' into feature/use-pyproject-toml 2024-10-16 15:40:36 +02:00
Janik Besendorf
a44688c501 change recursive search for indicator files from os.walk to glob.glob 2024-10-08 15:49:03 +02:00
github-actions[bot]
c66a38e5c0 Add new iOS versions and build numbers (#549)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-10-04 10:53:41 +02:00
Donncha Ó Cearbhaill
ee2fab8d87 Merge main and add dependency change 2024-09-30 16:53:50 +02:00
Donncha Ó Cearbhaill
f8e2b0921a Merge pull request #509 from scribblemaniac/multistage-docker
Docker improvements (multistage builds, separate os images, and more)
2024-09-30 12:50:51 +01:00
Donncha Ó Cearbhaill
5225600396 Remove duplicate CI file 2024-09-30 13:34:56 +02:00
Donncha Ó Cearbhaill
2c4c92f510 Try using package name as path 2024-09-30 13:21:02 +02:00
Donncha Ó Cearbhaill
656feb1da7 Try make sure pytest uses the local editable install 2024-09-30 13:11:21 +02:00
Donncha Ó Cearbhaill
79dd5b8bad Temporarily disable automatic type checks in CI
MyPy checks should be renabled once the types are fixed in
https://github.com/mvt-project/mvt/issues/545
2024-09-30 12:53:17 +02:00
Donncha Ó Cearbhaill
f79938b082 Run ruff on PRs 2024-09-30 12:44:21 +02:00
Donncha Ó Cearbhaill
822536a1cb Add formating change made by ruff linter 2024-09-30 12:41:46 +02:00
Donncha Ó Cearbhaill
69fb8c236f Simplify the CI tests using the Makefile 2024-09-30 12:39:21 +02:00
Donncha Ó Cearbhaill
5dfa0153ee Restructure MVT to use pyproject.toml 2024-09-30 12:26:29 +02:00
Donncha Ó Cearbhaill
d79f6cbd7d Run black linter on pull requests (#543)
The black linter was only being run on pushes to main and not on opened PRs. We should run on both to avoid linting errors after a PR is merged.
2024-09-30 11:49:00 +02:00
tek
617c5d9e1c Fixes import order 2024-09-28 13:15:43 +02:00
besendorf
ae9f874e1b Merge branch 'mvt-project:main' into main 2024-09-17 20:17:10 +02:00
github-actions[bot]
b58351bfbd Add new iOS versions and build numbers (#532)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-09-17 10:46:42 +02:00
Janik Besendorf
287a11a2ee also search for STIX2 files in directories in MVT_STIX2 2024-09-03 20:20:46 +02:00
github-actions[bot]
efe46d7b49 Add new iOS versions and build numbers (#521)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-08-23 15:10:39 +02:00
github-actions[bot]
102dd31bd6 Add new iOS versions and build numbers (#514)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-08-07 23:57:46 +02:00
scribblemaniac
e00895aa9d Explicitly install usb version of adb_shell
This works without [usb] in Debian, but not in Alpine for some
reason.
2024-07-03 15:45:47 -06:00
scribblemaniac
79dbf999a9 Use OCI standard labels for docker image 2024-07-03 15:45:47 -06:00
scribblemaniac
89d31f3212 Refactor Dockerfile into tool-specific multi-stage builds
Also made the following other changes:
- The final image for the tool-specific Dockerfiles is based off of
  alpine instead of ubuntu
- Add step to build libtatsu, which is a new dependency for
  libimobiledevice
- Multithread make operations during build
- Use ARG instead of ENV for build environment variables
- Use apt-get instead of apt
- Use non-dev library in the final image (except for manually built libraries)
2024-07-03 15:45:47 -06:00
Rory Flynn
caeeec2816 Add packages module for androidqf (#506)
* Add Packages module for androidqf

* Update test
2024-06-24 19:00:07 +02:00
Rory Flynn
9e19abb5d3 Fixes for failing CI (#507) 2024-06-24 18:50:42 +02:00
Rory Flynn
cf5cf3b85d Mark 2.5.4 release (#504) 2024-06-21 14:51:16 +02:00
Rory Flynn
f0dbe0bfa6 Prevent command.log from being appended to when run in a loop (#501)
* Prevent command.log from being appended to when run in a loop

* Ignore a rather stupid vulnerability scan alert for pip
2024-05-27 19:15:32 +02:00
github-actions[bot]
555e49fda7 Add new iOS versions and build numbers (#499)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-05-20 23:12:04 +02:00
Rory Flynn
a6d32e1c88 Fix dumpsys accessibility detections for v14+ (#483) 2024-05-19 22:27:28 +02:00
github-actions[bot]
f155146f1e Add new iOS versions and build numbers (#498)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-05-14 10:58:00 +02:00
Tek
9d47acc228 Returns empty string when no date in date converter (#493) 2024-04-30 16:51:58 +02:00
Rory Flynn
cbd41b2aff Mark 2.5.3 release (#490) 2024-04-19 17:23:55 +02:00
Rory Flynn
0509eaa162 Use backwards-compatible datetime.timezone.utc (#488) 2024-04-19 17:22:10 +02:00
Rory Flynn
59e6dff1e1 Fail builds on test failure (#489)
* Fail builds on test failure

* Deliberately fail a build to test

* Revert "Deliberately fail a build to test"

This reverts commit 666140a954.
2024-04-19 17:18:27 +02:00
Rory Flynn
f1821d1a02 Mark release 2.5.2 (#486) 2024-04-18 16:53:41 +02:00
Rory Flynn
6c7ad0ac95 Convert timezone-aware datetimes automatically to UTC (#485) 2024-04-18 16:49:30 +02:00
tek
3a997d30d2 Updates SMS module to highlight new text of Apple notifications 2024-04-15 23:28:36 +02:00
tek
6f56939dd7 Requires latest cryptography version 2024-04-15 22:41:01 +02:00
Donncha Ó Cearbhaill
7a4946e2c6 Mark release 2.5.1 (#481)
Signed-off-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2024-04-11 11:14:42 +02:00
r-tx
e1c4f4eb7a Add more short urls (#479)
Co-authored-by: r-tx <r-tx@users.noreply.github.com>
2024-04-11 11:08:15 +02:00
Donncha Ó Cearbhaill
f9d7b550dc Add docs explaining how to seek expert help for forensic analysis (#476)
* Update forensic support links in the documentation

* Add expert help message to MVT output

* Add warning to disable ADB after an Android acquisition

* Include Developer Options in the ADB warning text
2024-04-08 18:47:59 +02:00
renini
b738603911 Usbmuxd debug option changed from -d to -v (#464)
Co-authored-by: renini <renini@local>
2024-04-08 18:34:34 +02:00
tek
5826e6b11c Migrate dumpsys_packages parsing into an artifact 2024-04-01 01:49:08 +02:00
tek
54c5d549af Fixes bug in dumpsys package parsing 2024-04-01 00:56:37 +02:00
github-actions[bot]
dded863e58 Add new iOS versions and build numbers (#473)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-03-27 21:18:09 +01:00
github-actions[bot]
fc7ea5383e Add new iOS versions and build numbers (#472)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-03-21 19:06:47 +01:00
github-actions[bot]
04b78a4d60 Add new iOS versions and build numbers (#468)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-03-06 10:16:08 +01:00
Dean Ben
4ea53d707b Update install.md (#461)
fixed mistakes
2024-02-14 10:53:55 +01:00
github-actions[bot]
da743a2878 Add new iOS versions and build numbers (#460)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-02-09 15:17:53 +01:00
Rory Flynn
4681b57adc Handle no indicators provided in sms_attachments.py (#455)
* Handle no indicators provided in `sms_attachments.py`

* Move guard to a more specific place

* Unrelated black formatting

* Related black changes :)
2024-02-07 13:30:27 +01:00
Rory Flynn
bb7a22ed0b Update install docs (#449) 2024-02-05 14:17:40 +01:00
github-actions[bot]
b2df17b4a0 Add new iOS versions and build numbers (#451)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-01-24 11:03:09 +01:00
tek
278611a753 Update ios parsing 2024-01-18 23:42:05 +01:00
tek
cd4d468553 Update ios parsing 2024-01-18 19:43:13 +01:00
r-tx
1182587094 change vt flag to -V (#440)
Co-authored-by: r-tx <r-tx@users.noreply.github.com>
2024-01-10 15:38:15 +01:00
Rory Flynn
ad3bc3470e Mark release 2.5.0 (#445) 2024-01-04 20:08:42 +01:00
github-actions[bot]
2c5ae696b1 Add new iOS versions and build numbers (#439)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2024-01-03 19:08:15 +01:00
Christian Clauss
5d2ff32e3a dumpsys_accessibility.py: Spell accessibility correctly (#441)
* dumpsys_accessibility.py: Spell accessibility correctly

* Fix typo
2024-01-03 18:59:06 +01:00
Rory Flynn
2838bac63f Circular reference in SMS module serialization (#444)
* Fix circular reference in SMS module serialization
* Modify SMS test artifact to include date_read
2024-01-03 18:55:32 +01:00
msx98
b7df87a62f add uri=True to sqlite3.connect args (#442)
Co-authored-by: msx98 <msx98@xb.ax>
2023-12-28 11:44:38 +01:00
Donncha Ó Cearbhaill
013282dbba Impovements for SMS module (#438)
* Add indicator checking in the SMS module

* Don't add SMS entries when read timestamp not set

* Remove print() line
2023-12-17 12:59:35 +01:00
github-actions[bot]
ab33789f06 Add new iOS versions and build numbers (#437)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-12-12 08:40:32 +01:00
Rory Flynn
a1571c127d Mark release 2.4.5 (#436) 2023-12-11 11:10:36 +01:00
Rory Flynn
61f33f7ecb Fix typo in ios_models.json (#435) 2023-12-09 19:41:43 +01:00
Rory Flynn
4a6b483ce3 Mark release 2.4.4 (#433) 2023-12-04 17:05:04 +01:00
github-actions[bot]
101098cbb7 Add new iOS versions and build numbers (#432)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-12-01 10:40:09 +01:00
Rory Flynn
fd3ef76873 Open all iOS sqlite3 databases with immutable=1 (#430) 2023-11-28 12:46:18 +01:00
Rory Flynn
fb52f73556 Automatically add issues to development board (#428) 2023-11-27 14:10:54 +01:00
r-tx
acc950377f docker tweaks (#424)
Co-authored-by: r-tx <r-tx@users.noreply.github.com>
2023-11-27 12:53:03 +01:00
Rory Flynn
c8a0327768 Allow PR coverage comment to fail if running on a fork (#427) 2023-11-27 12:39:18 +01:00
Rory Flynn
1d075abde9 Make MVTModule.get_slug() a classmethod (#418) 2023-11-22 10:37:38 +01:00
tek
73104814ba Bumps version 2023-11-18 08:58:26 +01:00
Matthias
2098201024 Update net_base.py (#416)
Fix Bug in checking for ZLIVEUSAGE.TIMESTAMP
2023-11-15 23:27:51 +01:00
Rory Flynn
4b4cad46ba Add CustomJSONEncoder to handle bytes types (#414)
Adds a custom JSON encoder class to fix serialisation issues where modules included bytes types containing non-utf8 bytes, which can't be serialised to JSON.

---------

Co-authored-by: Rory Flynn <rory.flynn@amnesty.org>
2023-11-15 11:40:24 +01:00
Donncha Ó Cearbhaill
815678dff7 Merge pull request #413 from mvt-project/update-securitylab-url
Fix ReadTheDocs build with new configuration
2023-11-09 13:00:14 +01:00
Donncha Ó Cearbhaill
bdd0124b80 Fix invalid docs file path 2023-11-09 12:59:18 +01:00
Donncha Ó Cearbhaill
894b99b177 Try fix ReadTheDocs build 2023-11-09 12:54:41 +01:00
Donncha Ó Cearbhaill
8796a0b965 Merge pull request #412 from mvt-project/update-securitylab-url
Update docs to point to Security Lab website
2023-11-08 18:23:23 +01:00
Donncha Ó Cearbhaill
f786e2c9bf Update docs to point to Security Lab website 2023-11-08 16:42:44 +01:00
github-actions[bot]
5b0fdd117c Add new iOS versions and build numbers (#411)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-11-08 01:32:03 +01:00
tek
7b1c0f6443 Adds time info to OSAnalyticsADDaily module message 2023-11-07 10:22:28 +01:00
tek
353381a7d6 Adds warning if missing file in backup for configuration profiles 2023-11-02 18:54:18 +01:00
tek
5462c8d6e7 Fixes change in ruff syntax 2023-10-26 11:16:38 +02:00
github-actions[bot]
63299b530b Add new iOS versions and build numbers (#409)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-10-26 11:10:48 +02:00
github-actions[bot]
737d17086c Add new iOS versions and build numbers (#406)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-10-11 16:59:51 +02:00
tek
499ded4e7b Potential fix for user issue on android package module 2023-10-07 18:27:53 +02:00
Jakub Wilk
63c4dea3d0 Fix capitalization of "PyPI" (#404) 2023-10-04 23:43:45 +02:00
Donncha Ó Cearbhaill
e872e34c5a Merge pull request #405 from mvt-project/auto/add-new-ios-releases
[auto] Update iOS releases and versions
2023-10-04 20:25:30 +02:00
DonnchaC
4f3a16daf0 Add new iOS versions and build numbers 2023-10-04 18:04:42 +00:00
Donncha Ó Cearbhaill
eb5bfb7f35 Properly handle iOS version updates with multiple builds 2023-09-30 11:45:29 +02:00
tek
a7da992d75 Squashed commit of the following:
commit 2b8ab1bedf72769bcf915e0c78330de98722a449
Author: tek <tek@randhome.io>
Date:   Wed Sep 27 11:33:02 2023 +0200

    Fixes formatting issue

commit 6a653c0149
Author: DonnchaC <DonnchaC@users.noreply.github.com>
Date:   Wed Sep 27 00:15:14 2023 +0000

    Add new iOS versions and build numbers
2023-09-27 11:34:32 +02:00
tek
652dedd620 Fixes bug in locationd module 2023-09-26 16:53:33 +02:00
tek
4bdb97ebcc Fixes format changes in locationd file in iOS 17 2023-09-26 13:58:38 +02:00
Donncha Ó Cearbhaill
da6116ee20 Merge pull request #394 from mvt-project/auto/add-new-ios-releases
[auto] Update iOS releases and versions
2023-09-23 15:09:50 +02:00
DonnchaC
65cee2ae53 Add new iOS versions and build numbers 2023-09-23 00:14:31 +00:00
github-actions[bot]
97af2d3605 Add new iOS versions and build numbers (#393)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-09-22 09:47:38 +02:00
r-tx
b39936a060 additional iOS 17.0 build numbers (#390)
Co-authored-by: r-tx <r-tx@users.noreply.github.com>
2023-09-18 23:23:07 +02:00
github-actions[bot]
94a98b4f83 Add new iOS versions and build numbers (#388)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-09-18 23:07:25 +02:00
tek
0c7ea142f7 Fixes json format 2023-09-18 19:32:33 +02:00
r-tx
e96ffbb022 add 4 new iphone 15 models (#387)
Co-authored-by: r-tx <r-tx@users.noreply.github.com>
2023-09-18 17:03:25 +02:00
r-tx
ba1d9699c3 Update ios_versions.json (#386)
Add iOS 15.7.9 + build number
2023-09-13 00:05:17 +02:00
Nex
9988887d27 Updated copyright notice 2023-09-09 17:55:27 +02:00
tek
04b44826b4 Adds warning for lockdown mode 2023-09-08 13:26:36 +02:00
github-actions[bot]
2bfe5443c8 Add new iOS versions and build numbers (#385)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-09-08 09:51:19 +02:00
tek
c719c4da1e Bumps version 2023-09-05 13:01:20 +02:00
tek
0f3e93c152 Adds missing iphone models 2023-09-05 12:53:19 +02:00
tek
a2ee46b8f8 Refactors dumpsys receiver parsing into an artifact 2023-08-08 20:23:09 +02:00
tek
e60e5fdc6e Refactors DumpsysBatteryHistory and adds related androidqf module 2023-08-04 19:20:14 +02:00
tek
7e0e071c5d Refactor DumpsysBatteryDaily module and add related artifact 2023-08-04 16:17:52 +02:00
Nex
b259db30f8 Added missing empty lines 2023-08-03 08:06:59 +02:00
Donncha Ó Cearbhaill
26f981244d Merge pull request #380 from a-sdi/patch-1
Update applications.py to add extra valid source
2023-08-02 20:03:30 +02:00
Donncha Ó Cearbhaill
2069e2b760 Fix style error (need space after # in comment) 2023-08-02 19:57:26 +02:00
a-sdi
355480414f Update applications.py
Some apps installed from apple store with sourceApp "com.apple.AppStore.ProductPageExtension"
2023-08-02 19:26:06 +03:00
tek
9a831b5930 Adds GlobalPreferences iOS module 2023-08-02 15:28:16 +02:00
tek
a103b50759 Rename artifacts to avoid name collisions 2023-08-02 13:32:58 +02:00
tek
84dc13144d Refactor DumpsysAppOps 2023-08-01 11:58:20 +02:00
tek
6356a4ff87 Refactor code of DumpsysDBInfo 2023-07-31 23:43:20 +02:00
tek
f96f2fe34a refactor dumpsys package activity code 2023-07-31 18:38:41 +02:00
Donncha Ó Cearbhaill
ae0e470c56 Fix inconsisent filesytem tests on some platforms 2023-07-31 11:45:53 +02:00
tek
4c175530a8 Refactor dumpsys accessibility in an artifact 2023-07-27 19:42:06 +02:00
Donncha Ó Cearbhaill
ecf75447aa Only add coverage comment to pull requests 2023-07-27 17:44:18 +02:00
tek
0389d335ed Bumps version 2023-07-26 18:20:25 +02:00
tek
7f9acec108 Move verbose indicator information to debug 2023-07-26 15:12:58 +02:00
Tek
3ec3b86a45 Adds support for zip files in check-androidqf command (#372) 2023-07-26 13:53:54 +02:00
Donncha Ó Cearbhaill
57d4aca72e Refactor Android modules to remove duplication (#368)
* Remove duplicated detection logic from GetProp modules
* Deduplicate settings and processes
* Refactor detection in artifacts
* Improves Artifact class
---------

Co-authored-by: tek <tek@randhome.io>
2023-07-26 13:42:17 +02:00
github-actions[bot]
1d740ad802 Add new iOS versions and build numbers (#373)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2023-07-25 10:21:08 +02:00
Donncha Ó Cearbhaill
15ce1b7e64 Merge pull request #370 from mvt-project/android-backup-refactor
Refactor Android backup password handling and add tests
2023-07-22 20:17:47 +02:00
Donncha Ó Cearbhaill
d6fca2f8ae Fix bugs with running ADB commands 2023-07-22 20:16:23 +02:00
Donncha Ó Cearbhaill
cabb679ff1 Merge branch 'main' into android-backup-refactor 2023-07-22 19:59:42 +02:00
Donncha Ó Cearbhaill
829a9f0cf6 Merge pull request #371 from mvt-project/add-coverage
Add code test coverage reporting using pytest-cov
2023-07-22 19:56:04 +02:00
Donncha Ó Cearbhaill
52e0176d5d Add code test coverage reporting 2023-07-22 19:54:01 +02:00
Donncha Ó Cearbhaill
8d8bdf26de Fix black style checks 2023-07-22 19:52:25 +02:00
Donncha Ó Cearbhaill
34fa77ae4d Add documentation for new options 2023-07-22 19:49:59 +02:00
Donncha Ó Cearbhaill
ed7d6fb847 Add integration tests for 'mvt-android check-backup' 2023-07-22 19:26:05 +02:00
Donncha Ó Cearbhaill
a2386dbdf7 Refactor Android backup password handling and add tests 2023-07-22 19:17:27 +02:00
Donncha Ó Cearbhaill
019cfbb84e Merge pull request #363 from aticu/main
Add option to disable interactivity and pass Android backup password on CLI
2023-07-22 16:44:35 +02:00
Donncha Ó Cearbhaill
3d924e22ec Merge branch 'release/v2.4.0' 2023-07-21 12:17:32 +02:00
Donncha Ó Cearbhaill
ca3c1bade4 Bump version to v2.4.0
Bumping the minor version as we introduce some backwards-incompatible
API changes to module definition in #367.
2023-07-21 12:14:31 +02:00
Donncha Ó Cearbhaill
85877fd3eb Merge pull request #369 from mvt-project/move-indicator-checking
Move detection and alerts from run() to check_indicators()
2023-07-21 12:12:36 +02:00
Donncha Ó Cearbhaill
8015ff78e8 Fix black error 2023-07-21 12:10:45 +02:00
Donncha Ó Cearbhaill
1a07b9a78f Move syntax checking before unit tests 2023-07-21 11:30:59 +02:00
Donncha Ó Cearbhaill
0b88de9867 Move detection and alerts from run() to check_indicators() 2023-07-21 11:29:12 +02:00
Niclas Schwarzlose
0edc9d7b81 Add option to disable interactivity 2023-07-19 11:29:51 +02:00
Donncha Ó Cearbhaill
76d7534b05 Fix bug recording detections in WebkitResourceLoadStatistics module 2023-07-18 18:02:42 +02:00
Donncha Ó Cearbhaill
ae2ab02347 Merge pull request #367 from mvt-project/refactor-module-options
Add a module_options parameter to pass data from CLI to modules
2023-07-17 19:07:41 +02:00
Donncha Ó Cearbhaill
e2c623c40f Move --fast flag from being a top-level MVT module parameter to an option in a new module_options parameter 2023-07-17 18:52:35 +02:00
Christian Clauss
a6e1a3de12 Add GitHub Annotions to ruff output (#364)
* Add GitHub Annotions to ruff output
* Upgrade GitHub Actions
* No Py3.11
2023-07-15 14:42:13 +02:00
tek
e7270d6a07 Fixes import and adds test for PR 361 2023-07-10 22:55:22 +02:00
Niclas Schwarzlose
1968a0fca2 Improve appops parsing in dumpsys (#361)
Without this change the package doesn't get properly reset when a new
user starts.

See for example in this excerpt:

```
 1 |    Package com.android.bluetooth:
 2 |      READ_CONTACTS (allow):
 3 |        null=[
 4 |          Access: [pers-s] 2022-04-22 13:24:17.577 (-277d5h22m53s447ms)
 5 |        ]
 6 |      WAKE_LOCK (allow):
 7 |        null=[
 8 |          Access: [pers-s] 2023-01-24 17:45:49.712 (-1m21s312ms) duration=+3ms
 9 |        ]
10 |      GET_USAGE_STATS (default):
11 |        null=[
12 |          Reject: [pers-s]2022-04-22 13:23:53.964 (-277d5h23m17s60ms)
13 |        ]
14 |      BLUETOOTH_CONNECT (allow):
15 |        null=[
16 |          Access: [pers-s] 2022-04-22 13:23:53.988 (-277d5h23m17s36ms)
17 |        ]
18 |  Uid 1027:
19 |    state=pers
20 |    capability=LCMN
21 |    appWidgetVisible=false
22 |      LEGACY_STORAGE: mode=ignore
23 |    Package com.android.nfc:
24 |      WAKE_LOCK (allow):
25 |        null=[
26 |          Access: [pers-s] 2022-04-22 13:23:54.633 (-277d5h23m16s391ms) duration=+1s73ms
27 |        ]
```

Here the package "com.android.bluetooth" is not reset when in line 18,
so when "LEGACY_STORAGE:" in line 22 is encountered, it's added as
another permission to "com.android.bluetooth" with "access" set to
"ode=igno".

This PR fixes that by resetting the package whenever a new Uid is
encountered.

Co-authored-by: Niclas Schwarzlose <niclas.schwarzlose@reporter-ohne-grenzen.de>
2023-07-10 22:53:58 +02:00
Donncha Ó Cearbhaill
46cc54df74 Add information about public indicators and support avenues to documentation 2023-06-30 19:43:30 +02:00
Donncha Ó Cearbhaill
7046ff80d1 Add SMS read time in the MVT logs 2023-06-30 19:30:50 +02:00
Donncha Ó Cearbhaill
e2516f284b Bump version number 2023-06-29 17:03:26 +02:00
Donncha Ó Cearbhaill
17963f83d6 Fix URL to indicator repo in docs 2023-06-29 16:49:20 +02:00
Donncha Ó Cearbhaill
4f0c9c6077 Update README with information on indicators of compromise and path ways for forensic support 2023-06-29 16:48:56 +02:00
Donncha Ó Cearbhaill
27bd5f03a8 Merge pull request #359 from mvt-project/optimise-domain-checking
Optimise domain checking performance
2023-06-29 14:56:50 +02:00
Donncha Ó Cearbhaill
3babbadc1d Add docs for the profiling feature 2023-06-29 14:55:09 +02:00
Donncha Ó Cearbhaill
41db117168 Improve performance when checking URLs and domains
Some MVT modules such as the WhatsApp module can be very slow as it was taking a naive approach to look for IOCs. The code was checking URLs (potentially more than 100k) against
1000's of IOC domains resulting in a quadratic run-time with hundreds of millions of comparisons as the number of IOCs increases.

This commit add an Aho-Corasick library which allows the efficient search in a string (the URL in this case) for all matches in set of keys (the IOCs). This data structure is perfect for this use case.

A quick measurement shows a 80% performance improvement for a WhatsApp database with 100k entries. The slow path is now the time spent fetching and expanding short URLs found in the database. This
can also be sped up significantly by fetching each URL asynchronously. This would require reworking modules to split the URL expansion from the IOC check so I will implement in a separate PR.
2023-06-29 14:14:44 +02:00
Donncha Ó Cearbhaill
2b01ed7179 Add optional profiling for MVT modules 2023-06-29 13:31:13 +02:00
Donncha Ó Cearbhaill
78d493b17e Merge pull request #356 from mvt-project/auto/add-new-ios-releases
[auto] Update iOS releases and versions
2023-06-22 11:06:45 +02:00
DonnchaC
473c80009b Add new iOS versions and build numbers 2023-06-22 00:17:52 +00:00
tek
a1481683e3 Adapts linter workflow to black 2023-06-14 01:05:14 +02:00
Nex
bdd36a9179 Merge pull request #349 from mvt-project/code-cleanup
Linted code using isort + autoflake + black
2023-06-08 21:12:34 +02:00
Nex
e1677639c4 Linted code using isort + autoflake + black, fixed wrong use of Optional[bool] 2023-06-01 23:40:26 +02:00
tek
c2d740ed36 Handle better some empty database issues in iOS backups 2023-05-25 00:24:34 +02:00
tek
d0e24c6369 Fixes a bug in the applications module 2023-05-24 12:04:03 +02:00
tek
a1994079b1 Sort imports 2023-05-24 12:03:49 +02:00
Donncha Ó Cearbhaill
289b7efdeb Add missing iOS build numbers 2023-05-21 17:11:07 +01:00
Donncha Ó Cearbhaill
166a63e14c Merge pull request #347 from mvt-project/auto/add-new-ios-releases
[auto] Update iOS releases and versions
2023-05-21 17:54:25 +02:00
DonnchaC
1b933fdb12 Add new iOS versions and build numbers 2023-05-21 15:53:45 +00:00
Donncha Ó Cearbhaill
0c0ff7012b Set branch number for auto-generated pull request 2023-05-21 16:52:47 +01:00
Donncha Ó Cearbhaill
f9b0d07a81 Don't include information beta's in the version JSON 2023-05-21 16:49:14 +01:00
Donncha Ó Cearbhaill
d14bcdd05f Update title used in auto PR for new iOS versions 2023-05-21 16:47:56 +01:00
Donncha Ó Cearbhaill
e026bb0a76 Fix path to script in workflow 2023-05-21 16:44:17 +01:00
Donncha Ó Cearbhaill
253b4f031a Allow workflow to be triggered manually 2023-05-21 16:42:54 +01:00
Donncha Ó Cearbhaill
ec14297643 Merge pull request #345 from mvt-project/feature/auto-update-version-info
Add workflow to auto-update iOS builds and version numbers
2023-05-21 17:38:46 +02:00
Donncha Ó Cearbhaill
3142d86edd Fix path to include version JSON files in built package 2023-05-21 16:37:36 +01:00
Donncha Ó Cearbhaill
c18998d771 Add version 16.5 to resolve merge conflict from main 2023-05-21 16:26:12 +01:00
Donncha Ó Cearbhaill
22fd794fb8 Fix python style and setup.cfg syntax 2023-05-21 16:15:49 +01:00
Donncha Ó Cearbhaill
27c5c76dc2 Add script and worker to auto-update build and version info 2023-05-21 16:09:50 +01:00
Donncha Ó Cearbhaill
fafbac3545 Fix sorting of version numbers 2023-05-20 21:49:27 +01:00
Donncha Ó Cearbhaill
bbfaadd297 Load iOS device and build information from a JSON file. 2023-05-20 21:24:14 +01:00
tek
85abed55b6 Merge branch 'main' of github.com:mvt-project/mvt 2023-05-20 00:14:01 +02:00
tek
2fbd7607ef Adds latest iOS version 2023-05-20 00:11:16 +02:00
Donncha Ó Cearbhaill
3787dc48cd Fix bug where getprop sections where missing due to non-standard section header 2023-05-18 11:28:10 +02:00
tek
f814244ff8 Fixes bug in bugreport getprop module 2023-05-06 11:20:10 -04:00
tek
11730f164f Fixes an issue in androidqf SMS module 2023-05-06 11:04:42 -04:00
Sebastian Pederiva
912fb060cb Fix error when creating report: csv.Error (#341) 2023-05-02 17:09:16 +02:00
tek
a9edf4a9fe Bumps version 2023-04-25 12:20:45 +02:00
tek
ea7b9066ba Improves iOS app detection 2023-04-25 11:21:55 +02:00
tek
fd81e3aa13 Adds verbose mode 2023-04-25 11:13:46 +02:00
tek
15477cc187 Bumps version 2023-04-13 17:59:05 +02:00
tek
551b95b38b Improves documentation 2023-04-13 16:11:55 +02:00
tek
d767abb912 Fixes a bug in the calendar plugin 2023-04-13 13:21:33 +02:00
tek
8a507b0a0b Fixes a bug in WhatsApp iOS module 2023-04-13 09:26:52 +02:00
tek
63b95ee6a5 Bumps version 2023-04-12 12:52:57 +02:00
Tek
c8ae495971 Extract all messages from SMS and WhatsApp (#337) 2023-04-12 12:39:25 +02:00
tek
33d092692e Adds calendar iOS plugin 2023-04-12 10:21:17 +02:00
tek
b1e5dc715f Adds latest iOS version 2023-04-07 22:22:44 +02:00
tek
1dc1ee2238 Improves Indicator object 2023-04-07 15:07:45 +02:00
tek
a2cbaacfce Fixes hashing issue 2023-04-07 14:51:54 +02:00
tek
801fe367ac Improves WebkitResourceLoadStatistics module 2023-04-07 14:43:20 +02:00
tek
0d653be4dd Adds Applications iOS module 2023-04-07 14:10:24 +02:00
tek
179b6976fa Improves interactionc module 2023-04-07 12:25:30 +02:00
tek
577fcf752d Fixes issues in analytics module 2023-04-07 12:25:17 +02:00
tek
2942209f62 Improves module handling 2023-04-07 12:25:01 +02:00
tek
06bf7b9cb1 Bumps version 2023-03-29 14:44:59 +02:00
tek
b5d7e528de Adds indicators for android properties 2023-03-29 12:57:41 +02:00
tek
70c6f0c153 Adds latest iOS version 2023-03-27 23:10:25 +02:00
tek
49491800fb Improves typing 2023-03-24 19:02:02 +01:00
tek
1ad176788b Updates install instructions from sources 2023-03-24 15:11:21 +01:00
Donncha Ó Cearbhaill
11d58022cf Change checksum log message to info instead of warning 2023-03-03 21:21:32 +00:00
tek
cc205bfab0 Adds missing iOS versions 2023-03-02 15:47:37 -05:00
tek
671cd07200 Fixes a bug with YAML parsing of github workflow 2023-03-01 17:34:35 -05:00
tek
7581f81464 removes duplicated flake8 workflow 2023-03-01 16:50:33 -05:00
tek
4ed8ff51ff Improves code PEP8 compliance and adds ruff check 2023-03-01 16:43:08 -05:00
tek
fc4e2a9029 Improves logcat logging in mvt-android check-adb 2023-03-01 16:34:28 -05:00
tek
383d9b16de Bumps version 2023-02-21 15:34:48 -05:00
tek
55f6a4ae54 Fixes mypy typing issues 2023-02-21 15:18:36 -05:00
tek
89c6a35c26 Update documentation on making backups with Finder 2023-02-21 14:31:44 -05:00
Huy Ta
25614922d7 Added Documentation for Creating Encrypted iPhone Backup with Finder on macOS (#332)
* Added Documentation for Encrypted Iphone Backup with Finder on MacOS

* Added details on where to check the backups after completion and added screenshots for the process

* Added location of backups
2023-02-21 20:22:45 +01:00
Tek
7d79844749 Improves generation of hashes (#327)
* Improves generation of hashes

* Adds generation of reference info.json hash
2023-02-21 20:16:32 +01:00
tek
83447411ff Adds additional iOS versions 2023-02-14 16:05:11 -05:00
tek
ce177978cd Sort imports 2023-02-14 11:51:55 -05:00
tek
95842ac449 Fixes #329 outdated iOS version error 2023-02-14 11:51:38 -05:00
tek
8ce6b31299 Adding latest iOS version 16.3.1 2023-02-13 19:21:41 -05:00
tek
704ea39569 Removes empty lines to be PEP8 compliant 2023-02-08 20:20:13 +01:00
tek
81ed0b0c19 Update copyright information 2023-02-08 20:18:16 +01:00
tek
318c908dd8 Fixes bug in adb File module. Fixes #268 2023-02-08 20:03:45 +01:00
tek
a5cf5271fa Allows -h argument for --help 2023-02-08 19:09:47 +01:00
tek
716909b528 Temporary fix of setuptools issue 2023-01-24 16:42:50 +01:00
William Budington
cbd9158daf Fixes bug where su binary is present but privilege is not granted to com.android.shell (#326) 2023-01-24 16:22:52 +01:00
tek
013e3421c8 Adding iOS 16.3 2023-01-24 16:22:02 +01:00
tek
1042354be5 Adds serializing to iOS module webkit_resource_load_statistics 2023-01-13 12:58:26 +01:00
tek
96bc02d344 Adds new iOS versions 2022-12-14 17:18:42 +01:00
tek
d05e6fac00 Attempt to fix #268 bug in android files module 2022-12-08 12:04:15 +01:00
tek
200e26d906 Fixes a bug in shortcut parsing #296 2022-12-08 11:57:08 +01:00
tek
27fbdd2fd4 Merge branch 'main' of github.com:mvt-project/mvt 2022-12-08 11:12:43 +01:00
tek
4bbaa20e22 Adds iOS 16.1.2 build number 2022-12-08 11:05:26 +01:00
Nex
99e14ad8b0 Bumped version 2022-11-13 01:11:52 +01:00
tek
deaa68a2e0 Adds iOS 16.1.1 in iOS versions 2022-11-11 12:11:46 +01:00
tek
07f819bf5f Adds new iPhone hardware 2022-11-02 10:41:33 +01:00
tek
51fdfce7f4 Adds iOS 16.1 to iOS versions 2022-10-31 11:17:25 +01:00
Nex
41e05a107e Merge branch 'main' of github.com:mvt-project/mvt 2022-10-15 11:26:55 +02:00
Nex
e559fb223b Upgraded dependencies 2022-10-15 11:26:40 +02:00
Nex
b69bb92f3d Merge pull request #279 from Niek/main
Dockerfile improvements, support arm64 builds
2022-10-15 11:14:40 +02:00
Nex
42e8e41b7d Merge branch 'besendorf-patch-1' 2022-10-15 11:11:57 +02:00
Nex
00b7314395 Added quotes 2022-10-15 11:11:47 +02:00
Nex
39a8bf236d Merge branch 'patch-1' of github.com:besendorf/mvt into besendorf-patch-1 2022-10-15 11:11:29 +02:00
tek
d268b17284 Adds missing module in androidqf module list 2022-10-14 15:01:08 +02:00
tek
66c015bc23 Improves check-androidqf tests 2022-10-11 13:07:24 +02:00
tek
ba0106c476 Adds SMS androidqf module and improves tests 2022-10-11 12:41:42 +02:00
tek
41826d7951 Fixes PEP8 syntax issue 2022-10-05 15:30:39 +02:00
Nex
4e0a393a02 Bumped version 2022-10-01 12:40:04 +02:00
Tek
c3dc4174fc Adds detection for disabled security packages in Android (#306)
* Adds detection for disabled security packages in Android

* Update detection of disabled security packages
2022-09-26 12:17:09 +02:00
tek
e1d1b6c5de Fixes a minor issue in the iOS manifest module 2022-09-26 12:07:52 +02:00
tek
d0a893841b Adds new iOS versions 2022-09-12 23:49:33 +02:00
Nex
d4e99661c7 Merge pull request #300 from andefined/fix-idstatuscache-error
Fixed missing root_paths check for ios/idstatuscache module
2022-09-07 09:29:09 +02:00
Nex
6a00d3a14d Closing handle to ZipFile 2022-09-05 12:21:11 +02:00
Nex
a863209abb Added check-androidqf command 2022-09-05 12:12:36 +02:00
Nex
4c7db02da4 Bumped version 2022-09-01 09:42:03 +02:00
Nex
92dfefbdeb Added some support for patterns in backups' relative paths 2022-08-31 19:34:59 +02:00
Nex
8988adcf77 Warnings should be reserved for detections ideally 2022-08-25 17:22:24 +02:00
andefined
91667b0ded Fixed missing root_paths check for ios/idstatuscache module 2022-08-24 18:54:45 +03:00
tek
2365175dbd Adds check of process name in paths in indicators 2022-08-23 13:18:42 +02:00
Nex
528d43b914 Merge branch 'main' of github.com:mvt-project/mvt 2022-08-22 21:13:22 +02:00
Nex
f952ba5119 Removed comment with odd char 2022-08-22 21:12:59 +02:00
besendorf
d61b2751f1 Add pip command for update
Adds the pip comman for updating mvt. I think this would be helpfull for novice users as it already has been asked here: https://github.com/mvt-project/mvt/discussions/261
Also I sometimes forget the command too ;)
2022-08-22 12:20:56 +02:00
Nex
b4ed2c6ed4 Added commented backup ID 2022-08-22 10:40:36 +02:00
Nex
3eed1d6edf Sorted imports 2022-08-22 10:30:58 +02:00
Nex
83ef545cd1 Merge pull request #298 from jons44/patch-1
Fixed idevicebackup2 syntax
2022-08-20 16:29:57 +02:00
jons44
5d4fbec62b Fixed idevicebackup2 syntax 2022-08-19 19:34:12 +02:00
Nex
fa7d6166f4 Removed legacy print 2022-08-19 15:19:46 +02:00
Nex
429b223555 Bumped version 2022-08-18 18:31:32 +02:00
tek
e4b9a9652a Adds ios 15.6.1 version 2022-08-18 14:42:26 +02:00
Nex
134581c000 Merge pull request #297 from mvt-project/feature/dumpsys-packages-parsing
Improves Android dumpsys package parsing
2022-08-18 13:58:59 +02:00
tek
5356a399c9 Moves dumpsys parsing to android parsers and use the same parser for adb and bugreport modules 2022-08-17 18:24:51 +02:00
Nex
e0f563596d Setting a default value for list of ioc files in case none was specified 2022-08-17 15:58:53 +02:00
Nex
ea5de0203a Changed default for Optional[str] 2022-08-17 15:52:17 +02:00
Nex
ace965ee8a Changed default value for optional lists to None 2022-08-17 15:37:12 +02:00
Nex
ad8f455209 Sorted imports 2022-08-17 11:34:58 +02:00
tek
ae67b41374 Merge branch 'main' of github.com:mvt-project/mvt 2022-08-16 18:57:37 +02:00
tek
5fe88098b9 Improves dumpsys battery history parsing 2022-08-16 18:57:18 +02:00
Nex
d578c240f9 Added additional missing space in inline comment 2022-08-16 18:26:34 +02:00
Nex
427a29c2b6 Pylint notes to ignore some lines too long 2022-08-16 16:09:59 +02:00
Nex
5e6f6faa9c Sorted imports 2022-08-16 16:02:32 +02:00
Nex
74a3ecaa4e Linted code 2022-08-16 16:02:17 +02:00
Nex
f536af1124 Not using bare except and removed unused var 2022-08-16 15:55:29 +02:00
Nex
631354c131 Properly checking any potential domains in Manifest.db records (fixes: #293) 2022-08-16 15:40:28 +02:00
Nex
7ad7782b51 Merge branch 'main' of github.com:mvt-project/mvt 2022-08-16 13:40:14 +02:00
Nex
f04f91e1e3 Improved type hints and code style enforcement 2022-08-16 13:39:55 +02:00
Nex
6936908f86 Bumped version 2022-08-15 10:27:36 +02:00
Nex
f3e5763c6a Added SECURITY.md 2022-08-14 19:28:30 +02:00
Nex
f438f7b1fb Fixing unix epoch timestamps conversion to float 2022-08-13 23:37:35 +02:00
Nex
66a157868f Ensuring all adb connect/disconnect are happening in modules only 2022-08-13 23:12:43 +02:00
Nex
a966b694ea More line length enforcement 2022-08-13 18:27:54 +02:00
Nex
c9dd3af278 More line length enforcing 2022-08-13 18:24:11 +02:00
Nex
82a60ee07c Enforcing line length 2022-08-13 17:52:56 +02:00
Nex
8bc5113bd2 Enforcing line length 2022-08-13 17:51:06 +02:00
Nex
00d82f7f00 Enforcing line lenght 2022-08-13 17:50:00 +02:00
Nex
2781f33fb5 Added more date conversion wrappers 2022-08-13 14:04:10 +02:00
Nex
271fe5fbee Continuing enforcement of line length and simplifying date conversions 2022-08-13 02:14:24 +02:00
Nex
0f503f72b5 Starting to enforce line lengths on mvt-ios 2022-08-12 19:38:57 +02:00
Nex
424b86a261 Fixed typos 2022-08-12 19:25:56 +02:00
Nex
1fe595f4cc Added CONTRIBUTING.md file 2022-08-12 19:25:11 +02:00
Nex
b8c59f1183 Removed public_indicators.json legacy file 2022-08-12 19:15:17 +02:00
Nex
a935347aed Trying to enforce line lengths at 80/100 2022-08-12 19:14:05 +02:00
Nex
661d0a8669 Using Union type hints in order to support older versions of Python 2022-08-12 16:29:43 +02:00
Nex
63ff5fd334 Started linting the code 2022-08-12 16:20:16 +02:00
Nex
146b9245ab Sorted imports 2022-08-11 16:57:08 +02:00
Nex
99d33922be Conformed ways modules logger is initialized 2022-08-11 16:42:04 +02:00
Nex
c42634af3f Fixed logging in accessibility module 2022-08-11 14:50:25 +02:00
Nex
6cb59cc3ab Trying to tidy up ConfigurationProfiles module 2022-08-10 16:44:43 +02:00
Nex
e0481686b7 Fixed test file 2022-08-08 16:47:01 +02:00
Nex
804ade3a40 Conformed browerstate plugin to others with similar structure 2022-08-08 16:44:54 +02:00
tek
c5ccaef0c4 Fixes a bug in Safari Browser State module 2022-08-08 11:20:05 +02:00
Nex
c4416d406a Avoiding duplicate entries for stix2 files with multiple malware definitions 2022-08-06 14:49:05 +02:00
Nex
6b8a23ae10 Added an attribute list to keep track of executed modules 2022-08-05 13:52:51 +02:00
tek
872d5d766e Adds product name in iOS backup info module 2022-08-03 16:34:39 +02:00
Nex
f5abd0719c Bumped version 2022-08-02 18:26:29 +02:00
Nex
6462ffc15d Added iOS 15.6 2022-08-02 18:26:23 +02:00
Nex
6333cafd38 Bumped version 2022-07-25 17:43:37 +02:00
Nex
03c59811a3 Ordered imports 2022-07-25 17:43:27 +02:00
Nex
cfd3b5bbcb Merge branch 'main' of github.com:mvt-project/mvt 2022-07-25 17:43:08 +02:00
Nex
97ab67240f Creating MVT data folder when missing 2022-07-25 17:42:51 +02:00
Nex
7fc664185c Flake8 fixes 2022-07-20 15:49:51 +02:00
Nex
93094367c7 Bumped version 2022-07-20 15:41:42 +02:00
Nex
e8fa9c6eea Passing binary data to parse rather than a file path 2022-07-20 15:41:07 +02:00
Nex
79a01c45cc Bumped version 2022-07-20 14:12:17 +02:00
Nex
a440d12377 Merge branch 'main' of github.com:mvt-project/mvt 2022-07-20 14:12:08 +02:00
Nex
8085888c0c Improved parsing of profile events to support new formats as well 2022-07-20 14:11:36 +02:00
Nex
c2617fe778 Checking profile IDs in profile_events 2022-07-20 13:25:51 +02:00
Nex
2e1243864c Added check_indicators to profile_events 2022-07-20 13:24:20 +02:00
tek
ba5ff9b38c Fixes a minor typing bug 2022-07-18 14:25:01 +02:00
Nex
3fccebe132 Merge branch 'main' of github.com:mvt-project/mvt 2022-07-14 12:06:52 +02:00
Nex
1265b366c1 Added install_non_market_apps to settings warnings 2022-07-14 09:09:01 +02:00
Nex
c944fb3234 Enforcing quotes in timeline csv writing 2022-07-12 12:03:20 +02:00
Nex
e6b4d17027 Using error instead of warning for failed apk download 2022-07-12 11:55:31 +02:00
Nex
f55ac36189 Code style fixes 2022-07-12 11:55:10 +02:00
Nex
550d6037a6 Bumped version 2022-07-08 19:54:46 +02:00
Nex
e875c978c9 Optional address in SMS serialize 2022-07-08 19:54:33 +02:00
Nex
fbf510567c Bumped version 2022-07-07 13:51:56 +02:00
Nex
94fe98b9ec Removed unused imports 2022-07-07 13:00:38 +02:00
Nex
a328d57551 Added test-upload to Makefile 2022-07-07 12:31:35 +02:00
Nex
a9eabc5d9d Updated dependencies 2022-07-07 12:28:42 +02:00
Nex
1ed6140cb6 Got rid of tqdm in favor of rich progress bar 2022-07-07 12:28:30 +02:00
Nex
efceb777f0 Small clean ups and type hints of mvt-android 2022-07-06 18:38:16 +02:00
Nex
14bbbd9e45 Refactored mvt-android adb Files module in order to keep copy of suspicious payloads 2022-07-06 17:45:21 +02:00
Nex
3cdc6da428 Temporarily removed mvt-ios check-usb command 2022-07-06 13:01:55 +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
Niek van der Maas
067402831a Dockerfile improvements, support arm64 builds 2022-06-02 09:22:07 +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
Nex
c3acc95e9e Bumped version 2022-01-28 20:08:14 +01:00
Nex
90d05336da Added check for additional outgoing call event 2022-01-28 17:21:28 +01:00
Nex
5513e6e9e3 Ordered imports 2022-01-28 16:36:24 +01:00
Nex
38116f8405 Catching device not found exception 2022-01-28 15:47:50 +01:00
Nex
59b069f006 Added lookups for non-system packages on check-adb too 2022-01-28 12:25:50 +01:00
Nex
28e1348aa7 Added check-iocs command to mvt-android 2022-01-27 18:23:19 +01:00
Nex
034338d1f4 Added iOS 15.3 2022-01-27 17:04:48 +01:00
Nex
09d5eabf2f Changing check logic for Android settings 2022-01-27 15:24:17 +01:00
Nex
a425d6c511 Added missing comma and ordered imports 2022-01-27 14:56:02 +01:00
Nex
f8897a4f8c Added more dangerous settings 2022-01-27 14:54:31 +01:00
Nex
86eae68bdb Added Android settings module 2022-01-27 13:33:06 +01:00
Nex
d2bf348b03 Merge branch 'main' of github.com:mvt-project/mvt 2022-01-27 12:51:14 +01:00
Nex
25c6c03075 Added Getprop module and cleaned Files and Packages Android modules 2022-01-27 12:50:37 +01:00
tek
cf88740f6a Fixes bugs in SafariBrowserState module and add tests 2022-01-26 14:50:34 +01:00
tek
eb4810b0ad Fixes bug in parsing of configuration profiles 2022-01-25 20:32:27 +01:00
Nex
cce9159eda Adding indicator to matched results 2022-01-23 15:01:49 +01:00
354 changed files with 31559 additions and 6466 deletions

View File

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

23
.github/workflows/mypy.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Mypy
on: workflow_dispatch
jobs:
mypy_py3:
name: Mypy check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: 3.9
cache: 'pip'
- name: Checkout
uses: actions/checkout@master
- name: Install Dependencies
run: |
pip install mypy
- name: mypy
run: |
make mypy

View File

@@ -0,0 +1,61 @@
#
name: Create and publish a Docker image
# Configures this workflow to run every time a release is published.
on:
workflow_dispatch:
release:
types: [published]
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
attestations: write
id-token: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@@ -1,43 +0,0 @@
# 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
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# python-version: [3.7, 3.8, 3.9]
python-version: [3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest safety stix2
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
python -m pip install .
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Safety checks
run: safety check
- name: Test with pytest
run: pytest

27
.github/workflows/ruff.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Ruff
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
ruff_py3:
name: Ruff syntax check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: 3.9
cache: 'pip'
- name: Checkout
uses: actions/checkout@master
- name: Install Dependencies
run: |
pip install ruff
- name: ruff
run: |
make ruff

View File

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

38
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
name: Run Python Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.9', '3.10'] # , '3.11']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Python dependencies
run: |
make install
make test-requirements
- name: Test with pytest
run: |
set -o pipefail
make test-ci | tee pytest-coverage.txt
- name: Pytest coverage comment
continue-on-error: true # Workflows running on a fork can't post comments
uses: MishaKav/pytest-coverage-comment@main
if: github.event_name == 'pull_request'
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml

29
.github/workflows/update-ios-data.yml vendored Normal file
View File

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

8
.gitignore vendored
View File

@@ -50,6 +50,8 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
pytest-coverage.txt
pytest.xml
# Translations
*.mo
@@ -131,3 +133,9 @@ dmypy.json
# Temporal files
*~
# IDEA Dev Environment
.idea
# Sublime Text project files
*.sublime*

View File

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

11
.safety-policy.yml Normal file
View File

@@ -0,0 +1,11 @@
# Safety Security and License Configuration file
# We recommend checking this file into your source control in the root of your Python project
# If this file is named .safety-policy.yml and is in the same directory where you run `safety check` it will be used by default.
# Otherwise, you can use the flag `safety check --policy-file <path-to-this-file>` to specify a custom location and name for the file.
# To validate and review your policy file, run the validate command: `safety validate policy_file --path <path-to-this-file>`
security: # configuration for the `safety check` command
ignore-vulnerabilities: # Here you can list multiple specific vulnerabilities you want to ignore (optionally for a time period)
67599: # Example vulnerability ID
reason: disputed, inapplicable
70612:
reason: disputed, inapplicable

View File

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

65
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,65 @@
# Contributing to Mobile Verification Toolkit (MVT)
We greatly appreciate contributions to MVT!
Your involvement, whether through identifying issues, improving functionality, or enhancing documentation, is very much appreciated. To ensure smooth collaboration and a welcoming environment, we've outlined some key guidelines for contributing below.
## Getting started
Contributing to an open-source project like MVT might seem overwhelming at first, but we're here to support you!
Whether you're a technologist, a frontline human rights defender, a field researcher, or someone new to consensual spyware forensics, there are many ways to make meaningful contributions.
Here's how you can get started:
1. **Explore the codebase:**
- Browse the repository to get familar with MVT. Many MVT modules are simple in functionality and easy to understand.
- Look for `TODO:` or `FIXME:` comments in the code for areas that need attention.
2. **Check Github issues:**
- Look for issues tagged with ["help wanted"](https://github.com/mvt-project/mvt/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or ["good first issue"](https://github.com/mvt-project/mvt/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to find tasks that are beginner-friendly or where input from the community would be helpful.
3. **Ask for guidance:**
- If you're unsure where to start, feel free to open a [discussion](https://github.com/mvt-project/mvt/discussions) or comment on an issue.
## How to contribute:
1. **Report issues:**
- Found a bug? Please check existing issues to see if it's already reported. If not, open a new issue. Mobile operating systems and databases are constantly evolving, an new errors may appear spontaniously in new app versions.
**Please provide as much information as possible about the prodblem including: any error messages, steps to reproduce the problem, and any logs or screenshots that can help.**
2. **Suggest features:**
- If you have an idea for new functionality, create a feature request issue and describe your proposal.
3. **Submit code:**
- Fork the repository and create a new branch for your changes.
- Ensure your changes align with the code style guidelines (see below).
- Open a pull request (PR) with a clear description of your changes and link it to any relevant issues.
4. **Documentation contributions:**
- Improving documentation is just as valuable as contributing code! If you notice gaps or inaccuracies in the documentation, feel free to submit changes or suggest updates.
## Code style
Please follow these code style guidelines for consistency and readability:
- **Indentation**: use 4 spaces per tab.
- **Quotes**: Use double quotes (`"`) by default. Use single quotes (`'`) for nested strings instead of escaping (`\"`), or when using f-formatting.
- **Maximum line length**:
- Aim for lines no longer than 80 characters.
- Exceptions are allowed for long log lines or strings, which may extend up to 100 characters.
- Wrap lines that exceed 100 characters.
Follow [PEP 8 guidelines](https://peps.python.org/pep-0008/) for indentation and overall Python code style. All MVT code is automatically linted with [Ruff](https://github.com/astral-sh/ruff) before merging.
Please check your code before opening a pull request by running `make ruff`
## Community and support
We aim to create a supportive and collaborative environment for all contributors. If you run into any challenges, feel free to reach out through the discussions or issues section of the repository.
Your contributions, big or small, help improve MVT and are always appreciated.

View File

@@ -1,89 +1,159 @@
FROM ubuntu:20.04
# Base image for building libraries
# ---------------------------------
FROM ubuntu:22.04 as build-base
# Ref. https://github.com/mvt-project/mvt
ARG DEBIAN_FRONTEND=noninteractive
LABEL url="https://mvt.re"
LABEL vcs-url="https://github.com/mvt-project/mvt"
LABEL description="MVT is a forensic tool to look for signs of infection in smartphone devices."
ENV PIP_NO_CACHE_DIR=1
# Fixing major OS dependencies
# ----------------------------
RUN apt update \
&& apt install -y python3 python3-pip libusb-1.0-0-dev \
&& apt install -y wget unzip\
&& DEBIAN_FRONTEND=noninteractive apt-get -y install default-jre-headless \
# Install build tools for libimobiledevice
# ----------------------------------------
# Install build tools and dependencies
RUN apt-get update \
&& apt-get install -y \
build-essential \
checkinstall \
git \
autoconf \
automake \
libtool-bin \
libplist-dev \
libusbmuxd-dev \
libssl-dev \
sqlite3 \
pkg-config \
libcurl4-openssl-dev \
libusb-1.0-0-dev \
libssl-dev \
udev \
&& rm -rf /var/lib/apt/lists/*
# Clean up
# libplist
# --------
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt
FROM build-base as build-libplist
# Build
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libplist
# Build libimobiledevice
# ----------------------
RUN git clone https://github.com/libimobiledevice/libplist \
&& git clone https://github.com/libimobiledevice/libimobiledevice-glue \
&& git clone https://github.com/libimobiledevice/libusbmuxd \
&& git clone https://github.com/libimobiledevice/libimobiledevice \
&& git clone https://github.com/libimobiledevice/usbmuxd \
# libimobiledevice-glue
# ---------------------
FROM build-base as build-libimobiledevice-glue
&& cd libplist && ./autogen.sh && make && make install && ldconfig \
# Install dependencies
COPY --from=build-libplist /build /
&& cd ../libimobiledevice-glue && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr && make && make install && ldconfig \
# Build
RUN git clone https://github.com/libimobiledevice/libimobiledevice-glue && cd libimobiledevice-glue \
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libimobiledevice-glue
&& cd ../libusbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh && make && make install && ldconfig \
&& cd ../libimobiledevice && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --enable-debug && make && make install && ldconfig \
# libtatsu
# --------
FROM build-base as build-libtatsu
&& cd ../usbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make && make install \
# Install dependencies
COPY --from=build-libplist /build /
# Clean up.
&& cd .. && rm -rf libplist libimobiledevice-glue libusbmuxd libimobiledevice usbmuxd
# Build
RUN git clone https://github.com/libimobiledevice/libtatsu && cd libtatsu \
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libtatsu
# Installing MVT
# --------------
RUN pip3 install mvt
# libusbmuxd
# ----------
FROM build-base as build-libusbmuxd
# Install dependencies
COPY --from=build-libplist /build /
COPY --from=build-libimobiledevice-glue /build /
# Build
RUN git clone https://github.com/libimobiledevice/libusbmuxd && cd libusbmuxd \
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libusbmuxd
# libimobiledevice
# ----------------
FROM build-base as build-libimobiledevice
# Install dependencies
COPY --from=build-libplist /build /
COPY --from=build-libtatsu /build /
COPY --from=build-libimobiledevice-glue /build /
COPY --from=build-libusbmuxd /build /
# Build
RUN git clone https://github.com/libimobiledevice/libimobiledevice && cd libimobiledevice \
&& ./autogen.sh --enable-debug && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libimobiledevice
# usbmuxd
# -------
FROM build-base as build-usbmuxd
# Install dependencies
COPY --from=build-libplist /build /
COPY --from=build-libimobiledevice-glue /build /
COPY --from=build-libusbmuxd /build /
COPY --from=build-libimobiledevice /build /
# Build
RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
&& ./autogen.sh --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf usbmuxd && mv /build/lib /build/usr/lib
# Create main image
FROM ubuntu:22.04 as main
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
LABEL org.opencontainers.image.source="https://github.com/mvt-project/mvt"
LABEL org.opencontainers.image.title="Mobile Verification Toolkit"
LABEL org.opencontainers.image.description="MVT is a forensic tool to look for signs of infection in smartphone devices."
LABEL org.opencontainers.image.licenses="MVT License 1.1"
LABEL org.opencontainers.image.base.name=docker.io/library/ubuntu:22.04
# Install runtime dependencies
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y \
adb \
default-jre-headless \
libcurl4 \
libssl3 \
libusb-1.0-0 \
python3 \
sqlite3
COPY --from=build-libplist /build /
COPY --from=build-libimobiledevice-glue /build /
COPY --from=build-libtatsu /build /
COPY --from=build-libusbmuxd /build /
COPY --from=build-libimobiledevice /build /
COPY --from=build-usbmuxd /build /
# Install mvt using the locally checked out source
COPY . mvt/
RUN apt-get update \
&& apt-get install -y git python3-pip \
&& PIP_NO_CACHE_DIR=1 pip3 install --upgrade pip \
&& PIP_NO_CACHE_DIR=1 pip3 install ./mvt \
&& apt-get remove -y python3-pip git && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf mvt
# Installing ABE
# --------------
RUN mkdir /opt/abe \
&& wget https://github.com/nelenkov/android-backup-extractor/releases/download/20210709062403-4c55371/abe.jar -O /opt/abe/abe.jar \
ADD --checksum=sha256:a20e07f8b2ea47620aff0267f230c3f1f495f097081fd709eec51cf2a2e11632 \
https://github.com/nelenkov/android-backup-extractor/releases/download/master-20221109063121-8fdfc5e/abe.jar /opt/abe/abe.jar
# Create alias for abe
&& echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
RUN echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
# Install Android Platform Tools
# ------------------------------
RUN mkdir /opt/android \
&& wget -q https://dl.google.com/android/repository/platform-tools-latest-linux.zip \
&& unzip platform-tools-latest-linux.zip -d /opt/android \
# Create alias for adb
&& echo 'alias adb="/opt/android/platform-tools/adb"' >> ~/.bashrc
# Generate adb key folder
# ------------------------------
RUN mkdir /root/.android && /opt/android/platform-tools/adb keygen /root/.android/adbkey
# Generate adb key folder
RUN echo 'if [ ! -f /root/.android/adbkey ]; then adb keygen /root/.android/adbkey 2&>1 > /dev/null; fi' >> ~/.bashrc
RUN mkdir /root/.android
# Setup investigations environment
# --------------------------------
RUN mkdir /home/cases
WORKDIR /home/cases
WORKDIR /home/cases
RUN echo 'echo "Mobile Verification Toolkit @ Docker\n------------------------------------\n\nYou can find information about how to use this image for Android (https://github.com/mvt-project/mvt/tree/master/docs/android) and iOS (https://github.com/mvt-project/mvt/tree/master/docs/ios) in the official docs of the project.\n"' >> ~/.bashrc \
&& echo 'echo "Note that to perform the debug via USB you might need to give the Docker image access to the USB using \"docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt\" or, preferably, the \"--device=\" parameter.\n"' >> ~/.bashrc

36
Dockerfile.android Normal file
View File

@@ -0,0 +1,36 @@
# Create main image
FROM python:3.10.14-alpine3.20 as main
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
LABEL org.opencontainers.image.source="https://github.com/mvt-project/mvt"
LABEL org.opencontainers.image.title="Mobile Verification Toolkit (Android)"
LABEL org.opencontainers.image.description="MVT is a forensic tool to look for signs of infection in smartphone devices."
LABEL org.opencontainers.image.licenses="MVT License 1.1"
LABEL org.opencontainers.image.base.name=docker.io/library/python:3.10.14-alpine3.20
# Install runtime dependencies
RUN apk add --no-cache \
android-tools \
git \
libusb \
openjdk11-jre-headless \
sqlite
# Install mvt
COPY ./ mvt
RUN apk add --no-cache --virtual .build-deps gcc musl-dev \
&& PIP_NO_CACHE_DIR=1 pip3 install ./mvt \
&& apk del .build-deps gcc musl-dev && rm -rf ./mvt
# Installing ABE
ADD --checksum=sha256:a20e07f8b2ea47620aff0267f230c3f1f495f097081fd709eec51cf2a2e11632 \
https://github.com/nelenkov/android-backup-extractor/releases/download/master-20221109063121-8fdfc5e/abe.jar /opt/abe/abe.jar
# Create alias for abe
RUN echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
# Generate adb key folder
RUN echo 'if [ ! -f /root/.android/adbkey ]; then adb keygen /root/.android/adbkey 2&>1 > /dev/null; fi' >> ~/.bashrc
RUN mkdir /root/.android
ENTRYPOINT [ "/usr/local/bin/mvt-android" ]

137
Dockerfile.ios Normal file
View File

@@ -0,0 +1,137 @@
# Base image for building libraries
# ---------------------------------
FROM ubuntu:22.04 as build-base
ARG DEBIAN_FRONTEND=noninteractive
# Install build tools and dependencies
RUN apt-get update \
&& apt-get install -y \
build-essential \
git \
autoconf \
automake \
libtool-bin \
pkg-config \
libcurl4-openssl-dev \
libusb-1.0-0-dev \
libssl-dev \
udev \
&& rm -rf /var/lib/apt/lists/*
# libplist
# --------
FROM build-base as build-libplist
# Build
RUN git clone https://github.com/libimobiledevice/libplist && cd libplist \
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libplist
# libimobiledevice-glue
# ---------------------
FROM build-base as build-libimobiledevice-glue
# Install dependencies
COPY --from=build-libplist /build /
# Build
RUN git clone https://github.com/libimobiledevice/libimobiledevice-glue && cd libimobiledevice-glue \
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libimobiledevice-glue
# libtatsu
# --------
FROM build-base as build-libtatsu
# Install dependencies
COPY --from=build-libplist /build /
# Build
RUN git clone https://github.com/libimobiledevice/libtatsu && cd libtatsu \
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libtatsu
# libusbmuxd
# ----------
FROM build-base as build-libusbmuxd
# Install dependencies
COPY --from=build-libplist /build /
COPY --from=build-libimobiledevice-glue /build /
# Build
RUN git clone https://github.com/libimobiledevice/libusbmuxd && cd libusbmuxd \
&& ./autogen.sh && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libusbmuxd
# libimobiledevice
# ----------------
FROM build-base as build-libimobiledevice
# Install dependencies
COPY --from=build-libplist /build /
COPY --from=build-libtatsu /build /
COPY --from=build-libimobiledevice-glue /build /
COPY --from=build-libusbmuxd /build /
# Build
RUN git clone https://github.com/libimobiledevice/libimobiledevice && cd libimobiledevice \
&& ./autogen.sh --enable-debug && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf libimobiledevice
# usbmuxd
# -------
FROM build-base as build-usbmuxd
# Install dependencies
COPY --from=build-libplist /build /
COPY --from=build-libimobiledevice-glue /build /
COPY --from=build-libusbmuxd /build /
COPY --from=build-libimobiledevice /build /
# Build
RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
&& ./autogen.sh --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make -j "$(nproc)" && make install DESTDIR=/build \
&& cd .. && rm -rf usbmuxd && mv /build/lib /build/usr/lib
# Main image
# ----------
FROM python:3.10.14-alpine3.20 as main
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
LABEL org.opencontainers.image.source="https://github.com/mvt-project/mvt"
LABEL org.opencontainers.image.title="Mobile Verification Toolkit (iOS)"
LABEL org.opencontainers.image.description="MVT is a forensic tool to look for signs of infection in smartphone devices."
LABEL org.opencontainers.image.licenses="MVT License 1.1"
LABEL org.opencontainers.image.base.name=docker.io/library/python:3.10.14-alpine3.20
# Install runtime dependencies
RUN apk add --no-cache \
gcompat \
libcurl \
libssl3 \
libusb \
sqlite
COPY --from=build-libplist /build /
COPY --from=build-libimobiledevice-glue /build /
COPY --from=build-libtatsu /build /
COPY --from=build-libusbmuxd /build /
COPY --from=build-libimobiledevice /build /
COPY --from=build-usbmuxd /build /
# Install mvt using the locally checked out source
COPY ./ mvt
RUN apk add --no-cache --virtual .build-deps git gcc musl-dev \
&& PIP_NO_CACHE_DIR=1 pip3 install ./mvt \
&& apk del .build-deps git gcc musl-dev && rm -rf ./mvt
ENTRYPOINT [ "/usr/local/bin/mvt-ios" ]

View File

@@ -1,10 +1,44 @@
PWD = $(shell pwd)
autofix:
ruff format .
ruff check --fix .
check: ruff mypy
ruff:
ruff format --check .
ruff check -q .
mypy:
mypy
test:
python3 -m pytest
test-ci:
python3 -m pytest -v
install:
python3 -m pip install --upgrade -e .
test-requirements:
python3 -m pip install --upgrade -r test-requirements.txt
generate-proto-parsers:
# Generate python parsers for protobuf files
PROTO_FILES=$$(find src/mvt/android/parsers/proto/ -iname "*.proto"); \
protoc -Isrc/mvt/android/parsers/proto/ --python_betterproto_out=src/mvt/android/parsers/proto/ $$PROTO_FILES
clean:
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/mvt.egg-info
rm -rf $(PWD)/build $(PWD)/dist $(PWD)/src/mvt.egg-info
dist:
python3 setup.py sdist bdist_wheel
python3 -m pip install --upgrade build
python3 -m build
upload:
python3 -m twine upload dist/*
test-upload:
python3 -m twine upload --repository testpypi dist/*

View File

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

5
SECURITY.md Normal file
View File

@@ -0,0 +1,5 @@
# Reporting security issues
Thank you for your interest in reporting security issues and vulnerabilities! Security research is of utmost importance and we take all reports seriously. If you discover an issue please report it to us right away!
Please DO NOT file a public issue, instead send your report privately to *nex [at] nex [dot] sx*. You can also write PGP-encrypted emails to [this key](https://keybase.io/nex/pgp_keys.asc?fingerprint=05216f3b86848a303c2fe37dd166f1667359d880).

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env python3
# 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 os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mvt import android
android.cli()

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env python3
# 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 os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mvt import ios
ios.cli()

View File

@@ -11,18 +11,41 @@ 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 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
```
If the backup is encrypted, MVT will prompt you to enter the password. A backup password can also be provided with the `--backup-password` command line option or through the `MVT_ANDROID_BACKUP_PASSWORD` environment variable. The same options can also be used to when analysing an encrypted backup collected through AndroidQF in the `mvt-android check-androidqf` command:
```bash
$ mvt-android check-backup --backup-password "password123" --output /path/to/results/ /path/to/backup.ab
```
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 +56,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 with MVT by passing the folder path as parameter instead of the `.ab` file: `mvt-android check-backup --output /path/to/results/ /path/to/backup/` (the path to backup given should be the folder containing the `apps` folder).

View File

@@ -1,6 +1,6 @@
# Downloading APKs from an Android phone
MVT allows to attempt to download all available installed packages (APKs) in order to further inspect them and potentially identify any which might be malicious in nature.
MVT allows you to attempt to download all available installed packages (APKs) from a device in order to further inspect them and potentially identify any which might be malicious in nature.
You can do so by launching the following command:
@@ -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

@@ -0,0 +1,43 @@
# Command Completion
MVT utilizes the [Click](https://click.palletsprojects.com/en/stable/) library for creating its command line interface.
Click provides tab completion support for Bash (version 4.4 and up), Zsh, and Fish.
To enable it, you need to manually register a special function with your shell, which varies depending on the shell you are using.
The following describes how to generate the command completion scripts and add them to your shell configuration.
> **Note: You will need to start a new shell for the changes to take effect.**
### For Bash
```bash
# Generates bash completion scripts
echo "$(_MVT_IOS_COMPLETE=bash_source mvt-ios)" > ~/.mvt-ios-complete.bash &&
echo "$(_MVT_ANDROID_COMPLETE=bash_source mvt-android)" > ~/.mvt-android-complete.bash
```
Add the following to `~/.bashrc`:
```bash
# source mvt completion scripts
. ~/.mvt-ios-complete.bash && . ~/.mvt-android-complete.bash
```
### For Zsh
```bash
# Generates zsh completion scripts
echo "$(_MVT_IOS_COMPLETE=zsh_source mvt-ios)" > ~/.mvt-ios-complete.zsh &&
echo "$(_MVT_ANDROID_COMPLETE=zsh_source mvt-android)" > ~/.mvt-android-complete.zsh
```
Add the following to `~/.zshrc`:
```bash
# source mvt completion scripts
. ~/.mvt-ios-complete.zsh && . ~/.mvt-android-complete.zsh
```
For more information, visit the official [Click Docs](https://click.palletsprojects.com/en/stable/shell-completion/#enabling-completion).

27
docs/development.md Normal file
View File

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

View File

@@ -1,8 +1,23 @@
Using Docker simplifies having all the required dependencies and tools (including most recent versions of [libimobiledevice](https://libimobiledevice.org)) readily installed.
Using Docker simplifies having all the required dependencies and tools (including most recent versions of [libimobiledevice](https://libimobiledevice.org)) readily installed. Note that this requires a Linux host, as Docker for Windows and Mac [doesn't support passing through USB devices](https://docs.docker.com/desktop/faqs/#can-i-pass-through-a-usb-device-to-a-container).
Install Docker following the [official documentation](https://docs.docker.com/get-docker/).
Once installed, you can clone MVT's repository and build its Docker image:
Once Docker is installed, you can run MVT by downloading a prebuilt MVT Docker image, or by building a Docker image yourself from the MVT source repo.
### Using the prebuilt Docker image
```bash
docker pull ghcr.io/mvt-project/mvt
```
You can then run the Docker container with:
```
docker run -it ghcr.io/mvt-project/mvt
```
### Build and run Docker image from source
```bash
git clone https://github.com/mvt-project/mvt.git
@@ -18,6 +33,9 @@ docker run -it mvt
If a prompt is spawned successfully, you can close it with `exit`.
## Docker usage with Android devices
If you wish to use MVT to test an Android device you will need to enable the container's access to the host's USB devices. You can do so by enabling the `--privileged` flag and mounting the USB bus device as a volume:
```bash

BIN
docs/img/macos-backup2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

BIN
docs/img/macos-backups.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View File

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

View File

@@ -7,11 +7,27 @@ Before proceeding, please note that MVT requires Python 3.6+ to run. While it sh
First install some basic dependencies that will be necessary to build all required tools:
```bash
sudo apt install python3 python3-pip libusb-1.0-0 sqlite3
sudo apt install python3 python3-venv python3-pip sqlite3 libusb-1.0-0
```
*libusb-1.0-0* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
(Recommended) Set up `pipx`
For Ubuntu 23.04 or above:
```bash
sudo apt install pipx
pipx ensurepath
```
For Ubuntu 22.04 or below:
```
python3 -m pip install --user pipx
python3 -m pipx ensurepath
```
Other distributions: check for a `pipx` or `python-pipx` via your package manager.
When working with Android devices you should additionally install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you prefer to install a package made available by your distribution of choice, please make sure the version is recent to ensure compatibility with modern Android devices.
## Dependencies on macOS
@@ -21,7 +37,7 @@ Running MVT on macOS requires Xcode and [homebrew](https://brew.sh) to be instal
In order to install dependencies use:
```bash
brew install python3 libusb sqlite3
brew install python3 pipx libusb sqlite3
```
*libusb* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
@@ -42,24 +58,47 @@ It is recommended to try installing and running MVT from [Windows Subsystem Linu
## Installing MVT
If you haven't done so, you can add this to your `.bashrc` or `.zshrc` file in order to add locally installed Pypi binaries to your `$PATH`:
### Installing from PyPI with pipx (recommended)
1. Install `pipx` following the instructions above for your OS/distribution. Make sure to run `pipx ensurepath` and open a new terminal window.
2. ```bash
pipx install mvt
```
You now should have the `mvt-ios` and `mvt-android` utilities installed. If you run into problems with these commands not being found, ensure you have run `pipx ensurepath` and opened a new terminal window.
### Installing from PyPI directly into a virtual environment
You can use `pipenv`, `poetry` etc. for your virtual environment, but the provided example is with the built-in `venv` tool:
1. Create the virtual environment in a folder in the current directory named `env`:
```bash
export PATH=$PATH:~/.local/bin
python3 -m venv env
```
Then you can install MVT directly from [pypi](https://pypi.org/project/mvt/)
2. Activate the virtual environment:
```bash
pip3 install mvt
source env/bin/activate
```
Or from the source code:
3. Install `mvt` into the virtual environment:
```bash
pip install mvt
```
The `mvt-ios` and `mvt-android` utilities should now be available as commands whenever the virtual environment is active.
### Installing from git source with pipx
If you want to have the latest features in development, you can install MVT directly from the source code in git.
```bash
git clone https://github.com/mvt-project/mvt.git
cd mvt
pip3 install .
pipx install --force git+https://github.com/mvt-project/mvt.git
```
You now should have the `mvt-ios` and `mvt-android` utilities installed.
**Notes:**
1. The `--force` flag is necessary to force the reinstallation of the package.
2. To revert to using a PyPI version, it will be necessary to `pipx uninstall mvt` first.
## Setting up command completions
See ["Command completions"](command_completion.md)

View File

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

View File

@@ -34,13 +34,25 @@ It is also possible to load STIX2 files automatically from the environment varia
export MVT_STIX2="/home/user/IOC1.stix2:/home/user/IOC2.stix2"
```
## STIX2 Support
So far MVT implements only a subset of [STIX2 specifications](https://docs.oasis-open.org/cti/stix/v2.1/csprd01/stix-v2.1-csprd01.html):
* It only supports checks for one value (such as `[domain-name:value='DOMAIN']`) and not boolean expressions over multiple comparisons
* It only supports the following types: `domain-name:value`, `process:name`, `email-addr:value`, `file:name`, `file:path`, `file:hashes.md5`, `file:hashes.sha1`, `file:hashes.sha256`, `app:id`, `configuration-profile:id`, `android-property:name`, `url:value` (but each type will only be checked by a module if it is relevant to the type of data obtained)
## Known repositories of STIX2 IOCs
- The [Amnesty International investigations repository](https://github.com/AmnestyTech/investigations) contains STIX-formatted IOCs for:
- [Pegasus](https://en.wikipedia.org/wiki/Pegasus_(spyware)) ([STIX2](https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-07-18_nso/pegasus.stix2))
- [Predator from Cytrox](https://citizenlab.ca/2021/12/pegasus-vs-predator-dissidents-doubly-infected-iphone-reveals-cytrox-mercenary-spyware/) ([STIX2](https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-12-16_cytrox/cytrox.stix2))
- [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).
- [An Android Spyware Campaign Linked to a Mercenary Company](https://github.com/AmnestyTech/investigations/tree/master/2023-03-29_android_campaign) ([STIX2](https://github.com/AmnestyTech/investigations/blob/master/2023-03-29_android_campaign/malware.stix2))
- [This repository](https://github.com/Te-k/stalkerware-indicators) contains IOCs for Android stalkerware including [a STIX MVT-compatible file](https://raw.githubusercontent.com/Te-k/stalkerware-indicators/master/generated/stalkerware.stix2).
- We are also maintaining [a list of IOCs](https://github.com/mvt-project/mvt-indicators) in STIX format from public spyware campaigns.
You can automaticallly download the latest public indicator files with the command `mvt-ios download-iocs` or `mvt-android download-iocs`. These commands download the list of indicators 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 from the [mvt-indicators](https://github.com/mvt-project/mvt-indicators/blob/main/indicators.yaml) repository and store them in the [appdir](https://pypi.org/project/appdirs/) folder. They are then loaded automatically by MVT.
Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs.

View File

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

View File

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

View File

@@ -45,10 +45,10 @@ Once the idevice tools are available you can check if everything works fine by c
ideviceinfo
```
This should some many details on the connected iOS device. If you are connecting the device to your laptop for the first time, it will require to unlock and enter the PIN code on the mobile device. If it complains that no device is connected and the mobile device is indeed plugged in through the USB cable, you might need to do this first, although typically the pairing is automatically done when connecting the device:
This should show many details on the connected iOS device. If you are connecting the device to your laptop for the first time, it will require to unlock and enter the PIN code on the mobile device. If it complains that no device is connected and the mobile device is indeed plugged in through the USB cable, you might need to do this first, although typically the pairing is automatically done when connecting the device:
```bash
sudo usbmuxd -f -d
sudo usbmuxd -f -v
idevicepair pair
```

View File

@@ -16,9 +16,21 @@ If indicators are provided through the command-line, processes and domains are c
---
### `applications.json`
!!! info "Availability"
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Applications` module. The module extracts the list of applications installed on the device from the `Info.plist` file in backup, or from the `iTunesMetadata.plist` files in a file system dump. These records contains detailed information on the source and installation of the app.
If indicators are provided through the command-line, processes and application ids are checked against the app name of each application. It also flags any applications not installed from the AppStore. Any matches are stored in *applications_detected.json*.
---
### `backup_info.json`
!!! info "Availabiliy"
!!! info "Availability"
Backup: :material-check:
Full filesystem dump: :material-close:
@@ -38,6 +50,18 @@ If indicators are provided through the command-line, they are checked against th
---
### `calendar.json`
!!! info "Availability"
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Calendar` module. This module extracts all CalendarItems from the `Calendar.sqlitedb` database. This database contains all calendar entries from the different calendars installed on the phone.
If indicators are provided through the command-line, email addresses are checked against the inviter's email of the different events. Any matches are stored in *calendar_detected.json*.
---
### `calls.json`
!!! info "Availability"
@@ -118,6 +142,16 @@ If indicators are provided through the command-line, they are checked against th
---
### `global_preferences.json`
!!! info "Availability"
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `GlobalPreferences` module. The module extracts records from a Plist file located at */private/var/mobile/Library/Preferences/.GlobalPreferences.plist*, which contains a system preferences including if Lockdown Mode is enabled.
---
### `id_status_cache.json`
!!! info "Availability"
@@ -190,7 +224,7 @@ If indicators are provided through the command-line, they are checked against th
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.
@@ -272,7 +306,7 @@ If indicators are provided through the command-line, they are checked against th
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*.
This JSON file is created by mvt-ios' `SMS` module. The module extracts a list of SMS messages from the SQLite database located at */private/var/mobile/Library/SMS/sms.db*.
If indicators are provided through the command-line, they are checked against the extracted HTTP links. Any matches are stored in *sms_detected.json*.
@@ -374,7 +408,7 @@ If indicators are provided through the command-line, they are checked against th
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*.
This JSON file is created by mvt-ios' `WhatsApp` module. The module extracts a list of WhatsApp messages from the SQLite database located at *private/var/mobile/Containers/Shared/AppGroup/\*/ChatStorage.sqlite*.
If indicators are provided through the command-line, they are checked against the extracted HTTP links. Any matches are stored in *whatsapp_detected.json*.

View File

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

View File

@@ -1,14 +1,14 @@
site_name: Mobile Verification Toolkit
repo_url: https://github.com/mvt-project/mvt
edit_uri: edit/main/docs/
copyright: Copyright &copy; 2021 MVT Project Developers
copyright: Copyright &copy; 2021-2023 MVT Project Developers
site_description: Mobile Verification Toolkit Documentation
markdown_extensions:
- attr_list
- admonition
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.superfences
- pymdownx.inlinehilite
- pymdownx.highlight:
@@ -46,4 +46,5 @@ nav:
- Check an Android Backup (SMS messages): "android/backup.md"
- Download APKs: "android/download_apks.md"
- Indicators of Compromise: "iocs.md"
- Development: "development.md"
- License: "license.md"

View File

@@ -1,207 +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
import click
from rich.logging import RichHandler
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 .download_apks import DownloadAPKs
from .lookups.koodous import koodous_lookup
from .lookups.virustotal import virustotal_lookup
from .modules.adb import ADB_MODULES
from .modules.backup import BACKUP_MODULES
# Setup logging using Rich.
LOG_FORMAT = "[%(name)s] %(message)s"
logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__)
#==============================================================================
# Main
#==============================================================================
@click.group(invoke_without_command=False)
def cli():
logo()
#==============================================================================
# Command: version
#==============================================================================
@cli.command("version", help="Show the currently installed version of MVT")
def version():
return
#==============================================================================
# Download APKs
#==============================================================================
@cli.command("download-apks", help="Download all or non-safelisted installed APKs installed on the device")
@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):
try:
if from_file:
download = DownloadAPKs.from_json(from_file)
else:
# TODO: Do we actually want to be able to run without storing any file?
if not output:
log.critical("You need to specify an output folder with --output!")
ctx.exit(1)
if not os.path.exists(output):
try:
os.makedirs(output)
except Exception as e:
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
download = DownloadAPKs(output_folder=output, all_apks=all_apks,
log=logging.getLogger(DownloadAPKs.__module__))
if serial:
download.serial = serial
download.run()
packages = download.packages
if len(packages) == 0:
return
if virustotal or all_checks:
virustotal_lookup(packages)
if koodous or all_checks:
koodous_lookup(packages)
except KeyboardInterrupt:
print("")
ctx.exit(1)
#==============================================================================
# Checks through ADB
#==============================================================================
@cli.command("check-adb", help="Check an Android device over adb")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False),
help=HELP_MSG_OUTPUT)
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.pass_context
def check_adb(ctx, 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__)
return
log.info("Checking Android through adb 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)
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"))
#==============================================================================
# Check ADB 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.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)
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)
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
if os.path.isfile(backup_path):
log.critical("The path you specified is a not a folder!")
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)
#==============================================================================
# Command: download-iocs
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
def download_indicators():
download_indicators_files(log)

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,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,24 +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/
from .chrome_history import ChromeHistory
from .dumpsys_accessibility import DumpsysAccessibility
from .dumpsys_batterystats import DumpsysBatterystats
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 .logcat import Logcat
from .packages import Packages
from .processes import Processes
from .rootbinaries import RootBinaries
from .sms import SMS
from .whatsapp import Whatsapp
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes,
DumpsysAccessibility, DumpsysBatterystats, DumpsysProcstats,
DumpsysPackages, DumpsysReceivers, DumpsysFull,
Packages, RootBinaries, Logcat, Files]

View File

@@ -1,244 +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
import random
import string
import sys
import tempfile
import time
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,
UsbReadFailedError)
from usb1 import USBErrorAccess, USBErrorBusy
from mvt.common.module import InsufficientPrivileges, MVTModule
log = logging.getLogger(__name__)
ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey")
ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub")
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,
log=log, results=results)
self.device = None
self.serial = None
@staticmethod
def _adb_check_keys():
"""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))
if not os.path.exists(ADB_KEY_PATH):
keygen(ADB_KEY_PATH)
if not os.path.exists(ADB_PUB_KEY_PATH):
write_public_keyfile(ADB_KEY_PATH, ADB_PUB_KEY_PATH)
def _adb_connect(self):
"""Connect to the device over adb."""
self._adb_check_keys()
with open(ADB_KEY_PATH, "rb") as handle:
priv_key = handle.read()
with open(ADB_PUB_KEY_PATH, "rb") as handle:
pub_key = handle.read()
signer = PythonRSASigner(pub_key, priv_key)
# If no serial was specified or if the serial does not seem to be
# a HOST:PORT definition, we use the USB transport.
if not self.serial or ":" not in self.serial:
self.device = AdbDeviceUsb(serial=self.serial)
# Otherwise we try to use the TCP transport.
else:
addr = self.serial.split(":")
if len(addr) < 2:
raise ValueError("TCP serial number must follow the format: `address:port`")
self.device = AdbDeviceTcp(addr[0], int(addr[1]),
default_transport_timeout_s=30.)
while True:
try:
self.device.connect(rsa_keys=[signer], auth_timeout_s=5)
except (USBErrorBusy, USBErrorAccess):
log.critical("Device is busy, maybe run `adb kill-server` and try again.")
sys.exit(-1)
except DeviceAuthError:
log.error("You need to authorize this computer on the Android device. Retrying in 5 seconds...")
time.sleep(5)
except UsbReadFailedError:
log.error("Unable to connect to the device over USB. Try to unplug, plug the device and start again.")
sys.exit(-1)
except OSError as e:
if e.errno == 113 and self.serial:
log.critical("Unable to connect to the device %s: did you specify the correct IP addres?",
self.serial)
sys.exit(-1)
else:
break
def _adb_disconnect(self):
"""Close adb connection to the device."""
self.device.close()
def _adb_reconnect(self):
"""Reconnect to device using adb."""
log.info("Reconnecting ...")
self._adb_disconnect()
self._adb_connect()
def _adb_command(self, command):
"""Execute an adb shell command.
:param command: Shell command to execute
:returns: Output of command
"""
return self.device.shell(command, read_timeout_s=200.0)
def _adb_check_if_root(self):
"""Check if we have a `su` binary on the Android device.
:returns: Boolean indicating whether a `su` binary is present or not
"""
return bool(self._adb_command("command -v su"))
def _adb_root_or_die(self):
"""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!")
def _adb_command_as_root(self, command):
"""Execute an adb shell command.
:param command: Shell command to execute as root
:returns: Output of command
"""
return self._adb_command(f"su -c {command}")
def _adb_check_file_exists(self, file):
"""Verify that a file exists.
:param file: Path of the file
:returns: Boolean indicating whether the file exists or not
"""
# TODO: Need to support checking files without root privileges as well.
# Connect to the device over adb.
self._adb_connect()
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
return bool(self._adb_command_as_root(f"[ ! -f {file} ] || echo 1"))
def _adb_download(self, remote_path, local_path, progress_callback=None, retry_root=True):
"""Download a file form the device.
:param remote_path: Path to download from the device
:param local_path: Path to where to locally store the copy of the file
:param progress_callback: Callback for download progress bar (Default value = None)
:param retry_root: Default value = True)
"""
try:
self.device.pull(remote_path, local_path, progress_callback)
except AdbCommandFailureException as e:
if retry_root:
self._adb_download_root(remote_path, local_path, progress_callback)
else:
raise Exception(f"Unable to download file {remote_path}: {e}")
def _adb_download_root(self, remote_path, local_path, progress_callback=None):
try:
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
# We generate a random temporary filename.
tmp_filename = "tmp_" + ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=10))
# We create a temporary local file.
new_remote_path = f"/sdcard/{tmp_filename}"
# We copy the file from the data folder to /sdcard/.
cp = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if cp.startswith("cp: ") and "No such file or directory" in cp:
raise Exception(f"Unable to process file {remote_path}: File not found")
elif cp.startswith("cp: ") and "Permission denied" in cp:
raise Exception(f"Unable to process file {remote_path}: Permission denied")
# We download from /sdcard/ to the local temporary file.
# If it doesn't work now, don't try again (retry_root=False)
self._adb_download(new_remote_path, local_path, retry_root=False)
# Delete the copy on /sdcard/.
self._adb_command(f"rm -rf {new_remote_path}")
except AdbCommandFailureException as e:
raise Exception(f"Unable to download file {remote_path}: {e}")
def _adb_process_file(self, remote_path, process_routine):
"""Download a local copy of a file which is only accessible as root.
This is a wrapper around process_routine.
:param remote_path: Path of the file on the device to process
:param process_routine: Function to be called on the local copy of the
downloaded file
"""
# Connect to the device over adb.
self._adb_connect()
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
# We create a temporary local file.
tmp = tempfile.NamedTemporaryFile()
local_path = tmp.name
local_name = os.path.basename(tmp.name)
new_remote_path = f"/sdcard/Download/{local_name}"
# We copy the file from the data folder to /sdcard/.
cp = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if cp.startswith("cp: ") and "No such file or directory" in cp:
raise Exception(f"Unable to process file {remote_path}: File not found")
elif cp.startswith("cp: ") and "Permission denied" in cp:
raise Exception(f"Unable to process file {remote_path}: Permission denied")
# We download from /sdcard/ to the local temporary file.
self._adb_download(new_remote_path, local_path)
# Launch the provided process routine!
process_routine(local_path)
# Delete the local copy.
tmp.close()
# Delete the copy on /sdcard/.
self._adb_command(f"rm -f {new_remote_path}")
# Disconnect from the device.
self._adb_disconnect()
def run(self):
"""Run the main procedure."""
raise NotImplementedError

View File

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

View File

@@ -1,53 +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 io
import logging
import os
from .base import AndroidExtraction
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,
log=log, results=results)
def run(self):
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)
self._adb_disconnect()

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

@@ -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 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,
log=log, results=results)
def run(self):
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)
log.info("Full dumpsys output stored at %s",
stats_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,87 +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
from .base import AndroidExtraction
log = logging.getLogger(__name__)
ACTION_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
ACTION_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
ACTION_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
ACTION_PHONE_STATE = "android.intent.action.PHONE_STATE"
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,
log=log, results=results)
def run(self):
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(ACTION_NEW_OUTGOING_SMS):
activity = ACTION_NEW_OUTGOING_SMS
continue
elif line.strip().startswith(ACTION_SMS_RECEIVED):
activity = ACTION_SMS_RECEIVED
continue
elif line.strip().startswith(ACTION_PHONE_STATE):
activity = ACTION_PHONE_STATE
continue
elif line.strip().startswith(ACTION_DATA_SMS_RECEIVED):
activity = ACTION_DATA_SMS_RECEIVED
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
if activity == ACTION_NEW_OUTGOING_SMS:
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
receiver)
elif activity == ACTION_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
receiver)
elif activity == ACTION_DATA_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
receiver)
elif activity == ACTION_PHONE_STATE:
self.log.info("Found a receiver monitoring telephony state: \"%s\"",
receiver)
self.results.append({
"activity": activity,
"package_name": package_name,
"receiver": receiver,
})
self._adb_disconnect()

View File

@@ -1,119 +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 datetime
import logging
import stat
from mvt.common.utils import convert_timestamp_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Files(AndroidExtraction):
"""This module extracts the list of files on the device."""
def __init__(self, file_path=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)
self.full_find = None
def find_path(self, file_path):
"""Checks if Android system supports full find command output"""
# Check find command params on first run
# Run find command with correct args and parse results.
# Check that full file printf options are suppported on first run.
if self.full_find is None:
output = self._adb_command("find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
if not (output or output.strip().splitlines()):
# Full find command failed to generate output, fallback to basic file arguments
self.full_find = False
else:
self.full_find = True
found_files = []
if self.full_find is True:
# Run full file command and collect additonal file information.
output = self._adb_command(f"find '{file_path}' -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
for file_line in output.splitlines():
[unix_timestamp, mode, size, owner, group, full_path] = file_line.rstrip().split(" ", 5)
mod_time = convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(int(float(unix_timestamp))))
found_files.append({
"path": full_path,
"modified_time": mod_time,
"mode": mode,
"is_suid": (int(mode, 8) & stat.S_ISUID) == 2048,
"is_sgid": (int(mode, 8) & stat.S_ISGID) == 1024,
"size": size,
"owner": owner,
"group": group,
})
else:
# Run a basic listing of file paths.
output = self._adb_command(f"find '{file_path}' 2> /dev/null")
for file_line in output.splitlines():
found_files.append({
"path": file_line.rstrip()
})
return found_files
def serialize(self, record):
if "modified_time" in record:
return {
"timestamp": record["modified_time"],
"module": self.__class__.__name__,
"event": "file_modified",
"data": record["path"],
}
def check_suspicious(self):
"""Check for files with suspicious permissions"""
for result in sorted(self.results, key=lambda item: item["path"]):
if result.get("is_suid"):
self.log.warning("Found an SUID file in a non-standard directory \"%s\".",
result["path"])
self.detected.append(result)
def check_indicators(self):
"""Check file list for known suspicious files or suspicious properties"""
self.check_suspicious()
if not self.indicators:
return
for result in self.results:
if self.indicators.check_file_path(result["path"]):
self.log.warning("Found a known suspicous file at path: \"%s\"", result["path"])
self.detected.append(result)
def run(self):
self._adb_connect()
found_file_paths = []
DATA_PATHS = ["/data/local/tmp/", "/sdcard/", "/tmp/"]
for path in DATA_PATHS:
file_info = self.find_path(path)
found_file_paths.extend(file_info)
# Store results
self.results.extend(found_file_paths)
self.log.info("Found %s files in primary Android data directories.", len(found_file_paths))
if self.fast_mode:
self.log.info("Flag --fast was enabled: skipping full file listing")
else:
self.log.info("Flag --fast was not enabled: processing full file listing. "
"This may take a while...")
output = self.find_path("/")
if output and self.output_folder:
self.results.extend(output)
log.info("List of visible files stored in files.json")
self._adb_disconnect()

View File

@@ -1,48 +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 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,
log=log, results=results)
def run(self):
self._adb_connect()
# Get the current logcat.
output = self._adb_command("logcat -d")
# Get the locat prior to last reboot.
last_output = self._adb_command("logcat -L")
if self.output_folder:
logcat_path = os.path.join(self.output_folder,
"logcat.txt")
with open(logcat_path, "w") 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.txt")
with open(logcat_last_path, "w") as handle:
handle.write(last_output)
log.info("Logcat logs prior to last reboot stored at %s",
logcat_last_path)
self._adb_disconnect()

View File

@@ -1,170 +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
import pkg_resources
from .base import AndroidExtraction
log = logging.getLogger(__name__)
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,
log=log, results=results)
def serialize(self, record):
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"{record['package_name']} (system: {record['system']}, third party: {record['third_party']})",
})
return records
def check_indicators(self):
if not self.indicators:
return
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]
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)
if result["package_name"] in self.indicators.ioc_app_ids:
self.log.warning("Found a malicious package name: \"%s\"",
result["package_name"])
self.detected.append(result)
for file in result["files"]:
if file["sha256"] in self.indicators.ioc_files_sha256:
self.log.warning("Found a malicious APK: \"%s\" %s",
result["package_name"],
file["sha256"])
self.detected.append(result)
def _get_files_for_package(self, package_name):
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"):
file_path = file_path.strip()
md5 = self._adb_command(f"md5sum {file_path}").split(" ")[0]
sha1 = self._adb_command(f"sha1sum {file_path}").split(" ")[0]
sha256 = self._adb_command(f"sha256sum {file_path}").split(" ")[0]
sha512 = self._adb_command(f"sha512sum {file_path}").split(" ")[0]
package_files.append({
"path": file_path,
"md5": md5,
"sha1": sha1,
"sha256": sha256,
"sha512": sha512,
})
return package_files
def run(self):
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")
for line in packages.split("\n"):
line = line.strip()
if not line.startswith("package:"):
continue
fields = line.split()
file_name, package_name = fields[0].split(":")[1].rsplit("=", 1)
try:
installer = fields[1].split("=")[1].strip()
except IndexError:
installer = None
else:
if installer == "null":
installer = None
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({
"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,
})
cmds = [
{"field": "disabled", "arg": "-d"},
{"field": "system", "arg": "-s"},
{"field": "third_party", "arg": "-3"},
]
for cmd in cmds:
output = self._adb_command(f"pm list packages {cmd['arg']}")
for line in output.split("\n"):
line = line.strip()
if not line.startswith("package:"):
continue
package_name = line.split(":", 1)[1]
for i, result in enumerate(self.results):
if result["package_name"] == package_name:
self.results[i][cmd["field"]] = True
for result in self.results:
if result["system"]:
continue
self.log.info("Found non-system package with name \"%s\" installed by \"%s\" on %s",
result["package_name"], result["installer"], result["timestamp"])
self.log.info("Extracted at total of %d installed package names",
len(self.results))
self._adb_disconnect()

View File

@@ -1,55 +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
from .base import AndroidExtraction
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,
log=log, results=results)
def run(self):
self._adb_connect()
output = self._adb_command("ps -e")
for line in output.split("\n")[1:]:
line = line.strip()
if line == "":
continue
fields = line.split()
proc = {
"user": fields[0],
"pid": fields[1],
"parent_pid": fields[2],
"vsize": fields[3],
"rss": fields[4],
}
# Sometimes WCHAN is empty, so we need to re-align output fields.
if len(fields) == 8:
proc["wchan"] = ""
proc["pc"] = fields[5]
proc["name"] = fields[7]
elif len(fields) == 9:
proc["wchan"] = fields[5]
proc["pc"] = fields[6]
proc["name"] = fields[8]
self.results.append(proc)
self._adb_disconnect()
log.info("Extracted records on a total of %d processes", len(self.results))

View File

@@ -1,49 +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
import pkg_resources
from .base import AndroidExtraction
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,
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")
self._adb_connect()
for root_binary in root_binaries:
root_binary = root_binary.strip()
if not root_binary:
continue
output = self._adb_command(f"which -a {root_binary}")
output = output.strip()
if not output:
continue
if "which: not found" in output:
continue
self.detected.append(root_binary)
self.log.warning("Found root binary \"%s\"", root_binary)
self._adb_disconnect()

View File

@@ -1,115 +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
import sqlite3
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
SMS_BUGLE_QUERY = """
SELECT
ppl.normalized_destination AS number,
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
FROM messages m, conversations c, parts p,
participants ppl, conversation_participants cp
WHERE (m.conversation_id = c._id)
AND (m._id = p.message_id)
AND (cp.conversation_id = c._id)
AND (cp.participant_id = ppl._id);
"""
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
SMS_MMSMS_QUERY = """
SELECT
address AS number,
date_sent AS timestamp,
type as incoming,
body AS text
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,
log=log, results=results)
def serialize(self, record):
text = record["text"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": f"sms_{record['direction']}",
"data": f"{record['number']}: \"{text}\""
}
def check_indicators(self):
if not self.indicators:
return
for message in self.results:
if "text" not in message:
continue
message_links = check_for_links(message["text"])
if self.indicators.check_domains(message_links):
self.detected.append(message)
def _parse_db(self, db_path):
"""Parse an Android bugle_db SMS database file.
:param db_path: Path to the Android SMS database file to process
"""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
if (self.SMS_DB_TYPE == 1):
cur.execute(SMS_BUGLE_QUERY)
elif (self.SMS_DB_TYPE == 2):
cur.execute(SMS_MMSMS_QUERY)
names = [description[0] for description in cur.description]
for item in cur:
message = {}
for index, value in enumerate(item):
message[names[index]] = value
message["direction"] = ("received" if message["incoming"] == 1 else "sent")
message["isodate"] = convert_timestamp_to_iso(message["timestamp"])
# If we find links in the messages or if they are empty we add
# them to the list of results.
if check_for_links(message["text"]) or message["text"].strip() == "":
self.results.append(message)
cur.close()
conn.close()
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")

View File

@@ -1,64 +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 json
import os
import zlib
from mvt.common.module import MVTModule
from mvt.common.utils import check_for_links
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,
log=log, results=results)
def check_indicators(self):
if not self.indicators:
return
for message in self.results:
if "body" not in message:
continue
message_links = check_for_links(message["body"])
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)
with open(file_path, "rb") as handle:
data = zlib.decompress(handle.read())
json_data = json.loads(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",
len(self.results))

View File

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

View File

@@ -1,424 +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 io
import json
import os
import requests
from appdirs import user_data_dir
from .url import URL
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")
self.log = log
self.ioc_files = []
self.total_ioc_count = 0
def _load_downloaded_indicators(self):
if not os.path.isdir(self.data_dir):
return False
for f in os.listdir(self.data_dir):
if f.lower().endswith(".stix2"):
self.parse_stix2(os.path.join(self.data_dir, f))
def _check_stix2_env_variable(self):
"""
Checks if a variable MVT_STIX2 contains path to STIX Files.
"""
if "MVT_STIX2" not in os.environ:
return False
paths = os.environ["MVT_STIX2"].split(":")
for path in paths:
if os.path.isfile(path):
self.parse_stix2(path)
else:
self.log.info("Invalid STIX2 path %s in MVT_STIX2 environment variable", path)
def _generate_indicators_file(self):
return {
"name": "",
"description": "",
"file_name": "",
"file_path": "",
"domains": [],
"processes": [],
"emails": [],
"file_names": [],
"file_paths": [],
"files_sha256": [],
"app_ids": [],
"ios_profile_ids": [],
"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
self.total_ioc_count += 1
def parse_stix2(self, file_path):
"""Extract indicators from a STIX2 file.
:param file_path: Path to the STIX2 file to parse
:type file_path: str
"""
self.log.info("Parsing STIX2 indicators file at path %s", file_path)
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:
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.")
return
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
if entry_type != "indicator":
continue
key, value = entry.get("pattern", "").strip("[]").split("=")
value = value.strip("'")
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"])
self.log.info("Loaded %d indicators from \"%s\" indicators file",
ioc_file["count"], ioc_file["name"])
self.ioc_files.append(ioc_file)
def load_indicators_files(self, files, load_default=True):
"""
Load a list of indicators files.
"""
for file_path in files:
if os.path.isfile(file_path):
self.parse_stix2(file_path)
else:
self.log.warning("This indicators file %s does not exist", file_path)
# Load downloaded indicators and any indicators from env variable.
if load_default:
self._load_downloaded_indicators()
self._check_stix2_env_variable()
self.log.info("Loaded a total of %d unique indicators", self.total_ioc_count)
def get_iocs(self, ioc_type):
for ioc_file in self.ioc_files:
for ioc in ioc_file.get(ioc_type, []):
yield {
"value": ioc,
"name": ioc_file["name"]
}
def check_domain(self, url) -> bool:
"""Check if a given URL matches any of the provided domain indicators.
:param url: URL to match against domain indicators
:type url: str
:returns: True if the URL matched an indicator, otherwise False
:rtype: bool
"""
# TODO: If the IOC domain contains a subdomain, it is not currently
# being matched.
if not url:
return False
try:
# First we use the provided URL.
orig_url = URL(url)
if orig_url.check_if_shortened():
# If it is, we try to retrieve the actual URL making an
# HTTP HEAD request.
unshortened = orig_url.unshorten()
# self.log.info("Found a shortened URL %s -> %s",
# url, unshortened)
# Now we check for any nested URL shorteners.
dest_url = URL(unshortened)
if dest_url.check_if_shortened():
# self.log.info("Original URL %s appears to shorten another shortened URL %s ... checking!",
# orig_url.url, dest_url.url)
return self.check_domain(dest_url.url)
final_url = dest_url
else:
# If it's not shortened, we just use the original URL object.
final_url = orig_url
except Exception:
# If URL parsing failed, we just try to do a simple substring
# match.
for ioc in self.get_iocs("domains"):
if ioc["value"].lower() in url:
self.log.warning("Maybe found a known suspicious domain %s matching indicators from \"%s\"",
url, ioc["name"])
return True
# If nothing matched, we can quit here.
return False
# If all parsing worked, we start walking through available domain indicators.
for ioc in self.get_iocs("domains"):
# First we check the full domain.
if final_url.domain.lower() == ioc["value"]:
if orig_url.is_shortened and orig_url.url != final_url.url:
self.log.warning("Found a known suspicious domain %s shortened as %s matching indicators from \"%s\"",
final_url.url, orig_url.url, ioc["name"])
else:
self.log.warning("Found a known suspicious domain %s matching indicators from \"%s\"",
final_url.url, ioc["name"])
return True
# Then we just check the top level domain.
if final_url.top_level.lower() == ioc["value"]:
if orig_url.is_shortened and orig_url.url != final_url.url:
self.log.warning("Found a sub-domain with suspicious top level %s shortened as %s matching indicators from \"%s\"",
final_url.url, orig_url.url, ioc["name"])
else:
self.log.warning("Found a sub-domain with a suspicious top level %s matching indicators from \"%s\"",
final_url.url, ioc["name"])
return True
return False
def check_domains(self, urls) -> bool:
"""Check a list of URLs against the provided list of domain indicators.
:param urls: List of URLs to check against domain indicators
:type urls: list
:returns: True if any URL matched an indicator, otherwise False
:rtype: bool
"""
if not urls:
return False
for url in urls:
if self.check_domain(url):
return True
return False
def check_process(self, process) -> bool:
"""Check the provided process name against the list of process
indicators.
:param process: Process name to check against process indicators
:type process: str
:returns: True if process matched an indicator, otherwise False
:rtype: bool
"""
if not process:
return False
proc_name = os.path.basename(process)
for ioc in self.get_iocs("processes"):
if proc_name == ioc["value"]:
self.log.warning("Found a known suspicious process name \"%s\" matching indicators from \"%s\"",
process, ioc["name"])
return True
if len(proc_name) == 16:
if ioc["value"].startswith(proc_name):
self.log.warning("Found a truncated known suspicious process name \"%s\" matching indicators from \"%s\"",
process, ioc["name"])
return True
return False
def check_processes(self, processes) -> bool:
"""Check the provided list of processes against the list of
process indicators.
:param processes: List of processes to check against process indicators
:type processes: list
:returns: True if process matched an indicator, otherwise False
:rtype: bool
"""
if not processes:
return False
for process in processes:
if self.check_process(process):
return True
return False
def check_email(self, email) -> bool:
"""Check the provided email against the list of email indicators.
:param email: Email address to check against email indicators
:type email: str
:returns: True if email address matched an indicator, otherwise False
:rtype: bool
"""
if not email:
return False
for ioc in self.get_iocs("emails"):
if email.lower() == ioc["value"].lower():
self.log.warning("Found a known suspicious email address \"%s\" matching indicators from \"%s\"",
email, ioc["name"])
return True
return False
def check_file_name(self, file_name) -> bool:
"""Check the provided file name against the list of file indicators.
:param file_name: File name to check against file
indicators
:type file_name: str
:returns: True if the file name matched an indicator, otherwise False
:rtype: bool
"""
if not file_name:
return False
for ioc in self.get_iocs("file_names"):
if ioc["value"] == file_name:
self.log.warning("Found a known suspicious file name \"%s\" matching indicators from \"%s\"",
file_name, ioc["name"])
return True
return False
def check_file_path(self, file_path) -> bool:
"""Check the provided file path against the list of file indicators (both path and name).
:param file_path: File path or file name to check against file
indicators
:type file_path: str
:returns: True if the file path matched an indicator, otherwise False
:rtype: bool
"""
if not file_path:
return False
if self.check_file_name(os.path.basename(file_path)):
return True
for ioc in self.get_iocs("file_paths"):
# Strip any trailing slash from indicator paths to match directories.
if file_path.startswith(ioc["value"].rstrip("/")):
self.log.warning("Found a known suspicious file path \"%s\" matching indicators form \"%s\"",
file_path, ioc["name"])
return True
return False
def check_profile(self, profile_uuid) -> bool:
"""Check the provided configuration profile UUID against the list of indicators.
:param profile_uuid: Profile UUID to check against configuration profile indicators
:type profile_uuid: str
:returns: True if the UUID in indicator list, otherwise False
:rtype: bool
"""
for ioc in self.get_iocs("ios_profile_ids"):
if profile_uuid in ioc["value"]:
self.log.warning("Found a known suspicious profile ID \"%s\" matching indicators from \"%s\"",
profile_uuid, ioc["name"])
return True
return False
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 +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/
from rich import print
from .version import MVT_VERSION, check_for_updates
def logo():
print("\n")
print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
print("\t\thttps://mvt.re")
print(f"\t\tVersion: {MVT_VERSION}")
try:
latest_version = check_for_updates()
except Exception:
pass
else:
if latest_version:
print(f"\t\t[bold]Version {latest_version} is available! Upgrade mvt![/bold]")
print("\n")

View File

@@ -1,204 +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 csv
import io
import os
import re
import simplejson as json
class DatabaseNotFoundError(Exception):
pass
class DatabaseCorruptedError(Exception):
pass
class InsufficientPrivileges(Exception):
pass
class MVTModule(object):
"""This class provides a base for all extraction modules."""
enabled = True
slug = None
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=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)
:type file_path: str
:param output_folder: Folder where results will be stored
:type output_folder: str
:param fast_mode: Flag to enable or disable slow modules
:type fast_mode: bool
:param log: Handle to logger
:param results: Provided list of results entries
:type results: list
"""
self.file_path = file_path
self.base_folder = base_folder
self.output_folder = output_folder
self.fast_mode = fast_mode
self.log = log
self.indicators = None
self.results = results if results else []
self.detected = []
self.timeline = []
self.timeline_detected = []
@classmethod
def from_json(cls, json_path, log=None):
with open(json_path, "r") 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):
"""Use the module's class name to retrieve a slug"""
if self.slug:
return self.slug
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):
"""Check the results of this module against a provided list of
indicators.
"""
raise NotImplementedError
def save_to_json(self):
"""Save the collected results to a json file."""
if not self.output_folder:
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:
try:
json.dump(self.results, handle, indent=4, default=str)
except Exception as e:
self.log.error("Unable to store results of module %s to file %s: %s",
self.__class__.__name__, results_file_name, e)
if self.detected:
detected_file_name = f"{name}_detected.json"
detected_json_path = os.path.join(self.output_folder, detected_file_name)
with io.open(detected_json_path, "w", encoding="utf-8") as handle:
json.dump(self.detected, handle, indent=4, default=str)
def serialize(self, record):
raise NotImplementedError
@staticmethod
def _deduplicate_timeline(timeline):
"""Serialize entry as JSON to deduplicate repeated entries
:param timeline: List of entries from timeline to deduplicate
"""
timeline_set = set()
for record in timeline:
timeline_set.add(json.dumps(record, sort_keys=True))
return [json.loads(record) for record in timeline_set]
def to_timeline(self):
"""Convert results into a timeline."""
for result in self.results:
record = self.serialize(result)
if record:
if type(record) == list:
self.timeline.extend(record)
else:
self.timeline.append(record)
for detected in self.detected:
record = self.serialize(detected)
if record:
if type(record) == list:
self.timeline_detected.extend(record)
else:
self.timeline_detected.append(record)
# De-duplicate timeline entries.
self.timeline = self._deduplicate_timeline(self.timeline)
self.timeline_detected = self._deduplicate_timeline(self.timeline_detected)
def run(self):
"""Run the main module procedure."""
raise NotImplementedError
def run_module(module):
module.log.info("Running module %s...", module.__class__.__name__)
try:
module.run()
except NotImplementedError:
module.log.exception("The run() procedure of module %s was not implemented yet!",
module.__class__.__name__)
except InsufficientPrivileges as e:
module.log.info("Insufficient privileges for module %s: %s", module.__class__.__name__, e)
except DatabaseNotFoundError as e:
module.log.info("There might be no data to extract by module %s: %s",
module.__class__.__name__, e)
except DatabaseCorruptedError as e:
module.log.error("The %s module database seems to be corrupted: %s",
module.__class__.__name__, e)
except Exception as e:
module.log.exception("Error in running extraction from module %s: %s",
module.__class__.__name__, e)
else:
try:
module.check_indicators()
except NotImplementedError:
module.log.info("The %s module does not support checking for indicators",
module.__class__.__name__)
pass
else:
if module.indicators and not module.detected:
module.log.info("The %s module produced no detections!",
module.__class__.__name__)
try:
module.to_timeline()
except NotImplementedError:
pass
module.save_to_json()
def save_timeline(timeline, timeline_path):
"""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:
csvoutput = csv.writer(handle, delimiter=",", quotechar="\"")
csvoutput.writerow(["UTC Timestamp", "Plugin", "Event", "Description"])
for event in sorted(timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""):
csvoutput.writerow([
event.get("timestamp"),
event.get("module"),
event.get("event"),
event.get("data"),
])

View File

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

View File

@@ -1,20 +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 requests
from packaging import version
MVT_VERSION = "1.4.6"
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

View File

@@ -1,316 +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
import click
from rich.logging import RichHandler
from rich.prompt import Prompt
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
from mvt.common.logo import logo
from mvt.common.module import run_module, save_timeline
from mvt.common.options import MutuallyExclusiveOption
from .decrypt import DecryptBackup
from .modules.backup import BACKUP_MODULES
from .modules.fs import FS_MODULES
from .modules.mixed import MIXED_MODULES
# Setup logging using Rich.
LOG_FORMAT = "[%(name)s] %(message)s"
logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__)
# Set this environment variable to a password if needed.
PASSWD_ENV = "MVT_IOS_BACKUP_PASSWORD"
#==============================================================================
# Main
#==============================================================================
@click.group(invoke_without_command=False)
def cli():
logo()
#==============================================================================
# Command: version
#==============================================================================
@cli.command("version", help="Show the currently installed version of MVT")
def version():
return
#==============================================================================
# Command: decrypt-backup
#==============================================================================
@cli.command("decrypt-backup", help="Decrypt an encrypted iTunes backup")
@click.option("--destination", "-d", required=True,
help="Path to the folder where to store the decrypted backup")
@click.option("--password", "-p", cls=MutuallyExclusiveOption,
help=f"Password to use to decrypt the backup (or, set {PASSWD_ENV} environment variable)",
mutually_exclusive=["key_file"])
@click.option("--key-file", "-k", cls=MutuallyExclusiveOption,
type=click.Path(exists=True),
help="File containing raw encryption key to use to decrypt the backup",
mutually_exclusive=["password"])
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def decrypt_backup(ctx, destination, password, key_file, backup_path):
backup = DecryptBackup(backup_path, destination)
if key_file:
if PASSWD_ENV in os.environ:
log.info("Ignoring environment variable, using --key-file '%s' instead",
PASSWD_ENV, 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:
log.info("Ignoring %s environment variable, using --password argument instead",
PASSWD_ENV)
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])
else:
sekrit = Prompt.ask("Enter backup password", password=True)
backup.decrypt_with_password(sekrit)
if not backup.can_process():
ctx.exit(1)
backup.process_backup()
#==============================================================================
# Command: extract-key
#==============================================================================
@cli.command("extract-key", help="Extract decryption key from an iTunes backup")
@click.option("--password", "-p",
help=f"Password to use to decrypt the backup (or, set {PASSWD_ENV} 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):
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:
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]
else:
password = Prompt.ask("Enter backup password", password=True)
backup.decrypt_with_password(password)
backup.get_key()
if key_file:
backup.write_key(key_file)
#==============================================================================
# Command: check-backup
#==============================================================================
@cli.command("check-backup", help="Extract artifacts from an iTunes backup")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(ctx, iocs, output, fast, 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__)
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)
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:
log.warning("The analysis of the backup produced %d detections!",
len(timeline_detected))
#==============================================================================
# Command: check-fs
#==============================================================================
@cli.command("check-fs", help="Extract artifacts from a full filesystem dump")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.argument("DUMP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_fs(ctx, iocs, output, fast, 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__)
return
log.info("Checking filesystem dump located at: %s", dump_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)
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
#==============================================================================
@cli.command("check-iocs", help="Compare stored JSON results to provided indicators")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.argument("FOLDER", type=click.Path(exists=True))
@click.pass_context
def check_iocs(ctx, iocs, list_modules, module, folder):
all_modules = []
for entry in BACKUP_MODULES + FS_MODULES + MIXED_MODULES:
if entry not in all_modules:
all_modules.append(entry)
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__)
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)
#==============================================================================
# Command: download-iocs
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
def download_iocs():
download_indicators_files(log)

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,43 +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 os
import plistlib
from mvt.common.module import DatabaseNotFoundError
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,
log=log, results=results)
self.results = {}
def run(self):
info_path = os.path.join(self.base_folder, "Info.plist")
if not os.path.exists(info_path):
raise DatabaseNotFoundError("No Info.plist at backup path, unable to extract device information")
with open(info_path, "rb") as handle:
info = plistlib.load(handle)
fields = ["Build Version", "Device Name", "Display Name",
"GUID", "ICCID", "IMEI", "MEID", "Installed Applications",
"Last Backup Date", "Phone Number", "Product Name",
"Product Type", "Product Version", "Serial Number",
"Target Identifier", "Target Type", "Unique Identifier",
"iTunes Version"]
for field in fields:
value = info.get(field, None)
self.log.info("%s: %s", field, value)
self.results[field] = value

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 os
import plistlib
from base64 import b64encode
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
CONF_PROFILES_DOMAIN = "SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
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,
log=log, results=results)
def serialize(self, record):
if not record["install_date"]:
return
payload_name = record['plist'].get('PayloadDisplayName')
payload_description = record['plist'].get('PayloadDescription')
return {
"timestamp": record["install_date"],
"module": self.__class__.__name__,
"event": "configuration_profile_install",
"data": f"{record['plist']['PayloadType']} installed: {record['plist']['PayloadUUID']} - {payload_name}: {payload_description}"
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if result["plist"].get("PayloadUUID"):
payload_content = result["plist"]["PayloadContent"][0]
# Alert on any known malicious configuration profiles in the indicator list.
if self.indicators.check_profile(result["plist"]["PayloadUUID"]):
self.log.warning(f"Found a known malicious configuration profile \"{result['plist']['PayloadDisplayName']}\" with UUID '{result['plist']['PayloadUUID']}'.")
self.detected.append(result)
continue
# Highlight suspicious configuration profiles which may be used to hide notifications.
if payload_content["PayloadType"] in ["com.apple.notificationsettings"]:
self.log.warning(f"Found a potentially suspicious configuration profile \"{result['plist']['PayloadDisplayName']}\" with payload type '{payload_content['PayloadType']}'.")
self.detected.append(result)
continue
def run(self):
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.
if not conf_rel_path or not os.path.basename(conf_rel_path).startswith("profile-"):
continue
conf_file_path = self._get_backup_file_from_id(conf_file["file_id"])
if not conf_file_path:
continue
with open(conf_file_path, "rb") as handle:
try:
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 "PushTokenDataSentToServerKey" in conf_plist:
conf_plist["PushTokenDataSentToServerKey"] = b64encode(conf_plist["PushTokenDataSentToServerKey"])
if "LastPushTokenHash" in conf_plist:
conf_plist["LastPushTokenHash"] = b64encode(conf_plist["LastPushTokenHash"])
if "PayloadContent" in conf_plist:
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"])
self.results.append({
"file_id": conf_file["file_id"],
"relative_path": conf_file["relative_path"],
"domain": conf_file["domain"],
"plist": conf_plist,
"install_date": convert_timestamp_to_iso(conf_plist.get("InstallDate")),
})
self.log.info("Extracted details about %d configuration profiles", len(self.results))

View File

@@ -1,143 +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 datetime
import io
import os
import plistlib
import sqlite3
from mvt.common.module import DatabaseNotFoundError
from mvt.common.utils import convert_timestamp_to_iso
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,
log=log, results=results)
def _get_key(self, dictionary, key):
"""Unserialized plist objects can have keys which are str or byte types
This is a helper to try fetch a key as both a byte or string type.
:param dictionary:
:param key:
"""
return dictionary.get(key.encode("utf-8"), None) or dictionary.get(key, None)
@staticmethod
def _convert_timestamp(timestamp_or_unix_time_int):
"""Older iOS versions stored the manifest times as unix timestamps.
:param timestamp_or_unix_time_int:
"""
if isinstance(timestamp_or_unix_time_int, datetime.datetime):
return convert_timestamp_to_iso(timestamp_or_unix_time_int)
else:
timestamp = datetime.datetime.utcfromtimestamp(timestamp_or_unix_time_int)
return convert_timestamp_to_iso(timestamp)
def serialize(self, record):
records = []
if "modified" not in record or "status_changed" not in record:
return
for ts in set([record["created"], record["modified"], record["status_changed"]]):
macb = ""
macb += "M" if ts == record["modified"] else "-"
macb += "-"
macb += "C" if ts == record["status_changed"] else "-"
macb += "B" if ts == record["created"] else "-"
records.append({
"timestamp": ts,
"module": self.__class__.__name__,
"event": macb,
"data": f"{record['relative_path']} - {record['domain']}"
})
return records
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if not result.get("relative_path"):
continue
if result["domain"]:
if os.path.basename(result["relative_path"]) == "com.apple.CrashReporter.plist" and result["domain"] == "RootDomain":
self.log.warning("Found a potentially suspicious \"com.apple.CrashReporter.plist\" file created in RootDomain")
self.detected.append(result)
continue
if self.indicators.check_file_path("/" + result["relative_path"]):
self.detected.append(result)
continue
rel_path = result["relative_path"].lower()
for ioc in self.indicators.get_iocs("domains"):
if ioc["value"].lower() in rel_path:
self.log.warning("Found mention of domain \"%s\" in a backup file with path: %s",
ioc["value"], rel_path)
self.detected.append(result)
def run(self):
manifest_db_path = os.path.join(self.base_folder, "Manifest.db")
if not os.path.isfile(manifest_db_path):
raise DatabaseNotFoundError("unable to find backup's Manifest.db")
self.log.info("Found Manifest.db database at path: %s", manifest_db_path)
conn = sqlite3.connect(manifest_db_path)
cur = conn.cursor()
cur.execute("SELECT * FROM Files;")
names = [description[0] for description in cur.description]
for file_entry in cur:
file_data = {}
for index, value in enumerate(file_entry):
file_data[names[index]] = value
cleaned_metadata = {
"file_id": file_data["fileID"],
"domain": file_data["domain"],
"relative_path": file_data["relativePath"],
"flags": file_data["flags"],
"created": "",
}
if file_data["file"]:
try:
file_plist = plistlib.load(io.BytesIO(file_data["file"]))
file_metadata = self._get_key(file_plist, "$objects")[1]
cleaned_metadata.update({
"created": self._convert_timestamp(self._get_key(file_metadata, "Birth")),
"modified": self._convert_timestamp(self._get_key(file_metadata, "LastModified")),
"status_changed": self._convert_timestamp(self._get_key(file_metadata, "LastStatusChange")),
"mode": oct(self._get_key(file_metadata, "Mode")),
"owner": self._get_key(file_metadata, "UserID"),
"size": self._get_key(file_metadata, "Size"),
})
except Exception:
self.log.exception("Error reading manifest file metadata for file with ID %s and relative path %s",
file_data["fileID"], file_data["relativePath"])
pass
self.results.append(cleaned_metadata)
cur.close()
conn.close()
self.log.info("Extracted a total of %d file metadata items", len(self.results))

View File

@@ -1,61 +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 plistlib
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
CONF_PROFILES_EVENTS_RELPATH = "Library/ConfigurationProfiles/MCProfileEvents.plist"
class ProfileEvents(IOSExtraction):
"""This module extracts events related to the installation of 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,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record.get("timestamp"),
"module": self.__class__.__name__,
"event": "profile_operation",
"data": f"Process {record.get('process')} started operation {record.get('operation')} of profile {record.get('profile_id')}"
}
def run(self):
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:
continue
with open(events_file_path, "rb") as handle:
events_plist = plistlib.load(handle)
if "ProfileEvents" not in events_plist:
continue
for event in events_plist["ProfileEvents"]:
key = list(event.keys())[0]
self.log.info("On %s process \"%s\" started operation \"%s\" of profile \"%s\"",
event[key].get("timestamp"), event[key].get("process"),
event[key].get("operation"), key)
self.results.append({
"profile_id": key,
"timestamp": convert_timestamp_to_iso(event[key].get("timestamp")),
"operation": event[key].get("operation"),
"process": event[key].get("process"),
})
self.log.info("Extracted %d profile events", len(self.results))

View File

@@ -1,157 +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 glob
import os
import shutil
import sqlite3
import subprocess
from mvt.common.module import (DatabaseCorruptedError, DatabaseNotFoundError,
MVTModule)
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,
log=log, results=results)
self.is_backup = False
self.is_fs_dump = False
self.is_sysdiagnose = False
def _recover_sqlite_db_if_needed(self, file_path, forced=False):
"""Tries to recover a malformed database by running a .clone command.
:param file_path: Path to the malformed database file.
"""
# TODO: Find a better solution.
if not forced:
conn = sqlite3.connect(file_path)
cur = conn.cursor()
try:
recover = False
cur.execute("SELECT name FROM sqlite_master WHERE type='table';")
except sqlite3.DatabaseError as e:
if "database disk image is malformed" in str(e):
recover = True
finally:
conn.close()
if not recover:
return
self.log.info("Database at path %s is malformed. Trying to recover...", file_path)
if not shutil.which("sqlite3"):
raise DatabaseCorruptedError("failed to recover without sqlite3 binary: please install sqlite3!")
if '"' in file_path:
raise DatabaseCorruptedError(f"database at path '{file_path}' is corrupted. unable to recover because it has a quotation mark (\") in its name")
bak_path = f"{file_path}.bak"
shutil.move(file_path, bak_path)
ret = subprocess.call(["sqlite3", bak_path, f".clone \"{file_path}\""],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if ret != 0:
raise DatabaseCorruptedError("failed to recover database")
self.log.info("Database at path %s recovered successfully!", file_path)
def _get_backup_files_from_manifest(self, relative_path=None, domain=None):
"""Locate files from Manifest.db.
:param relative_path: Relative path to use as filter from Manifest.db. (Default value = None)
:param domain: Domain to use as filter from Manifest.db. (Default value = None)
"""
manifest_db_path = os.path.join(self.base_folder, "Manifest.db")
if not os.path.exists(manifest_db_path):
raise DatabaseNotFoundError("unable to find backup's Manifest.db")
base_sql = "SELECT fileID, domain, relativePath FROM Files WHERE "
try:
conn = sqlite3.connect(manifest_db_path)
cur = conn.cursor()
if relative_path and domain:
cur.execute(f"{base_sql} relativePath = ? AND domain = ?;",
(relative_path, domain))
else:
if relative_path:
cur.execute(f"{base_sql} relativePath = ?;", (relative_path,))
elif domain:
cur.execute(f"{base_sql} domain = ?;", (domain,))
except Exception as e:
raise DatabaseCorruptedError("failed to query Manifest.db: %s", e)
for row in cur:
yield {
"file_id": row[0],
"domain": row[1],
"relative_path": row[2],
}
def _get_backup_file_from_id(self, file_id):
file_path = os.path.join(self.base_folder, file_id[0:2], file_id)
if os.path.exists(file_path):
return file_path
return None
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)):
if not os.path.exists(found_path):
continue
yield found_path
def _find_ios_database(self, backup_ids=None, root_paths=[]):
"""Try to locate a module's database file from either an iTunes
backup or a full filesystem dump. This is intended only for
modules that expect to work with a single SQLite database.
If a module requires to process multiple databases or files,
you should use the helper functions above.
:param backup_id: iTunes backup database file's ID (or hash).
:param root_paths: Glob patterns for files to seek in filesystem dump. (Default value = [])
:param backup_ids: Default value = None)
"""
file_path = None
# First we check if the was an explicit file path specified.
if not self.file_path:
# If not, we first try with backups.
# We construct the path to the file according to the iTunes backup
# folder structure, if we have a valid ID.
if backup_ids:
for backup_id in backup_ids:
file_path = self._get_backup_file_from_id(backup_id)
if file_path:
break
# If this file does not exist we might be processing a full
# filesystem dump (checkra1n all the things!).
if not file_path or not os.path.exists(file_path):
# We reset the file_path.
file_path = None
for found_path in self._get_fs_files_from_patterns(root_paths):
file_path = found_path
break
# If we do not find any, we fail.
if file_path:
self.file_path = file_path
else:
raise DatabaseNotFoundError("unable to find the module's database file")
self._recover_sqlite_db_if_needed(self.file_path)

View File

@@ -1,120 +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 plistlib
import sqlite3
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
ANALYTICS_DB_PATH = [
"private/var/Keychains/Analytics/*.db",
]
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,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["timestamp"],
"module": self.__class__.__name__,
"event": record["artifact"],
"data": f"{record}",
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
for value in result.values():
if not isinstance(value, str):
continue
if self.indicators.check_process(value):
self.log.warning("Found mention of a malicious process \"%s\" in %s file at %s",
value, result["artifact"], result["timestamp"])
self.detected.append(result)
continue
if self.indicators.check_domain(value):
self.log.warning("Found mention of a malicious domain \"%s\" in %s file at %s",
value, result["artifact"], result["timestamp"])
self.detected.append(result)
def _extract_analytics_data(self):
artifact = self.file_path.split("/")[-1]
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
try:
cur.execute("""
SELECT
timestamp,
data
FROM hard_failures
UNION
SELECT
timestamp,
data
FROM soft_failures
UNION
SELECT
timestamp,
data
FROM all_events;
""")
except sqlite3.OperationalError:
cur.execute("""
SELECT
timestamp,
data
FROM hard_failures
UNION
SELECT
timestamp,
data
FROM soft_failures;
""")
for row in cur:
if row[0] and row[1]:
timestamp = convert_timestamp_to_iso(convert_mactime_to_unix(row[0], False))
data = plistlib.loads(row[1])
data["timestamp"] = timestamp
elif row[0]:
timestamp = convert_timestamp_to_iso(convert_mactime_to_unix(row[0], False))
data = {}
data["timestamp"] = timestamp
elif row[1]:
timestamp = ""
data = plistlib.loads(row[1])
data["timestamp"] = timestamp
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):
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()

View File

@@ -1,78 +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 os
import sqlite3
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,
log=log, results=results)
def serialize(self, record):
records = []
for item in self.results[record]:
records.append({
"timestamp": item["isodate"],
"module": self.__class__.__name__,
"event": "cache_response",
"data": f"{record} recorded visit to URL {item['url']}"
})
return records
def check_indicators(self):
if not self.indicators:
return
self.detected = {}
for key, values in self.results.items():
for value in values:
if self.indicators.check_domain(value["url"]):
if key not in self.detected:
self.detected[key] = [value, ]
else:
self.detected[key].append(value)
def _process_cache_file(self, file_path):
self.log.info("Processing cache file at path: %s", file_path)
conn = sqlite3.connect(file_path)
cur = conn.cursor()
try:
cur.execute("SELECT * FROM cfurl_cache_response;")
except sqlite3.OperationalError:
return
key_name = os.path.relpath(file_path, self.base_folder)
if key_name not in self.results:
self.results[key_name] = []
for row in cur:
self.results[key_name].append({
"entry_id": row[0],
"version": row[1],
"hash_value": row[2],
"storage_policy": row[3],
"url": row[4],
"isodate": row[5],
})
def run(self):
self.results = {}
for root, dirs, files in os.walk(self.base_folder):
for file_name in files:
if file_name != "Cache.db":
continue
file_path = os.path.join(root, file_name)
self._process_cache_file(file_path)

View File

@@ -1,39 +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 sqlite3
from ..net_base import NetBase
NETUSAGE_ROOT_PATHS = [
"private/var/networkd/netusage.sqlite",
"private/var/networkd/db/netusage.sqlite"
]
class Netusage(NetBase):
"""This class extracts data from netusage.sqlite and attempts to identify
any suspicious processes if running on a full filesystem dump.
"""
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,
log=log, results=results)
def run(self):
for netusage_path in self._get_fs_files_from_patterns(NETUSAGE_ROOT_PATHS):
self.file_path = netusage_path
self.log.info("Found NetUsage database at path: %s", self.file_path)
try:
self._extract_net_data()
except sqlite3.OperationalError as e:
self.log.info("Skipping this NetUsage database because it seems empty or malformed: %s", e)
continue
self._find_suspicious_processes()

View File

@@ -1,87 +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/
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
SHUTDOWN_LOG_PATH = [
"private/var/db/diagnostics/shutdown.log",
]
class ShutdownLog(IOSExtraction):
"""This module extracts processes information from the shutdown log 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,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "shutdown",
"data": f"Client {record['client']} with PID {record['pid']} was running when the device was shut down",
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if self.indicators.check_file_path(result["client"]):
self.detected.append(result)
continue
for ioc in self.indicators.get_iocs("processes"):
parts = result["client"].split("/")
if ioc in parts:
self.log.warning("Found mention of a known malicious process \"%s\" in shutdown.log",
ioc)
self.detected.append(result)
continue
def process_shutdownlog(self, content):
current_processes = []
for line in content.split("\n"):
line = line.strip()
if line.startswith("remaining client pid:"):
current_processes.append({
"pid": line[line.find("pid: ")+5:line.find(" (")],
"client": line[line.find("(")+1:line.find(")")],
})
elif line.startswith("SIGTERM: "):
try:
mac_timestamp = int(line[line.find("[")+1:line.find("]")])
except ValueError:
try:
start = line.find(" @")+2
mac_timestamp = int(line[start:start+10])
except Exception:
mac_timestamp = 0
timestamp = convert_mactime_to_unix(mac_timestamp, from_2001=False)
isodate = convert_timestamp_to_iso(timestamp)
for current_process in current_processes:
self.results.append({
"isodate": isodate,
"pid": current_process["pid"],
"client": current_process["client"],
})
current_processes = []
self.results = sorted(self.results, key=lambda entry: entry["isodate"])
def run(self):
self._find_ios_database(root_paths=SHUTDOWN_LOG_PATH)
self.log.info("Found shutdown log at path: %s", self.file_path)
with open(self.file_path, "r") as handle:
self.process_shutdownlog(handle.read())

View File

@@ -1,48 +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 datetime
import json
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
IOS_ANALYTICS_JOURNAL_PATHS = [
"private/var/db/analyticsd/Analytics-Journal-*.ips",
]
class IOSVersionHistory(IOSExtraction):
"""This module extracts iOS update history from Analytics Journal log 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,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "ios_version",
"data": f"Recorded iOS version {record['os_version']}",
}
def run(self):
for found_path in self._get_fs_files_from_patterns(IOS_ANALYTICS_JOURNAL_PATHS):
with open(found_path, "r") as analytics_log:
log_line = json.loads(analytics_log.readline().strip())
timestamp = datetime.datetime.strptime(log_line["timestamp"],
"%Y-%m-%d %H:%M:%S.%f %z")
timestamp_utc = timestamp.astimezone(datetime.timezone.utc)
self.results.append({
"isodate": convert_timestamp_to_iso(timestamp_utc),
"os_version": log_line["os_version"],
})
self.results = sorted(self.results, key=lambda entry: entry["isodate"])

View File

@@ -1,29 +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/
from .webkit_base import WebkitBase
WEBKIT_SAFARIVIEWSERVICE_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/",
]
class WebkitSafariViewService(WebkitBase):
"""This module looks extracts records from WebKit LocalStorage folders,
and checks them against any provided list of suspicious domains.
"""
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,
log=log, results=results)
def run(self):
self._process_webkit_folder(WEBKIT_SAFARIVIEWSERVICE_ROOT_PATHS)
self.log.info("Extracted a total of %d records from WebKit SafariViewService WebsiteData",
len(self.results))

View File

@@ -1,63 +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 sqlite3
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
CALLS_BACKUP_IDS = [
"5a4935c78a5255723f707230a451d79c540d2741",
]
CALLS_ROOT_PATHS = [
"private/var/mobile/Library/CallHistoryDB/CallHistory.storedata"
]
class Calls(IOSExtraction):
"""This module extracts phone calls details"""
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,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "call",
"data": f"From {record['number']} using {record['provider']} during {record['duration']} seconds"
}
def run(self):
self._find_ios_database(backup_ids=CALLS_BACKUP_IDS,
root_paths=CALLS_ROOT_PATHS)
self.log.info("Found Calls database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
ZDATE, ZDURATION, ZLOCATION, ZADDRESS, ZSERVICE_PROVIDER
FROM ZCALLRECORD;
""")
# names = [description[0] for description in cur.description]
for row in cur:
self.results.append({
"isodate": convert_timestamp_to_iso(convert_mactime_to_unix(row[0])),
"duration": row[1],
"location": row[2],
"number": row[3].decode("utf-8") if row[3] and row[3] is bytes else row[3],
"provider": row[4]
})
cur.close()
conn.close()
self.log.info("Extracted a total of %d calls", len(self.results))

View File

@@ -1,79 +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 sqlite3
from mvt.common.utils import (convert_chrometime_to_unix,
convert_timestamp_to_iso)
from ..base import IOSExtraction
CHROME_HISTORY_BACKUP_IDS = [
"faf971ce92c3ac508c018dce1bef2a8b8e9838f1",
]
# TODO: Confirm Chrome database path.
CHROME_HISTORY_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/History",
]
class ChromeHistory(IOSExtraction):
"""This module extracts all Chome visits."""
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,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "visit",
"data": f"{record['id']} - {record['url']} (visit ID: {record['visit_id']}, redirect source: {record['redirect_source']})"
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if self.indicators.check_domain(result["url"]):
self.detected.append(result)
def run(self):
self._find_ios_database(backup_ids=CHROME_HISTORY_BACKUP_IDS,
root_paths=CHROME_HISTORY_ROOT_PATHS)
self.log.info("Found Chrome history database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
urls.id,
urls.url,
visits.id,
visits.visit_time,
visits.from_visit
FROM urls
JOIN visits ON visits.url = urls.id
ORDER BY visits.visit_time;
""")
for item in cur:
self.results.append({
"id": item[0],
"url": item[1],
"visit_id": item[2],
"timestamp": item[3],
"isodate": convert_timestamp_to_iso(convert_chrometime_to_unix(item[3])),
"redirect_source": item[4],
})
cur.close()
conn.close()
self.log.info("Extracted a total of %d history items", len(self.results))

View File

@@ -1,54 +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 sqlite3
from ..base import IOSExtraction
CONTACTS_BACKUP_IDS = [
"31bb7ba8914766d4ba40d6dfb6113c8b614be442",
]
CONTACTS_ROOT_PATHS = [
"private/var/mobile/Library/AddressBook/AddressBook.sqlitedb",
]
class Contacts(IOSExtraction):
"""This module extracts all contact details from the phone's address book."""
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,
log=log, results=results)
def run(self):
self._find_ios_database(backup_ids=CONTACTS_BACKUP_IDS, root_paths=CONTACTS_ROOT_PATHS)
self.log.info("Found Contacts database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
multi.value, person.first, person.middle, person.last,
person.organization
FROM ABPerson person, ABMultiValue multi
WHERE person.rowid = multi.record_id and multi.value not null
ORDER by person.rowid ASC;
""")
names = [description[0] for description in cur.description]
for row in cur:
new_contact = {}
for index, value in enumerate(row):
new_contact[names[index]] = value
self.results.append(new_contact)
cur.close()
conn.close()
self.log.info("Extracted a total of %d contacts from the address book",
len(self.results))

View File

@@ -1,84 +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 sqlite3
from datetime import datetime
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
FIREFOX_HISTORY_BACKUP_IDS = [
"2e57c396a35b0d1bcbc624725002d98bd61d142b",
]
FIREFOX_HISTORY_ROOT_PATHS = [
"private/var/mobile/profile.profile/browser.db",
]
class FirefoxFavicon(IOSExtraction):
"""This module extracts all Firefox favicon"""
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,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "firefox_history",
"data": f"Firefox favicon {record['url']} when visiting {record['history_url']}",
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if (self.indicators.check_domain(result.get("url", "")) or
self.indicators.check_domain(result.get("history_url", ""))):
self.detected.append(result)
def run(self):
self._find_ios_database(backup_ids=FIREFOX_HISTORY_BACKUP_IDS,
root_paths=FIREFOX_HISTORY_ROOT_PATHS)
self.log.info("Found Firefox favicon database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
favicons.id,
favicons.url,
favicons.width,
favicons.height,
favicons.type,
favicons.date,
history.id,
history.url
FROM favicons
INNER JOIN favicon_sites ON favicon_sites.faviconID = favicons.id
INNER JOIN history ON favicon_sites.siteID = history.id;
""")
for item in cur:
self.results.append({
"id": item[0],
"url": item[1],
"width": item[2],
"height": item[3],
"type": item[4],
"isodate": convert_timestamp_to_iso(datetime.utcfromtimestamp(item[5])),
"history_id": item[6],
"history_url": item[7]
})
cur.close()
conn.close()
self.log.info("Extracted a total of %d history items", len(self.results))

View File

@@ -1,174 +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 sqlite3
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
INTERACTIONC_BACKUP_IDS = [
"1f5a521220a3ad80ebfdc196978df8e7a2e49dee",
]
INTERACTIONC_ROOT_PATHS = [
"private/var/mobile/Library/CoreDuet/People/interactionC.db",
]
class InteractionC(IOSExtraction):
"""This module extracts data from InteractionC db."""
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,
log=log, results=results)
self.timestamps = [
"start_date",
"end_date",
"interactions_creation_date",
"contacts_creation_date",
"first_incoming_recipient_date",
"first_incoming_sender_date",
"first_outgoing_recipient_date",
"last_incoming_sender_date",
"last_incoming_recipient_date",
"last_outgoing_recipient_date",
]
def serialize(self, record):
records = []
processed = []
for ts in self.timestamps:
# Check if the record has the current timestamp.
if ts not in record or not record[ts]:
continue
# Check if the timestamp was already processed.
if record[ts] in processed:
continue
records.append({
"timestamp": record[ts],
"module": self.__class__.__name__,
"event": ts,
"data": f"[{record['bundle_id']}] {record['account']} - from {record['sender_display_name']} "
f"({record['sender_identifier']}) to {record['recipient_display_name']} "
f"({record['recipient_identifier']}): {record['content']}"
})
processed.append(record[ts])
return records
def run(self):
self._find_ios_database(backup_ids=INTERACTIONC_BACKUP_IDS, root_paths=INTERACTIONC_ROOT_PATHS)
self.log.info("Found InteractionC database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
# TODO: Support all versions.
# Taken from:
# https://github.com/mac4n6/APOLLO/blob/master/modules/interaction_contact_interactions.txt
cur.execute("""
SELECT
ZINTERACTIONS.ZSTARTDATE,
ZINTERACTIONS.ZENDDATE,
ZINTERACTIONS.ZBUNDLEID,
ZINTERACTIONS.ZACCOUNT,
ZINTERACTIONS.ZTARGETBUNDLEID,
CASE ZINTERACTIONS.ZDIRECTION
WHEN '0' THEN 'INCOMING'
WHEN '1' THEN 'OUTGOING'
END 'DIRECTION',
ZCONTACTS.ZDISPLAYNAME,
ZCONTACTS.ZIDENTIFIER,
ZCONTACTS.ZPERSONID,
RECEIPIENTCONACT.ZDISPLAYNAME,
RECEIPIENTCONACT.ZIDENTIFIER,
RECEIPIENTCONACT.ZPERSONID,
ZINTERACTIONS.ZRECIPIENTCOUNT,
ZINTERACTIONS.ZDOMAINIDENTIFIER,
ZINTERACTIONS.ZISRESPONSE,
ZATTACHMENT.ZCONTENTTEXT,
ZATTACHMENT.ZUTI,
ZATTACHMENT.ZCONTENTURL,
ZATTACHMENT.ZSIZEINBYTES,
ZATTACHMENT.ZPHOTOLOCALIDENTIFIER,
HEX(ZATTACHMENT.ZIDENTIFIER),
ZATTACHMENT.ZCLOUDIDENTIFIER,
ZCONTACTS.ZINCOMINGRECIPIENTCOUNT,
ZCONTACTS.ZINCOMINGSENDERCOUNT,
ZCONTACTS.ZOUTGOINGRECIPIENTCOUNT,
ZINTERACTIONS.ZCREATIONDATE,
ZCONTACTS.ZCREATIONDATE,
ZCONTACTS.ZFIRSTINCOMINGRECIPIENTDATE,
ZCONTACTS.ZFIRSTINCOMINGSENDERDATE,
ZCONTACTS.ZFIRSTOUTGOINGRECIPIENTDATE,
ZCONTACTS.ZLASTINCOMINGSENDERDATE,
ZCONTACTS.ZLASTINCOMINGRECIPIENTDATE,
ZCONTACTS.ZLASTOUTGOINGRECIPIENTDATE,
ZCONTACTS.ZCUSTOMIDENTIFIER,
ZINTERACTIONS.ZCONTENTURL,
ZINTERACTIONS.ZLOCATIONUUID,
ZINTERACTIONS.ZGROUPNAME,
ZINTERACTIONS.ZDERIVEDINTENTIDENTIFIER,
ZINTERACTIONS.Z_PK
FROM ZINTERACTIONS
LEFT JOIN ZCONTACTS ON ZINTERACTIONS.ZSENDER = ZCONTACTS.Z_PK
LEFT JOIN Z_1INTERACTIONS ON ZINTERACTIONS.Z_PK == Z_1INTERACTIONS.Z_3INTERACTIONS
LEFT JOIN ZATTACHMENT ON Z_1INTERACTIONS.Z_1ATTACHMENTS == ZATTACHMENT.Z_PK
LEFT JOIN Z_2INTERACTIONRECIPIENT ON ZINTERACTIONS.Z_PK== Z_2INTERACTIONRECIPIENT.Z_3INTERACTIONRECIPIENT
LEFT JOIN ZCONTACTS RECEIPIENTCONACT ON Z_2INTERACTIONRECIPIENT.Z_2RECIPIENTS== RECEIPIENTCONACT.Z_PK;
""")
# names = [description[0] for description in cur.description]
for row in cur:
self.results.append({
"start_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[0])),
"end_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[1])),
"bundle_id": row[2],
"account": row[3],
"target_bundle_id": row[4],
"direction": row[5],
"sender_display_name": row[6],
"sender_identifier": row[7],
"sender_personid": row[8],
"recipient_display_name": row[9],
"recipient_identifier": row[10],
"recipient_personid": row[11],
"recipient_count": row[12],
"domain_identifier": row[13],
"is_response": row[14],
"content": row[15],
"uti": row[16],
"content_url": row[17],
"size": row[18],
"photo_local_id": row[19],
"attachment_id": row[20],
"cloud_id": row[21],
"incoming_recipient_count": row[22],
"incoming_sender_count": row[23],
"outgoing_recipient_count": row[24],
"interactions_creation_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[25])) if row[25] else None,
"contacts_creation_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[26])) if row[26] else None,
"first_incoming_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[27])) if row[27] else None,
"first_incoming_sender_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[28])) if row[28] else None,
"first_outgoing_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[29])) if row[29] else None,
"last_incoming_sender_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[30])) if row[30] else None,
"last_incoming_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[31])) if row[31] else None,
"last_outgoing_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[32])) if row[32] else None,
"custom_id": row[33],
"location_uuid": row[35],
"group_name": row[36],
"derivied_intent_id": row[37],
"table_id": row[38]
})
cur.close()
conn.close()
self.log.info("Extracted a total of %d InteractionC events", len(self.results))

View File

@@ -1,115 +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 plistlib
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
LOCATIOND_BACKUP_IDS = [
"a690d7769cce8904ca2b67320b107c8fe5f79412",
]
LOCATIOND_ROOT_PATHS = [
"private/var/mobile/Library/Caches/locationd/clients.plist",
"private/var/root/Library/Caches/locationd/clients.plist"
]
class LocationdClients(IOSExtraction):
"""Extract information from apps who used geolocation."""
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,
log=log, results=results)
self.timestamps = [
"ConsumptionPeriodBegin",
"ReceivingLocationInformationTimeStopped",
"VisitTimeStopped",
"LocationTimeStopped",
"BackgroundLocationTimeStopped",
"SignificantTimeStopped",
"NonPersistentSignificantTimeStopped",
"FenceTimeStopped",
"BeaconRegionTimeStopped",
]
def serialize(self, record):
records = []
for timestamp in self.timestamps:
if timestamp in record.keys():
records.append({
"timestamp": record[timestamp],
"module": self.__class__.__name__,
"event": timestamp,
"data": f"{timestamp} from {record['package']}"
})
return records
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
parts = result["package"].split("/")
proc_name = parts[len(parts)-1]
if self.indicators.check_process(proc_name):
self.log.warning("Found a suspicious process name in LocationD entry %s",
result["package"])
self.detected.append(result)
continue
if "BundlePath" in result:
if self.indicators.check_file_path(result["BundlePath"]):
self.log.warning("Found a suspicious file path in Location D: %s",
result["BundlePath"])
self.detected.append(result)
continue
if "Executable" in result:
if self.indicators.check_file_path(result["Executable"]):
self.log.warning("Found a suspicious file path in Location D: %s",
result["Executable"])
self.detected.append(result)
continue
if "Registered" in result:
if self.indicators.check_file_path(result["Registered"]):
self.log.warning("Found a suspicious file path in Location D: %s",
result["Registered"])
self.detected.append(result)
continue
def _extract_locationd_entries(self, file_path):
with open(file_path, "rb") as handle:
file_plist = plistlib.load(handle)
for key, values in file_plist.items():
result = file_plist[key]
result["package"] = key
for ts in self.timestamps:
if ts in result.keys():
result[ts] = convert_timestamp_to_iso(convert_mactime_to_unix(result[ts]))
self.results.append(result)
def run(self):
if self.is_backup:
self._find_ios_database(backup_ids=LOCATIOND_BACKUP_IDS)
self.log.info("Found Locationd Clients plist at path: %s", self.file_path)
self._extract_locationd_entries(self.file_path)
elif self.is_fs_dump:
for locationd_path in self._get_fs_files_from_patterns(LOCATIOND_ROOT_PATHS):
self.file_path = locationd_path
self.log.info("Found Locationd Clients plist at path: %s", self.file_path)
self._extract_locationd_entries(self.file_path)
self.log.info("Extracted a total of %d Locationd Clients entries", len(self.results))

View File

@@ -1,65 +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 plistlib
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
OSANALYTICS_ADDAILY_BACKUP_IDS = [
"f65b5fafc69bbd3c60be019c6e938e146825fa83",
]
OSANALYTICS_ADDAILY_ROOT_PATHS = [
"private/var/mobile/Library/Preferences/com.apple.osanalytics.addaily.plist",
]
class OSAnalyticsADDaily(IOSExtraction):
"""Extract network usage information by process, from com.apple.osanalytics.addaily.plist"""
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,
log=log, results=results)
def serialize(self, record):
record_data = f"{record['package']} WIFI IN: {record['wifi_in']}, WIFI OUT: {record['wifi_out']} - " \
f"WWAN IN: {record['wwan_in']}, WWAN OUT: {record['wwan_out']}"
return {
"timestamp": record["ts"],
"module": self.__class__.__name__,
"event": "osanalytics_addaily",
"data": record_data,
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if self.indicators.check_process(result["package"]):
self.detected.append(result)
def run(self):
self._find_ios_database(backup_ids=OSANALYTICS_ADDAILY_BACKUP_IDS,
root_paths=OSANALYTICS_ADDAILY_ROOT_PATHS)
self.log.info("Found com.apple.osanalytics.addaily plist at path: %s", self.file_path)
with open(self.file_path, "rb") as handle:
file_plist = plistlib.load(handle)
for app, values in file_plist.get("netUsageBaseline", {}).items():
self.results.append({
"package": app,
"ts": convert_timestamp_to_iso(values[0]),
"wifi_in": values[1],
"wifi_out": values[2],
"wwan_in": values[3],
"wwan_out": values[4],
})
self.log.info("Extracted a total of %d com.apple.osanalytics.addaily entries", len(self.results))

View File

@@ -1,124 +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 io
import os
import plistlib
import sqlite3
from mvt.common.utils import (convert_mactime_to_unix,
convert_timestamp_to_iso, keys_bytes_to_string)
from ..base import IOSExtraction
SAFARI_BROWSER_STATE_BACKUP_RELPATH = "Library/Safari/BrowserState.db"
SAFARI_BROWSER_STATE_ROOT_PATHS = [
"private/var/mobile/Library/Safari/BrowserState.db",
"private/var/mobile/Containers/Data/Application/*/Library/Safari/BrowserState.db",
]
class SafariBrowserState(IOSExtraction):
"""This module extracts all Safari browser state records."""
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,
log=log, results=results)
self._session_history_count = 0
def serialize(self, record):
return {
"timestamp": record["last_viewed_timestamp"],
"module": self.__class__.__name__,
"event": "tab",
"data": f"{record['tab_title']} - {record['tab_url']}"
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if "tab_url" in result and self.indicators.check_domain(result["tab_url"]):
self.detected.append(result)
continue
if "session_data" not in result:
continue
for session_entry in result["session_data"]:
if "entry_url" in session_entry and self.indicators.check_domain(session_entry["entry_url"]):
self.detected.append(result)
def _process_browser_state_db(self, db_path):
conn = sqlite3.connect(db_path)
cur = conn.cursor()
try:
cur.execute("""
SELECT
tabs.title,
tabs.url,
tabs.user_visible_url,
tabs.last_viewed_time,
tab_sessions.session_data
FROM tabs
JOIN tab_sessions ON tabs.uuid = tab_sessions.tab_uuid
ORDER BY tabs.last_viewed_time;
""")
except sqlite3.OperationalError:
# Old version iOS <12 likely
cur.execute("""
SELECT
title, url, user_visible_url, last_viewed_time, session_data
FROM tabs
ORDER BY last_viewed_time;
""")
for row in cur:
session_entries = []
if row[4]:
# Skip a 4 byte header before the plist content.
session_plist = row[4][4:]
session_data = plistlib.load(io.BytesIO(session_plist))
session_data = keys_bytes_to_string(session_data)
if "SessionHistoryEntries" in session_data.get("SessionHistory", {}):
for session_entry in session_data["SessionHistory"].get("SessionHistoryEntries"):
self._session_history_count += 1
session_entries.append({
"entry_title": session_entry.get("SessionHistoryEntryOriginalURL"),
"entry_url": session_entry.get("SessionHistoryEntryURL"),
"data_length": len(session_entry.get("SessionHistoryEntryData")) if "SessionHistoryEntryData" in session_entry else 0,
})
self.results.append({
"tab_title": row[0],
"tab_url": row[1],
"tab_visible_url": row[2],
"last_viewed_timestamp": convert_timestamp_to_iso(convert_mactime_to_unix(row[3])),
"session_data": session_entries,
"safari_browser_state_db": os.path.relpath(db_path, self.base_folder),
})
def run(self):
if self.is_backup:
for backup_file in self._get_backup_files_from_manifest(relative_path=SAFARI_BROWSER_STATE_BACKUP_RELPATH):
self.file_path = self._get_backup_file_from_id(backup_file["file_id"])
self.log.info("Found Safari browser state database at path: %s", self.file_path)
self._process_browser_state_db(self.file_path)
elif self.is_fs_dump:
for safari_browserstate_path in self._get_fs_files_from_patterns(SAFARI_BROWSER_STATE_ROOT_PATHS):
self.file_path = safari_browserstate_path
self.log.info("Found Safari browser state database at path: %s", self.file_path)
self._process_browser_state_db(self.file_path)
self.log.info("Extracted a total of %d tab records and %d session history entries",
len(self.results), self._session_history_count)

View File

@@ -1,119 +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 io
import itertools
import plistlib
import sqlite3
from mvt.common.utils import (check_for_links, convert_mactime_to_unix,
convert_timestamp_to_iso)
from ..base import IOSExtraction
SHORTCUT_BACKUP_IDS = [
"5b4d0b44b5990f62b9f4d34ad8dc382bf0b01094",
]
SHORTCUT_ROOT_PATHS = [
"private/var/mobile/Library/Shortcuts/Shortcuts.sqlite",
]
class Shortcuts(IOSExtraction):
"""This module extracts all info about SMS/iMessage attachments."""
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,
log=log, results=results)
def serialize(self, record):
found_urls = ""
if record["action_urls"]:
found_urls = "- URLs in actions: {}".format(", ".join(record["action_urls"]))
desc = ""
if record["description"]:
desc = record["description"].decode('utf-8', errors='ignore')
return [{
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "shortcut_created",
"data": f"iOS Shortcut '{record['shortcut_name'].decode('utf-8')}': {desc} {found_urls}"
}, {
"timestamp": record["modified_date"],
"module": self.__class__.__name__,
"event": "shortcut_modified",
"data": f"iOS Shortcut '{record['shortcut_name'].decode('utf-8')}': {desc} {found_urls}"
}]
def check_indicators(self):
if not self.indicators:
return
for action in self.results:
if self.indicators.check_domains(action["action_urls"]):
self.detected.append(action)
def run(self):
self._find_ios_database(backup_ids=SHORTCUT_BACKUP_IDS,
root_paths=SHORTCUT_ROOT_PATHS)
self.log.info("Found Shortcuts database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
conn.text_factory = bytes
cur = conn.cursor()
try:
cur.execute("""
SELECT
ZSHORTCUT.Z_PK as "shortcut_id",
ZSHORTCUT.ZNAME as "shortcut_name",
ZSHORTCUT.ZCREATIONDATE as "created_date",
ZSHORTCUT.ZMODIFICATIONDATE as "modified_date",
ZSHORTCUT.ZACTIONSDESCRIPTION as "description",
ZSHORTCUTACTIONS.ZDATA as "action_data"
FROM ZSHORTCUT
LEFT JOIN ZSHORTCUTACTIONS ON ZSHORTCUTACTIONS.ZSHORTCUT == ZSHORTCUT.Z_PK;
""")
except sqlite3.OperationalError:
# Table ZSHORTCUT does not exist
self.log.info("Invalid shortcut database format, skipping...")
cur.close()
conn.close()
return
names = [description[0] for description in cur.description]
for item in cur:
shortcut = {}
# We store the value of each column under the proper key.
for index, value in enumerate(item):
shortcut[names[index]] = value
action_data = plistlib.load(io.BytesIO(shortcut.pop("action_data", [])))
actions = []
for action_entry in action_data:
action = {}
action["identifier"] = action_entry["WFWorkflowActionIdentifier"]
action["parameters"] = action_entry["WFWorkflowActionParameters"]
# URLs might be in multiple fields, do a simple regex search across the parameters.
extracted_urls = check_for_links(str(action["parameters"]))
# Remove quoting characters that may have been captured by the regex.
action["urls"] = [url.rstrip("',") for url in extracted_urls]
actions.append(action)
shortcut["isodate"] = convert_timestamp_to_iso(convert_mactime_to_unix(shortcut.pop("created_date")))
shortcut["modified_date"] = convert_timestamp_to_iso(convert_mactime_to_unix(shortcut["modified_date"]))
shortcut["parsed_actions"] = len(actions)
shortcut["action_urls"] = list(itertools.chain(*[action["urls"] for action in actions]))
self.results.append(shortcut)
cur.close()
conn.close()
self.log.info("Extracted a total of %d Shortcuts", len(self.results))

View File

@@ -1,119 +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 sqlite3
from base64 import b64encode
from mvt.common.utils import (check_for_links, convert_mactime_to_unix,
convert_timestamp_to_iso)
from ..base import IOSExtraction
SMS_BACKUP_IDS = [
"3d0d7e5fb2ce288813306e4d4636395e047a3d28",
]
SMS_ROOT_PATHS = [
"private/var/mobile/Library/SMS/sms.db",
]
class SMS(IOSExtraction):
"""This module extracts all SMS messages containing links."""
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,
log=log, results=results)
def serialize(self, record):
text = record["text"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "sms_received",
"data": f"{record['service']}: {record['guid']} \"{text}\" from {record['phone_number']} ({record['account']})"
}
def check_indicators(self):
if not self.indicators:
return
for message in self.results:
message_links = check_for_links(message.get("text", ""))
if self.indicators.check_domains(message_links):
self.detected.append(message)
def run(self):
self._find_ios_database(backup_ids=SMS_BACKUP_IDS,
root_paths=SMS_ROOT_PATHS)
self.log.info("Found SMS database at path: %s", self.file_path)
try:
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
message.*,
handle.id as "phone_number"
FROM message, handle
WHERE handle.rowid = message.handle_id;
""")
# Force the query early to catch database issues
items = list(cur)
except sqlite3.DatabaseError as e:
conn.close()
if "database disk image is malformed" in str(e):
self._recover_sqlite_db_if_needed(self.file_path, forced=True)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
message.*,
handle.id as "phone_number"
FROM message, handle
WHERE handle.rowid = message.handle_id;
""")
items = list(cur)
else:
raise e
names = [description[0] for description in cur.description]
for item in items:
message = {}
for index, value in enumerate(item):
# We base64 escape some of the attributes that could contain
# binary data.
if (names[index] == "attributedBody" or
names[index] == "payload_data" or
names[index] == "message_summary_info") and value:
value = b64encode(value).decode()
# We store the value of each column under the proper key.
message[names[index]] = value
# We convert Mac's ridiculous timestamp format.
message["isodate"] = convert_timestamp_to_iso(convert_mactime_to_unix(message["date"]))
message["direction"] = ("sent" if message.get("is_from_me", 0) == 1 else "received")
# Sometimes "text" is None instead of empty string.
if not message.get("text", None):
message["text"] = ""
if message.get("text", "").startswith("ALERT: State-sponsored attackers may be targeting your iPhone"):
self.log.warn("Apple warning about state-sponsored attack received on the %s", message["isodate"])
self.results.append(message)
else:
# Extract links from the SMS message.
message_links = check_for_links(message.get("text", ""))
# If we find links in the messages or if they are empty we add them to the list.
if message_links or message.get("text", "").strip() == "":
self.results.append(message)
cur.close()
conn.close()
self.log.info("Extracted a total of %d SMS messages containing links", len(self.results))

View File

@@ -1,86 +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 sqlite3
from base64 import b64encode
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
SMS_BACKUP_IDS = [
"3d0d7e5fb2ce288813306e4d4636395e047a3d28",
]
SMS_ROOT_PATHS = [
"private/var/mobile/Library/SMS/sms.db",
]
class SMSAttachments(IOSExtraction):
"""This module extracts all info about SMS/iMessage attachments."""
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,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "sms_attachment",
"data": f"{record['service']}: Attachment '{record['transfer_name']}' {record['direction']} from {record['phone_number']} "
f"with {record['total_bytes']} bytes (is_sticker: {record['is_sticker']}, has_user_info: {record['has_user_info']})"
}
def run(self):
self._find_ios_database(backup_ids=SMS_BACKUP_IDS,
root_paths=SMS_ROOT_PATHS)
self.log.info("Found SMS database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
attachment.ROWID as "attachment_id",
attachment.*,
message.service as "service",
handle.id as "phone_number"
FROM attachment
LEFT JOIN message_attachment_join ON message_attachment_join.attachment_id = attachment.ROWID
LEFT JOIN message ON message.ROWID = message_attachment_join.message_id
LEFT JOIN handle ON handle.ROWID = message.handle_id;
""")
names = [description[0] for description in cur.description]
for item in cur:
attachment = {}
for index, value in enumerate(item):
if (names[index] in ["user_info", "sticker_user_info",
"attribution_info",
"ck_server_change_token_blob",
"sr_ck_server_change_token_blob"]) and value:
value = b64encode(value).decode()
attachment[names[index]] = value
attachment["isodate"] = convert_timestamp_to_iso(convert_mactime_to_unix(attachment["created_date"]))
attachment["start_date"] = convert_timestamp_to_iso(convert_mactime_to_unix(attachment["start_date"]))
attachment["direction"] = ("sent" if attachment["is_outgoing"] == 1 else "received")
attachment["has_user_info"] = attachment["user_info"] is not None
attachment["service"] = attachment["service"] or "Unknown"
attachment["filename"] = attachment["filename"] or "NULL"
if (attachment["filename"].startswith("/var/tmp/") and attachment["filename"].endswith("-1") and
attachment["direction"] == "received"):
self.log.warn(f"Suspicious iMessage attachment '{attachment['filename']}' on {attachment['isodate']}")
self.detected.append(attachment)
self.results.append(attachment)
cur.close()
conn.close()
self.log.info("Extracted a total of %d SMS attachments", len(self.results))

View File

@@ -1,164 +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 sqlite3
from datetime import datetime
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
TCC_BACKUP_IDS = [
"64d0019cb3d46bfc8cce545a8ba54b93e7ea9347",
]
TCC_ROOT_PATHS = [
"private/var/mobile/Library/TCC/TCC.db",
]
AUTH_VALUE_OLD = {
0: "denied",
1: "allowed"
}
AUTH_VALUES = {
0: "denied",
1: "unknown",
2: "allowed",
3: "limited",
}
AUTH_REASONS = {
1: "error",
2: "user_consent",
3: "user_set",
4: "system_set",
5: "service_policy",
6: "mdm_policy",
7: "override_policy",
8: "missing_usage_string",
9: "prompt_timeout",
10: "preflight_unknown",
11: "entitled",
12: "app_type_policy",
}
class TCC(IOSExtraction):
"""This module extracts records from the TCC.db SQLite database."""
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,
log=log, results=results)
def serialize(self, record):
if "last_modified" in record:
if "allowed_value" in record:
msg = f"Access to {record['service']} by {record['client']} {record['allowed_value']}"
else:
msg = f"Access to {record['service']} by {record['client']} {record['auth_value']}"
return {
"timestamp": record["last_modified"],
"module": self.__class__.__name__,
"event": "AccessRequest",
"data": msg
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if self.indicators.check_process(result["client"]):
self.log.warning("Found malicious process in TCC database: %s", result["client"])
self.detected.append(result)
def process_db(self, file_path):
conn = sqlite3.connect(file_path)
cur = conn.cursor()
db_version = "v3"
try:
cur.execute("""SELECT
service, client, client_type, auth_value, auth_reason, last_modified
FROM access;""")
except sqlite3.OperationalError:
# v2 version
try:
cur.execute("""SELECT
service, client, client_type, allowed, prompt_count, last_modified
FROM access;""")
db_version = "v2"
except sqlite3.OperationalError:
cur.execute("""SELECT
service, client, client_type, allowed, prompt_count
FROM access;""")
db_version = "v1"
for row in cur:
service = row[0]
client = row[1]
client_type = row[2]
client_type_desc = "bundle_id" if client_type == 0 else "absolute_path"
if db_version == "v3":
auth_value = row[3]
auth_value_desc = AUTH_VALUES.get(auth_value, "")
auth_reason = row[4]
auth_reason_desc = AUTH_REASONS.get(auth_reason, "unknown")
last_modified = convert_timestamp_to_iso(datetime.utcfromtimestamp((row[5])))
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
self.log.info("Found client \"%s\" with access %s to %s on %s by %s",
client, auth_value_desc, device, last_modified, auth_reason_desc)
self.results.append({
"service": service,
"client": client,
"client_type": client_type_desc,
"auth_value": auth_value_desc,
"auth_reason_desc": auth_reason_desc,
"last_modified": last_modified,
})
else:
allowed_value = row[3]
allowed_desc = AUTH_VALUE_OLD.get(allowed_value, "")
prompt_count = row[4]
if db_version == "v2":
last_modified = convert_timestamp_to_iso(datetime.utcfromtimestamp((row[5])))
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
self.log.info("Found client \"%s\" with access %s to %s at %s",
client, allowed_desc, device, last_modified)
self.results.append({
"service": service,
"client": client,
"client_type": client_type_desc,
"allowed_value": allowed_desc,
"prompt_count": prompt_count,
"last_modified": last_modified
})
else:
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
self.log.info("Found client \"%s\" with access %s to %s",
client, allowed_desc, device)
self.results.append({
"service": service,
"client": client,
"client_type": client_type_desc,
"allowed_value": allowed_desc,
"prompt_count": prompt_count
})
cur.close()
conn.close()
def run(self):
self._find_ios_database(backup_ids=TCC_BACKUP_IDS, root_paths=TCC_ROOT_PATHS)
self.log.info("Found TCC database at path: %s", self.file_path)
self.process_db(self.file_path)
self.log.info("Extracted a total of %d TCC items", len(self.results))

View File

@@ -1,86 +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 datetime
import os
import sqlite3
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
WEBKIT_RESOURCELOADSTATICS_BACKUP_RELPATH = "Library/WebKit/WebsiteData/ResourceLoadStatistics/observations.db"
WEBKIT_RESOURCELOADSTATICS_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/WebKit/WebsiteData/ResourceLoadStatistics/observations.db",
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/observations.db",
]
class WebkitResourceLoadStatistics(IOSExtraction):
"""This module extracts records from WebKit ResourceLoadStatistics observations.db."""
# TODO: Add serialize().
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,
log=log, results=results)
self.results = {}
def check_indicators(self):
if not self.indicators:
return
self.detected = {}
for key, items in self.results.items():
for item in items:
if self.indicators.check_domain(item["registrable_domain"]):
if key not in self.detected:
self.detected[key] = [item, ]
else:
self.detected[key].append(item)
def _process_observations_db(self, db_path, key):
self.log.info("Found WebKit ResourceLoadStatistics observations.db file at path %s", db_path)
self._recover_sqlite_db_if_needed(db_path)
conn = sqlite3.connect(db_path)
cur = conn.cursor()
try:
cur.execute("SELECT * from ObservedDomains;")
except sqlite3.OperationalError:
return
if key not in self.results:
self.results[key] = []
for row in cur:
self.results[key].append({
"domain_id": row[0],
"registrable_domain": row[1],
"last_seen": row[2],
"had_user_interaction": bool(row[3]),
"last_seen_isodate": convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(int(row[2]))),
})
if len(self.results[key]) > 0:
self.log.info("Extracted a total of %d records from %s", len(self.results[key]), db_path)
def run(self):
if self.is_backup:
try:
for backup_file in self._get_backup_files_from_manifest(relative_path=WEBKIT_RESOURCELOADSTATICS_BACKUP_RELPATH):
db_path = self._get_backup_file_from_id(backup_file["file_id"])
key = f"{backup_file['domain']}/{WEBKIT_RESOURCELOADSTATICS_BACKUP_RELPATH}"
if db_path:
self._process_observations_db(db_path=db_path, key=key)
except Exception as e:
self.log.info("Unable to search for WebKit observations.db: %s", e)
elif self.is_fs_dump:
for db_path in self._get_fs_files_from_patterns(WEBKIT_RESOURCELOADSTATICS_ROOT_PATHS):
self._process_observations_db(db_path=db_path, key=os.path.relpath(db_path, self.base_folder))

View File

@@ -1,133 +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 os
import plistlib
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
WEBKIT_SESSION_RESOURCE_LOG_BACKUP_IDS = [
"a500ee38053454a02e990957be8a251935e28d3f",
]
WEBKIT_SESSION_RESOURCE_LOG_BACKUP_RELPATH = "Library/WebKit/WebsiteData/ResourceLoadStatistics/full_browsing_session_resourceLog.plist"
WEBKIT_SESSION_RESOURCE_LOG_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/full_browsing_session_resourceLog.plist",
"private/var/mobile/Containers/Data/Application/*/Library/WebKit/WebsiteData/ResourceLoadStatistics/full_browsing_session_resourceLog.plist",
"private/var/mobile/Library/WebClips/*/Storage/full_browsing_session_resourceLog.plist",
]
class WebkitSessionResourceLog(IOSExtraction):
"""This module extracts records from WebKit browsing session
resource logs, and checks them against any provided list of
suspicious domains.
"""
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,
log=log, results=results)
self.results = {}
@staticmethod
def _extract_domains(entries):
if not entries:
return []
domains = []
for entry in entries:
if "origin" in entry:
domains.append(entry["origin"])
if "domain" in entry:
domains.append(entry["domain"])
return domains
def check_indicators(self):
if not self.indicators:
return
for key, entries in self.results.items():
for entry in entries:
source_domains = self._extract_domains(entry["redirect_source"])
destination_domains = self._extract_domains(entry["redirect_destination"])
# TODO: Currently not used.
# subframe_origins = self._extract_domains(entry["subframe_under_origin"])
# subresource_domains = self._extract_domains(entry["subresource_under_origin"])
all_origins = set([entry["origin"]] + source_domains + destination_domains)
if self.indicators.check_domains(all_origins):
self.detected.append(entry)
redirect_path = ""
if len(source_domains) > 0:
redirect_path += "SOURCE: "
for idx, item in enumerate(source_domains):
source_domains[idx] = f"\"{item}\""
redirect_path += ", ".join(source_domains)
redirect_path += " -> "
redirect_path += f"ORIGIN: \"{entry['origin']}\""
if len(destination_domains) > 0:
redirect_path += " -> "
redirect_path += "DESTINATION: "
for idx, item in enumerate(destination_domains):
destination_domains[idx] = f"\"{item}\""
redirect_path += ", ".join(destination_domains)
self.log.warning("Found HTTP redirect between suspicious domains: %s", redirect_path)
def _extract_browsing_stats(self, log_path):
items = []
with open(log_path, "rb") as handle:
file_plist = plistlib.load(handle)
if "browsingStatistics" not in file_plist:
return items
browsing_stats = file_plist["browsingStatistics"]
for item in browsing_stats:
items.append({
"origin": item.get("PrevalentResourceOrigin", ""),
"redirect_source": item.get("topFrameUniqueRedirectsFrom", ""),
"redirect_destination": item.get("topFrameUniqueRedirectsTo", ""),
"subframe_under_origin": item.get("subframeUnderTopFrameOrigins", ""),
"subresource_under_origin": item.get("subresourceUnderTopFrameOrigins", ""),
"user_interaction": item.get("hadUserInteraction"),
"most_recent_interaction": convert_timestamp_to_iso(item["mostRecentUserInteraction"]),
"last_seen": convert_timestamp_to_iso(item["lastSeen"]),
})
return items
def run(self):
if self.is_backup:
for log_file in self._get_backup_files_from_manifest(relative_path=WEBKIT_SESSION_RESOURCE_LOG_BACKUP_RELPATH):
log_path = self._get_backup_file_from_id(log_file["file_id"])
if not log_path:
continue
self.log.info("Found Safari browsing session resource log at path: %s", log_path)
self.results[log_path] = self._extract_browsing_stats(log_path)
elif self.is_fs_dump:
for log_path in self._get_fs_files_from_patterns(WEBKIT_SESSION_RESOURCE_LOG_ROOT_PATHS):
self.log.info("Found Safari browsing session resource log at path: %s", log_path)
key = os.path.relpath(log_path, self.base_folder)
self.results[key] = self._extract_browsing_stats(log_path)
self.log.info("Extracted records from %d Safari browsing session resource logs",
len(self.results))

View File

@@ -1,241 +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 operator
import sqlite3
from pathlib import Path
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from .base import IOSExtraction
class NetBase(IOSExtraction):
"""This class provides a base for DataUsage and NetUsage 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,
log=log, results=results)
def _extract_net_data(self):
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
ZPROCESS.ZFIRSTTIMESTAMP,
ZPROCESS.ZTIMESTAMP,
ZPROCESS.ZPROCNAME,
ZPROCESS.ZBUNDLENAME,
ZPROCESS.Z_PK,
ZLIVEUSAGE.ZWIFIIN,
ZLIVEUSAGE.ZWIFIOUT,
ZLIVEUSAGE.ZWWANIN,
ZLIVEUSAGE.ZWWANOUT,
ZLIVEUSAGE.Z_PK,
ZLIVEUSAGE.ZHASPROCESS,
ZLIVEUSAGE.ZTIMESTAMP
FROM ZLIVEUSAGE
LEFT JOIN ZPROCESS ON ZLIVEUSAGE.ZHASPROCESS = ZPROCESS.Z_PK
UNION
SELECT ZFIRSTTIMESTAMP, ZTIMESTAMP, ZPROCNAME, ZBUNDLENAME, Z_PK, NULL, NULL, NULL, NULL, NULL, NULL, NULL FROM ZPROCESS WHERE Z_PK NOT IN (SELECT ZHASPROCESS FROM ZLIVEUSAGE);
""")
for row in cur:
# ZPROCESS records can be missing after the JOIN. Handle NULL timestamps.
if row[0] and row[1]:
first_isodate = convert_timestamp_to_iso(convert_mactime_to_unix(row[0]))
isodate = convert_timestamp_to_iso(convert_mactime_to_unix(row[1]))
else:
first_isodate = row[0]
isodate = row[1]
if row[11]:
live_timestamp = convert_timestamp_to_iso(convert_mactime_to_unix(row[11]))
else:
live_timestamp = ""
self.results.append({
"first_isodate": first_isodate,
"isodate": isodate,
"proc_name": row[2],
"bundle_id": row[3],
"proc_id": row[4],
"wifi_in": row[5],
"wifi_out": row[6],
"wwan_in": row[7],
"wwan_out": row[8],
"live_id": row[9],
"live_proc_id": row[10],
"live_isodate": live_timestamp if row[10] else first_isodate,
})
cur.close()
conn.close()
self.log.info("Extracted information on %d processes", len(self.results))
def serialize(self, record):
record_data = f"{record['proc_name']} (Bundle ID: {record['bundle_id']}, ID: {record['proc_id']})"
record_data_usage = record_data + f" WIFI IN: {record['wifi_in']}, WIFI OUT: {record['wifi_out']} - " \
f"WWAN IN: {record['wwan_in']}, WWAN OUT: {record['wwan_out']}"
records = [{
"timestamp": record["live_isodate"],
"module": self.__class__.__name__,
"event": "live_usage",
"data": record_data_usage,
}]
# Only included first_usage and current_usage records when a ZPROCESS entry exists.
if "MANIPULATED" not in record["proc_name"] and "MISSING" not in record["proc_name"] and record["live_proc_id"] is not None:
records.extend([
{
"timestamp": record["first_isodate"],
"module": self.__class__.__name__,
"event": "first_usage",
"data": record_data,
},
{
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "current_usage",
"data": record_data,
}
])
return records
def _find_suspicious_processes(self):
if not self.is_fs_dump:
return
if not self.results:
return
# If we are instructed to run fast, we skip this.
if self.fast_mode:
self.log.info("Flag --fast was enabled: skipping extended search for suspicious processes")
return
self.log.info("Extended search for suspicious processes ...")
files = []
for posix_path in Path(self.base_folder).rglob("*"):
try:
if not posix_path.is_file():
continue
except PermissionError:
continue
files.append([posix_path.name, posix_path.__str__()])
for proc in self.results:
if not proc["bundle_id"]:
self.log.debug("Found process with no Bundle ID with name: %s", proc["proc_name"])
binary_path = None
for file in files:
if proc["proc_name"] == file[0]:
binary_path = file[1]
break
if binary_path:
self.log.debug("Located at %s", binary_path)
else:
msg = f"Could not find the binary associated with the process with name {proc['proc_name']}"
if (proc["proc_name"] is None):
msg = f"Found process entry with empty 'proc_name' : {proc['live_proc_id']} at {proc['live_isodate']}"
elif len(proc["proc_name"]) == 16:
msg = msg + " (However, the process name might have been truncated in the database)"
self.log.warning(msg)
if not proc["live_proc_id"]:
self.log.info(f"Found process entry in ZPROCESS but not in ZLIVEUSAGE : {proc['proc_name']} at {proc['live_isodate']}")
def check_manipulated(self):
"""Check for missing or manipulate DB entries"""
# Don't show duplicates for each missing process.
missing_process_cache = set()
for result in sorted(self.results, key=operator.itemgetter("live_isodate")):
if result["proc_id"]:
continue
# Avoid duplicate warnings for same process.
if result["live_proc_id"] not in missing_process_cache:
missing_process_cache.add(result["live_proc_id"])
self.log.warning("Found manipulated process entry %s. Entry on %s",
result["live_proc_id"], result["live_isodate"])
# Set manipulated proc timestamp so it appears in timeline.
result["first_isodate"] = result["isodate"] = result["live_isodate"]
result["proc_name"] = "MANIPULATED [process record deleted]"
self.detected.append(result)
def find_deleted(self):
"""Identify process which may have been deleted from the DataUsage database"""
results_by_proc = {proc["proc_id"]: proc for proc in self.results if proc["proc_id"]}
all_proc_id = sorted(results_by_proc.keys())
# Fix issue #108
if not all_proc_id:
return
missing_procs, last_proc_id = {}, None
for proc_id in range(min(all_proc_id), max(all_proc_id)):
if proc_id not in all_proc_id:
previous_proc = results_by_proc[last_proc_id]
self.log.info("Missing process %d. Previous process at \"%s\" (%s)",
proc_id, previous_proc["first_isodate"], previous_proc["proc_name"])
missing_procs[proc_id] = {
"proc_id": proc_id,
"prev_proc_id": last_proc_id,
"prev_proc_name": previous_proc["proc_name"],
"prev_proc_bundle": previous_proc["bundle_id"],
"prev_proc_first": previous_proc["first_isodate"],
}
else:
last_proc_id = proc_id
# Add a placeholder entry for the missing processes.
for proc_id, proc in missing_procs.items():
# Set default DataUsage keys.
result = {key: None for key in self.results[0].keys()}
result["first_isodate"] = result["isodate"] = result["live_isodate"] = proc["prev_proc_first"]
result["proc_name"] = "MISSING [follows {}]".format(proc["prev_proc_name"])
result["proc_id"] = result["live_proc_id"] = proc["proc_id"]
result["bundle_id"] = None
self.results.append(result)
self.results = sorted(self.results, key=operator.itemgetter("first_isodate"))
def check_indicators(self):
# Check for manipulated process records.
# TODO: Catching KeyError for live_isodate for retro-compatibility.
# This is not very good.
try:
self.check_manipulated()
self.find_deleted()
except KeyError:
pass
if not self.indicators:
return
for result in self.results:
try:
proc_name = result["proc_name"]
except KeyError:
continue
# Process ID may be empty if process records have been manipulated.
if not result["proc_id"]:
continue
if self.indicators.check_process(proc_name):
self.detected.append(result)

View File

@@ -1,252 +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/
IPHONE_MODELS = [
{"identifier": "iPhone4,1", "description": "iPhone 4S"},
{"identifier": "iPhone5,1", "description": "iPhone 5"},
{"identifier": "iPhone5,2", "description": "iPhone 5"},
{"identifier": "iPhone5,3", "description": "iPhone 5c"},
{"identifier": "iPhone5,4", "description": "iPhone 5c"},
{"identifier": "iPhone6,1", "description": "iPhone 5s"},
{"identifier": "iPhone6,2", "description": "iPhone 5s"},
{"identifier": "iPhone7,1", "description": "iPhone 6 Plus"},
{"identifier": "iPhone7,2", "description": "iPhone 6"},
{"identifier": "iPhone8,1", "description": "iPhone 6s"},
{"identifier": "iPhone8,2", "description": "iPhone 6s Plus"},
{"identifier": "iPhone8,4", "description": "iPhone SE (1st generation)"},
{"identifier": "iPhone9,1", "description": "iPhone 7"},
{"identifier": "iPhone9,2", "description": "iPhone 7 Plus"},
{"identifier": "iPhone9,3", "description": "iPhone 7"},
{"identifier": "iPhone9,4", "description": "iPhone 7 Plus"},
{"identifier": "iPhone10,1", "description": "iPhone 8"},
{"identifier": "iPhone10,2", "description": "iPhone 8 Plus"},
{"identifier": "iPhone10,3", "description": "iPhone X"},
{"identifier": "iPhone10,4", "description": "iPhone 8"},
{"identifier": "iPhone10,5", "description": "iPhone 8 Plus"},
{"identifier": "iPhone10,6", "description": "iPhone X"},
{"identifier": "iPhone11,2", "description": "iPhone XS"},
{"identifier": "iPhone11,4", "description": "iPhone XS Max"},
{"identifier": "iPhone11,6", "description": "iPhone XS Max"},
{"identifier": "iPhone11,8", "description": "iPhone XR"},
{"identifier": "iPhone12,1", "description": "iPhone 11"},
{"identifier": "iPhone12,3", "description": "iPhone 11 Pro"},
{"identifier": "iPhone12,5", "description": "iPhone 11 Pro Max"},
{"identifier": "iPhone12,8", "description": "iPhone SE (2nd generation)"},
{"identifier": "iPhone13,1", "description": "iPhone 12 mini"},
{"identifier": "iPhone13,2", "description": "iPhone 12"},
{"identifier": "iPhone13,3", "description": "iPhone 12 Pro"},
{"identifier": "iPhone13,4", "description": "iPhone 12 Pro Max"},
{"identifier": "iPhone14,4", "description": "iPhone 13 Mini"},
{"identifier": "iPhone14,5", "description": "iPhone 13"},
{"identifier": "iPhone14,2", "description": "iPhone 13 Pro"},
{"identifier": "iPhone14,3", "description": "iPhone 13 Pro Max"},
]
IPHONE_IOS_VERSIONS = [
{"build": "1A543a", "version": "1.0"},
{"build": "1C25", "version": "1.0.1"},
{"build": "1C28", "version": "1.0.2"},
{"build": "3A109a", "version": "1.1.1"},
{"build": "3B48b", "version": "1.1.2"},
{"build": "4A93", "version": "1.1.3"},
{"build": "4A102", "version": "1.1.4"},
{"build": "5A347", "version": "2.0"},
{"build": "5B108", "version": "2.0.1"},
{"build": "5C1", "version": "2.0.2"},
{"build": "5F136", "version": "2.1"},
{"build": "5G77", "version": "2.2"},
{"build": "5H11", "version": "2.2.1"},
{"build": "7A341", "version": "3.0"},
{"build": "7A400", "version": "3.0.1"},
{"build": "7C144", "version": "3.1"},
{"build": "7D11", "version": "3.1.2"},
{"build": "7E18", "version": "3.1.3"},
{"build": "8A293", "version": "4.0"},
{"build": "8A306", "version": "4.0.1"},
{"build": "8B117", "version": "4.1"},
{"build": "8C148", "version": "4.2"},
{"build": "8C148a", "version": "4.2.1"},
{"build": "8C148", "version": "4.2.1"},
{"build": "8E600", "version": "4.2.10"},
{"build": "8E401", "version": "4.2.8"},
{"build": "8E501", "version": "4.2.9"},
{"build": "8F190", "version": "4.3"},
{"build": "8J2", "version": "4.3.3"},
{"build": "8K2", "version": "4.3.4"},
{"build": "8L1", "version": "4.3.5"},
{"build": "9A334", "version": "5.0"},
{"build": "9A405", "version": "5.0.1"},
{"build": "9A406", "version": "5.0.1"},
{"build": "9B176", "version": "5.1"},
{"build": "9B179", "version": "5.1"},
{"build": "9B206", "version": "5.1.1"},
{"build": "9B208", "version": "5.1.1"},
{"build": "10A403", "version": "6.0"},
{"build": "10A405", "version": "6.0"},
{"build": "10A523", "version": "6.0.1"},
{"build": "10A525", "version": "6.0.1"},
{"build": "10A551", "version": "6.0.2"},
{"build": "10B141", "version": "6.1"},
{"build": "10B144", "version": "6.1"},
{"build": "10B142", "version": "6.1"},
{"build": "10B143", "version": "6.1"},
{"build": "10B145", "version": "6.1.1"},
{"build": "10B146", "version": "6.1.2"},
{"build": "10B329", "version": "6.1.3"},
{"build": "10B350", "version": "6.1.4"},
{"build": "10B500", "version": "6.1.6"},
{"build": "11B511", "version": "7.0.3"},
{"build": "11B554a", "version": "7.0.4"},
{"build": "11B601", "version": "7.0.5"},
{"build": "11B651", "version": "7.0.6"},
{"build": "11D169", "version": "7.1"},
{"build": "11D167", "version": "7.1"},
{"build": "11D201", "version": "7.1.1"},
{"build": "11D257", "version": "7.1.2"},
{"build": "12A365", "version": "8.0"},
{"build": "12A366", "version": "8.0"},
{"build": "12A402", "version": "8.0.1"},
{"build": "12A405", "version": "8.0.2"},
{"build": "12B411", "version": "8.1"},
{"build": "12B435", "version": "8.1.1"},
{"build": "12B436", "version": "8.1.1"},
{"build": "12B440", "version": "8.1.2"},
{"build": "12B466", "version": "8.1.3"},
{"build": "12D508", "version": "8.2"},
{"build": "12F70", "version": "8.3"},
{"build": "12H143", "version": "8.4"},
{"build": "12H321", "version": "8.4.1"},
{"build": "13A344", "version": "9.0"},
{"build": "13A342", "version": "9.0"},
{"build": "13A343", "version": "9.0"},
{"build": "13A404", "version": "9.0.1"},
{"build": "13A405", "version": "9.0.1"},
{"build": "13A452", "version": "9.0.2"},
{"build": "13B143", "version": "9.1"},
{"build": "13C75", "version": "9.2"},
{"build": "13D15", "version": "9.2.1"},
{"build": "13D20", "version": "9.2.1"},
{"build": "13E237", "version": "9.3"},
{"build": "13E233", "version": "9.3"},
{"build": "13E234", "version": "9.3"},
{"build": "13E238", "version": "9.3.1"},
{"build": "13F69", "version": "9.3.2"},
{"build": "13G34", "version": "9.3.3"},
{"build": "13G35", "version": "9.3.4"},
{"build": "13G36", "version": "9.3.5"},
{"build": "13G37", "version": "9.3.6"},
{"build": "14A403", "version": "10.0.1"},
{"build": "14A456", "version": "10.0.2"},
{"build": "14A551", "version": "10.0.3"},
{"build": "14B72", "version": "10.1"},
{"build": "14B72c", "version": "10.1"},
{"build": "14B150", "version": "10.1.1"},
{"build": "14B100", "version": "10.1.1"},
{"build": "14C92", "version": "10.2"},
{"build": "14D27", "version": "10.2.1"},
{"build": "14E277", "version": "10.3"},
{"build": "14E304", "version": "10.3.1"},
{"build": "14F89", "version": "10.3.2"},
{"build": "14G60", "version": "10.3.3"},
{"build": "14G61", "version": "10.3.4"},
{"build": "15A372", "version": "11.0"},
{"build": "15A402", "version": "11.0.1"},
{"build": "15A421", "version": "11.0.2"},
{"build": "15A432", "version": "11.0.3"},
{"build": "15B93", "version": "11.1"},
{"build": "15B150", "version": "11.1.1"},
{"build": "15B202", "version": "11.1.2"},
{"build": "15C114", "version": "11.2"},
{"build": "15C153", "version": "11.2.1"},
{"build": "15C202", "version": "11.2.2"},
{"build": "15D60", "version": "11.2.5"},
{"build": "15D100", "version": "11.2.6"},
{"build": "15E216", "version": "11.3"},
{"build": "15E302", "version": "11.3.1"},
{"build": "15F79", "version": "11.4"},
{"build": "15G77", "version": "11.4.1"},
{"build": "16A366", "version": "12.0"},
{"build": "16A404", "version": "12.0.1"},
{"build": "16A405", "version": "12.0.1"},
{"build": "16B92", "version": "12.1"},
{"build": "16B94", "version": "12.1"},
{"build": "16B93", "version": "12.1"},
{"build": "16C50", "version": "12.1.1"},
{"build": "16C104", "version": "12.1.2"},
{"build": "16C101", "version": "12.1.2"},
{"build": "16D39", "version": "12.1.3"},
{"build": "16D40", "version": "12.1.3"},
{"build": "16D57", "version": "12.1.4"},
{"build": "16E227", "version": "12.2"},
{"build": "16F156", "version": "12.3"},
{"build": "16F203", "version": "12.3.1"},
{"build": "16F250", "version": "12.3.2"},
{"build": "16G77", "version": "12.4"},
{"build": "16G102", "version": "12.4.1"},
{"build": "16G114", "version": "12.4.2"},
{"build": "16G130", "version": "12.4.3"},
{"build": "16G161", "version": "12.4.5"},
{"build": "16G183", "version": "12.4.6"},
{"build": "16G192", "version": "12.4.7"},
{"build": "16G201", "version": "12.4.8"},
{"build": "16H5", "version": "12.4.9"},
{"build": "16H20", "version": "12.5"},
{"build": "16H22", "version": "12.5.1"},
{"build": "17A577", "version": "13.0"},
{"build": "17A844", "version": "13.1"},
{"build": "17A854", "version": "13.1.1"},
{"build": "17A860", "version": "13.1.2"},
{"build": "17A861", "version": "13.1.2"},
{"build": "17A878", "version": "13.1.3"},
{"build": "17B84", "version": "13.2"},
{"build": "17B102", "version": "13.2.2"},
{"build": "17B111", "version": "13.2.3"},
{"build": "17C54", "version": "13.3"},
{"build": "17D50", "version": "13.3.1"},
{"build": "17E255", "version": "13.4"},
{"build": "17E262", "version": "13.4.1"},
{"build": "17E8258", "version": "13.4.1"},
{"build": "17F75", "version": "13.5"},
{"build": "17F80", "version": "13.5.1"},
{"build": "17G68", "version": "13.6"},
{"build": "17G80", "version": "13.6.1"},
{"build": "17H35", "version": "13.7"},
{"build": "18A373", "version": "14.0"},
{"build": "18A393", "version": "14.0.1"},
{"build": "18A8395", "version": "14.1"},
{"build": "18B92", "version": "14.2"},
{"build": "18C66", "version": "14.3"},
{"build": "18D52", "version": "14.4"},
{"build": "18D61", "version": "14.4.1"},
{"build": "18D70", "version": "14.4.2"},
{"build": "18E199", "version": "14.5"},
{"build": "18E212", "version": "14.5.1"},
{"build": "18F72", "version": "14.6"},
{"build": "18G69", "version": "14.7"},
{"build": "18G82", "version": "14.7.1"},
{"build": "18H17", "version": "14.8"},
{"build": "18H107", "version": "14.8.1"},
{"build": "19A341", "version": "15.0"},
{"build": "19A346", "version": "15.0"},
{"build": "19A348", "version": "15.0.1"},
{"build": "19A404", "version": "15.0.2"},
{"build": "19B74", "version": "15.1"},
{"build": "19B81", "version": "15.1.1"},
{"build": "19C56", "version": "15.2"},
{"build": "19C63", "version": "15.2.1"},
]
def get_device_desc_from_id(identifier, devices_list=IPHONE_MODELS):
for model in IPHONE_MODELS:
if identifier == model["identifier"]:
return model["description"]
def find_version_by_build(build):
build = build.upper()
for version in IPHONE_IOS_VERSIONS:
if build == version["build"]:
return version["version"]

View File

@@ -1,14 +0,0 @@
[
{
"name": "NSO Group Pegasus Indicators of Compromise",
"source": "Amnesty International",
"reference": "https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/",
"stix2_url": "https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-07-18_nso/pegasus.stix2"
},
{
"name": "Cytrox Predator Spyware Indicators of Compromise",
"source": "Meta, Amnesty International, Citizen Lab",
"reference": "https://citizenlab.ca/2021/12/pegasus-vs-predator-dissidents-doubly-infected-iphone-reveals-cytrox-mercenary-spyware/",
"stix2_url": "https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-12-16_cytrox/cytrox.stix2"
}
]

106
pyproject.toml Normal file
View File

@@ -0,0 +1,106 @@
[project]
name = "mvt"
dynamic = ["version"]
authors = [
{name = "Claudio Guarnieri", email = "nex@nex.sx"}
]
maintainers = [
{name = "Etienne Maynier", email = "tek@randhome.io"},
{name = "Donncha Ó Cearbhaill", email = "donncha.ocearbhaill@amnesty.org"},
{name = "Rory Flynn", email = "rory.flynn@amnesty.org"}
]
description = "Mobile Verification Toolkit"
readme = "README.md"
keywords = ["security", "mobile", "forensics", "malware"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Information Technology",
"Operating System :: OS Independent",
"Programming Language :: Python"
]
dependencies = [
"click >=8.1.3",
"rich >=12.6.0",
"tld >=0.12.6",
"requests >=2.28.1",
"simplejson >=3.17.6",
"packaging >=21.3",
"appdirs >=1.4.4",
"iOSbackup >=0.9.923",
"adb-shell[usb] >=0.4.3",
"libusb1 >=3.0.0",
"cryptography >=42.0.5",
"pyyaml >=6.0",
"pyahocorasick >= 2.0.0",
"betterproto >=1.2.0",
"pydantic >= 2.10.0",
"pydantic-settings >= 2.7.0",
'backports.zoneinfo; python_version < "3.9"',
]
requires-python = ">= 3.8"
[project.urls]
homepage = "https://docs.mvt.re/en/latest/"
repository = "https://github.com/mvt-project/mvt"
[project.scripts]
mvt-ios = "mvt.ios:cli"
mvt-android = "mvt.android:cli"
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[tool.coverage.run]
omit = [
"tests/*",
]
[tool.coverage.html]
directory= "htmlcov"
[tool.mypy]
install_types = true
non_interactive = true
ignore_missing_imports = true
packages = "src"
[tool.pytest.ini_options]
addopts = "-ra -q --cov=mvt --cov-report html --junitxml=pytest.xml --cov-report=term-missing:skip-covered"
testpaths = [
"tests"
]
[tool.ruff.lint]
select = ["C90", "E", "F", "W"] # flake8 default set
ignore = [
"E501", # don't enforce line length violations
"C901", # complex-structure
# These were previously ignored but don't seem to be required:
# "E265", # no-space-after-block-comment
# "F401", # unused-import
# "E127", # not included in ruff
# "W503", # not included in ruff
# "E226", # missing-whitespace-around-arithmetic-operator
# "E203", # whitespace-before-punctuation
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] # unused-import
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.setuptools]
include-package-data = true
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
mvt = ["ios/data/*.json"]
[tool.setuptools.dynamic]
version = {attr = "mvt.common.version.MVT_VERSION"}

View File

@@ -1,68 +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 os
from setuptools import find_packages, setup
from mvt.common.version import MVT_VERSION
this_directory = os.path.abspath(os.path.dirname(__file__))
readme_path = os.path.join(this_directory, "README.md")
with open(readme_path, encoding="utf-8") as handle:
long_description = handle.read()
requires = (
# Base dependencies:
"click>=8.0.3",
"rich>=10.12.0",
"tld>=0.12.6",
"tqdm>=4.62.3",
"requests>=2.26.0",
"simplejson>=3.17.5",
"packaging>=21.0",
"appdirs>=1.4.4",
# iOS dependencies:
"iOSbackup>=0.9.921",
# Android dependencies:
"adb-shell>=0.4.2",
"libusb1>=2.0.1",
)
def get_package_data(package):
walk = [(dirpath.replace(package + os.sep, "", 1), filenames)
for dirpath, dirnames, filenames in os.walk(package)
if not os.path.exists(os.path.join(dirpath, "__init__.py"))]
filepaths = []
for base, filenames in walk:
filepaths.extend([os.path.join(base, filename)
for filename in filenames])
return {package: filepaths}
setup(
name="mvt",
version=MVT_VERSION,
description="Mobile Verification Toolkit",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/mvt-project/mvt",
entry_points={
"console_scripts": [
"mvt-ios = mvt.ios:cli",
"mvt-android = mvt.android:cli",
],
},
install_requires=requires,
packages=find_packages(),
package_data=get_package_data("mvt"),
include_package_data=True,
keywords="security mobile forensics malware",
license="MVT",
classifiers=[
],
)

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

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