diff --git a/mvt/common/module.py b/mvt/common/module.py index b1e91f6..9f0873c 100644 --- a/mvt/common/module.py +++ b/mvt/common/module.py @@ -92,9 +92,9 @@ class MVTModule(object): if self.results: results_file_name = f"{name}.json" results_json_path = os.path.join(self.output_folder, results_file_name) - with open(results_json_path, "w") as handle: + with io.open(results_json_path, "w", encoding="utf-8") as handle: try: - json.dump(self.results, handle, indent=4) + json.dump(self.results, handle, indent=4, default=str) except Exception as e: self.log.error("Unable to store results of module %s to file %s: %s", self.__class__.__name__, results_file_name, e) @@ -102,8 +102,8 @@ class MVTModule(object): if self.detected: detected_file_name = f"{name}_detected.json" detected_json_path = os.path.join(self.output_folder, detected_file_name) - with open(detected_json_path, "w") as handle: - json.dump(self.detected, handle, indent=4) + with io.open(detected_json_path, "w", encoding="utf-8") as handle: + json.dump(self.detected, handle, indent=4, default=str) def serialize(self, record): raise NotImplementedError @@ -193,8 +193,8 @@ def save_timeline(timeline, timeline_path): csvoutput.writerow(["UTC Timestamp", "Plugin", "Event", "Description"]) for event in sorted(timeline, key=lambda x: x["timestamp"] if x["timestamp"] is not None else ""): csvoutput.writerow([ - event["timestamp"], - event["module"], - event["event"], - event["data"], + event.get("timestamp"), + event.get("module"), + event.get("event"), + event.get("data"), ]) diff --git a/mvt/ios/modules/backup/__init__.py b/mvt/ios/modules/backup/__init__.py index 882d782..05dd1f5 100644 --- a/mvt/ios/modules/backup/__init__.py +++ b/mvt/ios/modules/backup/__init__.py @@ -4,6 +4,8 @@ # https://license.mvt.re/1.1/ from .backup_info import BackupInfo +from .configuration_profiles import ConfigurationProfiles from .manifest import Manifest +from .profile_events import ProfileEvents -BACKUP_MODULES = [BackupInfo, Manifest,] +BACKUP_MODULES = [BackupInfo, ConfigurationProfiles, Manifest, ProfileEvents] diff --git a/mvt/ios/modules/backup/configuration_profiles.py b/mvt/ios/modules/backup/configuration_profiles.py new file mode 100644 index 0000000..a8ff09a --- /dev/null +++ b/mvt/ios/modules/backup/configuration_profiles.py @@ -0,0 +1,43 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021 The MVT Project Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import os +import plistlib +from base64 import b64encode + +from ..base import IOSExtraction + +CONF_PROFILES_DOMAIN = "SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles" + +class ConfigurationProfiles(IOSExtraction): + """This module extracts the full plist data from configuration profiles. + """ + + def __init__(self, file_path=None, base_folder=None, output_folder=None, + fast_mode=False, log=None, results=[]): + super().__init__(file_path=file_path, base_folder=base_folder, + output_folder=output_folder, fast_mode=fast_mode, + log=log, results=results) + + def run(self): + for conf_file in self._get_files_from_manifest(domain=CONF_PROFILES_DOMAIN): + 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) + + if "SignerCerts" in conf_plist: + conf_plist["SignerCerts"] = [b64encode(x) for x in conf_plist["SignerCerts"]] + + self.results.append(dict( + file_id=conf_file["file_id"], + relative_path=conf_file["relative_path"], + domain=conf_file["domain"], + plist=conf_plist, + )) + + self.log.info("Extracted details about %d configuration profiles", len(self.results)) diff --git a/mvt/ios/modules/backup/profile_events.py b/mvt/ios/modules/backup/profile_events.py new file mode 100644 index 0000000..31cacfd --- /dev/null +++ b/mvt/ios/modules/backup/profile_events.py @@ -0,0 +1,59 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021 The MVT Project Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import plistlib +from datetime import datetime + +from mvt.common.utils import convert_timestamp_to_iso + +from ..base import IOSExtraction + +CONF_PROFILES_EVENTS_RELPATH = "Library/ConfigurationProfiles/MCProfileEvents.plist" + +class ProfileEvents(IOSExtraction): + """This module extracts events related to the installation of configuration + profiles. + """ + + def __init__(self, file_path=None, base_folder=None, output_folder=None, + fast_mode=False, log=None, results=[]): + super().__init__(file_path=file_path, base_folder=base_folder, + output_folder=output_folder, fast_mode=fast_mode, + log=log, results=results) + + def serialize(self, record): + return { + "timestamp": record.get("timestamp"), + "module": self.__class__.__name__, + "event": "profile_operation", + "data": f"Process {record.get('process')} started operation {record.get('operation')} of profile {record.get('profile_id')}" + } + + def run(self): + for events_file in self._get_files_from_manifest(relative_path=CONF_PROFILES_EVENTS_RELPATH): + events_file_path = self._get_backup_file_from_id(events_file["file_id"]) + if not events_file_path: + continue + + with open(events_file_path, "rb") as handle: + events_plist = plistlib.load(handle) + + if "ProfileEvents" not in events_plist: + continue + + for event in events_plist["ProfileEvents"]: + key = list(event.keys())[0] + self.log.info("On %s process \"%s\" started operation \"%s\" of profile \"%s\"", + event[key].get("timestamp"), event[key].get("process"), + event[key].get("operation"), key) + + self.results.append({ + "profile_id": key, + "timestamp": convert_timestamp_to_iso(event[key].get("timestamp")), + "operation": event[key].get("operation"), + "process": event[key].get("process"), + }) + + self.log.info("Extracted %d profile events", len(self.results)) diff --git a/mvt/ios/modules/base.py b/mvt/ios/modules/base.py index dfcc72d..f925f4f 100644 --- a/mvt/ios/modules/base.py +++ b/mvt/ios/modules/base.py @@ -94,7 +94,18 @@ class IOSExtraction(MVTModule): raise Exception("Query to Manifest.db failed: %s", e) for row in cur: - yield dict(file_id=row[0], domain=row[1], relative_path=row[2]) + yield { + "file_id": row[0], + "domain": row[1], + "relative_path": row[2], + } + + def _get_backup_file_from_id(self, file_id): + file_path = os.path.join(self.base_folder, file_id[0:2], file_id) + if os.path.exists(file_path): + return file_path + + return None def _find_ios_database(self, backup_ids=None, root_paths=[]): """Try to locate the module's database file from either an iTunes @@ -110,9 +121,8 @@ class IOSExtraction(MVTModule): # folder structure, if we have a valid ID. if backup_ids: for backup_id in backup_ids: - file_path = os.path.join(self.base_folder, backup_id[0:2], backup_id) - # If we found the correct backup file, then we stop searching. - if os.path.exists(file_path): + file_path = self._get_backup_file_from_id(backup_id) + if file_path: break # If this file does not exist we might be processing a full diff --git a/mvt/ios/modules/fs/filesystem.py b/mvt/ios/modules/fs/filesystem.py index af333d0..e865065 100644 --- a/mvt/ios/modules/fs/filesystem.py +++ b/mvt/ios/modules/fs/filesystem.py @@ -25,7 +25,7 @@ class Filesystem(IOSExtraction): return { "timestamp": record["modified"], "module": self.__class__.__name__, - "event": f"file_modified", + "event": "file_modified", "data": record["file_path"], } diff --git a/mvt/ios/modules/mixed/webkit_resource_load_statistics.py b/mvt/ios/modules/mixed/webkit_resource_load_statistics.py index 7735334..2886503 100644 --- a/mvt/ios/modules/mixed/webkit_resource_load_statistics.py +++ b/mvt/ios/modules/mixed/webkit_resource_load_statistics.py @@ -28,6 +28,8 @@ class WebkitResourceLoadStatistics(IOSExtraction): output_folder=output_folder, fast_mode=fast_mode, log=log, results=results) + self.results = {} + def check_indicators(self): if not self.indicators: return @@ -72,8 +74,6 @@ class WebkitResourceLoadStatistics(IOSExtraction): self.log.info("Extracted a total of %d records from %s", len(self.results[key]), db_path) def run(self): - self.results = {} - if self.is_backup: try: for backup_file in self._get_files_from_manifest(relative_path=WEBKIT_RESOURCELOADSTATICS_BACKUP_RELPATH):