Compare commits

...

41 Commits

Author SHA1 Message Date
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
70 changed files with 850 additions and 295 deletions

View File

@@ -15,15 +15,15 @@ It has been developed and released by the [Amnesty International Security Lab](h
## Installation
MVT can be installed from sources or from [PyPi](https://pypi.org/project/mvt/) (you will need some dependencies, check the [documentation](https://docs.mvt.re/en/latest/install.html)):
MVT can be installed from sources or from [PyPi](https://pypi.org/project/mvt/) (you will need some dependencies, check the [documentation](https://docs.mvt.re/en/latest/install/)):
```
pip3 install mvt
```
Alternatively, you can decide to run MVT and all relevant tools through a [Docker container](https://docs.mvt.re/en/latest/docker.html).
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.html#mvt-on-windows)
**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
@@ -31,4 +31,4 @@ MVT provides two commands `mvt-ios` and `mvt-android`. [Check out the documentat
## License
The purpose of MVT is to facilitate the ***consensual forensic analysis*** of devices of those who might be targets of sophisticated mobile spyware attacks, especially members of civil society and marginalized communities. We do not want MVT to enable privacy violations of non-consenting individuals. In order to achieve this, MVT is released under its own license. [Read more here.](https://docs.mvt.re/en/latest/license.html)
The purpose of MVT is to facilitate the ***consensual forensic analysis*** of devices of those who might be targets of sophisticated mobile spyware attacks, especially members of civil society and marginalized communities. We do not want MVT to enable privacy violations of non-consenting individuals. In order to achieve this, MVT is released under its own license. [Read more here.](https://docs.mvt.re/en/latest/license/)

View File

@@ -28,10 +28,17 @@ The `--iocs` option can be invoked multiple times to let MVT import multiple STI
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://github.com/AmnestyTech/investigations/tree/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://github.com/Te-k/stalkerware-indicators/blob/master/stalkerware.stix2).
Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs.

View File

@@ -4,10 +4,22 @@ 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:
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.
@@ -17,7 +29,7 @@ This JSON file is created by mvt-ios' `BackupInfo` module. The module extracts s
### `cache_files.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `CacheFiles` module. The module extracts records from all SQLite database files stored on disk with the name *Cache.db*. These databases typically contain data from iOS' [internal URL caching](https://developer.apple.com/documentation/foundation/nsurlcache). Through this module you might be able to recover records of HTTP requests and responses performed my applications as well as system services, that would otherwise be unavailable. For example, you might see HTTP requests part of an exploitation chain performed by an iOS service attempting to download a first stage malicious payload.
@@ -29,7 +41,7 @@ If indicators are provided through the command-line, they are checked against th
### `calls.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Calls` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/CallHistoryDB/CallHistory.storedata*, which contains records of incoming and outgoing calls, including from messaging apps such as WhatsApp or Skype.
@@ -39,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.
@@ -51,29 +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:
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.
@@ -83,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.
@@ -95,19 +109,19 @@ 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 (before iOS 14.7): :material-check:
Backup (before iOS 14.7): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `IDStatusCache` module. The module extracts records from a plist file located at */private/var/mobile/Library/Preferences/com.apple.identityservices.idstatuscache.plist*, which contains a cache of Apple user ID authentication. This chance will indicate when apps like Facetime and iMessage first established contacts with other registered Apple IDs. This is significant because it might contain traces of malicious accounts involved in exploitation of those apps.
@@ -116,10 +130,20 @@ 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 (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `InteractionC` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/CoreDuet/People/interactionC.db*, which contains details about user interactions with installed apps.
@@ -129,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.
@@ -139,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.
@@ -151,7 +175,7 @@ If indicators are provided through the command-line, they are checked against th
### `os_analytics_ad_daily.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `OSAnalyticsADDaily` module. The module extracts records from a plist located *private/var/mobile/Library/Preferences/com.apple.osanalytics.addaily.plist*, which contains a history of data usage by processes running on the system. Besides the network statistics, these records are particularly important because they might show traces of malicious process executions and the relevant timeframe.
@@ -163,7 +187,7 @@ If indicators are provided through the command-line, they are checked against th
### `datausage.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `Datausage` module. The module extracts records from a SQLite database located */private/var/wireless/Library/Databases/DataUsage.sqlite*, which contains a history of data usage by processes running on the system. Besides the network statistics, these records are particularly important because they might show traces of malicious process executions and the relevant timeframe. In particular, processes which do not have a valid bundle ID might require particular attention.
@@ -175,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.
@@ -187,7 +211,7 @@ If indicators are provided through the command-line, they are checked against th
### `profile_events.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-close:
This JSON file is created by mvt-ios' `ProfileEvents` module. The module extracts a timeline of configuration profile operations. For example, it should indicate when a new profile was installed from the Settings app, or when one was removed.
@@ -197,19 +221,19 @@ This JSON file is created by mvt-ios' `ProfileEvents` module. The module extract
### `safari_browser_state.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SafariBrowserState` module. The module extracts records from the SQLite databases located at */private/var/mobile/Library/Safari/BrowserState.db* or */private/var/mobile/Containers/Data/Application/\*/Library/Safari/BrowserState.db*, which contain records of opened tabs.
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.
@@ -221,7 +245,7 @@ If indicators are provided through the command-line, they are checked against bo
### `safari_history.json`
!!! info "Availability"
Backup (if encrypted): :material-check:
Backup (if encrypted): :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SafariHistory` module. The module extracts records from the SQLite databases located at */private/var/mobile/Library/Safari/History.db* or */private/var/mobile/Containers/Data/Application/\*/Library/Safari/History.db*, which contain a history of URL visits.
@@ -233,10 +257,10 @@ If indicators are provided through the command-line, they are checked against th
### `shutdown_log.json`
!!! info "Availability"
Backup (if encrypted): :material-close:
Backup (if encrypted): :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `ShutdownLog` module. The module extracts records from the shutdown log located at *private/var/db/diagnostics/shutdown.log*. When shutting down an iPhone, a SIGTERM will be sent to all processes runnning. The `shutdown.log` file will log any process (with its pid and path) that did not shut down after the SIGTERM was sent.
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*.
@@ -245,7 +269,7 @@ If indicators are provided through the command-line, they are checked against th
### `sms.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SMS` module. The module extracts a list of SMS messages containing HTTP links from the SQLite database located at */private/var/mobile/Library/SMS/sms.db*.
@@ -257,7 +281,7 @@ If indicators are provided through the command-line, they are checked against th
### `sms_attachments.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `SMSAttachments` module. The module extracts details about attachments sent via SMS or iMessage from the same database used by the `SMS` module. These records might be useful to indicate unique patterns that might be indicative of exploitation attempts leveraging potential vulnerabilities in file format parsers or other forms of file handling by the Messages app.
@@ -267,7 +291,7 @@ This JSON file is created by mvt-ios' `SMSAttachments` module. The module extrac
### `tcc.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `TCC` module. The module extracts records from a SQLite database located at */private/var/mobile/Library/TCC/TCC.db*, which contains a list of which services such as microphone, camera, or location, apps have been granted or denied access to.
@@ -277,7 +301,7 @@ This JSON file is created by mvt-ios' `TCC` module. The module extracts records
### `version_history.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `IOSVersionHistory` module. The module extracts records of iOS software updates from analytics plist files located at */private/var/db/analyticsd/Analytics-Journal-\*.ips*.
@@ -287,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.
@@ -299,7 +323,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_local_storage.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitLocalStorage` module. The module extracts a list of file and folder names located at the following path */private/var/mobile/Containers/Data/Application/\*/Library/WebKit/WebsiteData/LocalStorage/*, which contains local storage files created by any app installed on the device.
@@ -311,7 +335,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_resource_load_statistics.json`
!!! info "Availability"
Backup: :material-check:
Backup: :material-check:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios `WebkitResourceLoadStatistics` module. The module extracts records from available WebKit ResourceLoadStatistics *observations.db* SQLite3 databases. These records should indicate domain names contacted by apps, including a timestamp.
@@ -323,7 +347,7 @@ If indicators are provided through the command-line, they are checked against th
### `webkit_safari_view_service.json`
!!! info "Availability"
Backup: :material-close:
Backup: :material-close:
Full filesystem dump: :material-check:
This JSON file is created by mvt-ios' `WebkitSafariViewService` module. The module extracts a list of file and folder names located at the following path */private/var/mobile/Containers/Data/Application/\*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/*, which contains files cached by SafariVewService.
@@ -335,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.
@@ -347,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

@@ -9,7 +9,9 @@ import os
import click
from rich.logging import RichHandler
from mvt.common.help import *
from mvt.common.help import HELP_MSG_MODULE, HELP_MSG_IOC
from mvt.common.help import HELP_MSG_FAST, HELP_MSG_OUTPUT, HELP_MSG_LIST_MODULES
from mvt.common.help import HELP_MSG_SERIAL
from mvt.common.indicators import Indicators, IndicatorsFileBadFormat
from mvt.common.logo import logo
from mvt.common.module import run_module, save_timeline
@@ -26,6 +28,7 @@ logging.basicConfig(level="INFO", format=LOG_FORMAT, handlers=[
RichHandler(show_path=False, log_time_format="%X")])
log = logging.getLogger(__name__)
#==============================================================================
# Main
#==============================================================================
@@ -104,10 +107,11 @@ def download_apks(ctx, all_apks, virustotal, koodous, all_checks, output, from_f
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, list_modules, module, serial):
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:
@@ -139,7 +143,8 @@ def check_adb(ctx, iocs, output, list_modules, module, serial):
if module and adb_module.__name__ != module:
continue
m = adb_module(output_folder=output, log=logging.getLogger(adb_module.__module__))
m = adb_module(output_folder=output, fast_mode=fast,
log=logging.getLogger(adb_module.__module__))
if serial:
m.serial = serial
@@ -191,7 +196,7 @@ def check_backup(ctx, iocs, output, backup_path, serial):
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!")
ctx.exit(1)
@@ -202,7 +207,7 @@ def check_backup(ctx, iocs, output, backup_path, serial):
if serial:
m.serial = serial
if iocs:
if len(indicators.ioc_count) > 0:
indicators.log = m.log
m.indicators = indicators

View File

@@ -7,7 +7,6 @@ import json
import logging
import os
import pkg_resources
from tqdm import tqdm
from mvt.common.module import InsufficientPrivileges
@@ -17,6 +16,7 @@ from .modules.adb.packages import Packages
log = logging.getLogger(__name__)
# TODO: Would be better to replace tqdm with rich.progress to reduce
# the number of dependencies. Need to investigate whether
# it's possible to have a similar callback system.
@@ -138,7 +138,7 @@ class DownloadAPKs(AndroidExtraction):
packages_selection.append(package)
log.info("Selected only %d packages which are not marked as system",
len(packages_selection))
len(packages_selection))
if len(packages_selection) == 0:
log.info("No packages were selected for download")

View File

@@ -13,6 +13,7 @@ from rich.text import Text
log = logging.getLogger(__name__)
def koodous_lookup(packages):
log.info("Looking up all extracted files on Koodous (www.koodous.com)")
log.info("This might take a while...")

View File

@@ -13,6 +13,7 @@ from rich.text import Text
log = logging.getLogger(__name__)
def get_virustotal_report(hashes):
apikey = "233f22e200ca5822bd91103043ccac138b910db79f29af5616a9afe8b6f215ad"
url = f"https://www.virustotal.com/partners/sysinternals/file-reports?apikey={apikey}"
@@ -36,6 +37,7 @@ 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)")
@@ -48,6 +50,7 @@ def virustotal_lookup(packages):
total_unique_hashes = len(unique_hashes)
detections = {}
def virustotal_query(batch):
report = get_virustotal_report(batch)
if not report:

View File

@@ -25,6 +25,7 @@ log = logging.getLogger(__name__)
ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey")
ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub")
class AndroidExtraction(MVTModule):
"""This class provides a base for all Android extraction modules."""
@@ -89,7 +90,7 @@ class AndroidExtraction(MVTModule):
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)
self.serial)
sys.exit(-1)
else:
break

View File

@@ -16,6 +16,7 @@ log = logging.getLogger(__name__)
CHROME_HISTORY_PATH = "data/data/com.android.chrome/app_chrome/Default/History"
class ChromeHistory(AndroidExtraction):
"""This module extracts records from Android's Chrome browsing history."""

View File

@@ -10,6 +10,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysBatterystats(AndroidExtraction):
"""This module extracts stats on battery consumption by processes."""
@@ -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

@@ -10,6 +10,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysFull(AndroidExtraction):
"""This module extracts stats on battery consumption by processes."""
@@ -30,6 +31,6 @@ class DumpsysFull(AndroidExtraction):
handle.write(stats)
log.info("Full dumpsys output stored at %s",
stats_path)
stats_path)
self._adb_disconnect()

View File

@@ -10,6 +10,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__)
class DumpsysProcstats(AndroidExtraction):
"""This module extracts stats on memory consumption by processes."""

View File

@@ -4,7 +4,6 @@
# https://license.mvt.re/1.1/
import logging
import os
from .base import AndroidExtraction
@@ -15,6 +14,7 @@ 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."""
@@ -67,16 +67,16 @@ class DumpsysReceivers(AndroidExtraction):
if activity == ACTION_NEW_OUTGOING_SMS:
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
receiver)
receiver)
elif activity == ACTION_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
receiver)
receiver)
elif activity == ACTION_DATA_SMS_RECEIVED:
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
receiver)
receiver)
elif activity == ACTION_PHONE_STATE:
self.log.info("Found a receiver monitoring telephony state: \"%s\"",
receiver)
receiver)
self.results.append({
"activity": activity,

View File

@@ -5,29 +5,120 @@
import logging
import os
import stat
import datetime
from mvt.common.utils import check_for_links, convert_timestamp_to_iso
from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Files(AndroidExtraction):
"""This module extracts the list of installed packages."""
"""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 == None:
output = self._adb_command(f"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 == 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_filename(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 = []
output = self._adb_command("find / -type f 2> /dev/null")
if output and self.output_folder:
files_txt_path = os.path.join(self.output_folder, "files.txt")
with open(files_txt_path, "w") as handle:
handle.write(output)
DATA_PATHS = ["/data/local/tmp/", "/sdcard/", "/tmp/"]
for path in DATA_PATHS:
file_info = self.find_path(path)
found_file_paths.extend(file_info)
log.info("List of visible files stored at %s", files_txt_path)
# 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

@@ -12,6 +12,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Packages(AndroidExtraction):
"""This module extracts the list of installed packages."""
@@ -49,11 +50,10 @@ class Packages(AndroidExtraction):
root_packages = root_packages_string.decode("utf-8").split("\n")
root_packages = [rp.strip() for rp in root_packages]
for result in self.results:
if result["package_name"] in root_packages:
self.log.warning("Found an installed package related to rooting/jailbreaking: \"%s\"",
result["package_name"])
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\"",

View File

@@ -9,6 +9,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__)
class Processes(AndroidExtraction):
"""This module extracts details on running processes."""

View File

@@ -12,6 +12,7 @@ from .base import AndroidExtraction
log = logging.getLogger(__name__)
class RootBinaries(AndroidExtraction):
"""This module extracts the list of installed packages."""

View File

@@ -15,12 +15,12 @@ log = logging.getLogger(__name__)
SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db"
SMS_BUGLE_QUERY = """
SELECT
SELECT
ppl.normalized_destination AS number,
p.timestamp AS timestamp,
CASE WHEN m.sender_id IN
CASE WHEN m.sender_id IN
(SELECT _id FROM participants WHERE contact_id=-1)
THEN 2 ELSE 1 END incoming, p.text AS text
THEN 2 ELSE 1 END incoming, p.text AS text
FROM messages m, conversations c, parts p,
participants ppl, conversation_participants cp
WHERE (m.conversation_id = c._id)
@@ -31,14 +31,15 @@ WHERE (m.conversation_id = c._id)
SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db"
SMS_MMSMS_QUERY = """
SELECT
SELECT
address AS number,
date_sent AS timestamp,
type as incoming,
body AS text
body AS text
FROM sms;
"""
class SMS(AndroidExtraction):
"""This module extracts all SMS messages containing links."""
@@ -62,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"])
@@ -77,7 +78,7 @@ class SMS(AndroidExtraction):
"""
conn = sqlite3.connect(db_path)
cur = conn.cursor()
if (self.SMS_DB_TYPE == 1):
cur.execute(SMS_BUGLE_QUERY)
elif (self.SMS_DB_TYPE == 2):

View File

@@ -16,6 +16,7 @@ log = logging.getLogger(__name__)
WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db"
class Whatsapp(AndroidExtraction):
"""This module extracts all WhatsApp messages containing links."""
@@ -39,7 +40,7 @@ class Whatsapp(AndroidExtraction):
return
for message in self.results:
if not "data" in message:
if "data" not in message:
continue
message_links = check_for_links(message["data"])

View File

@@ -5,4 +5,4 @@
from .sms import SMS
BACKUP_MODULES = [SMS,]
BACKUP_MODULES = [SMS]

View File

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

@@ -12,6 +12,7 @@ from .url import URL
class IndicatorsFileBadFormat(Exception):
pass
class Indicators:
"""This class is used to parse indicators from a STIX2 file and provide
functions to compare extracted artifacts to the indicators.
@@ -27,13 +28,27 @@ class Indicators:
self.ioc_files = []
self.ioc_files_sha256 = []
self.ioc_app_ids = []
self.ios_profile_ids = []
self.ioc_count = 0
self._check_env_variable()
def _add_indicator(self, ioc, iocs_list):
if ioc not in iocs_list:
iocs_list.append(ioc)
self.ioc_count += 1
def _check_env_variable(self):
"""
Checks if a variable MVT_STIX2 contains path to STIX Files
"""
if "MVT_STIX2" in os.environ:
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 parse_stix2(self, file_path):
"""Extract indicators from a STIX2 file.
@@ -74,6 +89,9 @@ class Indicators:
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)
@@ -115,7 +133,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:
@@ -231,7 +249,7 @@ class Indicators:
return False
def check_file(self, file_path) -> bool:
def check_filename(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
@@ -246,7 +264,39 @@ class Indicators:
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
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

View File

@@ -16,7 +16,7 @@ def logo():
try:
latest_version = check_for_updates()
except:
except Exception:
pass
else:
if latest_version:

View File

@@ -10,18 +10,19 @@ import re
import simplejson as json
from .indicators import Indicators
class DatabaseNotFoundError(Exception):
pass
class DatabaseCorruptedError(Exception):
pass
class InsufficientPrivileges(Exception):
pass
class MVTModule(object):
"""This class provides a base for all extraction modules."""

View File

@@ -250,6 +250,7 @@ SHORTENER_DOMAINS = [
"zz.gd",
]
class URL:
def __init__(self, url):
@@ -273,7 +274,7 @@ class URL:
# 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):
@@ -288,7 +289,7 @@ class URL:
# 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) -> bool:

View File

@@ -45,7 +45,7 @@ def convert_chrometime_to_unix(timestamp):
: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
@@ -64,6 +64,7 @@ def convert_timestamp_to_iso(timestamp):
except Exception:
return None
def check_for_links(text):
"""Checks if a given text contains HTTP links.
@@ -74,6 +75,7 @@ def check_for_links(text):
"""
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.
@@ -88,6 +90,7 @@ 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):

View File

@@ -6,7 +6,8 @@
import requests
from packaging import version
MVT_VERSION = "1.2.12"
MVT_VERSION = "1.3.2"
def check_for_updates():
res = requests.get("https://pypi.org/pypi/mvt/json")

View File

@@ -10,7 +10,9 @@ import click
from rich.logging import RichHandler
from rich.prompt import Prompt
from mvt.common.help import *
from mvt.common.help import HELP_MSG_MODULE, HELP_MSG_IOC
from mvt.common.help import HELP_MSG_FAST, HELP_MSG_OUTPUT
from mvt.common.help import HELP_MSG_LIST_MODULES
from mvt.common.indicators import Indicators, IndicatorsFileBadFormat
from mvt.common.logo import logo
from mvt.common.module import run_module, save_timeline
@@ -30,6 +32,7 @@ log = logging.getLogger(__name__)
# Set this environment variable to a password if needed.
PASSWD_ENV = "MVT_IOS_BACKUP_PASSWORD"
#==============================================================================
# Main
#==============================================================================
@@ -172,7 +175,7 @@ def check_backup(ctx, iocs, output, fast, backup_path, list_modules, module):
log=logging.getLogger(backup_module.__module__))
m.is_backup = True
if iocs:
if indicators.ioc_count > 0:
m.indicators = indicators
m.indicators.log = m.log

