Compare commits

...

385 Commits

Author SHA1 Message Date
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
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
Nex
bdbfe02315 Bumped version 2021-08-12 18:44:14 +02:00
Nex
54eaf046b0 Standardizing base classes declarations 2021-08-12 18:36:31 +02:00
Nex
23e4babbc9 Sorted imports 2021-08-12 18:34:33 +02:00
Nex
78b9fcd50c Added super init to NetBase 2021-08-12 18:34:23 +02:00
Nex
4eb7a64614 Removed serial in declaration 2021-08-12 18:33:58 +02:00
Nex
e512e0b72f Fixed download_apks init 2021-08-12 18:25:57 +02:00
Nex
7884c28253 Merge branch 'j0k2r-main' 2021-08-12 18:21:36 +02:00
Nex
8ca7030195 Refactored serial specification for ADB 2021-08-12 18:21:21 +02:00
Nex
f78c671885 Merge branch 'main' of https://github.com/j0k2r/mvt into j0k2r-main 2021-08-12 18:07:50 +02:00
Nex
411ac53522 Letting module handler catch any exception 2021-08-12 17:57:40 +02:00
Nex
8be60e8a04 Checking all processes 2021-08-12 17:53:19 +02:00
Nex
8a484b3b24 Added a more clear message regarding rooted Androids 2021-08-12 17:47:20 +02:00
Nex
0a7512cfb2 Checking for manipulated entries even when no indicators are provided 2021-08-12 12:57:27 +02:00
Nex
257f3732e3 Merge branch 'DL6ER-main' 2021-08-12 12:56:17 +02:00
Nex
8d93ab66c9 Improved logging around detection results 2021-08-12 12:56:12 +02:00
Nex
6e19d34700 Merge branch 'main' of https://github.com/DL6ER/mvt into DL6ER-main 2021-08-12 12:49:36 +02:00
Nex
271cdede0f Merge branch 'dkg-error-cleanup' 2021-08-12 12:48:47 +02:00
Nex
88324c7c42 Standardized to logging format 2021-08-12 12:48:29 +02:00
Daniel Kahn Gillmor
ec93c3d8b8 Even friendlier behaviors when the user mis-specifies the backup path
As discussed in #147
2021-08-10 23:19:45 -04:00
Daniel Kahn Gillmor
1288f8ca53 handle error cases better 2021-08-10 22:57:15 -04:00
DL6ER
290776a286 Log if there was no detection made by the module
Signed-off-by: DL6ER <dl6er@dl6er.de>
2021-08-10 12:13:23 +02:00
Nex
44b677fdb2 Updated README 2021-08-09 16:14:48 +02:00
Nex
3ae822d3ac Updated README 2021-08-09 16:14:08 +02:00
Nex
7940fb2879 Updated README 2021-08-09 16:12:23 +02:00
Nex
af7bc3ca31 Updated README 2021-08-09 16:12:10 +02:00
Nex
d606f9570f Updated README 2021-08-09 16:10:42 +02:00
Hamza Z
15c0d71933 Fix merge conflicts 2021-08-08 20:05:50 +02:00
Nex
24c89183a3 Bumped version 2021-08-06 18:44:16 +02:00
Nex
e5f7727c80 Fixed typo (closes: #157) 2021-08-06 18:40:09 +02:00
Nex
7b00f03f03 Bumped version 2021-08-05 09:04:22 +02:00
Nex
9f696dcb72 Added version 14.7.1 2021-08-05 09:03:02 +02:00
Nex
ef139effdb Merge branch 'dkg-clearer-error-reporting' 2021-08-05 08:56:52 +02:00
Nex
2302c9fb1c Fixed language 2021-08-05 08:56:41 +02:00
Nex
9bb8ae5187 Merge branch 'clearer-error-reporting' of https://github.com/dkg/mvt into dkg-clearer-error-reporting 2021-08-05 08:54:29 +02:00
Nex
76e6138d77 Catching check if root exception more grafully (closes: #5) 2021-08-05 08:49:34 +02:00
Nex
0bc660a2b3 Updated documentation (closes: #3) 2021-08-04 19:14:06 +02:00
Nex
7ae9ecbf5a Removed newline 2021-08-03 17:25:16 +02:00
Nex
1e8278aeec Updated README 2021-08-03 15:51:58 +02:00
Nex
995ebc02cf Fixing language 2021-08-03 10:28:28 +02:00
Nex
12e0f14400 Added note on running MVT on Windows 2021-08-03 10:24:38 +02:00
Nex
6ef5b9d311 Merge pull request #148 from dkg/quotes
mvt-ios sqlite3 db recovery: fix quoting sent to sqlite3 .clone
2021-08-03 09:31:43 +02:00
Daniel Kahn Gillmor
33e90c1707 mvt-ios sqlite3 db recovery: fix quoting sent to sqlite3 .clone
In b2afce5c79, the db filename is
wrapped in double-quotes when passing it to the sqlite3 tool's
`.clone` helper command.

For parsing safety, we avoid performing this cleanup if the filename
itself has a double-quote character in it.  Otherwise, a malformed
filename could lead to arbitrary injection into the sqlite3 command.

In be24680046, the sqlite3 wrapping
changes to single-quotes.  Either the safety check should be amended
to block pathnames with single-quotes, or the sqlite3 wrapping should
revert to double-quotes.

I opted for the latter here because i think single-quotes are more
likely than double-quotes to show up in pathnames (e.g. a folder named
"Daniel's files"), but either change would be fine, of course.
2021-08-02 11:26:00 -04:00
Daniel Kahn Gillmor
706c429595 mvt-ios decrypt-backup: Improve error messages for known cases
The two most common reasons that `mvt-ios decrypt-backup` can fail are
wrong passwords and not pointing to an actual backup.

We can distinguish these cases based on the kinds of errors thrown
from iOSbackup (at least for the current versions that i'm testing
with).

When we encounter those particular exceptions, just report a simple
summary and don't overwhelm the user with a backtrace.  If we
encounter an unexpected exception, leave the reporting as-is.

Closes: #28, #36
2021-08-02 11:07:31 -04:00
Nex
f011fd19e8 More explicit copyright and licensing notes 2021-08-01 21:11:08 +02:00
Nex
bc48dc2cf5 Fixed import order 2021-08-01 19:53:20 +02:00
Nex
f3c0948283 Fixing exception name in Manifest module 2021-08-01 19:50:25 +02:00
Nex
be24680046 Enforcing double quotes 2021-08-01 19:50:04 +02:00
Nex
a3d10c1824 Merge pull request #140 from dkg/avoid-shell-True
Avoid breakage with paths with unusual names
2021-08-01 19:45:11 +02:00
Daniel Kahn Gillmor
b2afce5c79 Avoid breakage with paths with unusual names
If file_path has any whitespace or shell metacharacters in it, then
the invocation of subprocess.call would be likely to break (or even
accidentally execute code, depending on how perverse the pathnames
are).

It's generally a good plan to avoid shell=True for subprocess.call
where you can lay out the arguments deliberately in python.  This one
looks relatively straightforward (but note, i have not tested it,
sorry!)

Note that if a name has a `"` character in it, we still fail, out of
safety reasons.

in particular, we want to avoid command injection into the sqlite
binary with particularly malicious names that look something like the
following:

```
foo.db"; .shell touch should-not-exist; .nullvalue "
```
2021-08-01 11:35:38 -04:00
Nex
b2e210e91c Removed unused import 2021-08-01 14:16:28 +02:00
Nex
6f83bf5ae1 Removed duplicates 2021-08-01 14:05:21 +02:00
Nex
a979b82ec6 Bumped version 2021-08-01 13:59:59 +02:00
Nex
eaef75d931 Added iPhone models definitions 2021-08-01 13:59:30 +02:00
Nex
1650aea248 pip3 for clarity 2021-07-31 19:48:19 +02:00
Nex
bc3634bf30 Specifying it is a password prompt 2021-07-31 10:27:44 +02:00
Nex
87ffd9e003 Bumped version 2021-07-31 10:23:38 +02:00
Nex
19f355810a Merge branch 'dkg-update-libimobiledevice-docs' 2021-07-31 10:19:46 +02:00
Nex
38b7aa6032 Updated doc on backup 2021-07-31 10:19:38 +02:00
Nex
feb285015a Merge branch 'update-libimobiledevice-docs' of https://github.com/dkg/mvt into dkg-update-libimobiledevice-docs 2021-07-31 10:16:58 +02:00
Nex
933ee65897 Merge branch 'dkg-mvt_decrypt-backup_password_from_env' 2021-07-31 10:13:43 +02:00
Nex
ad9ab1aeba Switched to using rich Prompt 2021-07-31 10:13:18 +02:00
Nex
4debee72cd Merge branch 'mvt_decrypt-backup_password_from_env' of https://github.com/dkg/mvt into dkg-mvt_decrypt-backup_password_from_env 2021-07-31 10:07:14 +02:00
Nex
d7031bd25f Merge branch 'dkg-ioc-docs' 2021-07-31 10:05:55 +02:00
Nex
5b5b065bc4 Updated doc page on IOCs 2021-07-31 10:05:41 +02:00
Daniel Kahn Gillmor
59206fc450 Describe how to use and find IOCs
This offers generic documentation, to show how MVT can be used with
arbitrary STIX-formatted IOCs, while still pointing users at some
known-to-be-useful sample files.
2021-07-31 00:46:36 -04:00
Daniel Kahn Gillmor
7b1b31f7be Update libimobiledevice docs about backup password reset
In this stage, the user is likely to want to run `idevicebackup2` in
interactive mode, so clearly specify the `-i` flag in the right place
(just dropping `-i` at the end of the command does not work as
expected -- i think `idevicebackup2 backup encryption on -i` tries to
set the password to `-i`).

More importantly, note that resetting the password by resetting all
the settings runs a risk of removing some of the forensic information.
Etienne identified a file that he thought was wiped as a result of
this in the call this morning, but I don't remember which file it was.

Maybe `id_status_cache.json` ?  If you have more concrete info, please
add it here too!
2021-07-30 23:49:06 -04:00
Daniel Kahn Gillmor
270e002f1b mvt-ios extract-key: enable pulling password from the environment
This enables automated use of extract-key without requiring a password
to be placed in the command line, where it might leak.
2021-07-30 23:10:54 -04:00
Daniel Kahn Gillmor
53adc05338 mvt-ios decrypt-backup: Enable pulling password from the environment.
Specifying the password on the command line with `--password XXX`
leaves the password itself visible to any process on the machine which
can scan the process table.

On some systems (including common GNU/Linux distributions) this
visibility is possible by default.

This change should make it possible to offer the password without
putting it into the process table; rather, the user puts the password
in the environment, and specifies the name of the environment
variable, like so:

```
$ export MVT_IOS_BACKUP_PASSWORD=WronglySconeRoundnessUnruffled
$ mvt-ios decrypt-backup -d /path/to/dest /path/to/data/XXXXXXXX-YYYYYYYYYYYYYYY/
$ unset MVT_IOS_BACKUP_PASSWORD
```

or you can do so using a prefixed env var, as described in the updated
check.md documentation.
2021-07-30 23:10:54 -04:00
Nex
d7f29a4e88 Updated README 2021-07-30 21:26:48 +02:00
Nex
444e70a6eb Merge branch 'pkirkovsky-extract-key' 2021-07-30 18:47:05 +02:00
Nex
b264ae946d Refactored to include functionality in existing DecryptBackup class 2021-07-30 18:46:45 +02:00
Nex
bfcfb3aa06 Merge branch 'extract-key' of https://github.com/pkirkovsky/mvt into pkirkovsky-extract-key 2021-07-30 18:29:47 +02:00
Nex
3e7d85039a Merge branch 'EmilienCourt-fix_SMS_PATH' 2021-07-30 18:09:13 +02:00
Nex
632409c81d Using consistent constant names 2021-07-30 18:08:52 +02:00
Nex
6df6064370 Merge branch 'fix_SMS_PATH' of https://github.com/EmilienCourt/mvt into EmilienCourt-fix_SMS_PATH 2021-07-30 18:04:16 +02:00
Nex
99e80fd942 Updated documentation links 2021-07-30 17:59:17 +02:00
Nex
9451da4514 Removed duplicate title 2021-07-30 17:56:05 +02:00
Tek
5ac0025470 Merge pull request #137 from opsec-infosec/main
Update Dockerfile missing sqlite3
2021-07-30 14:34:07 +02:00
opsec-infosec
9a6c4d251e Update Dockerfile
Add sqlite3 to Dockerfile for extraction of SMS messages
2021-07-30 16:13:06 +04:00
Nex
eda1976518 Added missing space in workflow file 2021-07-30 11:43:52 +02:00
Nex
c966eea7e6 Sorted imports 2021-07-30 11:40:09 +02:00
Nex
abcbefe359 Added safety checks to workflow 2021-07-30 11:39:43 +02:00
Nex
22d090569c Disabled pytest until unit tests are available 2021-07-30 11:20:59 +02:00
Nex
d490344142 Removed lint 2021-07-30 11:19:51 +02:00
Nex
7f361fb600 Create python-package.yml 2021-07-30 11:19:20 +02:00
Nex
18ed58cbf9 Removed unused dependency 2021-07-30 11:19:15 +02:00
Nex
3a6f57502e Merge branch 'febrezo-master' 2021-07-30 11:08:47 +02:00
Nex
490fb12302 Refactored creation of output folders 2021-07-30 11:08:32 +02:00
Nex
e2d82b0349 Merge branch 'master' of https://github.com/febrezo/mvt into febrezo-master 2021-07-30 10:48:34 +02:00
Nex
1bf7f54c72 Merge pull request #131 from macmade/main
Chrome History - Cheking extracted URLs against indicators.
2021-07-29 13:48:34 +02:00
Nex
60a2dbb860 Added module to parse WebKit ResourceLoadStatistics observations.db (ref: #133) 2021-07-29 13:46:58 +02:00
macmade
5e03c28dbd Chrome History - Cheking extracted URLs against indicators. 2021-07-29 02:33:32 +02:00
Nex
4fb6e204d1 Ordered iOS versions 2021-07-28 08:33:33 +02:00
Pavel Kirkovsky
f4340bd4f9 Merge branch 'mvt-project:main' into extract-key 2021-07-27 17:15:37 -07:00
Nex
7947d413b5 Update lint-python.yml 2021-07-27 21:44:31 +02:00
Nex
45beb6eeda Update lint-python.yml 2021-07-27 21:43:25 +02:00
Nex
ad81d5c450 Delete python-publish.yml 2021-07-27 21:42:21 +02:00
Nex
fe8c013b0f Bumped version 2021-07-27 21:40:15 +02:00
Nex
caa5d8ee8c Rename lint_python.yml to lint-python.yml 2021-07-27 21:37:26 +02:00
Nex
2baac1f52c Create python-publish.yml 2021-07-27 21:37:06 +02:00
Nex
dec7616a3d Merge pull request #124 from cclauss/patch-1
GitHub Action to lint Python code
2021-07-27 21:30:11 +02:00
Nex
b1ae777621 Fixed variable name 2021-07-27 21:29:14 +02:00
Nex
404edfee9a Merge branch 'main' of github.com:mvt-project/mvt 2021-07-27 21:28:36 +02:00
Nex
3bb0d5020c Fixed variable name 2021-07-27 21:27:43 +02:00
Christian Clauss
b500ee9429 codespell 2021-07-27 12:11:31 +02:00
Christian Clauss
3f2058441a bandit --recursive --skip B108,B112,B404,B602 . 2021-07-27 12:09:52 +02:00
Christian Clauss
9931edccc4 GitHub Action to lint Python code
Output:
2021-07-27 12:06:47 +02:00
tek
9e33ece3e9 Fixes issue with Manifest format 2021-07-27 01:23:22 +02:00
Nex
32aeaaf91c Update README.md 2021-07-26 21:48:55 +02:00
Nex
8b253b5e7c Update README.md 2021-07-26 21:39:49 +02:00
Nex
362bce7c76 Update README.md 2021-07-26 21:38:36 +02:00
Nex
e821421ca7 Update README.md 2021-07-26 21:35:35 +02:00
Nex
95ab269671 Fixed some formatting 2021-07-26 19:33:12 +02:00
Tek
49f592ebe8 Merge pull request #116 from adamstiefel/patch-1
fix: readme grammar
2021-07-26 10:53:24 +02:00
Adam Stiefel
6b436f2057 fix: readme grammar
Changed "evidences" to "evidence". Changed "understanding basics" to "understanding the basics". Changed "command line" to "command-line"
2021-07-25 17:16:26 -04:00
Nex
13ce55f4ac Added some context to error message 2021-07-25 15:51:24 +02:00
Tek
2ca0081833 Merge pull request #110 from EmilienCourt/fix_whatsapp
[ADB] Fix WhatsApp database parsing (thumb_image)
2021-07-25 15:25:39 +02:00
emilien
47df94fa12 fix typo 2021-07-25 15:13:23 +02:00
emilien
e5003b6490 Handle SMS bases in mmssms.db instead of bugle_db 2021-07-25 15:06:22 +02:00
emilien
3d9574682c Fix WhatsApp thumb image 2021-07-25 14:13:10 +02:00
Nex
3dcc24acd5 Added build 18G69 2021-07-25 12:19:45 +02:00
Nex
8f558db60b Fixed version number 2021-07-25 12:07:22 +02:00
Nex
7a02df4592 Merge branch 'main' of github.com:mvt-project/mvt 2021-07-25 12:04:07 +02:00
Nex
a61d4e17eb Snapshotting dependencies 2021-07-25 12:03:45 +02:00
Nex
3fd8d1524f Updated LICENSE 2021-07-25 12:01:23 +02:00
Nex
d8310797ef Merge pull request #109 from U039b/fix-#108
Fix #108
2021-07-25 11:49:12 +02:00
Nex
7fffef77ce Automatically recover malformed sqlite3 databases (closes: #25 #37) 2021-07-25 11:47:05 +02:00
U039b
b7d65e6123 Fix #108 2021-07-25 11:03:28 +02:00
Nex
9d9b77e02e Changing error message to info, to avoid confusion 2021-07-25 10:46:10 +02:00
Nex
6d0ff11540 Restored empty spaces for new line 2021-07-24 14:27:16 +02:00
Nex
97558ec3af Merge pull request #19 from goshawk22/patch-2
Better check for if device has root
2021-07-24 13:56:12 +02:00
Nex
4fdb868216 Merge pull request #76 from bryeetz/patch-1
Typo
2021-07-24 13:54:59 +02:00
Nex
25d6d52557 Merge pull request #98 from Trigus42/main
Fix download of APKs that require root privileges #2
2021-07-24 13:53:43 +02:00
Nex
d172a3fe69 Merge branch 'febrezo-dockerizing' 2021-07-24 13:24:12 +02:00
Nex
d6f49e76d6 Included Docker details in the documentation 2021-07-24 13:23:45 +02:00
Nex
8883306558 Merge branch 'dockerizing' of https://github.com/febrezo/mvt into febrezo-dockerizing 2021-07-24 13:10:04 +02:00
Trigus42
03523a40c0 Fix _adb_process_file & Improve _adb_download_root
- The _adb_download function doesn't need a package_name argument. This broke _adb_process_file and unnecessarily clutters function calls. Also, the function may be used to download other files or folders too. Generating a random filename seems like the best solution to me since it is less likely to get a duplicate filename and thus to replace an existing file.

- The path /sdcard/Download doesn't necessarily exist. Using /sdcard seems more reliable.
2021-07-24 12:09:59 +02:00
Nex
6c496ec3c2 Merge pull request #84 from pkirkovsky/package-versions
Require click >= 8.0.1
2021-07-23 21:08:07 +02:00
Pavel Kirkovsky
143ceafee2 Merge branch 'mvt-project:main' into package-versions 2021-07-23 12:02:11 -07:00
Pavel Kirkovsky
99640ac08c Merge branch 'mvt-project:main' into extract-key 2021-07-23 12:02:02 -07:00
Nex
ba84b3c18d Fixed variable name (closes: #72) 2021-07-23 18:05:51 +02:00
Nex
8e099e5985 Checking for valid indicators before continuing (closes: #35) 2021-07-23 18:04:41 +02:00
goshawk22
ad3faa186b Use command -v instead of which to check for root
`command` is built in, unlike `which`, and is more reliable.
https://github.com/mvt-project/mvt/pull/19#issuecomment-885650430
https://stackoverflow.com/questions/592620/how-can-i-check-if-a-program-exists-from-a-bash-script/677212#677212
2021-07-23 15:35:56 +01:00
Pavel Kirkovsky
30d0348256 Added extract-key info to main docs 2021-07-23 03:46:48 -07:00
Pavel Kirkovsky
8048ed8c3a Require click >= 8.0.1 2021-07-23 02:08:15 -07:00
Pavel Kirkovsky
af4826070a Update README with extract-key command 2021-07-22 23:55:08 -07:00
Pavel Kirkovsky
9fbcce4340 Add extract-key command 2021-07-22 23:52:52 -07:00
Pavel Kirkovsky
ece88744ed KeyUtils class for working with decryption keys 2021-07-22 23:52:39 -07:00
Bryan Scheetz
fa49203c9b Typo
adversial -> adversarial
2021-07-22 22:49:26 -04:00
tek
e69449a2f0 Fixes typos 2021-07-22 23:21:31 +02:00
febrezo
684aed8d11 Add compilation of libimobiledevice for iOS compatibility
Added considering the feedback reported in the #16 discussion.
2021-07-22 17:44:17 +02:00
tek
b19db5543b Update README 2021-07-21 13:59:54 +02:00
Hamza Z
2389d5e52d Add Android TCP connection support 2021-07-21 13:35:46 +02:00
Hamza Z
ccf0f3f18e Add Android device serial specification 2021-07-21 13:17:58 +02:00
Nex
af7c45ae22 Merge branch 'master' of github.com:mvt-project/mvt 2021-07-21 11:54:13 +02:00
Nex
8d68e7a166 Better handling of special characters when saving a timeline 2021-07-21 11:53:41 +02:00
Nex
3004690fd1 Merge pull request #21 from pkirkovsky/prompt-password
Prompt for password if none is given
2021-07-21 11:20:24 +02:00
Nex
2f05d4b4f9 Fixed typo 2021-07-21 11:07:15 +02:00
tek
f0a9196094 Merge branch 'master' of github.com:mvt-project/mvt 2021-07-21 10:44:43 +02:00
tek
ce46e608de fixes documentation 2021-07-21 10:44:10 +02:00
Tek
791e7db59c Merge pull request #7 from lunakk-PL/patch-1
Update download_apks.md
2021-07-21 10:32:48 +02:00
tek
3e048c4338 updated readme 2021-07-21 10:25:02 +02:00
Tek
a23b890350 Merge pull request #30 from runasand/patch-1
Update README.md
2021-07-21 10:16:29 +02:00
Tek
8fbf95a262 Merge pull request #31 from recurrence/master
[iOS CLI] Remove non-existent SYSDIAGNOSE_MODULES reference
2021-07-21 10:15:56 +02:00
Tyler Kellogg
967eb75e7c [iOS CLI] Remove non-existent SYSDIAGNOSE_MODULES reference 2021-07-20 15:01:09 -07:00
Runa Sandvik
2276df4f1b Update README.md
Use pip3 to install mvt from pypi
2021-07-20 17:55:22 -04:00
Pavel Kirkovsky
695555f26f Prompt for password if none is given 2021-07-20 05:44:36 -07:00
febrezo
1adf3f430b Add welcome message when the terminal is launched 2021-07-20 14:20:27 +02:00
Adam Lawson
9317586851 Better check for if device has root
"which su" will return the path of the su binary, or it will return nothing. 
The python boolean of a string with something in it (such as the path of the su binary), will be True.
An empty string (where there is no su binary) will be False.
2021-07-20 12:55:10 +01:00
Adam Lawson
cb6bde5b8c Fix download of APKs that require root privileges
Some system APKs are stored in directories that require root privileges, such as /system/product.
2021-07-20 12:53:44 +01:00
febrezo
f3afc871cd Create alias for abe instead of custom command 2021-07-20 13:45:55 +02:00
febrezo
8c855b645d Add Dockerfile with Android dependencies solved 2021-07-20 12:10:37 +02:00
febrezo
732db070f2 Add implicit creation of output folders 2021-07-20 03:09:53 +02:00
lunakk-PL
167f7e3d77 Update download_apks.md
proper Koodous link -> https://koodous.com/
2021-07-19 13:45:47 +02:00
114 changed files with 4538 additions and 1972 deletions

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

@@ -0,0 +1,43 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Python package
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest safety
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Safety checks
run: safety check
# - name: Test with pytest
# run: |
# pytest

2
.gitignore vendored
View File

@@ -129,3 +129,5 @@ dmypy.json
.pyre/
*.pyc
# Temporal files
*~

7
AUTHORS Normal file
View File

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

90
Dockerfile Normal file
View File

@@ -0,0 +1,90 @@
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 unzip\
&& DEBIAN_FRONTEND=noninteractive apt-get -y install default-jre-headless \
# Install build tools for libimobiledevice
# ----------------------------------------
build-essential \
checkinstall \
git \
autoconf \
automake \
libtool-bin \
libplist-dev \
libusbmuxd-dev \
libssl-dev \
sqlite3 \
pkg-config \
# Clean up
# --------
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt
# Build libimobiledevice
# ----------------------
RUN git clone https://github.com/libimobiledevice/libplist \
&& git clone https://github.com/libimobiledevice/libimobiledevice-glue \
&& git clone https://github.com/libimobiledevice/libusbmuxd \
&& git clone https://github.com/libimobiledevice/libimobiledevice \
&& git clone https://github.com/libimobiledevice/usbmuxd \
&& cd libplist && ./autogen.sh && make && make install && ldconfig \
&& cd ../libimobiledevice-glue && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh --prefix=/usr && make && make install && ldconfig \
&& cd ../libusbmuxd && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./autogen.sh && make && make install && ldconfig \
&& 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
# --------------
RUN pip3 install mvt
# Installing ABE
# --------------
RUN mkdir /opt/abe \
&& wget https://github.com/nelenkov/android-backup-extractor/releases/download/20210709062403-4c55371/abe.jar -O /opt/abe/abe.jar \
# Create alias for abe
&& 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 \
&& 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

20
LICENSE
View File

@@ -1,4 +1,4 @@
MVT License 1.0
MVT License 1.1
===============
1. Definitions
@@ -35,7 +35,7 @@ MVT License 1.0
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
@@ -89,16 +89,16 @@ MVT License 1.0
a Larger Work.
1.16. "Device Owner" (or "Device Owners")
means an individal or a legal entity with legal ownership of an
means an individual or a legal entity with legal ownership of an
electronic device which is being analysed through the use of
Covered Software or a Larger Work, or from which Data was extracted
for subsequent analysis.
1.17. "Data Owner" (or "Data Owners")
means an individial or group of individuals who made use of the
electronic device from which Data that is extracted and/or analyzed
originated. "Data Owner" might or might not differ from "Device
Owner".
means an individual or group of individuals who made legitimate use
of the electronic device from which Data that is extracted and/or
analyzed originated. "Data Owner" might or might not differ from
"Device Owner".
2. License Grants and Conditions
--------------------------------
@@ -381,8 +381,8 @@ Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the MVT License,
v. 1.0. If a copy of the MVT License was not distributed with this
file, You can obtain one at TODO.
v. 1.1. If a copy of the MVT License was not distributed with this
file, You can obtain one at https://license.mvt.re/1.1/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
@@ -395,7 +395,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the MVT License, v. 1.0.
defined by the MVT License, v. 1.1.
This license is an adaption of Mozilla Public License, v. 2.0.

View File

@@ -1,45 +1,34 @@
<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)
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.
[Please check out the documentation.](https://mvt.readthedocs.io/en/latest/)
It has been developed and released by the [Amnesty International Security Lab](https://www.amnesty.org/en/tech/) in July 2021 in the context of the [Pegasus project](https://forbiddenstories.org/about-the-pegasus-project/) along with [a technical forensic methodology and forensic evidence](https://www.amnesty.org/en/latest/research/2021/07/forensic-methodology-report-how-to-catch-nso-groups-pegasus/).
*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
First you need to install dependencies, on Linux `sudo apt install python3 python3-pip libusb-1.0-0` or on MacOS `brew install python3 libusb`.
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/)):
Then you can install mvt from pypi with `pip install mvt`, or directly form sources:
```bash
git clone https://github.com/mvt-project/mvt.git
cd mvt
pip3 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/).
**Please note:** MVT is best run on Linux or Mac systems. [It does not currently support running natively on Windows.](https://docs.mvt.re/en/latest/install/#mvt-on-windows)
## Usage
MVT provides two commands `mvt-ios` and `mvt-android` with the following subcommands available:
* `mvt-ios`:
* `check-backup`: Extract artifacts from an iTunes backup
* `check-fs`: Extract artifacts from a full filesystem dump
* `check-iocs`: Compare stored JSON results to provided indicators
* `decrypt-backup`: Decrypt an encrypted iTunes backup
* `mvt-android`:
* `check-backup`: Check an Android Backup
* `download-apks`: Download all or non-safelisted installed APKs
Check out [the documentation to see how to use them](https://mvt.readthedocs.io/en/latest/).
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. Therefore, the goal of this license is to prohibit the use of MVT (and any other software licensed the same) for the purpose of *adversarial forensics*.
In order to achieve this, MVT is released under an adaptation of [Mozilla Public License v2.0](https://www.mozilla.org/MPL). This modified license includes a new clause 3.0, "Consensual Use Restriction" which permits the use of the licensed software (and any *"Larger Work"* derived from it) exclusively with the explicit consent of the person/s whose data is being extracted and/or analysed (*"Data Owner"*).
[Read the LICENSE](https://github.com/mvt-project/mvt/blob/main/LICENSE)
The purpose of MVT is to facilitate the ***consensual forensic analysis*** of devices of those who might be targets of sophisticated mobile spyware attacks, especially members of civil society and marginalized communities. We do not want MVT to enable privacy violations of non-consenting individuals. In order to achieve this, MVT is released under its own license. [Read more here.](https://docs.mvt.re/en/latest/license/)

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import sys
@@ -10,4 +10,5 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mvt import android
android.cli()

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import sys
@@ -10,4 +10,5 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mvt import ios
ios.cli()

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,49 @@
# 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
```
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 -all
```
## Unpack the backup
In order to unpack the backup, use [Android Backup Extractor (ABE)](https://github.com/nelenkov/android-backup-extractor) to convert it to a readable file format. Make sure that java is installed on your system and use the following command:
```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.
Alternatively, [ab-decrypt](https://github.com/joernheissler/ab-decrypt) can be used for that purpose.
## Check the backup
You can then extract SMSs containing links with MVT:
```bash
$ mvt-android check-backup --output sms .
$ mvt-android check-backup --output /path/to/results/ /path/to/backup/
16:18:38 INFO [mvt.android.cli] Checking ADB backup located at: .
INFO [mvt.android.modules.backup.sms] Running module SMS...
INFO [mvt.android.modules.backup.sms] Processing SMS backup
file at ./apps/com.android.providers.telephony/d_f/000
000_sms_backup
INFO [mvt.android.modules.backup.sms] Processing SMS backup file at /path/to/backup/apps/com.android.providers.telephony/d_f/000000_sms_backup
16:18:39 INFO [mvt.android.modules.backup.sms] Extracted a total of
64 SMS messages containing links
```
Through the `--iocs` argument you can specify a [STIX2](https://oasis-open.github.io/cti-documentation/stix/intro) file defining a list of malicious indicators to check against the records extracted from the backup by mvt. Any matches will be highlighted in the terminal output.
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.

View File

@@ -1,24 +1,34 @@
# 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
```
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://www.koodous.com). While these lookups do not provide any conclusive assessment on all of the extracted APKs, they might highlight any known malicious ones:
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
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,8 +1,21 @@
# Methodology for Android forensic
For different technical reasons, it is more complex to do a forensic analysis of an Android phone.
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.
Currently MVT allows to perform two different checks on an Android phone:
However, not all is lost.
* Download APKs installed in order to analyze them
* Extract Android backup in order to look for suspicious SMS
## 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.

33
docs/docker.md Normal file
View File

@@ -0,0 +1,33 @@
Using Docker simplifies having all the required dependencies and tools (including most recent versions of [libimobiledevice](https://libimobiledevice.org)) readily installed.
Install Docker following the [official documentation](https://docs.docker.com/get-docker/).
Once installed, you can clone MVT's repository and build its Docker image:
```bash
git clone https://github.com/mvt-project/mvt.git
cd mvt
docker build -t mvt .
```
Test if the image was created successfully:
```bash
docker run -it mvt
```
If a prompt is spawned successfully, you can close it with `exit`.
If you wish to use MVT to test an Android device you will need to enable the container's access to the host's USB devices. You can do so by enabling the `--privileged` flag and mounting the USB bus device as a volume:
```bash
docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt
```
**Please note:** the `--privileged` parameter is generally regarded as a security risk. If you want to learn more about this check out [this explainer on container escapes](https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/) as it gives access to the whole system.
Recent versions of Docker provide a `--device` parameter allowing to specify a precise USB device without enabling `--privileged`:
```bash
docker run -it --device=/dev/<your_usb_port> mvt
```

View File

@@ -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,29 +1,45 @@
# 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
First install some basic dependencies that will be necessary to build all required tools:
```bash
sudo apt install python3 python3-pip libusb-1.0-0
sudo apt install python3 python3-pip libusb-1.0-0 sqlite3
```
*libusb-1.0-0* is not required if you intend to only use `mvt-ios` and not `mvt-android`.
## Dependencies on Mac
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.
Running MVT on Mac requires Xcode and [homebrew](https://brew.sh) to be installed.
## Dependencies on macOS
Running MVT on macOS requires Xcode and [homebrew](https://brew.sh) to be installed.
In order to install dependencies use:
```bash
brew install python3 libusb
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](https://developer.android.com/studio/releases/platform-tools):
```bash
brew install --cask android-platform-tools
```
Or by downloading the [official binary releases](https://developer.android.com/studio/releases/platform-tools).
## MVT on Windows
MVT does not currently officially support running natively on Windows. While most functionality should work out of the box, there are known issues especially with `mvt-android`.
It is recommended to try installing and running MVT from [Windows Subsystem Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/about) and follow Linux installation instructions for your distribution of choice.
## Installing MVT
If you haven't done so, you can add this to your `.bashrc` or `.zshrc` file in order to add locally installed Pypi binaries to your `$PATH`:
@@ -35,7 +51,7 @@ export PATH=$PATH:~/.local/bin
Then you can install MVT directly from [pypi](https://pypi.org/project/mvt/)
```bash
pip install mvt
pip3 install mvt
```
Or from the source code:

View File

@@ -10,8 +10,10 @@ 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.
MVT's purpose is not to facilitate adversial forensics of non-consenting individuals' devices. The use of MVT and derivative products to extract and/or analyse data originating from devices used by individuals not consenting to the procedure is explicitly prohibited in the [license](license.md).
MVT's purpose is not to facilitate adversarial forensics of non-consenting individuals' devices. The use of MVT and derivative products to extract and/or analyse data originating from devices used by individuals not consenting to the procedure is explicitly prohibited in the [license](license.md).

46
docs/iocs.md Normal file
View File

@@ -0,0 +1,46 @@
# Indicators of Compromise (IOCs)
MVT uses [Structured Threat Information Expression (STIX)](https://oasis-open.github.io/cti-documentation/stix/intro.html) files to identify potential traces of compromise.
These indicators of compromise are contained in a file with a particular structure of [JSON](https://en.wikipedia.org/wiki/JSON) with the `.stix2` or `.json` extensions.
You can indicate a path to a STIX2 indicators file when checking iPhone backups or filesystem dumps. For example:
```bash
mvt-ios check-backup --iocs ~/ios/malware.stix2 --output /path/to/iphone/output /path/to/backup
```
Or, with data from an Android backup:
```bash
mvt-android check-backup --iocs ~/iocs/malware.stix2 /path/to/android/backup/
```
After extracting forensics data from a device, you are also able to compare it with any STIX2 file you indicate:
```bash
mvt-ios check-iocs --iocs ~/iocs/malware.stix2 /path/to/iphone/output/
```
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/stalkerware.stix2).
You can automaticallly download the latest public indicator files with the command `mvt-ios download-iocs` or `mvt-android download-iocs`.
Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs.

View File

@@ -2,6 +2,32 @@
The backup might take some time. It is best to make sure the phone remains unlocked during the backup process. Afterwards, a new folder will be created under the path you specified using the UDID of the iPhone you backed up.
## Extracting and saving the decryption key (optional)
If you do not wish to enter a password every time when decrypting a backup, MVT can accept a key file instead. This key can be used with the `decrypt-backup` command.
To generate a key file, you will need your device backup and the backup password:
$ mvt-ios extract-key --help
Usage: mvt-ios extract-key [OPTIONS] BACKUP_PATH
Extract decryption key from an iTunes backup
Options:
-p, --password TEXT Password to use to decrypt the backup [required]
-k, --key-file FILE Key file to be written (if unset, will print to STDOUT)
--help Show this message and exit.
You can specify the password on the command line, or omit the `-p` option to have MVT prompt for a password. The `-k` option specifies where to save the file containing the decryption key. If `-k` is omitted, MVT will display the decryption key without saving.
_Note_: This decryption key is sensitive data! Keep the file safe.
To extract the key and have MVT prompt for a password:
```bash
mvt-ios extract-key -k /path/to/save/key /path/to/backup
```
## Decrypting a backup
In case you have an encrypted backup, you will need to decrypt it first. This can be done with `mvt-ios` as well:
@@ -15,9 +41,10 @@ In case you have an encrypted backup, you will need to decrypt it first. This ca
-d, --destination TEXT Path to the folder where to store the decrypted
backup [required]
-p, --password TEXT Password to use to decrypt the backup NOTE: This
argument is mutually exclusive with arguments:
[key_file].
-p, --password TEXT Password to use to decrypt the backup (or, set
MVT_IOS_BACKUP_PASSWORD environment variable)
NOTE: This argument is mutually exclusive with
arguments: [key_file].
-k, --key-file PATH File containing raw encryption key to use to decrypt
the backup NOTE: This argument is mutually exclusive
@@ -25,10 +52,10 @@ In case you have an encrypted backup, you will need to decrypt it first. This ca
--help Show this message and exit.
You can specify either a password via command-line or pass a key file, and you need to specify a destination path where the decrypted backup will be stored. Following is an example usage of `decrypt-backup`:
You can specify the password in the environment variable `MVT_IOS_BACKUP_PASSWORD`, or via command-line argument, or you can pass a key file. You need to specify a destination path where the decrypted backup will be stored. If a password cannot be found and no key file is specified, MVT will ask for a password. Following is an example usage of `decrypt-backup` sending the password via an environment variable:
```bash
mvt-ios decrypt-backup -p password -d /path/to/decrypted /path/to/backup
MVT_IOS_BACKUP_PASSWORD="mypassword" mvt-ios decrypt-backup -d /path/to/decrypted /path/to/backup
```
## Run `mvt-ios` on a Backup

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

@@ -3,10 +3,14 @@
If you have correctly [installed libimobiledevice](../install.md) you can easily generate an iTunes backup using the `idevicebackup2` tool included in the suite. First, you might want to ensure that backup encryption is enabled (**note: encrypted backup contain more data than unencrypted backups**):
```bash
idevicebackup2 backup encryption on
idevicebackup2 -i backup encryption on
```
Note that if a backup password was previously set on this device, you might need to use the same or change it. You can try changing password using `idevicebackup2 backup changepw` or resetting the password by resetting only the settings through the iPhone's Settings app.
Note that if a backup password was previously set on this device, you might need to use the same or change it. You can try changing password using `idevicebackup2 -i backup changepw`, or by turning off encryption (`idevicebackup2 -i backup encryption off`) and turning it back on again.
If you are not able to recover or change the password, you should try to disable encryption and obtain an unencrypted backup.
If all else fails, as a *last resort* you can try resetting the password by [resetting all the settings through the iPhone's Settings app](https://support.apple.com/en-us/HT205220), via `Settings » General » Reset » Reset All Settings`. Note that resetting the settings through the iPhone's Settings app will wipe some of the files that contain useful forensic traces, so try the options explained above first.
Once ready, you can proceed performing the backup:

View File

@@ -1,6 +1,6 @@
# Dumping the filesystem
While iTunes backup provide a lot of very useful databases and diagnistic data, in some cases you might want to jailbreak the device and perform a full filesystem dump. In that case, you should take a look at [checkra1n](https://checkra.in/), which provides an easy way to obtain root on most recent iPhone models.
While iTunes backup provide a lot of very useful databases and diagnostic data, in some cases you might want to jailbreak the device and perform a full filesystem dump. In that case, you should take a look at [checkra1n](https://checkra.in/), which provides an easy way to obtain root on most recent iPhone models.
!!! warning
Before you checkra1n any device, make sure you take a full backup, and that you are prepared to do a full factory reset before restoring it. Even after using checkra1n's "Restore System", some traces of the jailbreak are still left on the device and [apps with anti-jailbreaks will be able to detect them](https://github.com/checkra1n/BugTracker/issues/279) and stop functioning.

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

@@ -6,10 +6,10 @@ Before jumping into acquiring and analyzing data from an iOS device, you should
You will need to decide whether to attempt to jailbreak the device and obtain a full filesystem dump, or not.
While access the full filesystem allows to extact data that would otherwise be unavailable, it might not always be possible to jailbreak a certain iPhone model or version of iOS. In addition, depending on the type of jailbreak available, doing so might compromise some important records, pollute others, or potentially cause unintended malfunctioning of the device later in case it is used again.
While access the full filesystem allows to extract data that would otherwise be unavailable, it might not always be possible to jailbreak a certain iPhone model or version of iOS. In addition, depending on the type of jailbreak available, doing so might compromise some important records, pollute others, or potentially cause unintended malfunctioning of the device later in case it is used again.
If you are not expected to return the phone, you might want to consider to attempt a jailbreak after having exhausted all other options, including a backup.
#### 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,10 +172,22 @@ 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.
@@ -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,19 +323,31 @@ 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 lsit 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.
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.
If indicators are provided through the command-line, they are checked against the extracted names. Any matches are stored in *webkit_local_storage_detected.json*.
---
### `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

@@ -28,6 +28,7 @@ nav:
- Welcome: "index.md"
- Introduction: "introduction.md"
- Installation: "install.md"
- Using Docker: "docker.md"
- MVT for iOS:
- iOS Forensic Methodology: "ios/methodology.md"
- Install libimobiledevice: "ios/install.md"
@@ -41,6 +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 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,6 +1,6 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .cli import cli

View File

@@ -1,17 +1,21 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import sys
import click
import argparse
import logging
import os
import click
from rich.logging import RichHandler
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_IOC,
HELP_MSG_LIST_MODULES, HELP_MSG_MODULE,
HELP_MSG_OUTPUT, HELP_MSG_SERIAL)
from mvt.common.indicators import Indicators, download_indicators_files
from mvt.common.logo import logo
from mvt.common.module import run_module, save_timeline
from mvt.common.indicators import Indicators
from .download_apks import DownloadAPKs
from .lookups.koodous import koodous_lookup
from .lookups.virustotal import virustotal_lookup
@@ -24,15 +28,20 @@ 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"
#==============================================================================
# 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
@@ -40,25 +49,38 @@ def cli():
# Download APKs
#==============================================================================
@cli.command("download-apks", help="Download all or non-safelisted installed APKs installed on the device")
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--all-apks", "-a", is_flag=True,
help="Extract all packages installed on the phone, 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")
@click.option("--output", "-o", type=click.Path(exists=True),
help="Specify a path to a folder where you want to store JSON results")
@click.option("--output", "-o", type=click.Path(exists=False),
help="Specify a path to a folder where you want to store the APKs")
@click.option("--from-file", "-f", type=click.Path(exists=True),
help="Instead of acquiring from phone, load an existing packages.json file for lookups (mainly for debug purposes)")
def download_apks(all_apks, virustotal, koodous, all_checks, output, from_file):
@click.pass_context
def download_apks(ctx, all_apks, virustotal, koodous, all_checks, output, from_file, serial):
try:
if from_file:
download = DownloadAPKs.from_json(from_file)
else:
# TODO: Do we actually want to be able to run without storing any file?
if not output:
log.critical("You need to specify an output folder (with --output, -o) when extracting APKs from a device")
sys.exit(-1)
log.critical("You need to specify an output folder with --output!")
ctx.exit(1)
download = DownloadAPKs(output_folder=output, all_apks=all_apks)
if not os.path.exists(output):
try:
os.makedirs(output)
except Exception as e:
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
download = DownloadAPKs(output_folder=output, all_apks=all_apks,
log=logging.getLogger(DownloadAPKs.__module__))
if serial:
download.serial = serial
download.run()
packages = download.packages
@@ -73,19 +95,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
#==============================================================================
@cli.command("check-adb", help="Check an Android device over adb")
@click.option("--iocs", "-i", type=click.Path(exists=True), help="Path to indicators file")
@click.option("--output", "-o", type=click.Path(exists=True),
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):
@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL)
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
default=[], help=HELP_MSG_IOC)
@click.option("--output", "-o", type=click.Path(exists=False),
help=HELP_MSG_OUTPUT)
@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST)
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
@click.option("--module", "-m", help=HELP_MSG_MODULE)
@click.pass_context
def check_adb(ctx, iocs, output, fast, list_modules, module, serial):
if list_modules:
log.info("Following is the list of available check-adb modules:")
for adb_module in ADB_MODULES:
@@ -95,10 +121,15 @@ def check_adb(iocs, output, list_modules, module):
log.info("Checking Android through adb bridge")
if iocs:
# Pre-load indicators for performance reasons.
log.info("Loading indicators from provided file at %s", iocs)
indicators = Indicators(iocs)
if output and not os.path.exists(output):
try:
os.makedirs(output)
except Exception as e:
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
timeline = []
timeline_detected = []
@@ -106,11 +137,13 @@ def check_adb(iocs, output, list_modules, module):
if module and adb_module.__name__ != module:
continue
m = adb_module(output_folder=output, log=logging.getLogger(adb_module.__module__))
if iocs:
indicators.log = m.log
m = adb_module(output_folder=output, fast_mode=fast,
log=logging.getLogger(adb_module.__module__))
if indicators.ioc_count:
m.indicators = indicators
m.indicators.log = m.log
if serial:
m.serial = serial
run_module(m)
timeline.extend(m.timeline)
@@ -122,35 +155,53 @@ def check_adb(iocs, output, list_modules, module):
if len(timeline_detected) > 0:
save_timeline(timeline_detected, os.path.join(output, "timeline_detected.csv"))
#==============================================================================
# Check ADB backup
#==============================================================================
@cli.command("check-backup", help="Check an Android Backup")
@click.option("--iocs", "-i", type=click.Path(exists=True), help="Path to indicators file")
@click.option("--output", "-o", type=click.Path(exists=True), help=OUTPUT_HELP_MESSAGE)
@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))
def check_backup(iocs, output, backup_path):
@click.pass_context
def check_backup(ctx, iocs, output, backup_path, serial):
log.info("Checking ADB backup located at: %s", backup_path)
if iocs:
# Pre-load indicators for performance reasons.
log.info("Loading indicators from provided file at %s", iocs)
indicators = Indicators(iocs)
if output and not os.path.exists(output):
try:
os.makedirs(output)
except Exception as e:
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
indicators = Indicators(log=log)
indicators.load_indicators_files(iocs)
if os.path.isfile(backup_path):
log.critical("The path you specified is a not a folder!")
if os.path.basename(backup_path) == "backup.ab":
log.info("You can use ABE (https://github.com/nelenkov/android-backup-extractor) " \
log.info("You can use ABE (https://github.com/nelenkov/android-backup-extractor) "
"to extract 'backup.ab' files!")
sys.exit(-1)
ctx.exit(1)
for module in BACKUP_MODULES:
m = module(base_folder=backup_path, output_folder=output,
log=logging.getLogger(module.__module__))
if iocs:
indicators.log = m.log
if indicators.ioc_count:
m.indicators = indicators
m.indicators.log = m.log
if serial:
m.serial = serial
run_module(m)
#==============================================================================
# Command: download-iocs
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
def download_indicators():
download_indicators_files(log)

View File

@@ -1,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,22 +1,25 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import json
import logging
import pkg_resources
import os
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 simialr callback system.
# it's possible to have a similar callback system.
class PullProgress(tqdm):
"""PullProgress is a tqdm update system for APK downloads."""
@@ -26,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
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)
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)
@@ -137,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)
@@ -145,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__)
"""Save the results to the package.json file."""
json_path = os.path.join(self.output_folder, "apks.json")
with open(json_path, "w") as handle:
json.dump(packages, handle, indent=4)
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 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,18 +1,19 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import requests
import logging
from rich.text import Text
from rich.table import Table
from rich.progress import track
import requests
from rich.console import Console
from rich.progress import track
from rich.table import Table
from rich.text import Text
log = logging.getLogger(__name__)
def koodous_lookup(packages):
log.info("Looking up all extracted files on Koodous (www.koodous.com)")
log.info("This might take a while...")
@@ -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,17 +1,19 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import requests
import logging
from rich.text import Text
from rich.table import Table
from rich.progress import track
from rich.console import Console
from rich.progress import track
from rich.table import Table
from rich.text import Text
log = logging.getLogger(__name__)
def get_virustotal_report(hashes):
apikey = "233f22e200ca5822bd91103043ccac138b910db79f29af5616a9afe8b6f215ad"
url = f"https://www.virustotal.com/partners/sysinternals/file-reports?apikey={apikey}"
@@ -35,18 +37,20 @@ def get_virustotal_report(hashes):
log.error("Unexpected response from VirusTotal: %s", res.status_code)
return None
def virustotal_lookup(packages):
log.info("Looking up all extracted files on VirusTotal (www.virustotal.com)")
unique_hashes = []
for package in packages:
for file in package.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:
@@ -73,8 +77,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 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -1,18 +1,24 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .chrome_history import ChromeHistory
from .dumpsys_accessibility import DumpsysAccessibility
from .dumpsys_batterystats import DumpsysBatterystats
from .dumpsys_full import DumpsysFull
from .dumpsys_packages import DumpsysPackages
from .dumpsys_procstats import DumpsysProcstats
from .dumpsys_receivers import DumpsysReceivers
from .files import Files
from .logcat import Logcat
from .packages import Packages
from .processes import Processes
from .rootbinaries import RootBinaries
from .sms import SMS
from .whatsapp import Whatsapp
from .packages import Packages
from .rootbinaries import RootBinaries
ADB_MODULES = [ChromeHistory, SMS, Whatsapp, Processes,
DumpsysBatterystats, DumpsysProcstats,
DumpsysPackages, Packages, RootBinaries]
DumpsysAccessibility, DumpsysBatterystats, DumpsysProcstats,
DumpsysPackages, DumpsysReceivers, DumpsysFull,
Packages, RootBinaries, Logcat, Files]

View File

@@ -1,46 +1,49 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import sys
import time
import logging
import os
import random
import string
import sys
import tempfile
from adb_shell.adb_device import AdbDeviceUsb
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 DeviceAuthError, AdbCommandFailureException
from usb1 import USBErrorBusy, USBErrorAccess
from adb_shell.exceptions import (AdbCommandFailureException, DeviceAuthError,
UsbReadFailedError)
from usb1 import USBErrorAccess, USBErrorBusy
from mvt.common.module import MVTModule
from mvt.common.module import InsufficientPrivileges, MVTModule
log = logging.getLogger(__name__)
ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey")
ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub")
class AndroidExtraction(MVTModule):
"""This class provides a base for all Android extraction modules."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
"""Initialize Android extraction module.
:param file_path: Path to the database file to parse
:param base_folder: Path to a base folder containing an Android dump
:param output_folder: Path to the folder where to store extraction
results
"""
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
self.device = None
self.serial = None
@staticmethod
def _adb_check_keys():
"""Make sure Android adb keys exist."""
if not os.path.isdir(os.path.dirname(ADB_KEY_PATH)):
os.makedirs(os.path.dirname(ADB_KEY_PATH))
def _adb_check_keys(self):
"""Make sure Android adb keys exist.
"""
if not os.path.exists(ADB_KEY_PATH):
keygen(ADB_KEY_PATH)
@@ -48,15 +51,29 @@ 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)
self.device = AdbDeviceUsb()
with open(ADB_PUB_KEY_PATH, "rb") as handle:
pub_key = handle.read()
signer = PythonRSASigner(pub_key, priv_key)
# If no serial was specified or if the serial does not seem to be
# a HOST:PORT definition, we use the USB transport.
if not self.serial or ":" not in self.serial:
self.device = AdbDeviceUsb(serial=self.serial)
# Otherwise we try to use the TCP transport.
else:
addr = self.serial.split(":")
if len(addr) < 2:
raise ValueError("TCP serial number must follow the format: `address:port`")
self.device = AdbDeviceTcp(addr[0], int(addr[1]),
default_transport_timeout_s=30.)
while True:
try:
@@ -67,67 +84,129 @@ 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("[ ! -f /sbin/su ] || echo 1"))
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 Exception("The Android device does not seem to have a `su` binary. Cannot run this module.")
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_download(self, remote_path, local_path, progress_callback=None):
def _adb_check_file_exists(self, file):
"""Verify that a file exists.
:param file: Path of the file
:returns: Boolean indicating whether the file exists or not
"""
# TODO: Need to support checking files without root privileges as well.
# Connect to the device over adb.
self._adb_connect()
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
return bool(self._adb_command_as_root(f"[ ! -f {file} ] || echo 1"))
def _adb_download(self, remote_path, local_path, progress_callback=None, retry_root=True):
"""Download a file form the device.
:param remote_path: Path to download from the device
:param local_path: Path to where to locally store the copy of the file
:param progress_callback: Callback for download progress bar
:param progress_callback: Callback for download progress bar (Default value = None)
:param retry_root: Default value = True)
"""
try:
self.device.pull(remote_path, local_path, progress_callback)
except AdbCommandFailureException as e:
if retry_root:
self._adb_download_root(remote_path, local_path, progress_callback)
else:
raise Exception(f"Unable to download file {remote_path}: {e}")
def _adb_download_root(self, remote_path, local_path, progress_callback=None):
try:
# Check if we have root, if not raise an Exception.
self._adb_root_or_die()
# We generate a random temporary filename.
tmp_filename = "tmp_" + ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=10))
# We create a temporary local file.
new_remote_path = f"/sdcard/{tmp_filename}"
# We copy the file from the data folder to /sdcard/.
cp = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}")
if cp.startswith("cp: ") and "No such file or directory" in cp:
raise Exception(f"Unable to process file {remote_path}: File not found")
elif cp.startswith("cp: ") and "Permission denied" in cp:
raise Exception(f"Unable to process file {remote_path}: Permission denied")
# We download from /sdcard/ to the local temporary file.
# If it doesn't work now, don't try again (retry_root=False)
self._adb_download(new_remote_path, local_path, retry_root=False)
# Delete the copy on /sdcard/.
self._adb_command(f"rm -rf {new_remote_path}")
except AdbCommandFailureException as e:
raise Exception(f"Unable to download file {remote_path}: {e}")
def _adb_process_file(self, remote_path, process_routine):
"""Download a local copy of a file which is only accessible as root.
This is a wrapper around process_routine.
:param remote_path: Path of the file on the device to process
:param process_routine: Function to be called on the local copy of the
downloaded file
"""
# Connect to the device over adb.
self._adb_connect()
@@ -161,6 +240,5 @@ class AndroidExtraction(MVTModule):
self._adb_disconnect()
def run(self):
"""Run the main procedure.
"""
"""Run the main procedure."""
raise NotImplementedError

View File

@@ -1,13 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import os
import sqlite3
import logging
from mvt.common.utils import convert_chrometime_to_unix, convert_timestamp_to_iso
from mvt.common.utils import (convert_chrometime_to_unix,
convert_timestamp_to_iso)
from .base import AndroidExtraction
@@ -15,11 +16,12 @@ log = logging.getLogger(__name__)
CHROME_HISTORY_PATH = "data/data/com.android.chrome/app_chrome/Default/History"
class ChromeHistory(AndroidExtraction):
"""This module extracts records from Android's Chrome browsing history."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
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)
@@ -32,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()
@@ -51,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()

View File

@@ -0,0 +1,53 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import io
import logging
import os
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysAccessibility(AndroidExtraction):
"""This module extracts stats on accessibility."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def run(self):
self._adb_connect()
stats = self._adb_command("dumpsys accessibility")
in_services = False
for line in stats.split("\n"):
if line.strip().startswith("installed services:"):
in_services = True
continue
if not in_services:
continue
if line.strip() == "}":
break
service = line.split(":")[1].strip()
log.info("Found installed accessibility service \"%s\"", service)
if self.output_folder:
acc_path = os.path.join(self.output_folder,
"dumpsys_accessibility.txt")
with io.open(acc_path, "w", encoding="utf-8") as handle:
handle.write(stats)
log.info("Records from dumpsys accessibility stored at %s",
acc_path)
self._adb_disconnect()

View File

@@ -1,20 +1,21 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import 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,
fast_mode=False, log=None, results=[]):
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)
@@ -30,7 +31,7 @@ class DumpsysBatterystats(AndroidExtraction):
handle.write(stats)
log.info("Records from dumpsys batterystats stored at %s",
stats_path)
stats_path)
history = self._adb_command("dumpsys batterystats --history")
if self.output_folder:

View File

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

View File

@@ -1,20 +1,21 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import logging
import os
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysPackages(AndroidExtraction):
"""This module extracts stats on installed packages."""
"""This module extracts details on installed packages."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
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)
@@ -23,6 +24,7 @@ class DumpsysPackages(AndroidExtraction):
self._adb_connect()
output = self._adb_command("dumpsys package")
if self.output_folder:
packages_path = os.path.join(self.output_folder,
"dumpsys_packages.txt")

