diff --git a/mvt/ios/modules/fs/analytics.py b/mvt/ios/modules/fs/analytics.py index ec02610..742418d 100644 --- a/mvt/ios/modules/fs/analytics.py +++ b/mvt/ios/modules/fs/analytics.py @@ -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/ +import copy import logging import plistlib import sqlite3 -import copy from typing import Optional, Union from mvt.common.utils import convert_mactime_to_iso diff --git a/mvt/ios/modules/fs/filesystem.py b/mvt/ios/modules/fs/filesystem.py index 4aff021..7c5d6f3 100644 --- a/mvt/ios/modules/fs/filesystem.py +++ b/mvt/ios/modules/fs/filesystem.py @@ -15,8 +15,6 @@ from ..base import IOSExtraction class Filesystem(IOSExtraction): """This module extracts creation and modification date of files from a full file-system dump. - - """ def __init__( diff --git a/mvt/ios/modules/mixed/__init__.py b/mvt/ios/modules/mixed/__init__.py index 86c2a11..70cb2f4 100644 --- a/mvt/ios/modules/mixed/__init__.py +++ b/mvt/ios/modules/mixed/__init__.py @@ -4,6 +4,7 @@ # https://license.mvt.re/1.1/ from .applications import Applications +from .calendar import Calendar from .calls import Calls from .chrome_favicon import ChromeFavicon from .chrome_history import ChromeHistory @@ -29,4 +30,5 @@ MIXED_MODULES = [Calls, ChromeFavicon, ChromeHistory, Contacts, FirefoxFavicon, FirefoxHistory, IDStatusCache, InteractionC, LocationdClients, OSAnalyticsADDaily, Datausage, SafariBrowserState, SafariHistory, TCC, SMS, SMSAttachments, WebkitResourceLoadStatistics, - WebkitSessionResourceLog, Whatsapp, Shortcuts, Applications] + WebkitSessionResourceLog, Whatsapp, Shortcuts, Applications, + Calendar] diff --git a/mvt/ios/modules/mixed/applications.py b/mvt/ios/modules/mixed/applications.py index 7b255bb..0e0d23a 100644 --- a/mvt/ios/modules/mixed/applications.py +++ b/mvt/ios/modules/mixed/applications.py @@ -3,16 +3,15 @@ # 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 plistlib import hashlib +import logging +import os +import plistlib from datetime import datetime, timezone -from typing import Optional, Union, Dict, Any +from typing import Any, Dict, Optional, Union -from mvt.common.utils import convert_datetime_to_iso from mvt.common.module import DatabaseNotFoundError - +from mvt.common.utils import convert_datetime_to_iso from mvt.ios.modules.base import IOSExtraction APPLICATIONS_DB_PATH = [ diff --git a/mvt/ios/modules/mixed/calendar.py b/mvt/ios/modules/mixed/calendar.py new file mode 100644 index 0000000..4813800 --- /dev/null +++ b/mvt/ios/modules/mixed/calendar.py @@ -0,0 +1,136 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 Claudio Guarnieri. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging +import sqlite3 +from typing import Optional, Union + +from mvt.common.utils import convert_mactime_to_iso + +from ..base import IOSExtraction + +CALENDAR_BACKUP_IDS = [ + "2041457d5fe04d39d0ab481178355df6781e6858", +] +CALENDAR_ROOT_PATHS = [ + "private/var/mobile/Library/Calendar/Calendar.sqlitedb" +] + + +class Calendar(IOSExtraction): + """This module extracts all calendar entries.""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + fast_mode: Optional[bool] = False, + log: logging.Logger = logging.getLogger(__name__), + results: Optional[list] = None + ) -> None: + super().__init__(file_path=file_path, target_path=target_path, + results_path=results_path, fast_mode=fast_mode, + log=log, results=results) + self.timestamps = [ + "start_date", + "end_date", + "last_modified", + "creation_date", + "participant_last_modified" + ] + + def serialize(self, record: dict) -> Union[dict, list]: + records = [] + for timestamp in self.timestamps: + if timestamp not in record or not record[timestamp]: + continue + + records.append({ + "timestamp": record[timestamp], + "module": self.__class__.__name__, + "event": timestamp, + "data": f"Calendar event {record['summary']} ({record['description']}) " + f"(invitation by {record['participant_email']})" + }) + return records + + def check_indicators(self) -> None: + for result in self.results: + if result["participant_email"]: + ioc = self.indicators.check_email(result["participant_email"]) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + # Custom check for Quadream exploit + if result["summary"] == "Meeting" and result["description"] == "Notes": + self.log.warning("Potential Quadream exploit event identified: %s", result["uuid"]) + self.detected.append(result) + + def _parse_calendar_db(self): + """ + Parse the calendar database + """ + conn = sqlite3.connect(self.file_path) + cur = conn.cursor() + + cur.execute(""" + SELECT + CalendarItem.ROWID as "id", + CalendarItem.summary as "summary", + CalendarItem.description as "description", + CalendarItem.start_date as "start_date", + CalendarItem.end_date as "end_date", + CalendarItem.all_day as "all_day", + CalendarItem.calendar_id as "calendar_id", + CalendarItem.organizer_id as "organizer_id", + CalendarItem.url as "url", + CalendarItem.last_modified as "last_modified", + CalendarItem.external_id as "external_id", + CalendarItem.external_mod_tag as "external_mod_tag", + CalendarItem.unique_identifier as "unique_identifier", + CalendarItem.hidden as "hidden", + CalendarItem.UUID as "uuid", + CalendarItem.creation_date as "creation_date", + CalendarItem.action as "action", + CalendarItem.created_by_id as "created_by_id", + Participant.UUID as "participant_uuid", + Participant.email as "participant_email", + Participant.phone_number as "participant_phone", + Participant.comment as "participant_comment", + Participant.last_modified as "participant_last_modified" + FROM CalendarItem + LEFT JOIN Participant ON Participant.ROWID = CalendarItem.organizer_id; + """) + + names = [description[0] for description in cur.description] + for item in cur: + entry = {} + for index, value in enumerate(item): + if names[index] in self.timestamps: + if value is None or isinstance(value, str): + entry[names[index]] = value + else: + entry[names[index]] = convert_mactime_to_iso(value) + else: + entry[names[index]] = value + + self.results.append(entry) + + cur.close() + conn.close() + + def run(self) -> None: + self._find_ios_database(backup_ids=CALENDAR_BACKUP_IDS, + root_paths=CALENDAR_ROOT_PATHS) + self.log.info("Found calendar database at path: %s", + self.file_path) + + self._parse_calendar_db() + + self.log.info("Extracted a total of %d calendar items", + len(self.results)) diff --git a/tests/artifacts/ios_backup/20/2041457d5fe04d39d0ab481178355df6781e6858 b/tests/artifacts/ios_backup/20/2041457d5fe04d39d0ab481178355df6781e6858 new file mode 100644 index 0000000..0931e92 Binary files /dev/null and b/tests/artifacts/ios_backup/20/2041457d5fe04d39d0ab481178355df6781e6858 differ diff --git a/tests/ios_backup/test_calendar.py b/tests/ios_backup/test_calendar.py new file mode 100644 index 0000000..75383c6 --- /dev/null +++ b/tests/ios_backup/test_calendar.py @@ -0,0 +1,34 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 Claudio Guarnieri. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging + +from mvt.common.indicators import Indicators +from mvt.common.module import run_module +from mvt.ios.modules.mixed.calendar import Calendar + +from ..utils import get_ios_backup_folder + + +class TestCalendarModule: + + def test_calendar(self): + m = Calendar(target_path=get_ios_backup_folder()) + run_module(m) + assert len(m.results) == 1 + assert len(m.timeline) == 4 + assert len(m.detected) == 0 + assert m.results[0]["summary"] == "Super interesting meeting" + + def test_calendar_detection(self, indicator_file): + m = Calendar(target_path=get_ios_backup_folder()) + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + ind.ioc_collections[0]["emails"].append("user@example.org") + m.indicators = ind + run_module(m) + assert len(m.results) == 1 + assert len(m.timeline) == 4 + assert len(m.detected) == 1 diff --git a/tests/ios_fs/test_filesystem.py b/tests/ios_fs/test_filesystem.py index a8d8b89..c313c99 100644 --- a/tests/ios_fs/test_filesystem.py +++ b/tests/ios_fs/test_filesystem.py @@ -17,8 +17,8 @@ class TestFilesystem: def test_filesystem(self): m = Filesystem(target_path=get_ios_backup_folder()) run_module(m) - assert len(m.results) == 12 - assert len(m.timeline) == 12 + assert len(m.results) == 14 + assert len(m.timeline) == 14 assert len(m.detected) == 0 def test_detection(self, indicator_file): @@ -29,6 +29,6 @@ class TestFilesystem: ind.ioc_collections[0]["processes"].append("64d0019cb3d46bfc8cce545a8ba54b93e7ea9347") m.indicators = ind run_module(m) - assert len(m.results) == 12 - assert len(m.timeline) == 12 + assert len(m.results) == 14 + assert len(m.timeline) == 14 assert len(m.detected) == 1