Compare commits

...

386 Commits

Author SHA1 Message Date
Nex
bb0e41e949 Bumped version 2022-06-03 11:44:44 +02:00
Nex
6844f0b90b Added new iOS version 2022-06-03 11:43:42 +02:00
Tek
fb2a0ba668 Merge pull request #280 from Niek/patch-1
Fix stalkerware STIX URL
2022-06-02 10:29:53 +02:00
Niek van der Maas
e34f8f3660 Fix stalkerware STIX URL 2022-06-02 09:57:56 +02:00
tek
fd3f9dba8f Adds automated flake8 check in github workflow 2022-05-25 14:38:51 +02:00
Nex
27f0364c1d Removed static android data files 2022-05-09 11:09:07 +02:00
Nex
8dac714214 Added support for multiprocessing when decrypting an iOS backup 2022-05-08 17:25:27 +02:00
Nex
732a712e3d Changed path where to seek packages 2022-05-08 17:06:34 +02:00
Nex
6d278d4bec Actually, can also move version to setup.cfg 2022-05-08 15:17:19 +02:00
Nex
c39b4d2179 Minimized setup.py and moved all possible to setup.cfg 2022-05-08 15:15:00 +02:00
Nex
a653fd5253 Moved dependencies to setup.cfg 2022-05-08 15:07:17 +02:00
Nex
f754bf274d Unfortunately until #233 is resolved, we have to enforce Python >= 3.8 as well 2022-05-08 14:59:34 +02:00
Nex
fcac8a8c7d Updated README 2022-05-08 14:57:33 +02:00
Nex
d82c788a18 Removed AUTHORS file in favor of explicit copyright notice 2022-05-08 14:53:50 +02:00
Nex
946a9ef02b Added missing import 2022-05-08 14:51:31 +02:00
Nex
c343eed5a0 Moved flake8 config to setup.cfg 2022-05-08 14:50:31 +02:00
Nex
6162a1e1f2 Sorted imports 2022-05-08 14:47:54 +02:00
Nex
f61729deed Starting to move declarative information to setup.cfg 2022-05-08 14:45:14 +02:00
Nex
7a00e88f1f Explicit license version in setup.py 2022-05-08 14:26:43 +02:00
Nex
ff41efba72 Bumped version 2022-04-05 21:46:38 +02:00
Nex
26e6a00bf5 Added new iOS version 2022-04-04 13:25:13 +02:00
Nex
9d61b9048c Fixed variable names mismatch and styling 2022-03-30 08:49:22 +02:00
tek
9950b3d6c2 Add appops dumpsys parser and modules 2022-03-30 01:16:22 +02:00
tek
e0d30ea990 Removes check for a deprecated Android setting 2022-03-29 18:37:56 +02:00
tek
293752f90a Merge branch 'main' of github.com:mvt-project/mvt 2022-03-28 20:12:17 +02:00
tek
ac1e5c29d3 Clarifies the backup path needed in the documentation 2022-03-28 15:38:20 +02:00
Nex
d868e6d9f0 Merge pull request #259 from mlowdi/configuration_profiles_fix
base64 encoding fixes in ConfigurationProfiles module
2022-03-28 14:08:22 +02:00
Martin L. Fällman
f5cb7f06e1 Fix for missing base64 encoding of MDM certificate data in JSON output 2022-03-25 20:36:30 +01:00
Martin L. Fällman
5ce8035820 Add Sublime Text project files to .gitignore 2022-03-25 20:16:20 +01:00
Donncha Ó Cearbhaill
e3a8bde150 Fix path error when relative '.' used as backup source directory 2022-03-20 15:56:13 +01:00
Nex
d6af7c8cca Updating flake8 config and fixed some violations 2022-03-18 11:10:06 +01:00
Nex
6584d8232c Fixed bug in bugreport packages parser 2022-03-16 10:20:53 +01:00
Nex
3487078c03 Added flake8 configuration file 2022-03-15 13:36:03 +01:00
Nex
bc5d386be7 Bumped version 2022-03-15 11:19:22 +01:00
Nex
03efc8494b Added new iOS version 2022-03-15 11:19:05 +01:00
Nex
0b3f529cfa Bumped version 2022-03-14 10:22:29 +01:00
Nex
9bdef6ede4 Fixing spacing 2022-03-10 11:35:49 +01:00
Nex
fc9a27d030 Sorted imports 2022-03-10 11:33:54 +01:00
tek
f5f3660d82 Updates the documentation 2022-03-08 14:17:41 +01:00
Tek
712f5bcb9b Merge pull request #251 from mvt-project/feature/read-sms-adb-backup
Add initial implementation of SMS extraction using ADB
2022-03-05 23:27:55 +01:00
Donncha Ó Cearbhaill
ac26aa964a Fix exception with bad password 2022-03-04 17:24:26 +01:00
Donncha Ó Cearbhaill
be511dcb51 Refactor SMS ADB code to use backup functions 2022-03-04 17:06:10 +01:00
Donncha Ó Cearbhaill
b44c67e699 Refactor some of the decryption code 2022-03-04 17:04:32 +01:00
tek
a4d08f8f35 Replaces pyaes with cryptography and reorganize backup parser code 2022-03-04 15:05:10 +01:00
tek
6cc67f3c1d Fixes testing issue 2022-03-04 12:34:54 +01:00
tek
0d5377597f Merge branch 'main' into feature/read-sms-adb-backup 2022-03-04 12:30:45 +01:00
tek
86c79075ff Reorganise code for backup modules 2022-03-04 10:10:56 +01:00
tek
9940b1d145 Adds test of the check-backup command 2022-03-01 18:54:34 +01:00
tek
b07fb092aa Adds tests for SMS module 2022-03-01 13:11:50 +01:00
tek
639c163297 Adds partial compression support in Android Backup parsing 2022-02-23 16:18:45 +01:00
tek
8eb30e3a02 Improves android backup parsing for check-backup and check-adb 2022-02-23 15:07:13 +01:00
Donncha Ó Cearbhaill
cd0e7d9879 Fix syntax error with broken comment 2022-02-18 15:09:08 +01:00
Donncha Ó Cearbhaill
bdaaf15434 Add initial implementation of SMS extraction using ADB 2022-02-17 18:17:38 +01:00
tek
699824d9ff Adds iOS version 15.3.1 2022-02-11 12:25:53 +01:00
Nex
8cca78d222 Missing newline 2022-02-09 13:31:27 +01:00
Nex
57cbb0ed56 Fixed typo 2022-02-09 13:30:31 +01:00
Nex
e9cc6b3928 Fixed code styling and added missing check in adb getprop 2022-02-09 13:20:09 +01:00
tek
6d47d4d416 Adds warning for outdated iOS systems 2022-02-08 15:49:10 +01:00
tek
ed54761747 Adds warning if phone is outdated in getprop module 2022-02-07 17:28:01 +01:00
Nex
71c4ba799f Fixed help message for download-apks 2022-02-04 13:42:32 +01:00
Nex
09a6f291c0 Bumped version 2022-02-04 13:39:37 +01:00
Nex
b50be69dd4 Bumped version 2022-02-04 13:38:04 +01:00
Nex
6fc6102b73 Improved parsing of bugreports by finding dumpstate file name from main_entry.txt 2022-02-04 13:34:40 +01:00
Nex
3fe5d8dc8d Fixing battery stats history parsing 2022-02-03 22:18:37 +01:00
Nex
fec6210d1b Fixed parsing of dbinfo to support multiple formats 2022-02-03 20:36:47 +01:00
Nex
6a723e533f Fixed logging in adb modules 2022-02-03 20:19:07 +01:00
Nex
ed8a5a3845 Fixed dumpstate parsing for different formats and added logging 2022-02-03 19:55:18 +01:00
Nex
04225a4455 Ignoring decoding errors 2022-02-03 19:40:36 +01:00
Nex
5987f218be Supporting multiple file names 2022-02-03 19:26:45 +01:00
Nex
748780476e Fixed a typo and catching exception 2022-02-03 19:20:26 +01:00
Nex
c522b54326 Supporting searching files by multiple patterns 2022-02-03 17:21:29 +01:00
Nex
0e0e346916 Fixed issue in parsing batterystats daily 2022-02-03 13:36:08 +01:00
Nex
69daf3c3cd Added module checking SELinux enforcement status 2022-02-03 11:34:02 +01:00
Nex
998d87900d Merge pull request #247 from mvt-project/android-split-parsers
Android split parsers
2022-02-03 00:06:53 +01:00
Nex
230f81879a Added check for indicators to Processes 2022-02-03 00:06:15 +01:00
Nex
df42efb7cb Added getprop parser 2022-02-02 22:07:47 +01:00
Nex
0922e569b0 Sorted imports 2022-02-02 22:00:48 +01:00
Nex
03092cf3b7 Attempting split of parsers 2022-02-02 21:58:11 +01:00
Nex
ab63a02c9f Code clean-ups 2022-02-02 19:18:47 +01:00
Nex
a833dda581 Added getprop bugreport module 2022-02-02 19:00:20 +01:00
Nex
189b1d7fc6 Fixed tests 2022-02-02 18:14:10 +01:00
Nex
b1b282ac20 Merge pull request #246 from mvt-project/check-bugreport
Check bugreport
2022-02-02 18:12:24 +01:00
Nex
512c349c2c Sorted imports 2022-02-02 16:10:24 +01:00
Nex
b94ba28873 Supporting loading from extracted folder 2022-02-02 16:10:12 +01:00
Nex
564efc3629 Sorted imports 2022-02-02 15:49:24 +01:00
Nex
9c62e6e4d6 Added Packages module 2022-02-02 15:47:55 +01:00
Nex
153f6cce02 Returning stix2 file name with iocs as well 2022-02-02 14:57:32 +01:00
Nex
47f9a0104c Added a break for speed 2022-02-02 14:54:40 +01:00
Nex
bdad23feee Refactored indicators to support multiple malware/collections per stix2 file 2022-02-02 14:53:26 +01:00
Donncha Ó Cearbhaill
5416b66915 Add CI and downloads page 2022-02-02 12:45:06 +01:00
Nex
e2936c3d33 Added new check-bugreport command and modules 2022-02-02 00:09:53 +01:00
Nex
3483ca1584 Package dumpsys parsing as static method 2022-02-01 21:45:26 +01:00
Nex
7b107edf1f Bumped version 2022-02-01 17:54:01 +01:00
Nex
b97ce7651a Fixed missing checks for indicators instance (ref: #245) 2022-02-01 17:48:19 +01:00
Nex
52a204cab6 Obtaining permissions for installed packages 2022-02-01 15:33:19 +01:00
Nex
1b335fda1d Renamed function argument to more descriptive 2022-02-01 15:07:43 +01:00
Nex
2ad175eae2 Renamed package to package_name for consistency 2022-02-01 14:27:00 +01:00
Nex
2d00dca5bd Bumped version 2022-02-01 12:46:31 +01:00
Nex
c8e50eb958 Merge pull request #244 from dangaffey/patch-1
Update docker.md
2022-02-01 11:54:04 +01:00
Dan Gaffey
1f049fc8ba Update docker.md
Had to run an additional Docker flag to get it building on the new M1 chip from Apple. Figured it would be helpful to point that out in the Docs for the less initiated users.
2022-01-31 20:22:54 -05:00
Nex
434738a306 Better regexp formatting 2022-01-31 13:05:03 +01:00
Nex
06cd640c5e Using static methods 2022-01-31 12:58:33 +01:00
Nex
fb8a7ca104 Enforce consistency in Android modules 2022-01-31 11:30:49 +01:00
Nex
8d15ff58dd Renamed matched field name to singular 2022-01-30 20:29:09 +01:00
Nex
eb5f07a75d Updated copyright notice 2022-01-30 20:15:01 +01:00
Nex
ececf1a6b2 Added module to extract db queries 2022-01-30 19:43:09 +01:00
Nex
851cd52602 Ordering and clean-up 2022-01-30 16:41:32 +01:00
Nex
8db04fc991 Added module to parse battery daily stats package updates 2022-01-30 16:02:24 +01:00
Nex
3d0ba56e1f Fixed parsing of wake events 2022-01-30 15:20:03 +01:00
Nex
c48a4e8f50 Fixed variable name 2022-01-30 04:12:19 +01:00
Nex
001c2998a5 Removed unnecessary newlines 2022-01-30 04:11:46 +01:00
Nex
5e7c5727af Added check for indicators to dumpsys modules 2022-01-30 04:08:48 +01:00
Nex
883fbaeb88 Parsing records from accessibility and battery history 2022-01-30 03:44:41 +01:00
Nex
6f0012cede Removed modules which are only duplicated outputs from dumpsys full 2022-01-30 03:39:26 +01:00
Nex
458e80ccbb Adding module to process battery history 2022-01-30 03:34:16 +01:00
Nex
c8185fdbd8 Small code clean-ups 2022-01-29 15:13:35 +01:00
Nex
67eea3edec Merge pull request #241 from yallxe/main
Make utf-8 as a default for open()
2022-01-29 14:44:16 +01:00
Yallxe
bc86d159b8 Clear 'debugging' things 2022-01-29 12:28:22 +01:00
Yallxe
43b1612dfe Set utf-8 as an encoding for open()
Not every system uses 'utf-8' as a default encoding for opening files in Python.

Before you say that there must be a way to set default encoding in one line, no, there is not. At least, I didn't found a way to do this.
2022-01-29 12:18:18 +01:00
Yallxe
156f1084f1 Add IDEA to gitignore 2022-01-29 12:03:00 +01:00
Nex
49e34f6299 Better parsing of dumpsys package and added parsing of Activities too 2022-01-29 03:50:33 +01:00
Nex
d88a66dd54 Fixed typo 2022-01-29 01:13:52 +01:00
Nex
d3ed778ae4 Fixed comment stylling 2022-01-29 01:13:29 +01:00
tek
4c3306c272 Separate receivers parsing in DumpsysReceivers 2022-01-29 01:06:32 +01:00
Nex
1c912f68fe Bumped version 2022-01-28 22:25:41 +01:00
Nex
10a640d3f7 Temporary disabing VirusTotal lookup because of API issues 2022-01-28 22:25:21 +01:00
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
Nex
e1211991aa Bumped version 2022-01-23 14:17:43 +01:00
Nex
8ae9ca328c Added log line at the end to highlight number of detections 2022-01-21 16:50:32 +01:00
Nex
0e2eb51732 Fixed checking of indicators in filesystem module 2022-01-21 16:30:34 +01:00
Nex
b35cd4bc73 Added support for context-aware indicators.
This way when a detection is logged, the user can know which STIX2
file was matched by the module
2022-01-21 16:26:58 +01:00
Nex
1b4f99a31d Trying to catch missing argument error (ref: #211) 2022-01-21 12:20:22 +01:00
tek
e4e1716729 Bumped version 2022-01-20 15:28:42 +01:00
tek
083bc12351 Merge branch 'feature/check-file-path' 2022-01-20 15:19:37 +01:00
tek
cf6d392460 Adds more details on the download-iocs command 2022-01-20 13:29:50 +01:00
tek
95205d8e17 Adds indicators check to iOS TCC module 2022-01-18 17:12:20 +01:00
Nex
1460828c30 Uniforming style in test units 2022-01-18 16:33:13 +01:00
Nex
fa84b3f296 Revert "Testing with slightly older version of iOSbackup"
This reverts commit e1efaa5467.
2022-01-18 16:32:22 +01:00
Nex
e1efaa5467 Testing with slightly older version of iOSbackup 2022-01-18 16:27:14 +01:00
Nex
696d42fc6e Disabling tests for 3.7 due to iOSbackup requirements of >= 3.8 2022-01-18 16:22:29 +01:00
Nex
a0e1662726 Somehow mysteriously with >= pip doesn't find the version, with == does 2022-01-18 16:16:03 +01:00
Nex
51645bdbc0 Adding pip install for deps 2022-01-18 16:10:59 +01:00
Nex
bb1b108fd7 Cleaning build workflow 2022-01-18 16:09:01 +01:00
Nex
92f9dcb8a5 Tring to fix build 2022-01-18 16:08:14 +01:00
Nex
a6fd5fe1f3 Bumped version 2022-01-18 16:06:14 +01:00
Nex
3e0ef20fcd . 2022-01-18 16:05:01 +01:00
Nex
01f3acde2e Merge branch 'main' of github.com:mvt-project/mvt 2022-01-18 16:00:52 +01:00
Nex
b697874f56 Conforming the test files 2022-01-18 16:00:03 +01:00
Donncha Ó Cearbhaill
41d699f457 Add PyTest to Github actions 2022-01-18 15:59:16 +01:00
Donncha Ó Cearbhaill
6fcd40f6b6 Fix use of global list instance as self.results variable 2022-01-18 15:53:05 +01:00
tek
38bb583a9e Improves management of file path indicators 2022-01-18 15:50:31 +01:00
Donncha Ó Cearbhaill
48ec2d8fa8 Merge branch 'main' into tests 2022-01-18 15:30:40 +01:00
tek
798805c583 Improves Shortcut output 2022-01-18 13:06:35 +01:00
Nex
24be9e9570 Use default list of indicators files now that some default ones are automatically loaded 2022-01-14 16:26:14 +01:00
Nex
adbd95c559 Dots 2022-01-14 02:01:59 +01:00
Nex
8a707c288a Bumped version 2022-01-14 01:53:10 +01:00
Nex
4c906ad52e Renamed download iocs function 2022-01-14 01:52:57 +01:00
Nex
a2f8030cce Added new iOS versions 2022-01-14 01:41:48 +01:00
Nex
737007afdb Bumped version 2022-01-12 16:18:13 +01:00
Nex
33efeda90a Added TODO note 2022-01-12 16:10:15 +01:00
Nex
146f2ae57d Renaming check function for consistency 2022-01-12 16:02:13 +01:00
Nex
11bc916854 Sorted imports 2022-01-11 16:02:44 +01:00
Nex
3084876f31 Removing unused imports, fixing conditions, new lines 2022-01-11 16:02:01 +01:00
Nex
f63cb585b2 Shortened command to download-iocs 2022-01-11 15:59:01 +01:00
Nex
637aebcd89 Small cleanup 2022-01-11 15:53:10 +01:00
Nex
16a0de3af4 Added new module to highlight installed accessibility services 2022-01-11 15:16:26 +01:00
tek
15fbedccc9 Fixes a minor bug in WebkitResourceLoadStatistics 2022-01-10 18:09:31 +01:00
tek
e0514b20dd Catches exception in Shortcuts module if the table does not exist 2022-01-10 16:58:12 +01:00
Donncha Ó Cearbhaill
b2e9f0361b Fix repeated results due to global results[] variable 2022-01-07 18:24:24 +01:00
Donncha Ó Cearbhaill
e85c70c603 Generate stix2 for each test run 2022-01-07 17:51:21 +01:00
Donncha Ó Cearbhaill
3f8dade610 Move backup binary artifact to seperate folder 2022-01-07 17:08:46 +01:00
Donncha Ó Cearbhaill
54963b0b59 Update test PR to work with latest code, fix flake8 2022-01-07 17:03:53 +01:00
tek
513e2cc704 First test structure 2022-01-07 16:41:19 +01:00
tek
28d57e7178 Add command to download latest public indicators
Squashed commit of the following:

commit c0d9e8d5d188c13e7e5ec0612e99bfb7e25f47d4
Author: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
Date:   Fri Jan 7 16:05:12 2022 +0100

    Update name of indicators JSON file

commit f719e49c5f942cef64931ecf422b6a6e7b8c9f17
Author: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
Date:   Fri Jan 7 15:38:03 2022 +0100

    Do not set indicators option on module if no indicators were loaded

commit a289eb8de936f7d74c6c787cbb8daf5c5bec015c
Author: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
Date:   Fri Jan 7 14:43:00 2022 +0100

    Simplify code for loading IoCs

commit 0804563415ee80d76c13d3b38ffe639fa14caa14
Author: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
Date:   Fri Jan 7 13:43:47 2022 +0100

    Add metadata to IoC entries

commit 97d0e893c1a0736c4931363ff40f09a030b90cf6
Author: tek <tek@randhome.io>
Date:   Fri Dec 17 16:43:09 2021 +0100

    Implements automated loading of indicators

commit c381e14df92ae4d7d846a1c97bcf6639cc526082
Author: tek <tek@randhome.io>
Date:   Fri Dec 17 12:41:15 2021 +0100

    Improves download-indicators

commit b938e02ddfd0b916fd883f510b467491a4a84e5f
Author: tek <tek@randhome.io>
Date:   Fri Dec 17 01:44:26 2021 +0100

    Adds download-indicators for mvt-ios and mvt-android
2022-01-07 16:38:04 +01:00
Nex
dc8eeb618e Merge pull request #229 from NicolaiSoeborg/patch-1
Bump adb read timeout
2021-12-31 11:59:40 +01:00
Nicolai Søborg
c282d4341d Bump adb read timeout
Some adb commands (like `dumpsys`) are very slow and the default timeout is "only" 10s. 
A timeout of 200 seconds is chosen completely at random - works on my phone 🤷

Fixes https://github.com/mvt-project/mvt/issues/113
Fixes https://github.com/mvt-project/mvt/issues/228
2021-12-28 13:56:04 +01:00
tek
681bae2f66 Bump version to v1.4.1 2021-12-27 16:19:25 +01:00
tek
b079246c8a Fixes links to STIX files in the documentation 2021-12-22 16:18:28 +01:00
tek
82b57f1997 Fixes IOC issue in android CLI 2021-12-22 00:19:16 +01:00
Donncha Ó Cearbhaill
8f88f872df Bump to 1.4.0 to skip previously used PyPi versions 2021-12-17 12:52:06 +01:00
Donncha Ó Cearbhaill
2d16218489 Bump version to v1.3.2 2021-12-17 12:24:41 +01:00
Donncha Ó Cearbhaill
3215e797ec Bug fixes for config profile and shortcut module 2021-12-16 22:58:36 +01:00
Donncha Ó Cearbhaill
e65a598903 Add link to Cytrox indicators of compromise in docs 2021-12-16 21:01:56 +01:00
Donncha Ó Cearbhaill
e80c02451c Bump version to 1.3.1. Skipping 1.3 as a tag already exists 2021-12-16 19:27:58 +01:00
Donncha Ó Cearbhaill
5df50f864c Merge branch 'main' into main 2021-12-16 19:21:18 +01:00
Donncha Ó Cearbhaill
45b31bb718 Add support for indentifying known malicious file paths over ADB 2021-12-16 19:16:24 +01:00
Donncha Ó Cearbhaill
e10f1767e6 Update WhatsApp module to search for links in attachments 2021-12-16 18:46:31 +01:00
tek
d64277c0bf Adds missing iOS version 2021-12-16 18:39:22 +01:00
Donncha Ó Cearbhaill
3f3261511a Add module to search for known malicious or suspicious configuration profiles 2021-12-16 17:57:26 +01:00
Donncha Ó Cearbhaill
4cfe75e2d4 Add module to parse iOS Shortcuts and search for malicious actions 2021-12-16 17:47:08 +01:00
tek
cdd90332f7 Adds timeline support to TCC iOS module 2021-12-16 13:57:44 +01:00
tek
d9b29b3739 Fixes indicator issue in the android cli 2021-12-16 12:51:57 +01:00
tek
79bb7d1d4b Fixes indiator parsing bug 2021-12-13 18:37:05 +01:00
tek
a653cb3cfc Implements loading STIX files from env variable MVT_STIX2 2021-12-10 16:11:59 +01:00
tek
b25cc48be0 Fixes issue in Safari Browser State for older iOS versions 2021-12-06 15:04:52 +01:00
tek
40bd9ddc1d Fixes issue with different TCC database versions 2021-12-03 20:31:12 +01:00
Tek
deb95297da Merge pull request #219 from workingreact/main
Fix ConfigurationProfiles
2021-12-03 19:56:43 +01:00
tek
02014b414b Add warning for apple notification 2021-12-03 19:42:35 +01:00
tek
7dd5fe7831 Catch and recover malformed SMS database 2021-12-03 17:46:41 +01:00
workingreact
11d1a3dcee fix typo 2021-12-02 18:31:07 +01:00
workingreact
74f9db2bf2 fix ConfigurationProfiles 2021-12-02 16:55:14 +01:00
tek
356bddc3af Adds new iOS versions 2021-11-28 17:43:50 +01:00
Nex
512f40dcb4 Standardized code with flake8 2021-11-19 15:27:51 +01:00
Nex
b3a464ba58 Removed unused imports 2021-11-19 14:54:53 +01:00
Nex
529df85f0f Sorted imports 2021-11-04 12:58:35 +01:00
Nex
19a6da8fe7 Merge pull request #213 from panelmix/main
Replace NetworkingAnalytics with Analytics
2021-11-02 15:02:57 +01:00
panelmix
34c997f923 Replace NetworkingAnalytics with Analytics 2021-11-02 13:29:12 +01:00
Nex
02bf903411 Bumped version 2021-10-30 13:40:25 +02:00
Nex
7019375767 Merge pull request #210 from hurtcrushing/main
Search for entries in ZPROCESS but not in ZLIVEUSAGE
2021-10-27 14:22:40 +02:00
Nex
34dd27c5d2 Added iPhone 13 2021-10-26 18:33:07 +02:00
Nex
a4d6a08a8b Added iOS 15.1 2021-10-26 18:09:31 +02:00
hurtcrushing
635d3a392d change warning to info 2021-10-25 14:54:03 +02:00
hurtcrushing
2d78bddbba Search for entries in ZPROCESS but not in ZLIVEUSAGE 2021-10-25 14:34:18 +02:00
Nex
c1938d2ead Merge branch 'main' of github.com:mvt-project/mvt 2021-10-25 11:18:12 +02:00
Nex
104b01e5cd Fixed links to docs 2021-10-25 09:19:10 +02:00
Nex
7087e8adb2 Merge pull request #209 from mvt-project/dependabot/pip/docs/mkdocs-1.2.3
Bump mkdocs from 1.2.1 to 1.2.3 in /docs
2021-10-23 20:17:18 +02:00
dependabot[bot]
67608ac02b Bump mkdocs from 1.2.1 to 1.2.3 in /docs
Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.2.1 to 1.2.3.
- [Release notes](https://github.com/mkdocs/mkdocs/releases)
- [Commits](https://github.com/mkdocs/mkdocs/compare/1.2.1...1.2.3)

---
updated-dependencies:
- dependency-name: mkdocs
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-23 11:56:25 +00:00
Nex
6d8de5b461 Bumped version 2021-10-23 13:51:44 +02:00
Nex
b0177d6104 Upgraded adb-shell 2021-10-23 13:51:33 +02:00
tek
e0c9a44b10 Merge branch 'main' of github.com:mvt-project/mvt 2021-10-21 21:17:31 +02:00
tek
ef8c1ae895 Adds recent iOS versions 2021-10-21 21:17:09 +02:00
Nex
3165801e2b Bumped version 2021-10-18 13:40:30 +02:00
Nex
1aa371a398 Upgraded dependencies 2021-10-18 12:57:27 +02:00
Nex
f8e380baa1 Minor style fixes 2021-10-18 12:51:20 +02:00
Nex
35559b09a8 Merge pull request #206 from colossalzippy/main
improve Filesystem module
2021-10-18 12:48:58 +02:00
Nex
daf5c1f3de Merge pull request #205 from witchbuild/main
New artefact, networking_analytics.db
2021-10-18 12:46:39 +02:00
colossalzippy
f601db2174 improve Filesystem 2021-10-15 14:58:50 +02:00
witchbuild
3ce9641c23 add NetworkingAnalytics 2021-10-15 11:53:06 +02:00
Nex
9be393e3f6 Bumped version 2021-10-14 19:59:09 +02:00
Nex
5f125974b8 Upgraded adb-shell 2021-10-14 10:10:38 +02:00
Nex
aa0f152ba1 Merge branch 'main' of github.com:mvt-project/mvt 2021-10-12 18:07:44 +02:00
Nex
169f5fbc26 Pyment to reST 2021-10-12 18:06:58 +02:00
tek
5ea3460c09 Minor documentation update 2021-10-12 12:20:50 +02:00
Nex
c38df37967 Merge pull request #183 from l0s/libimobiledevice-glue_not-found
Install libimobiledevice-glue from source
2021-10-11 11:13:18 +02:00
Nex
7f29b522fa Merge pull request #202 from vin01/main
Specify public key for PythonRSASigner
2021-10-11 11:12:27 +02:00
vin01
40b0da9885 Specify public key for PythonRSASigner 2021-10-08 21:36:49 +02:00
tek
94a8d9dd91 Fixes bug in adb handling 2021-09-29 18:16:33 +02:00
tek
963d3db51a Fixes a bug in android packages module 2021-09-29 17:59:50 +02:00
Nex
660e208473 Bumped version 2021-09-28 15:40:26 +02:00
Nex
01e68ccc6a Fixed dict decl 2021-09-28 12:45:15 +02:00
Nex
fba0fa1f2c Removed newline 2021-09-28 12:44:15 +02:00
Nex
1cbf55e50e Merge branch 'pungentsneak-main' 2021-09-28 12:43:26 +02:00
Nex
8fcc79ebfa Adapted for better support 2021-09-28 12:42:57 +02:00
Nex
423462395a Merge branch 'main' of https://github.com/pungentsneak/mvt into pungentsneak-main 2021-09-28 12:33:14 +02:00
Nex
1f08572a6a Bumped version 2021-09-22 17:32:22 +02:00
Nex
94e3c0ce7b Added iOS 15.0 2021-09-22 17:27:29 +02:00
pungentsneak
904daad935 add ShutdownLog 2021-09-22 13:24:17 +02:00
Nex
eb2a8b8b41 Merge branch 'Te-k-stalkerware' 2021-09-21 22:27:54 +02:00
Nex
60a17381a2 Standardized code 2021-09-21 22:27:35 +02:00
tek
ef2bb93dc4 Adds indicator check for android package name and file hash 2021-09-21 19:43:02 +02:00
Nex
f68b7e7089 Pull file hashes fom Packages module directly 2021-09-20 19:15:39 +02:00
Nex
a22241ec32 Added version commands 2021-09-17 14:19:03 +02:00
Nex
8ad1bc7a2b Bumped version 2021-09-16 10:45:26 +02:00
Nex
c6b3509ed4 Merge branch 'main' of github.com:mvt-project/mvt 2021-09-16 10:45:00 +02:00
Nex
75b5b296a5 Added check for indicators (closes: #189) 2021-09-16 10:44:39 +02:00
Nex
2d62e31eaa Merge pull request #188 from Kvek/fix/iOS-docs
docs: update libimobiledevice url in docs
2021-09-15 14:41:11 +02:00
Kvek
1bfc683e4b docs: update libimobiledevice url in docs 2021-09-15 13:21:38 +01:00
Nex
7ab09669b5 Merge pull request #187 from kmaria/patch-1
Fix url for Koodous
2021-09-15 13:15:31 +02:00
Maria Kispal
757bd8618e Fix url for Koodous
with www in the url ends up in 404 page
2021-09-15 13:04:52 +02:00
Nex
f1d039346d Bumped version 2021-09-14 14:33:17 +02:00
Nex
ccdfd92d4a Merge branch 'dozenfossil-main' 2021-09-14 14:29:21 +02:00
Nex
032b229eb8 Minor changes for consistency 2021-09-14 14:29:04 +02:00
Nex
93936976c7 Merge branch 'main' of https://github.com/dozenfossil/mvt into dozenfossil-main 2021-09-14 14:26:37 +02:00
Nex
f3a4e9d108 Merge pull request #186 from beneficentboast/main
fix error for manipulated entries in DataUsage/NetUsage
2021-09-14 14:26:00 +02:00
Nex
93a9735b5e Reordering 2021-09-14 14:21:54 +02:00
Nex
7b0e2d4564 Added version 2021-09-14 14:20:54 +02:00
beneficentboast
725a99bcd5 fix error for manipulated entries in DataUsage 2021-09-13 20:13:43 +02:00
dozenfossil
35a6f6ec9a fix multi path/file issue 2021-09-13 20:02:48 +02:00
Carlos Macasaet
f4ba29f1ef Install libimobiledevice-glue from source
This installs libimobiledevice-glue from source as it appears it is no
longer available to `apt-get`.

Resolves: #182
2021-09-12 18:28:17 -07:00
Nex
3f9809f36c Formatting docstrings 2021-09-11 02:39:33 +02:00
Nex
6da6595108 More docstrings 2021-09-10 20:09:37 +02:00
Nex
35dfeaccee Re-ordered list of shortener domains 2021-09-10 15:21:02 +02:00
Nex
e5f2aa3c3d Standardizing reST docstrings 2021-09-10 15:18:13 +02:00
Nex
3236c1b390 Added new TCC module 2021-09-09 12:00:48 +02:00
Nex
80a670273d Added additional locationd path 2021-09-07 15:18:00 +02:00
Nex
969b5cc506 Fixed bug in locationd module 2021-09-07 15:06:19 +02:00
Nex
ef8622d4c3 Changed event name 2021-09-03 14:49:04 +02:00
Nex
e39e9e6f92 Cleaned up and simplified module 2021-09-03 14:48:24 +02:00
Nex
7b32ed3179 Compacted record data 2021-09-03 14:41:55 +02:00
Nex
315317863e Fixed documentation 2021-09-03 14:06:01 +02:00
Nex
08d35b056a Merge branch 'guitarsinger-main' 2021-09-03 13:35:59 +02:00
Nex
3e679312d1 Renamed module 2021-09-03 13:35:27 +02:00
guitarsinger
be4f1afed6 add OSAnalyticsADDAILY 2021-09-03 11:59:44 +02:00
Nex
0dea25d86e Reverted version number to minor 2021-09-02 15:33:36 +02:00
Nex
505d3c7e60 Bumped version 2021-09-02 15:31:25 +02:00
Nex
8f04c09b75 Removed duplicate 2021-09-02 15:28:17 +02:00
Nex
595b7e2066 Fixed typo 2021-09-02 15:27:00 +02:00
Nex
d3941bb5d3 Merge pull request #177 from harsaphes/main
Checking idstatuscache.plist in a dump for iOS>14.7
2021-09-01 22:00:51 +02:00
Nex
194c8a0ac1 Using new function to retrieve local db path 2021-09-01 21:59:12 +02:00
Nex
bef190fe50 Merge pull request #178 from mvt-project/webkit_error
Fixes a bug in retrieving the backup file path in webkit session resource log
2021-09-01 21:57:49 +02:00
tek
cacf027051 Fixes a bug in retrieving the backup file path in webkit session resource logs 2021-09-01 15:49:23 -04:00
tek
da97f5ca30 Add db recovery to Safari history module 2021-09-01 15:40:45 -04:00
Nex
a774577940 Handling some exceptions more gracefully 2021-09-01 13:41:21 +02:00
Nex
7252cc82a7 Added module to dump full output of dumpsys 2021-08-30 22:20:05 +02:00
Nex
b34d80fd11 Logging module completed 2021-08-30 22:19:28 +02:00
Nex
0347dfa3c9 Added module Files to pull list of visible file pathso 2021-08-30 22:11:07 +02:00
Nex
28647b8493 Fixed is_dir() to isdir() 2021-08-30 22:08:29 +02:00
harsaphes
c2ec26fd75 Checking idstatuscache.plist in a dump for iOS>14.7 2021-08-30 21:01:59 +02:00
Nex
856a6fb895 Cleaning up some classes 2021-08-28 12:33:27 +02:00
Nex
62f3c535df Merge pull request #176 from JeffLIrion/patch-1
Fix `_adb_check_keys` method
2021-08-28 12:25:52 +02:00
Jeff Irion
34c64af815 Fix _adb_check_keys method 2021-08-27 23:26:50 -07:00
Nex
ea4da71277 Creating android home folder if missing 2021-08-27 19:12:09 +02:00
Nex
94fe3c90e0 Added logcat modules 2021-08-26 15:23:54 +02:00
Nex
f78332aa71 Split receivers into a new package 2021-08-26 14:51:56 +02:00
Nex
0c4eb0bb34 Added discovery of Android packages with potentially abusive receivers 2021-08-26 14:08:39 +02:00
Nex
e70054d0c2 Bumped version 2021-08-26 12:48:09 +02:00
Nex
a75cf58f72 Added missing dependency 2021-08-26 12:47:46 +02:00
Nex
c859b43220 Adding logo to iOS cli 2021-08-26 12:40:45 +02:00
Nex
75ee2db02e Upgrading version 2021-08-26 12:36:37 +02:00
Nex
f6efb3c89a Bumped version 2021-08-25 21:58:38 +02:00
Nex
b27047ed27 Updated lookup modules to new format (closes: #175) 2021-08-25 21:58:03 +02:00
Nex
d43c8109d1 Bumped version 2021-08-25 16:32:05 +02:00
Nex
79f313827f Changed mvt-android download-apks to only fetch non-system packages 2021-08-25 13:35:21 +02:00
Nex
67d8820cc9 Merge pull request #174 from arky/adb-keygen-fix
Create adb keys (Fixes #165)
2021-08-21 18:43:14 +02:00
Arky
9297e06cc4 Create adb keys (Fixes #165) 2021-08-21 22:43:41 +07:00
Nex
faf44b0d4d Merge pull request #173 from arky/android-tools-fix
Use latest Android platform tools
2021-08-21 17:25:34 +02:00
Nex
4ebe0b6971 Shrink logo in README 2021-08-21 15:58:35 +02:00
Arky
3cbeb4befa Use latest Android platform tools 2021-08-21 20:53:33 +07:00
Nex
0005ad2abd Removed unused imports 2021-08-21 15:50:12 +02:00
Nex
a16b0c12d2 Added shared help messages 2021-08-21 15:48:52 +02:00
Nex
e0a6608b9d Logging which files error the manifest module 2021-08-20 17:15:35 +02:00
Nex
80a91bb2ad Checking if the backup is actually encrypted before proceeding (closes: #48) 2021-08-20 15:18:08 +02:00
Nex
9a7970e8a0 Merge pull request #172 from jekil/main
Some esthetic fixes to documentation
2021-08-20 09:07:05 +02:00
jekil
05a82075cf Some esthetic fixes to documentation 2021-08-20 08:58:08 +02:00
Nex
d99a8be632 Merge pull request #170 from jekil/main
Dockerfile lifting
2021-08-20 08:17:38 +02:00
jekil
4882ce9c88 Lifting to avoid not needed layers 2021-08-19 23:26:00 +02:00
Nex
2d277d2d14 Catching in case uid field is not present 2021-08-18 23:11:18 +02:00
Nex
1fc6c49d4f Inverted buttons 2021-08-18 19:56:27 +02:00
Nex
6a3b2dde81 Reintroduced newline 2021-08-18 19:23:12 +02:00
Nex
51a71bceb3 Added notice about target audience in introduction 2021-08-18 17:50:12 +02:00
Nex
ee5ac2a502 Updated Android documentation 2021-08-18 17:47:24 +02:00
Nex
b74d7719ea Merge pull request #169 from gregzo/main
Added availability details to records.md
2021-08-18 17:20:47 +02:00
Nex
7887ad6ee4 Removed trailing dot 2021-08-18 17:03:49 +02:00
Nex
012a6ead77 Bumped version 2021-08-18 13:26:50 +02:00
Nex
803dd2ff3a Add note in documentation about ability to invoke multiple --iocs options 2021-08-18 13:25:52 +02:00
Nex
817aaab258 Indicate in help message that option can be invoked multiple times 2021-08-18 13:24:10 +02:00
Nex
4d8d91846c Added missing import of IndicatorsFileBadFormat 2021-08-18 13:21:54 +02:00
Nex
e31e08e710 Added multiple indicators to Android cli 2021-08-18 13:19:34 +02:00
Nex
27847bf16c Added counter for loaded indicators 2021-08-18 13:18:34 +02:00
Nex
f2b1311ff7 Sorted imports 2021-08-18 13:18:28 +02:00
Nex
48810af83d Fixed creation of Indicators instance 2021-08-18 13:12:37 +02:00
Nex
6a63256b5c Added ability to import multiple STIX2 indicators files 2021-08-18 13:08:32 +02:00
Nex
07cf14a921 Updated docs 2021-08-18 10:34:31 +02:00
Gregorio Zanon
e30f6d9134 Added availability details to records.md
Added availability details for backup records which require encryption or aren't available anymore in recent iOS versions.
2021-08-18 10:07:39 +02:00
Nex
d77809060f Added newline 2021-08-17 22:54:33 +02:00
Nex
d61d40ee5a Updated documentation on mvt-android 2021-08-17 16:36:48 +02:00
Nex
99d539b040 Renamed packages.json to apks.json to avoid conflicts with other module 2021-08-17 13:26:26 +02:00
Nex
7edf147112 Better handling of package parsing and more logging (closes: #102) 2021-08-17 13:26:04 +02:00
Nex
39b81214c2 Catching exception when unable to connect to device over TCP 2021-08-17 13:10:36 +02:00
Nex
94fd6b5208 Catching errors more gracefully when downloading apks (closes: #158) 2021-08-17 13:06:31 +02:00
Nex
71e270fdf8 Bumped version 2021-08-16 14:56:46 +02:00
Nex
8125f1ba14 Updated docs with new modules 2021-08-16 11:12:57 +02:00
Nex
96e4a9a4a4 Overhaul of mvt-ios modules 2021-08-16 10:50:35 +02:00
Nex
24d7187303 Fixed variable name 2021-08-15 20:02:17 +02:00
Nex
6af6c52f60 Renamed function for consistency 2021-08-15 20:01:33 +02:00
Nex
fdaf2fc760 Fixed WebkitSessionResourceLog module, still needs testing 2021-08-15 20:00:29 +02:00
Nex
fda621672d Renamed webkit helper function 2021-08-15 19:50:55 +02:00
Nex
ce6cc771b4 Replaced leftover dicts 2021-08-15 19:20:41 +02:00
Nex
e1e4476bee Standardizing Manifest results structure 2021-08-15 19:07:45 +02:00
Nex
9582778adf Getting rid of dict() 2021-08-15 19:05:15 +02:00
Nex
5e6e4fa8d0 Added modules to extract details on configuration profiles from backup 2021-08-15 18:53:02 +02:00
Nex
9e5a412fe2 Creating helper function to locate files in Manifest.db 2021-08-15 17:39:14 +02:00
Nex
763cb6e06c DeviceInfo module is now BackupInfo and only running on backups 2021-08-15 13:16:00 +02:00
Nex
cbdbf41e1e Restructured modules folders 2021-08-15 13:14:18 +02:00
Nex
cf630f7c2b Fixed unused imports 2021-08-14 18:56:33 +02:00
Nex
3d6e01179a Fixed typo 2021-08-14 18:52:00 +02:00
Nex
8260bda308 Got rid of biplist, using standard plistlib 2021-08-14 18:50:11 +02:00
Nex
30e00e0707 Added module to extract information on device 2021-08-14 18:39:46 +02:00
Nex
88e2576334 Copying plist files too when decrypting a backup 2021-08-14 18:25:41 +02:00
Nex
076930c2c9 Added newline 2021-08-14 18:06:30 +02:00
Nex
8a91e64bb9 Catching gracefully if indicators file parse fails 2021-08-12 20:17:37 +02:00
171 changed files with 7004 additions and 2017 deletions

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

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

View File

@@ -1,7 +1,7 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Python package
name: CI
on:
push:
@@ -16,7 +16,8 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.7, 3.8, 3.9]
# python-version: [3.7, 3.8, 3.9]
python-version: [3.8, 3.9]
steps:
- uses: actions/checkout@v2
@@ -27,8 +28,9 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest safety
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
@@ -37,7 +39,5 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Safety checks
run: safety check
# - name: Test with pytest
# run: |
# pytest
- name: Test with pytest
run: pytest

6
.gitignore vendored
View File

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

View File

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

View File

@@ -2,48 +2,59 @@ FROM ubuntu:20.04
# Ref. https://github.com/mvt-project/mvt
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 \
&& apt install -y adb \
&& DEBIAN_FRONTEND=noninteractive apt-get -y install default-jre-headless
&& apt install -y wget unzip\
&& DEBIAN_FRONTEND=noninteractive apt-get -y install default-jre-headless \
# Install build tools for libimobiledevice
# ----------------------------------------
RUN apt install -y build-essential \
checkinstall \
git \
autoconf \
automake \
libtool-bin \
libplist-dev \
libusbmuxd-dev \
libssl-dev \
sqlite3 \
pkg-config
build-essential \
checkinstall \
git \
autoconf \
automake \
libtool-bin \
libplist-dev \
libusbmuxd-dev \
libssl-dev \
sqlite3 \
pkg-config \
# Clean up
# --------
RUN apt-get clean \
&& rm -rf /var/lib/apt/lists/*
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt
# Build libimobiledevice
# ----------------------
RUN git clone https://github.com/libimobiledevice/libplist
RUN git clone https://github.com/libimobiledevice/libusbmuxd
RUN git clone https://github.com/libimobiledevice/libimobiledevice
RUN git clone https://github.com/libimobiledevice/usbmuxd
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 \
RUN cd libplist && ./autogen.sh && make && make install && ldconfig
&& cd libplist && ./autogen.sh && make && make install && ldconfig \
RUN cd libusbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh && make && make install && ldconfig
&& cd ../libimobiledevice-glue && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr && make && make install && ldconfig \
RUN cd libimobiledevice && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --enable-debug && make && make install && ldconfig
&& cd ../libusbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh && make && make install && ldconfig \
RUN cd usbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make && make install
&& cd ../libimobiledevice && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --enable-debug && make && make install && ldconfig \
&& cd ../usbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr --sysconfdir=/etc --localstatedir=/var --runstatedir=/run && make && make install \
# Clean up.
&& cd .. && rm -rf libplist libimobiledevice-glue libusbmuxd libimobiledevice usbmuxd
# Installing MVT
# --------------
@@ -51,16 +62,29 @@ RUN pip3 install mvt
# Installing ABE
# --------------
RUN mkdir /opt/abe
RUN wget https://github.com/nelenkov/android-backup-extractor/releases/download/20210709062403-4c55371/abe.jar -O /opt/abe/abe.jar
RUN mkdir /opt/abe \
&& wget https://github.com/nelenkov/android-backup-extractor/releases/download/20210709062403-4c55371/abe.jar -O /opt/abe/abe.jar \
# Create alias for abe
RUN echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
&& echo 'alias abe="java -jar /opt/abe/abe.jar"' >> ~/.bashrc
# Install Android Platform Tools
# ------------------------------
RUN mkdir /opt/android \
&& wget -q https://dl.google.com/android/repository/platform-tools-latest-linux.zip \
&& unzip platform-tools-latest-linux.zip -d /opt/android \
# Create alias for adb
&& echo 'alias adb="/opt/android/platform-tools/adb"' >> ~/.bashrc
# Generate adb key folder
# ------------------------------
RUN mkdir /root/.android && /opt/android/platform-tools/adb keygen /root/.android/adbkey
# Setup investigations environment
# --------------------------------
RUN mkdir /home/cases
WORKDIR /home/cases
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
RUN 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
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
CMD /bin/bash

View File

@@ -1,11 +1,13 @@
<p align="center">
<img src="./docs/mvt.png" width="300" />
<img src="./docs/mvt.png" width="200" />
</p>
# Mobile Verification Toolkit
[![](https://img.shields.io/pypi/v/mvt)](https://pypi.org/project/mvt/)
[![Documentation Status](https://readthedocs.org/projects/mvt/badge/?version=latest)](https://docs.mvt.re/en/latest/?badge=latest)
[![CI](https://github.com/mvt-project/mvt/actions/workflows/python-package.yml/badge.svg)](https://github.com/mvt-project/mvt/actions/workflows/python-package.yml)
[![Downloads](https://pepy.tech/badge/mvt)](https://pepy.tech/project/mvt)
Mobile Verification Toolkit (MVT) is a collection of utilities to simplify and automate the process of gathering forensic traces helpful to identify a potential compromise of Android and iOS devices.
@@ -13,22 +15,23 @@ It has been developed and released by the [Amnesty International Security Lab](h
*Warning*: MVT is a forensic research tool intended for technologists and investigators. Using it requires understanding the basics of forensic analysis and using command-line tools. This is not intended for end-user self-assessment. If you are concerned with the security of your device please seek expert assistance.
## Installation
MVT can be installed from sources or from [PyPi](https://pypi.org/project/mvt/) (you will need some dependencies, check the [documentation](https://docs.mvt.re/en/latest/install.html)):
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.html).
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.html#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/).
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.html)
The purpose of MVT is to facilitate the ***consensual forensic analysis*** of devices of those who might be targets of sophisticated mobile spyware attacks, especially members of civil society and marginalized communities. We do not want MVT to enable privacy violations of non-consenting individuals. In order to achieve this, MVT is released under its own license. [Read more here.](https://docs.mvt.re/en/latest/license/)

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

42
docs/android/adb.md Normal file
View File

@@ -0,0 +1,42 @@
# Check over ADB
In order to check an Android device over the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) you will first need to install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you have installed [Android Studio](https://developer.android.com/studio/) you should already have access to `adb` and other utilities.
While many Linux distributions already package Android Platform Tools (for example `android-platform-tools-base` on Debian), it is preferable to install the most recent version from the official website. Packaged versions might be outdated and incompatible with most recent Android handsets.
Next you will need to enable debugging on the Android device you are testing. [Please follow the official instructions on how to do so.](https://developer.android.com/studio/command-line/adb)
## Connecting over USB
The easiest way to check the device is over a USB transport. You will need to have USB debugging enabled and the device plugged into your computer. If everything is configured appropriately you should see your device when launching the command `adb devices`.
Now you can try launching MVT with:
```bash
mvt-android check-adb --output /path/to/results
```
If you have previously started an adb daemon MVT will alert you and require you to kill it with `adb kill-server` and relaunch the command.
!!! warning
MVT relies on the Python library [adb-shell](https://pypi.org/project/adb-shell/) to connect to an Android device, which relies on libusb for the USB transport. Because of known driver issues, Windows users [are recommended](https://github.com/JeffLIrion/adb_shell/issues/118) to install appropriate drivers using [Zadig](https://zadig.akeo.ie/). Alternatively, an easier option might be to use the TCP transport and connect over Wi-Fi as describe next.
## Connecting over Wi-FI
When connecting to the device over USB is not possible or not working properly, an alternative option is to connect over the network. In order to do so, first launch an adb daemon at a fixed port number:
```bash
adb tcpip 5555
```
Then you can specify the IP address of the phone with the adb port number to MVT like so:
```bash
mvt-android check-adb --serial 192.168.1.20:5555 --output /path/to/results
```
Where `192.168.1.20` is the correct IP address of your device.
## MVT modules requiring root privileges
Of the currently available `mvt-android check-adb` modules a handful require root privileges to function correctly. This is because certain files, such as browser history and SMS messages databases are not accessible with user privileges through adb. These modules are to be considered OPTIONALLY available in case the device was already jailbroken. **Do NOT jailbreak your own device unless you are sure of what you are doing!** Jailbreaking your phone exposes it to considerable security risks!

View File

@@ -1,38 +1,55 @@
# Checking SMSs from Android backup
# Check an Android Backup (SMS messages)
Some attacks against Android phones are done by sending malicious links by SMS. The Android backup feature does not allow to gather much information that can be interesting for a forensic analysis, but it can be used to extract SMSs and check them with MVT.
Android supports generating a backup archive of all the installed applications which supports it. However, over the years this functionality has been increasingly abandoned in favor of enabling users to remotely backup their personal data over the cloud. App developers can therefore decide to opt out from allowing the apps' data from being exported locally.
To do so, you need to connect your Android device to your computer. You will then need to [enable USB debugging](https://developer.android.com/studio/debug/dev-options#enable>) on the Android device.
At the time of writing, the Android Debug Bridge (adb) command to generate backups is still available but marked as deprecated.
If this is the first time you connect to this device, you will need to approve the authentication keys through a prompt that will appear on your Android device.
That said, most versions of Android should still allow to locally backup SMS messages, and since messages are still a prime vehicle for phishing and malware attacks, you might still want to take advantage of this functionality while it is supported.
Then you can use adb to extract the backup for SMS only with the following command:
## Generate a backup
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
```
You will need to approve the backup on the phone and potentially enter a password to encrypt the backup. The backup will then be stored in a file named `backup.ab`.
In case you nonetheless wish to take a full backup, you can do so with
You will need to use [Android Backup Extractor](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 ~/Download/abe.jar unpack backup.ab backup.tar
adb backup -nocompress -all
```
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.
## Unpack and check the backup
MVT includes a partial implementation of the Android Backup parsing, because of the implementation difference in the compression algorithm between Java and Python. The `-nocompress` option passed to adb in the section above allows to avoid this issue. You can analyse and extract SMSs containing links from the backup directly with MVT:
```bash
$ mvt-android check-backup --output /path/to/results/ /path/to/backup.ab
14:09:45 INFO [mvt.android.cli] Checking ADB backup located at: backup.ab
INFO [mvt.android.modules.backup.sms] Running module SMS...
INFO [mvt.android.modules.backup.sms] Processing SMS backup file at
apps/com.android.providers.telephony/d_f/000000_sms_backup
INFO [mvt.android.modules.backup.sms] Extracted a total of 64 SMS messages containing links
```
If the backup is encrypted, MVT will prompt you to enter the password.
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by MVT. Any matches will be highlighted in the terminal output.
## Alternative ways to unpack and check the backup
If you encounter an issue during the analysis of the backup, you can alternatively use [Android Backup Extractor (ABE)](https://github.com/nelenkov/android-backup-extractor) to convert it to a readable file format. Make sure that java is installed on your system and use the following command:
```bash
java -jar ~/path/to/abe.jar unpack backup.ab backup.tar
tar xvf backup.tar
```
(If the backup is encrypted, the password will be asked by Android Backup Extractor).
If the backup is encrypted, ABE will prompt you to enter the password.
You can then extract SMSs containing links with MVT:
Alternatively, [ab-decrypt](https://github.com/joernheissler/ab-decrypt) can be used for that purpose.
```bash
$ mvt-android check-backup --output . .
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 ./apps/com.android.providers.telephony/d_f/000
000_sms_backup
16:18:39 INFO [mvt.android.modules.backup.sms] Extracted a total of
64 SMS messages containing links
```
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by mvt. Any matches will be highlighted in the terminal output.
You can then extract SMSs containing links with MVT by passing the folder path as parameter instead of the `.ab` file: `mvt-android check-backup --output /path/to/results/ /path/to/backup/` (the path to backup given should be the folder containing the `apps` folder).

View File

@@ -1,15 +1,18 @@
# Downloading APKs from an Android phone
In order to use `mvt-android` you need to connect your Android device to your computer. You will then need to [enable USB debugging](https://developer.android.com/studio/debug/dev-options#enable>) on the Android device.
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.
If this is the first time you connect to this device, you will need to approve the authentication keys through a prompt that will appear on your Android device.
Now you can launch `mvt-android` and specify the `download-apks` command and the path to the folder where you want to store the extracted data:
You can do so by launching the following command:
```bash
mvt-android download-apks --output /path/to/folder
```
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:
```bash
@@ -17,8 +20,15 @@ mvt-android download-apks --output /path/to/folder --virustotal
mvt-android download-apks --output /path/to/folder --koodous
```
Or, to launch all available lookups::
Or, to launch all available lookups:
```bash
mvt-android download-apks --output /path/to/folder --all-checks
```
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

@@ -1,3 +1,21 @@
# Methodology for Android forensic
TODO
Unfortunately Android devices provide much less observability than their iOS cousins. Android stores very little diagnostic information useful to triage potential compromises, and because of this `mvt-android` capabilities are limited as well.
However, not all is lost.
## Check installed Apps
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.
## Check the device over Android Debug Bridge
Some additional diagnostic information can be extracted from the phone using the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb). `mvt-android` allows to automatically extract information including [dumpsys](https://developer.android.com/studio/command-line/dumpsys) results, details on installed packages (without download), running processes, presence of root binaries and packages, and more.
## Check an Android Backup (SMS messages)
Although Android backups are becoming deprecated, it is still possible to generate one. Unfortunately, because apps these days typically favor backup over the cloud, the amount of data available is limited. Currently, `mvt-android check-backup` only supports checking SMS messages containing links.

View File

@@ -10,6 +10,11 @@ cd mvt
docker build -t mvt .
```
Optionally, you may need to specify your platform to Docker in order to build successfully (Apple M1)
```bash
docker build --platform amd64 -t mvt .
```
Test if the image was created successfully:
```bash

View File

@@ -10,4 +10,4 @@ In this documentation you will find instructions on how to install and run the `
## Resources
[:fontawesome-brands-python: Python Package](https://pypi.org/project/mvt){: .md-button .md-button--primary } [:fontawesome-brands-github: GitHub](https://github.com/mvt-project/mvt){: .md-button }
[:fontawesome-brands-github: GitHub](https://github.com/mvt-project/mvt){: .md-button .md-button--primary } [:fontawesome-brands-python: Python Package](https://pypi.org/project/mvt){: .md-button }

View File

@@ -1,6 +1,6 @@
# Installation
Before proceeding, please note that mvt requires Python 3.6+ to run. While it should be available on most operating systems, please make sure of that before proceeding.
Before proceeding, please note that MVT requires Python 3.6+ to run. While it should be available on most operating systems, please make sure of that before proceeding.
## Dependencies on Linux
@@ -14,9 +14,9 @@ sudo apt install python3 python3-pip libusb-1.0-0 sqlite3
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 Mac
## Dependencies on macOS
Running MVT on Mac requires Xcode and [homebrew](https://brew.sh) to be installed.
Running MVT on macOS requires Xcode and [homebrew](https://brew.sh) to be installed.
In order to install dependencies use:
@@ -26,7 +26,7 @@ brew install python3 libusb sqlite3
*libusb* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
When working with Android devices you should additionally install Android SDK Platform Tools:
When working with Android devices you should additionally install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools):
```bash
brew install --cask android-platform-tools

View File

@@ -10,6 +10,8 @@ Mobile Verification Toolkit (MVT) is a collection of utilities designed to facil
- Generate JSON logs of extracted records, and separate JSON logs of all detected malicious traces.
- Generate a unified chronological timeline of extracted records, along with a timeline all detected malicious traces.
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.
## 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

@@ -22,11 +22,25 @@ After extracting forensics data from a device, you are also able to compare it w
mvt-ios check-iocs --iocs ~/iocs/malware.stix2 /path/to/iphone/output/
```
If you're looking for indicators of compromise for a specific piece of malware or adversary, please ask investigators or anti-malware researchers who have the relevant expertise for a STIX file.
The `--iocs` option can be invoked multiple times to let MVT import multiple STIX2 files at once. For example:
```bash
mvt-ios check-backup --iocs ~/iocs/malware1.stix --iocs ~/iocs/malware2.stix2 /path/to/backup
```
It is also possible to load STIX2 files automatically from the environment variable `MVT_STIX2`:
```bash
export MVT_STIX2="/home/user/IOC1.stix2:/home/user/IOC2.stix2"
```
## 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/generated/stalkerware.stix2).
You can automaticallly download the latest public indicator files with the command `mvt-ios download-iocs` or `mvt-android download-iocs`. These commands download the list of indicators listed [here](https://github.com/mvt-project/mvt/blob/main/public_indicators.json) and store them in the [appdir](https://pypi.org/project/appdirs/) folder. They are then loaded automatically by mvt.
Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs.

View File

@@ -1,16 +1,16 @@
# Backup with iTunes app
It is possible to do an iPhone backup by using iTunes on Windows or Mac computers (in most recent versions of Mac OS, 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).
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 Mac OS).
* Open the device in iTunes (or Finder on macOS).
* If you want to have a more accurate detection, ensure that the encrypted backup option is activated and choose a secure password for the backup.
* Start the backup and wait for it to finish (this may take up to 30 minutes).
![](../../../img/macos-backup.jpg)
_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 Mac OS, 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/`.

View File

@@ -1,6 +1,6 @@
# Install libimobiledevice
Before proceeding with doing any acquisition of iOS devices we recommend installing [libimobiledevice](https://www.libimobiledevice.org/) utilities. These utilities will become useful when extracting crash logs and generating iTunes backups. Because the utilities and its libraries are subject to frequent changes in response to new versions of iOS, you might want to consider compiling libimobiledevice utilities from sources. Otherwise, if available, you can try installing packages available in your distribution:
Before proceeding with doing any acquisition of iOS devices we recommend installing [libimobiledevice](https://libimobiledevice.org/) utilities. These utilities will become useful when extracting crash logs and generating iTunes backups. Because the utilities and its libraries are subject to frequent changes in response to new versions of iOS, you might want to consider compiling libimobiledevice utilities from sources. Otherwise, if available, you can try installing packages available in your distribution:
```bash
sudo apt install libimobiledevice-utils

View File

@@ -12,4 +12,4 @@ If you are not expected to return the phone, you might want to consider to attem
#### iTunes Backup
An alternative option is to generate an iTunes backup (in most recent version of mac OS, they are no longer launched from iTunes, but directly from Finder). While backups only provide a subset of the files stored on the device, in many cases it might be sufficient to at least detect some suspicious artifacts. Backups encrypted with a password will have some additional interesting records not available in unencrypted ones, such as Safari history, Safari state, etc.
An alternative option is to generate an iTunes backup (in most recent version of macOS, they are no longer launched from iTunes, but directly from Finder). While backups only provide a subset of the files stored on the device, in many cases it might be sufficient to at least detect some suspicious artifacts. Backups encrypted with a password will have some additional interesting records not available in unencrypted ones, such as Safari history, Safari state, etc.

View File

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

View File

@@ -1,4 +1,4 @@
mkdocs==1.2.1
mkdocs==1.2.3
mkdocs-autorefs
mkdocs-material
mkdocs-material-extensions

View File

@@ -42,7 +42,8 @@ nav:
- Records extracted by mvt-ios: "ios/records.md"
- MVT for Android:
- Android Forensic Methodology: "android/methodology.md"
- Check APKs: "android/download_apks.md"
- Check an Android Backup: "android/backup.md"
- Check over ADB: "android/adb.md"
- Check an Android Backup (SMS messages): "android/backup.md"
- Download APKs: "android/download_apks.md"
- Indicators of Compromise: "iocs.md"
- License: "license.md"

View File

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

View File

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

View File

@@ -1,17 +1,27 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import argparse
import getpass
import io
import logging
import os
import sys
import tarfile
from pathlib import Path
from zipfile import ZipFile
import click
from rich.logging import RichHandler
from mvt.common.indicators import Indicators
from mvt.android.parsers.backup import (AndroidBackupParsingError,
InvalidBackupPassword, parse_ab_header,
parse_backup_file)
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
@@ -19,6 +29,7 @@ from .lookups.koodous import koodous_lookup
from .lookups.virustotal import virustotal_lookup
from .modules.adb import ADB_MODULES
from .modules.backup import BACKUP_MODULES
from .modules.bugreport import BUGREPORT_MODULES
# Setup logging using Rich.
LOG_FORMAT = "[%(name)s] %(message)s"
@@ -26,25 +37,30 @@ logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__)
# Help messages of repeating options.
OUTPUT_HELP_MESSAGE = "Specify a path to a folder where you want to store JSON results"
SERIAL_HELP_MESSAGE = "Specify a device serial number or HOST:PORT connection string"
#==============================================================================
# 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
# Command: 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=SERIAL_HELP_MESSAGE)
@cli.command("download-apks", help="Download all or only non-system installed APKs")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--all-apks", "-a", is_flag=True,
help="Extract all packages installed on the phone, even those marked as safe")
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")
@@ -52,19 +68,26 @@ def cli():
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)")
def download_apks(all_apks, virustotal, koodous, all_checks, output, from_file, serial):
@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:
if output and not os.path.exists(output):
# 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)
sys.exit(-1)
ctx.exit(1)
download = DownloadAPKs(output_folder=output, all_apks=all_apks)
download = DownloadAPKs(output_folder=output, all_apks=all_apks,
log=logging.getLogger(DownloadAPKs.__module__))
if serial:
download.serial = serial
download.run()
@@ -81,20 +104,23 @@ def download_apks(all_apks, virustotal, koodous, all_checks, output, from_file,
koodous_lookup(packages)
except KeyboardInterrupt:
print("")
sys.exit(-1)
ctx.exit(1)
#==============================================================================
# Checks through ADB
# Command: check-adb
#==============================================================================
@cli.command("check-adb", help="Check an Android device over adb")
@click.option("--serial", "-s", type=str, help=SERIAL_HELP_MESSAGE)
@click.option("--iocs", "-i", type=click.Path(exists=True), help="Path to indicators file")
@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="Specify a path to a folder where you want to store JSON results")
@click.option("--list-modules", "-l", is_flag=True, help="Print list of available modules and exit")
@click.option("--module", "-m", help="Name of a single module you would like to run instead of all")
def check_adb(iocs, output, list_modules, module, serial):
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:
@@ -109,12 +135,10 @@ def check_adb(iocs, output, list_modules, module, serial):
os.makedirs(output)
except Exception as e:
log.critical("Unable to create output folder %s: %s", output, e)
sys.exit(-1)
ctx.exit(1)
if iocs:
# Pre-load indicators for performance reasons.
log.info("Loading indicators from provided file at %s", iocs)
indicators = Indicators(iocs)
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
timeline = []
timeline_detected = []
@@ -122,14 +146,14 @@ def check_adb(iocs, output, list_modules, module, serial):
if module and adb_module.__name__ != module:
continue
m = adb_module(output_folder=output, log=logging.getLogger(adb_module.__module__))
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
if iocs:
indicators.log = m.log
m.indicators = indicators
run_module(m)
timeline.extend(m.timeline)
timeline_detected.extend(m.timeline_detected)
@@ -140,46 +164,228 @@ def check_adb(iocs, output, list_modules, module, serial):
if len(timeline_detected) > 0:
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
#==============================================================================
# Check ADB backup
# Command: check-bugreport
#==============================================================================
@cli.command("check-backup", help="Check an Android Backup")
@click.option("--serial", "-s", type=str, help=SERIAL_HELP_MESSAGE)
@click.option("--iocs", "-i", type=click.Path(exists=True), help="Path to indicators file")
@click.option("--output", "-o", type=click.Path(exists=False), help=OUTPUT_HELP_MESSAGE)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
def check_backup(iocs, output, backup_path, serial):
log.info("Checking ADB backup located at: %s", backup_path)
@cli.command("check-bugreport", help="Check an Android Bug Report")
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
@click.pass_context
def check_bugreport(ctx, iocs, output, list_modules, module, bugreport_path):
if list_modules:
log.info("Following is the list of available check-bugreport modules:")
for adb_module in BUGREPORT_MODULES:
log.info(" - %s", adb_module.__name__)
return
log.info("Checking an Android Bug Report located at: %s", bugreport_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)
sys.exit(-1)
ctx.exit(1)
if iocs:
# Pre-load indicators for performance reasons.
log.info("Loading indicators from provided file at %s", iocs)
indicators = Indicators(iocs)
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
if os.path.isfile(bugreport_path):
bugreport_format = "zip"
zip_archive = ZipFile(bugreport_path)
zip_files = []
for file_name in zip_archive.namelist():
zip_files.append(file_name)
elif os.path.isdir(bugreport_path):
bugreport_format = "dir"
folder_files = []
parent_path = Path(bugreport_path).absolute().as_posix()
for root, subdirs, subfiles in os.walk(os.path.abspath(bugreport_path)):
for file_name in subfiles:
folder_files.append(os.path.relpath(os.path.join(root, file_name), parent_path))
timeline = []
timeline_detected = []
for bugreport_module in BUGREPORT_MODULES:
if module and bugreport_module.__name__ != module:
continue
m = bugreport_module(base_folder=bugreport_path, output_folder=output,
log=logging.getLogger(bugreport_module.__module__))
if bugreport_format == "zip":
m.from_zip(zip_archive, zip_files)
else:
m.from_folder(bugreport_path, folder_files)
if indicators.total_ioc_count:
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"))
#==============================================================================
# Command: check-backup
#==============================================================================
@cli.command("check-backup", help="Check an Android Backup")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT)
@click.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 os.path.isfile(backup_path):
log.critical("The path you specified is a not a folder!")
# AB File
backup_type = "ab"
with open(backup_path, "rb") as handle:
data = handle.read()
header = parse_ab_header(data)
if not header["backup"]:
log.critical("Invalid backup format, file should be in .ab format")
ctx.exit(1)
password = None
if header["encryption"] != "none":
password = getpass.getpass(prompt="Backup Password: ", stream=None)
try:
tardata = parse_backup_file(data, password=password)
except InvalidBackupPassword:
log.critical("Invalid backup password")
ctx.exit(1)
except AndroidBackupParsingError:
log.critical("Impossible to parse this backup file, please use Android Backup Extractor instead")
ctx.exit(1)
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!")
sys.exit(-1)
dbytes = io.BytesIO(tardata)
tar = tarfile.open(fileobj=dbytes)
files = []
for member in tar:
files.append(member.name)
elif os.path.isdir(backup_path):
backup_type = "folder"
backup_path = Path(backup_path).absolute().as_posix()
files = []
for root, subdirs, subfiles in os.walk(os.path.abspath(backup_path)):
for fname in subfiles:
files.append(os.path.relpath(os.path.join(root, fname), backup_path))
else:
log.critical("Invalid backup path, path should be a folder or an Android Backup (.ab) file")
ctx.exit(1)
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)
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
if iocs:
indicators.log = m.log
m.indicators = indicators
if backup_type == "folder":
m.from_folder(backup_path, files)
else:
m.from_ab(backup_path, tar, files)
run_module(m)
#==============================================================================
# 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 + ADB_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)
# TODO: Skipping processing of result files that are not json.
# We might want to revisit this eventually.
if ext != ".json":
continue
for iocs_module in all_modules:
if module and iocs_module.__name__ != module:
continue
if iocs_module().get_slug() != name_only:
continue
log.info("Loading results from \"%s\" with module %s", file_name,
iocs_module.__name__)
m = iocs_module.from_json(file_path,
log=logging.getLogger(iocs_module.__module__))
if indicators.total_ioc_count > 0:
m.indicators = indicators
m.indicators.log = m.log
try:
m.check_indicators()
except NotImplementedError:
continue
else:
total_detections += len(m.detected)
if total_detections > 0:
log.warning("The check of the results produced %d detections!",
total_detections)
#==============================================================================
# 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,182 +0,0 @@
android
android.auto_generated_rro__
android.autoinstalls.config.google.nexus
com.android.backupconfirm
com.android.bips
com.android.bluetooth
com.android.bluetoothmidiservice
com.android.bookmarkprovider
com.android.calllogbackup
com.android.captiveportallogin
com.android.carrierconfig
com.android.carrierdefaultapp
com.android.cellbroadcastreceiver
com.android.certinstaller
com.android.chrome
com.android.companiondevicemanager
com.android.connectivity.metrics
com.android.cts.ctsshim
com.android.cts.priv.ctsshim
com.android.defcontainer
com.android.documentsui
com.android.dreams.basic
com.android.egg
com.android.emergency
com.android.externalstorage
com.android.facelock
com.android.hotwordenrollment
com.android.hotwordenrollment.okgoogle
com.android.hotwordenrollment.tgoogle
com.android.hotwordenrollment.xgoogle
com.android.htmlviewer
com.android.inputdevices
com.android.keychain
com.android.location.fused
com.android.managedprovisioning
com.android.mms.service
com.android.mtp
com.android.musicfx
com.android.nfc
com.android.omadm.service
com.android.pacprocessor
com.android.phone
com.android.printspooler
com.android.providers.blockednumber
com.android.providers.calendar
com.android.providers.contacts
com.android.providers.downloads
com.android.providers.downloads.ui
com.android.providers.media
com.android.providers.partnerbookmarks
com.android.providers.settings
com.android.providers.telephony
com.android.providers.userdictionary
com.android.proxyhandler
com.android.retaildemo
com.android.safetyregulatoryinfo
com.android.sdm.plugins.connmo
com.android.sdm.plugins.dcmo
com.android.sdm.plugins.diagmon
com.android.sdm.plugins.sprintdm
com.android.server.telecom
com.android.service.ims
com.android.service.ims.presence
com.android.settings
com.android.sharedstoragebackup
com.android.shell
com.android.statementservice
com.android.stk
com.android.systemui
com.android.systemui.theme.dark
com.android.vending
com.android.vpndialogs
com.android.vzwomatrigger
com.android.wallpaperbackup
com.android.wallpaper.livepicker
com.breel.wallpapers
com.customermobile.preload.vzw
com.google.android.apps.cloudprint
com.google.android.apps.docs
com.google.android.apps.docs.editors.docs
com.google.android.apps.enterprise.dmagent
com.google.android.apps.gcs
com.google.android.apps.helprtc
com.google.android.apps.inputmethod.hindi
com.google.android.apps.maps
com.google.android.apps.messaging
com.google.android.apps.nexuslauncher
com.google.android.apps.photos
com.google.android.apps.pixelmigrate
com.google.android.apps.tachyon
com.google.android.apps.turbo
com.google.android.apps.tycho
com.google.android.apps.wallpaper
com.google.android.apps.wallpaper.nexus
com.google.android.apps.work.oobconfig
com.google.android.apps.youtube.vr
com.google.android.asdiv
com.google.android.backuptransport
com.google.android.calculator
com.google.android.calendar
com.google.android.carrier
com.google.android.carrier.authdialog
com.google.android.carrierentitlement
com.google.android.carriersetup
com.google.android.configupdater
com.google.android.contacts
com.google.android.deskclock
com.google.android.dialer
com.google.android.euicc
com.google.android.ext.services
com.google.android.ext.shared
com.google.android.feedback
com.google.android.gm
com.google.android.gms
com.google.android.gms.policy_auth
com.google.android.gms.policy_sidecar_o
com.google.android.gms.setup
com.google.android.GoogleCamera
com.google.android.googlequicksearchbox
com.google.android.gsf
com.google.android.gsf.login
com.google.android.hardwareinfo
com.google.android.hiddenmenu
com.google.android.ims
com.google.android.inputmethod.japanese
com.google.android.inputmethod.korean
com.google.android.inputmethod.latin
com.google.android.inputmethod.pinyin
com.google.android.instantapps.supervisor
com.google.android.keep
com.google.android.marvin.talkback
com.google.android.music
com.google.android.nexusicons
com.google.android.onetimeinitializer
com.google.android.packageinstaller
com.google.android.partnersetup
com.google.android.printservice.recommendation
com.google.android.setupwizard
com.google.android.soundpicker
com.google.android.storagemanager
com.google.android.syncadapters.contacts
com.google.android.tag
com.google.android.talk
com.google.android.tetheringentitlement
com.google.android.theme.pixel
com.google.android.tts
com.google.android.videos
com.google.android.vr.home
com.google.android.vr.inputmethod
com.google.android.webview
com.google.android.wfcactivation
com.google.android.youtube
com.google.ar.core
com.google.intelligence.sense
com.google.modemservice
com.google.pixel.wahoo.gfxdrv
com.google.SSRestartDetector
com.google.tango
com.google.vr.apps.ornament
com.google.vr.vrcore
com.htc.omadm.trigger
com.qti.qualcomm.datastatusnotification
com.qualcomm.atfwd
com.qualcomm.embms
com.qualcomm.fastdormancy
com.qualcomm.ltebc_vzw
com.qualcomm.qcrilmsgtunnel
com.qualcomm.qti.ims
com.qualcomm.qti.networksetting
com.qualcomm.qti.telephonyservice
com.qualcomm.qti.uceShimService
com.qualcomm.shutdownlistner
com.qualcomm.timeservice
com.qualcomm.vzw_api
com.quicinc.cne.CNEService
com.verizon.llkagent
com.verizon.mips.services
com.verizon.obdm
com.verizon.obdm_permissions
com.verizon.services
com.vzw.apnlib
qualcomm.com.vzw_msdc_api

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -7,15 +7,16 @@ import json
import logging
import os
import pkg_resources
from tqdm import tqdm
from mvt.common.utils import get_sha256_from_file_path
from mvt.common.module import InsufficientPrivileges
from .modules.adb.base import AndroidExtraction
from .modules.adb.packages import Packages
log = logging.getLogger(__name__)
# TODO: Would be better to replace tqdm with rich.progress to reduce
# the number of dependencies. Need to investigate whether
# it's possible to have a similar callback system.
@@ -28,94 +29,45 @@ class PullProgress(tqdm):
self.update(current - self.n)
class Package:
"""Package indicates a package name and all the files associated with it."""
def __init__(self, name, files=None):
self.name = name
self.files = files or []
class DownloadAPKs(AndroidExtraction):
"""DownloadAPKs is the main class operating the download of APKs
from the device."""
from the device.
def __init__(self, output_folder=None, all_apks=False, packages=None):
"""
def __init__(self, output_folder=None, all_apks=False, log=None,
packages=None):
"""Initialize module.
:param output_folder: Path to the folder where data should be stored
:param all_apks: Boolean indicating whether to download all packages
or filter known-goods
:param packages: Provided list of packages, typically for JSON checks
"""
super().__init__(file_path=None, base_folder=None,
output_folder=output_folder)
super().__init__(output_folder=output_folder, log=log)
self.output_folder_apk = None
self.packages = packages or []
self.packages = packages
self.all_apks = all_apks
self._safe_packages = []
self.output_folder_apk = None
@classmethod
def from_json(cls, json_path):
"""Initialize this class from an existing packages.json file.
:param json_path: Path to the packages.json file to parse.
"""Initialize this class from an existing apks.json file.
:param json_path: Path to the apks.json file to parse.
"""
with open(json_path, "r") as handle:
data = json.load(handle)
packages = []
for entry in data:
package = Package(entry["name"], entry["files"])
packages.append(package)
with open(json_path, "r", encoding="utf-8") as handle:
packages = json.load(handle)
return cls(packages=packages)
def _load_safe_packages(self):
"""Load known-good package names.
"""
safe_packages_path = os.path.join("data", "safe_packages.txt")
safe_packages_string = pkg_resources.resource_string(__name__, safe_packages_path)
safe_packages_list = safe_packages_string.decode("utf-8").split("\n")
self._safe_packages.extend(safe_packages_list)
def _clean_output(self, output):
"""Clean adb shell command output.
:param output: Command output to clean.
"""
return output.strip().replace("package:", "")
def get_packages(self):
"""Retrieve package names from the device using adb.
"""
log.info("Retrieving package names ...")
if not self.all_apks:
self._load_safe_packages()
output = self._adb_command("pm list packages")
total = 0
for line in output.split("\n"):
package_name = self._clean_output(line)
if package_name == "":
continue
total += 1
if not self.all_apks and package_name in self._safe_packages:
continue
if package_name not in self.packages:
self.packages.append(Package(package_name))
log.info("There are %d packages installed on the device. I selected %d for inspection.",
total, len(self.packages))
def pull_package_file(self, package_name, remote_path):
"""Pull files related to specific package from the device.
:param package_name: Name of the package to download
:param remote_path: Path to the file to download
:returns: Path to the local copy
"""
log.info("Downloading %s ...", remote_path)
@@ -139,6 +91,11 @@ class DownloadAPKs(AndroidExtraction):
miniters=1) as pp:
self._adb_download(remote_path, local_path,
progress_callback=pp.update_to)
except InsufficientPrivileges:
log.warn("Unable to pull package file from %s: insufficient privileges, it might be a system app",
remote_path)
self._adb_reconnect()
return None
except Exception as e:
log.exception("Failed to pull package file from %s: %s",
remote_path, e)
@@ -147,68 +104,82 @@ class DownloadAPKs(AndroidExtraction):
return local_path
def pull_packages(self):
"""Download all files of all selected packages from the device.
def get_packages(self):
"""Use the Packages adb module to retrieve the list of packages.
We reuse the same extraction logic to then download the APKs.
"""
self.log.info("Retrieving list of installed packages...")
m = Packages()
m.log = self.log
m.run()
self.packages = m.results
def pull_packages(self):
"""Download all files of all selected packages from the device."""
log.info("Starting extraction of installed APKs at folder %s", self.output_folder)
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
# If the user provided the flag --all-apks we select all packages.
packages_selection = []
if self.all_apks:
log.info("Selected all %d available packages", len(self.packages))
packages_selection = self.packages
else:
# Otherwise we loop through the packages and get only those that
# are not marked as system.
for package in self.packages:
if not package.get("system", False):
packages_selection.append(package)
log.info("Selected only %d packages which are not marked as system",
len(packages_selection))
if len(packages_selection) == 0:
log.info("No packages were selected for download")
return
log.info("Downloading packages from device. This might take some time ...")
self.output_folder_apk = os.path.join(self.output_folder, "apks")
if not os.path.exists(self.output_folder_apk):
os.mkdir(self.output_folder_apk)
total_packages = len(self.packages)
counter = 0
for package in self.packages:
for package in packages_selection:
counter += 1
log.info("[%d/%d] Package: %s", counter, total_packages, package.name)
try:
output = self._adb_command(f"pm path {package.name}")
output = self._clean_output(output)
if not output:
continue
except Exception as e:
log.exception("Failed to get path of package %s: %s", package.name, e)
self._adb_reconnect()
continue
log.info("[%d/%d] Package: %s", counter, len(packages_selection),
package["package_name"])
# Sometimes the package path contains multiple lines for multiple apks.
# We loop through each line and download each file.
for path in output.split("\n"):
device_path = path.strip()
file_path = self.pull_package_file(package.name, device_path)
if not file_path:
for package_file in package["files"]:
device_path = package_file["path"]
local_path = self.pull_package_file(package["package_name"],
device_path)
if not local_path:
continue
# We add the apk metadata to the package object.
package.files.append({
"path": device_path,
"local_name": file_path,
"sha256": get_sha256_from_file_path(file_path),
})
package_file["local_path"] = local_path
log.info("Download of selected packages completed")
def save_json(self):
"""Save the results to the package.json file.
"""
json_path = os.path.join(self.output_folder, "packages.json")
packages = []
for package in self.packages:
packages.append(package.__dict__)
with open(json_path, "w") as handle:
json.dump(packages, handle, indent=4)
"""Save the results to the package.json file."""
json_path = os.path.join(self.output_folder, "apks.json")
with open(json_path, "w", encoding="utf-8") as handle:
json.dump(self.packages, handle, indent=4)
def run(self):
"""Run all steps of fetch-apk.
"""
self._adb_connect()
"""Run all steps of fetch-apk."""
self.get_packages()
self._adb_connect()
self.pull_packages()
self.save_json()
self._adb_disconnect()

View File

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

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -13,6 +13,7 @@ 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...")
@@ -27,12 +28,12 @@ def koodous_lookup(packages):
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.files:
for file in package.get("files", []):
url = f"https://api.koodous.com/apks/{file['sha256']}"
res = requests.get(url)
report = res.json()
row = [package.name, file["local_name"]]
row = [package["package_name"], file["path"]]
if "package_name" in report:
trusted = "no"

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -13,6 +13,7 @@ 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}"
@@ -36,18 +37,24 @@ def get_virustotal_report(hashes):
log.error("Unexpected response from VirusTotal: %s", res.status_code)
return None
def virustotal_lookup(packages):
# NOTE: This is temporary, until we resolved the issue.
log.error("Unfortunately VirusTotal lookup is disabled until further notice, due to unresolved issues with the API service.")
return
log.info("Looking up all extracted files on VirusTotal (www.virustotal.com)")
unique_hashes = []
for package in packages:
for file in package.files:
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:
@@ -74,8 +81,8 @@ def virustotal_lookup(packages):
table.add_column("Detections")
for package in packages:
for file in package.files:
row = [package.name, file["local_name"]]
for file in package.get("files", []):
row = [package["package_name"], file["path"]]
if file["sha256"] in detections:
detection = detections[file["sha256"]]

View File

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

View File

@@ -1,18 +1,30 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .chrome_history import ChromeHistory
from .dumpsys_batterystats import DumpsysBatterystats
from .dumpsys_packages import DumpsysPackages
from .dumpsys_procstats import DumpsysProcstats
from .dumpsys_accessibility import DumpsysAccessibility
from .dumpsys_activities import DumpsysActivities
from .dumpsys_appops import DumpsysAppOps
from .dumpsys_battery_daily import DumpsysBatteryDaily
from .dumpsys_battery_history import DumpsysBatteryHistory
from .dumpsys_dbinfo import DumpsysDBInfo
from .dumpsys_full import DumpsysFull
from .dumpsys_receivers import DumpsysReceivers
from .files import Files
from .getprop import Getprop
from .logcat import Logcat
from .packages import Packages
from .processes import Processes
from .rootbinaries import RootBinaries
from .root_binaries import RootBinaries
from .selinux_status import SELinuxStatus
from .settings import Settings
from .sms import SMS
from .whatsapp import Whatsapp
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes,
DumpsysBatterystats, DumpsysProcstats,
DumpsysPackages, Packages, RootBinaries]
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes, Getprop, Settings,
SELinuxStatus, DumpsysBatteryHistory, DumpsysBatteryDaily,
DumpsysReceivers, DumpsysActivities, DumpsysAccessibility,
DumpsysDBInfo, DumpsysFull, DumpsysAppOps, Packages, Logcat,
RootBinaries, Files]

View File

@@ -1,8 +1,10 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import base64
import getpass
import logging
import os
import random
@@ -14,9 +16,12 @@ 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
from adb_shell.exceptions import (AdbCommandFailureException, DeviceAuthError,
UsbDeviceNotFoundError, UsbReadFailedError)
from usb1 import USBErrorAccess, USBErrorBusy
from mvt.android.parsers.backup import (InvalidBackupPassword, parse_ab_header,
parse_backup_file)
from mvt.common.module import InsufficientPrivileges, MVTModule
log = logging.getLogger(__name__)
@@ -24,6 +29,7 @@ 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."""
@@ -36,9 +42,12 @@ class AndroidExtraction(MVTModule):
self.device = None
self.serial = None
def _adb_check_keys(self):
"""Make sure Android adb keys exist.
"""
@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)
@@ -46,19 +55,25 @@ class AndroidExtraction(MVTModule):
write_public_keyfile(ADB_KEY_PATH, ADB_PUB_KEY_PATH)
def _adb_connect(self):
"""Connect to the device over adb.
"""
"""Connect to the device over adb."""
self._adb_check_keys()
with open(ADB_KEY_PATH, "rb") as handle:
priv_key = handle.read()
signer = PythonRSASigner("", priv_key)
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)
try:
self.device = AdbDeviceUsb(serial=self.serial)
except UsbDeviceNotFoundError:
log.critical("No device found. Make sure it is connected and unlocked.")
sys.exit(-1)
# Otherwise we try to use the TCP transport.
else:
addr = self.serial.split(":")
@@ -77,54 +92,65 @@ class AndroidExtraction(MVTModule):
except DeviceAuthError:
log.error("You need to authorize this computer on the Android device. Retrying in 5 seconds...")
time.sleep(5)
except Exception as e:
log.critical(e)
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.
"""
"""Close adb connection to the device."""
self.device.close()
def _adb_reconnect(self):
"""Reconnect to device using adb.
"""
"""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)
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.
"""
"""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.
@@ -138,9 +164,12 @@ class AndroidExtraction(MVTModule):
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
: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)
@@ -149,7 +178,7 @@ class AndroidExtraction(MVTModule):
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.
@@ -174,16 +203,18 @@ class AndroidExtraction(MVTModule):
# 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()
@@ -216,7 +247,32 @@ class AndroidExtraction(MVTModule):
# Disconnect from the device.
self._adb_disconnect()
def _generate_backup(self, package_name):
self.log.warning("Please check phone and accept Android backup prompt. You may need to set a backup password. \a")
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over the shell transport...
backup_output_b64 = self._adb_command("/system/bin/bu backup -nocompress '{}' | base64".format(
package_name))
backup_output = base64.b64decode(backup_output_b64)
header = parse_ab_header(backup_output)
if not header["backup"]:
self.log.error("Extracting SMS via Android backup failed. No valid backup data found.")
return
if header["encryption"] == "none":
return parse_backup_file(backup_output, password=None)
for password_retry in range(0, 3):
backup_password = getpass.getpass(prompt="Backup password: ", stream=None)
try:
decrypted_backup_tar = parse_backup_file(backup_output, backup_password)
return decrypted_backup_tar
except InvalidBackupPassword:
self.log.error("You provided the wrong password! Please try again...")
self.log.warn("All attempts to decrypt backup with password failed!")
def run(self):
"""Run the main procedure.
"""
"""Run the main procedure."""
raise NotImplementedError

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -16,6 +16,7 @@ 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."""
@@ -33,9 +34,19 @@ class ChromeHistory(AndroidExtraction):
"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()
@@ -52,14 +63,14 @@ class ChromeHistory(AndroidExtraction):
""")
for item in cur:
self.results.append(dict(
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],
))
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()
@@ -67,5 +78,8 @@ class ChromeHistory(AndroidExtraction):
log.info("Extracted a total of %d history items", len(self.results))
def run(self):
self._adb_process_file(os.path.join("/", CHROME_HISTORY_PATH),
self._parse_db)
try:
self._adb_process_file(os.path.join("/", CHROME_HISTORY_PATH),
self._parse_db)
except Exception as e:
self.log.error(e)

View File

@@ -0,0 +1,45 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_accessibility
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysAccessibility(AndroidExtraction):
"""This module extracts stats on accessibility."""
def __init__(self, file_path=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 check_indicators(self):
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys accessibility")
self._adb_disconnect()
self.results = parse_dumpsys_accessibility(output)
for result in self.results:
log.info("Found installed accessibility service \"%s\"", result.get("service"))
self.log.info("Identified a total of %d accessibility services", len(self.results))

View File

@@ -0,0 +1,45 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_activity_resolver_table
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysActivities(AndroidExtraction):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path=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.results = results if results else {}
def check_indicators(self):
if not self.indicators:
return
for intent, activities in self.results.items():
for activity in activities:
ioc = self.indicators.check_app_id(activity["package_name"])
if ioc:
activity["matched_indicator"] = ioc
self.detected.append({intent: activity})
continue
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys package")
self._adb_disconnect()
self.results = parse_dumpsys_activity_resolver_table(output)
self.log.info("Extracted activities for %d intents", len(self.results))

View File

@@ -0,0 +1,66 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import re
from mvt.android.parsers.dumpsys import parse_dumpsys_appops
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysAppOps(AndroidExtraction):
"""This module extracts records from App-op Manager."""
slug = "dumpsys_appops"
def __init__(self, file_path=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 = []
for perm in record["permissions"]:
if "entries" not in perm:
continue
for entry in perm["entries"]:
if "timestamp" in entry:
records.append({
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to {perm['name']} : {entry['access']}",
})
return records
def check_indicators(self):
for result in self.results:
if self.indicators:
ioc = self.indicators.check_app_id(result.get("package_name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
for perm in result["permissions"]:
if perm["name"] == "REQUEST_INSTALL_PACKAGES" and perm["access"] == "allow":
self.log.info("Package %s with REQUEST_INSTALL_PACKAGES permission",
result["package_name"])
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys appops")
self._adb_disconnect()
self.results = parse_dumpsys_appops(output)
self.log.info("Extracted a total of %d records from app-ops manager",
len(self.results))

View File

@@ -0,0 +1,50 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_battery_daily
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysBatteryDaily(AndroidExtraction):
"""This module extracts records from battery daily updates."""
def __init__(self, file_path=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["from"],
"module": self.__class__.__name__,
"event": "battery_daily",
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys batterystats --daily")
self._adb_disconnect()
self.results = parse_dumpsys_battery_daily(output)
self.log.info("Extracted %d records from battery daily stats", len(self.results))

View File

@@ -0,0 +1,42 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_battery_history
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysBatteryHistory(AndroidExtraction):
"""This module extracts records from battery history events."""
def __init__(self, file_path=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 check_indicators(self):
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys batterystats --history")
self._adb_disconnect()
self.results = parse_dumpsys_battery_history(output)
self.log.info("Extracted %d records from battery history", len(self.results))

View File

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

View File

@@ -0,0 +1,48 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import re
from mvt.android.parsers import parse_dumpsys_dbinfo
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysDBInfo(AndroidExtraction):
"""This module extracts records from battery daily updates."""
slug = "dumpsys_dbinfo"
def __init__(self, file_path=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 check_indicators(self):
if not self.indicators:
return
for result in self.results:
path = result.get("path", "")
for part in path.split("/"):
ioc = self.indicators.check_app_id(part)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys dbinfo")
self._adb_disconnect()
self.results = parse_dumpsys_dbinfo(output)
self.log.info("Extracted a total of %d records from database information",
len(self.results))

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -10,8 +10,9 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysPackages(AndroidExtraction):
"""This module extracts stats on installed packages."""
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=[]):
@@ -22,14 +23,12 @@ class DumpsysPackages(AndroidExtraction):
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys package")
output = self._adb_command("dumpsys")
if self.output_folder:
packages_path = os.path.join(self.output_folder,
"dumpsys_packages.txt")
with open(packages_path, "w") as handle:
output_path = os.path.join(self.output_folder, "dumpsys.txt")
with open(output_path, "w", encoding="utf-8") as handle:
handle.write(output)
log.info("Records from dumpsys package stored at %s",
packages_path)
log.info("Full dumpsys output stored at %s", output_path)
self._adb_disconnect()

View File

@@ -0,0 +1,66 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_receiver_resolver_table
from .base import AndroidExtraction
log = logging.getLogger(__name__)
INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
INTENT_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE"
INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
class DumpsysReceivers(AndroidExtraction):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path=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.results = results if results else {}
def check_indicators(self):
if not self.indicators:
return
for intent, receivers in self.results.items():
for receiver in receivers:
if intent == INTENT_NEW_OUTGOING_SMS:
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
receiver["receiver"])
elif intent == INTENT_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
receiver["receiver"])
elif intent == INTENT_DATA_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
receiver["receiver"])
elif intent == INTENT_PHONE_STATE:
self.log.info("Found a receiver monitoring telephony state/incoming calls: \"%s\"",
receiver["receiver"])
elif intent == INTENT_NEW_OUTGOING_CALL:
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
receiver["receiver"])
ioc = self.indicators.check_app_id(receiver["package_name"])
if ioc:
receiver["matched_indicator"] = ioc
self.detected.append({intent: receiver})
continue
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys package")
self.results = parse_dumpsys_receiver_resolver_table(output)
self._adb_disconnect()

View File

@@ -0,0 +1,97 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import datetime
import logging
import stat
from mvt.common.utils import convert_timestamp_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Files(AndroidExtraction):
"""This module extracts the list of files on the device."""
def __init__(self, file_path=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 = False
def find_files(self, folder):
if self.full_find:
output = self._adb_command(f"find '{folder}' -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
for file_line in output.splitlines():
[unix_timestamp, mode, size, owner, group, full_path] = file_line.rstrip().split(" ", 5)
mod_time = convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(int(float(unix_timestamp))))
self.results.append({
"path": full_path,
"modified_time": mod_time,
"mode": mode,
"is_suid": (int(mode, 8) & stat.S_ISUID) == 2048,
"is_sgid": (int(mode, 8) & stat.S_ISGID) == 1024,
"size": size,
"owner": owner,
"group": group,
})
else:
output = self._adb_command(f"find '{folder}' 2> /dev/null")
for file_line in output.splitlines():
self.results.append({"path": file_line.rstrip()})
def serialize(self, record):
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()
output = self._adb_command("find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null")
if output or output.strip().splitlines():
self.full_find = True
for data_path in ["/data/local/tmp/", "/sdcard/", "/tmp/"]:
self.find_files(data_path)
self.log.info("Found %s files in primary Android data directories", len(self.results))
if self.fast_mode:
self.log.info("Flag --fast was enabled: skipping full file listing")
else:
self.log.info("Processing full file listing. This may take a while...")
self.find_files("/")
self.log.info("Found %s total files", len(self.results))
self._adb_disconnect()

View File

@@ -0,0 +1,43 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import re
from datetime import datetime, timedelta
from mvt.android.parsers import parse_getprop
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Getprop(AndroidExtraction):
"""This module extracts device properties from getprop command."""
def __init__(self, file_path=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.results = {} if not results else results
def run(self):
self._adb_connect()
output = self._adb_command("getprop")
self._adb_disconnect()
self.results = parse_getprop(output)
# Alert if phone is outdated.
security_patch = self.results.get("ro.build.version.security_patch", "")
if security_patch:
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
if (datetime.now() - patch_date) > timedelta(days=6*30):
self.log.warning("This phone has not received security updates for more than "
"six months (last update: %s)", security_patch)
self.log.info("Extracted %d Android system properties", len(self.results))

View File

@@ -0,0 +1,48 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
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", encoding="utf-8") as handle:
handle.write(output)
log.info("Current logcat logs stored at %s",
logcat_path)
logcat_last_path = os.path.join(self.output_folder,
"logcat_last.txt")
with open(logcat_last_path, "w", encoding="utf-8") as handle:
handle.write(last_output)
log.info("Logcat logs prior to last reboot stored at %s",
logcat_last_path)
self._adb_disconnect()

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -8,10 +8,66 @@ import os
import pkg_resources
from mvt.android.lookups.koodous import koodous_lookup
from mvt.android.lookups.virustotal import virustotal_lookup
from .base import AndroidExtraction
log = logging.getLogger(__name__)
DANGEROUS_PERMISSIONS_THRESHOLD = 10
DANGEROUS_PERMISSIONS = [
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.CAMERA",
"android.permission.DISABLE_KEYGUARD",
"android.permission.PROCESS_OUTGOING_CALLS",
"android.permission.READ_CALENDAR",
"android.permission.READ_CALL_LOG",
"android.permission.READ_CONTACTS",
"android.permission.READ_PHONE_STATE",
"android.permission.READ_SMS",
"android.permission.RECEIVE_MMS",
"android.permission.RECEIVE_SMS",
"android.permission.RECEIVE_WAP_PUSH",
"android.permission.RECORD_AUDIO",
"android.permission.SEND_SMS",
"android.permission.SYSTEM_ALERT_WINDOW",
"android.permission.USE_CREDENTIALS",
"android.permission.USE_SIP",
"com.android.browser.permission.READ_HISTORY_BOOKMARKS",
]
ROOT_PACKAGES = [
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.thirdparty.superuser",
"com.yellowes.su",
"com.koushikdutta.rommanager",
"com.koushikdutta.rommanager.license",
"com.dimonvideo.luckypatcher",
"com.chelpus.lackypatch",
"com.ramdroid.appquarantine",
"com.ramdroid.appquarantinepro",
"com.devadvance.rootcloak",
"com.devadvance.rootcloakplus",
"de.robv.android.xposed.installer",
"com.saurik.substrate",
"com.zachspong.temprootremovejb",
"com.amphoras.hidemyroot",
"com.amphoras.hidemyrootadfree",
"com.formyhm.hiderootPremium",
"com.formyhm.hideroot",
"me.phh.superuser",
"eu.chainfire.supersu.pro",
"com.kingouser.com",
"com.topjohnwu.magisk",
]
class Packages(AndroidExtraction):
"""This module extracts the list of installed packages."""
@@ -41,53 +97,130 @@ class Packages(AndroidExtraction):
return records
def check_indicators(self):
root_packages_path = os.path.join("..", "..", "data", "root_packages.txt")
root_packages_string = pkg_resources.resource_string(__name__, root_packages_path)
root_packages = root_packages_string.decode("utf-8").split("\n")
for root_package in root_packages:
root_package = root_package.strip()
if not root_package:
for result in self.results:
if result["package_name"] in ROOT_PACKAGES:
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
result["package_name"])
self.detected.append(result)
continue
if root_package in self.results:
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
root_package)
self.detected.append(root_package)
if not self.indicators:
continue
ioc = self.indicators.check_app_id(result.get("package_name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
for package_file in result["files"]:
ioc = self.indicators.check_file_hash(package_file["sha256"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
@staticmethod
def parse_package_for_details(output):
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"requested_permissions": [],
}
in_permissions = False
for line in output.splitlines():
if in_permissions:
if line.startswith(" " * 4) and not line.startswith(" " * 6):
in_permissions = False
continue
permission = line.strip().split(":")[0]
details["requested_permissions"].append(permission)
if line.strip().startswith("userId="):
details["uid"] = line.split("=")[1].strip()
elif line.strip().startswith("versionName="):
details["version_name"] = line.split("=")[1].strip()
elif line.strip().startswith("versionCode="):
details["version_code"] = line.split("=", 1)[1].strip()
elif line.strip().startswith("timeStamp="):
details["timestamp"] = line.split("=")[1].strip()
elif line.strip().startswith("firstInstallTime="):
details["first_install_time"] = line.split("=")[1].strip()
elif line.strip().startswith("lastUpdateTime="):
details["last_update_time"] = line.split("=")[1].strip()
elif line.strip() == "requested permissions:":
in_permissions = True
continue
return details
def _get_files_for_package(self, package_name):
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.splitlines():
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")
for line in packages.split("\n"):
packages = self._adb_command("pm list packages -u -i -f")
for line in packages.splitlines():
line = line.strip()
if not line.startswith("package:"):
continue
fields = line.split()
file_name, package_name = fields[0].split(":")[1].rsplit("=", 1)
installer = fields[1].split("=")[1].strip()
if installer == "null":
try:
installer = fields[1].split("=")[1].strip()
except IndexError:
installer = None
uid = fields[2].split(":")[1].strip()
else:
if installer == "null":
installer = 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)
new_package = {
"package_name": package_name,
"file_name": file_name,
"installer": installer,
"disabled": False,
"system": False,
"third_party": False,
"files": package_files,
}
self.results.append(dict(
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,
))
dumpsys_package = self._adb_command(f"dumpsys package {package_name}")
package_details = self.parse_package_for_details(dumpsys_package)
new_package.update(package_details)
self.results.append(new_package)
cmds = [
{"field": "disabled", "arg": "-d"},
@@ -96,7 +229,7 @@ class Packages(AndroidExtraction):
]
for cmd in cmds:
output = self._adb_command(f"pm list packages {cmd['arg']}")
for line in output.split("\n"):
for line in output.splitlines():
line = line.strip()
if not line.startswith("package:"):
continue
@@ -106,6 +239,32 @@ class Packages(AndroidExtraction):
if result["package_name"] == package_name:
self.results[i][cmd["field"]] = True
for result in self.results:
if not result["third_party"]:
continue
dangerous_permissions_count = 0
for perm in result["requested_permissions"]:
if perm in DANGEROUS_PERMISSIONS:
dangerous_permissions_count += 1
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
self.log.info("Third-party package \"%s\" requested %d potentially dangerous permissions",
result["package_name"], dangerous_permissions_count)
packages_to_lookup = []
for result in self.results:
if result["system"]:
continue
packages_to_lookup.append(result)
self.log.info("Found non-system package with name \"%s\" installed by \"%s\" on %s",
result["package_name"], result["installer"], result["timestamp"])
if not self.fast_mode:
virustotal_lookup(packages_to_lookup)
koodous_lookup(packages_to_lookup)
self.log.info("Extracted at total of %d installed package names",
len(self.results))

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -9,6 +9,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Processes(AndroidExtraction):
"""This module extracts details on running processes."""
@@ -18,24 +19,34 @@ class Processes(AndroidExtraction):
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result.get("name", ""))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self):
self._adb_connect()
output = self._adb_command("ps -e")
for line in output.split("\n")[1:]:
for line in output.splitlines()[1:]:
line = line.strip()
if line == "":
continue
fields = line.split()
proc = dict(
user=fields[0],
pid=fields[1],
parent_pid=fields[2],
vsize=fields[3],
rss=fields[4],
)
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:

View File

@@ -1,17 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import 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."""
@@ -22,9 +20,18 @@ class RootBinaries(AndroidExtraction):
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")
root_binaries = [
"su",
"busybox",
"supersu",
"Superuser.apk",
"KingoUser.apk",
"SuperSu.apk",
"magisk",
"magiskhide",
"magiskinit",
"magiskpolicy",
]
self._adb_connect()

View File

@@ -1,17 +1,22 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import os
import pkg_resources
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysProcstats(AndroidExtraction):
"""This module extracts stats on memory consumption by processes."""
class SELinuxStatus(AndroidExtraction):
"""This module checks if SELinux is being enforced."""
slug = "selinux_status"
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
@@ -19,17 +24,17 @@ class DumpsysProcstats(AndroidExtraction):
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
self.results = {} if not results else 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)
output = self._adb_command("getenforce")
self._adb_disconnect()
status = output.lower().strip()
self.results["status"] = status
if status == "enforcing":
self.log.info("SELinux is being regularly enforced")
else:
self.log.warning("SELinux status is \"%s\"!", status)

View File

@@ -0,0 +1,100 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from .base import AndroidExtraction
log = logging.getLogger(__name__)
ANDROID_DANGEROUS_SETTINGS = [
{
"description": "disabled Google Play Services apps verification",
"key": "verifier_verify_adb_installs",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "package_verifier_enable",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "package_verifier_user_consent",
"safe_value": "1",
},
{
"description": "disabled Google Play Protect",
"key": "upload_apk_enable",
"safe_value": "1",
},
{
"description": "disabled confirmation of adb apps installation",
"key": "adb_install_need_confirm",
"safe_value": "1",
},
{
"description": "disabled sharing of security reports",
"key": "send_security_reports",
"safe_value": "1",
},
{
"description": "disabled sharing of crash logs with manufacturer",
"key": "samsung_errorlog_agree",
"safe_value": "1",
},
{
"description": "disabled applications errors reports",
"key": "send_action_app_error",
"safe_value": "1",
},
]
class Settings(AndroidExtraction):
"""This module extracts Android system settings."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
self.results = {} if not results else results
def check_indicators(self):
for namespace, settings in self.results.items():
for key, value in settings.items():
for danger in ANDROID_DANGEROUS_SETTINGS:
# Check if one of the dangerous settings is using an unsafe
# value (different than the one specified).
if danger["key"] == key and danger["safe_value"] != value:
self.log.warning("Found suspicious setting \"%s = %s\" (%s)",
key, value, danger["description"])
break
def run(self):
self._adb_connect()
for namespace in ["system", "secure", "global"]:
out = self._adb_command(f"cmd settings list {namespace}")
if not out:
continue
self.results[namespace] = {}
for line in out.splitlines():
line = line.strip()
if line == "":
continue
fields = line.split("=", 1)
try:
self.results[namespace][fields[0]] = fields[1]
except IndexError:
continue
self._adb_disconnect()

View File

@@ -1,12 +1,17 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import base64
import getpass
import logging
import os
import sqlite3
from mvt.android.parsers.backup import (AndroidBackupParsingError,
parse_tar_for_sms)
from mvt.common.module import InsufficientPrivileges
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from .base import AndroidExtraction
@@ -15,12 +20,12 @@ 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,
SELECT
ppl.normalized_destination AS address,
p.timestamp AS timestamp,
CASE WHEN m.sender_id IN
CASE WHEN m.sender_id IN
(SELECT _id FROM participants WHERE contact_id=-1)
THEN 2 ELSE 1 END incoming, p.text AS text
THEN 2 ELSE 1 END incoming, p.text AS body
FROM messages m, conversations c, parts p,
participants ppl, conversation_participants cp
WHERE (m.conversation_id = c._id)
@@ -31,14 +36,15 @@ WHERE (m.conversation_id = c._id)
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
SMS_MMSMS_QUERY = """
SELECT
address AS number,
SELECT
address AS address,
date_sent AS timestamp,
type as incoming,
body AS text
body AS body
FROM sms;
"""
class SMS(AndroidExtraction):
"""This module extracts all SMS messages containing links."""
@@ -49,12 +55,12 @@ class SMS(AndroidExtraction):
log=log, results=results)
def serialize(self, record):
text = record["text"].replace("\n", "\\n")
body = record["body"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": f"sms_{record['direction']}",
"data": f"{record['number']}: \"{text}\""
"data": f"{record['address']}: \"{body}\""
}
def check_indicators(self):
@@ -62,29 +68,32 @@ class SMS(AndroidExtraction):
return
for message in self.results:
if not "text" in message:
if "body" not in message:
continue
message_links = check_for_links(message["text"])
# FIXME: check links exported from the body previously
message_links = check_for_links(message["body"])
if self.indicators.check_domains(message_links):
self.detected.append(message)
def _parse_db(self, db_path):
"""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):
if self.SMS_DB_TYPE == 1:
cur.execute(SMS_BUGLE_QUERY)
elif (self.SMS_DB_TYPE == 2):
elif self.SMS_DB_TYPE == 2:
cur.execute(SMS_MMSMS_QUERY)
names = [description[0] for description in cur.description]
for item in cur:
message = dict()
message = {}
for index, value in enumerate(item):
message[names[index]] = value
@@ -93,7 +102,7 @@ class SMS(AndroidExtraction):
# If we find links in the messages or if they are empty we add
# them to the list of results.
if check_for_links(message["text"]) or message["text"].strip() == "":
if check_for_links(message["body"]) or message["body"].strip() == "":
self.results.append(message)
cur.close()
@@ -101,12 +110,35 @@ class SMS(AndroidExtraction):
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
def _extract_sms_adb(self):
"""Use the Android backup command to extract SMS data from the native SMS app
It is crucial to use the under-documented "-nocompress" flag to disable the non-standard Java compression
algorithim. This module only supports an unencrypted ADB backup.
"""
backup_tar = self._generate_backup("com.android.providers.telephony")
if not backup_tar:
return
try:
self.results = parse_tar_for_sms(backup_tar)
except AndroidBackupParsingError:
self.log.info("Impossible to read SMS from the Android Backup, please extract the SMS and try extracting it with Android Backup Extractor")
return
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
def run(self):
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")
try:
if (self._adb_check_file_exists(os.path.join("/", SMS_BUGLE_PATH))):
self.SMS_DB_TYPE = 1
self._adb_process_file(os.path.join("/", SMS_BUGLE_PATH), self._parse_db)
elif (self._adb_check_file_exists(os.path.join("/", SMS_MMSSMS_PATH))):
self.SMS_DB_TYPE = 2
self._adb_process_file(os.path.join("/", SMS_MMSSMS_PATH), self._parse_db)
return
except InsufficientPrivileges:
pass
self.log.warn("No SMS database found. Trying extraction of SMS data using Android backup feature.")
self._extract_sms_adb()

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -16,6 +16,7 @@ log = logging.getLogger(__name__)
WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db"
class Whatsapp(AndroidExtraction):
"""This module extracts all WhatsApp messages containing links."""
@@ -39,7 +40,7 @@ class Whatsapp(AndroidExtraction):
return
for message in self.results:
if not "data" in message:
if "data" not in message:
continue
message_links = check_for_links(message["data"])
@@ -48,7 +49,9 @@ class Whatsapp(AndroidExtraction):
def _parse_db(self, db_path):
"""Parse an Android msgstore.db WhatsApp database file.
:param db_path: Path to the Android WhatsApp database file to process
"""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
@@ -59,7 +62,7 @@ class Whatsapp(AndroidExtraction):
messages = []
for item in cur:
message = dict()
message = {}
for index, value in enumerate(item):
message[names[index]] = value
@@ -82,4 +85,7 @@ class Whatsapp(AndroidExtraction):
self.results = messages
def run(self):
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
try:
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
except Exception as e:
self.log.error(e)

View File

@@ -1,8 +1,8 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .sms import SMS
BACKUP_MODULES = [SMS,]
BACKUP_MODULES = [SMS]

View File

@@ -0,0 +1,46 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import fnmatch
import os
from mvt.common.module import MVTModule
class BackupExtraction(MVTModule):
"""This class provides a base for all backup extractios modules"""
ab = None
def from_folder(self, backup_path, files):
"""
Get all the files and list them
"""
self.backup_path = backup_path
self.files = files
def from_ab(self, file_path, tar, files):
"""
Extract the files
"""
self.ab = file_path
self.tar = tar
self.files = files
def _get_files_by_pattern(self, pattern):
return fnmatch.filter(self.files, pattern)
def _get_file_content(self, file_path):
if self.ab:
try:
member = self.tar.getmember(file_path)
except KeyError:
return None
handle = self.tar.extractfile(member)
else:
handle = open(os.path.join(self.backup_path, file_path), "rb")
data = handle.read()
handle.close()
return data

View File

@@ -1,64 +1,36 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import json
import os
import zlib
from mvt.common.module import MVTModule
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from mvt.android.modules.backup.base import BackupExtraction
from mvt.android.parsers.backup import parse_sms_file
from mvt.common.utils import check_for_links
class SMS(MVTModule):
class SMS(BackupExtraction):
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
for message in self.results:
if not "body" in message:
if "body" not in message:
continue
message_links = check_for_links(message["body"])
if self.indicators.check_domains(message_links):
if self.indicators.check_domains(message["links"]):
self.detected.append(message)
def _process_sms_file(self, file_path):
self.log.info("Processing SMS backup file at %s", file_path)
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)
for file in self._get_files_by_pattern("apps/com.android.providers.telephony/d_f/*_sms_backup"):
self.log.info("Processing SMS backup file at %s", file)
data = self._get_file_content(file)
self.results.extend(parse_sms_file(data))
self.log.info("Extracted a total of %d SMS messages containing links",
len(self.results))

View File

@@ -0,0 +1,17 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .accessibility import Accessibility
from .activities import Activities
from .appops import Appops
from .battery_daily import BatteryDaily
from .battery_history import BatteryHistory
from .dbinfo import DBInfo
from .getprop import Getprop
from .packages import Packages
from .receivers import Receivers
BUGREPORT_MODULES = [Accessibility, Activities, Appops, BatteryDaily,
BatteryHistory, DBInfo, Getprop, Packages, Receivers]

View File

@@ -0,0 +1,60 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_accessibility
from .base import BugReportModule
log = logging.getLogger(__name__)
class Accessibility(BugReportModule):
"""This module extracts stats on accessibility."""
def __init__(self, file_path=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 check_indicators(self):
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self):
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
return
lines = []
in_accessibility = False
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE accessibility:":
in_accessibility = True
continue
if not in_accessibility:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_accessibility("\n".join(lines))
for result in self.results:
log.info("Found installed accessibility service \"%s\"", result.get("service"))
self.log.info("Identified a total of %d accessibility services", len(self.results))

View File

@@ -0,0 +1,61 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_activity_resolver_table
from .base import BugReportModule
log = logging.getLogger(__name__)
class Activities(BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path=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.results = results if results else {}
def check_indicators(self):
if not self.indicators:
return
for intent, activities in self.results.items():
for activity in activities:
ioc = self.indicators.check_app_id(activity["package_name"])
if ioc:
activity["matched_indicator"] = ioc
self.detected.append({intent: activity})
continue
def run(self):
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
return
lines = []
in_package = False
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE package:":
in_package = True
continue
if not in_package:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_activity_resolver_table("\n".join(lines))
self.log.info("Extracted activities for %d intents", len(self.results))

View File

@@ -0,0 +1,78 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_appops
from .base import BugReportModule
log = logging.getLogger(__name__)
class Appops(BugReportModule):
"""This module extracts information on package from App-Ops Manager."""
def __init__(self, file_path=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 = []
for perm in record["permissions"]:
if "entries" not in perm:
continue
for entry in perm["entries"]:
if "timestamp" in entry:
records.append({
"timestamp": entry["timestamp"],
"module": self.__class__.__name__,
"event": entry["access"],
"data": f"{record['package_name']} access to {perm['name']} : {entry['access']}",
})
return records
def check_indicators(self):
for result in self.results:
if self.indicators:
ioc = self.indicators.check_app_id(result.get("package_name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
for perm in result["permissions"]:
if perm["name"] == "REQUEST_INSTALL_PACKAGES" and perm["access"] == "allow":
self.log.info("Package %s with REQUEST_INSTALL_PACKAGES permission", result["package_name"])
def run(self):
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
return
lines = []
in_appops = False
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE appops:":
in_appops = True
continue
if not in_appops:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_appops("\n".join(lines))
self.log.info("Identified a total of %d packages in App-Ops Manager",
len(self.results))

View File

@@ -0,0 +1,70 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
import fnmatch
import logging
import os
from mvt.common.module import MVTModule
log = logging.getLogger(__name__)
class BugReportModule(MVTModule):
"""This class provides a base for all Android Bug Report modules."""
zip_archive = None
def from_folder(self, extract_path, extract_files):
self.extract_path = extract_path
self.extract_files = extract_files
def from_zip(self, zip_archive, zip_files):
self.zip_archive = zip_archive
self.zip_files = zip_files
def _get_files_by_pattern(self, pattern):
file_names = []
if self.zip_archive:
for zip_file in self.zip_files:
file_names.append(zip_file)
else:
file_names = self.extract_files
return fnmatch.filter(file_names, pattern)
def _get_files_by_patterns(self, patterns):
for pattern in patterns:
matches = self._get_files_by_pattern(pattern)
if matches:
return matches
def _get_file_content(self, file_path):
if self.zip_archive:
handle = self.zip_archive.open(file_path)
else:
handle = open(os.path.join(self.extract_path, file_path), "rb")
data = handle.read()
handle.close()
return data
def _get_dumpstate_file(self):
main = self._get_files_by_pattern("main_entry.txt")
if main:
main_content = self._get_file_content(main[0])
try:
return self._get_file_content(main_content.decode().strip())
except KeyError:
return None
else:
dumpstate_logs = self._get_files_by_pattern("dumpState_*.log")
if not dumpstate_logs:
return None
return self._get_file_content(dumpstate_logs[0])
return None

View File

@@ -0,0 +1,76 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_battery_daily
from .base import BugReportModule
log = logging.getLogger(__name__)
class BatteryDaily(BugReportModule):
"""This module extracts records from battery daily updates."""
def __init__(self, file_path=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["from"],
"module": self.__class__.__name__,
"event": "battery_daily",
"data": f"Recorded update of package {record['package_name']} with vers {record['vers']}"
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self):
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
return
lines = []
in_batterystats = False
in_daily = False
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE batterystats:":
in_batterystats = True
continue
if not in_batterystats:
continue
if line.strip() == "Daily stats:":
lines.append(line)
in_daily = True
continue
if not in_daily:
continue
if line.strip() == "":
break
lines.append(line)
self.results = parse_dumpsys_battery_daily("\n".join(lines))
self.log.info("Extracted a total of %d battery daily stats",
len(self.results))

View File

@@ -0,0 +1,60 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_battery_history
from .base import BugReportModule
log = logging.getLogger(__name__)
class BatteryHistory(BugReportModule):
"""This module extracts records from battery daily updates."""
def __init__(self, file_path=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 check_indicators(self):
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self):
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
return
lines = []
in_history = False
for line in content.decode(errors="ignore").splitlines():
if line.strip().startswith("Battery History "):
lines.append(line)
in_history = True
continue
if not in_history:
continue
if line.strip() == "":
break
lines.append(line)
self.results = parse_dumpsys_battery_history("\n".join(lines))
self.log.info("Extracted a total of %d battery history records",
len(self.results))

View File

@@ -0,0 +1,63 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_dbinfo
from .base import BugReportModule
log = logging.getLogger(__name__)
class DBInfo(BugReportModule):
"""This module extracts records from battery daily updates."""
slug = "dbinfo"
def __init__(self, file_path=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 check_indicators(self):
if not self.indicators:
return
for result in self.results:
path = result.get("path", "")
for part in path.split("/"):
ioc = self.indicators.check_app_id(part)
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
def run(self):
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
return
in_dbinfo = False
lines = []
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE dbinfo:":
in_dbinfo = True
continue
if not in_dbinfo:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_dbinfo("\n".join(lines))
self.log.info("Extracted a total of %d database connection pool records",
len(self.results))

View File

@@ -0,0 +1,59 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import re
from datetime import datetime, timedelta
from mvt.android.parsers import parse_getprop
from .base import BugReportModule
log = logging.getLogger(__name__)
class Getprop(BugReportModule):
"""This module extracts device properties from getprop command."""
def __init__(self, file_path=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.results = {} if not results else results
def run(self):
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
return
lines = []
in_getprop = False
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "------ SYSTEM PROPERTIES (getprop) ------":
in_getprop = True
continue
if not in_getprop:
continue
if line.strip() == "------":
break
lines.append(line)
self.results = parse_getprop("\n".join(lines))
# Alert if phone is outdated.
security_patch = self.results.get("ro.build.version.security_patch", "")
if security_patch:
patch_date = datetime.strptime(security_patch, "%Y-%m-%d")
if (datetime.now() - patch_date) > timedelta(days=6*30):
self.log.warning("This phone has not received security updates for more than "
"six months (last update: %s)", security_patch)
self.log.info("Extracted %d Android system properties", len(self.results))

View File

@@ -0,0 +1,167 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import re
from .base import BugReportModule
log = logging.getLogger(__name__)
class Packages(BugReportModule):
"""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 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"Install or update of package {record['package_name']}",
})
return records
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
ioc = self.indicators.check_app_id(result["package_name"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue
@staticmethod
def parse_package_for_details(output):
details = {
"uid": "",
"version_name": "",
"version_code": "",
"timestamp": "",
"first_install_time": "",
"last_update_time": "",
"requested_permissions": [],
}
in_install_permissions = False
in_runtime_permissions = False
for line in output.splitlines():
if in_install_permissions:
if line.startswith(" " * 4) and not line.startswith(" " * 6):
in_install_permissions = False
continue
permission = line.strip().split(":")[0]
if permission not in details["requested_permissions"]:
details["requested_permissions"].append(permission)
if in_runtime_permissions:
if not line.startswith(" " * 8):
in_runtime_permissions = False
continue
permission = line.strip().split(":")[0]
if permission not in details["requested_permissions"]:
details["requested_permissions"].append(permission)
if line.strip().startswith("userId="):
details["uid"] = line.split("=")[1].strip()
elif line.strip().startswith("versionName="):
details["version_name"] = line.split("=")[1].strip()
elif line.strip().startswith("versionCode="):
details["version_code"] = line.split("=", 1)[1].strip()
elif line.strip().startswith("timeStamp="):
details["timestamp"] = line.split("=")[1].strip()
elif line.strip().startswith("firstInstallTime="):
details["first_install_time"] = line.split("=")[1].strip()
elif line.strip().startswith("lastUpdateTime="):
details["last_update_time"] = line.split("=")[1].strip()
elif line.strip() == "install permissions:":
in_install_permissions = True
elif line.strip() == "runtime permissions:":
in_runtime_permissions = True
return details
def parse_packages_list(self, output):
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
results = []
package_name = None
package = {}
lines = []
for line in output.splitlines():
if line.startswith(" Package ["):
if len(lines) > 0:
details = self.parse_package_for_details("\n".join(lines))
package.update(details)
results.append(package)
lines = []
package = {}
matches = pkg_rxp.findall(line)
if not matches:
continue
package_name = matches[0]
package["package_name"] = package_name
continue
if not package_name:
continue
lines.append(line)
return results
def run(self):
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
return
in_package = False
in_packages_list = False
lines = []
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE package:":
in_package = True
continue
if not in_package:
continue
if line.strip() == "Packages:":
in_packages_list = True
continue
if not in_packages_list:
continue
if line.strip() == "":
break
lines.append(line)
self.results = self.parse_packages_list("\n".join(lines))
self.log.info("Extracted details on %d packages", len(self.results))

View File

@@ -0,0 +1,83 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.android.parsers import parse_dumpsys_receiver_resolver_table
from .base import BugReportModule
log = logging.getLogger(__name__)
INTENT_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
INTENT_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
INTENT_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
INTENT_PHONE_STATE = "android.intent.action.PHONE_STATE"
INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
class Receivers(BugReportModule):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path=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.results = results if results else {}
def check_indicators(self):
if not self.indicators:
return
for intent, receivers in self.results.items():
for receiver in receivers:
if intent == INTENT_NEW_OUTGOING_SMS:
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
receiver["receiver"])
elif intent == INTENT_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
receiver["receiver"])
elif intent == INTENT_DATA_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
receiver["receiver"])
elif intent == INTENT_PHONE_STATE:
self.log.info("Found a receiver monitoring telephony state/incoming calls: \"%s\"",
receiver["receiver"])
elif intent == INTENT_NEW_OUTGOING_CALL:
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
receiver["receiver"])
ioc = self.indicators.check_app_id(receiver["package_name"])
if ioc:
receiver["matched_indicator"] = ioc
self.detected.append({intent: receiver})
continue
def run(self):
content = self._get_dumpstate_file()
if not content:
self.log.error("Unable to find dumpstate file. Did you provide a valid bug report archive?")
return
in_receivers = False
lines = []
for line in content.decode(errors="ignore").splitlines():
if line.strip() == "DUMP OF SERVICE package:":
in_receivers = True
continue
if not in_receivers:
continue
if line.strip().startswith("------------------------------------------------------------------------------"):
break
lines.append(line)
self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines))
self.log.info("Extracted receivers for %d intents", len(self.results))

View File

@@ -0,0 +1,11 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .dumpsys import (parse_dumpsys_accessibility,
parse_dumpsys_activity_resolver_table,
parse_dumpsys_appops, parse_dumpsys_battery_daily,
parse_dumpsys_battery_history, parse_dumpsys_dbinfo,
parse_dumpsys_receiver_resolver_table)
from .getprop import parse_getprop

View File

@@ -0,0 +1,202 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import datetime
import io
import json
import tarfile
import zlib
from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
PBKDF2_KEY_SIZE = 32
class AndroidBackupParsingError(Exception):
"""Exception raised file parsing an android backup file"""
class AndroidBackupNotImplemented(AndroidBackupParsingError):
pass
class InvalidBackupPassword(AndroidBackupParsingError):
pass
def to_utf8_bytes(input_bytes):
output = []
for byte in input_bytes:
if byte < ord(b'\x80'):
output.append(byte)
else:
output.append(ord('\xef') | (byte >> 12))
output.append(ord('\xbc') | ((byte >> 6) & ord('\x3f')))
output.append(ord('\x80') | (byte & ord('\x3f')))
return bytes(output)
def parse_ab_header(data):
"""
Parse the header of an Android Backup file
Returns a dict {'backup': True, 'compression': False,
'encryption': "none", 'version': 4}
"""
if data.startswith(b"ANDROID BACKUP"):
[magic_header, version, is_compressed, encryption, tar_data] = data.split(b"\n", 4)
return {
"backup": True,
"compression": (is_compressed == b"1"),
"version": int(version),
"encryption": encryption.decode("utf-8")
}
return {
"backup": False,
"compression": None,
"version": None,
"encryption": None
}
def decrypt_master_key(password, user_salt, user_iv, pbkdf2_rounds, master_key_blob, format_version, checksum_salt):
"""Generate AES key from user password uisng PBKDF2
The backup master key is extracted from the master key blog after decryption.
"""
# Derive key from password using PBKDF2
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=32, salt=user_salt, iterations=pbkdf2_rounds)
key = kdf.derive(password.encode("utf-8"))
# Decrypt master key blob
cipher = Cipher(algorithms.AES(key), modes.CBC(user_iv))
decryptor = cipher.decryptor()
try:
decryted_master_key_blob = decryptor.update(master_key_blob) + decryptor.finalize()
# Extract key and IV from decrypted blob.
key_blob = io.BytesIO(decryted_master_key_blob)
master_iv_length = ord(key_blob.read(1))
master_iv = key_blob.read(master_iv_length)
master_key_length = ord(key_blob.read(1))
master_key = key_blob.read(master_key_length)
master_key_checksum_length = ord(key_blob.read(1))
master_key_checksum = key_blob.read(master_key_checksum_length)
except TypeError:
raise InvalidBackupPassword()
# Handle quirky encoding of master key bytes in Android original Java crypto code
if format_version > 1:
hmac_mk = to_utf8_bytes(master_key)
else:
hmac_mk = master_key
# Derive checksum to confirm successful backup decryption.
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=32, salt=checksum_salt, iterations=pbkdf2_rounds)
calculated_checksum = kdf.derive(hmac_mk)
if master_key_checksum != calculated_checksum:
raise InvalidBackupPassword()
return master_key, master_iv
def decrypt_backup_data(encrypted_backup, password, encryption_algo, format_version):
"""
Generate encryption keyffrom password and do decryption
"""
if encryption_algo != b"AES-256":
raise AndroidBackupNotImplemented("Encryption Algorithm not implemented")
if password is None:
raise InvalidBackupPassword()
[user_salt, checksum_salt, pbkdf2_rounds, user_iv, master_key_blob, encrypted_data] = encrypted_backup.split(b"\n", 5)
user_salt = bytes.fromhex(user_salt.decode("utf-8"))
checksum_salt = bytes.fromhex(checksum_salt.decode("utf-8"))
pbkdf2_rounds = int(pbkdf2_rounds)
user_iv = bytes.fromhex(user_iv.decode("utf-8"))
master_key_blob = bytes.fromhex(master_key_blob.decode("utf-8"))
# Derive decryption master key from password
master_key, master_iv = decrypt_master_key(password=password, user_salt=user_salt, user_iv=user_iv,
pbkdf2_rounds=pbkdf2_rounds, master_key_blob=master_key_blob,
format_version=format_version, checksum_salt=checksum_salt)
# Decrypt and unpad backup data using derivied key
cipher = Cipher(algorithms.AES(master_key), modes.CBC(master_iv))
decryptor = cipher.decryptor()
decrypted_tar = decryptor.update(encrypted_data) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
return unpadder.update(decrypted_tar)
def parse_backup_file(data, password=None):
"""
Parse an ab file, returns a tar file
"""
if not data.startswith(b"ANDROID BACKUP"):
raise AndroidBackupParsingError("Invalid file header")
[magic_header, version, is_compressed, encryption_algo, tar_data] = data.split(b"\n", 4)
version = int(version)
is_compressed = int(is_compressed)
if encryption_algo != b"none":
tar_data = decrypt_backup_data(tar_data, password, encryption_algo, format_version=version)
if is_compressed:
try:
tar_data = zlib.decompress(tar_data)
except zlib.error:
raise AndroidBackupParsingError("Impossible to decompress the backup file")
return tar_data
def parse_tar_for_sms(data):
"""
Extract SMS from a tar backup archive
Returns an array of SMS
"""
dbytes = io.BytesIO(data)
tar = tarfile.open(fileobj=dbytes)
try:
member = tar.getmember("apps/com.android.providers.telephony/d_f/000000_sms_backup")
except KeyError:
return []
dhandler = tar.extractfile(member)
return parse_sms_file(dhandler.read())
def parse_sms_file(data):
"""
Parse an SMS file extracted from a folder
Returns a list of SMS entries
"""
res = []
data = zlib.decompress(data)
json_data = json.loads(data)
for entry in json_data:
message_links = check_for_links(entry["body"])
utc_timestamp = datetime.datetime.utcfromtimestamp(int(entry["date"]) / 1000)
entry["isodate"] = convert_timestamp_to_iso(utc_timestamp)
entry["direction"] = ("sent" if int(entry["date_sent"]) else "received")
# If we find links in the messages or if they are empty we add them to the list.
if message_links or entry["body"].strip() == "":
entry["links"] = message_links
res.append(entry)
return res

View File

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

View File

@@ -0,0 +1,26 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import re
def parse_getprop(output):
results = {}
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
for line in output.splitlines():
line = line.strip()
if line == "":
continue
matches = re.findall(rxp, line)
if not matches or len(matches[0]) != 2:
continue
key = matches[0][0]
value = matches[0][1]
results[key] = value
return results

View File

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

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

@@ -0,0 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
# Help messages of repeating options.
HELP_MSG_OUTPUT = "Specify a path to a folder where you want to store JSON results"
HELP_MSG_IOC = "Path to indicators file (can be invoked multiple time)"
HELP_MSG_FAST = "Avoid running time/resource consuming features"
HELP_MSG_LIST_MODULES = "Print list of available modules and exit"
HELP_MSG_MODULE = "Name of a single module you would like to run instead of all"
# Android-specific.
HELP_MSG_SERIAL = "Specify a device serial number or HOST:PORT connection string"

View File

@@ -1,11 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import json
import os
import requests
from appdirs import user_data_dir
from .url import URL
@@ -14,51 +17,207 @@ class Indicators:
functions to compare extracted artifacts to the indicators.
"""
def __init__(self, file_path, log=None):
self.file_path = file_path
with open(self.file_path, "r") as handle:
self.data = json.load(handle)
def __init__(self, log=None):
self.data_dir = user_data_dir("mvt")
self.log = log
self.ioc_domains = []
self.ioc_processes = []
self.ioc_emails = []
self.ioc_files = []
self._parse_stix_file()
self.ioc_collections = []
self.total_ioc_count = 0
def _parse_stix_file(self):
"""Extract IOCs of given type from STIX2 definitions.
def _load_downloaded_indicators(self):
if not os.path.isdir(self.data_dir):
return
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):
"""
for entry in self.data["objects"]:
Checks if a variable MVT_STIX2 contains path to a STIX files.
"""
if "MVT_STIX2" not in os.environ:
return
paths = os.environ["MVT_STIX2"].split(":")
for path in paths:
if os.path.isfile(path):
self.parse_stix2(path)
else:
self.log.error("Path specified with env MVT_STIX2 is not a valid file: %s",
path)
def _new_collection(self, cid="", name="", description="", file_name="",
file_path=""):
return {
"id": cid,
"name": name,
"description": description,
"stix2_file_name": file_name,
"stix2_file_path": file_path,
"domains": [],
"processes": [],
"emails": [],
"file_names": [],
"file_paths": [],
"files_sha256": [],
"app_ids": [],
"ios_profile_ids": [],
"count": 0,
}
def _add_indicator(self, ioc, ioc_coll, ioc_coll_list):
ioc = ioc.strip("'")
if ioc not in ioc_coll_list:
ioc_coll_list.append(ioc)
ioc_coll["count"] += 1
self.total_ioc_count += 1
def parse_stix2(self, file_path):
"""Extract indicators from a STIX2 file.
:param file_path: Path to the STIX2 file to parse
:type file_path: str
"""
self.log.info("Parsing STIX2 indicators file at path %s", file_path)
with open(file_path, "r", encoding="utf-8") as handle:
try:
if entry["type"] != "indicator":
data = json.load(handle)
except json.decoder.JSONDecodeError:
self.log.critical("Unable to parse STIX2 indicator file. "
"The file is corrupted or in the wrong format!")
return
malware = {}
indicators = []
relationships = []
for entry in data.get("objects", []):
entry_type = entry.get("type", "")
if entry_type == "malware":
malware[entry["id"]] = {
"name": entry["name"],
"description": entry["description"],
}
elif entry_type == "indicator":
indicators.append(entry)
elif entry_type == "relationship":
relationships.append(entry)
collections = []
for mal_id, mal_values in malware.items():
collection = self._new_collection(mal_id, mal_values.get("name"),
mal_values.get("description"),
os.path.basename(file_path),
file_path)
collections.append(collection)
# We loop through all indicators.
for indicator in indicators:
malware_id = None
# We loop through all relationships and find the one pertinent to
# the current indicator.
for relationship in relationships:
if relationship["source_ref"] != indicator["id"]:
continue
except KeyError:
continue
key, value = entry["pattern"].strip("[]").split("=")
value = value.strip("'")
# Look for a malware definition with the correct identifier.
if relationship["target_ref"] in malware.keys():
malware_id = relationship["target_ref"]
break
if key == "domain-name:value":
# We force domain names to lower case.
value = value.lower()
if value not in self.ioc_domains:
self.ioc_domains.append(value)
elif key == "process:name":
if value not in self.ioc_processes:
self.ioc_processes.append(value)
elif key == "email-addr:value":
# We force email addresses to lower case.
value = value.lower()
if value not in self.ioc_emails:
self.ioc_emails.append(value)
elif key == "file:name":
if value not in self.ioc_files:
self.ioc_files.append(value)
# Now we look for the correct collection matching the malware ID we
# got from the relationship.
for collection in collections:
if collection["id"] != malware_id:
continue
key, value = indicator.get("pattern", "").strip("[]").split("=")
if key == "domain-name:value":
# We force domain names to lower case.
self._add_indicator(ioc=value.lower(),
ioc_coll=collection,
ioc_coll_list=collection["domains"])
elif key == "process:name":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["processes"])
elif key == "email-addr:value":
# We force email addresses to lower case.
self._add_indicator(ioc=value.lower(),
ioc_coll=collection,
ioc_coll_list=collection["emails"])
elif key == "file:name":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["file_names"])
elif key == "file:path":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["file_paths"])
elif key == "file:hashes.sha256":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["files_sha256"])
elif key == "app:id":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["app_ids"])
elif key == "configuration-profile:id":
self._add_indicator(ioc=value,
ioc_coll=collection,
ioc_coll_list=collection["ios_profile_ids"])
break
for coll in collections:
self.log.info("Extracted %d indicators for collection with name \"%s\"",
coll["count"], coll["name"])
self.ioc_collections.extend(collections)
def load_indicators_files(self, files, 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("No indicators file exists at path %s",
file_path)
# Load downloaded indicators and any indicators from env variable.
if load_default:
self._load_downloaded_indicators()
self._check_stix2_env_variable()
self.log.info("Loaded a total of %d unique indicators", self.total_ioc_count)
def get_iocs(self, ioc_type):
for ioc_collection in self.ioc_collections:
for ioc in ioc_collection.get(ioc_type, []):
yield {
"value": ioc,
"type": ioc_type,
"name": ioc_collection["name"],
"stix2_file_name": ioc_collection["stix2_file_name"],
}
def check_domain(self, url):
"""Check if a given URL matches any of the provided domain indicators.
:param url: URL to match against domain indicators
:type url: str
:returns: Indicator details if matched, otherwise None
"""
# TODO: If the IOC domain contains a subdomain, it is not currently
# being matched.
# being matched.
if not url:
return None
try:
# First we use the provided URL.
@@ -83,94 +242,239 @@ class Indicators:
else:
# If it's not shortened, we just use the original URL object.
final_url = orig_url
except Exception as e:
except Exception:
# If URL parsing failed, we just try to do a simple substring
# match.
for ioc in self.ioc_domains:
if ioc.lower() in url:
self.log.warning("Maybe found a known suspicious domain: %s", url)
return True
for ioc in self.get_iocs("domains"):
if ioc["value"].lower() in url:
self.log.warning("Maybe found a known suspicious domain %s matching indicators from \"%s\"",
url, ioc["name"])
return ioc
# If nothing matched, we can quit here.
return False
return None
# If all parsing worked, we start walking through available domain indicators.
for ioc in self.ioc_domains:
for ioc in self.get_iocs("domains"):
# First we check the full domain.
if final_url.domain.lower() == ioc:
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",
final_url.url, orig_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", final_url.url)
self.log.warning("Found a known suspicious domain %s matching indicators from \"%s\"",
final_url.url, ioc["name"])
return True
return ioc
# Then we just check the top level domain.
if final_url.top_level.lower() == ioc:
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 matching a suspicious top level %s shortened as %s",
final_url.url, orig_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 matching a suspicious top level: %s", final_url.url)
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 ioc
def check_domains(self, urls):
"""Check the provided list of (suspicious) domains against a list of URLs.
:param urls: List of URLs to check
"""Check a list of URLs against the provided list of domain indicators.
:param urls: List of URLs to check against domain indicators
:type urls: list
:returns: Indicator details if matched, otherwise None
"""
if not urls:
return None
for url in urls:
if self.check_domain(url):
return True
check = self.check_domain(url)
if check:
return check
def check_process(self, process):
"""Check the provided process name against the list of process
indicators.
:param process: Process name to check
:param process: Process name to check against process indicators
:type process: str
:returns: Indicator details if matched, otherwise None
"""
if not process:
return False
return None
proc_name = os.path.basename(process)
if proc_name in self.ioc_processes:
self.log.warning("Found a known suspicious process name \"%s\"", process)
return True
for ioc in self.get_iocs("processes"):
if proc_name == ioc["value"]:
self.log.warning("Found a known suspicious process name \"%s\" matching indicators from \"%s\"",
process, ioc["name"])
return ioc
if len(proc_name) == 16:
for bad_proc in self.ioc_processes:
if bad_proc.startswith(proc_name):
self.log.warning("Found a truncated known suspicious process name \"%s\"", process)
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 ioc
def check_processes(self, processes):
"""Check the provided list of processes against the list of
process indicators.
:param processes: List of processes to check
:param processes: List of processes to check against process indicators
:type processes: list
:returns: Indicator details if matched, otherwise None
"""
if not processes:
return None
for process in processes:
if self.check_process(process):
return True
check = self.check_process(process)
if check:
return check
def check_email(self, email):
"""Check the provided email against the list of email indicators.
:param email: Suspicious email to check
:param email: Email address to check against email indicators
:type email: str
:returns: Indicator details if matched, otherwise None
"""
if not email:
return False
return None
if email.lower() in self.ioc_emails:
self.log.warning("Found a known suspicious email address: \"%s\"", email)
return True
for ioc in self.get_iocs("emails"):
if email.lower() == ioc["value"].lower():
self.log.warning("Found a known suspicious email address \"%s\" matching indicators from \"%s\"",
email, ioc["name"])
return ioc
def check_file_name(self, file_name):
"""Check the provided file name against the list of file indicators.
:param file_name: File name to check against file
indicators
:type file_name: str
:returns: Indicator details if matched, otherwise None
"""
if not file_name:
return None
for ioc in self.get_iocs("file_names"):
if ioc["value"] == file_name:
self.log.warning("Found a known suspicious file name \"%s\" matching indicators from \"%s\"",
file_name, ioc["name"])
return ioc
def check_file_path(self, file_path):
"""Check the provided file path against the list of file indicators (both path and name).
:param file_path: File path or file name to check against file
indicators
:type file_path: str
:returns: Indicator details if matched, otherwise None
def check_file(self, file_path):
"""Check the provided file path against the list of file indicators.
:param file_path: Path or name of the file to check
"""
if not file_path:
return False
return None
file_name = os.path.basename(file_path)
if file_name in self.ioc_files:
self.log.warning("Found a known suspicious file: \"%s\"", file_path)
return True
ioc = self.check_file_name(os.path.basename(file_path))
if ioc:
return ioc
for ioc in self.get_iocs("file_paths"):
# Strip any trailing slash from indicator paths to match directories.
if file_path.startswith(ioc["value"].rstrip("/")):
self.log.warning("Found a known suspicious file path \"%s\" matching indicators form \"%s\"",
file_path, ioc["name"])
return ioc
def check_profile(self, profile_uuid):
"""Check the provided configuration profile UUID against the list of indicators.
:param profile_uuid: Profile UUID to check against configuration profile indicators
:type profile_uuid: str
:returns: Indicator details if matched, otherwise None
"""
if not profile_uuid:
return None
for ioc in self.get_iocs("ios_profile_ids"):
if profile_uuid in ioc["value"]:
self.log.warning("Found a known suspicious profile ID \"%s\" matching indicators from \"%s\"",
profile_uuid, ioc["name"])
return ioc
def check_file_hash(self, file_hash):
"""Check the provided SHA256 file hash against the list of indicators.
:param file_hash: SHA256 hash to check
:type file_hash: str
:returns: Indicator details if matched, otherwise None
"""
if not file_hash:
return None
for ioc in self.get_iocs("files_sha256"):
if file_hash.lower() == ioc["value"].lower():
self.log.warning("Found a known suspicious file with hash \"%s\" matching indicators from \"%s\"",
file_hash, ioc["name"])
return ioc
def check_app_id(self, app_id):
"""Check the provided app identifier (typically an Android package name)
against the list of indicators.
:param app_id: App ID to check against the list of indicators
:type app_id: str
:returns: Indicator details if matched, otherwise None
"""
if not app_id:
return None
for ioc in self.get_iocs("app_ids"):
if app_id.lower() == ioc["value"].lower():
self.log.warning("Found a known suspicious app with ID \"%s\" matching indicators from \"%s\"",
app_id, ioc["name"])
return ioc
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 open(ioc_path, "w", encoding="utf-8") as handle:
handle.write(res.text)
log.info("Saved indicator file to %s", os.path.basename(ioc_path))

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

@@ -0,0 +1,26 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from rich import print
from .updates import check_for_updates
from .version import MVT_VERSION
def logo():
print("\n")
print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
print("\t\thttps://mvt.re")
print(f"\t\tVersion: {MVT_VERSION}")
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,29 +1,27 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import csv
import glob
import io
import logging
import os
import re
import simplejson as json
from .indicators import Indicators
class DatabaseNotFoundError(Exception):
pass
class DatabaseCorruptedError(Exception):
pass
class InsufficientPrivileges(Exception):
pass
class MVTModule(object):
"""This class provides a base for all extraction modules."""
@@ -31,14 +29,20 @@ class MVTModule(object):
slug = None
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
fast_mode=False, log=None, results=None):
"""Initialize module.
:param file_path: Path to the module's database file, if there is any.
: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
@@ -46,14 +50,14 @@ class MVTModule(object):
self.fast_mode = fast_mode
self.log = log
self.indicators = None
self.results = results
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:
with open(json_path, "r", encoding="utf-8") as handle:
results = json.load(handle)
if log:
log.info("Loaded %d results from \"%s\"",
@@ -61,30 +65,23 @@ class MVTModule(object):
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 _find_paths(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 load_indicators(self, file_path):
self.indicators = Indicators(file_path, self.log)
def check_indicators(self):
"""Check the results of this module against a provided list of
indicators."""
indicators.
"""
raise NotImplementedError
def save_to_json(self):
"""Save the collected results to a json file.
"""
"""Save the collected results to a json file."""
if not self.output_folder:
return
@@ -93,9 +90,9 @@ class MVTModule(object):
if self.results:
results_file_name = f"{name}.json"
results_json_path = os.path.join(self.output_folder, results_file_name)
with open(results_json_path, "w") as handle:
with open(results_json_path, "w", encoding="utf-8") as handle:
try:
json.dump(self.results, handle, indent=4)
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)
@@ -103,15 +100,26 @@ class MVTModule(object):
if self.detected:
detected_file_name = f"{name}_detected.json"
detected_json_path = os.path.join(self.output_folder, detected_file_name)
with open(detected_json_path, "w") as handle:
json.dump(self.detected, handle, indent=4)
with 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
def to_timeline(self):
"""Convert results into a timeline.
@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:
@@ -129,19 +137,11 @@ class MVTModule(object):
self.timeline_detected.append(record)
# De-duplicate timeline entries.
self.timeline = self.timeline_deduplicate(self.timeline)
self.timeline_detected = self.timeline_deduplicate(self.timeline_detected)
def timeline_deduplicate(self, timeline):
"""Serialize entry as JSON to deduplicate repeated entries"""
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]
self.timeline = self._deduplicate_timeline(self.timeline)
self.timeline_detected = self._deduplicate_timeline(self.timeline_detected)
def run(self):
"""Run the main module procedure.
"""
"""Run the main module procedure."""
raise NotImplementedError
@@ -159,7 +159,7 @@ def run_module(module):
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 and recovery failed: %s",
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",
@@ -186,16 +186,18 @@ def run_module(module):
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.
:param timeline: List of records to order and store
:param timeline_path: Path to the csv file to store the timeline to
"""
with io.open(timeline_path, "a+", encoding="utf-8") as handle:
with open(timeline_path, "a+", encoding="utf-8") as handle:
csvoutput = csv.writer(handle, delimiter=",", quotechar="\"")
csvoutput.writerow(["UTC Timestamp", "Plugin", "Event", "Description"])
for event in sorted(timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""):
csvoutput.writerow([
event["timestamp"],
event["module"],
event["event"],
event["data"],
event.get("timestamp"),
event.get("module"),
event.get("event"),
event.get("data"),
])

View File

@@ -1,16 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
# From: https://gist.github.com/stanchan/bce1c2d030c76fe9223b5ff6ad0f03db
from click import Option, UsageError, command, option
from click import Option, UsageError
class MutuallyExclusiveOption(Option):
"""This class extends click to support mutually exclusive options.
"""
"""This class extends click to support mutually exclusive options."""
def __init__(self, *args, **kwargs):
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))

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

@@ -0,0 +1,20 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import requests
from packaging import version
from .version import MVT_VERSION
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,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -7,6 +7,7 @@ import requests
from tld import get_tld
SHORTENER_DOMAINS = [
"1drv.ms",
"1link.in",
"1url.com",
"2big.at",
@@ -15,29 +16,29 @@ SHORTENER_DOMAINS = [
"2ya.com",
"4url.cc",
"6url.com",
"a.gg",
"a.nf",
"a2a.me",
"abbrr.com",
"adf.ly",
"adjix.com",
"a.gg",
"alturl.com",
"a.nf",
"atu.ca",
"b23.ru",
"bacn.me",
"bit.ly",
"bit.do",
"bit.ly",
"bkite.com",
"bloat.me",
"budurl.com",
"buff.ly",
"buk.me",
"burnurl.com",
"c-o.in",
"chilp.it",
"clck.ru",
"clickmeter.com",
"cli.gs",
"c-o.in",
"clickmeter.com",
"cort.as",
"cut.ly",
"cuturl.com",
@@ -55,19 +56,20 @@ SHORTENER_DOMAINS = [
"esyurl.com",
"ewerl.com",
"fa.b",
"fff.to",
"ff.im",
"fff.to",
"fhurl.com",
"fire.to",
"firsturl.de",
"flic.kr",
"fly2.ws",
"fon.gs",
"forms.gle",
"fwd4.me",
"gl.am",
"go2cut.com",
"go2.me",
"go.9nl.com",
"go2.me",
"go2cut.com",
"goo.gl",
"goshrink.com",
"gowat.ch",
@@ -77,6 +79,7 @@ SHORTENER_DOMAINS = [
"hex.io",
"hover.com",
"href.in",
"ht.ly",
"htxt.it",
"hugeurl.com",
"hurl.it",
@@ -85,8 +88,8 @@ SHORTENER_DOMAINS = [
"icanhaz.com",
"idek.net",
"inreply.to",
"iscool.net",
"is.gd",
"iscool.net",
"iterasi.net",
"jijr.com",
"jmp2.net",
@@ -102,10 +105,11 @@ SHORTENER_DOMAINS = [
"linkbee.com",
"linkbun.ch",
"liurl.cn",
"lnk.gd",
"lnk.in",
"ln-s.net",
"ln-s.ru",
"lnk.gd",
"lnk.in",
"lnkd.in",
"loopt.us",
"lru.jp",
"lt.tl",
@@ -122,44 +126,44 @@ SHORTENER_DOMAINS = [
"nn.nf",
"notlong.com",
"nsfw.in",
"o-x.fr",
"om.ly",
"ow.ly",
"o-x.fr",
"pd.am",
"pic.gd",
"ping.fm",
"piurl.com",
"pnt.me",
"poprl.com",
"posted.at",
"post.ly",
"posted.at",
"profile.to",
"qicute.com",
"qlnk.net",
"quip-art.com",
"rb6.me",
"redirx.com",
"rickroll.it",
"ri.ms",
"rickroll.it",
"riz.gd",
"rsmonkey.com",
"rubyurl.com",
"ru.ly",
"rubyurl.com",
"s7y.us",
"safe.mn",
"sharein.com",
"sharetabs.com",
"shorl.com",
"short.ie",
"short.to",
"shortlinks.co.uk",
"shortna.me",
"short.to",
"shorturl.com",
"shoturl.us",
"shrinkify.com",
"shrinkster.com",
"shrten.com",
"shrt.st",
"shrten.com",
"shrunkin.com",
"shw.me",
"simurl.com",
@@ -177,20 +181,20 @@ SHORTENER_DOMAINS = [
"tcrn.ch",
"thrdl.es",
"tighturl.com",
"tiny123.com",
"tinyarro.ws",
"tiny.cc",
"tiny.pl",
"tiny123.com",
"tinyarro.ws",
"tinytw.it",
"tinyuri.ca",
"tinyurl.com",
"tinyvid.io",
"tnij.org",
"togoto.us",
"to.ly",
"traceurl.com",
"togoto.us",
"tr.im",
"tr.my",
"traceurl.com",
"turo.us",
"tweetburner.com",
"twirl.at",
@@ -200,21 +204,21 @@ SHORTENER_DOMAINS = [
"twiturl.de",
"twurl.cc",
"twurl.nl",
"u6e.de",
"ub0.cc",
"u.mavrev.com",
"u.nu",
"u6e.de",
"ub0.cc",
"updating.me",
"ur1.ca",
"url.co.uk",
"url.ie",
"url4.eu",
"urlao.com",
"urlbrief.com",
"url.co.uk",
"urlcover.com",
"urlcut.com",
"urlenco.de",
"urlhawk.com",
"url.ie",
"urlkiss.com",
"urlot.com",
"urlpire.com",
@@ -227,29 +231,26 @@ SHORTENER_DOMAINS = [
"wapurl.co.uk",
"wipi.es",
"wp.me",
"xaddr.com",
"x.co",
"x.se",
"xaddr.com",
"xeeurl.com",
"xr.com",
"xrl.in",
"xrl.us",
"x.se",
"xurl.jp",
"xzb.cc",
"yep.it",
"yfrog.com",
"ymlp.com",
"yweb.com",
"zi.ma",
"zi.pe",
"zipmyurl.com",
"zz.gd",
"ymlp.com",
"forms.gle",
"ht.ly",
"lnkd.in",
"1drv.ms",
]
class URL:
def __init__(self, url):
@@ -263,33 +264,50 @@ class URL:
def get_domain(self):
"""Get the domain from a URL.
:param url: URL to parse
:returns: Just the domain name extracted from the URL
:type url: str
:returns: Domain name extracted from URL
:rtype: str
"""
# TODO: Properly handle exception.
try:
return get_tld(self.url, as_object=True, fix_protocol=True).parsed_url.netloc.lower().lstrip("www.")
except:
except Exception:
return None
def get_top_level(self):
"""Get only the top level domain from a URL.
"""Get only the top-level domain from a URL.
:param url: URL to parse
:returns: The top level domain extracted from the URL
:type url: str
:returns: Top-level domain name extracted from URL
:rtype: str
"""
# TODO: Properly handle exception.
try:
return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower()
except:
except Exception:
return None
def check_if_shortened(self):
def check_if_shortened(self) -> bool:
"""Check if the URL is among list of shortener services.
:returns: True if the URL is shortened, otherwise False
:rtype: bool
"""
if self.domain.lower() in SHORTENER_DOMAINS:
self.is_shortened = True
return self.is_shortened
def unshorten(self):
"""Unshorten the URL by requesting an HTTP HEAD response."""
res = requests.head(self.url)
if str(res.status_code).startswith("30"):
return res.headers["Location"]

View File

@@ -1,18 +1,22 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import datetime
import hashlib
import os
import re
def convert_mactime_to_unix(timestamp, from_2001=True):
"""Converts Mac Standard Time to a Unix timestamp.
:param timestamp: MacTime timestamp (either int or float)
:returns: Unix epoch 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
@@ -35,35 +39,49 @@ def convert_mactime_to_unix(timestamp, from_2001=True):
def convert_chrometime_to_unix(timestamp):
"""Converts Chrome timestamp to a Unix timestamp.
:param timestamp: Chrome timestamp as int
:returns: Unix epoch timestamp
:param timestamp: Chrome timestamp as int.
:type timestamp: int
:returns: Unix epoch timestamp.
"""
epoch_start = datetime.datetime(1601, 1 , 1)
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
:returns: ISO timestamp string in YYYY-mm-dd HH:MM:SS.ms format
: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
:returns: Search results
:param text: Any provided text.
:type text: str
:returns: Search results.
"""
return re.findall("(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
return re.findall(r"(?P<url>https?://[^\s]+)", text, re.IGNORECASE)
def get_sha256_from_file_path(file_path):
"""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:
@@ -72,12 +90,16 @@ def get_sha256_from_file_path(file_path):
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: Converted object.
:returns: Object converted to string.
:rtype: str
"""
new_obj = {}
if not isinstance(obj, dict):

6
mvt/common/version.py Normal file
View File

@@ -0,0 +1,6 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
MVT_VERSION = "1.5.5"

View File

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

View File

@@ -1,22 +1,27 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import os
import tarfile
import click
from rich.logging import RichHandler
from rich.prompt import Prompt
from mvt.common.indicators import Indicators
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.fs import BACKUP_MODULES, FS_MODULES
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"
@@ -24,17 +29,23 @@ logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__)
# Help messages of repeating options.
OUTPUT_HELP_MESSAGE = "Specify a path to a folder where you want to store JSON results"
# 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
@@ -79,6 +90,7 @@ def decrypt_backup(ctx, destination, password, key_file, backup_path):
if not backup.can_process():
ctx.exit(1)
backup.process_backup()
@@ -119,17 +131,18 @@ def extract_key(password, backup_path, 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), help="Path to indicators file")
@click.option("--output", "-o", type=click.Path(exists=False), help=OUTPUT_HELP_MESSAGE)
@click.option("--fast", "-f", is_flag=True, help="Avoid running time/resource consuming features")
@click.option("--list-modules", "-l", is_flag=True, help="Print list of available modules and exit")
@click.option("--module", "-m", help="Name of a single module you would like to run instead of all")
@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:
for backup_module in BACKUP_MODULES + MIXED_MODULES:
log.info(" - %s", backup_module.__name__)
return
@@ -143,24 +156,21 @@ def check_backup(ctx, iocs, output, fast, backup_path, list_modules, module):
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
if iocs:
# Pre-load indicators for performance reasons.
log.info("Loading indicators from provided file at: %s", iocs)
indicators = Indicators(iocs)
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
timeline = []
timeline_detected = []
for backup_module in BACKUP_MODULES:
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 iocs:
indicators.log = m.log
if indicators.total_ioc_count > 0:
m.indicators = indicators
m.indicators.log = m.log
run_module(m)
timeline.extend(m.timeline)
@@ -172,22 +182,27 @@ def check_backup(ctx, iocs, output, fast, backup_path, list_modules, module):
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), help="Path to indicators file")
@click.option("--output", "-o", type=click.Path(exists=False), help=OUTPUT_HELP_MESSAGE)
@click.option("--fast", "-f", is_flag=True, help="Avoid running time/resource consuming features")
@click.option("--list-modules", "-l", is_flag=True, help="Print list of available modules and exit")
@click.option("--module", "-m", help="Name of a single module you would like to run instead of all")
@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:
for fs_module in FS_MODULES + MIXED_MODULES:
log.info(" - %s", fs_module.__name__)
return
@@ -201,14 +216,12 @@ def check_fs(ctx, iocs, output, fast, dump_path, list_modules, module):
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
if iocs:
# Pre-load indicators for performance reasons.
log.info("Loading indicators from provided file at: %s", iocs)
indicators = Indicators(iocs)
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
timeline = []
timeline_detected = []
for fs_module in FS_MODULES:
for fs_module in FS_MODULES + MIXED_MODULES:
if module and fs_module.__name__ != module:
continue
@@ -216,10 +229,9 @@ def check_fs(ctx, iocs, output, fast, dump_path, list_modules, module):
log=logging.getLogger(fs_module.__module__))
m.is_fs_dump = True
if iocs:
indicators.log = m.log
if indicators.total_ioc_count > 0:
m.indicators = indicators
m.indicators.log = m.log
run_module(m)
timeline.extend(m.timeline)
@@ -231,19 +243,24 @@ def check_fs(ctx, iocs, output, fast, dump_path, list_modules, module):
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", required=True, type=click.Path(exists=True),
help="Path to indicators file")
@click.option("--list-modules", "-l", is_flag=True, help="Print list of available modules and exit")
@click.option("--module", "-m", help="Name of a single module you would like to run instead of all")
@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))
def check_iocs(iocs, list_modules, module, folder):
@click.pass_context
def check_iocs(ctx, iocs, list_modules, module, folder):
all_modules = []
for entry in BACKUP_MODULES + FS_MODULES:
for entry in BACKUP_MODULES + FS_MODULES + MIXED_MODULES:
if entry not in all_modules:
all_modules.append(entry)
@@ -256,10 +273,10 @@ def check_iocs(iocs, list_modules, module, folder):
log.info("Checking stored results against provided indicators...")
# Pre-load indicators for performance reasons.
log.info("Loading indicators from provided file at: %s", iocs)
indicators = Indicators(iocs)
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)
@@ -276,11 +293,25 @@ def check_iocs(iocs, list_modules, module, folder):
m = iocs_module.from_json(file_path,
log=logging.getLogger(iocs_module.__module__))
indicators.log = m.log
m.indicators = indicators
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,12 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import binascii
import glob
import logging
import multiprocessing
import os
import os.path
import shutil
import sqlite3
@@ -14,9 +16,12 @@ from iOSbackup import iOSbackup
log = logging.getLogger(__name__)
class DecryptBackup:
"""This class provides functions to decrypt an encrypted iTunes backup
using either a password or a key file.
"""
def __init__(self, backup_path, dest_path=None):
@@ -24,14 +29,37 @@ class DecryptBackup:
:param backup_path: Path to the encrypted backup folder
:param dest_path: Path to the folder where to store the decrypted backup
"""
self.backup_path = backup_path
self.backup_path = os.path.abspath(backup_path)
self.dest_path = dest_path
self._backup = None
self._decryption_key = None
def can_process(self) -> bool:
return self._backup is not None
@staticmethod
def is_encrypted(backup_path) -> bool:
"""Query Manifest.db file to see if it's encrypted or not.
:param backup_path: Path to the backup to decrypt
"""
conn = sqlite3.connect(os.path.join(backup_path, "Manifest.db"))
cur = conn.cursor()
try:
cur.execute("SELECT fileID FROM Files LIMIT 1;")
except sqlite3.DatabaseError:
return True
else:
log.critical("The backup does not seem encrypted!")
return False
def _process_file(self, relative_path, domain, item, file_id, item_folder):
self._backup.getFileDecryptedCopy(manifestEntry=item,
targetName=file_id,
targetFolder=item_folder)
log.info("Decrypted file %s [%s] to %s/%s", relative_path, domain, item_folder, file_id)
def process_backup(self):
if not os.path.exists(self.dest_path):
os.makedirs(self.dest_path)
@@ -42,6 +70,8 @@ class DecryptBackup:
# We store it to the destination folder.
shutil.copy(self._backup.manifestDB, manifest_path)
pool = multiprocessing.Pool(multiprocessing.cpu_count())
for item in self._backup.getBackupFilesList():
try:
file_id = item["backupFile"]
@@ -64,16 +94,28 @@ class DecryptBackup:
# Add manifest plist to both keys to handle this.
item["manifest"] = item["file"]
self._backup.getFileDecryptedCopy(manifestEntry=item,
targetName=file_id,
targetFolder=item_folder)
log.info("Decrypted file %s [%s] to %s/%s", relative_path, domain, item_folder, file_id)
pool.apply_async(self._process_file, args=(relative_path,
domain, item,
file_id,
item_folder))
except Exception as e:
log.error("Failed to decrypt file %s: %s", relative_path, e)
pool.close()
pool.join()
# Copying over the root plist files as well.
for file_name in os.listdir(self.backup_path):
if file_name.endswith(".plist"):
log.info("Copied plist file %s to %s", file_name, self.dest_path)
shutil.copy(os.path.join(self.backup_path, file_name),
self.dest_path)
def decrypt_with_password(self, password):
"""Decrypts an encrypted iOS backup.
:param password: Password to use to decrypt the original backup
"""
log.info("Decrypting iOS backup at path %s with password", self.backup_path)
@@ -88,6 +130,11 @@ class DecryptBackup:
log.critical("No Manifest.plist in %s, and %d Manifest.plist files in subdirs. Please choose one!",
self.backup_path, len(possible))
return
# Before proceeding, we check whether the backup is indeed encrypted.
if not self.is_encrypted(self.backup_path):
return
try:
self._backup = iOSbackup(udid=os.path.basename(self.backup_path),
cleartextpassword=password,
@@ -104,11 +151,17 @@ class DecryptBackup:
def decrypt_with_key_file(self, key_file):
"""Decrypts an encrypted iOS backup using a key file.
:param key_file: File to read the key bytes to decrypt the backup
"""
log.info("Decrypting iOS backup at path %s with key file %s",
self.backup_path, key_file)
# Before proceeding, we check whether the backup is indeed encrypted.
if not self.is_encrypted(self.backup_path):
return
with open(key_file, "rb") as handle:
key_bytes = handle.read()
@@ -127,8 +180,7 @@ class DecryptBackup:
log.critical("Failed to decrypt backup. Did you provide the correct key file?")
def get_key(self):
"""Retrieve and prints the encryption key.
"""
"""Retrieve and prints the encryption key."""
if not self._backup:
return
@@ -138,13 +190,15 @@ class DecryptBackup:
def write_key(self, key_path):
"""Save extracted key to file.
:param key_path: Path to the file where to write the derived decryption key.
"""
if not self._decryption_key:
return
try:
with open(key_path, 'w') as handle:
with open(key_path, 'w', encoding="utf-8") as handle:
handle.write(self._decryption_key)
except Exception as e:
log.exception(e)

View File

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

View File

@@ -0,0 +1,11 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .backup_info import BackupInfo
from .configuration_profiles import ConfigurationProfiles
from .manifest import Manifest
from .profile_events import ProfileEvents
BACKUP_MODULES = [BackupInfo, ConfigurationProfiles, Manifest, ProfileEvents]

View File

@@ -0,0 +1,50 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import plistlib
from mvt.common.module import DatabaseNotFoundError
from mvt.ios.versions import latest_ios_version
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
if "Product Version" in info:
latest = latest_ios_version()
if info["Product Version"] != latest["version"]:
self.log.warning("This phone is running an outdated iOS version: %s (latest is %s)",
info["Product Version"], latest['version'])

View File

@@ -0,0 +1,104 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import 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.
ioc = self.indicators.check_profile(result["plist"]["PayloadUUID"])
if ioc:
self.log.warning(f"Found a known malicious configuration profile \"{result['plist']['PayloadDisplayName']}\" with UUID '{result['plist']['PayloadUUID']}'.")
result["matched_indicator"] = ioc
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 "OTAProfileStub" in conf_plist:
if "SignerCerts" in conf_plist["OTAProfileStub"]:
conf_plist["OTAProfileStub"]["SignerCerts"] = [b64encode(x) for x in conf_plist["OTAProfileStub"]["SignerCerts"]]
if "PayloadContent" in conf_plist["OTAProfileStub"]:
if "EnrollmentIdentityPersistentID" in conf_plist["OTAProfileStub"]["PayloadContent"]:
conf_plist["OTAProfileStub"]["PayloadContent"]["EnrollmentIdentityPersistentID"] = b64encode(conf_plist["OTAProfileStub"]["PayloadContent"]["EnrollmentIdentityPersistentID"])
if "PushTokenDataSentToServerKey" in conf_plist:
conf_plist["PushTokenDataSentToServerKey"] = b64encode(conf_plist["PushTokenDataSentToServerKey"])
if "LastPushTokenHash" in conf_plist:
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"])
if "IdentityPersistentRef" in conf_plist["PayloadContent"][x]:
conf_plist["PayloadContent"][x]["IdentityPersistentRef"] = b64encode(conf_plist["PayloadContent"][x]["IdentityPersistentRef"])
self.results.append({
"file_id": conf_file["file_id"],
"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,19 +1,18 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import datetime
import io
import os
import plistlib
import sqlite3
import biplist
from mvt.common.module import DatabaseNotFoundError
from mvt.common.utils import convert_timestamp_to_iso
from .base import IOSExtraction
from ..base import IOSExtraction
class Manifest(IOSExtraction):
@@ -26,15 +25,22 @@ class Manifest(IOSExtraction):
log=log, results=results)
def _get_key(self, dictionary, key):
"""
Unserialized plist objects can have keys which are str or byte types
"""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)
def _convert_timestamp(self, timestamp_or_unix_time_int):
"""Older iOS versions stored the manifest times as unix timestamps."""
@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:
@@ -43,20 +49,20 @@ class Manifest(IOSExtraction):
def serialize(self, record):
records = []
if "modified" not in record or "statusChanged" not in record:
if "modified" not in record or "status_changed" not in record:
return
for ts in set([record["created"], record["modified"], record["statusChanged"]]):
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["statusChanged"] else "-"
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['relativePath']} - {record['domain']}"
"data": f"{record['relative_path']} - {record['domain']}"
})
return records
@@ -66,33 +72,30 @@ class Manifest(IOSExtraction):
return
for result in self.results:
if not "relativePath" in result:
continue
if not result["relativePath"]:
if not result.get("relative_path"):
continue
if result["domain"]:
if os.path.basename(result["relativePath"]) == "com.apple.CrashReporter.plist" and result["domain"] == "RootDomain":
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(result["relativePath"]):
self.log.warning("Found a known malicious file at path: %s", result["relativePath"])
if self.indicators.check_file_path("/" + result["relative_path"]):
self.detected.append(result)
continue
relPath = result["relativePath"].lower()
for ioc in self.indicators.ioc_domains:
if ioc.lower() in relPath:
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, relPath)
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("Impossible to find the module's database file")
raise DatabaseNotFoundError("unable to find backup's Manifest.db")
self.log.info("Found Manifest.db database at path: %s", manifest_db_path)
@@ -103,32 +106,33 @@ class Manifest(IOSExtraction):
names = [description[0] for description in cur.description]
for file_entry in cur:
file_data = dict()
file_data = {}
for index, value in enumerate(file_entry):
file_data[names[index]] = value
cleaned_metadata = {
"fileID": file_data["fileID"],
"file_id": file_data["fileID"],
"domain": file_data["domain"],
"relativePath": file_data["relativePath"],
"relative_path": file_data["relativePath"],
"flags": file_data["flags"],
"created": "",
}
if file_data["file"]:
try:
file_plist = biplist.readPlist(io.BytesIO(file_data["file"]))
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")),
"statusChanged": self._convert_timestamp(self._get_key(file_metadata, "LastStatusChange")),
"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:
self.log.exception("Error reading manifest file metadata")
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)

View File

@@ -0,0 +1,61 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import 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))

157
mvt/ios/modules/base.py Normal file
View File

@@ -0,0 +1,157 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import 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,44 +1,19 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .analytics import Analytics
from .cache_files import CacheFiles
from .calls import Calls
from .chrome_favicon import ChromeFavicon
from .chrome_history import ChromeHistory
from .contacts import Contacts
from .filesystem import Filesystem
from .firefox_favicon import FirefoxFavicon
from .firefox_history import FirefoxHistory
from .idstatuscache import IDStatusCache
from .interactionc import InteractionC
from .locationd import LocationdClients
from .manifest import Manifest
from .net_datausage import Datausage
from .net_netusage import Netusage
from .safari_browserstate import SafariBrowserState
from .safari_favicon import SafariFavicon
from .safari_history import SafariHistory
from .sms import SMS
from .sms_attachments import SMSAttachments
from .shutdownlog import ShutdownLog
from .version_history import IOSVersionHistory
from .webkit_indexeddb import WebkitIndexedDB
from .webkit_localstorage import WebkitLocalStorage
from .webkit_resource_load_statistics import WebkitResourceLoadStatistics
from .webkit_safariviewservice import WebkitSafariViewService
from .webkit_session_resource_log import WebkitSessionResourceLog
from .whatsapp import Whatsapp
BACKUP_MODULES = [SafariBrowserState, SafariHistory, Datausage, SMS, SMSAttachments,
ChromeHistory, ChromeFavicon, WebkitSessionResourceLog,
WebkitResourceLoadStatistics, Calls, IDStatusCache, LocationdClients,
InteractionC, FirefoxHistory, FirefoxFavicon, Contacts, Manifest, Whatsapp]
FS_MODULES = [IOSVersionHistory, SafariHistory, SafariFavicon, SafariBrowserState,
WebkitIndexedDB, WebkitLocalStorage, WebkitSafariViewService,
WebkitResourceLoadStatistics, WebkitSessionResourceLog,
Datausage, Netusage, ChromeHistory,
ChromeFavicon, Calls, IDStatusCache, SMS, SMSAttachments,
LocationdClients, InteractionC, FirefoxHistory, FirefoxFavicon,
Contacts, CacheFiles, Whatsapp, Filesystem]
FS_MODULES = [CacheFiles, Filesystem, Netusage, Analytics, SafariFavicon, ShutdownLog,
IOSVersionHistory, WebkitIndexedDB, WebkitLocalStorage,
WebkitSafariViewService]

View File

@@ -0,0 +1,124 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import 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
ioc = self.indicators.check_process(value)
if ioc:
self.log.warning("Found mention of a malicious process \"%s\" in %s file at %s",
value, result["artifact"], result["timestamp"])
result["matched_indicator"] = ioc
self.detected.append(result)
continue
ioc = self.indicators.check_domain(value)
if ioc:
self.log.warning("Found mention of a malicious domain \"%s\" in %s file at %s",
value, result["artifact"], result["timestamp"])
result["matched_indicator"] = ioc
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,113 +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 io
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 _is_database_malformed(self, file_path):
# Check if the database is malformed.
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()
return recover
def _recover_database(self, file_path):
"""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.
self.log.info("Database at path %s is malformed. Trying to recover...", file_path)
if not os.path.exists(file_path):
return
if not shutil.which("sqlite3"):
raise DatabaseCorruptedError("Unable 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("Recovery of database failed")
self.log.info("Database at path %s recovered successfully!", file_path)
def _find_ios_database(self, backup_ids=None, root_paths=[]):
"""Try to locate the module's database file from either an iTunes
backup or a full filesystem dump.
:param backup_id: iTunes backup database file's ID (or hash).
:param root_paths: Glob patterns for files to seek in filesystem dump.
"""
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 = os.path.join(self.base_folder, backup_id[0:2], backup_id)
# If we found the correct backup file, then we stop searching.
if os.path.exists(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 root_path in root_paths:
for found_path in glob.glob(os.path.join(self.base_folder, root_path)):
# If we find a valid path, we set file_path.
if os.path.exists(found_path):
file_path = found_path
break
# Otherwise, we reset the file_path again.
file_path = None
# 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")
if self._is_database_malformed(self.file_path):
self._recover_database(self.file_path)

View File

@@ -1,12 +1,12 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import sqlite3
from .base import IOSExtraction
from ..base import IOSExtraction
class CacheFiles(IOSExtraction):
@@ -34,13 +34,15 @@ class CacheFiles(IOSExtraction):
return
self.detected = {}
for key, items in self.results.items():
for item in items:
if self.indicators.check_domain(item["url"]):
for key, values in self.results.items():
for value in values:
ioc = self.indicators.check_domain(value["url"])
if ioc:
value["matched_indicator"] = ioc
if key not in self.detected:
self.detected[key] = [item,]
self.detected[key] = [value, ]
else:
self.detected[key].append(item)
self.detected[key].append(value)
def _process_cache_file(self, file_path):
self.log.info("Processing cache file at path: %s", file_path)
@@ -54,18 +56,18 @@ class CacheFiles(IOSExtraction):
return
key_name = os.path.relpath(file_path, self.base_folder)
if not key_name in self.results:
if key_name not in self.results:
self.results[key_name] = []
for row in cur:
self.results[key_name].append(dict(
entry_id=row[0],
version=row[1],
hash_value=row[2],
storage_policy=row[3],
url=row[4],
isodate=row[5],
))
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 = {}

View File

@@ -1,5 +1,5 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Copyright (c) 2021-2022 Claudio Guarnieri.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
@@ -8,12 +8,15 @@ import os
from mvt.common.utils import convert_timestamp_to_iso
from .base import IOSExtraction
from ..base import IOSExtraction
class Filesystem(IOSExtraction):
"""This module extracts creation and modification date of files from a
full file-system dump."""
full file-system dump.
"""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
@@ -25,8 +28,8 @@ class Filesystem(IOSExtraction):
return {
"timestamp": record["modified"],
"module": self.__class__.__name__,
"event": f"file_modified",
"data": record["file_path"],
"event": "entry_modified",
"data": record["path"],
}
def check_indicators(self):
@@ -34,19 +37,48 @@ class Filesystem(IOSExtraction):
return
for result in self.results:
if self.indicators.check_file(result["file_path"]):
if "path" not in result:
continue
ioc = self.indicators.check_file_path(result["path"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
# If we are instructed to run fast, we skip the rest.
if self.fast_mode:
continue
for ioc in self.indicators.get_iocs("processes"):
parts = result["path"].split("/")
if ioc["value"] in parts:
self.log.warning("Found known suspicious process name mentioned in file at path \"%s\" matching indicators from \"%s\"",
result["path"], ioc["name"])
result["matched_indicator"] = ioc
self.detected.append(result)
def run(self):
for root, dirs, files in os.walk(self.base_folder):
for dir_name in dirs:
try:
dir_path = os.path.join(root, dir_name)
result = {
"path": os.path.relpath(dir_path, self.base_folder),
"modified": convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(os.stat(dir_path).st_mtime)),
}
except Exception:
continue
else:
self.results.append(result)
for file_name in files:
try:
file_path = os.path.join(root, file_name)
result = {
"file_path": os.path.relpath(file_path, self.base_folder),
"path": os.path.relpath(file_path, self.base_folder),
"modified": convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(os.stat(file_path).st_mtime)),
}
except:
except Exception:
continue
else:
self.results.append(result)

View File

@@ -1,70 +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 biplist
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",
]
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 ts in self.timestamps:
if ts in record.keys():
records.append({
"timestamp": record[ts],
"module": self.__class__.__name__,
"event": ts,
"data": f"{ts} from {record['package']}"
})
return records
def run(self):
self._find_ios_database(backup_ids=LOCATIOND_BACKUP_IDS, root_paths=LOCATIOND_ROOT_PATHS)
self.log.info("Found Locationd Clients plist at path: %s", self.file_path)
file_plist = biplist.readPlist(self.file_path)
for app in file_plist:
if file_plist[app] is dict:
result = file_plist[app]
result["package"] = app
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)
self.log.info("Extracted a total of %d Locationd Clients entries", len(self.results))

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