View File

@@ -1,20 +1,21 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import logging
import os
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysProcstats(AndroidExtraction):
"""This module extracts stats on memory consumption by processes."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
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)

View File

@@ -0,0 +1,87 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from .base import AndroidExtraction
log = logging.getLogger(__name__)
ACTION_NEW_OUTGOING_SMS = "android.provider.Telephony.NEW_OUTGOING_SMS"
ACTION_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"
ACTION_DATA_SMS_RECEIVED = "android.intent.action.DATA_SMS_RECEIVED"
ACTION_PHONE_STATE = "android.intent.action.PHONE_STATE"
class DumpsysReceivers(AndroidExtraction):
"""This module extracts details on receivers for risky activities."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def run(self):
self._adb_connect()
output = self._adb_command("dumpsys package")
if not output:
return
activity = None
for line in output.split("\n"):
# Find activity block markers.
if line.strip().startswith(ACTION_NEW_OUTGOING_SMS):
activity = ACTION_NEW_OUTGOING_SMS
continue
elif line.strip().startswith(ACTION_SMS_RECEIVED):
activity = ACTION_SMS_RECEIVED
continue
elif line.strip().startswith(ACTION_PHONE_STATE):
activity = ACTION_PHONE_STATE
continue
elif line.strip().startswith(ACTION_DATA_SMS_RECEIVED):
activity = ACTION_DATA_SMS_RECEIVED
continue
# If we are not in an activity block yet, skip.
if not activity:
continue
# If we are in a block but the line does not start with 8 spaces
# it means the block ended a new one started, so we reset and
# continue.
if not line.startswith(" " * 8):
activity = None
continue
# If we got this far, we are processing receivers for the
# activities we are interested in.
receiver = line.strip().split(" ")[1]
package_name = receiver.split("/")[0]
if package_name == "com.google.android.gms":
continue
if activity == ACTION_NEW_OUTGOING_SMS:
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
receiver)
elif activity == ACTION_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
receiver)
elif activity == ACTION_DATA_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
receiver)
elif activity == ACTION_PHONE_STATE:
self.log.info("Found a receiver monitoring telephony state: \"%s\"",
receiver)
self.results.append({
"activity": activity,
"package_name": package_name,
"receiver": receiver,
})
self._adb_disconnect()

