2021-07-16 06:05:01 +00:00
|
|
|
# Mobile Verification Toolkit (MVT)
|
2023-09-09 15:55:27 +00:00
|
|
|
# Copyright (c) 2021-2023 The MVT Authors.
|
2021-08-01 19:11:08 +00:00
|
|
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
|
|
# https://license.mvt.re/1.1/
|
2021-07-16 06:05:01 +00:00
|
|
|
|
2022-06-17 20:30:46 +00:00
|
|
|
import logging
|
2021-07-30 09:40:09 +00:00
|
|
|
import os
|
2021-08-14 16:50:11 +00:00
|
|
|
import plistlib
|
2022-08-16 11:39:55 +00:00
|
|
|
from typing import Optional
|
2021-07-16 06:05:01 +00:00
|
|
|
|
2022-08-13 00:14:24 +00:00
|
|
|
from mvt.common.utils import convert_datetime_to_iso
|
2021-07-16 06:05:01 +00:00
|
|
|
|
2021-08-15 11:14:18 +00:00
|
|
|
from ..base import IOSExtraction
|
2021-07-16 06:05:01 +00:00
|
|
|
|
|
|
|
WEBKIT_SESSION_RESOURCE_LOG_BACKUP_IDS = [
|
|
|
|
"a500ee38053454a02e990957be8a251935e28d3f",
|
|
|
|
]
|
2022-08-16 16:26:34 +00:00
|
|
|
WEBKIT_SESSION_RESOURCE_LOG_BACKUP_RELPATH = "Library/WebKit/WebsiteData/ResourceLoadStatistics/full_browsing_session_resourceLog.plist" # pylint: disable=line-too-long
|
2021-07-16 06:05:01 +00:00
|
|
|
WEBKIT_SESSION_RESOURCE_LOG_ROOT_PATHS = [
|
2022-08-16 16:26:34 +00:00
|
|
|
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/full_browsing_session_resourceLog.plist", # pylint: disable=line-too-long
|
|
|
|
"private/var/mobile/Containers/Data/Application/*/Library/WebKit/WebsiteData/ResourceLoadStatistics/full_browsing_session_resourceLog.plist", # pylint: disable=line-too-long
|
|
|
|
"private/var/mobile/Library/WebClips/*/Storage/full_browsing_session_resourceLog.plist", # pylint: disable=line-too-long
|
2021-07-16 06:05:01 +00:00
|
|
|
]
|
|
|
|
|
2021-11-19 14:27:51 +00:00
|
|
|
|
2021-07-16 06:05:01 +00:00
|
|
|
class WebkitSessionResourceLog(IOSExtraction):
|
|
|
|
"""This module extracts records from WebKit browsing session
|
|
|
|
resource logs, and checks them against any provided list of
|
2021-09-10 13:18:13 +00:00
|
|
|
suspicious domains.
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
2021-07-16 06:05:01 +00:00
|
|
|
|
2022-08-16 11:39:55 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
2022-08-17 13:52:17 +00:00
|
|
|
file_path: Optional[str] = None,
|
|
|
|
target_path: Optional[str] = None,
|
|
|
|
results_path: Optional[str] = None,
|
2023-07-17 16:29:43 +00:00
|
|
|
module_options: Optional[dict] = None,
|
2022-08-16 11:39:55 +00:00
|
|
|
log: logging.Logger = logging.getLogger(__name__),
|
2023-06-01 21:40:26 +00:00
|
|
|
results: Optional[list] = None,
|
2022-08-16 11:39:55 +00:00
|
|
|
) -> None:
|
2023-06-01 21:40:26 +00:00
|
|
|
super().__init__(
|
|
|
|
file_path=file_path,
|
|
|
|
target_path=target_path,
|
|
|
|
results_path=results_path,
|
2023-07-17 16:29:43 +00:00
|
|
|
module_options=module_options,
|
2023-06-01 21:40:26 +00:00
|
|
|
log=log,
|
|
|
|
results=results,
|
|
|
|
)
|
2021-07-16 06:05:01 +00:00
|
|
|
|
2022-01-27 17:23:19 +00:00
|
|
|
self.results = {} if not results else results
|
2021-08-15 18:00:29 +00:00
|
|
|
|
2021-07-16 06:05:01 +00:00
|
|
|
@staticmethod
|
|
|
|
def _extract_domains(entries):
|
|
|
|
if not entries:
|
|
|
|
return []
|
|
|
|
|
|
|
|
domains = []
|
|
|
|
for entry in entries:
|
|
|
|
if "origin" in entry:
|
|
|
|
domains.append(entry["origin"])
|
|
|
|
if "domain" in entry:
|
|
|
|
domains.append(entry["domain"])
|
|
|
|
|
|
|
|
return domains
|
|
|
|
|
2022-06-17 20:30:46 +00:00
|
|
|
def check_indicators(self) -> None:
|
2021-07-23 16:04:41 +00:00
|
|
|
if not self.indicators:
|
|
|
|
return
|
|
|
|
|
2022-08-12 14:20:16 +00:00
|
|
|
for _, entries in self.results.items():
|
2021-07-16 06:05:01 +00:00
|
|
|
for entry in entries:
|
|
|
|
source_domains = self._extract_domains(entry["redirect_source"])
|
2022-08-16 11:39:55 +00:00
|
|
|
destination_domains = self._extract_domains(
|
2023-06-01 21:40:26 +00:00
|
|
|
entry["redirect_destination"]
|
|
|
|
)
|
2021-07-16 06:05:01 +00:00
|
|
|
|
|
|
|
# TODO: Currently not used.
|
2022-08-16 11:39:55 +00:00
|
|
|
# subframe_origins = self._extract_domains(
|
|
|
|
# entry["subframe_under_origin"])
|
|
|
|
# subresource_domains = self._extract_domains(
|
|
|
|
# entry["subresource_under_origin"])
|
2021-07-16 06:05:01 +00:00
|
|
|
|
2022-08-16 11:39:55 +00:00
|
|
|
all_origins = set(
|
2023-06-01 21:40:26 +00:00
|
|
|
[entry["origin"]] + source_domains + destination_domains
|
2022-08-16 11:39:55 +00:00
|
|
|
)
|
2021-07-16 06:05:01 +00:00
|
|
|
|
2022-01-23 14:01:49 +00:00
|
|
|
ioc = self.indicators.check_domains(all_origins)
|
|
|
|
if ioc:
|
|
|
|
entry["matched_indicator"] = ioc
|
2021-07-16 06:05:01 +00:00
|
|
|
self.detected.append(entry)
|
|
|
|
|
|
|
|
redirect_path = ""
|
|
|
|
if len(source_domains) > 0:
|
|
|
|
redirect_path += "SOURCE: "
|
|
|
|
for idx, item in enumerate(source_domains):
|
2023-06-01 21:40:26 +00:00
|
|
|
source_domains[idx] = f'"{item}"'
|
2021-07-16 06:05:01 +00:00
|
|
|
|
|
|
|
redirect_path += ", ".join(source_domains)
|
|
|
|
redirect_path += " -> "
|
|
|
|
|
|
|
|
redirect_path += f"ORIGIN: \"{entry['origin']}\""
|
|
|
|
|
|
|
|
if len(destination_domains) > 0:
|
|
|
|
redirect_path += " -> "
|
|
|
|
redirect_path += "DESTINATION: "
|
|
|
|
for idx, item in enumerate(destination_domains):
|
2023-06-01 21:40:26 +00:00
|
|
|
destination_domains[idx] = f'"{item}"'
|
2021-07-16 06:05:01 +00:00
|
|
|
|
|
|
|
redirect_path += ", ".join(destination_domains)
|
|
|
|
|
2023-06-01 21:40:26 +00:00
|
|
|
self.log.warning(
|
|
|
|
"Found HTTP redirect between suspicious domains: %s",
|
|
|
|
redirect_path,
|
|
|
|
)
|
2021-07-16 06:05:01 +00:00
|
|
|
|
2021-08-16 08:50:35 +00:00
|
|
|
def _extract_browsing_stats(self, log_path):
|
|
|
|
items = []
|
|
|
|
|
|
|
|
with open(log_path, "rb") as handle:
|
|
|
|
file_plist = plistlib.load(handle)
|
|
|
|
|
|
|
|
if "browsingStatistics" not in file_plist:
|
|
|
|
return items
|
|
|
|
|
|
|
|
browsing_stats = file_plist["browsingStatistics"]
|
|
|
|
|
|
|
|
for item in browsing_stats:
|
2023-06-01 21:40:26 +00:00
|
|
|
items.append(
|
|
|
|
{
|
|
|
|
"origin": item.get("PrevalentResourceOrigin", ""),
|
|
|
|
"redirect_source": item.get("topFrameUniqueRedirectsFrom", ""),
|
|
|
|
"redirect_destination": item.get("topFrameUniqueRedirectsTo", ""),
|
|
|
|
"subframe_under_origin": item.get(
|
|
|
|
"subframeUnderTopFrameOrigins", ""
|
|
|
|
),
|
|
|
|
"subresource_under_origin": item.get(
|
|
|
|
"subresourceUnderTopFrameOrigins", ""
|
|
|
|
),
|
|
|
|
"user_interaction": item.get("hadUserInteraction"),
|
|
|
|
"most_recent_interaction": convert_datetime_to_iso(
|
|
|
|
item["mostRecentUserInteraction"]
|
|
|
|
),
|
|
|
|
"last_seen": convert_datetime_to_iso(item["lastSeen"]),
|
|
|
|
}
|
|
|
|
)
|
2021-08-16 08:50:35 +00:00
|
|
|
|
|
|
|
return items
|
|
|
|
|
2022-06-17 20:30:46 +00:00
|
|
|
def run(self) -> None:
|
2021-08-15 18:00:29 +00:00
|
|
|
if self.is_backup:
|
2022-08-16 11:39:55 +00:00
|
|
|
for log_file in self._get_backup_files_from_manifest(
|
2023-06-01 21:40:26 +00:00
|
|
|
relative_path=WEBKIT_SESSION_RESOURCE_LOG_BACKUP_RELPATH
|
|
|
|
):
|
2021-09-01 19:49:23 +00:00
|
|
|
log_path = self._get_backup_file_from_id(log_file["file_id"])
|
2022-08-16 11:39:55 +00:00
|
|
|
|
2021-09-01 19:49:23 +00:00
|
|
|
if not log_path:
|
|
|
|
continue
|
2022-08-08 14:44:54 +00:00
|
|
|
|
2023-06-01 21:40:26 +00:00
|
|
|
self.log.info(
|
|
|
|
"Found Safari browsing session resource log at path: %s", log_path
|
|
|
|
)
|
2021-08-16 08:50:35 +00:00
|
|
|
self.results[log_path] = self._extract_browsing_stats(log_path)
|
|
|
|
elif self.is_fs_dump:
|
2022-08-16 11:39:55 +00:00
|
|
|
for log_path in self._get_fs_files_from_patterns(
|
2023-06-01 21:40:26 +00:00
|
|
|
WEBKIT_SESSION_RESOURCE_LOG_ROOT_PATHS
|
|
|
|
):
|
|
|
|
self.log.info(
|
|
|
|
"Found Safari browsing session resource log at path: %s", log_path
|
|
|
|
)
|
2022-06-16 13:18:50 +00:00
|
|
|
key = os.path.relpath(log_path, self.target_path)
|
2021-08-16 08:50:35 +00:00
|
|
|
self.results[key] = self._extract_browsing_stats(log_path)
|
|
|
|
|
2023-06-01 21:40:26 +00:00
|
|
|
self.log.info(
|
|
|
|
"Extracted records from %d Safari browsing session resource logs",
|
|
|
|
len(self.results),
|
|
|
|
)
|