# Mobile Verification Toolkit (MVT) # Copyright (c) 2021 The MVT Project Authors. # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ import glob import io import os import shutil import sqlite3 import subprocess from mvt.common.module import (DatabaseCorruptedError, DatabaseNotFoundError, MVTModule) class IOSExtraction(MVTModule): """This class provides a base for all iOS filesystem/backup extraction modules.""" is_backup = False is_fs_dump = False is_sysdiagnose = False def _is_database_malformed(self, file_path): # Check if the database is malformed. conn = sqlite3.connect(file_path) cur = conn.cursor() try: recover = False cur.execute("SELECT name FROM sqlite_master WHERE type='table';") except sqlite3.DatabaseError as e: if "database disk image is malformed" in str(e): recover = True finally: conn.close() return recover def _recover_database(self, file_path): """Tries to recover a malformed database by running a .clone command. :param file_path: Path to the malformed database file. """ # TODO: Find a better solution. self.log.info("Database at path %s is malformed. Trying to recover...", file_path) if not os.path.exists(file_path): return if not shutil.which("sqlite3"): raise DatabaseCorruptedError("Unable to recover without sqlite3 binary. Please install sqlite3!") if '"' in file_path: raise DatabaseCorruptedError(f"Database at path '{file_path}' is corrupted. unable to recover because it has a quotation mark (\") in its name.") bak_path = f"{file_path}.bak" shutil.move(file_path, bak_path) ret = subprocess.call(["sqlite3", bak_path, f".clone '{file_path}'"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if ret != 0: raise DatabaseCorruptedError("Recovery of database failed") self.log.info("Database at path %s recovered successfully!", file_path) def _find_ios_database(self, backup_ids=None, root_paths=[]): """Try to locate the module's database file from either an iTunes backup or a full filesystem dump. :param backup_id: iTunes backup database file's ID (or hash). :param root_paths: Glob patterns for files to seek in filesystem dump. """ file_path = None # First we check if the was an explicit file path specified. if not self.file_path: # If not, we first try with backups. # We construct the path to the file according to the iTunes backup # folder structure, if we have a valid ID. if backup_ids: for backup_id in backup_ids: file_path = os.path.join(self.base_folder, backup_id[0:2], backup_id) # If we found the correct backup file, then we stop searching. if os.path.exists(file_path): break # If this file does not exist we might be processing a full # filesystem dump (checkra1n all the things!). if not file_path or not os.path.exists(file_path): # We reset the file_path. file_path = None for root_path in root_paths: for found_path in glob.glob(os.path.join(self.base_folder, root_path)): # If we find a valid path, we set file_path. if os.path.exists(found_path): file_path = found_path break # Otherwise, we reset the file_path again. file_path = None # If we do not find any, we fail. if file_path: self.file_path = file_path else: raise DatabaseNotFoundError("Unable to find the module's database file") if self._is_database_malformed(self.file_path): self._recover_database(self.file_path)