View File

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

View File

@@ -0,0 +1,48 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import os
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Logcat(AndroidExtraction):
"""This module extracts details on installed packages."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
serial=None, fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def run(self):
self._adb_connect()
# Get the current logcat.
output = self._adb_command("logcat -d")
# Get the locat prior to last reboot.
last_output = self._adb_command("logcat -L")
if self.output_folder:
logcat_path = os.path.join(self.output_folder,
"logcat.txt")
with open(logcat_path, "w") as handle:
handle.write(output)
log.info("Current logcat logs stored at %s",
logcat_path)
logcat_last_path = os.path.join(self.output_folder,
"logcat_last.txt")
with open(logcat_last_path, "w") as handle:
handle.write(last_output)
log.info("Logcat logs prior to last reboot stored at %s",
logcat_last_path)
self._adb_disconnect()

View File

@@ -1,21 +1,23 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import logging
import os
import pkg_resources
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Packages(AndroidExtraction):
"""This module extracts the list of installed packages."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
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)
@@ -40,19 +42,54 @@ class Packages(AndroidExtraction):
return records
def check_indicators(self):
if not self.indicators:
return
root_packages_path = os.path.join("..", "..", "data", "root_packages.txt")
root_packages_string = pkg_resources.resource_string(__name__, root_packages_path)
root_packages = root_packages_string.decode("utf-8").split("\n")
root_packages = [rp.strip() for rp in root_packages]
for root_package in root_packages:
root_package = root_package.strip()
if not root_package:
continue
if root_package in self.results:
for result in self.results:
if result["package_name"] in root_packages:
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
root_package)
self.detected.append(root_package)
result["package_name"])
self.detected.append(result)
if result["package_name"] in self.indicators.ioc_app_ids:
self.log.warning("Found a malicious package name: \"%s\"",
result["package_name"])
self.detected.append(result)
for file in result["files"]:
if file["sha256"] in self.indicators.ioc_files_sha256:
self.log.warning("Found a malicious APK: \"%s\" %s",
result["package_name"],
file["sha256"])
self.detected.append(result)
def _get_files_for_package(self, package_name):
output = self._adb_command(f"pm path {package_name}")
output = output.strip().replace("package:", "")
if not output:
return []
package_files = []
for file_path in output.split("\n"):
file_path = file_path.strip()
md5 = self._adb_command(f"md5sum {file_path}").split(" ")[0]
sha1 = self._adb_command(f"sha1sum {file_path}").split(" ")[0]
sha256 = self._adb_command(f"sha256sum {file_path}").split(" ")[0]
sha512 = self._adb_command(f"sha512sum {file_path}").split(" ")[0]
package_files.append({
"path": file_path,
"md5": md5,
"sha1": sha1,
"sha256": sha256,
"sha512": sha512,
})
return package_files
def run(self):
self._adb_connect()
@@ -65,28 +102,40 @@ class Packages(AndroidExtraction):
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
try:
uid = fields[2].split(":")[1].strip()
except IndexError:
uid = None
dumpsys = self._adb_command(f"dumpsys package {package_name} | grep -A2 timeStamp").split("\n")
timestamp = dumpsys[0].split("=")[1].strip()
first_install = dumpsys[1].split("=")[1].strip()
last_update = dumpsys[2].split("=")[1].strip()
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,
))
package_files = self._get_files_for_package(package_name)
self.results.append({
"package_name": package_name,
"file_name": file_name,
"installer": installer,
"timestamp": timestamp,
"first_install_time": first_install,
"last_update_time": last_update,
"uid": uid,
"disabled": False,
"system": False,
"third_party": False,
"files": package_files,
})
cmds = [
{"field": "disabled", "arg": "-d"},
@@ -105,6 +154,13 @@ class Packages(AndroidExtraction):
if result["package_name"] == package_name:
self.results[i][cmd["field"]] = True
for result in self.results:
if result["system"]:
continue
self.log.info("Found non-system package with name \"%s\" installed by \"%s\" on %s",
result["package_name"], result["installer"], result["timestamp"])
self.log.info("Extracted at total of %d installed package names",
len(self.results))

View File

@@ -1,7 +1,7 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# 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
@@ -9,11 +9,12 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Processes(AndroidExtraction):
"""This module extracts details on running processes."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
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)
@@ -21,7 +22,7 @@ class Processes(AndroidExtraction):
def run(self):
self._adb_connect()
output = self._adb_command("ps")
output = self._adb_command("ps -e")
for line in output.split("\n")[1:]:
line = line.strip()
@@ -29,13 +30,13 @@ class Processes(AndroidExtraction):
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,21 +1,23 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import logging
import os
import pkg_resources
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class RootBinaries(AndroidExtraction):
"""This module extracts the list of installed packages."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
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)