View File

@@ -14,6 +14,7 @@ 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.

View File

@@ -1,15 +1,19 @@
# 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."""
@@ -19,23 +23,73 @@ class ConfigurationProfiles(IOSExtraction):
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:
conf_plist = plistlib.load(handle)
try:
conf_plist = plistlib.load(handle)
except:
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

@@ -28,8 +28,8 @@ class Manifest(IOSExtraction):
"""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:
:param key:
:param dictionary:
:param key:
"""
return dictionary.get(key.encode("utf-8"), None) or dictionary.get(key, None)
@@ -38,7 +38,7 @@ class Manifest(IOSExtraction):
def _convert_timestamp(timestamp_or_unix_time_int):
"""Older iOS versions stored the manifest times as unix timestamps.
:param timestamp_or_unix_time_int:
:param timestamp_or_unix_time_int:
"""
if isinstance(timestamp_or_unix_time_int, datetime.datetime):
@@ -72,7 +72,7 @@ class Manifest(IOSExtraction):
return
for result in self.results:
if not "relative_path" in result:
if "relative_path" not in result:
continue
if not result["relative_path"]:
continue
@@ -83,7 +83,7 @@ class Manifest(IOSExtraction):
self.detected.append(result)
continue
if self.indicators.check_file(result["relative_path"]):
if self.indicators.check_filename(result["relative_path"]):
self.log.warning("Found a known malicious file at path: %s", result["relative_path"])
self.detected.append(result)
continue
@@ -133,7 +133,7 @@ class Manifest(IOSExtraction):
"owner": self._get_key(file_metadata, "UserID"),
"size": self._get_key(file_metadata, "Size"),
})
except:
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

