mirror of https://github.com/mvt-project/mvt.git
Compare commits
2 Commits
529df85f0f
...
512f40dcb4
Author | SHA1 | Date |
---|---|---|
Nex | 512f40dcb4 | |
Nex | b3a464ba58 |
|
@ -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_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
|
||||
#==============================================================================
|
||||
|
@ -191,7 +194,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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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...")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -10,6 +10,7 @@ from .base import AndroidExtraction
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpsysProcstats(AndroidExtraction):
|
||||
"""This module extracts stats on memory consumption by processes."""
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -10,6 +10,7 @@ from .base import AndroidExtraction
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Files(AndroidExtraction):
|
||||
"""This module extracts the list of installed packages."""
|
||||
|
||||
|
|
|
@ -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\"",
|
||||
|
|
|
@ -9,6 +9,7 @@ from .base import AndroidExtraction
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Processes(AndroidExtraction):
|
||||
"""This module extracts details on running processes."""
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ from .base import AndroidExtraction
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RootBinaries(AndroidExtraction):
|
||||
"""This module extracts the list of installed packages."""
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
|
||||
from .sms import SMS
|
||||
|
||||
BACKUP_MODULES = [SMS,]
|
||||
BACKUP_MODULES = [SMS]
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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.
|
||||
|
@ -115,7 +116,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:
|
||||
|
|
|
@ -16,7 +16,7 @@ def logo():
|
|||
|
||||
try:
|
||||
latest_version = check_for_updates()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if latest_version:
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -8,6 +8,7 @@ from packaging import version
|
|||
|
||||
MVT_VERSION = "1.2.14"
|
||||
|
||||
|
||||
def check_for_updates():
|
||||
res = requests.get("https://pypi.org/pypi/mvt/json")
|
||||
data = res.json()
|
||||
|
|
|
@ -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
|
||||
#==============================================================================
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -10,6 +10,7 @@ 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."""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -16,4 +16,4 @@ from .webkit_safariviewservice import WebkitSafariViewService
|
|||
|
||||
FS_MODULES = [CacheFiles, Filesystem, Netusage, Analytics, SafariFavicon, ShutdownLog,
|
||||
IOSVersionHistory, WebkitIndexedDB, WebkitLocalStorage,
|
||||
WebkitSafariViewService,]
|
||||
WebkitSafariViewService]
|
||||
|
|
|
@ -14,6 +14,7 @@ ANALYTICS_DB_PATH = [
|
|||
"private/var/Keychains/Analytics/*.db",
|
||||
]
|
||||
|
||||
|
||||
class Analytics(IOSExtraction):
|
||||
"""This module extracts information from the private/var/Keychains/Analytics/*.db files."""
|
||||
|
||||
|
@ -30,7 +31,7 @@ class Analytics(IOSExtraction):
|
|||
"event": record["artifact"],
|
||||
"data": f"{record}",
|
||||
}
|
||||
|
||||
|
||||
def check_indicators(self):
|
||||
if not self.indicators:
|
||||
return
|
||||
|
@ -50,7 +51,7 @@ class Analytics(IOSExtraction):
|
|||
ioc, result["artifact"], result["timestamp"])
|
||||
self.detected.append(result)
|
||||
break
|
||||
|
||||
|
||||
def _extract_analytics_data(self):
|
||||
artifact = self.file_path.split("/")[-1]
|
||||
|
||||
|
@ -87,7 +88,6 @@ class Analytics(IOSExtraction):
|
|||
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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -60,7 +60,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 +72,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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -27,4 +27,4 @@ MIXED_MODULES = [Calls, ChromeFavicon, ChromeHistory, Contacts, FirefoxFavicon,
|
|||
FirefoxHistory, IDStatusCache, InteractionC, LocationdClients,
|
||||
OSAnalyticsADDaily, Datausage, SafariBrowserState, SafariHistory,
|
||||
TCC, SMS, SMSAttachments, WebkitResourceLoadStatistics,
|
||||
WebkitSessionResourceLog, Whatsapp,]
|
||||
WebkitSessionResourceLog, Whatsapp]
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"]:
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
@ -67,8 +68,8 @@ class SMS(IOSExtraction):
|
|||
# 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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ AUTH_REASONS = {
|
|||
12: "app_type_policy",
|
||||
}
|
||||
|
||||
|
||||
class TCC(IOSExtraction):
|
||||
"""This module extracts records from the TCC.db SQLite database."""
|
||||
|
||||
|
@ -50,7 +51,7 @@ class TCC(IOSExtraction):
|
|||
def process_db(self, file_path):
|
||||
conn = sqlite3.connect(file_path)
|
||||
cur = conn.cursor()
|
||||
cur.execute("""SELECT
|
||||
cur.execute("""SELECT
|
||||
service, client, client_type, auth_value, auth_reason, last_modified
|
||||
FROM access;""")
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -81,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"],
|
||||
|
|
|
@ -233,11 +233,13 @@ IPHONE_IOS_VERSIONS = [
|
|||
{"build": "19B74", "version": "15.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:
|
||||
|
|
2
setup.py
2
setup.py
|
@ -30,6 +30,7 @@ requires = (
|
|||
"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,
|
||||
|
|
Loading…
Reference in New Issue