View File

@@ -1,24 +1,50 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import os
import sqlite3
import logging
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from .base import AndroidExtraction
from mvt.common.utils import convert_timestamp_to_iso, check_for_links
log = logging.getLogger(__name__)
SMS_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
SMS_BUGLE_QUERY = """
SELECT
ppl.normalized_destination AS number,
p.timestamp AS timestamp,
CASE WHEN m.sender_id IN
(SELECT _id FROM participants WHERE contact_id=-1)
THEN 2 ELSE 1 END incoming, p.text AS text
FROM messages m, conversations c, parts p,
participants ppl, conversation_participants cp
WHERE (m.conversation_id = c._id)
AND (m._id = p.message_id)
AND (cp.conversation_id = c._id)
AND (cp.participant_id = ppl._id);
"""
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
SMS_MMSMS_QUERY = """
SELECT
address AS number,
date_sent AS timestamp,
type as incoming,
body AS text
FROM sms;
"""
class SMS(AndroidExtraction):
"""This module extracts all SMS messages containing links."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
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)
@@ -37,7 +63,7 @@ class SMS(AndroidExtraction):
return
for message in self.results:
if not "text" in message:
if "text" not in message:
continue
message_links = check_for_links(message["text"])
@@ -46,28 +72,22 @@ class SMS(AndroidExtraction):
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()
cur.execute("""
SELECT
ppl.normalized_destination AS number,
p.timestamp AS timestamp,
CASE WHEN m.sender_id IN
(SELECT _id FROM participants WHERE contact_id=-1)
THEN 2 ELSE 1 END incoming, p.text AS text
FROM messages m, conversations c, parts p,
participants ppl, conversation_participants cp
WHERE (m.conversation_id = c._id)
AND (m._id = p.message_id)
AND (cp.conversation_id = c._id)
AND (cp.participant_id = ppl._id);
""")
if (self.SMS_DB_TYPE == 1):
cur.execute(SMS_BUGLE_QUERY)
elif (self.SMS_DB_TYPE == 2):
cur.execute(SMS_MMSMS_QUERY)
names = [description[0] for description in cur.description]
for item in cur:
message = dict()
message = {}
for index, value in enumerate(item):
message[names[index]] = value
@@ -85,7 +105,11 @@ class SMS(AndroidExtraction):
log.info("Extracted a total of %d SMS messages containing links", len(self.results))
def run(self):
try:
self._adb_process_file(os.path.join("/", SMS_PATH), self._parse_db)
except Exception as e:
self.log.error(e)
if (self._adb_check_file_exists(os.path.join("/", SMS_BUGLE_PATH))):
self.SMS_DB_TYPE = 1
self._adb_process_file(os.path.join("/", SMS_BUGLE_PATH), self._parse_db)
elif (self._adb_check_file_exists(os.path.join("/", SMS_MMSSMS_PATH))):
self.SMS_DB_TYPE = 2
self._adb_process_file(os.path.join("/", SMS_MMSSMS_PATH), self._parse_db)
else:
self.log.error("No SMS database found")

View File

@@ -1,24 +1,27 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# 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 base64
import logging
import os
import sqlite3
import logging
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from .base import AndroidExtraction
from mvt.common.utils import convert_timestamp_to_iso, check_for_links
log = logging.getLogger(__name__)
WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db"
class Whatsapp(AndroidExtraction):
"""This module extracts all WhatsApp messages containing links."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
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)
@@ -37,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"])
@@ -46,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()
@@ -57,7 +62,7 @@ class Whatsapp(AndroidExtraction):
messages = []
for item in cur:
message = dict()
message = {}
for index, value in enumerate(item):
message[names[index]] = value
@@ -69,6 +74,8 @@ class Whatsapp(AndroidExtraction):
# If we find links in the messages or if they are empty we add them to the list.
if check_for_links(message["data"]) or message["data"].strip() == "":
if (message.get('thumb_image') is not None):
message['thumb_image'] = base64.b64encode(message['thumb_image'])
messages.append(message)
cur.close()
@@ -78,7 +85,4 @@ class Whatsapp(AndroidExtraction):
self.results = messages
def run(self):
try:
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)
except Exception as e:
self.log.error(e)
self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db)

View File

@@ -1,8 +1,8 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .sms import SMS
BACKUP_MODULES = [SMS,]
BACKUP_MODULES = [SMS]

View File

@@ -1,15 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import json
import os
import zlib
from mvt.common.module import MVTModule
from mvt.common.utils import check_for_links
from mvt.common.utils import convert_timestamp_to_iso
class SMS(MVTModule):
@@ -24,7 +24,7 @@ class SMS(MVTModule):
return
for message in self.results:
if not "body" in message:
if "body" not in message:
continue
message_links = check_for_links(message["body"])

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# 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/

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

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

View File

