mirror of
https://github.com/mvt-project/mvt.git
synced 2026-11-01 02:34:16 +01:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0bc233eec | ||
|
|
1db5d239d0 | ||
|
|
2426178baa | ||
|
|
e0697a58e9 | ||
|
|
d0fc0379b9 | ||
|
|
7973e90589 | ||
|
|
935747519f | ||
|
|
178ac52aab | ||
|
|
f20fd14075 | ||
|
|
a9d787c446 | ||
|
|
518becb825 |
No files matched your search
@@ -34,6 +34,7 @@ jobs:
|
||||
publish:
|
||||
if: github.ref_type == 'tag'
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi # only tag refs may deploy; the PyPI trusted publisher requires it
|
||||
permissions:
|
||||
id-token: write # PyPI trusted publishing
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
prune tests
|
||||
prune docs
|
||||
prune .github
|
||||
exclude uv.lock Dockerfile* mkdocs.yml .readthedocs.yaml .safety-policy.yml .python-version .gitignore Makefile
|
||||
+1
-3
@@ -24,10 +24,8 @@ dependencies = [
|
||||
"simplejson==4.1.1",
|
||||
"packaging==26.3",
|
||||
"appdirs==1.4.4",
|
||||
"iphone_backup_decrypt==0.9.0",
|
||||
"iphone_backup_decrypt==0.10.0",
|
||||
"pycryptodome>=3.20.0",
|
||||
"adb-shell[usb]==0.4.4",
|
||||
"libusb1==3.4.0",
|
||||
"cryptography==50.0.0",
|
||||
"PyYAML>=6.0.2",
|
||||
"pyahocorasick==2.3.1",
|
||||
|
||||
@@ -43,25 +43,11 @@ class DumpsysAppopsArtifact(AndroidArtifact):
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for result in self.results:
|
||||
# A package IOC match is not tied to one AppOps entry. Use the latest
|
||||
# recorded access or rejection to place the package-level alert in time.
|
||||
event_time = max(
|
||||
(
|
||||
entry["timestamp"]
|
||||
for permission in result["permissions"]
|
||||
for entry in permission.get("entries", [])
|
||||
if entry.get("timestamp")
|
||||
),
|
||||
default="",
|
||||
)
|
||||
if self.indicators:
|
||||
ioc_match = self.indicators.check_app_id(result.get("package_name"))
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
event_time,
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
@@ -43,10 +43,7 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
|
||||
ioc_match = self.indicators.check_app_id(result["package_name"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("period_start") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
@@ -24,10 +24,7 @@ class DumpsysBatteryHistoryArtifact(AndroidArtifact):
|
||||
ioc_match = self.indicators.check_app_id(package_name)
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("timestamp") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -23,10 +23,7 @@ class DumpsysDBInfoArtifact(AndroidArtifact):
|
||||
ioc_match = self.indicators.check_app_id(part)
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("timestamp") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
|
||||
alerted_root_packages.add(package_name)
|
||||
self.alertstore.medium(
|
||||
f'Found an installed package related to rooting/jailbreaking: "{package_name}"',
|
||||
result.get("timestamp") or "",
|
||||
"",
|
||||
result,
|
||||
)
|
||||
if not self.indicators:
|
||||
@@ -43,10 +43,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
|
||||
ioc_match = self.indicators.check_app_id(package_name)
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("timestamp") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
|
||||
|
||||
@@ -136,6 +136,10 @@ class Mounts(AndroidArtifact):
|
||||
mount_options = fields[5].split(",")
|
||||
super_options = fields[separator + 3].split(",")
|
||||
options = list(dict.fromkeys(mount_options + super_options))
|
||||
# fields[5] are the per-mount (VFS) flags, the field after the
|
||||
# separator the superblock's; a mount is writable only if both
|
||||
# allow it. The merged list made every read-only overlay a HIGH.
|
||||
is_read_write = "rw" in mount_options and "rw" in super_options
|
||||
mount_point = fields[4].replace("\\040", " ")
|
||||
device = fields[separator + 2].replace("\\040", " ")
|
||||
filesystem_type = fields[separator + 1]
|
||||
@@ -156,7 +160,7 @@ class Mounts(AndroidArtifact):
|
||||
"options_list": options,
|
||||
"optional_fields": fields[6:separator],
|
||||
"is_system_partition": is_system,
|
||||
"is_read_write": "rw" in options,
|
||||
"is_read_write": is_read_write,
|
||||
"process_ids": [process_id],
|
||||
}
|
||||
)
|
||||
@@ -191,7 +195,14 @@ class Mounts(AndroidArtifact):
|
||||
)
|
||||
|
||||
# Check for other suspicious mount options
|
||||
suspicious_opts = [opt for opt in options if opt in SUSPICIOUS_OPTIONS]
|
||||
# `rw` sitting in the merged option list is not evidence of
|
||||
# writability (see is_read_write in parse_mountinfo); every other
|
||||
# suspicious option is meaningful in either list.
|
||||
suspicious_opts = [
|
||||
opt
|
||||
for opt in options
|
||||
if opt in SUSPICIOUS_OPTIONS and (opt != "rw" or mount["is_read_write"])
|
||||
]
|
||||
if suspicious_opts and mount["is_system_partition"]:
|
||||
if (
|
||||
"noatime" in mount["mount_options"]
|
||||
|
||||
@@ -99,10 +99,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
||||
ioc_match = self.indicators.check_process(result["process_name"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("timestamp") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -111,10 +108,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
||||
ioc_match = self.indicators.check_process(command_name)
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("timestamp") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -129,7 +123,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
||||
f"Potentially suspicious crash in process '{result['process_name']}' "
|
||||
f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}"
|
||||
),
|
||||
result.get("timestamp") or "",
|
||||
"",
|
||||
result,
|
||||
)
|
||||
|
||||
|
||||
@@ -90,10 +90,7 @@ class AQFFiles(AndroidQFModule):
|
||||
ioc_match = self.indicators.check_file_path(result["path"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("modified_time") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -108,7 +105,7 @@ class AQFFiles(AndroidQFModule):
|
||||
file_type = "executable "
|
||||
|
||||
msg = f'Found {file_type}file at suspicious path "{result["path"]}"'
|
||||
self.alertstore.high(msg, result.get("modified_time") or "", result)
|
||||
self.alertstore.high(msg, "", result)
|
||||
|
||||
for hash_key in ("sha256", "sha1", "md5"):
|
||||
file_hash = result.get(hash_key, "")
|
||||
@@ -117,10 +114,7 @@ class AQFFiles(AndroidQFModule):
|
||||
ioc_match = self.indicators.check_file_hash(file_hash)
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("modified_time") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
@@ -54,10 +54,7 @@ class SMS(BackupModule):
|
||||
):
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
message.get("isodate") or "",
|
||||
message,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", message, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def collect_url_results(self) -> None:
|
||||
|
||||
@@ -1275,5 +1275,9 @@
|
||||
{
|
||||
"version": "26.6.2",
|
||||
"build": "23G90"
|
||||
},
|
||||
{
|
||||
"version": "27.0",
|
||||
"build": "24A437"
|
||||
}
|
||||
]
|
||||
@@ -52,7 +52,7 @@ class MVTEncryptedBackup(EncryptedBackup):
|
||||
|
||||
NOTE: This subclass relies on internal APIs of iphone_backup_decrypt
|
||||
(specifically _read_and_unlock_keybag, _keybag, and the Keybag class
|
||||
internals). Pinned to iphone_backup_decrypt==0.9.0.
|
||||
internals). Pinned to iphone_backup_decrypt==0.10.0.
|
||||
"""
|
||||
|
||||
def __init__(self, *, backup_directory, passphrase=None, derived_key=None):
|
||||
|
||||
@@ -74,10 +74,7 @@ class ConfigurationProfiles(IOSExtraction):
|
||||
if ioc_match:
|
||||
warning_message = f'Found a known malicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with UUID "{result["plist"]["PayloadUUID"]}"'
|
||||
self.alertstore.critical(
|
||||
warning_message,
|
||||
result.get("install_date") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
warning_message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -85,9 +82,7 @@ class ConfigurationProfiles(IOSExtraction):
|
||||
# to hide notifications.
|
||||
if payload_content["PayloadType"] in ["com.apple.notificationsettings"]:
|
||||
warning_message = f'Found a potentially suspicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with payload type {payload_content["PayloadType"]}'
|
||||
self.alertstore.medium(
|
||||
warning_message, result.get("install_date") or "", result
|
||||
)
|
||||
self.alertstore.medium(warning_message, "", result)
|
||||
continue
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -104,10 +104,7 @@ class Manifest(IOSExtraction):
|
||||
ioc_match = self.indicators.check_file_path("/" + result["relative_path"])
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
ioc_match.message,
|
||||
result.get("modified") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -123,7 +120,7 @@ class Manifest(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
f'Found mention of domain "{ioc_match.ioc.value}" in a backup file with path: {rel_path}',
|
||||
result.get("modified") or "",
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
|
||||
@@ -67,20 +67,14 @@ class ProfileEvents(IOSExtraction):
|
||||
ioc_match = self.indicators.check_process(result.get("process") or "")
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("timestamp") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
ioc_match = self.indicators.check_profile(result.get("profile_id") or "")
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("timestamp") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -68,10 +68,7 @@ class Analytics(IOSExtraction):
|
||||
warning_message = f'Found mention of a malicious process "{value}" in {result["artifact"]} file at {result["isodate"]}'
|
||||
new_result = copy.copy(result)
|
||||
self.alertstore.critical(
|
||||
warning_message,
|
||||
result.get("isodate") or "",
|
||||
new_result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
warning_message, "", new_result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -80,7 +77,7 @@ class Analytics(IOSExtraction):
|
||||
new_result = copy.copy(result)
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
"",
|
||||
new_result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
|
||||
@@ -59,10 +59,7 @@ class Filesystem(IOSExtraction):
|
||||
ioc_match = self.indicators.check_file_path(result["path"])
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
ioc_match.message,
|
||||
result.get("modified") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
# If we are instructed to run fast, we skip the rest.
|
||||
@@ -72,10 +69,7 @@ class Filesystem(IOSExtraction):
|
||||
ioc_match = self.indicators.check_file_path_process(result["path"])
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
ioc_match.message,
|
||||
result.get("modified") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
@@ -63,10 +63,7 @@ class SafariFavicon(IOSExtraction):
|
||||
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def _process_favicon_db(self, file_path):
|
||||
|
||||
@@ -60,10 +60,7 @@ class ShutdownLog(IOSExtraction):
|
||||
ioc_match = self.indicators.check_file_path(result["client"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -72,7 +69,7 @@ class ShutdownLog(IOSExtraction):
|
||||
if ioc.value in parts:
|
||||
self.alertstore.critical(
|
||||
f'Found mention of a known malicious process "{ioc.value}" in shutdown.log',
|
||||
result.get("isodate") or "",
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc,
|
||||
)
|
||||
|
||||
@@ -21,10 +21,7 @@ class WebkitBase(IOSExtraction):
|
||||
ioc_match = self.indicators.check_url(result["url"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class Applications(IOSExtraction):
|
||||
if "softwareVersionBundleId" not in result:
|
||||
self.alertstore.medium(
|
||||
"Suspicious application identified without softwareVersionBundleId",
|
||||
result.get("isodate") or "",
|
||||
"",
|
||||
result,
|
||||
)
|
||||
continue
|
||||
@@ -79,7 +79,7 @@ class Applications(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
f"Malicious application {result['softwareVersionBundleId']} identified",
|
||||
result.get("isodate") or "",
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
@@ -91,7 +91,7 @@ class Applications(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
f"Malicious application {result['softwareVersionBundleId']} identified",
|
||||
result.get("isodate") or "",
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
@@ -104,7 +104,7 @@ class Applications(IOSExtraction):
|
||||
):
|
||||
self.alertstore.medium(
|
||||
f"Suspicious app not installed from the App Store or MDM: {result['softwareVersionBundleId']}",
|
||||
result.get("isodate") or "",
|
||||
"",
|
||||
result,
|
||||
)
|
||||
|
||||
|
||||
@@ -72,10 +72,7 @@ class Calendar(IOSExtraction):
|
||||
ioc_match = self.indicators.check_email(result["participant_email"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("start_date") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -83,7 +80,7 @@ class Calendar(IOSExtraction):
|
||||
if result["summary"] == "Meeting" and result["description"] == "Notes":
|
||||
self.alertstore.high(
|
||||
f"Potential Quadream exploit event identified: {result['uuid']}",
|
||||
result.get("start_date") or "",
|
||||
"",
|
||||
result,
|
||||
)
|
||||
|
||||
|
||||
@@ -61,10 +61,7 @@ class ChromeFavicon(IOSExtraction):
|
||||
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
@@ -62,10 +62,7 @@ class ChromeHistory(IOSExtraction):
|
||||
ioc_match = self.indicators.check_url(result["url"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
@@ -64,10 +64,7 @@ class FirefoxFavicon(IOSExtraction):
|
||||
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
@@ -64,10 +64,7 @@ class FirefoxHistory(IOSExtraction):
|
||||
ioc_match = self.indicators.check_url(result["url"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
@@ -66,17 +66,14 @@ class IDStatusCache(IOSExtraction):
|
||||
ioc_match = self.indicators.check_email(email)
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
if "\\x00\\x00" in result.get("user", ""):
|
||||
self.alertstore.high(
|
||||
f"Found an ID Status Cache entry with suspicious patterns: {result.get('user')}",
|
||||
result.get("isodate") or "",
|
||||
"",
|
||||
result,
|
||||
)
|
||||
|
||||
|
||||
@@ -79,14 +79,6 @@ class LocationdClients(IOSExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
event_time = next(
|
||||
(
|
||||
result[timestamp]
|
||||
for timestamp in self.timestamps
|
||||
if result.get(timestamp)
|
||||
),
|
||||
"",
|
||||
)
|
||||
parts = result["package"].split("/")
|
||||
proc_name = parts[len(parts) - 1]
|
||||
|
||||
@@ -94,7 +86,7 @@ class LocationdClients(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
f"Found a suspicious process name in LocationD entry {result['package']}",
|
||||
event_time,
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
@@ -105,7 +97,7 @@ class LocationdClients(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
f"Found a suspicious process name in LocationD entry {result['package']}",
|
||||
event_time,
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
@@ -115,7 +107,7 @@ class LocationdClients(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
f"Found a known malicious domain in LocationD entry {result['package']}",
|
||||
event_time,
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
@@ -126,7 +118,7 @@ class LocationdClients(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
f"Found a suspicious file path in LocationD entry {result['Executable']}",
|
||||
event_time,
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
@@ -141,7 +133,7 @@ class LocationdClients(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
f"Found a suspicious file path in LocationD entry {result['Registered']}",
|
||||
event_time,
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
|
||||
@@ -65,10 +65,7 @@ class OSAnalyticsADDaily(IOSExtraction):
|
||||
ioc_match = self.indicators.check_process(result["package"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("ts") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
@@ -73,10 +73,7 @@ class SafariBrowserState(IOSExtraction):
|
||||
ioc_match = self.indicators.check_url(result["tab_url"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("last_viewed_timestamp") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -89,7 +86,7 @@ class SafariBrowserState(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("last_viewed_timestamp") or "",
|
||||
"",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
|
||||
@@ -126,10 +126,7 @@ class SafariHistory(IOSExtraction):
|
||||
ioc_match = self.indicators.check_url(result["url"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def _process_history_db(self, history_path):
|
||||
|
||||
@@ -82,10 +82,7 @@ class Shortcuts(IOSExtraction):
|
||||
):
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
@@ -98,10 +98,7 @@ class SMS(IOSExtraction):
|
||||
):
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def collect_url_results(self) -> None:
|
||||
|
||||
@@ -67,7 +67,7 @@ class SMSAttachments(IOSExtraction):
|
||||
if ioc_match:
|
||||
self.alertstore.high(
|
||||
ioc_match.message,
|
||||
attachment.get("isodate") or "",
|
||||
"",
|
||||
attachment,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
)
|
||||
|
||||
@@ -97,10 +97,7 @@ class TCC(IOSExtraction):
|
||||
ioc_match = self.indicators.check_process(result["client"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("last_modified") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def process_db(self, file_path):
|
||||
|
||||
@@ -69,10 +69,7 @@ class WebkitResourceLoadStatistics(IOSExtraction):
|
||||
ioc_match = self.indicators.check_url(result["registrable_domain"])
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("last_seen_isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def _process_observations_db(self, db_path: str, domain: str, path: str) -> None:
|
||||
|
||||
@@ -105,12 +105,7 @@ class WebkitSessionResourceLog(IOSExtraction):
|
||||
if ioc_match:
|
||||
entry, source_domains, destination_domains = record
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
entry.get("last_seen")
|
||||
or entry.get("most_recent_interaction")
|
||||
or "",
|
||||
entry,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", entry, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
redirect_path = ""
|
||||
@@ -134,9 +129,7 @@ class WebkitSessionResourceLog(IOSExtraction):
|
||||
|
||||
self.alertstore.high(
|
||||
f"Found HTTP redirect between suspicious domains: {redirect_path}",
|
||||
entry.get("last_seen")
|
||||
or entry.get("most_recent_interaction")
|
||||
or "",
|
||||
"",
|
||||
entry,
|
||||
)
|
||||
|
||||
|
||||
@@ -189,10 +189,7 @@ class Whatsapp(IOSExtraction):
|
||||
):
|
||||
if ioc_match:
|
||||
self.alertstore.critical(
|
||||
ioc_match.message,
|
||||
result.get("isodate") or "",
|
||||
result,
|
||||
matched_indicator=ioc_match.ioc,
|
||||
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
|
||||
)
|
||||
|
||||
def collect_url_results(self) -> None:
|
||||
|
||||
@@ -90,10 +90,4 @@ class TestDumpsysAppopsArtifact:
|
||||
assert len(da.alertstore.alerts) == 3
|
||||
assert len(detected_by_ioc) == 1
|
||||
assert detected_by_ioc[0].matched_indicator is not None
|
||||
assert detected_by_ioc[0].event_time == max(
|
||||
entry["timestamp"]
|
||||
for permission in detected_by_ioc[0].event["permissions"]
|
||||
for entry in permission.get("entries", [])
|
||||
if entry.get("timestamp")
|
||||
)
|
||||
assert len(detected_by_permission_heuristic) == 2
|
||||
@@ -0,0 +1,43 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2026 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
"""A mount is writable only if both mountinfo option layers allow it."""
|
||||
|
||||
from mvt.android.artifacts.mounts import Mounts
|
||||
from mvt.common.alerts import AlertLevel
|
||||
|
||||
# fields[5] is the per-mount (VFS) layer, the field after "-" the superblock.
|
||||
RO_OVERLAY = (
|
||||
"304 166 0:130 / /product/usr rw,relatime shared:63 - overlay overlay "
|
||||
"ro,seclabel,lowerdir=/mnt/vendor/ext/product/usr:/product/usr"
|
||||
)
|
||||
RW_SYSTEM = (
|
||||
"305 166 0:131 / /system rw,relatime shared:64 - ext4 /dev/block/dm-1 "
|
||||
"rw,seclabel,errors=panic"
|
||||
)
|
||||
|
||||
|
||||
def _run(line):
|
||||
mounts = Mounts()
|
||||
mounts.results = Mounts.parse_mountinfo(line, 1)
|
||||
mounts.check_indicators()
|
||||
return mounts
|
||||
|
||||
|
||||
class TestMountsReadWriteLayers:
|
||||
def test_rw_vfs_over_ro_superblock_is_not_writable(self):
|
||||
mounts = _run(RO_OVERLAY)
|
||||
assert mounts.results[0]["is_read_write"] is False
|
||||
assert mounts.alertstore.count(AlertLevel.HIGH) == 0
|
||||
assert mounts.alertstore.count(AlertLevel.MEDIUM) == 0
|
||||
|
||||
def test_rw_in_both_layers_still_alerts(self):
|
||||
mounts = _run(RW_SYSTEM)
|
||||
assert mounts.results[0]["is_read_write"] is True
|
||||
assert mounts.alertstore.count(AlertLevel.HIGH) == 1
|
||||
|
||||
def test_both_option_layers_are_still_reported(self):
|
||||
mounts = _run(RO_OVERLAY)
|
||||
options = mounts.results[0]["options_list"]
|
||||
assert "rw" in options and "ro" in options
|
||||
@@ -44,4 +44,3 @@ class TestCalendarModule:
|
||||
assert len(m.results) == 1
|
||||
assert len(m.timeline) == 4
|
||||
assert len(m.alertstore.alerts) == 1
|
||||
assert m.alertstore.alerts[0].event_time == m.results[0]["start_date"]
|
||||
@@ -6,22 +6,6 @@ resolution-markers = [
|
||||
"python_full_version < '3.15'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adb-shell"
|
||||
version = "0.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "rsa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/73/d246034db6f3e374dad9a35ee3f61345a6b239d4febd2a41ab69df9936fe/adb_shell-0.4.4.tar.gz", hash = "sha256:04c305f30a2ca25d5c54b3cd6ce9bb64c36e5f07967b23b3fb6aaecc851b90b6", size = 61822, upload-time = "2023-09-01T03:48:40.348Z" }
|
||||
|
||||
[package.optional-dependencies]
|
||||
usb = [
|
||||
{ name = "libusb1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.8.0"
|
||||
@@ -625,14 +609,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "iphone-backup-decrypt"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycryptodome" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/e7/bcdacdec21d628122ba240e7f742ab2175149e58672be63af55ff37a0f28/iphone_backup_decrypt-0.9.0.tar.gz", hash = "sha256:13b18fef3c8e3af627914f8c1a429bbc5555dfb0505239ba49efe99984cc0c96", size = 16125, upload-time = "2024-09-18T15:50:12.179Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/b8/a2ec2b10d792403d36bb6661fea6357179c31ece1ed3285c442e636d6335/iphone_backup_decrypt-0.10.0.tar.gz", hash = "sha256:5f2799e58f908cce28d7929dd82231abc8acd57c25b37bce6a2d6a82e018d343", size = 20000, upload-time = "2026-09-17T21:46:44Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/94/64a31be93f72e0a254bde68e4cf7d24aef37a0a985754a197fa1b028a665/iphone_backup_decrypt-0.9.0-py3-none-any.whl", hash = "sha256:55b5adfafac757f58aa6444b83a4cc2c20cdd699c6ff1d2f4b549936a5dad92c", size = 15767, upload-time = "2024-09-18T15:50:10.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/10/349b2a720e012921d1457e1d1c2b921b16300a5f3570c4c64d0d3787539f/iphone_backup_decrypt-0.10.0-py3-none-any.whl", hash = "sha256:1ac4345a07b104a3eacab20932b8621cd1e0164e0f03d913e66eed6ea5ee173e", size = 17400, upload-time = "2026-09-17T21:46:42Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -777,17 +761,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libusb1"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/35/f9d2a990d092d647b47540cd229e1d68432c0f51183484ca189612a4824c/libusb1-3.4.0.tar.gz", hash = "sha256:9cf5638506d54f21bf36550d97ea63189111a23c4d8078f630103a2052135f45", size = 91206, upload-time = "2026-05-16T20:59:19.315Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/64/d4b59444e4d3b6979aa5eb58840634465a24b41a9ab03dcf8434c9b89551/libusb1-3.4.0-py3-none-any.whl", hash = "sha256:e83d034e44c3efe1c4599c6281d34bca50a38c12cab3b7b6217d583161a01ffd", size = 67373, upload-time = "2026-05-16T20:59:13.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/55/a838a4278fac4ee49bf670ddc59d621999a1cb2a3188fb13a23609ea4f06/libusb1-3.4.0-py3-none-win32.whl", hash = "sha256:0a1aa1416034690eb9dc9a895eda0fef44d853bca1053eb1de50a5906684846d", size = 129704, upload-time = "2026-05-16T20:59:15.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/08/02aecf6dad627534a5835244ece14d7f187ef430354d9b8551199934d059/libusb1-3.4.0-py3-none-win_amd64.whl", hash = "sha256:b7dcc1f324a895af6aac708bc5513a17373f97349cc2ab8a277519c788bd18ca", size = 141212, upload-time = "2026-05-16T20:59:17.286Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.10.3"
|
||||
@@ -1016,13 +989,11 @@ wheels = [
|
||||
name = "mvt"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "adb-shell", extra = ["usb"] },
|
||||
{ name = "appdirs" },
|
||||
{ name = "betterproto2" },
|
||||
{ name = "click" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "iphone-backup-decrypt" },
|
||||
{ name = "libusb1" },
|
||||
{ name = "nskeyedunarchiver" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyahocorasick" },
|
||||
@@ -1063,13 +1034,11 @@ docs = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "adb-shell", extras = ["usb"], specifier = "==0.4.4" },
|
||||
{ name = "appdirs", specifier = "==1.4.4" },
|
||||
{ name = "betterproto2", specifier = "==0.10.0" },
|
||||
{ name = "click", specifier = "==8.4.2" },
|
||||
{ name = "cryptography", specifier = "==50.0.0" },
|
||||
{ name = "iphone-backup-decrypt", specifier = "==0.9.0" },
|
||||
{ name = "libusb1", specifier = "==3.4.0" },
|
||||
{ name = "iphone-backup-decrypt", specifier = "==0.10.0" },
|
||||
{ name = "nskeyedunarchiver", specifier = "==1.5.2" },
|
||||
{ name = "packaging", specifier = "==26.3" },
|
||||
{ name = "pyahocorasick", specifier = "==2.3.1" },
|
||||
@@ -1274,15 +1243,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/96/37c50ac951bb0260ec38d8d12e5b51587ef1ef4035c279088f2771544b28/pyahocorasick-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4acb11a0a2ff10519465749d22ad70789e9fe7f81dc8fe9957a8868e499e18ab", size = 35987, upload-time = "2026-04-27T16:32:07.08Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
@@ -1684,18 +1644,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "4.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyasn1" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.16.2"
|
||||
|
||||
Reference in new issue
Block a user