View File

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

View File

@@ -26,27 +26,28 @@ class IOSExtraction(MVTModule):
self.is_fs_dump = False
self.is_sysdiagnose = False
def _recover_sqlite_db_if_needed(self, file_path):
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.
conn = sqlite3.connect(file_path)
cur = conn.cursor()
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()
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
if not recover:
return
self.log.info("Database at path %s is malformed. Trying to recover...", file_path)

View File

@@ -3,10 +3,10 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from .analytics import Analytics
from .cache_files import CacheFiles
from .filesystem import Filesystem
from .net_netusage import Netusage
from .networking_analytics import NetworkingAnalytics
from .safari_favicon import SafariFavicon
from .shutdownlog import ShutdownLog
from .version_history import IOSVersionHistory
@@ -14,6 +14,6 @@ from .webkit_indexeddb import WebkitIndexedDB
from .webkit_localstorage import WebkitLocalStorage
from .webkit_safariviewservice import WebkitSafariViewService
FS_MODULES = [CacheFiles, Filesystem, Netusage, NetworkingAnalytics, SafariFavicon, ShutdownLog,
FS_MODULES = [CacheFiles, Filesystem, Netusage, Analytics, SafariFavicon, ShutdownLog,
IOSVersionHistory, WebkitIndexedDB, WebkitLocalStorage,
WebkitSafariViewService,]
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

@@ -38,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)
@@ -54,7 +54,7 @@ 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:

View File

@@ -38,7 +38,11 @@ class Filesystem(IOSExtraction):
for result in self.results:
if self.indicators.check_file(result["path"]):
self.log.warning("Found a known malicious file at path: %s", 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.
@@ -60,7 +64,7 @@ class Filesystem(IOSExtraction):
"path": os.path.relpath(dir_path, self.base_folder),
"modified": convert_timestamp_to_iso(datetime.datetime.utcfromtimestamp(os.stat(dir_path).st_mtime)),
}
except:
except Exception:
continue
else:
self.results.append(result)
@@ -72,7 +76,7 @@ class Filesystem(IOSExtraction):
"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

@@ -12,6 +12,7 @@ NETUSAGE_ROOT_PATHS = [
"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.

View File

@@ -1,91 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import plistlib
import sqlite3
from mvt.common.utils import convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
NETWORKING_ANALYTICS_DB_PATH = [
"private/var/Keychains/Analytics/networking_analytics.db",
]
class NetworkingAnalytics(IOSExtraction):
"""This module extracts information from the networking_analytics.db file."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
return {
"timestamp": record["timestamp"],
"module": self.__class__.__name__,
"event": "network_crash",
"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 known malicious process \"%s\" in networking_analytics.db at %s",
ioc, result["timestamp"])
self.detected.append(result)
break
def _extract_networking_analytics_data(self):
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
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
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 network crashes", len(self.results))
def run(self):
self._find_ios_database(root_paths=NETWORKING_ANALYTICS_DB_PATH)
if self.file_path:
self.log.info("Found networking_analytics.db log at path: %s", self.file_path)
self._extract_networking_analytics_data()
else:
self.log.info("networking_analytics.db not found")

View File

@@ -14,6 +14,7 @@ SAFARI_FAVICON_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/Image Cache/Favicons/Favicons.db",
]
class SafariFavicon(IOSExtraction):
"""This module extracts all Safari favicon records."""

View File

@@ -11,6 +11,7 @@ SHUTDOWN_LOG_PATH = [
"private/var/db/diagnostics/shutdown.log",
]
class ShutdownLog(IOSExtraction):
"""This module extracts processes information from the shutdown log file."""
@@ -27,7 +28,7 @@ class ShutdownLog(IOSExtraction):
"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
@@ -57,7 +58,7 @@ class ShutdownLog(IOSExtraction):
try:
start = line.find(" @")+2
mac_timestamp = int(line[start:start+10])
except:
except Exception:
mac_timestamp = 0
timestamp = convert_mactime_to_unix(mac_timestamp, from_2001=False)

View File

@@ -14,6 +14,7 @@ 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."""

View File

@@ -9,6 +9,7 @@ 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.

View File

@@ -9,6 +9,7 @@ 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.

View File

@@ -9,6 +9,7 @@ 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.

View File

@@ -22,9 +22,10 @@ from .tcc import TCC
from .webkit_resource_load_statistics import WebkitResourceLoadStatistics
from .webkit_session_resource_log import WebkitSessionResourceLog
from .whatsapp import Whatsapp
from .shortcuts import Shortcuts
MIXED_MODULES = [Calls, ChromeFavicon, ChromeHistory, Contacts, FirefoxFavicon,
FirefoxHistory, IDStatusCache, InteractionC, LocationdClients,
OSAnalyticsADDaily, Datausage, SafariBrowserState, SafariHistory,
TCC, SMS, SMSAttachments, WebkitResourceLoadStatistics,
WebkitSessionResourceLog, Whatsapp,]
WebkitSessionResourceLog, Whatsapp, Shortcuts]