@@ -1,63 +1,136 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import io
import json
import os
import requests
from appdirs import user_data_dir
from .url import URL
class Indicators:
"""This class is used to parse indicators from a STIX2 file and provide
functions to compare extracted artifacts to the indicators.
"""
def __init__(self, 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_files_sha256 = []
self.ioc_app_ids = []
self.ios_profile_ids = []
self.ioc_count = 0
def _parse_stix_file(self):
"""Extract IOCs of given type from STIX2 definitions.
def _add_indicator(self, ioc, iocs_list):
if ioc not in iocs_list:
iocs_list.append(ioc)
self.ioc_count += 1
def _load_downloaded_indicators(self):
if not os.path.isdir(self.data_dir):
return False
for f in os.listdir(self.data_dir):
if f.lower().endswith(".stix2"):
self.parse_stix2(os.path.join(self.data_dir, f))
def _check_stix2_env_variable(self):
"""
for entry in self.data["objects"]:
Checks if a variable MVT_STIX2 contains path to STIX Files
"""
if "MVT_STIX2" not in os.environ:
return False
paths = os.environ["MVT_STIX2"].split(":")
for path in paths:
if os.path.isfile(path):
self.parse_stix2(path)
else:
self.log.info("Invalid STIX2 path %s in MVT_STIX2 environment variable", path)
def load_indicators_files(self, files):
"""
Load a list of indicators files
"""
for file_path in files:
if os.path.isfile(file_path):
self.parse_stix2(file_path)
else:
self.log.warning("This indicators file %s does not exist", file_path)
# Load downloaded indicators and any indicators from env variable
self._load_downloaded_indicators()
self._check_stix2_env_variable()
self.log.info("Loaded a total of %d unique indicators", self.ioc_count)
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") as handle:
try:
if entry["type"] != "indicator":
continue
except KeyError:
data = json.load(handle)
except json.decoder.JSONDecodeError:
self.log.critical("Unable to parse STIX2 indicator file. The file is malformed or in the wrong format.")
return
for entry in data.get("objects", []):
if entry.get("type", "") != "indicator":
continue
key, value = entry["pattern"].strip("[]").split("=")
key, value = entry.get("pattern", "").strip("[]").split("=")
value = value.strip("'")
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)
self._add_indicator(ioc=value.lower(),
iocs_list=self.ioc_domains)
elif key == "process:name":
if value not in self.ioc_processes:
self.ioc_processes.append(value)
self._add_indicator(ioc=value,
iocs_list=self.ioc_processes)
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)
self._add_indicator(ioc=value.lower(),
iocs_list=self.ioc_emails)
elif key == "file:name":
if value not in self.ioc_files:
self.ioc_files.append(value)
self._add_indicator(ioc=value,
iocs_list=self.ioc_files)
elif key == "app:id":
self._add_indicator(ioc=value,
iocs_list=self.ioc_app_ids)
elif key == "configuration-profile:id":
self._add_indicator(ioc=value,
iocs_list=self.ios_profile_ids)
elif key == "file:hashes.sha256":
self._add_indicator(ioc=value,
iocs_list=self.ioc_files_sha256)
def check_domain(self, url):
def check_domain(self, url) -> bool:
"""Check if a given URL matches any of the provided domain indicators.
:param url: URL to match against domain indicators
:type url: str
:returns: True if the URL matched an indicator, otherwise False
:rtype: bool
"""
# TODO: If the IOC domain contains a subdomain, it is not currently
# being matched.
if not url:
return False
try:
# First we use the provided URL.
@@ -82,7 +155,7 @@ 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:
@@ -108,25 +181,42 @@ class Indicators:
# Then we just check the top level domain.
if final_url.top_level.lower() == ioc:
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",
self.log.warning("Found a sub-domain matching a known suspicious top level %s shortened as %s",
final_url.url, orig_url.url)
else:
self.log.warning("Found a sub-domain matching a suspicious top level: %s", final_url.url)
self.log.warning("Found a sub-domain matching a known suspicious top level: %s", final_url.url)
return True
def check_domains(self, urls):
"""Check the provided list of (suspicious) domains against a list of URLs.
:param urls: List of URLs to check
return False
def check_domains(self, urls) -> bool:
"""Check a list of URLs against the provided list of domain indicators.
:param urls: List of URLs to check against domain indicators
:type urls: list
:returns: True if any URL matched an indicator, otherwise False
:rtype: bool
"""
if not urls:
return False
for url in urls:
if self.check_domain(url):
return True
def check_process(self, process):
return False
def check_process(self, process) -> bool:
"""Check the provided process name against the list of process
indicators.
:param process: Process name to check
:param process: Process name to check against process indicators
:type process: str
:returns: True if process matched an indicator, otherwise False
:rtype: bool
"""
if not process:
return False
@@ -142,18 +232,35 @@ class Indicators:
self.log.warning("Found a truncated known suspicious process name \"%s\"", process)
return True
def check_processes(self, processes):
return False
def check_processes(self, processes) -> bool:
"""Check the provided list of processes against the list of
process indicators.
:param processes: List of processes to check
:param processes: List of processes to check against process indicators
:type processes: list
:returns: True if process matched an indicator, otherwise False
:rtype: bool
"""
if not processes:
return False
for process in processes:
if self.check_process(process):
return True
def check_email(self, email):
return False
def check_email(self, email) -> bool:
"""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: True if email address matched an indicator, otherwise False
:rtype: bool
"""
if not email:
return False
@@ -162,14 +269,92 @@ class Indicators:
self.log.warning("Found a known suspicious email address: \"%s\"", email)
return True
def check_file(self, file_path):
return False
def check_file_name(self, file_path) -> bool:
"""Check the provided file path against the list of file indicators.
:param file_path: Path or name of the file to check
:param file_path: File path or file name to check against file
indicators
:type file_path: str
:returns: True if the file path matched an indicator, otherwise False
:rtype: bool
"""
if not file_path:
return False
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
return False
# TODO: The difference between check_file_name() and check_file_path()
# needs to be more explicit and clear. Probably, the two should just
# be combined into one function.
def check_file_path(self, file_path) -> bool:
"""Check the provided file path against the list of file indicators.
:param file_path: File path or file name to check against file
indicators
:type file_path: str
:returns: True if the file path matched an indicator, otherwise False
:rtype: bool
"""
if not file_path:
return False
for ioc_file in self.ioc_files:
# Strip any trailing slash from indicator paths to match directories.
if file_path.startswith(ioc_file.rstrip("/")):
return True
return False
def check_profile(self, profile_uuid) -> bool:
"""Check the provided configuration profile UUID against the list of indicators.
:param profile_uuid: Profile UUID to check against configuration profile indicators
:type profile_uuid: str
:returns: True if the UUID in indicator list, otherwise False
:rtype: bool
"""
if profile_uuid in self.ios_profile_ids:
return True
return False
def download_indicators_files(log):
"""
Download indicators from repo into MVT app data directory
"""
data_dir = user_data_dir("mvt")
if not os.path.isdir(data_dir):
os.makedirs(data_dir, exist_ok=True)
# Download latest list of indicators from the MVT repo.
res = requests.get("https://github.com/mvt-project/mvt/raw/main/public_indicators.json")
if res.status_code != 200:
log.warning("Unable to find retrieve list of indicators from the MVT repository.")
return
for ioc_entry in res.json():
ioc_url = ioc_entry["stix2_url"]
log.info("Downloading indicator file '%s' from '%s'", ioc_entry["name"], ioc_url)
res = requests.get(ioc_url)
if res.status_code != 200:
log.warning("Could not find indicator file '%s'", ioc_url)
continue
clean_file_name = ioc_url.lstrip("https://").replace("/", "_")
ioc_path = os.path.join(data_dir, clean_file_name)
# Write file to disk. This will overwrite any older version of the STIX2 file.
with io.open(ioc_path, "w") as f:
f.write(res.text)
log.info("Saved indicator file to '%s'", os.path.basename(ioc_path))

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

@@ -0,0 +1,25 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from rich import print
from .version import MVT_VERSION, check_for_updates
def logo():
print("\n")
print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
print("\t\thttps://mvt.re")
print(f"\t\tVersion: {MVT_VERSION}")
try:
latest_version = check_for_updates()
except Exception:
pass
else:
if latest_version:
print(f"\t\t[bold]Version {latest_version} is available! Upgrade mvt![/bold]")
print("\n")

View File

@@ -1,16 +1,27 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import csv
import io
import os
import re
import csv
import glob
import logging
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."""
@@ -21,12 +32,18 @@ class MVTModule(object):
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
"""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
@@ -49,30 +66,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 againt a provided list of
indicators."""
"""Check the results of this module against a provided list of
indicators.
"""
raise NotImplementedError
def save_to_json(self):
"""Save the collected results to a json file.
"""
"""Save the collected results to a json file."""
if not self.output_folder:
return
@@ -81,49 +91,58 @@ 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:
json.dump(self.results, handle, indent=4)
with io.open(results_json_path, "w", encoding="utf-8") as handle:
try:
json.dump(self.results, handle, indent=4, default=str)
except Exception as e:
self.log.error("Unable to store results of module %s to file %s: %s",
self.__class__.__name__, results_file_name, e)
if self.detected:
detected_file_name = f"{name}_detected.json"
detected_json_path = os.path.join(self.output_folder, detected_file_name)
with open(detected_json_path, "w") as handle:
json.dump(self.detected, handle, indent=4)
with io.open(detected_json_path, "w", encoding="utf-8") as handle:
json.dump(self.detected, handle, indent=4, default=str)
def serialize(self, record):
raise NotImplementedError
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
"""
for result in self.results:
record = self.serialize(result)
if type(record) == list:
self.timeline.extend(record)
else:
self.timeline.append(record)
for detected in self.detected:
record = self.serialize(detected)
if type(record) == list:
self.timeline_detected.extend(record)
else:
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]
def to_timeline(self):
"""Convert results into a timeline."""
for result in self.results:
record = self.serialize(result)
if record:
if type(record) == list:
self.timeline.extend(record)
else:
self.timeline.append(record)
for detected in self.detected:
record = self.serialize(detected)
if record:
if type(record) == list:
self.timeline_detected.extend(record)
else:
self.timeline_detected.append(record)
# De-duplicate timeline entries.
self.timeline = self._deduplicate_timeline(self.timeline)
self.timeline_detected = self._deduplicate_timeline(self.timeline_detected)
def run(self):
"""Run the main module procedure.
"""
"""Run the main module procedure."""
raise NotImplementedError
@@ -135,8 +154,13 @@ def run_module(module):
except NotImplementedError:
module.log.exception("The run() procedure of module %s was not implemented yet!",
module.__class__.__name__)
except FileNotFoundError as e:
module.log.error("There might be no data to extract by module %s: %s",
except InsufficientPrivileges as e:
module.log.info("Insufficient privileges for module %s: %s", module.__class__.__name__, e)
except DatabaseNotFoundError as e:
module.log.info("There might be no data to extract by module %s: %s",
module.__class__.__name__, e)
except DatabaseCorruptedError as e:
module.log.error("The %s module database seems to be corrupted: %s",
module.__class__.__name__, e)
except Exception as e:
module.log.exception("Error in running extraction from module %s: %s",
@@ -145,7 +169,13 @@ def run_module(module):
try:
module.check_indicators()
except NotImplementedError:
module.log.info("The %s module does not support checking for indicators",
module.__class__.__name__)
pass
else:
if module.indicators and not module.detected:
module.log.info("The %s module produced no detections!",
module.__class__.__name__)
try:
module.to_timeline()
@@ -157,16 +187,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 open(timeline_path, "a+") as handle:
with io.open(timeline_path, "a+", encoding="utf-8") as handle:
csvoutput = csv.writer(handle, delimiter=",", quotechar="\"")
csvoutput.writerow(["UTC Timestamp", "Plugin", "Event", "Description"])
for event in sorted(timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""):
csvoutput.writerow([
event["timestamp"],
event["module"],
event["event"],
event["data"],
event.get("timestamp"),
event.get("module"),
event.get("event"),
event.get("data"),
])

View File

@@ -1,15 +1,15 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
# From: https://gist.github.com/stanchan/bce1c2d030c76fe9223b5ff6ad0f03db
from click import command, option, Option, UsageError
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", []))

View File

@@ -1,12 +1,13 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import requests
from 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,17 +1,22 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import re
import datetime
import hashlib
import re
def convert_mactime_to_unix(timestamp, from_2001=True):
"""Converts Mac Standard Time to a Unix timestamp.
:param timestamp: MacTime timestamp (either int or float)
: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
@@ -34,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)
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:
@@ -71,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):

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

@@ -0,0 +1,20 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import requests
from packaging import version
MVT_VERSION = "1.4.3"
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,6 +1,6 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .cli import cli

View File

@@ -1,21 +1,27 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import sys
import click
import tarfile
import logging
from rich.logging import RichHandler
import os
import click
from rich.logging import RichHandler
from rich.prompt import Prompt
from mvt.common.help import (HELP_MSG_FAST, HELP_MSG_IOC,
HELP_MSG_LIST_MODULES, HELP_MSG_MODULE,
HELP_MSG_OUTPUT)
from mvt.common.indicators import Indicators, download_indicators_files
from mvt.common.logo import logo
from mvt.common.module import run_module, save_timeline
from mvt.common.options import MutuallyExclusiveOption
from mvt.common.indicators import Indicators
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"
@@ -23,8 +29,8 @@ 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"
#==============================================================================
@@ -32,6 +38,14 @@ OUTPUT_HELP_MESSAGE = "Specify a path to a folder where you want to store JSON r
#==============================================================================
@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
@@ -42,62 +56,121 @@ def cli():
@click.option("--destination", "-d", required=True,
help="Path to the folder where to store the decrypted backup")
@click.option("--password", "-p", cls=MutuallyExclusiveOption,
help="Password to use to decrypt the backup",
help=f"Password to use to decrypt the backup (or, set {PASSWD_ENV} environment variable)",
mutually_exclusive=["key_file"])
@click.option("--key-file", "-k", cls=MutuallyExclusiveOption,
type=click.Path(exists=True),
help="File containing raw encryption key to use to decrypt the backup",
mutually_exclusive=["password"])
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
def decrypt_backup(destination, password, key_file, backup_path):
@click.pass_context
def decrypt_backup(ctx, destination, password, key_file, backup_path):
backup = DecryptBackup(backup_path, destination)
if password:
backup.decrypt_with_password(password)
elif key_file:
if key_file:
if PASSWD_ENV in os.environ:
log.info("Ignoring environment variable, using --key-file '%s' instead",
PASSWD_ENV, key_file)
backup.decrypt_with_key_file(key_file)
elif password:
log.info("Your password may be visible in the process table because it was supplied on the command line!")
if PASSWD_ENV in os.environ:
log.info("Ignoring %s environment variable, using --password argument instead",
PASSWD_ENV)
backup.decrypt_with_password(password)
elif PASSWD_ENV in os.environ:
log.info("Using password from %s environment variable", PASSWD_ENV)
backup.decrypt_with_password(os.environ[PASSWD_ENV])
else:
raise click.ClickException("Missing required option. Specify either "
"--password or --key-file.")
sekrit = Prompt.ask("Enter backup password", password=True)
backup.decrypt_with_password(sekrit)
if not backup.can_process():
ctx.exit(1)
backup.process_backup()
#==============================================================================
# Command: extract-key
#==============================================================================
@cli.command("extract-key", help="Extract decryption key from an iTunes backup")
@click.option("--password", "-p",
help=f"Password to use to decrypt the backup (or, set {PASSWD_ENV} environment variable)")
@click.option("--key-file", "-k",
help="Key file to be written (if unset, will print to STDOUT)",
required=False,
type=click.Path(exists=False, file_okay=True, dir_okay=False, writable=True))
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
def extract_key(password, backup_path, key_file):
backup = DecryptBackup(backup_path)
if password:
log.info("Your password may be visible in the process table because it was supplied on the command line!")
if PASSWD_ENV in os.environ:
log.info("Ignoring %s environment variable, using --password argument instead",
PASSWD_ENV)
elif PASSWD_ENV in os.environ:
log.info("Using password from %s environment variable", PASSWD_ENV)
password = os.environ[PASSWD_ENV]
else:
password = Prompt.ask("Enter backup password", password=True)
backup.decrypt_with_password(password)
backup.get_key()
if key_file:
backup.write_key(key_file)
#==============================================================================
# Command: check-backup
#==============================================================================
@cli.command("check-backup", help="Extract artifacts from an iTunes backup")
@click.option("--iocs", "-i", type=click.Path(exists=True), help="Path to indicators file")
@click.option("--output", "-o", type=click.Path(exists=True), 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))
def check_backup(iocs, output, fast, backup_path, list_modules, module):
@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
log.info("Checking iTunes backup located at: %s", backup_path)
if iocs:
# Pre-load indicators for performance reasons.
log.info("Loading indicators from provided file at: %s", iocs)
indicators = Indicators(iocs)
if output and not os.path.exists(output):
try:
os.makedirs(output)
except Exception as e:
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
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.ioc_count:
m.indicators = indicators
m.indicators.log = m.log
run_module(m)
timeline.extend(m.timeline)
@@ -114,30 +187,37 @@ def check_backup(iocs, output, fast, backup_path, list_modules, module):
# 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=True), 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))
def check_fs(iocs, output, fast, dump_path, list_modules, module):
@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
log.info("Checking filesystem dump located at: %s", dump_path)
if iocs:
# Pre-load indicators for performance reasons.
log.info("Loading indicators from provided file at: %s", iocs)
indicators = Indicators(iocs)
if output and not os.path.exists(output):
try:
os.makedirs(output)
except Exception as e:
log.critical("Unable to create output folder %s: %s", output, e)
ctx.exit(1)
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
@@ -145,10 +225,9 @@ def check_fs(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.ioc_count:
m.indicators = indicators
m.indicators.log = m.log
run_module(m)
timeline.extend(m.timeline)
@@ -165,14 +244,15 @@ def check_fs(iocs, output, fast, dump_path, list_modules, module):
# 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=[], required=True, 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 + SYSDIAGNOSE_MODULES:
for entry in BACKUP_MODULES + FS_MODULES:
if entry not in all_modules:
all_modules.append(entry)
@@ -185,9 +265,8 @@ 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)
for file_name in os.listdir(folder):
name_only, ext = os.path.splitext(file_name)
@@ -205,11 +284,19 @@ 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.ioc_count:
m.indicators = indicators
m.indicators.log = m.log
try:
m.check_indicators()
except NotImplementedError:
continue
#==============================================================================
# Command: download-iocs
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
def download_iocs():
download_indicators_files(log)

View File

@@ -1,23 +1,28 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# 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 binascii
import glob
import logging
import os
import shutil
import sqlite3
import logging
import binascii
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):
def __init__(self, backup_path, dest_path=None):
"""Decrypts an encrypted iOS backup.
:param backup_path: Path to the encrypted backup folder
:param dest_path: Path to the folder where to store the decrypted backup
@@ -25,8 +30,32 @@ class DecryptBackup:
self.backup_path = 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_backup(self):
if not os.path.exists(self.dest_path):
os.makedirs(self.dest_path)
def _process_backup(self):
manifest_path = os.path.join(self.dest_path, "Manifest.db")
# We extract a decrypted Manifest.db.
self._backup.getManifestDB()
@@ -62,35 +91,63 @@ class DecryptBackup:
except Exception as e:
log.error("Failed to decrypt file %s: %s", relative_path, e)
# 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)
if not os.path.exists(self.dest_path):
os.makedirs(self.dest_path)
if not os.path.exists(os.path.join(self.backup_path, "Manifest.plist")):
possible = glob.glob(os.path.join(self.backup_path, "*", "Manifest.plist"))
if len(possible) == 1:
newpath = os.path.dirname(possible[0])
log.warning("No Manifest.plist in %s, using %s instead.",
self.backup_path, newpath)
self.backup_path = newpath
elif len(possible) > 1:
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,
backuproot=os.path.dirname(self.backup_path))
except Exception as e:
log.exception(e)
log.critical("Failed to decrypt backup. Did you provide the correct password?")
return
else:
self._process_backup()
if isinstance(e, KeyError) and len(e.args) > 0 and e.args[0] == b"KEY":
log.critical("Failed to decrypt backup. Password is probably wrong.")
elif isinstance(e, FileNotFoundError) and os.path.basename(e.filename) == "Manifest.plist":
log.critical("Failed to find a valid backup at %s. Did you point to the right backup path?",
self.backup_path)
else:
log.exception(e)
log.critical("Failed to decrypt backup. Did you provide the correct password? Did you point to the right backup path?")
def decrypt_with_key_file(self, key_file):
"""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)
if not os.path.exists(self.dest_path):
os.makedirs(self.dest_path)
# 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()
@@ -108,6 +165,32 @@ class DecryptBackup:
except Exception as e:
log.exception(e)
log.critical("Failed to decrypt backup. Did you provide the correct key file?")
def get_key(self):
"""Retrieve and prints the encryption key."""
if not self._backup:
return
self._decryption_key = self._backup.getDecryptionKey()
log.info("Derived decryption key for backup at path %s is: \"%s\"",
self.backup_path, self._decryption_key)
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:
handle.write(self._decryption_key)
except Exception as e:
log.exception(e)
log.critical("Failed to write key to file: %s", key_path)
return
else:
self._process_backup()
log.info("Wrote decryption key to file: %s. This file is equivalent to a plaintext password. Keep it safe!",
key_path)

View File

@@ -1,4 +1,4 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/

View File

@@ -0,0 +1,11 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .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,43 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import plistlib
from mvt.common.module import DatabaseNotFoundError
from ..base import IOSExtraction
class BackupInfo(IOSExtraction):
"""This module extracts information about the device and the backup."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
self.results = {}
def run(self):
info_path = os.path.join(self.base_folder, "Info.plist")
if not os.path.exists(info_path):
raise DatabaseNotFoundError("No Info.plist at backup path, unable to extract device information")
with open(info_path, "rb") as handle:
info = plistlib.load(handle)
fields = ["Build Version", "Device Name", "Display Name",
"GUID", "ICCID", "IMEI", "MEID", "Installed Applications",
"Last Backup Date", "Phone Number", "Product Name",
"Product Type", "Product Version", "Serial Number",
"Target Identifier", "Target Type", "Unique Identifier",
"iTunes Version"]
for field in fields:
value = info.get(field, None)
self.log.info("%s: %s", field, value)
self.results[field] = value

View File

@@ -0,0 +1,96 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import plistlib
from base64 import b64encode
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
CONF_PROFILES_DOMAIN = "SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
class ConfigurationProfiles(IOSExtraction):
"""This module extracts the full plist data from configuration profiles."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
if not record["install_date"]:
return
payload_name = record['plist'].get('PayloadDisplayName')
payload_description = record['plist'].get('PayloadDescription')
return {
"timestamp": record["install_date"],
"module": self.__class__.__name__,
"event": "configuration_profile_install",
"data": f"{record['plist']['PayloadType']} installed: {record['plist']['PayloadUUID']} - {payload_name}: {payload_description}"
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if result["plist"].get("PayloadUUID"):
payload_content = result["plist"]["PayloadContent"][0]
# Alert on any known malicious configuration profiles in the indicator list.
if self.indicators.check_profile(result["plist"]["PayloadUUID"]):
self.log.warning(f"Found a known malicious configuration profile \"{result['plist']['PayloadDisplayName']}\" with UUID '{result['plist']['PayloadUUID']}'.")
self.detected.append(result)
continue
# Highlight suspicious configuration profiles which may be used to hide notifications.
if payload_content["PayloadType"] in ["com.apple.notificationsettings"]:
self.log.warning(f"Found a potentially suspicious configuration profile \"{result['plist']['PayloadDisplayName']}\" with payload type '{payload_content['PayloadType']}'.")
self.detected.append(result)
continue
def run(self):
for conf_file in self._get_backup_files_from_manifest(domain=CONF_PROFILES_DOMAIN):
conf_rel_path = conf_file["relative_path"]
# Filter out all configuration files that are not configuration profiles.
if not conf_rel_path or not os.path.basename(conf_rel_path).startswith("profile-"):
continue
conf_file_path = self._get_backup_file_from_id(conf_file["file_id"])
if not conf_file_path:
continue
with open(conf_file_path, "rb") as handle:
try:
conf_plist = plistlib.load(handle)
except Exception:
conf_plist = {}
if "SignerCerts" in conf_plist:
conf_plist["SignerCerts"] = [b64encode(x) for x in conf_plist["SignerCerts"]]
if "PushTokenDataSentToServerKey" in conf_plist:
conf_plist["PushTokenDataSentToServerKey"] = b64encode(conf_plist["PushTokenDataSentToServerKey"])
if "LastPushTokenHash" in conf_plist:
conf_plist["LastPushTokenHash"] = b64encode(conf_plist["LastPushTokenHash"])
if "PayloadContent" in conf_plist:
for x in range(len(conf_plist["PayloadContent"])):
if "PERSISTENT_REF" in conf_plist["PayloadContent"][x]:
conf_plist["PayloadContent"][x]["PERSISTENT_REF"] = b64encode(conf_plist["PayloadContent"][x]["PERSISTENT_REF"])
self.results.append({
"file_id": conf_file["file_id"],
"relative_path": conf_file["relative_path"],
"domain": conf_file["domain"],
"plist": conf_plist,
"install_date": convert_timestamp_to_iso(conf_plist.get("InstallDate")),
})
self.log.info("Extracted details about %d configuration profiles", len(self.results))

View File

@@ -1,17 +1,19 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import datetime
import io
import os
import biplist
import plistlib
import sqlite3
import datetime
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):
"""This module extracts information from a backup Manifest.db file."""
@@ -23,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:
@@ -40,18 +49,20 @@ class Manifest(IOSExtraction):
def serialize(self, record):
records = []
for ts in set([record["created"], record["modified"], record["statusChanged"]]):
if "modified" not in record or "status_changed" not in record:
return
for ts in set([record["created"], record["modified"], record["status_changed"]]):
macb = ""
macb += "M" if ts == record["modified"] else "-"
macb += "-"
macb += "C" if ts == record["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
@@ -61,20 +72,23 @@ class Manifest(IOSExtraction):
return
for result in self.results:
if not "relativePath" in result:
if "relative_path" not in result:
continue
if not result["relative_path"]:
continue
if os.path.basename(result["relativePath"]) == "com.apple.CrashReporter.plist" and result["domain"] == "RootDomain":
self.log.warning("Found a potentially suspicious \"com.apple.CrashReporter.plist\" file created in RootDomain")
if result["domain"]:
if os.path.basename(result["relative_path"]) == "com.apple.CrashReporter.plist" and result["domain"] == "RootDomain":
self.log.warning("Found a potentially suspicious \"com.apple.CrashReporter.plist\" file created in RootDomain")
self.detected.append(result)
continue
if self.indicators.check_file_name(result["relative_path"]):
self.log.warning("Found a known malicious file at path: %s", result["relative_path"])
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"])
self.detected.append(result)
continue
relPath = result["relativePath"].lower()
relPath = result["relative_path"].lower()
for ioc in self.indicators.ioc_domains:
if ioc.lower() in relPath:
self.log.warning("Found mention of domain \"%s\" in a backup file with path: %s",
@@ -84,7 +98,7 @@ class Manifest(IOSExtraction):
def run(self):
manifest_db_path = os.path.join(self.base_folder, "Manifest.db")
if not os.path.isfile(manifest_db_path):
raise FileNotFoundError("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)
@@ -95,32 +109,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 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import plistlib
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
CONF_PROFILES_EVENTS_RELPATH = "Library/ConfigurationProfiles/MCProfileEvents.plist"
class ProfileEvents(IOSExtraction):
"""This module extracts events related to the installation of configuration
profiles.
"""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record.get("timestamp"),
"module": self.__class__.__name__,
"event": "profile_operation",
"data": f"Process {record.get('process')} started operation {record.get('operation')} of profile {record.get('profile_id')}"
}
def run(self):
for events_file in self._get_backup_files_from_manifest(relative_path=CONF_PROFILES_EVENTS_RELPATH):
events_file_path = self._get_backup_file_from_id(events_file["file_id"])
if not events_file_path:
continue
with open(events_file_path, "rb") as handle:
events_plist = plistlib.load(handle)
if "ProfileEvents" not in events_plist:
continue
for event in events_plist["ProfileEvents"]:
key = list(event.keys())[0]
self.log.info("On %s process \"%s\" started operation \"%s\" of profile \"%s\"",
event[key].get("timestamp"), event[key].get("process"),
event[key].get("operation"), key)
self.results.append({
"profile_id": key,
"timestamp": convert_timestamp_to_iso(event[key].get("timestamp")),
"operation": event[key].get("operation"),
"process": event[key].get("process"),
})
self.log.info("Extracted %d profile events", len(self.results))

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

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

View File

@@ -1,42 +1,19 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .manifest import Manifest
from .contacts import Contacts
from .analytics import Analytics
from .cache_files import CacheFiles
from .filesystem import Filesystem
from .net_netusage import Netusage
from .net_datausage import Datausage
from .safari_history import SafariHistory
from .safari_favicon import SafariFavicon
from .safari_browserstate import SafariBrowserState
from .shutdownlog import ShutdownLog
from .version_history import IOSVersionHistory
from .webkit_indexeddb import WebkitIndexedDB
from .webkit_localstorage import WebkitLocalStorage
from .webkit_safariviewservice import WebkitSafariViewService
from .webkit_session_resource_log import WebkitSessionResourceLog
from .chrome_history import ChromeHistory
from .chrome_favicon import ChromeFavicon
from .firefox_history import FirefoxHistory
from .firefox_favicon import FirefoxFavicon
from .version_history import IOSVersionHistory
from .idstatuscache import IDStatusCache
from .locationd import LocationdClients
from .interactionc import InteractionC
from .sms import SMS
from .sms_attachments import SMSAttachments
from .calls import Calls
from .whatsapp import Whatsapp
from .cache_files import CacheFiles
from .filesystem import Filesystem
BACKUP_MODULES = [SafariBrowserState, SafariHistory, Datausage, SMS, SMSAttachments,
ChromeHistory, ChromeFavicon, WebkitSessionResourceLog,
Calls, IDStatusCache, LocationdClients, InteractionC,
FirefoxHistory, FirefoxFavicon, Contacts, Manifest, Whatsapp]
FS_MODULES = [IOSVersionHistory, SafariHistory, SafariFavicon, SafariBrowserState,
WebkitIndexedDB, WebkitLocalStorage, WebkitSafariViewService,
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,119 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import plistlib
import sqlite3
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
ANALYTICS_DB_PATH = [
"private/var/Keychains/Analytics/*.db",
]
class Analytics(IOSExtraction):
"""This module extracts information from the private/var/Keychains/Analytics/*.db files."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["timestamp"],
"module": self.__class__.__name__,
"event": record["artifact"],
"data": f"{record}",
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
for ioc in self.indicators.ioc_processes:
for key in result.keys():
if ioc == result[key]:
self.log.warning("Found mention of a malicious process \"%s\" in %s file at %s",
ioc, result["artifact"], result["timestamp"])
self.detected.append(result)
break
for ioc in self.indicators.ioc_domains:
for key in result.keys():
if ioc in str(result[key]):
self.log.warning("Found mention of a malicious domain \"%s\" in %s file at %s",
ioc, result["artifact"], result["timestamp"])
self.detected.append(result)
break
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,55 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
import os
import glob
from mvt.common.module import MVTModule
class IOSExtraction(MVTModule):
"""This class provides a base for all iOS filesystem/backup extraction modules."""
is_backup = False
is_fs_dump = False
is_sysdiagnose = False
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).
"""
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 FileNotFoundError("Unable to find the module's database file")

View File

@@ -1,12 +1,13 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import sqlite3
from .base import IOSExtraction
from ..base import IOSExtraction
class CacheFiles(IOSExtraction):
@@ -37,7 +38,7 @@ class CacheFiles(IOSExtraction):
for item in items:
if self.indicators.check_domain(item["url"]):
if key not in self.detected:
self.detected[key] = [item,]
self.detected[key] = [item, ]
else:
self.detected[key].append(item)
@@ -53,18 +54,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,18 +1,22 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import datetime
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=[]):
@@ -24,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):
@@ -33,19 +37,46 @@ class Filesystem(IOSExtraction):
return
for result in self.results:
if self.indicators.check_file(result["file_path"]):
if self.indicators.check_file(result["path"]):
self.log.warning("Found a known malicious file name at path: %s", result["path"])
self.detected.append(result)
if self.indicators.check_file_path(result["path"]):
self.log.warning("Found a known malicious file path at path: %s", result["path"])
self.detected.append(result)
# If we are instructed to run fast, we skip this.
if self.fast_mode:
self.log.info("Flag --fast was enabled: skipping extended search for suspicious files/processes")
else:
for ioc in self.indicators.ioc_processes:
parts = result["path"].split("/")
if ioc in parts:
self.log.warning("Found a known malicious file/process at path: %s", result["path"])
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,69 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
import os
import glob
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": entry[ts],
"module": self.__class__.__name__,
"event": ts,
"data": f"{ts} from {entry['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[date]))
self.results.append(result)
self.log.info("Extracted a total of %d Locationd Clients entries", len(self.results))

View File

@@ -1,18 +1,24 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .net_base import NetBase
import sqlite3
from ..net_base import NetBase
NETUSAGE_ROOT_PATHS = [
"private/var/networkd/netusage.sqlite",
"private/var/networkd/db/netusage.sqlite"
]
class Netusage(NetBase):
"""This class extracts data from netusage.sqlite and attempts to identify
any suspicious processes if running on a full filesystem dump."""
any suspicious processes if running on a full filesystem dump.
"""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
@@ -21,8 +27,13 @@ class Netusage(NetBase):
log=log, results=results)
def run(self):
self._find_ios_database(root_paths=NETUSAGE_ROOT_PATHS)
self.log.info("Found NetUsage database at path: %s", self.file_path)
for netusage_path in self._get_fs_files_from_patterns(NETUSAGE_ROOT_PATHS):
self.file_path = netusage_path
self.log.info("Found NetUsage database at path: %s", self.file_path)
try:
self._extract_net_data()
except sqlite3.OperationalError as e:
self.log.info("Skipping this NetUsage database because it seems empty or malformed: %s", e)
continue
self._extract_net_data()
self._find_suspicious_processes()

View File

@@ -1,103 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
import io
import biplist
import sqlite3
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from mvt.common.utils import keys_bytes_to_string
from .base import IOSExtraction
SAFARI_BROWSER_STATE_BACKUP_IDS = [
"3a47b0981ed7c10f3e2800aa66bac96a3b5db28e",
]
SAFARI_BROWSER_STATE_ROOT_PATHS = [
"private/var/mobile/Library/Safari/BrowserState.db",
"private/var/mobile/Containers/Data/Application/*/Library/Safari/BrowserState.db",
]
class SafariBrowserState(IOSExtraction):
"""This module extracts all Safari browser state records."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["last_viewed_timestamp"],
"module": self.__class__.__name__,
"event": "tab",
"data": f"{record['tab_title']} - {record['tab_url']}"
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if "tab_url" in result and self.indicators.check_domain(result["tab_url"]):
self.detected.append(result)
continue
if not "session_data" in result:
continue
for session_entry in result["session_data"]:
if "entry_url" in session_entry and self.indicators.check_domain(session_entry["entry_url"]):
self.detected.append(result)
def run(self):
self._find_ios_database(backup_ids=SAFARI_BROWSER_STATE_BACKUP_IDS,
root_paths=SAFARI_BROWSER_STATE_ROOT_PATHS)
self.log.info("Found Safari browser state database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
# Fetch valid icon cache.
cur = conn.cursor()
cur.execute("""SELECT
tabs.title,
tabs.url,
tabs.user_visible_url,
tabs.last_viewed_time,
tab_sessions.session_data
FROM tabs
JOIN tab_sessions ON tabs.uuid = tab_sessions.tab_uuid
ORDER BY tabs.last_viewed_time;""")
session_history_count = 0
for item in cur:
session_entries = []
if item[4]:
# Skip a 4 byte header before the plist content.
session_plist = item[4][4:]
session_data = biplist.readPlist(io.BytesIO(session_plist))
session_data = keys_bytes_to_string(session_data)
if "SessionHistoryEntries" in session_data["SessionHistory"]:
for session_entry in session_data["SessionHistory"]["SessionHistoryEntries"]:
session_history_count += 1
session_entries.append(dict(
entry_title=session_entry["SessionHistoryEntryOriginalURL"],
entry_url=session_entry["SessionHistoryEntryURL"],
data_length=len(session_entry["SessionHistoryEntryData"]) if "SessionHistoryEntryData" in session_entry else 0,
))
self.results.append(dict(
tab_title=item[0],
tab_url=item[1],
tab_visible_url=item[2],
last_viewed_timestamp=convert_timestamp_to_iso(convert_mactime_to_unix(item[3])),
session_data=session_entries,
))
self.log.info("Extracted a total of %d tab records and %d session history entries",
len(self.results), session_history_count)

View File

@@ -1,19 +1,20 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import sqlite3
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from .base import IOSExtraction
from ..base import IOSExtraction
SAFARI_FAVICON_ROOT_PATHS = [
"private/var/mobile/Library/Image Cache/Favicons/Favicons.db",
"private/var/mobile/Containers/Data/Application/*/Library/Image Cache/Favicons/Favicons.db",
]
class SafariFavicon(IOSExtraction):
"""This module extracts all Safari favicon records."""
@@ -39,50 +40,57 @@ class SafariFavicon(IOSExtraction):
if self.indicators.check_domain(result["url"]) or self.indicators.check_domain(result["icon_url"]):
self.detected.append(result)
def run(self):
self._find_ios_database(root_paths=SAFARI_FAVICON_ROOT_PATHS)
self.log.info("Found Safari favicon cache database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
def _process_favicon_db(self, file_path):
conn = sqlite3.connect(file_path)
# Fetch valid icon cache.
cur = conn.cursor()
cur.execute("""SELECT
cur.execute("""
SELECT
page_url.url,
icon_info.url,
icon_info.timestamp
FROM page_url
JOIN icon_info ON page_url.uuid = icon_info.uuid
ORDER BY icon_info.timestamp;""")
ORDER BY icon_info.timestamp;
""")
items = []
for item in cur:
items.append(dict(
url=item[0],
icon_url=item[1],
timestamp=item[2],
isodate=convert_timestamp_to_iso(convert_mactime_to_unix(item[2])),
type="valid",
))
for row in cur:
self.results.append({
"url": row[0],
"icon_url": row[1],
"timestamp": row[2],
"isodate": convert_timestamp_to_iso(convert_mactime_to_unix(row[2])),
"type": "valid",
"safari_favicon_db_path": file_path,
})
# Fetch icons from the rejected icons table.
cur.execute("""SELECT
cur.execute("""
SELECT
page_url,
icon_url,
timestamp
FROM rejected_resources ORDER BY timestamp;""")
FROM rejected_resources ORDER BY timestamp;
""")
for item in cur:
items.append(dict(
url=item[0],
icon_url=item[1],
timestamp=item[2],
isodate=convert_timestamp_to_iso(convert_mactime_to_unix(item[2])),
type="rejected",
))
for row in cur:
self.results.append({
"url": row[0],
"icon_url": row[1],
"timestamp": row[2],
"isodate": convert_timestamp_to_iso(convert_mactime_to_unix(row[2])),
"type": "rejected",
"safari_favicon_db_path": file_path,
})
cur.close()
conn.close()
self.log.info("Extracted a total of %d favicon records", len(items))
self.results = sorted(items, key=lambda item: item["isodate"])
def run(self):
for file_path in self._get_fs_files_from_patterns(SAFARI_FAVICON_ROOT_PATHS):
self.log.info("Found Safari favicon cache database at path: %s", file_path)
self._process_favicon_db(file_path)
self.log.info("Extracted a total of %d favicon records", len(self.results))
self.results = sorted(self.results, key=lambda x: x["isodate"])

View File

@@ -0,0 +1,82 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
SHUTDOWN_LOG_PATH = [
"private/var/db/diagnostics/shutdown.log",
]
class ShutdownLog(IOSExtraction):
"""This module extracts processes information from the shutdown log file."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "shutdown",
"data": f"Client {record['client']} with PID {record['pid']} was running when the device was shut down",
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
for ioc in self.indicators.ioc_processes:
parts = result["client"].split("/")
if ioc in parts:
self.log.warning("Found mention of a known malicious process \"%s\" in shutdown.log",
ioc)
self.detected.append(result)
def process_shutdownlog(self, content):
current_processes = []
for line in content.split("\n"):
line = line.strip()
if line.startswith("remaining client pid:"):
current_processes.append({
"pid": line[line.find("pid: ")+5:line.find(" (")],
"client": line[line.find("(")+1:line.find(")")],
})
elif line.startswith("SIGTERM: "):
try:
mac_timestamp = int(line[line.find("[")+1:line.find("]")])
except ValueError:
try:
start = line.find(" @")+2
mac_timestamp = int(line[start:start+10])
except Exception:
mac_timestamp = 0
timestamp = convert_mactime_to_unix(mac_timestamp, from_2001=False)
isodate = convert_timestamp_to_iso(timestamp)
for current_process in current_processes:
self.results.append({
"isodate": isodate,
"pid": current_process["pid"],
"client": current_process["client"],
})
current_processes = []
self.results = sorted(self.results, key=lambda entry: entry["isodate"])
def run(self):
self._find_ios_database(root_paths=SHUTDOWN_LOG_PATH)
self.log.info("Found shutdown log at path: %s", self.file_path)
with open(self.file_path, "r") as handle:
self.process_shutdownlog(handle.read())

View File

@@ -1,97 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
import sqlite3
from base64 import b64encode
from mvt.common.utils import check_for_links
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from .base import IOSExtraction
SMS_BACKUP_IDS = [
"3d0d7e5fb2ce288813306e4d4636395e047a3d28",
]
SMS_ROOT_PATHS = [
"private/var/mobile/Library/SMS/sms.db",
]
class SMS(IOSExtraction):
"""This module extracts all SMS messages containing links."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
text = record["text"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "sms_received",
"data": f"{record['service']}: {record['guid']} \"{text}\" from {record['phone_number']} ({record['account']})"
}
def check_indicators(self):
if not self.indicators:
return
for message in self.results:
if not "text" in message:
continue
message_links = check_for_links(message["text"])
if self.indicators.check_domains(message_links):
self.detected.append(message)
def run(self):
self._find_ios_database(backup_ids=SMS_BACKUP_IDS, root_paths=SMS_ROOT_PATHS)
self.log.info("Found SMS database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
message.*,
handle.id as "phone_number"
FROM message, handle
WHERE handle.rowid = message.handle_id;
""")
names = [description[0] for description in cur.description]
for item in cur:
message = dict()
for index, value in enumerate(item):
# We base64 escape some of the attributes that could contain
# binary data.
if (names[index] == "attributedBody" or
names[index] == "payload_data" or
names[index] == "message_summary_info") and value:
value = b64encode(value).decode()
# We store the value of each column under the proper key.
message[names[index]] = value
# We convert Mac's ridiculous timestamp format.
message["isodate"] = convert_timestamp_to_iso(convert_mactime_to_unix(message["date"]))
message["direction"] = ("sent" if message["is_from_me"] == 1 else "received")
# Sometimes "text" is None instead of empty string.
if message["text"] is None:
message["text"] = ""
# Extract links from the SMS message.
message_links = check_for_links(message["text"])
# If we find links in the messages or if they are empty we add them to the list.
if message_links or message["text"].strip() == "":
self.results.append(message)
cur.close()
conn.close()
self.log.info("Extracted a total of %d SMS messages containing links", len(self.results))