View File

@@ -16,6 +16,7 @@ CALLS_ROOT_PATHS = [
"private/var/mobile/Library/CallHistoryDB/CallHistory.storedata"
]
class Calls(IOSExtraction):
"""This module extracts phone calls details"""
@@ -45,7 +46,7 @@ class Calls(IOSExtraction):
ZDATE, ZDURATION, ZLOCATION, ZADDRESS, ZSERVICE_PROVIDER
FROM ZCALLRECORD;
""")
names = [description[0] for description in cur.description]
# names = [description[0] for description in cur.description]
for row in cur:
self.results.append({

View File

@@ -19,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."""

View File

@@ -13,12 +13,12 @@ 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."""

View File

@@ -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."""

View File

@@ -17,6 +17,7 @@ FIREFOX_HISTORY_ROOT_PATHS = [
"private/var/mobile/profile.profile/browser.db",
]
class FirefoxFavicon(IOSExtraction):
"""This module extracts all Firefox favicon"""
@@ -39,8 +40,8 @@ class FirefoxFavicon(IOSExtraction):
return
for result in self.results:
if (self.indicators.check_domain(result.get("url", "")) or
self.indicators.check_domain(result.get("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):

View File

@@ -17,6 +17,7 @@ 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.

View File

@@ -18,6 +18,7 @@ IDSTATUSCACHE_ROOT_PATHS = [
"private/var/mobile/Library/IdentityServices/idstatuscache.plist",
]
class IDStatusCache(IOSExtraction):
"""Extracts Apple Authentication information from idstatuscache.plist"""
@@ -91,5 +92,5 @@ class IDStatusCache(IOSExtraction):
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

@@ -16,6 +16,7 @@ INTERACTIONC_ROOT_PATHS = [
"private/var/mobile/Library/CoreDuet/People/interactionC.db",
]
class InteractionC(IOSExtraction):
"""This module extracts data from InteractionC db."""
@@ -54,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])
@@ -123,8 +124,7 @@ class InteractionC(IOSExtraction):
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 row in cur:
self.results.append({

View File

@@ -17,6 +17,7 @@ LOCATIOND_ROOT_PATHS = [
"private/var/root/Library/Caches/locationd/clients.plist"
]
class LocationdClients(IOSExtraction):
"""Extract information from apps who used geolocation."""

View File

@@ -12,6 +12,7 @@ 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.

View File

@@ -16,6 +16,7 @@ OSANALYTICS_ADDAILY_ROOT_PATHS = [
"private/var/mobile/Library/Preferences/com.apple.osanalytics.addaily.plist",
]
class OSAnalyticsADDaily(IOSExtraction):
"""Extract network usage information by process, from com.apple.osanalytics.addaily.plist"""
@@ -34,14 +35,14 @@ class OSAnalyticsADDaily(IOSExtraction):
"event": "osanalytics_addaily",
"data": record_data,
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if self.indicators.check_process(result["package"]):
self.detected.append(result)
self.detected.append(result)
def run(self):
self._find_ios_database(backup_ids=OSANALYTICS_ADDAILY_BACKUP_IDS,

View File

@@ -19,6 +19,7 @@ SAFARI_BROWSER_STATE_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/Safari/BrowserState.db",
]
class SafariBrowserState(IOSExtraction):
"""This module extracts all Safari browser state records."""
@@ -47,7 +48,7 @@ class SafariBrowserState(IOSExtraction):
self.detected.append(result)
continue
if not "session_data" in result:
if "session_data" not in result:
continue
for session_entry in result["session_data"]:
@@ -58,17 +59,26 @@ class SafariBrowserState(IOSExtraction):
conn = sqlite3.connect(db_path)
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;
""")
try:
cur.execute("""
SELECT
tabs.title,
tabs.url,
tabs.user_visible_url,
tabs.last_viewed_time,
tab_sessions.session_data
FROM tabs
JOIN tab_sessions ON tabs.uuid = tab_sessions.tab_uuid
ORDER BY tabs.last_viewed_time;
""")
except sqlite3.OperationalError:
# Old version iOS <12 likely
cur.execute("""
SELECT
title, url, user_visible_url, last_viewed_time, session_data
FROM tabs
ORDER BY last_viewed_time;
""")
for row in cur:
session_entries = []

View File

@@ -17,6 +17,7 @@ SAFARI_HISTORY_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/Safari/History.db",
]
class SafariHistory(IOSExtraction):
"""This module extracts all Safari visits and tries to detect potential
network injection attacks.
@@ -62,7 +63,7 @@ class SafariHistory(IOSExtraction):
continue
self.log.info("Found HTTP redirect to different domain: \"%s\" -> \"%s\"",
origin_domain, redirect_domain)
origin_domain, redirect_domain)
redirect_time = convert_mactime_to_unix(redirect["timestamp"])
origin_time = convert_mactime_to_unix(result["timestamp"])

View File

@@ -0,0 +1,103 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import sqlite3
import io
import plistlib
import itertools
from mvt.common.utils import check_for_links, convert_mactime_to_unix, convert_timestamp_to_iso
from ..base import IOSExtraction
SHORTCUT_BACKUP_IDS = [
"5b4d0b44b5990f62b9f4d34ad8dc382bf0b01094",
]
SHORTCUT_ROOT_PATHS = [
"private/var/mobile/Library/Shortcuts/Shortcuts.sqlite",
]
class Shortcuts(IOSExtraction):
"""This module extracts all info about SMS/iMessage attachments."""
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
super().__init__(file_path=file_path, base_folder=base_folder,
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
found_urls = ""
if record["action_urls"]:
found_urls = "- URLs in actions: {}".format(", ".join(record["action_urls"]))
return {
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "shortcut",
"data": f"iOS Shortcut '{record['shortcut_name']}': {record['description']} {found_urls}"
}
def check_indicators(self):
if not self.indicators:
return
for action in self.results:
if self.indicators.check_domains(action["action_urls"]):
self.detected.append(action)
def run(self):
self._find_ios_database(backup_ids=SHORTCUT_BACKUP_IDS,
root_paths=SHORTCUT_ROOT_PATHS)
self.log.info("Found Shortcuts database at path: %s", self.file_path)
conn = sqlite3.connect(self.file_path)
conn.text_factory = bytes
cur = conn.cursor()
cur.execute("""
SELECT
ZSHORTCUT.Z_PK as "shortcut_id",
ZSHORTCUT.ZNAME as "shortcut_name",
ZSHORTCUT.ZCREATIONDATE as "created_date",
ZSHORTCUT.ZMODIFICATIONDATE as "modified_date",
ZSHORTCUT.ZACTIONSDESCRIPTION as "description",
ZSHORTCUTACTIONS.ZDATA as "action_data"
FROM ZSHORTCUT
LEFT JOIN ZSHORTCUTACTIONS ON ZSHORTCUTACTIONS.ZSHORTCUT == ZSHORTCUT.Z_PK;
""")
names = [description[0] for description in cur.description]
for item in cur:
shortcut = {}
# We store the value of each column under the proper key.
for index, value in enumerate(item):
shortcut[names[index]] = value
action_data = plistlib.load(io.BytesIO(shortcut.pop("action_data", [])))
actions = []
for action_entry in action_data:
action = {}
action["identifier"] = action_entry["WFWorkflowActionIdentifier"]
action["parameters"] = action_entry["WFWorkflowActionParameters"]
# URLs might be in multiple fields, do a simple regex search across the parameters
extracted_urls = check_for_links(str(action["parameters"]))
# Remove quoting characters that may have been captured by the regex
action["urls"] = [url.rstrip("',") for url in extracted_urls]
actions.append(action)
# pprint.pprint(actions)
shortcut["isodate"] = convert_timestamp_to_iso(convert_mactime_to_unix(shortcut.pop("created_date")))
shortcut["modified_date"] = convert_timestamp_to_iso(convert_mactime_to_unix(shortcut["modified_date"]))
shortcut["parsed_actions"] = len(actions)
shortcut["action_urls"] = list(itertools.chain(*[action["urls"] for action in actions]))
self.results.append(shortcut)
cur.close()
conn.close()
self.log.info("Extracted a total of %d Shortcuts", len(self.results))

View File

@@ -18,6 +18,7 @@ SMS_ROOT_PATHS = [
"private/var/mobile/Library/SMS/sms.db",
]
class SMS(IOSExtraction):
"""This module extracts all SMS messages containing links."""
@@ -50,25 +51,44 @@ class SMS(IOSExtraction):
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;
""")
try:
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
message.*,
handle.id as "phone_number"
FROM message, handle
WHERE handle.rowid = message.handle_id;
""")
# Force the query early to catch database issues
items = list(cur)
except sqlite3.DatabaseError as e:
conn.close()
if "database disk image is malformed" in str(e):
self._recover_sqlite_db_if_needed(self.file_path, forced=True)
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("""
SELECT
message.*,
handle.id as "phone_number"
FROM message, handle
WHERE handle.rowid = message.handle_id;
""")
items = list(cur)
else:
raise e
names = [description[0] for description in cur.description]
for item in cur:
for item in items:
message = {}
for index, value in enumerate(item):
# We base64 escape some of the attributes that could contain
# binary data.
if (names[index] == "attributedBody" or
names[index] == "payload_data" or
names[index] == "message_summary_info") and value:
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.
@@ -82,12 +102,16 @@ class SMS(IOSExtraction):
if not message.get("text", None):
message["text"] = ""
# Extract links from the SMS message.
message_links = check_for_links(message.get("text", ""))
# If we find links in the messages or if they are empty we add them to the list.
if message_links or message.get("text", "").strip() == "":
if message.get("text", "").startswith("ALERT: State-sponsored attackers may be targeting your iPhone"):
self.log.warn("Apple warning about state-sponsored attack received on the %s", message["isodate"])
self.results.append(message)
else:
# Extract links from the SMS message.
message_links = check_for_links(message.get("text", ""))
# If we find links in the messages or if they are empty we add them to the list.
if message_links or message.get("text", "").strip() == "":
self.results.append(message)
cur.close()
conn.close()

View File

@@ -17,6 +17,7 @@ SMS_ROOT_PATHS = [
"private/var/mobile/Library/SMS/sms.db",
]
class SMSAttachments(IOSExtraction):
"""This module extracts all info about SMS/iMessage attachments."""
@@ -45,7 +46,7 @@ class SMSAttachments(IOSExtraction):
cur.execute("""
SELECT
attachment.ROWID as "attachment_id",
attachment.*,
attachment.*,
message.service as "service",
handle.id as "phone_number"
FROM attachment
@@ -73,7 +74,7 @@ class SMSAttachments(IOSExtraction):
attachment["filename"] = attachment["filename"] or "NULL"
if (attachment["filename"].startswith("/var/tmp/") and attachment["filename"].endswith("-1") and
attachment["direction"] == "received"):
attachment["direction"] == "received"):
self.log.warn(f"Suspicious iMessage attachment '{attachment['filename']}' on {attachment['isodate']}")
self.detected.append(attachment)

View File

@@ -17,6 +17,11 @@ TCC_ROOT_PATHS = [
"private/var/mobile/Library/TCC/TCC.db",
]
AUTH_VALUE_OLD = {
0: "denied",
1: "allowed"
}
AUTH_VALUES = {
0: "denied",
1: "unknown",
@@ -38,6 +43,7 @@ AUTH_REASONS = {
12: "app_type_policy",
}
class TCC(IOSExtraction):
"""This module extracts records from the TCC.db SQLite database."""
@@ -47,37 +53,96 @@ class TCC(IOSExtraction):
output_folder=output_folder, fast_mode=fast_mode,
log=log, results=results)
def serialize(self, record):
if "last_modified" in record:
if "allowed_value" in record:
msg = f"Access to {record['service']} by {record['client']} {record['allowed_value']}"
else:
msg = f"Access to {record['service']} by {record['client']} {record['auth_value']}"
return {
"timestamp": record["last_modified"],
"module": self.__class__.__name__,
"event": "AccessRequest",
"data": msg
}
def process_db(self, file_path):
conn = sqlite3.connect(file_path)
cur = conn.cursor()
cur.execute("""SELECT
service, client, client_type, auth_value, auth_reason, last_modified
FROM access;""")
db_version = "v3"
try:
cur.execute("""SELECT
service, client, client_type, auth_value, auth_reason, last_modified
FROM access;""")
except sqlite3.OperationalError:
# v2 version
try:
cur.execute("""SELECT
service, client, client_type, allowed, prompt_count, last_modified
FROM access;""")
db_version = "v2"
except sqlite3.OperationalError:
cur.execute("""SELECT
service, client, client_type, allowed, prompt_count
FROM access;""")
db_version = "v1"
for row in cur:
service = row[0]
client = row[1]
client_type = row[2]
client_type_desc = "bundle_id" if client_type == 0 else "absolute_path"
auth_value = row[3]
auth_value_desc = AUTH_VALUES.get(auth_value, "")
auth_reason = row[4]
auth_reason_desc = AUTH_REASONS.get(auth_reason, "unknown")
last_modified = convert_timestamp_to_iso(datetime.utcfromtimestamp((row[5])))
if db_version == "v3":
auth_value = row[3]
auth_value_desc = AUTH_VALUES.get(auth_value, "")
auth_reason = row[4]
auth_reason_desc = AUTH_REASONS.get(auth_reason, "unknown")
last_modified = convert_timestamp_to_iso(datetime.utcfromtimestamp((row[5])))
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
self.log.info("Found client \"%s\" with access %s to %s on %s by %s",
client, auth_value_desc, device, last_modified, auth_reason_desc)
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
self.log.info("Found client \"%s\" with access %s to %s on %s by %s",
client, auth_value_desc, device, last_modified, auth_reason_desc)
self.results.append({
"service": service,
"client": client,
"client_type": client_type_desc,
"auth_value": auth_value_desc,
"auth_reason_desc": auth_reason_desc,
"last_modified": last_modified,
})
self.results.append({
"service": service,
"client": client,
"client_type": client_type_desc,
"auth_value": auth_value_desc,
"auth_reason_desc": auth_reason_desc,
"last_modified": last_modified,
})
else:
allowed_value = row[3]
allowed_desc = AUTH_VALUE_OLD.get(allowed_value, "")
prompt_count = row[4]
if db_version == "v2":
last_modified = convert_timestamp_to_iso(datetime.utcfromtimestamp((row[5])))
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
self.log.info("Found client \"%s\" with access %s to %s at %s",
client, allowed_desc, device, last_modified)
self.results.append({
"service": service,
"client": client,
"client_type": client_type_desc,
"allowed_value": allowed_desc,
"prompt_count": prompt_count,
"last_modified": last_modified
})
else:
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
self.log.info("Found client \"%s\" with access %s to %s",
client, allowed_desc, device)
self.results.append({
"service": service,
"client": client,
"client_type": client_type_desc,
"allowed_value": allowed_desc,
"prompt_count": prompt_count
})
cur.close()
conn.close()

View File

@@ -17,6 +17,7 @@ WEBKIT_RESOURCELOADSTATICS_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/observations.db",
]
class WebkitResourceLoadStatistics(IOSExtraction):
"""This module extracts records from WebKit ResourceLoadStatistics observations.db."""
# TODO: Add serialize().
@@ -38,7 +39,7 @@ class WebkitResourceLoadStatistics(IOSExtraction):
for item in items:
if self.indicators.check_domain(item["registrable_domain"]):
if key not in self.detected:
self.detected[key] = [item,]
self.detected[key] = [item, ]
else:
self.detected[key].append(item)
@@ -55,7 +56,7 @@ class WebkitResourceLoadStatistics(IOSExtraction):
except sqlite3.OperationalError:
return
if not key in self.results:
if key not in self.results:
self.results[key] = []
for row in cur:

View File

@@ -20,6 +20,7 @@ WEBKIT_SESSION_RESOURCE_LOG_ROOT_PATHS = [
"private/var/mobile/Library/WebClips/*/Storage/full_browsing_session_resourceLog.plist",
]
class WebkitSessionResourceLog(IOSExtraction):
"""This module extracts records from WebKit browsing session
resource logs, and checks them against any provided list of

View File

@@ -20,6 +20,7 @@ WHATSAPP_ROOT_PATHS = [
"private/var/mobile/Containers/Shared/AppGroup/*/ChatStorage.sqlite",
]
class Whatsapp(IOSExtraction):
"""This module extracts all WhatsApp messages containing links."""
@@ -31,11 +32,14 @@ class Whatsapp(IOSExtraction):
def serialize(self, record):
text = record.get("ZTEXT", "").replace("\n", "\\n")
links_text = ""
if record["links"]:
links_text = " - Embedded links: " + ", ".join(record["links"])
return {
"timestamp": record.get("isodate"),
"module": self.__class__.__name__,
"event": "message",
"data": f"{text} from {record.get('ZFROMJID', 'Unknown')}",
"data": f"\'{text}\' from {record.get('ZFROMJID', 'Unknown')}{links_text}",
}
def check_indicators(self):
@@ -43,8 +47,7 @@ class Whatsapp(IOSExtraction):
return
for message in self.results:
message_links = check_for_links(message.get("ZTEXT", ""))
if self.indicators.check_domains(message_links):
if self.indicators.check_domains(message["links"]):
self.detected.append(message)
def run(self):
@@ -54,26 +57,49 @@ class Whatsapp(IOSExtraction):
conn = sqlite3.connect(self.file_path)
cur = conn.cursor()
cur.execute("SELECT * FROM ZWAMESSAGE;")
# Query all messages and join tables which can contain media attachments and links
cur.execute("""
SELECT
ZWAMESSAGE.*,
ZWAMEDIAITEM.ZAUTHORNAME,
ZWAMEDIAITEM.ZMEDIAURL,
ZWAMESSAGEDATAITEM.ZCONTENT1,
ZWAMESSAGEDATAITEM.ZCONTENT2,
ZWAMESSAGEDATAITEM.ZMATCHEDTEXT,
ZWAMESSAGEDATAITEM.ZSUMMARY,
ZWAMESSAGEDATAITEM.ZTITLE
FROM ZWAMESSAGE
LEFT JOIN ZWAMEDIAITEM ON ZWAMEDIAITEM.ZMESSAGE = ZWAMESSAGE.Z_PK
LEFT JOIN ZWAMESSAGEDATAITEM ON ZWAMESSAGEDATAITEM.ZMESSAGE = ZWAMESSAGE.Z_PK;
""")
names = [description[0] for description in cur.description]
for message in cur:
new_message = {}
for index, value in enumerate(message):
new_message[names[index]] = value
for message_row in cur:
message = {}
for index, value in enumerate(message_row):
message[names[index]] = value
if not new_message.get("ZTEXT", None):
continue
message["isodate"] = convert_timestamp_to_iso(convert_mactime_to_unix(message.get("ZMESSAGEDATE")))
message["ZTEXT"] = message["ZTEXT"] if message["ZTEXT"] else ""
# We convert Mac's silly timestamp again.
new_message["isodate"] = convert_timestamp_to_iso(convert_mactime_to_unix(new_message.get("ZMESSAGEDATE")))
# Extract links from the WhatsApp message. URLs can be stored in multiple fields/columns. Check each of them!
message_links = []
fields_with_links = ["ZTEXT", "ZMATCHEDTEXT", "ZMEDIAURL", "ZCONTENT1", "ZCONTENT2"]
for field in fields_with_links:
if message.get(field):
message_links.extend(check_for_links(message.get(field, "")))
# Extract links from the WhatsApp message.
message_links = check_for_links(new_message["ZTEXT"])
# Remove WhatsApp internal media URLs
filtered_links = []
for link in message_links:
if not (link.startswith("https://mmg-fna.whatsapp.net/") or link.startswith("https://mmg.whatsapp.net/")):
filtered_links.append(link)
# If we find messages, 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)
# If we find messages with links, or if there's an empty message we add it to the results list.
if filtered_links or (message.get("ZTEXT") or "").strip() == "":
message["links"] = list(set(filtered_links))
self.results.append(message)
cur.close()
conn.close()

View File

@@ -39,7 +39,9 @@ class NetBase(IOSExtraction):
ZLIVEUSAGE.ZHASPROCESS,
ZLIVEUSAGE.ZTIMESTAMP
FROM ZLIVEUSAGE
LEFT JOIN ZPROCESS ON ZLIVEUSAGE.ZHASPROCESS = ZPROCESS.Z_PK;
LEFT JOIN ZPROCESS ON ZLIVEUSAGE.ZHASPROCESS = ZPROCESS.Z_PK
UNION
SELECT ZFIRSTTIMESTAMP, ZTIMESTAMP, ZPROCNAME, ZBUNDLENAME, Z_PK, NULL, NULL, NULL, NULL, NULL, NULL, NULL FROM ZPROCESS WHERE Z_PK NOT IN (SELECT ZHASPROCESS FROM ZLIVEUSAGE);
""")
for row in cur:
@@ -68,7 +70,7 @@ class NetBase(IOSExtraction):
"wwan_out": row[8],
"live_id": row[9],
"live_proc_id": row[10],
"live_isodate": live_timestamp,
"live_isodate": live_timestamp if row[10] else first_isodate,
})
cur.close()
@@ -79,7 +81,7 @@ class NetBase(IOSExtraction):
def serialize(self, record):
record_data = f"{record['proc_name']} (Bundle ID: {record['bundle_id']}, ID: {record['proc_id']})"
record_data_usage = record_data + f" WIFI IN: {record['wifi_in']}, WIFI OUT: {record['wifi_out']} - " \
f"WWAN IN: {record['wwan_in']}, WWAN OUT: {record['wwan_out']}"
f"WWAN IN: {record['wwan_in']}, WWAN OUT: {record['wwan_out']}"
records = [{
"timestamp": record["live_isodate"],
@@ -89,7 +91,7 @@ class NetBase(IOSExtraction):
}]
# Only included first_usage and current_usage records when a ZPROCESS entry exists.
if "MANIPULATED" not in record["proc_name"] and "MISSING" not in record["proc_name"]:
if "MANIPULATED" not in record["proc_name"] and "MISSING" not in record["proc_name"] and record["live_proc_id"] is not None:
records.extend([
{
"timestamp": record["first_isodate"],
@@ -151,6 +153,8 @@ class NetBase(IOSExtraction):
msg = msg + " (However, the process name might have been truncated in the database)"
self.log.warning(msg)
if not proc["live_proc_id"]:
self.log.info(f"Found process entry in ZPROCESS but not in ZLIVEUSAGE : {proc['proc_name']} at {proc['live_isodate']}")
def check_manipulated(self):
"""Check for missing or manipulate DB entries"""

View File

@@ -38,6 +38,10 @@ IPHONE_MODELS = [
{"identifier": "iPhone13,2", "description": "iPhone 12"},
{"identifier": "iPhone13,3", "description": "iPhone 12 Pro"},
{"identifier": "iPhone13,4", "description": "iPhone 12 Pro Max"},
{"identifier": "iPhone14,4", "description": "iPhone 13 Mini"},
{"identifier": "iPhone14,5", "description": "iPhone 13"},
{"identifier": "iPhone14,2", "description": "iPhone 13 Pro"},
{"identifier": "iPhone14,3", "description": "iPhone 13 Pro Max"},
]
IPHONE_IOS_VERSIONS = [
@@ -223,14 +227,22 @@ IPHONE_IOS_VERSIONS = [
{"build": "18G69", "version": "14.7"},
{"build": "18G82", "version": "14.7.1"},
{"build": "18H17", "version": "14.8"},
{"build": "18H107", "version": "14.8.1"},
{"build": "19A341", "version": "15.0"},
{"build": "19A346", "version": "15.0"},
{"build": "19A348", "version": "15.0.1"},
{"build": "19A404", "version": "15.0.2"},
{"build": "19B74", "version": "15.1"},
{"build": "19B81", "version": "15.1.1"},
]
def get_device_desc_from_id(identifier, devices_list=IPHONE_MODELS):
for model in IPHONE_MODELS:
if identifier == model["identifier"]:
return model["description"]
def find_version_by_build(build):
build = build.upper()
for version in IPHONE_IOS_VERSIONS:

View File

@@ -26,10 +26,11 @@ requires = (
# iOS dependencies:
"iOSbackup>=0.9.921",
# Android dependencies:
"adb-shell>=0.4.1",
"adb-shell>=0.4.2",
"libusb1>=2.0.1",
)
def get_package_data(package):
walk = [(dirpath.replace(package + os.sep, "", 1), filenames)
for dirpath, dirnames, filenames in os.walk(package)
@@ -41,6 +42,7 @@ def get_package_data(package):
for filename in filenames])
return {package: filepaths}
setup(
name="mvt",
version=MVT_VERSION,