View File

@@ -1,19 +1,20 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import json
import datetime
import json
from mvt.common.utils import convert_timestamp_to_iso
from .base import IOSExtraction
from ..base import IOSExtraction
IOS_ANALYTICS_JOURNAL_PATHS = [
"private/var/db/analyticsd/Analytics-Journal-*.ips",
]
class IOSVersionHistory(IOSExtraction):
"""This module extracts iOS update history from Analytics Journal log files."""
@@ -32,7 +33,7 @@ class IOSVersionHistory(IOSExtraction):
}
def run(self):
for found_path in self._find_paths(IOS_ANALYTICS_JOURNAL_PATHS):
for found_path in self._get_fs_files_from_patterns(IOS_ANALYTICS_JOURNAL_PATHS):
with open(found_path, "r") as analytics_log:
log_line = json.loads(analytics_log.readline().strip())

View File

@@ -1,15 +1,16 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import datetime
from .base import IOSExtraction
import os
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
class WebkitBase(IOSExtraction):
"""This class is a base for other WebKit-related modules."""
@@ -21,8 +22,8 @@ class WebkitBase(IOSExtraction):
if self.indicators.check_domain(item["url"]):
self.detected.append(item)
def _database_from_path(self, root_paths):
for found_path in self._find_paths(root_paths):
def _process_webkit_folder(self, root_paths):
for found_path in self._get_fs_files_from_patterns(root_paths):
key = os.path.relpath(found_path, self.base_folder)
for name in os.listdir(found_path):
@@ -33,8 +34,8 @@ class WebkitBase(IOSExtraction):
name = name.replace("https_", "https://")
url = name.split("_")[0]
self.results.append(dict(
folder=key,
url=url,
isodate=convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(os.stat(found_path).st_mtime)),
))
self.results.append({
"folder": key,
"url": url,
"isodate": convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(os.stat(found_path).st_mtime)),
})

View File

@@ -1,7 +1,7 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .webkit_base import WebkitBase
@@ -9,9 +9,13 @@ WEBKIT_INDEXEDDB_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/WebKit/WebsiteData/IndexedDB",
]
class WebkitIndexedDB(WebkitBase):
"""This module looks extracts records from WebKit IndexedDB folders,
and checks them against any provided list of suspicious domains."""
and checks them against any provided list of suspicious domains.
"""
slug = "webkit_indexeddb"
@@ -30,6 +34,6 @@ class WebkitIndexedDB(WebkitBase):
}
def run(self):
self._database_from_path(WEBKIT_INDEXEDDB_ROOT_PATHS)
self._process_webkit_folder(WEBKIT_INDEXEDDB_ROOT_PATHS)
self.log.info("Extracted a total of %d WebKit IndexedDB records",
len(self.results))

View File

@@ -1,7 +1,7 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .webkit_base import WebkitBase
@@ -9,9 +9,13 @@ WEBKIT_LOCALSTORAGE_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/WebKit/WebsiteData/LocalStorage/",
]
class WebkitLocalStorage(WebkitBase):
"""This module looks extracts records from WebKit LocalStorage folders,
and checks them against any provided list of suspicious domains."""
and checks them against any provided list of suspicious domains.
"""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
@@ -28,6 +32,6 @@ class WebkitLocalStorage(WebkitBase):
}
def run(self):
self._database_from_path(WEBKIT_LOCALSTORAGE_ROOT_PATHS)
self._process_webkit_folder(WEBKIT_LOCALSTORAGE_ROOT_PATHS)
self.log.info("Extracted a total of %d records from WebKit Local Storages",
len(self.results))

View File

@@ -1,7 +1,7 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .webkit_base import WebkitBase
@@ -9,9 +9,13 @@ WEBKIT_SAFARIVIEWSERVICE_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/",
]
class WebkitSafariViewService(WebkitBase):
"""This module looks extracts records from WebKit LocalStorage folders,
and checks them against any provided list of suspicious domains."""
and checks them against any provided list of suspicious domains.
"""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
@@ -20,4 +24,6 @@ class WebkitSafariViewService(WebkitBase):
log=log, results=results)
def run(self):
self._database_from_path(WEBKIT_SAFARIVIEWSERVICE_ROOT_PATHS)
self._process_webkit_folder(WEBKIT_SAFARIVIEWSERVICE_ROOT_PATHS)
self.log.info("Extracted a total of %d records from WebKit SafariViewService WebsiteData",
len(self.results))

View File

@@ -1,83 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
import sqlite3
import logging
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso, check_for_links
from .base import IOSExtraction
log = logging.getLogger(__name__)
WHATSAPP_BACKUP_IDS = [
"7c7fba66680ef796b916b067077cc246adacf01d",
]
WHATSAPP_ROOT_PATHS = [
"private/var/mobile/Containers/Shared/AppGroup/*/ChatStorage.sqlite",
]
class Whatsapp(IOSExtraction):
"""This module extracts all WhatsApp messages containing links."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
text = record["ZTEXT"].replace("\n", "\\n")
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "message",
"data": f"{text} from {record['ZFROMJID']}"
}
def check_indicators(self):
if not self.indicators:
return
for message in self.results:
if not "ZTEXT" in message:
continue
message_links = check_for_links(message["ZTEXT"])
if self.indicators.check_domains(message_links):
self.detected.append(message)
def run(self):
self._find_ios_database(backup_ids=WHATSAPP_BACKUP_IDS, root_paths=WHATSAPP_ROOT_PATHS)
log.info("Found WhatsApp database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("SELECT * FROM ZWAMESSAGE;")
names = [description[0] for description in cur.description]
for message in cur:
new_message = dict()
for index, value in enumerate(message):
new_message[names[index]] = value
if not new_message["ZTEXT"]:
continue
# We convert Mac's silly timestamp again.
new_message["isodate"] = convert_timestamp_to_iso(convert_mactime_to_unix(new_message["ZMESSAGEDATE"]))
# Extract links from the WhatsApp message.
message_links = check_for_links(new_message["ZTEXT"])
# If we find mesages, or if there's an empty message we add it to the list.
if new_message["ZTEXT"] and (message_links or new_message["ZTEXT"].strip() == ""):
self.results.append(new_message)
cur.close()
conn.close()
log.info("Extracted a total of %d WhatsApp messages containing links", len(self.results))

View File

@@ -0,0 +1,31 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .calls import Calls
from .chrome_favicon import ChromeFavicon
from .chrome_history import ChromeHistory
from .contacts import Contacts
from .firefox_favicon import FirefoxFavicon
from .firefox_history import FirefoxHistory
from .idstatuscache import IDStatusCache
from .interactionc import InteractionC
from .locationd import LocationdClients
from .net_datausage import Datausage
from .osanalytics_addaily import OSAnalyticsADDaily
from .safari_browserstate import SafariBrowserState
from .safari_history import SafariHistory
from .shortcuts import Shortcuts
from .sms import SMS
from .sms_attachments import SMSAttachments
from .tcc import TCC
from .webkit_resource_load_statistics import WebkitResourceLoadStatistics
from .webkit_session_resource_log import WebkitSessionResourceLog
from .whatsapp import Whatsapp
MIXED_MODULES = [Calls, ChromeFavicon, ChromeHistory, Contacts, FirefoxFavicon,
FirefoxHistory, IDStatusCache, InteractionC, LocationdClients,
OSAnalyticsADDaily, Datausage, SafariBrowserState, SafariHistory,
TCC, SMS, SMSAttachments, WebkitResourceLoadStatistics,
WebkitSessionResourceLog, Whatsapp, Shortcuts]

View File

@@ -1,12 +1,13 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import sqlite3
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from .base import IOSExtraction
from ..base import IOSExtraction
CALLS_BACKUP_IDS = [
"5a4935c78a5255723f707230a451d79c540d2741",
@@ -15,6 +16,7 @@ CALLS_ROOT_PATHS = [
"private/var/mobile/Library/CallHistoryDB/CallHistory.storedata"
]
class Calls(IOSExtraction):
"""This module extracts phone calls details"""
@@ -33,7 +35,8 @@ class Calls(IOSExtraction):
}
def run(self):
self._find_ios_database(backup_ids=CALLS_BACKUP_IDS, root_paths=CALLS_ROOT_PATHS)
self._find_ios_database(backup_ids=CALLS_BACKUP_IDS,
root_paths=CALLS_ROOT_PATHS)
self.log.info("Found Calls database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
@@ -41,17 +44,17 @@ class Calls(IOSExtraction):
cur.execute("""
SELECT
ZDATE, ZDURATION, ZLOCATION, ZADDRESS, ZSERVICE_PROVIDER
FROM ZCALLRECORD;
FROM ZCALLRECORD;
""")
names = [description[0] for description in cur.description]
# names = [description[0] for description in cur.description]
for entry in cur:
for row in cur:
self.results.append({
"isodate": convert_timestamp_to_iso(convert_mactime_to_unix(entry[0])),
"duration": entry[1],
"location": entry[2],
"number": entry[3].decode("utf-8") if entry[3] and entry[3] is bytes else entry[3],
"provider": entry[4]
"isodate": convert_timestamp_to_iso(convert_mactime_to_unix(row[0])),
"duration": row[1],
"location": row[2],
"number": row[3].decode("utf-8") if row[3] and row[3] is bytes else row[3],
"provider": row[4]
})
cur.close()

View File

@@ -1,13 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import sqlite3
from mvt.common.utils import convert_chrometime_to_unix, convert_timestamp_to_iso
from mvt.common.utils import (convert_chrometime_to_unix,
convert_timestamp_to_iso)
from .base import IOSExtraction
from ..base import IOSExtraction
CHROME_FAVICON_BACKUP_IDS = [
"55680ab883d0fdcffd94f959b1632e5fbbb18c5b"
@@ -18,6 +19,7 @@ CHROME_FAVICON_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/Favicons",
]
class ChromeFavicon(IOSExtraction):
"""This module extracts all Chrome favicon records."""
@@ -44,14 +46,16 @@ class ChromeFavicon(IOSExtraction):
self.detected.append(result)
def run(self):
self._find_ios_database(backup_ids=CHROME_FAVICON_BACKUP_IDS, root_paths=CHROME_FAVICON_ROOT_PATHS)
self._find_ios_database(backup_ids=CHROME_FAVICON_BACKUP_IDS,
root_paths=CHROME_FAVICON_ROOT_PATHS)
self.log.info("Found Chrome favicon cache database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
# Fetch icon cache
cur = conn.cursor()
cur.execute("""SELECT
cur.execute("""
SELECT
icon_mapping.page_url,
favicons.url,
favicon_bitmaps.last_updated,
@@ -59,20 +63,21 @@ class ChromeFavicon(IOSExtraction):
FROM icon_mapping
JOIN favicon_bitmaps ON icon_mapping.icon_id = favicon_bitmaps.icon_id
JOIN favicons ON icon_mapping.icon_id = favicons.id
ORDER BY icon_mapping.id;""")
ORDER BY icon_mapping.id;
""")
items = []
for item in cur:
last_timestamp = int(item[2]) or int(item[3])
items.append(dict(
url=item[0],
icon_url=item[1],
timestamp=last_timestamp,
isodate=convert_timestamp_to_iso(convert_chrometime_to_unix(last_timestamp)),
))
records = []
for row in cur:
last_timestamp = int(row[2]) or int(row[3])
records.append({
"url": row[0],
"icon_url": row[1],
"timestamp": last_timestamp,
"isodate": convert_timestamp_to_iso(convert_chrometime_to_unix(last_timestamp)),
})
cur.close()
conn.close()
self.log.info("Extracted a total of %d favicon records", len(items))
self.results = sorted(items, key=lambda item: item["isodate"])
self.log.info("Extracted a total of %d favicon records", len(records))
self.results = sorted(records, key=lambda row: row["isodate"])

View File

@@ -1,23 +1,24 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import sqlite3
from mvt.common.utils import convert_chrometime_to_unix, convert_timestamp_to_iso
from mvt.common.utils import (convert_chrometime_to_unix,
convert_timestamp_to_iso)
from .base import IOSExtraction
from ..base import IOSExtraction
CHROME_HISTORY_BACKUP_IDS = [
"faf971ce92c3ac508c018dce1bef2a8b8e9838f1",
]
# TODO: Confirm Chrome database path.
CHROME_HISTORY_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/History",
]
class ChromeHistory(IOSExtraction):
"""This module extracts all Chome visits."""
@@ -35,8 +36,17 @@ class ChromeHistory(IOSExtraction):
"data": f"{record['id']} - {record['url']} (visit ID: {record['visit_id']}, redirect source: {record['redirect_source']})"
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if self.indicators.check_domain(result["url"]):
self.detected.append(result)
def run(self):
self._find_ios_database(backup_ids=CHROME_HISTORY_BACKUP_IDS, root_paths=CHROME_HISTORY_ROOT_PATHS)
self._find_ios_database(backup_ids=CHROME_HISTORY_BACKUP_IDS,
root_paths=CHROME_HISTORY_ROOT_PATHS)
self.log.info("Found Chrome history database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
@@ -54,14 +64,14 @@ class ChromeHistory(IOSExtraction):
""")
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()

View File

@@ -1,11 +1,11 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import sqlite3
from .base import IOSExtraction
from ..base import IOSExtraction
CONTACTS_BACKUP_IDS = [
"31bb7ba8914766d4ba40d6dfb6113c8b614be442",
@@ -14,6 +14,7 @@ CONTACTS_ROOT_PATHS = [
"private/var/mobile/Library/AddressBook/AddressBook.sqlitedb",
]
class Contacts(IOSExtraction):
"""This module extracts all contact details from the phone's address book."""
@@ -39,9 +40,9 @@ class Contacts(IOSExtraction):
""")
names = [description[0] for description in cur.description]
for entry in cur:
new_contact = dict()
for index, value in enumerate(entry):
for row in cur:
new_contact = {}
for index, value in enumerate(row):
new_contact[names[index]] = value
self.results.append(new_contact)
@@ -49,4 +50,5 @@ class Contacts(IOSExtraction):
cur.close()
conn.close()
self.log.info("Extracted a total of %d contacts from the address book", len(self.results))
self.log.info("Extracted a total of %d contacts from the address book",
len(self.results))

View File

@@ -1,15 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import sqlite3
from datetime import datetime
from mvt.common.url import URL
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from .base import IOSExtraction
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
FIREFOX_HISTORY_BACKUP_IDS = [
"2e57c396a35b0d1bcbc624725002d98bd61d142b",
@@ -18,6 +17,7 @@ FIREFOX_HISTORY_ROOT_PATHS = [
"private/var/mobile/profile.profile/browser.db",
]
class FirefoxFavicon(IOSExtraction):
"""This module extracts all Firefox favicon"""
@@ -40,11 +40,13 @@ class FirefoxFavicon(IOSExtraction):
return
for result in self.results:
if self.indicators.check_domain(result["url"]) or self.indicators.check_domain(result["history_url"]):
if (self.indicators.check_domain(result.get("url", "")) or
self.indicators.check_domain(result.get("history_url", ""))):
self.detected.append(result)
def run(self):
self._find_ios_database(backup_ids=FIREFOX_HISTORY_BACKUP_IDS, root_paths=FIREFOX_HISTORY_ROOT_PATHS)
self._find_ios_database(backup_ids=FIREFOX_HISTORY_BACKUP_IDS,
root_paths=FIREFOX_HISTORY_ROOT_PATHS)
self.log.info("Found Firefox favicon database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
@@ -65,16 +67,16 @@ class FirefoxFavicon(IOSExtraction):
""")
for item in cur:
self.results.append(dict(
id=item[0],
url=item[1],
width=item[2],
height=item[3],
type=item[4],
isodate=convert_timestamp_to_iso(datetime.utcfromtimestamp(item[5])),
history_id=item[6],
history_url=item[7]
))
self.results.append({
"id": item[0],
"url": item[1],
"width": item[2],
"height": item[3],
"type": item[4],
"isodate": convert_timestamp_to_iso(datetime.utcfromtimestamp(item[5])),
"history_id": item[6],
"history_url": item[7]
})
cur.close()
conn.close()

View File

@@ -1,15 +1,14 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import sqlite3
from datetime import datetime
from mvt.common.url import URL
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from .base import IOSExtraction
from mvt.common.utils import convert_timestamp_to_iso
from ..base import IOSExtraction
FIREFOX_HISTORY_BACKUP_IDS = [
"2e57c396a35b0d1bcbc624725002d98bd61d142b",
@@ -18,9 +17,13 @@ FIREFOX_HISTORY_ROOT_PATHS = [
"private/var/mobile/profile.profile/browser.db",
]
class FirefoxHistory(IOSExtraction):
"""This module extracts all Firefox visits and tries to detect potential
network injection attacks."""
network injection attacks.
"""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
@@ -62,15 +65,15 @@ class FirefoxHistory(IOSExtraction):
WHERE visits.siteID = history.id;
""")
for item in cur:
self.results.append(dict(
id=item[0],
isodate=convert_timestamp_to_iso(datetime.utcfromtimestamp(item[1])),
url=item[2],
title=item[3],
i1000000s_local=item[4],
type=item[5]
))
for row in cur:
self.results.append({
"id": row[0],
"isodate": convert_timestamp_to_iso(datetime.utcfromtimestamp(row[1])),
"url": row[2],
"title": row[3],
"i1000000s_local": row[4],
"type": row[5]
})
cur.close()
conn.close()

View File

@@ -1,24 +1,24 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import glob
import biplist
import collections
import plistlib
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from .base import IOSExtraction
from ..base import IOSExtraction
IDSTATUSCACHE_BACKUP_IDS = [
"6b97989189901ceaa4e5be9b7f05fb584120e27b",
]
IDSTATUSCACHE_ROOT_PATHS = [
"private/var/mobile/Library/Preferences/com.apple.identityservices.idstatuscache.plist",
"private/var/mobile/Library/IdentityServices/idstatuscache.plist",
]
class IDStatusCache(IOSExtraction):
"""Extracts Apple Authentication information from idstatuscache.plist"""
@@ -41,22 +41,20 @@ class IDStatusCache(IOSExtraction):
return
for result in self.results:
if result["user"].startswith("mailto:"):
if result.get("user", "").startswith("mailto:"):
email = result["user"][7:].strip("'")
if self.indicators.check_email(email):
self.detected.append(result)
continue
if "\\x00\\x00" in result["user"]:
if "\\x00\\x00" in result.get("user", ""):
self.log.warning("Found an ID Status Cache entry with suspicious patterns: %s",
result["user"])
result.get("user"))
self.detected.append(result)
def run(self):
self._find_ios_database(backup_ids=IDSTATUSCACHE_BACKUP_IDS, root_paths=IDSTATUSCACHE_ROOT_PATHS)
self.log.info("Found IDStatusCache plist at path: %s", self.file_path)
file_plist = biplist.readPlist(self.file_path)
def _extract_idstatuscache_entries(self, file_path):
with open(file_path, "rb") as handle:
file_plist = plistlib.load(handle)
id_status_cache_entries = []
for app in file_plist:
@@ -79,8 +77,20 @@ class IDStatusCache(IOSExtraction):
entry_counter = collections.Counter([entry["user"] for entry in id_status_cache_entries])
for entry in id_status_cache_entries:
# Add total count of occurrences to the status cache entry
# Add total count of occurrences to the status cache entry.
entry["occurrences"] = entry_counter[entry["user"]]
self.results.append(entry)
def run(self):
if self.is_backup:
self._find_ios_database(backup_ids=IDSTATUSCACHE_BACKUP_IDS)
self.log.info("Found IDStatusCache plist at path: %s", self.file_path)
self._extract_idstatuscache_entries(self.file_path)
elif self.is_fs_dump:
for idstatuscache_path in self._get_fs_files_from_patterns(IDSTATUSCACHE_ROOT_PATHS):
self.file_path = idstatuscache_path
self.log.info("Found IDStatusCache plist at path: %s", self.file_path)
self._extract_idstatuscache_entries(self.file_path)
self.log.info("Extracted a total of %d ID Status Cache entries", len(self.results))

View File

@@ -1,14 +1,13 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import sqlite3
from base64 import b64encode
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from .base import IOSExtraction
from ..base import IOSExtraction
INTERACTIONC_BACKUP_IDS = [
"1f5a521220a3ad80ebfdc196978df8e7a2e49dee",
@@ -17,6 +16,7 @@ INTERACTIONC_ROOT_PATHS = [
"private/var/mobile/Library/CoreDuet/People/interactionC.db",
]
class InteractionC(IOSExtraction):
"""This module extracts data from InteractionC db."""
@@ -55,8 +55,8 @@ class InteractionC(IOSExtraction):
"timestamp": record[ts],
"module": self.__class__.__name__,
"event": ts,
"data": f"[{record['bundle_id']}] {record['account']} - from {record['sender_display_name']} " \
f"({record['sender_identifier']}) to {record['recipient_display_name']} " \
"data": f"[{record['bundle_id']}] {record['account']} - from {record['sender_display_name']} "
f"({record['sender_identifier']}) to {record['recipient_display_name']} "
f"({record['recipient_identifier']}): {record['content']}"
})
processed.append(record[ts])
@@ -117,55 +117,55 @@ class InteractionC(IOSExtraction):
ZINTERACTIONS.ZGROUPNAME,
ZINTERACTIONS.ZDERIVEDINTENTIDENTIFIER,
ZINTERACTIONS.Z_PK
FROM ZINTERACTIONS
LEFT JOIN ZCONTACTS ON ZINTERACTIONS.ZSENDER = ZCONTACTS.Z_PK
LEFT JOIN Z_1INTERACTIONS ON ZINTERACTIONS.Z_PK == Z_1INTERACTIONS.Z_3INTERACTIONS
LEFT JOIN ZATTACHMENT ON Z_1INTERACTIONS.Z_1ATTACHMENTS == ZATTACHMENT.Z_PK
LEFT JOIN Z_2INTERACTIONRECIPIENT ON ZINTERACTIONS.Z_PK== Z_2INTERACTIONRECIPIENT.Z_3INTERACTIONRECIPIENT
LEFT JOIN ZCONTACTS RECEIPIENTCONACT ON Z_2INTERACTIONRECIPIENT.Z_2RECIPIENTS== RECEIPIENTCONACT.Z_PK;
FROM ZINTERACTIONS
LEFT JOIN ZCONTACTS ON ZINTERACTIONS.ZSENDER = ZCONTACTS.Z_PK
LEFT JOIN Z_1INTERACTIONS ON ZINTERACTIONS.Z_PK == Z_1INTERACTIONS.Z_3INTERACTIONS
LEFT JOIN ZATTACHMENT ON Z_1INTERACTIONS.Z_1ATTACHMENTS == ZATTACHMENT.Z_PK
LEFT JOIN Z_2INTERACTIONRECIPIENT ON ZINTERACTIONS.Z_PK== Z_2INTERACTIONRECIPIENT.Z_3INTERACTIONRECIPIENT
LEFT JOIN ZCONTACTS RECEIPIENTCONACT ON Z_2INTERACTIONRECIPIENT.Z_2RECIPIENTS== RECEIPIENTCONACT.Z_PK;
""")
names = [description[0] for description in cur.description]
# names = [description[0] for description in cur.description]
for item in cur:
for row in cur:
self.results.append({
"start_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[0])),
"end_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[1])),
"bundle_id": item[2],
"account": item[3],
"target_bundle_id": item[4],
"direction": item[5],
"sender_display_name": item[6],
"sender_identifier": item[7],
"sender_personid": item[8],
"recipient_display_name": item[9],
"recipient_identifier": item[10],
"recipient_personid": item[11],
"recipient_count": item[12],
"domain_identifier": item[13],
"is_response": item[14],
"content": item[15],
"uti": item[16],
"content_url": item[17],
"size": item[18],
"photo_local_id": item[19],
"attachment_id": item[20],
"cloud_id": item[21],
"incoming_recipient_count": item[22],
"incoming_sender_count": item[23],
"outgoing_recipient_count": item[24],
"interactions_creation_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[25])) if item[25] else None,
"contacts_creation_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[26])) if item[26] else None,
"first_incoming_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[27])) if item[27] else None,
"first_incoming_sender_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[28])) if item[28] else None,
"first_outgoing_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[29])) if item[29] else None,
"last_incoming_sender_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[30])) if item[30] else None,
"last_incoming_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[31])) if item[31] else None,
"last_outgoing_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(item[32])) if item[32] else None,
"custom_id": item[33],
"location_uuid": item[35],
"group_name": item[36],
"derivied_intent_id": item[37],
"table_id": item[38]
"start_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[0])),
"end_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[1])),
"bundle_id": row[2],
"account": row[3],
"target_bundle_id": row[4],
"direction": row[5],
"sender_display_name": row[6],
"sender_identifier": row[7],
"sender_personid": row[8],
"recipient_display_name": row[9],
"recipient_identifier": row[10],
"recipient_personid": row[11],
"recipient_count": row[12],
"domain_identifier": row[13],
"is_response": row[14],
"content": row[15],
"uti": row[16],
"content_url": row[17],
"size": row[18],
"photo_local_id": row[19],
"attachment_id": row[20],
"cloud_id": row[21],
"incoming_recipient_count": row[22],
"incoming_sender_count": row[23],
"outgoing_recipient_count": row[24],
"interactions_creation_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[25])) if row[25] else None,
"contacts_creation_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[26])) if row[26] else None,
"first_incoming_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[27])) if row[27] else None,
"first_incoming_sender_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[28])) if row[28] else None,
"first_outgoing_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[29])) if row[29] else None,
"last_incoming_sender_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[30])) if row[30] else None,
"last_incoming_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[31])) if row[31] else None,
"last_outgoing_recipient_date": convert_timestamp_to_iso(convert_mactime_to_unix(row[32])) if row[32] else None,
"custom_id": row[33],
"location_uuid": row[35],
"group_name": row[36],
"derivied_intent_id": row[37],
"table_id": row[38]
})
cur.close()

View File

@@ -0,0 +1,91 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import plistlib
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
LOCATIOND_BACKUP_IDS = [
"a690d7769cce8904ca2b67320b107c8fe5f79412",
]
LOCATIOND_ROOT_PATHS = [
"private/var/mobile/Library/Caches/locationd/clients.plist",
"private/var/root/Library/Caches/locationd/clients.plist"
]
class LocationdClients(IOSExtraction):
"""Extract information from apps who used geolocation."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
self.timestamps = [
"ConsumptionPeriodBegin",
"ReceivingLocationInformationTimeStopped",
"VisitTimeStopped",
"LocationTimeStopped",
"BackgroundLocationTimeStopped",
"SignificantTimeStopped",
"NonPersistentSignificantTimeStopped",
"FenceTimeStopped",
"BeaconRegionTimeStopped",
]
def serialize(self, record):
records = []
for 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 check_indicators(self):
if not self.indicators:
return
for result in self.results:
parts = result["package"].split("/")
proc_name = parts[len(parts)-1]
if self.indicators.check_process(proc_name):
self.detected.append(result)
def _extract_locationd_entries(self, file_path):
with open(file_path, "rb") as handle:
file_plist = plistlib.load(handle)
for key, values in file_plist.items():
result = file_plist[key]
result["package"] = key
for ts in self.timestamps:
if ts in result.keys():
result[ts] = convert_timestamp_to_iso(convert_mactime_to_unix(result[ts]))
self.results.append(result)
def run(self):
if self.is_backup:
self._find_ios_database(backup_ids=LOCATIOND_BACKUP_IDS)
self.log.info("Found Locationd Clients plist at path: %s", self.file_path)
self._extract_locationd_entries(self.file_path)
elif self.is_fs_dump:
for locationd_path in self._get_fs_files_from_patterns(LOCATIOND_ROOT_PATHS):
self.file_path = locationd_path
self.log.info("Found Locationd Clients plist at path: %s", self.file_path)
self._extract_locationd_entries(self.file_path)
self.log.info("Extracted a total of %d Locationd Clients entries", len(self.results))

View File

@@ -1,9 +1,9 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 MVT Project Developers.
# See the file 'LICENSE' for usage and copying permissions, or find a copy at
# https://github.com/mvt-project/mvt/blob/main/LICENSE
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .net_base import NetBase
from ..net_base import NetBase
DATAUSAGE_BACKUP_IDS = [
"0d609c54856a9bb2d56729df1d68f2958a88426b",
@@ -12,9 +12,13 @@ DATAUSAGE_ROOT_PATHS = [
"private/var/wireless/Library/Databases/DataUsage.sqlite",
]
class Datausage(NetBase):
"""This class extracts data from DataUsage.sqlite and attempts to identify
any suspicious processes if running on a full filesystem dump."""
any suspicious processes if running on a full filesystem dump.
"""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
@@ -23,7 +27,8 @@ class Datausage(NetBase):
log=log, results=results)
def run(self):
self._find_ios_database(backup_ids=DATAUSAGE_BACKUP_IDS, root_paths=DATAUSAGE_ROOT_PATHS)
self._find_ios_database(backup_ids=DATAUSAGE_BACKUP_IDS,
root_paths=DATAUSAGE_ROOT_PATHS)
self.log.info("Found DataUsage database at path: %s", self.file_path)
self._extract_net_data()

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