mirror of
https://github.com/mvt-project/mvt.git
synced 2026-02-15 18:02:44 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e0a393a02 | ||
|
|
c3dc4174fc | ||
|
|
e1d1b6c5de | ||
|
|
d0a893841b | ||
|
|
d4e99661c7 | ||
|
|
6a00d3a14d | ||
|
|
a863209abb | ||
|
|
4c7db02da4 | ||
|
|
92dfefbdeb | ||
|
|
8988adcf77 | ||
|
|
91667b0ded | ||
|
|
2365175dbd | ||
|
|
528d43b914 | ||
|
|
f952ba5119 | ||
|
|
b4ed2c6ed4 | ||
|
|
3eed1d6edf | ||
|
|
83ef545cd1 | ||
|
|
5d4fbec62b | ||
|
|
fa7d6166f4 | ||
|
|
429b223555 | ||
|
|
e4b9a9652a | ||
|
|
134581c000 | ||
|
|
5356a399c9 | ||
|
|
e0f563596d | ||
|
|
ea5de0203a | ||
|
|
ace965ee8a | ||
|
|
ad8f455209 | ||
|
|
ae67b41374 | ||
|
|
5fe88098b9 | ||
|
|
d578c240f9 | ||
|
|
427a29c2b6 | ||
|
|
5e6f6faa9c | ||
|
|
74a3ecaa4e | ||
|
|
f536af1124 | ||
|
|
631354c131 | ||
|
|
7ad7782b51 | ||
|
|
f04f91e1e3 |
@@ -16,4 +16,4 @@ When contributing code to
|
||||
|
||||
- **Quotes**: we use double quotes (`"`) as a default. Single quotes (`'`) can be favored with nested strings instead of escaping (`\"`), or when using f-formatting.
|
||||
|
||||
- **Maximum line length**: we strongly encourage to respect a 80 characters long lines and to follow [PEP8 indentation guidelines](https://peps.python.org/pep-0008/#indentation) when having to wrap. However, if breaking at 80 is not possible or is detrimental to the readability of the code, exceptions are tolerated so long as they remain within a hard maximum length of 100 characters.
|
||||
- **Maximum line length**: we strongly encourage to respect a 80 characters long lines and to follow [PEP8 indentation guidelines](https://peps.python.org/pep-0008/#indentation) when having to wrap. However, if breaking at 80 is not possible or is detrimental to the readability of the code, exceptions are tolerated. For example, long log lines, or long strings can be extended to 100 characters long. Please hard wrap anything beyond 100 characters.
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
If you have correctly [installed libimobiledevice](../install.md) you can easily generate an iTunes backup using the `idevicebackup2` tool included in the suite. First, you might want to ensure that backup encryption is enabled (**note: encrypted backup contain more data than unencrypted backups**):
|
||||
|
||||
```bash
|
||||
idevicebackup2 -i backup encryption on
|
||||
idevicebackup2 -i encryption on
|
||||
```
|
||||
|
||||
Note that if a backup password was previously set on this device, you might need to use the same or change it. You can try changing password using `idevicebackup2 -i backup changepw`, or by turning off encryption (`idevicebackup2 -i backup encryption off`) and turning it back on again.
|
||||
Note that if a backup password was previously set on this device, you might need to use the same or change it. You can try changing password using `idevicebackup2 -i changepw`, or by turning off encryption (`idevicebackup2 -i encryption off`) and turning it back on again.
|
||||
|
||||
If you are not able to recover or change the password, you should try to disable encryption and obtain an unencrypted backup.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from mvt.common.logo import logo
|
||||
from mvt.common.updates import IndicatorsUpdates
|
||||
|
||||
from .cmd_check_adb import CmdAndroidCheckADB
|
||||
from .cmd_check_androidqf import CmdAndroidCheckAndroidQF
|
||||
from .cmd_check_backup import CmdAndroidCheckBackup
|
||||
from .cmd_check_bugreport import CmdAndroidCheckBugreport
|
||||
from .cmd_download_apks import DownloadAPKs
|
||||
@@ -121,9 +122,9 @@ def check_adb(ctx, serial, iocs, output, fast, list_modules, module):
|
||||
|
||||
cmd.run()
|
||||
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
if cmd.detected_count > 0:
|
||||
log.warning("The analysis of the Android device produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
cmd.detected_count)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
@@ -151,9 +152,9 @@ def check_bugreport(ctx, iocs, output, list_modules, module, bugreport_path):
|
||||
|
||||
cmd.run()
|
||||
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
if cmd.detected_count > 0:
|
||||
log.warning("The analysis of the Android bug report produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
cmd.detected_count)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
@@ -179,9 +180,39 @@ def check_backup(ctx, iocs, output, list_modules, backup_path):
|
||||
|
||||
cmd.run()
|
||||
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
if cmd.detected_count > 0:
|
||||
log.warning("The analysis of the Android backup produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
cmd.detected_count)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Command: check-androidqf
|
||||
#==============================================================================
|
||||
@cli.command("check-androidqf", help="Check data collected with AndroidQF")
|
||||
@click.option("--iocs", "-i", type=click.Path(exists=True), multiple=True,
|
||||
default=[], help=HELP_MSG_IOC)
|
||||
@click.option("--output", "-o", type=click.Path(exists=False),
|
||||
help=HELP_MSG_OUTPUT)
|
||||
@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES)
|
||||
@click.option("--module", "-m", help=HELP_MSG_MODULE)
|
||||
@click.argument("ANDROIDQF_PATH", type=click.Path(exists=True))
|
||||
@click.pass_context
|
||||
def check_androidqf(ctx, iocs, output, list_modules, module, androidqf_path):
|
||||
cmd = CmdAndroidCheckAndroidQF(target_path=androidqf_path,
|
||||
results_path=output, ioc_files=iocs,
|
||||
module_name=module)
|
||||
|
||||
if list_modules:
|
||||
cmd.list_modules()
|
||||
return
|
||||
|
||||
log.info("Checking AndroidQF acquisition at path: %s", androidqf_path)
|
||||
|
||||
cmd.run()
|
||||
|
||||
if cmd.detected_count > 0:
|
||||
log.warning("The analysis of the AndroidQF acquisition produced %d detections!",
|
||||
cmd.detected_count)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.command import Command
|
||||
|
||||
@@ -14,9 +15,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CmdAndroidCheckADB(Command):
|
||||
|
||||
def __init__(self, target_path: str = None, results_path: str = None,
|
||||
ioc_files: list = [], module_name: str = None,
|
||||
serial: str = None, fast_mode: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
) -> None:
|
||||
super().__init__(target_path=target_path, results_path=results_path,
|
||||
ioc_files=ioc_files, module_name=module_name,
|
||||
serial=serial, fast_mode=fast_mode, log=log)
|
||||
|
||||
32
mvt/android/cmd_check_androidqf.py
Normal file
32
mvt/android/cmd_check_androidqf.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.command import Command
|
||||
|
||||
from .modules.androidqf import ANDROIDQF_MODULES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdAndroidCheckAndroidQF(Command):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
) -> None:
|
||||
super().__init__(target_path=target_path, results_path=results_path,
|
||||
ioc_files=ioc_files, module_name=module_name,
|
||||
serial=serial, fast_mode=fast_mode, log=log)
|
||||
|
||||
self.name = "check-androidqf"
|
||||
self.modules = ANDROIDQF_MODULES
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
import sys
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
from rich.prompt import Prompt
|
||||
|
||||
@@ -25,9 +25,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CmdAndroidCheckBackup(Command):
|
||||
|
||||
def __init__(self, target_path: str = None, results_path: str = None,
|
||||
ioc_files: list = [], module_name: str = None,
|
||||
serial: str = None, fast_mode: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
) -> None:
|
||||
super().__init__(target_path=target_path, results_path=results_path,
|
||||
ioc_files=ioc_files, module_name=module_name,
|
||||
serial=serial, fast_mode=fast_mode, log=log)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
from zipfile import ZipFile
|
||||
|
||||
from mvt.common.command import Command
|
||||
@@ -18,9 +18,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CmdAndroidCheckBugreport(Command):
|
||||
|
||||
def __init__(self, target_path: str = None, results_path: str = None,
|
||||
ioc_files: list = [], module_name: str = None,
|
||||
serial: str = None, fast_mode: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
) -> None:
|
||||
super().__init__(target_path=target_path, results_path=results_path,
|
||||
ioc_files=ioc_files, module_name=module_name,
|
||||
serial=serial, fast_mode=fast_mode, log=log)
|
||||
@@ -52,3 +58,7 @@ class CmdAndroidCheckBugreport(Command):
|
||||
module.from_zip(self.bugreport_archive, self.bugreport_files)
|
||||
else:
|
||||
module.from_folder(self.target_path, self.bugreport_files)
|
||||
|
||||
def finish(self) -> None:
|
||||
if self.bugreport_archive:
|
||||
self.bugreport_archive.close()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
from rich.progress import track
|
||||
|
||||
@@ -25,8 +25,12 @@ class DownloadAPKs(AndroidExtraction):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, results_path: str = "", all_apks: bool = False,
|
||||
packages: list = []):
|
||||
def __init__(
|
||||
self,
|
||||
results_path: Optional[str] = None,
|
||||
all_apks: Optional[bool] = False,
|
||||
packages: Optional[list] = None
|
||||
) -> None:
|
||||
"""Initialize module.
|
||||
:param results_path: Path to the folder where data should be stored
|
||||
:param all_apks: Boolean indicating whether to download all packages
|
||||
@@ -78,9 +82,8 @@ class DownloadAPKs(AndroidExtraction):
|
||||
try:
|
||||
self._adb_download(remote_path, local_path)
|
||||
except InsufficientPrivileges:
|
||||
log.error("Unable to pull package file from %s: insufficient "
|
||||
"privileges, it might be a system app",
|
||||
remote_path)
|
||||
log.error("Unable to pull package file from %s: insufficient privileges, "
|
||||
"it might be a system app", remote_path)
|
||||
self._adb_reconnect()
|
||||
return None
|
||||
except Exception as exc:
|
||||
@@ -122,8 +125,8 @@ class DownloadAPKs(AndroidExtraction):
|
||||
if not package.get("system", False):
|
||||
packages_selection.append(package)
|
||||
|
||||
log.info("Selected only %d packages which are not marked as "
|
||||
"\"system\"", len(packages_selection))
|
||||
log.info("Selected only %d packages which are not marked as \"system\"",
|
||||
len(packages_selection))
|
||||
|
||||
if len(packages_selection) == 0:
|
||||
log.info("No packages were selected for download")
|
||||
|
||||
@@ -11,7 +11,7 @@ import string
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
from adb_shell.adb_device import AdbDeviceTcp, AdbDeviceUsb
|
||||
from adb_shell.auth.keygen import keygen, write_public_keyfile
|
||||
@@ -32,10 +32,15 @@ ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub")
|
||||
class AndroidExtraction(MVTModule):
|
||||
"""This class provides a base for all Android extraction modules."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -73,15 +78,13 @@ class AndroidExtraction(MVTModule):
|
||||
try:
|
||||
self.device = AdbDeviceUsb(serial=self.serial)
|
||||
except UsbDeviceNotFoundError:
|
||||
self.log.critical("No device found. Make sure it is connected "
|
||||
"and unlocked.")
|
||||
self.log.critical("No device found. Make sure it is connected and unlocked.")
|
||||
sys.exit(-1)
|
||||
# Otherwise we try to use the TCP transport.
|
||||
else:
|
||||
addr = self.serial.split(":")
|
||||
if len(addr) < 2:
|
||||
raise ValueError("TCP serial number must follow the format: "
|
||||
"`address:port`")
|
||||
raise ValueError("TCP serial number must follow the format: `address:port`")
|
||||
|
||||
self.device = AdbDeviceTcp(addr[0], int(addr[1]),
|
||||
default_transport_timeout_s=30.)
|
||||
@@ -90,12 +93,11 @@ class AndroidExtraction(MVTModule):
|
||||
try:
|
||||
self.device.connect(rsa_keys=[signer], auth_timeout_s=5)
|
||||
except (USBErrorBusy, USBErrorAccess):
|
||||
self.log.critical("Device is busy, maybe run `adb kill-server` "
|
||||
"and try again.")
|
||||
self.log.critical("Device is busy, maybe run `adb kill-server` and try again.")
|
||||
sys.exit(-1)
|
||||
except DeviceAuthError:
|
||||
self.log.error("You need to authorize this computer on the "
|
||||
"Android device. Retrying in 5 seconds...")
|
||||
self.log.error("You need to authorize this computer on the Android device. "
|
||||
"Retrying in 5 seconds...")
|
||||
time.sleep(5)
|
||||
except UsbReadFailedError:
|
||||
self.log.error("Unable to connect to the device over USB. "
|
||||
@@ -104,7 +106,7 @@ class AndroidExtraction(MVTModule):
|
||||
except OSError as exc:
|
||||
if exc.errno == 113 and self.serial:
|
||||
self.log.critical("Unable to connect to the device %s: "
|
||||
"did you specify the correct IP addres?",
|
||||
"did you specify the correct IP address?",
|
||||
self.serial)
|
||||
sys.exit(-1)
|
||||
else:
|
||||
@@ -169,9 +171,13 @@ class AndroidExtraction(MVTModule):
|
||||
|
||||
return bool(self._adb_command_as_root(f"[ ! -f {file} ] || echo 1"))
|
||||
|
||||
def _adb_download(self, remote_path: str, local_path: str,
|
||||
progress_callback: Callable = None,
|
||||
retry_root: bool = True) -> None:
|
||||
def _adb_download(
|
||||
self,
|
||||
remote_path: str,
|
||||
local_path: str,
|
||||
progress_callback: Optional[Callable] = None,
|
||||
retry_root: Optional[bool] = True
|
||||
) -> None:
|
||||
"""Download a file form the device.
|
||||
|
||||
:param remote_path: Path to download from the device
|
||||
@@ -190,8 +196,12 @@ class AndroidExtraction(MVTModule):
|
||||
else:
|
||||
raise Exception(f"Unable to download file {remote_path}: {exc}") from exc
|
||||
|
||||
def _adb_download_root(self, remote_path: str, local_path: str,
|
||||
progress_callback: Callable = None) -> None:
|
||||
def _adb_download_root(
|
||||
self,
|
||||
remote_path: str,
|
||||
local_path: str,
|
||||
progress_callback: Optional[Callable] = None
|
||||
) -> None:
|
||||
try:
|
||||
# Check if we have root, if not raise an Exception.
|
||||
self._adb_root_or_die()
|
||||
@@ -262,8 +272,8 @@ class AndroidExtraction(MVTModule):
|
||||
self._adb_command(f"rm -f {new_remote_path}")
|
||||
|
||||
def _generate_backup(self, package_name: str) -> bytes:
|
||||
self.log.warning("Please check phone and accept Android backup prompt. "
|
||||
"You may need to set a backup password. \a")
|
||||
self.log.info("Please check phone and accept Android backup prompt. "
|
||||
"You may need to set a backup password. \a")
|
||||
|
||||
# TODO: Base64 encoding as temporary fix to avoid byte-mangling over
|
||||
# the shell transport...
|
||||
@@ -288,10 +298,9 @@ class AndroidExtraction(MVTModule):
|
||||
backup_password)
|
||||
return decrypted_backup_tar
|
||||
except InvalidBackupPassword:
|
||||
self.log.error("You provided the wrong password! "
|
||||
"Please try again...")
|
||||
self.log.error("You provided the wrong password! Please try again...")
|
||||
|
||||
self.log.warn("All attempts to decrypt backup with password failed!")
|
||||
self.log.error("All attempts to decrypt backup with password failed!")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import (convert_chrometime_to_datetime,
|
||||
convert_datetime_to_iso)
|
||||
@@ -19,10 +19,15 @@ 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."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -70,7 +75,8 @@ class ChromeHistory(AndroidExtraction):
|
||||
"url": item[1],
|
||||
"visit_id": item[2],
|
||||
"timestamp": item[3],
|
||||
"isodate": convert_datetime_to_iso(convert_chrometime_to_datetime(item[3])),
|
||||
"isodate": convert_datetime_to_iso(
|
||||
convert_chrometime_to_datetime(item[3])),
|
||||
"redirect_source": item[4],
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_accessibility
|
||||
|
||||
@@ -13,10 +14,15 @@ from .base import AndroidExtraction
|
||||
class DumpsysAccessibility(AndroidExtraction):
|
||||
"""This module extracts stats on accessibility."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_activity_resolver_table
|
||||
|
||||
@@ -13,10 +14,15 @@ from .base import AndroidExtraction
|
||||
class DumpsysActivities(AndroidExtraction):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.android.parsers.dumpsys import parse_dumpsys_appops
|
||||
|
||||
@@ -16,10 +16,15 @@ class DumpsysAppOps(AndroidExtraction):
|
||||
|
||||
slug = "dumpsys_appops"
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_battery_daily
|
||||
|
||||
@@ -14,10 +14,15 @@ from .base import AndroidExtraction
|
||||
class DumpsysBatteryDaily(AndroidExtraction):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -49,4 +54,5 @@ class DumpsysBatteryDaily(AndroidExtraction):
|
||||
|
||||
self.results = parse_dumpsys_battery_daily(output)
|
||||
|
||||
self.log.info("Extracted %d records from battery daily stats", len(self.results))
|
||||
self.log.info("Extracted %d records from battery daily stats",
|
||||
len(self.results))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_battery_history
|
||||
|
||||
@@ -13,10 +14,15 @@ from .base import AndroidExtraction
|
||||
class DumpsysBatteryHistory(AndroidExtraction):
|
||||
"""This module extracts records from battery history events."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_dbinfo
|
||||
|
||||
@@ -15,10 +16,15 @@ class DumpsysDBInfo(AndroidExtraction):
|
||||
|
||||
slug = "dumpsys_dbinfo"
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
@@ -12,10 +13,15 @@ from .base import AndroidExtraction
|
||||
class DumpsysFull(AndroidExtraction):
|
||||
"""This module extracts stats on battery consumption by processes."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_receiver_resolver_table
|
||||
|
||||
@@ -19,10 +20,15 @@ INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
|
||||
class DumpsysReceivers(AndroidExtraction):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -36,24 +42,20 @@ class DumpsysReceivers(AndroidExtraction):
|
||||
for intent, receivers in self.results.items():
|
||||
for receiver in receivers:
|
||||
if intent == INTENT_NEW_OUTGOING_SMS:
|
||||
self.log.info("Found a receiver to intercept "
|
||||
"outgoing SMS messages: \"%s\"",
|
||||
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept "
|
||||
"incoming SMS messages: \"%s\"",
|
||||
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_DATA_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept "
|
||||
"incoming data SMS message: \"%s\"",
|
||||
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_PHONE_STATE:
|
||||
self.log.info("Found a receiver monitoring "
|
||||
"telephony state/incoming calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_NEW_OUTGOING_CALL:
|
||||
self.log.info("Found a receiver monitoring "
|
||||
"outgoing calls: \"%s\"",
|
||||
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
|
||||
ioc = self.indicators.check_app_id(receiver["package_name"])
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
import os
|
||||
import stat
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_unix_to_iso
|
||||
|
||||
@@ -25,10 +25,15 @@ ANDROID_MEDIA_FOLDERS = [
|
||||
class Files(AndroidExtraction):
|
||||
"""This module extracts the list of files on the device."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -48,8 +53,8 @@ class Files(AndroidExtraction):
|
||||
def check_indicators(self) -> None:
|
||||
for result in self.results:
|
||||
if result.get("is_suid"):
|
||||
self.log.warning("Found an SUID file in a non-standard "
|
||||
"directory \"%s\".", result["path"])
|
||||
self.log.warning("Found an SUID file in a non-standard directory \"%s\".",
|
||||
result["path"])
|
||||
|
||||
if self.indicators and self.indicators.check_file_path(result["path"]):
|
||||
self.log.warning("Found a known suspicous file at path: \"%s\"",
|
||||
@@ -124,8 +129,7 @@ class Files(AndroidExtraction):
|
||||
if self.fast_mode:
|
||||
self.log.info("Flag --fast was enabled: skipping full file listing")
|
||||
else:
|
||||
self.log.info("Processing full file listing. "
|
||||
"This may take a while...")
|
||||
self.log.info("Processing full file listing. This may take a while...")
|
||||
self.find_files("/")
|
||||
|
||||
self.log.info("Found %s total files", len(self.results))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_getprop
|
||||
|
||||
@@ -14,10 +15,15 @@ from .base import AndroidExtraction
|
||||
class Getprop(AndroidExtraction):
|
||||
"""This module extracts device properties from getprop command."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
@@ -12,10 +13,15 @@ from .base import AndroidExtraction
|
||||
class Logcat(AndroidExtraction):
|
||||
"""This module extracts details on installed packages."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from rich.console import Console
|
||||
from rich.progress import track
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from mvt.android.parsers.dumpsys import parse_dumpsys_package_for_details
|
||||
from mvt.common.virustotal import VTNoKey, VTQuotaExceeded, virustotal_lookup
|
||||
|
||||
from .base import AndroidExtraction
|
||||
@@ -38,7 +39,6 @@ DANGEROUS_PERMISSIONS = [
|
||||
"android.permission.USE_SIP",
|
||||
"com.android.browser.permission.READ_HISTORY_BOOKMARKS",
|
||||
]
|
||||
|
||||
ROOT_PACKAGES = [
|
||||
"com.noshufou.android.su",
|
||||
"com.noshufou.android.su.elite",
|
||||
@@ -66,15 +66,27 @@ ROOT_PACKAGES = [
|
||||
"com.kingouser.com",
|
||||
"com.topjohnwu.magisk",
|
||||
]
|
||||
SECURITY_PACKAGES = [
|
||||
"com.policydm",
|
||||
"com.samsung.android.app.omcagent",
|
||||
"com.samsung.android.securitylogagent",
|
||||
"com.sec.android.soagent",
|
||||
"com.wssyncmldm",
|
||||
]
|
||||
|
||||
|
||||
class Packages(AndroidExtraction):
|
||||
"""This module extracts the list of installed packages."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -117,6 +129,10 @@ class Packages(AndroidExtraction):
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if result["package_name"] in SECURITY_PACKAGES and result["disabled"]:
|
||||
self.log.warning("Found a security package disabled: \"%s\"",
|
||||
result["package_name"])
|
||||
|
||||
if not self.indicators:
|
||||
continue
|
||||
|
||||
@@ -187,43 +203,17 @@ class Packages(AndroidExtraction):
|
||||
|
||||
@staticmethod
|
||||
def parse_package_for_details(output: str) -> dict:
|
||||
details = {
|
||||
"uid": "",
|
||||
"version_name": "",
|
||||
"version_code": "",
|
||||
"timestamp": "",
|
||||
"first_install_time": "",
|
||||
"last_update_time": "",
|
||||
"requested_permissions": [],
|
||||
}
|
||||
|
||||
in_permissions = False
|
||||
lines = []
|
||||
in_packages = False
|
||||
for line in output.splitlines():
|
||||
if in_permissions:
|
||||
if line.startswith(" " * 4) and not line.startswith(" " * 6):
|
||||
in_permissions = False
|
||||
continue
|
||||
if in_packages:
|
||||
if line.strip() == "":
|
||||
break
|
||||
lines.append(line)
|
||||
if line.strip() == "Packages:":
|
||||
in_packages = True
|
||||
|
||||
permission = line.strip().split(":")[0]
|
||||
details["requested_permissions"].append(permission)
|
||||
|
||||
if line.strip().startswith("userId="):
|
||||
details["uid"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionName="):
|
||||
details["version_name"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionCode="):
|
||||
details["version_code"] = line.split("=", 1)[1].strip()
|
||||
elif line.strip().startswith("timeStamp="):
|
||||
details["timestamp"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("firstInstallTime="):
|
||||
details["first_install_time"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("lastUpdateTime="):
|
||||
details["last_update_time"] = line.split("=")[1].strip()
|
||||
elif line.strip() == "requested permissions:":
|
||||
in_permissions = True
|
||||
continue
|
||||
|
||||
return details
|
||||
return parse_dumpsys_package_for_details("\n".join(lines))
|
||||
|
||||
def _get_files_for_package(self, package_name: str) -> list:
|
||||
output = self._adb_command(f"pm path {package_name}")
|
||||
@@ -235,10 +225,14 @@ class Packages(AndroidExtraction):
|
||||
for file_path in output.splitlines():
|
||||
file_path = file_path.strip()
|
||||
|
||||
md5 = self._adb_command(f"md5sum {file_path}").split(" ", maxsplit=1)[0]
|
||||
sha1 = self._adb_command(f"sha1sum {file_path}").split(" ", maxsplit=1)[0]
|
||||
sha256 = self._adb_command(f"sha256sum {file_path}").split(" ", maxsplit=1)[0]
|
||||
sha512 = self._adb_command(f"sha512sum {file_path}").split(" ", maxsplit=1)[0]
|
||||
md5 = self._adb_command(
|
||||
f"md5sum {file_path}").split(" ", maxsplit=1)[0]
|
||||
sha1 = self._adb_command(
|
||||
f"sha1sum {file_path}").split(" ", maxsplit=1)[0]
|
||||
sha256 = self._adb_command(
|
||||
f"sha256sum {file_path}").split(" ", maxsplit=1)[0]
|
||||
sha512 = self._adb_command(
|
||||
f"sha512sum {file_path}").split(" ", maxsplit=1)[0]
|
||||
|
||||
package_files.append({
|
||||
"path": file_path,
|
||||
@@ -282,7 +276,8 @@ class Packages(AndroidExtraction):
|
||||
"files": package_files,
|
||||
}
|
||||
|
||||
dumpsys_package = self._adb_command(f"dumpsys package {package_name}")
|
||||
dumpsys_package = self._adb_command(
|
||||
f"dumpsys package {package_name}")
|
||||
package_details = self.parse_package_for_details(dumpsys_package)
|
||||
new_package.update(package_details)
|
||||
|
||||
@@ -326,9 +321,9 @@ class Packages(AndroidExtraction):
|
||||
continue
|
||||
|
||||
packages_to_lookup.append(result)
|
||||
self.log.info("Found non-system package with name \"%s\" installed "
|
||||
"by \"%s\" on %s", result["package_name"],
|
||||
result["installer"], result["timestamp"])
|
||||
self.log.info("Found non-system package with name \"%s\" installed by \"%s\" on %s",
|
||||
result["package_name"], result["installer"],
|
||||
result["timestamp"])
|
||||
|
||||
if not self.fast_mode:
|
||||
self.check_virustotal(packages_to_lookup)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
@@ -11,10 +12,15 @@ from .base import AndroidExtraction
|
||||
class Processes(AndroidExtraction):
|
||||
"""This module extracts details on running processes."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -24,7 +30,21 @@ class Processes(AndroidExtraction):
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result.get("name", ""))
|
||||
proc_name = result.get("proc_name", "")
|
||||
if not proc_name:
|
||||
continue
|
||||
|
||||
# Skipping this process because of false positives.
|
||||
if result["proc_name"] == "gatekeeperd":
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_app_id(proc_name)
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_process(proc_name)
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
@@ -32,7 +52,7 @@ class Processes(AndroidExtraction):
|
||||
def run(self) -> None:
|
||||
self._adb_connect()
|
||||
|
||||
output = self._adb_command("ps -e")
|
||||
output = self._adb_command("ps -A")
|
||||
|
||||
for line in output.splitlines()[1:]:
|
||||
line = line.strip()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
@@ -11,10 +12,15 @@ from .base import AndroidExtraction
|
||||
class RootBinaries(AndroidExtraction):
|
||||
"""This module extracts the list of installed packages."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
@@ -13,10 +14,15 @@ class SELinuxStatus(AndroidExtraction):
|
||||
|
||||
slug = "selinux_status"
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
@@ -59,10 +60,15 @@ ANDROID_DANGEROUS_SETTINGS = [
|
||||
class Settings(AndroidExtraction):
|
||||
"""This module extracts Android system settings."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.android.parsers.backup import (AndroidBackupParsingError,
|
||||
parse_tar_for_sms)
|
||||
@@ -45,10 +45,15 @@ FROM sms;
|
||||
class SMS(AndroidExtraction):
|
||||
"""This module extracts all SMS messages containing links."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -153,7 +158,7 @@ class SMS(AndroidExtraction):
|
||||
except InsufficientPrivileges:
|
||||
pass
|
||||
|
||||
self.log.warn("No SMS database found. Trying extraction of SMS data "
|
||||
self.log.info("No SMS database found. Trying extraction of SMS data "
|
||||
"using Android backup feature.")
|
||||
self._extract_sms_adb()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import base64
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import check_for_links, convert_unix_to_iso
|
||||
|
||||
@@ -19,10 +19,15 @@ WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db"
|
||||
class Whatsapp(AndroidExtraction):
|
||||
"""This module extracts all WhatsApp messages containing links."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -75,17 +80,19 @@ class Whatsapp(AndroidExtraction):
|
||||
|
||||
# If we find links in the messages or if they are empty we add them
|
||||
# to the list.
|
||||
if check_for_links(message["data"]) or message["data"].strip() == "":
|
||||
if (check_for_links(message["data"])
|
||||
or message["data"].strip() == ""):
|
||||
if message.get("thumb_image"):
|
||||
message["thumb_image"] = base64.b64encode(message["thumb_image"])
|
||||
message["thumb_image"] = base64.b64encode(
|
||||
message["thumb_image"])
|
||||
|
||||
messages.append(message)
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
self.log.info("Extracted a total of %d WhatsApp messages "
|
||||
"containing links", len(messages))
|
||||
self.log.info("Extracted a total of %d WhatsApp messages containing links",
|
||||
len(messages))
|
||||
self.results = messages
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
15
mvt/android/modules/androidqf/__init__.py
Normal file
15
mvt/android/modules/androidqf/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
from .dumpsys_accessibility import DumpsysAccessibility
|
||||
from .dumpsys_activities import DumpsysActivities
|
||||
from .dumpsys_appops import DumpsysAppops
|
||||
from .dumpsys_receivers import DumpsysReceivers
|
||||
from .getprop import Getprop
|
||||
from .processes import Processes
|
||||
from .settings import Settings
|
||||
|
||||
ANDROIDQF_MODULES = [DumpsysActivities, DumpsysReceivers, DumpsysAccessibility,
|
||||
DumpsysAppops, Processes, Getprop, Settings]
|
||||
38
mvt/android/modules/androidqf/base.py
Normal file
38
mvt/android/modules/androidqf/base.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.module import MVTModule
|
||||
|
||||
|
||||
class AndroidQFModule(MVTModule):
|
||||
"""This class provides a base for all Android Data analysis modules."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self._path = target_path
|
||||
self._files = []
|
||||
|
||||
for root, dirs, files in os.walk(target_path):
|
||||
for name in files:
|
||||
self._files.append(os.path.join(root, name))
|
||||
|
||||
def _get_files_by_pattern(self, pattern):
|
||||
return fnmatch.filter(self._files, pattern)
|
||||
68
mvt/android/modules/androidqf/dumpsys_accessibility.py
Normal file
68
mvt/android/modules/androidqf/dumpsys_accessibility.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_accessibility
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysAccessibility(AndroidQFModule):
|
||||
"""This module analyse dumpsys accessbility"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
ioc = self.indicators.check_app_id(result["package_name"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_accessibility = False
|
||||
with open(dumpsys_file[0]) as handle:
|
||||
for line in handle:
|
||||
if line.strip().startswith("DUMP OF SERVICE accessibility:"):
|
||||
in_accessibility = True
|
||||
continue
|
||||
|
||||
if not in_accessibility:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("-------------------------------------------------------------------------------"): # pylint: disable=line-too-long
|
||||
break
|
||||
|
||||
lines.append(line.rstrip())
|
||||
|
||||
self.results = parse_dumpsys_accessibility("\n".join(lines))
|
||||
|
||||
for result in self.results:
|
||||
self.log.info("Found installed accessibility service \"%s\"",
|
||||
result.get("service"))
|
||||
|
||||
self.log.info("Identified a total of %d accessibility services",
|
||||
len(self.results))
|
||||
66
mvt/android/modules/androidqf/dumpsys_activities.py
Normal file
66
mvt/android/modules/androidqf/dumpsys_activities.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_activity_resolver_table
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysActivities(AndroidQFModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = results if results else {}
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for intent, activities in self.results.items():
|
||||
for activity in activities:
|
||||
ioc = self.indicators.check_app_id(activity["package_name"])
|
||||
if ioc:
|
||||
activity["matched_indicator"] = ioc
|
||||
self.detected.append({intent: activity})
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_package = False
|
||||
with open(dumpsys_file[0]) as handle:
|
||||
for line in handle:
|
||||
if line.strip() == "DUMP OF SERVICE package:":
|
||||
in_package = True
|
||||
continue
|
||||
|
||||
if not in_package:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"): # pylint: disable=line-too-long
|
||||
break
|
||||
|
||||
lines.append(line.rstrip())
|
||||
|
||||
self.results = parse_dumpsys_activity_resolver_table("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted activities for %d intents", len(self.results))
|
||||
83
mvt/android/modules/androidqf/dumpsys_appops.py
Normal file
83
mvt/android/modules/androidqf/dumpsys_appops.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_appops
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysAppops(AndroidQFModule):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def serialize(self, record: dict) -> Union[dict, list]:
|
||||
records = []
|
||||
for perm in record["permissions"]:
|
||||
if "entries" not in perm:
|
||||
continue
|
||||
|
||||
for entry in perm["entries"]:
|
||||
if "timestamp" in entry:
|
||||
records.append({
|
||||
"timestamp": entry["timestamp"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": entry["access"],
|
||||
"data": f"{record['package_name']} access to "
|
||||
f"{perm['name']} : {entry['access']}",
|
||||
})
|
||||
|
||||
return records
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
for result in self.results:
|
||||
if self.indicators:
|
||||
ioc = self.indicators.check_app_id(result.get("package_name"))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
for perm in result["permissions"]:
|
||||
if (perm["name"] == "REQUEST_INSTALL_PACKAGES"
|
||||
and perm["access"] == "allow"):
|
||||
self.log.info("Package %s with REQUEST_INSTALL_PACKAGES permission",
|
||||
result["package_name"])
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
lines = []
|
||||
in_package = False
|
||||
with open(dumpsys_file[0]) as handle:
|
||||
for line in handle:
|
||||
if line.startswith("DUMP OF SERVICE appops:"):
|
||||
in_package = True
|
||||
continue
|
||||
|
||||
if in_package:
|
||||
if line.startswith("-------------------------------------------------------------------------------"): # pylint: disable=line-too-long
|
||||
break
|
||||
|
||||
lines.append(line.rstrip())
|
||||
|
||||
self.results = parse_dumpsys_appops("\n".join(lines))
|
||||
self.log.info("Identified %d applications in AppOps Manager",
|
||||
len(self.results))
|
||||
108
mvt/android/modules/androidqf/dumpsys_packages.py
Normal file
108
mvt/android/modules/androidqf/dumpsys_packages.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.android.modules.adb.packages import (DANGEROUS_PERMISSIONS,
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD,
|
||||
ROOT_PACKAGES)
|
||||
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
|
||||
from mvt.common.utils import convert_datetime_to_iso
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysPackages(AndroidQFModule):
|
||||
"""This module analyse dumpsys packages"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def serialize(self, record: dict) -> Union[dict, list]:
|
||||
entries = []
|
||||
for entry in ["timestamp", "first_install_time", "last_update_time"]:
|
||||
if entry in record:
|
||||
entries.append({
|
||||
"timestamp": record[entry],
|
||||
"module": self.__class__.__name__,
|
||||
"event": entry,
|
||||
"data": f"Package {record['package_name']} "
|
||||
f"({record['uid']})",
|
||||
})
|
||||
|
||||
return entries
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
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"])
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if not self.indicators:
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_app_id(result.get("package_name", ""))
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if len(dumpsys_file) != 1:
|
||||
self.log.info("Dumpsys file not found")
|
||||
return
|
||||
|
||||
with open(dumpsys_file[0]) as handle:
|
||||
data = handle.read().split("\n")
|
||||
|
||||
package = []
|
||||
in_service = False
|
||||
in_package_list = False
|
||||
for line in data:
|
||||
if line.strip().startswith("DUMP OF SERVICE package:"):
|
||||
in_service = True
|
||||
continue
|
||||
|
||||
if in_service and line.startswith("Packages:"):
|
||||
in_package_list = True
|
||||
continue
|
||||
|
||||
if not in_service or not in_package_list:
|
||||
continue
|
||||
|
||||
if line.strip() == "":
|
||||
break
|
||||
|
||||
package.append(line)
|
||||
|
||||
self.results = parse_dumpsys_packages("\n".join(package))
|
||||
|
||||
for result in self.results:
|
||||
dangerous_permissions_count = 0
|
||||
for perm in result["permissions"]:
|
||||
if perm["name"] in DANGEROUS_PERMISSIONS:
|
||||
dangerous_permissions_count += 1
|
||||
|
||||
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
|
||||
self.log.info("Found package \"%s\" requested %d potentially dangerous permissions",
|
||||
result["package_name"],
|
||||
dangerous_permissions_count)
|
||||
|
||||
self.log.info("Extracted details on %d packages", len(self.results))
|
||||
86
mvt/android/modules/androidqf/dumpsys_receivers.py
Normal file
86
mvt/android/modules/androidqf/dumpsys_receivers.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.modules.adb.dumpsys_receivers import (
|
||||
INTENT_DATA_SMS_RECEIVED, INTENT_NEW_OUTGOING_CALL,
|
||||
INTENT_NEW_OUTGOING_SMS, INTENT_PHONE_STATE, INTENT_SMS_RECEIVED)
|
||||
from mvt.android.parsers import parse_dumpsys_receiver_resolver_table
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysReceivers(AndroidQFModule):
|
||||
"""This module analyse dumpsys receivers"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.results = results if results else {}
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for intent, receivers in self.results.items():
|
||||
for receiver in receivers:
|
||||
if intent == INTENT_NEW_OUTGOING_SMS:
|
||||
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_DATA_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_PHONE_STATE:
|
||||
self.log.info("Found a receiver monitoring "
|
||||
"telephony state/incoming calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_NEW_OUTGOING_CALL:
|
||||
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
|
||||
ioc = self.indicators.check_app_id(receiver["package_name"])
|
||||
if ioc:
|
||||
receiver["matched_indicator"] = ioc
|
||||
self.detected.append({intent: receiver})
|
||||
|
||||
def run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
in_receivers = False
|
||||
lines = []
|
||||
with open(dumpsys_file[0]) as handle:
|
||||
for line in handle:
|
||||
if line.strip() == "DUMP OF SERVICE package:":
|
||||
in_receivers = True
|
||||
continue
|
||||
|
||||
if not in_receivers:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"): # pylint: disable=line-too-long
|
||||
break
|
||||
|
||||
lines.append(line.rstrip())
|
||||
|
||||
self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines))
|
||||
|
||||
self.log.info("Extracted receivers for %d intents", len(self.results))
|
||||
66
mvt/android/modules/androidqf/getprop.py
Normal file
66
mvt/android/modules/androidqf/getprop.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import getprop
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
INTERESTING_PROPERTIES = [
|
||||
"gsm.sim.operator.alpha",
|
||||
"gsm.sim.operator.iso-country",
|
||||
"persist.sys.timezone",
|
||||
"ro.boot.serialno",
|
||||
"ro.build.version.sdk",
|
||||
"ro.build.version.security_patch",
|
||||
"ro.product.cpu.abi",
|
||||
"ro.product.locale",
|
||||
"ro.product.vendor.manufacturer",
|
||||
"ro.product.vendor.model",
|
||||
"ro.product.vendor.name"
|
||||
]
|
||||
|
||||
|
||||
class Getprop(AndroidQFModule):
|
||||
"""This module extracts data from get properties."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
self.results = {}
|
||||
|
||||
def run(self) -> None:
|
||||
getprop_files = self._get_files_by_pattern("*/getprop.txt")
|
||||
if not getprop_files:
|
||||
self.log.info("getprop.txt file not found")
|
||||
return
|
||||
|
||||
with open(getprop_files[0]) as f:
|
||||
data = f.read()
|
||||
|
||||
self.results = getprop.parse_getprop(data)
|
||||
for entry in self.results:
|
||||
if entry in INTERESTING_PROPERTIES:
|
||||
self.log.info("%s: %s", entry, self.results[entry])
|
||||
if entry == "ro.build.version.security_patch":
|
||||
last_patch = datetime.strptime(self.results[entry], "%Y-%m-%d")
|
||||
if (datetime.now() - last_patch) > timedelta(days=6*31):
|
||||
self.log.warning("This phone has not received security "
|
||||
"updates for more than six months "
|
||||
"(last update: %s)", self.results[entry])
|
||||
|
||||
self.log.info("Extracted a total of %d properties", len(self.results))
|
||||
92
mvt/android/modules/androidqf/processes.py
Normal file
92
mvt/android/modules/androidqf/processes.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class Processes(AndroidQFModule):
|
||||
"""This module analyse running processes"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
proc_name = result.get("proc_name", "")
|
||||
if not proc_name:
|
||||
continue
|
||||
|
||||
# Skipping this process because of false positives.
|
||||
if result["proc_name"] == "gatekeeperd":
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_app_id(proc_name)
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_process(proc_name)
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def _parse_ps(self, data):
|
||||
for line in data.split("\n")[1:]:
|
||||
proc = line.split()
|
||||
|
||||
# Sometimes WCHAN is empty.
|
||||
if len(proc) == 8:
|
||||
proc = proc[:5] + [''] + proc[5:]
|
||||
|
||||
# Sometimes there is the security label.
|
||||
if proc[0].startswith("u:r"):
|
||||
label = proc[0]
|
||||
proc = proc[1:]
|
||||
else:
|
||||
label = ""
|
||||
|
||||
# Sometimes there is no WCHAN.
|
||||
if len(proc) < 9:
|
||||
proc = proc[:5] + [""] + proc[5:]
|
||||
|
||||
self.results.append({
|
||||
"user": proc[0],
|
||||
"pid": int(proc[1]),
|
||||
"ppid": int(proc[2]),
|
||||
"virtual_memory_size": int(proc[3]),
|
||||
"resident_set_size": int(proc[4]),
|
||||
"wchan": proc[5],
|
||||
"aprocress": proc[6],
|
||||
"stat": proc[7],
|
||||
"proc_name": proc[8].strip("[]"),
|
||||
"label": label,
|
||||
})
|
||||
|
||||
def run(self) -> None:
|
||||
ps_files = self._get_files_by_pattern("*/ps.txt")
|
||||
if not ps_files:
|
||||
return
|
||||
|
||||
with open(ps_files[0]) as handle:
|
||||
self._parse_ps(handle.read())
|
||||
|
||||
self.log.info("Identified %d running processes", len(self.results))
|
||||
58
mvt/android/modules/androidqf/settings.py
Normal file
58
mvt/android/modules/androidqf/settings.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2022 Claudio Guarnieri.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.modules.adb.settings import ANDROID_DANGEROUS_SETTINGS
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class Settings(AndroidQFModule):
|
||||
"""This module analyse setting files"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
self.results = {}
|
||||
|
||||
def run(self) -> None:
|
||||
for setting_file in self._get_files_by_pattern("*/settings_*.txt"):
|
||||
namespace = setting_file[setting_file.rfind("_")+1:-4]
|
||||
|
||||
self.results[namespace] = {}
|
||||
|
||||
with open(setting_file) as handle:
|
||||
for line in handle:
|
||||
line = line.strip()
|
||||
try:
|
||||
key, value = line.split("=", 1)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
try:
|
||||
self.results[namespace][key] = value
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
for danger in ANDROID_DANGEROUS_SETTINGS:
|
||||
if (danger["key"] == key
|
||||
and danger["safe_value"] != value):
|
||||
self.log.warning("Found suspicious setting \"%s = %s\" (%s)",
|
||||
key, value, danger["description"])
|
||||
break
|
||||
|
||||
self.log.info("Identified %d settings",
|
||||
sum([len(val) for val in self.results.values()]))
|
||||
@@ -7,6 +7,7 @@ import fnmatch
|
||||
import logging
|
||||
import os
|
||||
from tarfile import TarFile
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.module import MVTModule
|
||||
|
||||
@@ -14,14 +15,18 @@ from mvt.common.module import MVTModule
|
||||
class BackupExtraction(MVTModule):
|
||||
"""This class provides a base for all backup extractios modules"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
self.ab = None
|
||||
self.backup_path = None
|
||||
self.tar = None
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.modules.backup.base import BackupExtraction
|
||||
from mvt.android.parsers.backup import parse_sms_file
|
||||
|
||||
|
||||
class SMS(BackupExtraction):
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -43,5 +50,5 @@ class SMS(BackupExtraction):
|
||||
data = self._get_file_content(file)
|
||||
self.results.extend(parse_sms_file(data))
|
||||
|
||||
self.log.info("Extracted a total of %d SMS & MMS messages "
|
||||
"containing links", len(self.results))
|
||||
self.log.info("Extracted a total of %d SMS & MMS messages containing links",
|
||||
len(self.results))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_accessibility
|
||||
|
||||
@@ -13,10 +14,15 @@ from .base import BugReportModule
|
||||
class Accessibility(BugReportModule):
|
||||
"""This module extracts stats on accessibility."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -35,8 +41,8 @@ class Accessibility(BugReportModule):
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a "
|
||||
"valid bug report archive?")
|
||||
self.log.error("Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
@@ -49,7 +55,7 @@ class Accessibility(BugReportModule):
|
||||
if not in_accessibility:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"): # pylint: disable=line-too-long
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_activity_resolver_table
|
||||
|
||||
@@ -13,10 +14,15 @@ from .base import BugReportModule
|
||||
class Activities(BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -38,8 +44,8 @@ class Activities(BugReportModule):
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a "
|
||||
"valid bug report archive?")
|
||||
self.log.error("Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
@@ -52,7 +58,7 @@ class Activities(BugReportModule):
|
||||
if not in_package:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"): # pylint: disable=line-too-long
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_appops
|
||||
|
||||
@@ -14,10 +14,15 @@ from .base import BugReportModule
|
||||
class Appops(BugReportModule):
|
||||
"""This module extracts information on package from App-Ops Manager."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -58,8 +63,8 @@ class Appops(BugReportModule):
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a "
|
||||
"valid bug report archive?")
|
||||
self.log.error("Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
@@ -72,7 +77,7 @@ class Appops(BugReportModule):
|
||||
if not in_appops:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"): # pylint: disable=line-too-long
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
from zipfile import ZipFile
|
||||
|
||||
from mvt.common.module import MVTModule
|
||||
@@ -14,10 +15,15 @@ from mvt.common.module import MVTModule
|
||||
class BugReportModule(MVTModule):
|
||||
"""This class provides a base for all Android Bug Report modules."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -51,6 +57,8 @@ class BugReportModule(MVTModule):
|
||||
if matches:
|
||||
return matches
|
||||
|
||||
return []
|
||||
|
||||
def _get_file_content(self, file_path: str) -> bytes:
|
||||
if self.zip_archive:
|
||||
handle = self.zip_archive.open(file_path)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_battery_daily
|
||||
|
||||
@@ -14,10 +14,15 @@ from .base import BugReportModule
|
||||
class BatteryDaily(BugReportModule):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -45,8 +50,8 @@ class BatteryDaily(BugReportModule):
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a "
|
||||
"valid bug report archive?")
|
||||
self.log.error("Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_battery_history
|
||||
|
||||
@@ -13,10 +14,15 @@ from .base import BugReportModule
|
||||
class BatteryHistory(BugReportModule):
|
||||
"""This module extracts records from battery daily updates."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -35,8 +41,8 @@ class BatteryHistory(BugReportModule):
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide "
|
||||
"a valid bug report archive?")
|
||||
self.log.error("Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_dbinfo
|
||||
|
||||
@@ -15,10 +16,15 @@ class DBInfo(BugReportModule):
|
||||
|
||||
slug = "dbinfo"
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -39,8 +45,8 @@ class DBInfo(BugReportModule):
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a "
|
||||
"valid bug report archive?")
|
||||
self.log.error("Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
in_dbinfo = False
|
||||
@@ -53,7 +59,7 @@ class DBInfo(BugReportModule):
|
||||
if not in_dbinfo:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"): # pylint: disable=line-too-long
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_getprop
|
||||
|
||||
@@ -14,10 +15,15 @@ from .base import BugReportModule
|
||||
class Getprop(BugReportModule):
|
||||
"""This module extracts device properties from getprop command."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -27,8 +33,8 @@ class Getprop(BugReportModule):
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a "
|
||||
"valid bug report archive?")
|
||||
self.log.error("Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
lines = []
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.android.modules.adb.packages import (DANGEROUS_PERMISSIONS,
|
||||
DANGEROUS_PERMISSIONS_THRESHOLD,
|
||||
ROOT_PACKAGES)
|
||||
from mvt.android.parsers.dumpsys import parse_dumpsys_packages
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
@@ -17,10 +17,15 @@ from .base import BugReportModule
|
||||
class Packages(BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -71,94 +76,11 @@ class Packages(BugReportModule):
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
@staticmethod
|
||||
def parse_package_for_details(output: str) -> dict:
|
||||
details = {
|
||||
"uid": "",
|
||||
"version_name": "",
|
||||
"version_code": "",
|
||||
"timestamp": "",
|
||||
"first_install_time": "",
|
||||
"last_update_time": "",
|
||||
"requested_permissions": [],
|
||||
}
|
||||
|
||||
in_install_permissions = False
|
||||
in_runtime_permissions = False
|
||||
for line in output.splitlines():
|
||||
if in_install_permissions:
|
||||
if line.startswith(" " * 4) and not line.startswith(" " * 6):
|
||||
in_install_permissions = False
|
||||
continue
|
||||
|
||||
permission = line.strip().split(":")[0]
|
||||
if permission not in details["requested_permissions"]:
|
||||
details["requested_permissions"].append(permission)
|
||||
|
||||
if in_runtime_permissions:
|
||||
if not line.startswith(" " * 8):
|
||||
in_runtime_permissions = False
|
||||
continue
|
||||
|
||||
permission = line.strip().split(":")[0]
|
||||
if permission not in details["requested_permissions"]:
|
||||
details["requested_permissions"].append(permission)
|
||||
|
||||
if line.strip().startswith("userId="):
|
||||
details["uid"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionName="):
|
||||
details["version_name"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionCode="):
|
||||
details["version_code"] = line.split("=", 1)[1].strip()
|
||||
elif line.strip().startswith("timeStamp="):
|
||||
details["timestamp"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("firstInstallTime="):
|
||||
details["first_install_time"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("lastUpdateTime="):
|
||||
details["last_update_time"] = line.split("=")[1].strip()
|
||||
elif line.strip() == "install permissions:":
|
||||
in_install_permissions = True
|
||||
elif line.strip() == "runtime permissions:":
|
||||
in_runtime_permissions = True
|
||||
|
||||
return details
|
||||
|
||||
def parse_packages_list(self, output: str) -> list:
|
||||
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
|
||||
|
||||
results = []
|
||||
package_name = None
|
||||
package = {}
|
||||
lines = []
|
||||
for line in output.splitlines():
|
||||
if line.startswith(" Package ["):
|
||||
if len(lines) > 0:
|
||||
details = self.parse_package_for_details("\n".join(lines))
|
||||
package.update(details)
|
||||
results.append(package)
|
||||
lines = []
|
||||
package = {}
|
||||
|
||||
matches = pkg_rxp.findall(line)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
package_name = matches[0]
|
||||
package["package_name"] = package_name
|
||||
continue
|
||||
|
||||
if not package_name:
|
||||
continue
|
||||
|
||||
lines.append(line)
|
||||
|
||||
return results
|
||||
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a "
|
||||
"valid bug report archive?")
|
||||
self.log.error("Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
in_package = False
|
||||
@@ -184,17 +106,17 @@ class Packages(BugReportModule):
|
||||
|
||||
lines.append(line)
|
||||
|
||||
self.results = self.parse_packages_list("\n".join(lines))
|
||||
self.results = parse_dumpsys_packages("\n".join(lines))
|
||||
|
||||
for result in self.results:
|
||||
dangerous_permissions_count = 0
|
||||
for perm in result["requested_permissions"]:
|
||||
if perm in DANGEROUS_PERMISSIONS:
|
||||
for perm in result["permissions"]:
|
||||
if perm["name"] in DANGEROUS_PERMISSIONS:
|
||||
dangerous_permissions_count += 1
|
||||
|
||||
if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD:
|
||||
self.log.info("Found package \"%s\" requested %d potentially "
|
||||
"dangerous permissions", result["package_name"],
|
||||
self.log.info("Found package \"%s\" requested %d potentially dangerous permissions",
|
||||
result["package_name"],
|
||||
dangerous_permissions_count)
|
||||
|
||||
self.log.info("Extracted details on %d packages", len(self.results))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.parsers import parse_dumpsys_receiver_resolver_table
|
||||
|
||||
@@ -19,10 +20,15 @@ INTENT_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"
|
||||
class Receivers(BugReportModule):
|
||||
"""This module extracts details on receivers for risky activities."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -36,24 +42,20 @@ class Receivers(BugReportModule):
|
||||
for intent, receivers in self.results.items():
|
||||
for receiver in receivers:
|
||||
if intent == INTENT_NEW_OUTGOING_SMS:
|
||||
self.log.info("Found a receiver to intercept "
|
||||
"outgoing SMS messages: \"%s\"",
|
||||
self.log.info("Found a receiver to intercept outgoing SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept "
|
||||
"incoming SMS messages: \"%s\"",
|
||||
self.log.info("Found a receiver to intercept incoming SMS messages: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_DATA_SMS_RECEIVED:
|
||||
self.log.info("Found a receiver to intercept "
|
||||
"incoming data SMS message: \"%s\"",
|
||||
self.log.info("Found a receiver to intercept incoming data SMS message: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_PHONE_STATE:
|
||||
self.log.info("Found a receiver monitoring "
|
||||
"telephony state/incoming calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
elif intent == INTENT_NEW_OUTGOING_CALL:
|
||||
self.log.info("Found a receiver monitoring "
|
||||
"outgoing calls: \"%s\"",
|
||||
self.log.info("Found a receiver monitoring outgoing calls: \"%s\"",
|
||||
receiver["receiver"])
|
||||
|
||||
ioc = self.indicators.check_app_id(receiver["package_name"])
|
||||
@@ -65,8 +67,8 @@ class Receivers(BugReportModule):
|
||||
def run(self) -> None:
|
||||
content = self._get_dumpstate_file()
|
||||
if not content:
|
||||
self.log.error("Unable to find dumpstate file. Did you provide a "
|
||||
"valid bug report archive?")
|
||||
self.log.error("Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?")
|
||||
return
|
||||
|
||||
in_receivers = False
|
||||
@@ -79,7 +81,7 @@ class Receivers(BugReportModule):
|
||||
if not in_receivers:
|
||||
continue
|
||||
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"):
|
||||
if line.strip().startswith("------------------------------------------------------------------------------"): # pylint: disable=line-too-long
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
@@ -169,6 +169,17 @@ def parse_dumpsys_battery_history(output: str) -> list:
|
||||
continue
|
||||
|
||||
package_name = service.split("/")[0]
|
||||
elif (line.find("+top=") > 0) or (line.find("-top") > 0):
|
||||
if line.find("+top=") > 0:
|
||||
event = "start_top"
|
||||
top_pos = line.find("+top=")
|
||||
else:
|
||||
event = "end_top"
|
||||
top_pos = line.find("-top=")
|
||||
colon_pos = top_pos+line[top_pos:].find(":")
|
||||
uid = line[top_pos+5:colon_pos]
|
||||
service = ""
|
||||
package_name = line[colon_pos+1:].strip('"')
|
||||
else:
|
||||
continue
|
||||
|
||||
@@ -186,8 +197,8 @@ def parse_dumpsys_battery_history(output: str) -> list:
|
||||
def parse_dumpsys_dbinfo(output: str) -> list:
|
||||
results = []
|
||||
|
||||
rxp = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\].*\[Pid:\((\d+)\)\](\w+).*sql\=\"(.+?)\"')
|
||||
rxp_no_pid = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\][ ]{1}(\w+).*sql\=\"(.+?)\"')
|
||||
rxp = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\].*\[Pid:\((\d+)\)\](\w+).*sql\=\"(.+?)\"') # pylint: disable=line-too-long
|
||||
rxp_no_pid = re.compile(r'.*\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3})\][ ]{1}(\w+).*sql\=\"(.+?)\"') # pylint: disable=line-too-long
|
||||
|
||||
pool = None
|
||||
in_operations = False
|
||||
@@ -376,3 +387,134 @@ def parse_dumpsys_appops(output: str) -> list:
|
||||
results.append(package)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_dumpsys_package_for_details(output: str) -> dict:
|
||||
"""
|
||||
Parse one entry of a dumpsys package information
|
||||
"""
|
||||
details = {
|
||||
"uid": "",
|
||||
"version_name": "",
|
||||
"version_code": "",
|
||||
"timestamp": "",
|
||||
"first_install_time": "",
|
||||
"last_update_time": "",
|
||||
"permissions": [],
|
||||
"requested_permissions": [],
|
||||
}
|
||||
|
||||
in_install_permissions = False
|
||||
in_runtime_permissions = False
|
||||
in_declared_permissions = False
|
||||
in_requested_permissions = True
|
||||
for line in output.splitlines():
|
||||
if in_install_permissions:
|
||||
if line.startswith(" " * 4) and not line.startswith(" " * 6):
|
||||
in_install_permissions = False
|
||||
else:
|
||||
lineinfo = line.strip().split(":")
|
||||
permission = lineinfo[0]
|
||||
granted = None
|
||||
if "granted=" in lineinfo[1]:
|
||||
granted = ("granted=true" in lineinfo[1])
|
||||
|
||||
details["permissions"].append({
|
||||
"name": permission,
|
||||
"granted": granted,
|
||||
"type": "install"
|
||||
})
|
||||
|
||||
if in_runtime_permissions:
|
||||
if not line.startswith(" " * 8):
|
||||
in_runtime_permissions = False
|
||||
else:
|
||||
lineinfo = line.strip().split(":")
|
||||
permission = lineinfo[0]
|
||||
granted = None
|
||||
if "granted=" in lineinfo[1]:
|
||||
granted = ("granted=true" in lineinfo[1])
|
||||
|
||||
details["permissions"].append({
|
||||
"name": permission,
|
||||
"granted": granted,
|
||||
"type": "runtime"
|
||||
})
|
||||
|
||||
if in_declared_permissions:
|
||||
if not line.startswith(" " * 6):
|
||||
in_declared_permissions = False
|
||||
else:
|
||||
permission = line.strip().split(":")[0]
|
||||
details["permissions"].append({
|
||||
"name": permission,
|
||||
"type": "declared"
|
||||
})
|
||||
if in_requested_permissions:
|
||||
if not line.startswith(" " * 6):
|
||||
in_requested_permissions = False
|
||||
else:
|
||||
details["requested_permissions"].append(line.strip())
|
||||
|
||||
if line.strip().startswith("userId="):
|
||||
details["uid"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionName="):
|
||||
details["version_name"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("versionCode="):
|
||||
details["version_code"] = line.split("=", 1)[1].strip()
|
||||
elif line.strip().startswith("timeStamp="):
|
||||
details["timestamp"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("firstInstallTime="):
|
||||
details["first_install_time"] = line.split("=")[1].strip()
|
||||
elif line.strip().startswith("lastUpdateTime="):
|
||||
details["last_update_time"] = line.split("=")[1].strip()
|
||||
elif line.strip() == "install permissions:":
|
||||
in_install_permissions = True
|
||||
elif line.strip() == "runtime permissions:":
|
||||
in_runtime_permissions = True
|
||||
elif line.strip() == "declared permissions:":
|
||||
in_declared_permissions = True
|
||||
elif line.strip() == "requested permissions:":
|
||||
in_requested_permissions = True
|
||||
|
||||
return details
|
||||
|
||||
|
||||
def parse_dumpsys_packages(output: str) -> list:
|
||||
"""
|
||||
Parse the dumpsys package service data
|
||||
"""
|
||||
pkg_rxp = re.compile(r" Package \[(.+?)\].*")
|
||||
|
||||
results = []
|
||||
package_name = None
|
||||
package = {}
|
||||
lines = []
|
||||
for line in output.splitlines():
|
||||
if line.startswith(" Package ["):
|
||||
if len(lines) > 0:
|
||||
details = parse_dumpsys_package_for_details("\n".join(lines))
|
||||
package.update(details)
|
||||
results.append(package)
|
||||
lines = []
|
||||
package = {}
|
||||
|
||||
matches = pkg_rxp.findall(line)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
package_name = matches[0]
|
||||
package["package_name"] = package_name
|
||||
continue
|
||||
|
||||
if not package_name:
|
||||
continue
|
||||
|
||||
lines.append(line)
|
||||
|
||||
if len(lines) > 0:
|
||||
details = parse_dumpsys_package_for_details("\n".join(lines))
|
||||
package.update(details)
|
||||
results.append(package)
|
||||
|
||||
return results
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.command import Command
|
||||
|
||||
@@ -13,9 +14,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CmdCheckIOCS(Command):
|
||||
|
||||
def __init__(self, target_path: str = None, results_path: str = None,
|
||||
ioc_files: list = [], module_name: str = None,
|
||||
serial: str = None, fast_mode: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
) -> None:
|
||||
super().__init__(target_path=target_path, results_path=results_path,
|
||||
ioc_files=ioc_files, module_name=module_name,
|
||||
serial=serial, fast_mode=fast_mode, log=log)
|
||||
@@ -42,8 +49,8 @@ class CmdCheckIOCS(Command):
|
||||
if iocs_module().get_slug() != name_only:
|
||||
continue
|
||||
|
||||
log.info("Loading results from \"%s\" with module %s", file_name,
|
||||
iocs_module.__name__)
|
||||
log.info("Loading results from \"%s\" with module %s",
|
||||
file_name, iocs_module.__name__)
|
||||
|
||||
m = iocs_module.from_json(file_path,
|
||||
log=logging.getLogger(iocs_module.__module__))
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
from mvt.common.indicators import Indicators
|
||||
from mvt.common.module import run_module, save_timeline
|
||||
@@ -19,28 +19,36 @@ from mvt.common.version import MVT_VERSION
|
||||
|
||||
class Command:
|
||||
|
||||
def __init__(self, target_path: str = None, results_path: str = None,
|
||||
ioc_files: list = [], module_name: str = None,
|
||||
serial: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__)):
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
) -> None:
|
||||
self.name = ""
|
||||
self.modules = []
|
||||
|
||||
self.target_path = target_path
|
||||
self.results_path = results_path
|
||||
self.ioc_files = ioc_files
|
||||
self.ioc_files = ioc_files if ioc_files else []
|
||||
self.module_name = module_name
|
||||
self.serial = serial
|
||||
self.fast_mode = fast_mode
|
||||
self.log = log
|
||||
|
||||
self.iocs = Indicators(log=log)
|
||||
self.iocs.load_indicators_files(ioc_files)
|
||||
self.iocs.load_indicators_files(self.ioc_files)
|
||||
|
||||
# This list will contain all executed modules.
|
||||
# We can use this to reference e.g. self.executed[0].results.
|
||||
self.executed = []
|
||||
|
||||
self.detected_count = 0
|
||||
|
||||
self.timeline = []
|
||||
self.timeline_detected = []
|
||||
|
||||
@@ -121,12 +129,12 @@ class Command:
|
||||
with open(file_path, "rb") as handle:
|
||||
sha256.update(handle.read())
|
||||
except FileNotFoundError:
|
||||
self.log.error("Failed to hash the file %s: might "
|
||||
"be a symlink", file_path)
|
||||
self.log.error("Failed to hash the file %s: might be a symlink",
|
||||
file_path)
|
||||
continue
|
||||
except PermissionError:
|
||||
self.log.error("Failed to hash the file %s: "
|
||||
"permission denied", file_path)
|
||||
self.log.error("Failed to hash the file %s: permission denied",
|
||||
file_path)
|
||||
continue
|
||||
|
||||
info["hashes"].append({
|
||||
@@ -190,6 +198,8 @@ class Command:
|
||||
|
||||
self.executed.append(m)
|
||||
|
||||
self.detected_count += len(m.detected)
|
||||
|
||||
self.timeline.extend(m.timeline)
|
||||
self.timeline_detected.extend(m.timeline_detected)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from appdirs import user_data_dir
|
||||
|
||||
@@ -47,12 +47,17 @@ class Indicators:
|
||||
if os.path.isfile(path):
|
||||
self.parse_stix2(path)
|
||||
else:
|
||||
self.log.error("Path specified with env MVT_STIX2 is not "
|
||||
"a valid file: %s", path)
|
||||
self.log.error("Path specified with env MVT_STIX2 is not a valid file: %s",
|
||||
path)
|
||||
|
||||
def _new_collection(self, cid: str = "", name: str = "",
|
||||
description: str = "", file_name: str = "",
|
||||
file_path: str = "") -> dict:
|
||||
def _new_collection(
|
||||
self,
|
||||
cid: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
file_name: Optional[str] = None,
|
||||
file_path: Optional[str] = None
|
||||
) -> dict:
|
||||
return {
|
||||
"id": cid,
|
||||
"name": name,
|
||||
@@ -130,8 +135,7 @@ class Indicators:
|
||||
data = json.load(handle)
|
||||
except json.decoder.JSONDecodeError:
|
||||
self.log.critical("Unable to parse STIX2 indicator file. "
|
||||
"The file is corrupted or in the wrong "
|
||||
"format!")
|
||||
"The file is corrupted or in the wrong format!")
|
||||
return
|
||||
|
||||
malware = {}
|
||||
@@ -186,7 +190,7 @@ class Indicators:
|
||||
self.ioc_collections.extend(collections)
|
||||
|
||||
def load_indicators_files(self, files: list,
|
||||
load_default: bool = True) -> None:
|
||||
load_default: Optional[bool] = True) -> None:
|
||||
"""
|
||||
Load a list of indicators files.
|
||||
"""
|
||||
@@ -272,9 +276,8 @@ class Indicators:
|
||||
if final_url.domain.lower() == ioc["value"]:
|
||||
if orig_url.is_shortened and orig_url.url != final_url.url:
|
||||
self.log.warning("Found a known suspicious domain %s "
|
||||
"shortened as %s matching indicators "
|
||||
"from \"%s\"", final_url.url, orig_url.url,
|
||||
ioc["name"])
|
||||
"shortened as %s matching indicators from \"%s\"",
|
||||
final_url.url, orig_url.url, ioc["name"])
|
||||
else:
|
||||
self.log.warning("Found a known suspicious domain %s "
|
||||
"matching indicators from \"%s\"",
|
||||
@@ -339,8 +342,8 @@ class Indicators:
|
||||
if len(proc_name) == 16:
|
||||
if ioc["value"].startswith(proc_name):
|
||||
self.log.warning("Found a truncated known suspicious "
|
||||
"process name \"%s\" matching indicators "
|
||||
"from \"%s\"", process, ioc["name"])
|
||||
"process name \"%s\" matching indicators from \"%s\"",
|
||||
process, ioc["name"])
|
||||
return ioc
|
||||
|
||||
return None
|
||||
@@ -377,8 +380,8 @@ class Indicators:
|
||||
|
||||
for ioc in self.get_iocs("emails"):
|
||||
if email.lower() == ioc["value"].lower():
|
||||
self.log.warning("Found a known suspicious email address \"%s\""
|
||||
" matching indicators from \"%s\"",
|
||||
self.log.warning("Found a known suspicious email address \"%s\" "
|
||||
"matching indicators from \"%s\"",
|
||||
email, ioc["name"])
|
||||
return ioc
|
||||
|
||||
@@ -433,6 +436,27 @@ class Indicators:
|
||||
|
||||
return None
|
||||
|
||||
def check_file_path_process(self, file_path: str) -> Union[dict, None]:
|
||||
"""Check the provided file path contains a process name from the
|
||||
list of indicators
|
||||
|
||||
:param file_path: File path or file name to check against file
|
||||
indicators
|
||||
:type file_path: str
|
||||
:returns: Indicator details if matched, otherwise None
|
||||
|
||||
"""
|
||||
if not file_path:
|
||||
return None
|
||||
|
||||
for ioc in self.get_iocs("processes"):
|
||||
parts = file_path.split("/")
|
||||
if ioc["value"] in parts:
|
||||
self.log.warning("Found known suspicious process name mentioned in file at "
|
||||
"path \"%s\" matching indicators from \"%s\"",
|
||||
file_path, ioc["name"])
|
||||
return ioc
|
||||
|
||||
def check_profile(self, profile_uuid: str) -> Union[dict, None]:
|
||||
"""Check the provided configuration profile UUID against the list of
|
||||
indicators.
|
||||
@@ -468,8 +492,8 @@ class Indicators:
|
||||
|
||||
for ioc in self.get_iocs("files_sha256"):
|
||||
if file_hash.lower() == ioc["value"].lower():
|
||||
self.log.warning("Found a known suspicious file with hash "
|
||||
"\"%s\" matching indicators from \"%s\"",
|
||||
self.log.warning("Found a known suspicious file with hash \"%s\" "
|
||||
"matching indicators from \"%s\"",
|
||||
file_hash, ioc["name"])
|
||||
return ioc
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
from rich import print
|
||||
from rich import print as rich_print
|
||||
|
||||
from .updates import IndicatorsUpdates, MVTUpdates
|
||||
from .version import MVT_VERSION
|
||||
@@ -18,8 +18,8 @@ def check_updates() -> None:
|
||||
pass
|
||||
else:
|
||||
if latest_version:
|
||||
print(f"\t\t[bold]Version {latest_version} is available! "
|
||||
"Upgrade mvt![/bold]")
|
||||
rich_print(f"\t\t[bold]Version {latest_version} is available! "
|
||||
"Upgrade mvt![/bold]")
|
||||
|
||||
# Then we check for indicators files updates.
|
||||
ioc_updates = IndicatorsUpdates()
|
||||
@@ -27,8 +27,8 @@ def check_updates() -> None:
|
||||
# Before proceeding, we check if we have downloaded an indicators index.
|
||||
# If not, there's no point in proceeding with the updates check.
|
||||
if ioc_updates.get_latest_update() == 0:
|
||||
print("\t\t[bold]You have not yet downloaded any indicators, check "
|
||||
"the `download-iocs` command![/bold]")
|
||||
rich_print("\t\t[bold]You have not yet downloaded any indicators, check "
|
||||
"the `download-iocs` command![/bold]")
|
||||
return
|
||||
|
||||
# We only perform this check at a fixed frequency, in order to not
|
||||
@@ -36,8 +36,8 @@ def check_updates() -> None:
|
||||
# multiple times.
|
||||
should_check, hours = ioc_updates.should_check()
|
||||
if not should_check:
|
||||
print(f"\t\tIndicators updates checked recently, next automatic check "
|
||||
f"in {int(hours)} hours")
|
||||
rich_print(f"\t\tIndicators updates checked recently, next automatic check "
|
||||
f"in {int(hours)} hours")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -46,18 +46,18 @@ def check_updates() -> None:
|
||||
pass
|
||||
else:
|
||||
if ioc_to_update:
|
||||
print("\t\t[bold]There are updates to your indicators files! "
|
||||
"Run the `download-iocs` command to update![/bold]")
|
||||
rich_print("\t\t[bold]There are updates to your indicators files! "
|
||||
"Run the `download-iocs` command to update![/bold]")
|
||||
else:
|
||||
print("\t\tYour indicators files seem to be up to date.")
|
||||
rich_print("\t\tYour indicators files seem to be up to date.")
|
||||
|
||||
|
||||
def logo() -> None:
|
||||
print("\n")
|
||||
print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
|
||||
print("\t\thttps://mvt.re")
|
||||
print(f"\t\tVersion: {MVT_VERSION}")
|
||||
rich_print("\n")
|
||||
rich_print("\t[bold]MVT[/bold] - Mobile Verification Toolkit")
|
||||
rich_print("\t\thttps://mvt.re")
|
||||
rich_print(f"\t\tVersion: {MVT_VERSION}")
|
||||
|
||||
check_updates()
|
||||
|
||||
print("\n")
|
||||
rich_print("\n")
|
||||
|
||||
@@ -7,7 +7,7 @@ import csv
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Callable, Union
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
import simplejson as json
|
||||
|
||||
@@ -30,9 +30,15 @@ class MVTModule:
|
||||
enabled = True
|
||||
slug = None
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = None, results: list = None):
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
"""Initialize module.
|
||||
|
||||
:param file_path: Path to the module's database file, if there is any
|
||||
@@ -99,9 +105,9 @@ class MVTModule:
|
||||
try:
|
||||
json.dump(self.results, handle, indent=4, default=str)
|
||||
except Exception as exc:
|
||||
self.log.error("Unable to store results of module %s "
|
||||
"to file %s: %s", self.__class__.__name__,
|
||||
results_file_name, exc)
|
||||
self.log.error("Unable to store results of module %s to file %s: %s",
|
||||
self.__class__.__name__, results_file_name,
|
||||
exc)
|
||||
|
||||
if self.detected:
|
||||
detected_file_name = f"{name}_detected.json"
|
||||
@@ -145,7 +151,8 @@ class MVTModule:
|
||||
|
||||
# De-duplicate timeline entries.
|
||||
self.timeline = self._deduplicate_timeline(self.timeline)
|
||||
self.timeline_detected = self._deduplicate_timeline(self.timeline_detected)
|
||||
self.timeline_detected = self._deduplicate_timeline(
|
||||
self.timeline_detected)
|
||||
|
||||
def run(self) -> None:
|
||||
"""Run the main module procedure."""
|
||||
@@ -158,8 +165,8 @@ def run_module(module: Callable) -> None:
|
||||
try:
|
||||
module.run()
|
||||
except NotImplementedError:
|
||||
module.log.exception("The run() procedure of module %s was not "
|
||||
"implemented yet!", module.__class__.__name__)
|
||||
module.log.exception("The run() procedure of module %s was not implemented yet!",
|
||||
module.__class__.__name__)
|
||||
except InsufficientPrivileges as exc:
|
||||
module.log.info("Insufficient privileges for module %s: %s",
|
||||
module.__class__.__name__, exc)
|
||||
@@ -176,8 +183,8 @@ def run_module(module: Callable) -> None:
|
||||
try:
|
||||
module.check_indicators()
|
||||
except NotImplementedError:
|
||||
module.log.info("The %s module does not support checking for "
|
||||
"indicators", module.__class__.__name__)
|
||||
module.log.info("The %s module does not support checking for indicators",
|
||||
module.__class__.__name__)
|
||||
else:
|
||||
if module.indicators and not module.detected:
|
||||
module.log.info("The %s module produced no detections!",
|
||||
|
||||
@@ -16,18 +16,16 @@ class MutuallyExclusiveOption(Option):
|
||||
help_msg = kwargs.get("help", "")
|
||||
if self.mutually_exclusive:
|
||||
ex_str = ", ".join(self.mutually_exclusive)
|
||||
kwargs["help"] = help_msg + (
|
||||
" NOTE: This argument is mutually exclusive with "
|
||||
"arguments: [" + ex_str + "]."
|
||||
)
|
||||
kwargs["help"] = (f"{help_msg} NOTE: This argument is mutually exclusive with arguments"
|
||||
f"[{ex_str}].")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def handle_parse_result(self, ctx, opts, args):
|
||||
if self.mutually_exclusive.intersection(opts) and self.name in opts:
|
||||
raise UsageError(
|
||||
f"Illegal usage: `{self.name}` is mutually exclusive with "
|
||||
f"arguments `{', '.join(self.mutually_exclusive)}`."
|
||||
f"Illegal usage: `{self.name}` is mutually exclusive "
|
||||
f"with arguments `{', '.join(self.mutually_exclusive)}`."
|
||||
)
|
||||
|
||||
return super().handle_parse_result(ctx, opts, args)
|
||||
|
||||
@@ -88,8 +88,8 @@ class IndicatorsUpdates:
|
||||
self.index_branch, self.index_path)
|
||||
res = requests.get(url)
|
||||
if res.status_code != 200:
|
||||
log.error("Failed to retrieve indicators index located at %s "
|
||||
"(error %d)", url, res.status_code)
|
||||
log.error("Failed to retrieve indicators index located at %s (error %d)",
|
||||
url, res.status_code)
|
||||
return None
|
||||
|
||||
return yaml.safe_load(res.content)
|
||||
@@ -131,8 +131,8 @@ class IndicatorsUpdates:
|
||||
ioc_url = ioc.get("download_url", "")
|
||||
|
||||
if not ioc_url:
|
||||
log.error("Could not find a way to download indicator file "
|
||||
"for %s", ioc.get("name"))
|
||||
log.error("Could not find a way to download indicator file for %s",
|
||||
ioc.get("name"))
|
||||
continue
|
||||
|
||||
ioc_local_path = self.download_remote_ioc(ioc_url)
|
||||
@@ -162,8 +162,7 @@ class IndicatorsUpdates:
|
||||
latest_commit = details[0]
|
||||
latest_commit_date = latest_commit.get("commit", {}).get("author", {}).get("date", None)
|
||||
if not latest_commit_date:
|
||||
log.error("Failed to retrieve date of latest update to indicators "
|
||||
"index file")
|
||||
log.error("Failed to retrieve date of latest update to indicators index file")
|
||||
return -1
|
||||
|
||||
latest_commit_dt = datetime.strptime(latest_commit_date,
|
||||
|
||||
@@ -264,7 +264,7 @@ class URL:
|
||||
self.top_level = self.get_top_level()
|
||||
self.is_shortened = False
|
||||
|
||||
def get_domain(self) -> None:
|
||||
def get_domain(self) -> str:
|
||||
"""Get the domain from a URL.
|
||||
|
||||
:param url: URL to parse
|
||||
@@ -273,15 +273,11 @@ class URL:
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# TODO: Properly handle exception.
|
||||
try:
|
||||
return get_tld(self.url,
|
||||
as_object=True,
|
||||
fix_protocol=True).parsed_url.netloc.lower().lstrip("www.")
|
||||
except Exception:
|
||||
return None
|
||||
return get_tld(self.url,
|
||||
as_object=True,
|
||||
fix_protocol=True).parsed_url.netloc.lower().lstrip("www.")
|
||||
|
||||
def get_top_level(self) -> None:
|
||||
def get_top_level(self) -> str:
|
||||
"""Get only the top-level domain from a URL.
|
||||
|
||||
:param url: URL to parse
|
||||
@@ -290,11 +286,9 @@ class URL:
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# TODO: Properly handle exception.
|
||||
try:
|
||||
return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower()
|
||||
except Exception:
|
||||
return None
|
||||
return get_tld(self.url,
|
||||
as_object=True,
|
||||
fix_protocol=True).fld.lower()
|
||||
|
||||
def check_if_shortened(self) -> bool:
|
||||
"""Check if the URL is among list of shortener services.
|
||||
|
||||
@@ -22,7 +22,7 @@ def convert_chrometime_to_datetime(timestamp: int) -> int:
|
||||
return epoch_start + delta
|
||||
|
||||
|
||||
def convert_datetime_to_iso(datetime: datetime.datetime) -> str:
|
||||
def convert_datetime_to_iso(date_time: datetime.datetime) -> str:
|
||||
"""Converts datetime to ISO string.
|
||||
|
||||
:param datetime: datetime.
|
||||
@@ -32,12 +32,14 @@ def convert_datetime_to_iso(datetime: datetime.datetime) -> str:
|
||||
|
||||
"""
|
||||
try:
|
||||
return datetime.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
return date_time.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def convert_unix_to_utc_datetime(timestamp: Union[int, float, str]) -> datetime.datetime:
|
||||
def convert_unix_to_utc_datetime(
|
||||
timestamp: Union[int, float, str]
|
||||
) -> datetime.datetime:
|
||||
"""Converts a unix epoch timestamp to UTC datetime.
|
||||
|
||||
:param timestamp: Epoc timestamp to convert.
|
||||
@@ -105,8 +107,8 @@ def convert_mactime_to_iso(timestamp: int, from_2001: bool = True):
|
||||
|
||||
"""
|
||||
|
||||
return convert_datetime_to_iso(convert_mactime_to_datetime(timestamp,
|
||||
from_2001))
|
||||
return convert_datetime_to_iso(
|
||||
convert_mactime_to_datetime(timestamp, from_2001))
|
||||
|
||||
|
||||
def check_for_links(text: str) -> list:
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
MVT_VERSION = "2.1.4"
|
||||
MVT_VERSION = "2.2"
|
||||
|
||||
@@ -42,8 +42,7 @@ def virustotal_lookup(file_hash: str):
|
||||
if res.status_code == 404:
|
||||
log.info("Could not find results for file with hash %s", file_hash)
|
||||
elif res.status_code == 429:
|
||||
raise VTQuotaExceeded("You have exceeded the quota for your "
|
||||
"VirusTotal API key")
|
||||
raise VTQuotaExceeded("You have exceeded the quota for your VirusTotal API key")
|
||||
else:
|
||||
raise Exception(f"Unexpected response from VirusTotal: {res.status_code}")
|
||||
|
||||
|
||||
@@ -162,9 +162,9 @@ def check_backup(ctx, iocs, output, fast, list_modules, module, backup_path):
|
||||
|
||||
cmd.run()
|
||||
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
if cmd.detected_count > 0:
|
||||
log.warning("The analysis of the backup produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
cmd.detected_count)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
@@ -192,9 +192,9 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, dump_path):
|
||||
|
||||
cmd.run()
|
||||
|
||||
if len(cmd.timeline_detected) > 0:
|
||||
if cmd.detected_count > 0:
|
||||
log.warning("The analysis of the iOS filesystem produced %d detections!",
|
||||
len(cmd.timeline_detected))
|
||||
cmd.detected_count)
|
||||
|
||||
|
||||
#==============================================================================
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.command import Command
|
||||
|
||||
@@ -15,9 +16,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CmdIOSCheckBackup(Command):
|
||||
|
||||
def __init__(self, target_path: str = None, results_path: str = None,
|
||||
ioc_files: list = [], module_name: str = None,
|
||||
serial: str = None, fast_mode: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
) -> None:
|
||||
super().__init__(target_path=target_path, results_path=results_path,
|
||||
ioc_files=ioc_files, module_name=module_name,
|
||||
serial=serial, fast_mode=fast_mode, log=log)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.command import Command
|
||||
|
||||
@@ -15,9 +16,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CmdIOSCheckFS(Command):
|
||||
|
||||
def __init__(self, target_path: str = None, results_path: str = None,
|
||||
ioc_files: list = [], module_name: str = None,
|
||||
serial: str = None, fast_mode: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
ioc_files: Optional[list] = None,
|
||||
module_name: Optional[str] = None,
|
||||
serial: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
) -> None:
|
||||
super().__init__(target_path=target_path, results_path=results_path,
|
||||
ioc_files=ioc_files, module_name=module_name,
|
||||
serial=serial, fast_mode=fast_mode, log=log)
|
||||
|
||||
@@ -11,6 +11,7 @@ import os
|
||||
import os.path
|
||||
import shutil
|
||||
import sqlite3
|
||||
from typing import Optional
|
||||
|
||||
from iOSbackup import iOSbackup
|
||||
|
||||
@@ -24,7 +25,7 @@ class DecryptBackup:
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, backup_path: str, dest_path: str = None) -> None:
|
||||
def __init__(self, backup_path: str, dest_path: Optional[str] = None) -> None:
|
||||
"""Decrypts an encrypted iOS backup.
|
||||
:param backup_path: Path to the encrypted backup folder
|
||||
:param dest_path: Path to the folder where to store the decrypted backup
|
||||
@@ -93,8 +94,8 @@ class DecryptBackup:
|
||||
if not os.path.exists(item_folder):
|
||||
os.makedirs(item_folder)
|
||||
|
||||
# iOSBackup getFileDecryptedCopy() claims to read a "file" parameter
|
||||
# but the code actually is reading the "manifest" key.
|
||||
# iOSBackup getFileDecryptedCopy() claims to read a "file"
|
||||
# parameter but the code actually is reading the "manifest" key.
|
||||
# Add manifest plist to both keys to handle this.
|
||||
item["manifest"] = item["file"]
|
||||
|
||||
@@ -111,7 +112,8 @@ class DecryptBackup:
|
||||
# Copying over the root plist files as well.
|
||||
for file_name in os.listdir(self.backup_path):
|
||||
if file_name.endswith(".plist"):
|
||||
log.info("Copied plist file %s to %s", file_name, self.dest_path)
|
||||
log.info("Copied plist file %s to %s",
|
||||
file_name, self.dest_path)
|
||||
shutil.copy(os.path.join(self.backup_path, file_name),
|
||||
self.dest_path)
|
||||
|
||||
@@ -121,18 +123,21 @@ class DecryptBackup:
|
||||
:param password: Password to use to decrypt the original backup
|
||||
|
||||
"""
|
||||
log.info("Decrypting iOS backup at path %s with password", self.backup_path)
|
||||
log.info("Decrypting iOS backup at path %s with password",
|
||||
self.backup_path)
|
||||
|
||||
if not os.path.exists(os.path.join(self.backup_path, "Manifest.plist")):
|
||||
possible = glob.glob(os.path.join(self.backup_path, "*", "Manifest.plist"))
|
||||
possible = glob.glob(os.path.join(
|
||||
self.backup_path, "*", "Manifest.plist"))
|
||||
|
||||
if len(possible) == 1:
|
||||
newpath = os.path.dirname(possible[0])
|
||||
log.warning("No Manifest.plist in %s, using %s instead.",
|
||||
self.backup_path, newpath)
|
||||
self.backup_path = newpath
|
||||
elif len(possible) > 1:
|
||||
log.critical("No Manifest.plist in %s, and %d Manifest.plist "
|
||||
"files in subdirs. Please choose one!",
|
||||
log.critical("No Manifest.plist in %s, and %d Manifest.plist files in subdirs. "
|
||||
"Please choose one!",
|
||||
self.backup_path, len(possible))
|
||||
return
|
||||
|
||||
@@ -145,7 +150,9 @@ class DecryptBackup:
|
||||
cleartextpassword=password,
|
||||
backuproot=os.path.dirname(self.backup_path))
|
||||
except Exception as exc:
|
||||
if isinstance(exc, KeyError) and len(exc.args) > 0 and exc.args[0] == b"KEY":
|
||||
if (isinstance(exc, KeyError)
|
||||
and len(exc.args) > 0
|
||||
and exc.args[0] == b"KEY"):
|
||||
log.critical("Failed to decrypt backup. Password is probably wrong.")
|
||||
elif (isinstance(exc, FileNotFoundError)
|
||||
and os.path.basename(exc.filename) == "Manifest.plist"):
|
||||
@@ -154,9 +161,8 @@ class DecryptBackup:
|
||||
self.backup_path)
|
||||
else:
|
||||
log.exception(exc)
|
||||
log.critical("Failed to decrypt backup. Did you provide the "
|
||||
"correct password? Did you point to the right "
|
||||
"backup path?")
|
||||
log.critical("Failed to decrypt backup. Did you provide the correct password? "
|
||||
"Did you point to the right backup path?")
|
||||
|
||||
def decrypt_with_key_file(self, key_file: str) -> None:
|
||||
"""Decrypts an encrypted iOS backup using a key file.
|
||||
@@ -176,8 +182,7 @@ class DecryptBackup:
|
||||
|
||||
# Key should be 64 hex encoded characters (32 raw bytes)
|
||||
if len(key_bytes) != 64:
|
||||
log.critical("Invalid key from key file. Did you provide the "
|
||||
"correct key file?")
|
||||
log.critical("Invalid key from key file. Did you provide the correct key file?")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -187,8 +192,7 @@ class DecryptBackup:
|
||||
backuproot=os.path.dirname(self.backup_path))
|
||||
except Exception as exc:
|
||||
log.exception(exc)
|
||||
log.critical("Failed to decrypt backup. Did you provide the "
|
||||
"correct key file?")
|
||||
log.critical("Failed to decrypt backup. Did you provide the correct key file?")
|
||||
|
||||
def get_key(self) -> None:
|
||||
"""Retrieve and prints the encryption key."""
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import logging
|
||||
import os
|
||||
import plistlib
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.module import DatabaseNotFoundError
|
||||
from mvt.ios.versions import get_device_desc_from_id, latest_ios_version
|
||||
@@ -16,10 +17,15 @@ from ..base import IOSExtraction
|
||||
class BackupInfo(IOSExtraction):
|
||||
"""This module extracts information about the device and the backup."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -29,8 +35,8 @@ class BackupInfo(IOSExtraction):
|
||||
def run(self) -> None:
|
||||
info_path = os.path.join(self.target_path, "Info.plist")
|
||||
if not os.path.exists(info_path):
|
||||
raise DatabaseNotFoundError("No Info.plist at backup path, unable "
|
||||
"to extract device information")
|
||||
raise DatabaseNotFoundError("No Info.plist at backup path, unable to extract device "
|
||||
"information")
|
||||
|
||||
with open(info_path, "rb") as handle:
|
||||
info = plistlib.load(handle)
|
||||
@@ -44,7 +50,7 @@ class BackupInfo(IOSExtraction):
|
||||
|
||||
for field in fields:
|
||||
value = info.get(field, None)
|
||||
# Converting the product type in product name
|
||||
|
||||
if field == "Product Type" and value:
|
||||
product_name = get_device_desc_from_id(value)
|
||||
if product_name:
|
||||
@@ -53,11 +59,11 @@ class BackupInfo(IOSExtraction):
|
||||
self.log.info("%s: %s", field, value)
|
||||
else:
|
||||
self.log.info("%s: %s", field, value)
|
||||
|
||||
self.results[field] = value
|
||||
|
||||
if "Product Version" in info:
|
||||
latest = latest_ios_version()
|
||||
if info["Product Version"] != latest["version"]:
|
||||
self.log.warning("This phone is running an outdated iOS "
|
||||
"version: %s (latest is %s)",
|
||||
self.log.warning("This phone is running an outdated iOS version: %s (latest is %s)",
|
||||
info["Product Version"], latest['version'])
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
import os
|
||||
import plistlib
|
||||
from base64 import b64encode
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_datetime_to_iso
|
||||
|
||||
@@ -19,10 +19,15 @@ CONF_PROFILES_DOMAIN = "SysSharedContainerDomain-systemgroup.com.apple.configura
|
||||
class ConfigurationProfiles(IOSExtraction):
|
||||
"""This module extracts the full plist data from configuration profiles."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -37,9 +42,8 @@ class ConfigurationProfiles(IOSExtraction):
|
||||
"timestamp": record["install_date"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "configuration_profile_install",
|
||||
"data": f"{record['plist']['PayloadType']} installed: "
|
||||
f"{record['plist']['PayloadUUID']} - "
|
||||
f"{payload_name}: {payload_description}"
|
||||
"data": f"{record['plist']['PayloadType']} installed: {record['plist']['PayloadUUID']} "
|
||||
f"- {payload_name}: {payload_description}"
|
||||
}
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
@@ -54,9 +58,10 @@ class ConfigurationProfiles(IOSExtraction):
|
||||
# indicator list.
|
||||
ioc = self.indicators.check_profile(result["plist"]["PayloadUUID"])
|
||||
if ioc:
|
||||
self.log.warning(f"Found a known malicious configuration profile "
|
||||
f"\"{result['plist']['PayloadDisplayName']}\" "
|
||||
f"with UUID '{result['plist']['PayloadUUID']}'.")
|
||||
self.log.warning("Found a known malicious configuration "
|
||||
"profile \"%s\" with UUID %s",
|
||||
result['plist']['PayloadDisplayName'],
|
||||
result['plist']['PayloadUUID'])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
@@ -64,19 +69,22 @@ class ConfigurationProfiles(IOSExtraction):
|
||||
# Highlight suspicious configuration profiles which may be used
|
||||
# to hide notifications.
|
||||
if payload_content["PayloadType"] in ["com.apple.notificationsettings"]:
|
||||
self.log.warning(f"Found a potentially suspicious configuration profile "
|
||||
f"\"{result['plist']['PayloadDisplayName']}\" with "
|
||||
f"payload type '{payload_content['PayloadType']}'.")
|
||||
self.log.warning("Found a potentially suspicious configuration profile "
|
||||
"\"%s\" with payload type %s",
|
||||
result['plist']['PayloadDisplayName'],
|
||||
payload_content['PayloadType'])
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
def run(self) -> None:
|
||||
for conf_file in self._get_backup_files_from_manifest(domain=CONF_PROFILES_DOMAIN):
|
||||
for conf_file in self._get_backup_files_from_manifest(
|
||||
domain=CONF_PROFILES_DOMAIN):
|
||||
conf_rel_path = conf_file["relative_path"]
|
||||
|
||||
# Filter out all configuration files that are not configuration
|
||||
# profiles.
|
||||
if not conf_rel_path or not os.path.basename(conf_rel_path).startswith("profile-"):
|
||||
if not conf_rel_path or not os.path.basename(
|
||||
conf_rel_path).startswith("profile-"):
|
||||
continue
|
||||
|
||||
conf_file_path = self._get_backup_file_from_id(conf_file["file_id"])
|
||||
@@ -89,6 +97,8 @@ class ConfigurationProfiles(IOSExtraction):
|
||||
except Exception:
|
||||
conf_plist = {}
|
||||
|
||||
# TODO: Tidy up the following code hell.
|
||||
|
||||
if "SignerCerts" in conf_plist:
|
||||
conf_plist["SignerCerts"] = [b64encode(x) for x in conf_plist["SignerCerts"]]
|
||||
|
||||
@@ -122,4 +132,5 @@ class ConfigurationProfiles(IOSExtraction):
|
||||
"install_date": convert_datetime_to_iso(conf_plist.get("InstallDate")),
|
||||
})
|
||||
|
||||
self.log.info("Extracted details about %d configuration profiles", len(self.results))
|
||||
self.log.info("Extracted details about %d configuration profiles",
|
||||
len(self.results))
|
||||
|
||||
@@ -9,8 +9,10 @@ import logging
|
||||
import os
|
||||
import plistlib
|
||||
import sqlite3
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.module import DatabaseNotFoundError
|
||||
from mvt.common.url import URL
|
||||
from mvt.common.utils import convert_datetime_to_iso, convert_unix_to_iso
|
||||
|
||||
from ..base import IOSExtraction
|
||||
@@ -19,10 +21,15 @@ from ..base import IOSExtraction
|
||||
class Manifest(IOSExtraction):
|
||||
"""This module extracts information from a backup Manifest.db file."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -73,9 +80,6 @@ class Manifest(IOSExtraction):
|
||||
return records
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
if not self.indicators:
|
||||
return
|
||||
|
||||
for result in self.results:
|
||||
if not result.get("relative_path"):
|
||||
continue
|
||||
@@ -84,21 +88,30 @@ class Manifest(IOSExtraction):
|
||||
if (os.path.basename(result["relative_path"]) == "com.apple.CrashReporter.plist"
|
||||
and result["domain"] == "RootDomain"):
|
||||
self.log.warning("Found a potentially suspicious "
|
||||
"\"com.apple.CrashReporter.plist\" "
|
||||
"file created in RootDomain")
|
||||
"\"com.apple.CrashReporter.plist\" file created in RootDomain")
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
if not self.indicators:
|
||||
continue
|
||||
|
||||
if self.indicators.check_file_path("/" + result["relative_path"]):
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
rel_path = result["relative_path"].lower()
|
||||
for ioc in self.indicators.get_iocs("domains"):
|
||||
if ioc["value"].lower() in rel_path:
|
||||
self.log.warning("Found mention of domain \"%s\" in a "
|
||||
"backup file with path: %s",
|
||||
ioc["value"], rel_path)
|
||||
parts = rel_path.split("_")
|
||||
for part in parts:
|
||||
try:
|
||||
URL(part)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
ioc = self.indicators.check_domain(part)
|
||||
if ioc:
|
||||
self.log.warning("Found mention of domain \"%s\" in a backup file with "
|
||||
"path: %s", ioc["value"], rel_path)
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self) -> None:
|
||||
@@ -132,19 +145,23 @@ class Manifest(IOSExtraction):
|
||||
try:
|
||||
file_plist = plistlib.load(io.BytesIO(file_data["file"]))
|
||||
file_metadata = self._get_key(file_plist, "$objects")[1]
|
||||
|
||||
birth = self._get_key(file_metadata, "Birth")
|
||||
last_modified = self._get_key(file_metadata, "LastModified")
|
||||
last_status_change = self._get_key(file_metadata,
|
||||
"LastStatusChange")
|
||||
|
||||
cleaned_metadata.update({
|
||||
"created": self._convert_timestamp(self._get_key(file_metadata, "Birth")),
|
||||
"modified": self._convert_timestamp(self._get_key(file_metadata,
|
||||
"LastModified")),
|
||||
"status_changed": self._convert_timestamp(self._get_key(file_metadata,
|
||||
"LastStatusChange")),
|
||||
"created": self._convert_timestamp(birth),
|
||||
"modified": self._convert_timestamp(last_modified),
|
||||
"status_changed": self._convert_timestamp(last_status_change),
|
||||
"mode": oct(self._get_key(file_metadata, "Mode")),
|
||||
"owner": self._get_key(file_metadata, "UserID"),
|
||||
"size": self._get_key(file_metadata, "Size"),
|
||||
})
|
||||
except Exception:
|
||||
self.log.exception("Error reading manifest file metadata "
|
||||
"for file with ID %s and relative path %s",
|
||||
self.log.exception("Error reading manifest file metadata for file with ID %s "
|
||||
"and relative path %s",
|
||||
file_data["fileID"],
|
||||
file_data["relativePath"])
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
import logging
|
||||
import plistlib
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_datetime_to_iso
|
||||
|
||||
from ..base import IOSExtraction
|
||||
|
||||
# CONF_PROFILES_EVENTS_ID = "aeb25de285ea542f7ac7c2070cddd1961e369df1"
|
||||
CONF_PROFILES_EVENTS_RELPATH = "Library/ConfigurationProfiles/MCProfileEvents.plist"
|
||||
|
||||
|
||||
@@ -20,10 +21,15 @@ class ProfileEvents(IOSExtraction):
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -85,8 +91,10 @@ class ProfileEvents(IOSExtraction):
|
||||
return results
|
||||
|
||||
def run(self) -> None:
|
||||
for events_file in self._get_backup_files_from_manifest(relative_path=CONF_PROFILES_EVENTS_RELPATH):
|
||||
events_file_path = self._get_backup_file_from_id(events_file["file_id"])
|
||||
for events_file in self._get_backup_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
|
||||
|
||||
@@ -97,8 +105,7 @@ class ProfileEvents(IOSExtraction):
|
||||
self.results.extend(self.parse_profile_events(handle.read()))
|
||||
|
||||
for result in self.results:
|
||||
self.log.info("On %s process \"%s\" started operation \"%s\" "
|
||||
"of profile \"%s\"",
|
||||
self.log.info("On %s process \"%s\" started operation \"%s\" of profile \"%s\"",
|
||||
result.get("timestamp"), result.get("process"),
|
||||
result.get("operation"), result.get("profile_id"))
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
from typing import Iterator, Optional, Union
|
||||
|
||||
from mvt.common.module import (DatabaseCorruptedError, DatabaseNotFoundError,
|
||||
MVTModule)
|
||||
@@ -18,10 +19,15 @@ class IOSExtraction(MVTModule):
|
||||
"""This class provides a base for all iOS filesystem/backup extraction
|
||||
modules."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -30,7 +36,8 @@ class IOSExtraction(MVTModule):
|
||||
self.is_fs_dump = False
|
||||
self.is_sysdiagnose = False
|
||||
|
||||
def _recover_sqlite_db_if_needed(self, file_path, forced=False):
|
||||
def _recover_sqlite_db_if_needed(self, file_path: str,
|
||||
forced: Optional[bool] = False) -> None:
|
||||
"""Tries to recover a malformed database by running a .clone command.
|
||||
|
||||
:param file_path: Path to the malformed database file.
|
||||
@@ -57,13 +64,11 @@ class IOSExtraction(MVTModule):
|
||||
file_path)
|
||||
|
||||
if not shutil.which("sqlite3"):
|
||||
raise DatabaseCorruptedError("failed to recover without sqlite3 "
|
||||
"binary: please install sqlite3!")
|
||||
raise DatabaseCorruptedError("failed 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")
|
||||
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)
|
||||
@@ -75,7 +80,11 @@ class IOSExtraction(MVTModule):
|
||||
|
||||
self.log.info("Database at path %s recovered successfully!", file_path)
|
||||
|
||||
def _get_backup_files_from_manifest(self, relative_path=None, domain=None):
|
||||
def _get_backup_files_from_manifest(
|
||||
self,
|
||||
relative_path: Optional[str] = None,
|
||||
domain: Optional[str] = None
|
||||
) -> Iterator[dict]:
|
||||
"""Locate files from Manifest.db.
|
||||
|
||||
:param relative_path: Relative path to use as filter from Manifest.db.
|
||||
@@ -98,8 +107,12 @@ class IOSExtraction(MVTModule):
|
||||
(relative_path, domain))
|
||||
else:
|
||||
if relative_path:
|
||||
cur.execute(f"{base_sql} relativePath = ?;",
|
||||
(relative_path,))
|
||||
if "*" in relative_path:
|
||||
cur.execute(f"{base_sql} relativePath LIKE ?;",
|
||||
(relative_path.replace("*", "%"),))
|
||||
else:
|
||||
cur.execute(f"{base_sql} relativePath = ?;",
|
||||
(relative_path,))
|
||||
elif domain:
|
||||
cur.execute(f"{base_sql} domain = ?;", (domain,))
|
||||
except Exception as exc:
|
||||
@@ -112,14 +125,14 @@ class IOSExtraction(MVTModule):
|
||||
"relative_path": row[2],
|
||||
}
|
||||
|
||||
def _get_backup_file_from_id(self, file_id):
|
||||
def _get_backup_file_from_id(self, file_id: str) -> Union[str, None]:
|
||||
file_path = os.path.join(self.target_path, file_id[0:2], file_id)
|
||||
if os.path.exists(file_path):
|
||||
return file_path
|
||||
|
||||
return None
|
||||
|
||||
def _get_fs_files_from_patterns(self, root_paths):
|
||||
def _get_fs_files_from_patterns(self, root_paths: list) -> Iterator[str]:
|
||||
for root_path in root_paths:
|
||||
for found_path in glob.glob(os.path.join(self.target_path,
|
||||
root_path)):
|
||||
@@ -128,7 +141,11 @@ class IOSExtraction(MVTModule):
|
||||
|
||||
yield found_path
|
||||
|
||||
def _find_ios_database(self, backup_ids=None, root_paths=[]):
|
||||
def _find_ios_database(
|
||||
self,
|
||||
backup_ids: Optional[list] = None,
|
||||
root_paths: Optional[list] = None
|
||||
) -> None:
|
||||
"""Try to locate a module's database file from either an iTunes
|
||||
backup or a full filesystem dump. This is intended only for
|
||||
modules that expect to work with a single SQLite database.
|
||||
@@ -152,21 +169,21 @@ class IOSExtraction(MVTModule):
|
||||
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
|
||||
# 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 found_path in self._get_fs_files_from_patterns(root_paths):
|
||||
file_path = found_path
|
||||
break
|
||||
|
||||
if root_paths:
|
||||
# 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 found_path in self._get_fs_files_from_patterns(root_paths):
|
||||
file_path = found_path
|
||||
break
|
||||
|
||||
# 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")
|
||||
raise DatabaseNotFoundError("unable to find the module's database file")
|
||||
|
||||
self._recover_sqlite_db_if_needed(self.file_path)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
import plistlib
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
|
||||
@@ -21,10 +21,15 @@ class Analytics(IOSExtraction):
|
||||
"""This module extracts information from the
|
||||
private/var/Keychains/Analytics/*.db files."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -48,8 +53,7 @@ class Analytics(IOSExtraction):
|
||||
|
||||
ioc = self.indicators.check_process(value)
|
||||
if ioc:
|
||||
self.log.warning("Found mention of a malicious process "
|
||||
"\"%s\" in %s file at %s",
|
||||
self.log.warning("Found mention of a malicious process \"%s\" in %s file at %s",
|
||||
value, result["artifact"],
|
||||
result["timestamp"])
|
||||
result["matched_indicator"] = ioc
|
||||
@@ -58,8 +62,7 @@ class Analytics(IOSExtraction):
|
||||
|
||||
ioc = self.indicators.check_domain(value)
|
||||
if ioc:
|
||||
self.log.warning("Found mention of a malicious domain "
|
||||
"\"%s\" in %s file at %s",
|
||||
self.log.warning("Found mention of a malicious domain \"%s\" in %s file at %s",
|
||||
value, result["artifact"],
|
||||
result["timestamp"])
|
||||
result["matched_indicator"] = ioc
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.ios.versions import find_version_by_build
|
||||
|
||||
@@ -18,10 +18,15 @@ class AnalyticsIOSVersions(IOSExtraction):
|
||||
a timeline of build numbers from the private/var/Keychains/Analytics/*.db
|
||||
files."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -6,17 +6,22 @@
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from ..base import IOSExtraction
|
||||
|
||||
|
||||
class CacheFiles(IOSExtraction):
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_unix_to_iso
|
||||
|
||||
@@ -19,10 +19,15 @@ class Filesystem(IOSExtraction):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -52,15 +57,10 @@ class Filesystem(IOSExtraction):
|
||||
if self.fast_mode:
|
||||
continue
|
||||
|
||||
for ioc in self.indicators.get_iocs("processes"):
|
||||
parts = result["path"].split("/")
|
||||
if ioc["value"] in parts:
|
||||
self.log.warning("Found known suspicious process name "
|
||||
"mentioned in file at path \"%s\" "
|
||||
"matching indicators from \"%s\"",
|
||||
result["path"], ioc["name"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
ioc = self.indicators.check_file_path_process(result["path"])
|
||||
if ioc:
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
|
||||
def run(self) -> None:
|
||||
for root, dirs, files in os.walk(self.target_path):
|
||||
@@ -69,7 +69,8 @@ class Filesystem(IOSExtraction):
|
||||
dir_path = os.path.join(root, dir_name)
|
||||
result = {
|
||||
"path": os.path.relpath(dir_path, self.target_path),
|
||||
"modified": convert_unix_to_iso(os.stat(dir_path).st_mtime),
|
||||
"modified": convert_unix_to_iso(
|
||||
os.stat(dir_path).st_mtime),
|
||||
}
|
||||
except Exception:
|
||||
continue
|
||||
@@ -81,7 +82,8 @@ class Filesystem(IOSExtraction):
|
||||
file_path = os.path.join(root, file_name)
|
||||
result = {
|
||||
"path": os.path.relpath(file_path, self.target_path),
|
||||
"modified": convert_unix_to_iso(os.stat(file_path).st_mtime),
|
||||
"modified": convert_unix_to_iso(
|
||||
os.stat(file_path).st_mtime),
|
||||
}
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional
|
||||
|
||||
from ..net_base import NetBase
|
||||
|
||||
@@ -21,10 +22,15 @@ class Netusage(NetBase):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
|
||||
@@ -20,10 +20,15 @@ SAFARI_FAVICON_ROOT_PATHS = [
|
||||
class SafariFavicon(IOSExtraction):
|
||||
"""This module extracts all Safari favicon records."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -105,4 +110,5 @@ class SafariFavicon(IOSExtraction):
|
||||
|
||||
self.log.info("Extracted a total of %d favicon records",
|
||||
len(self.results))
|
||||
|
||||
self.results = sorted(self.results, key=lambda x: x["isodate"])
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
|
||||
@@ -18,10 +18,15 @@ SHUTDOWN_LOG_PATH = [
|
||||
class ShutdownLog(IOSExtraction):
|
||||
"""This module extracts processes information from the shutdown log file."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -49,9 +54,8 @@ class ShutdownLog(IOSExtraction):
|
||||
for ioc in self.indicators.get_iocs("processes"):
|
||||
parts = result["client"].split("/")
|
||||
if ioc in parts:
|
||||
self.log.warning("Found mention of a known malicious "
|
||||
"process \"%s\" in shutdown.log",
|
||||
ioc)
|
||||
self.log.warning("Found mention of a known malicious process \"%s\" in "
|
||||
"shutdown.log", ioc)
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_datetime_to_iso
|
||||
|
||||
@@ -20,10 +20,15 @@ IOS_ANALYTICS_JOURNAL_PATHS = [
|
||||
class IOSVersionHistory(IOSExtraction):
|
||||
"""This module extracts iOS update history from Analytics Journal log files."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from .webkit_base import WebkitBase
|
||||
|
||||
@@ -22,10 +22,15 @@ class WebkitIndexedDB(WebkitBase):
|
||||
|
||||
slug = "webkit_indexeddb"
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from .webkit_base import WebkitBase
|
||||
|
||||
@@ -20,10 +20,15 @@ class WebkitLocalStorage(WebkitBase):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -39,6 +44,5 @@ class WebkitLocalStorage(WebkitBase):
|
||||
|
||||
def run(self) -> None:
|
||||
self._process_webkit_folder(WEBKIT_LOCALSTORAGE_ROOT_PATHS)
|
||||
self.log.info("Extracted a total of %d records from WebKit "
|
||||
"Local Storages",
|
||||
self.log.info("Extracted a total of %d records from WebKit Local Storages",
|
||||
len(self.results))
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .webkit_base import WebkitBase
|
||||
|
||||
WEBKIT_SAFARIVIEWSERVICE_ROOT_PATHS = [
|
||||
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/",
|
||||
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/", # pylint: disable=line-too-long
|
||||
]
|
||||
|
||||
|
||||
@@ -19,16 +20,20 @@ class WebkitSafariViewService(WebkitBase):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
def run(self) -> None:
|
||||
self._process_webkit_folder(WEBKIT_SAFARIVIEWSERVICE_ROOT_PATHS)
|
||||
self.log.info("Extracted a total of %d records from WebKit "
|
||||
"SafariViewService WebsiteData",
|
||||
self.log.info("Extracted a total of %d records from WebKit SafariViewService WebsiteData",
|
||||
len(self.results))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import (convert_chrometime_to_datetime,
|
||||
convert_datetime_to_iso)
|
||||
@@ -15,7 +15,6 @@ from ..base import IOSExtraction
|
||||
CHROME_FAVICON_BACKUP_IDS = [
|
||||
"55680ab883d0fdcffd94f959b1632e5fbbb18c5b"
|
||||
]
|
||||
|
||||
# TODO: Confirm Chrome database path.
|
||||
CHROME_FAVICON_ROOT_PATHS = [
|
||||
"private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/Favicons",
|
||||
@@ -25,10 +24,15 @@ CHROME_FAVICON_ROOT_PATHS = [
|
||||
class ChromeFavicon(IOSExtraction):
|
||||
"""This module extracts all Chrome favicon records."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -83,7 +87,8 @@ class ChromeFavicon(IOSExtraction):
|
||||
"url": row[0],
|
||||
"icon_url": row[1],
|
||||
"timestamp": last_timestamp,
|
||||
"isodate": convert_datetime_to_iso(convert_chrometime_to_datetime(last_timestamp)),
|
||||
"isodate": convert_datetime_to_iso(
|
||||
convert_chrometime_to_datetime(last_timestamp)),
|
||||
})
|
||||
|
||||
cur.close()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import (convert_chrometime_to_datetime,
|
||||
convert_datetime_to_iso)
|
||||
@@ -17,17 +17,22 @@ CHROME_HISTORY_BACKUP_IDS = [
|
||||
]
|
||||
# TODO: Confirm Chrome database path.
|
||||
CHROME_HISTORY_ROOT_PATHS = [
|
||||
"private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/History",
|
||||
"private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/History", # pylint: disable=line-too-long
|
||||
]
|
||||
|
||||
|
||||
class ChromeHistory(IOSExtraction):
|
||||
"""This module extracts all Chome visits."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -78,7 +83,8 @@ class ChromeHistory(IOSExtraction):
|
||||
"url": item[1],
|
||||
"visit_id": item[2],
|
||||
"timestamp": item[3],
|
||||
"isodate": convert_datetime_to_iso(convert_chrometime_to_datetime(item[3])),
|
||||
"isodate": convert_datetime_to_iso(
|
||||
convert_chrometime_to_datetime(item[3])),
|
||||
"redirect_source": item[4],
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Optional
|
||||
|
||||
from ..base import IOSExtraction
|
||||
|
||||
@@ -19,10 +20,15 @@ CONTACTS_ROOT_PATHS = [
|
||||
class Contacts(IOSExtraction):
|
||||
"""This module extracts all contact details from the phone's address book."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_unix_to_iso
|
||||
|
||||
@@ -22,10 +22,15 @@ FIREFOX_HISTORY_ROOT_PATHS = [
|
||||
class FirefoxFavicon(IOSExtraction):
|
||||
"""This module extracts all Firefox favicon"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_unix_to_iso
|
||||
|
||||
@@ -26,10 +26,15 @@ class FirefoxHistory(IOSExtraction):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -39,8 +44,7 @@ class FirefoxHistory(IOSExtraction):
|
||||
"timestamp": record["isodate"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "firefox_history",
|
||||
"data": f"Firefox visit with ID {record['id']} "
|
||||
f"to URL: {record['url']}",
|
||||
"data": f"Firefox visit with ID {record['id']} to URL: {record['url']}",
|
||||
}
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import collections
|
||||
import logging
|
||||
import plistlib
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
|
||||
@@ -24,10 +24,15 @@ IDSTATUSCACHE_ROOT_PATHS = [
|
||||
class IDStatusCache(IOSExtraction):
|
||||
"""Extracts Apple Authentication information from idstatuscache.plist"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -55,8 +60,7 @@ class IDStatusCache(IOSExtraction):
|
||||
continue
|
||||
|
||||
if "\\x00\\x00" in result.get("user", ""):
|
||||
self.log.warning("Found an ID Status Cache entry with "
|
||||
"suspicious patterns: %s",
|
||||
self.log.warning("Found an ID Status Cache entry with suspicious patterns: %s",
|
||||
result.get("user"))
|
||||
self.detected.append(result)
|
||||
|
||||
@@ -83,7 +87,9 @@ class IDStatusCache(IOSExtraction):
|
||||
"idstatus": id_status,
|
||||
})
|
||||
|
||||
entry_counter = collections.Counter([entry["user"] for entry in id_status_cache_entries])
|
||||
entry_counter = collections.Counter([entry["user"]
|
||||
for entry in
|
||||
id_status_cache_entries])
|
||||
for entry in id_status_cache_entries:
|
||||
# Add total count of occurrences to the status cache entry.
|
||||
entry["occurrences"] = entry_counter[entry["user"]]
|
||||
@@ -97,7 +103,8 @@ class IDStatusCache(IOSExtraction):
|
||||
self.file_path)
|
||||
self._extract_idstatuscache_entries(self.file_path)
|
||||
elif self.is_fs_dump:
|
||||
for idstatuscache_path in self._get_fs_files_from_patterns(IDSTATUSCACHE_ROOT_PATHS):
|
||||
for idstatuscache_path in self._get_fs_files_from_patterns(
|
||||
IDSTATUSCACHE_ROOT_PATHS):
|
||||
self.file_path = idstatuscache_path
|
||||
self.log.info("Found IDStatusCache plist at path: %s",
|
||||
self.file_path)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
|
||||
@@ -22,10 +22,15 @@ INTERACTIONC_ROOT_PATHS = [
|
||||
class InteractionC(IOSExtraction):
|
||||
"""This module extracts data from InteractionC db."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -60,11 +65,9 @@ class InteractionC(IOSExtraction):
|
||||
"module": self.__class__.__name__,
|
||||
"event": timestamp,
|
||||
"data": f"[{record['bundle_id']}] {record['account']} - "
|
||||
f"from {record['sender_display_name']} "
|
||||
f"({record['sender_identifier']}) "
|
||||
f"to {record['recipient_display_name']} "
|
||||
f"({record['recipient_identifier']}): "
|
||||
f"{record['content']}"
|
||||
f"from {record['sender_display_name']} ({record['sender_identifier']}) "
|
||||
f"to {record['recipient_display_name']} ({record['recipient_identifier']}):"
|
||||
f" {record['content']}"
|
||||
})
|
||||
processed.append(record[timestamp])
|
||||
|
||||
@@ -126,11 +129,16 @@ class InteractionC(IOSExtraction):
|
||||
ZINTERACTIONS.ZDERIVEDINTENTIDENTIFIER,
|
||||
ZINTERACTIONS.Z_PK
|
||||
FROM ZINTERACTIONS
|
||||
LEFT JOIN ZCONTACTS ON ZINTERACTIONS.ZSENDER = ZCONTACTS.Z_PK
|
||||
LEFT JOIN Z_1INTERACTIONS ON ZINTERACTIONS.Z_PK == Z_1INTERACTIONS.Z_3INTERACTIONS
|
||||
LEFT JOIN ZATTACHMENT ON Z_1INTERACTIONS.Z_1ATTACHMENTS == ZATTACHMENT.Z_PK
|
||||
LEFT JOIN Z_2INTERACTIONRECIPIENT ON ZINTERACTIONS.Z_PK== Z_2INTERACTIONRECIPIENT.Z_3INTERACTIONRECIPIENT
|
||||
LEFT JOIN ZCONTACTS RECEIPIENTCONACT ON Z_2INTERACTIONRECIPIENT.Z_2RECIPIENTS== RECEIPIENTCONACT.Z_PK;
|
||||
LEFT JOIN ZCONTACTS
|
||||
ON ZINTERACTIONS.ZSENDER = ZCONTACTS.Z_PK
|
||||
LEFT JOIN Z_1INTERACTIONS
|
||||
ON ZINTERACTIONS.Z_PK == Z_1INTERACTIONS.Z_3INTERACTIONS
|
||||
LEFT JOIN ZATTACHMENT
|
||||
ON Z_1INTERACTIONS.Z_1ATTACHMENTS == ZATTACHMENT.Z_PK
|
||||
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]
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import plistlib
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
|
||||
@@ -23,10 +23,15 @@ LOCATIOND_ROOT_PATHS = [
|
||||
class LocationdClients(IOSExtraction):
|
||||
"""Extract information from apps who used geolocation."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -66,8 +71,8 @@ class LocationdClients(IOSExtraction):
|
||||
|
||||
ioc = self.indicators.check_process(proc_name)
|
||||
if ioc:
|
||||
self.log.warning("Found a suspicious process name in "
|
||||
"LocationD entry %s", result["package"])
|
||||
self.log.warning("Found a suspicious process name in LocationD entry %s",
|
||||
result["package"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
@@ -75,8 +80,8 @@ class LocationdClients(IOSExtraction):
|
||||
if "BundlePath" in result:
|
||||
ioc = self.indicators.check_file_path(result["BundlePath"])
|
||||
if ioc:
|
||||
self.log.warning("Found a suspicious file path in "
|
||||
"Location D: %s", result["BundlePath"])
|
||||
self.log.warning("Found a suspicious file path in Location D: %s",
|
||||
result["BundlePath"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
@@ -84,8 +89,8 @@ class LocationdClients(IOSExtraction):
|
||||
if "Executable" in result:
|
||||
ioc = self.indicators.check_file_path(result["Executable"])
|
||||
if ioc:
|
||||
self.log.warning("Found a suspicious file path in "
|
||||
"Location D: %s", result["Executable"])
|
||||
self.log.warning("Found a suspicious file path in Location D: %s",
|
||||
result["Executable"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
@@ -93,8 +98,8 @@ class LocationdClients(IOSExtraction):
|
||||
if "Registered" in result:
|
||||
ioc = self.indicators.check_file_path(result["Registered"])
|
||||
if ioc:
|
||||
self.log.warning("Found a suspicious file path in "
|
||||
"Location D: %s", result["Registered"])
|
||||
self.log.warning("Found a suspicious file path in Location D: %s",
|
||||
result["Registered"])
|
||||
result["matched_indicator"] = ioc
|
||||
self.detected.append(result)
|
||||
continue
|
||||
@@ -108,7 +113,8 @@ class LocationdClients(IOSExtraction):
|
||||
result["package"] = key
|
||||
for timestamp in self.timestamps:
|
||||
if timestamp in result.keys():
|
||||
result[timestamp] = convert_mactime_to_iso(result[timestamp])
|
||||
result[timestamp] = convert_mactime_to_iso(
|
||||
result[timestamp])
|
||||
|
||||
self.results.append(result)
|
||||
|
||||
@@ -119,7 +125,8 @@ class LocationdClients(IOSExtraction):
|
||||
self.file_path)
|
||||
self._extract_locationd_entries(self.file_path)
|
||||
elif self.is_fs_dump:
|
||||
for locationd_path in self._get_fs_files_from_patterns(LOCATIOND_ROOT_PATHS):
|
||||
for locationd_path in self._get_fs_files_from_patterns(
|
||||
LOCATIOND_ROOT_PATHS):
|
||||
self.file_path = locationd_path
|
||||
self.log.info("Found Locationd Clients plist at path: %s",
|
||||
self.file_path)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from ..net_base import NetBase
|
||||
|
||||
@@ -22,10 +23,15 @@ class Datausage(NetBase):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import plistlib
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_datetime_to_iso
|
||||
|
||||
@@ -23,10 +23,15 @@ class OSAnalyticsADDaily(IOSExtraction):
|
||||
"""Extract network usage information by process,
|
||||
from com.apple.osanalytics.addaily.plist"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -71,5 +76,5 @@ class OSAnalyticsADDaily(IOSExtraction):
|
||||
"wwan_out": values[4],
|
||||
})
|
||||
|
||||
self.log.info("Extracted a total of %d com.apple.osanalytics.addaily "
|
||||
"entries", len(self.results))
|
||||
self.log.info("Extracted a total of %d com.apple.osanalytics.addaily entries",
|
||||
len(self.results))
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
import os
|
||||
import plistlib
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso, keys_bytes_to_string
|
||||
|
||||
@@ -24,10 +24,15 @@ SAFARI_BROWSER_STATE_ROOT_PATHS = [
|
||||
class SafariBrowserState(IOSExtraction):
|
||||
"""This module extracts all Safari browser state records."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -104,12 +109,18 @@ class SafariBrowserState(IOSExtraction):
|
||||
pass
|
||||
|
||||
if "SessionHistoryEntries" in session_data.get("SessionHistory", {}):
|
||||
for session_entry in session_data["SessionHistory"].get("SessionHistoryEntries"):
|
||||
for session_entry in session_data["SessionHistory"].get(
|
||||
"SessionHistoryEntries"):
|
||||
self._session_history_count += 1
|
||||
|
||||
data_length = 0
|
||||
if "SessionHistoryEntryData" in session_entry:
|
||||
data_length = len(session_entry.get("SessionHistoryEntryData"))
|
||||
|
||||
session_entries.append({
|
||||
"entry_title": session_entry.get("SessionHistoryEntryOriginalURL"),
|
||||
"entry_url": session_entry.get("SessionHistoryEntryURL"),
|
||||
"data_length": len(session_entry.get("SessionHistoryEntryData")) if "SessionHistoryEntryData" in session_entry else 0,
|
||||
"data_length": data_length,
|
||||
})
|
||||
|
||||
self.results.append({
|
||||
@@ -124,8 +135,11 @@ class SafariBrowserState(IOSExtraction):
|
||||
|
||||
def run(self) -> None:
|
||||
if self.is_backup:
|
||||
for backup_file in self._get_backup_files_from_manifest(relative_path=SAFARI_BROWSER_STATE_BACKUP_RELPATH):
|
||||
browserstate_path = self._get_backup_file_from_id(backup_file["file_id"])
|
||||
for backup_file in self._get_backup_files_from_manifest(
|
||||
relative_path=SAFARI_BROWSER_STATE_BACKUP_RELPATH):
|
||||
browserstate_path = self._get_backup_file_from_id(
|
||||
backup_file["file_id"])
|
||||
|
||||
if not browserstate_path:
|
||||
continue
|
||||
|
||||
@@ -133,11 +147,11 @@ class SafariBrowserState(IOSExtraction):
|
||||
browserstate_path)
|
||||
self._process_browser_state_db(browserstate_path)
|
||||
elif self.is_fs_dump:
|
||||
for browserstate_path in self._get_fs_files_from_patterns(SAFARI_BROWSER_STATE_ROOT_PATHS):
|
||||
for browserstate_path in self._get_fs_files_from_patterns(
|
||||
SAFARI_BROWSER_STATE_ROOT_PATHS):
|
||||
self.log.info("Found Safari browser state database at path: %s",
|
||||
browserstate_path)
|
||||
self._process_browser_state_db(browserstate_path)
|
||||
|
||||
self.log.info("Extracted a total of %d tab records and %d session "
|
||||
"history entries", len(self.results),
|
||||
self._session_history_count)
|
||||
self.log.info("Extracted a total of %d tab records and %d session history entries",
|
||||
len(self.results), self._session_history_count)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.url import URL
|
||||
from mvt.common.utils import (convert_mactime_to_datetime,
|
||||
@@ -28,10 +28,15 @@ class SafariHistory(IOSExtraction):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -41,7 +46,8 @@ class SafariHistory(IOSExtraction):
|
||||
"timestamp": record["isodate"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "safari_history",
|
||||
"data": f"Safari visit to {record['url']} (ID: {record['id']}, Visit ID: {record['visit_id']})",
|
||||
"data": f"Safari visit to {record['url']} (ID: {record['id']}, "
|
||||
f"Visit ID: {record['visit_id']})",
|
||||
}
|
||||
|
||||
def _find_injections(self):
|
||||
@@ -54,14 +60,21 @@ class SafariHistory(IOSExtraction):
|
||||
if not result["redirect_destination"]:
|
||||
continue
|
||||
|
||||
origin_domain = URL(result["url"]).domain
|
||||
try:
|
||||
origin_domain = URL(result["url"]).domain
|
||||
except Exception:
|
||||
origin_domain = ""
|
||||
|
||||
# We loop again through visits in order to find redirect record.
|
||||
for redirect in self.results:
|
||||
if redirect["visit_id"] != result["redirect_destination"]:
|
||||
continue
|
||||
|
||||
redirect_domain = URL(redirect["url"]).domain
|
||||
try:
|
||||
redirect_domain = URL(redirect["url"]).domain
|
||||
except Exception:
|
||||
redirect_domain = ""
|
||||
|
||||
# If the redirect destination is the same domain as the origin,
|
||||
# it's most likely an HTTPS upgrade.
|
||||
if origin_domain == redirect_domain:
|
||||
@@ -76,7 +89,8 @@ class SafariHistory(IOSExtraction):
|
||||
elapsed_ms = elapsed_time.microseconds / 1000
|
||||
|
||||
if elapsed_time.seconds == 0:
|
||||
self.log.warning("Redirect took less than a second! (%d milliseconds)", elapsed_ms)
|
||||
self.log.warning("Redirect took less than a second! (%d milliseconds)",
|
||||
elapsed_ms)
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
self._find_injections()
|
||||
@@ -116,7 +130,8 @@ class SafariHistory(IOSExtraction):
|
||||
"isodate": convert_mactime_to_iso(row[3]),
|
||||
"redirect_source": row[4],
|
||||
"redirect_destination": row[5],
|
||||
"safari_history_db": os.path.relpath(history_path, self.target_path),
|
||||
"safari_history_db": os.path.relpath(history_path,
|
||||
self.target_path),
|
||||
})
|
||||
|
||||
cur.close()
|
||||
@@ -124,16 +139,24 @@ class SafariHistory(IOSExtraction):
|
||||
|
||||
def run(self) -> None:
|
||||
if self.is_backup:
|
||||
for history_file in self._get_backup_files_from_manifest(relative_path=SAFARI_HISTORY_BACKUP_RELPATH):
|
||||
history_path = self._get_backup_file_from_id(history_file["file_id"])
|
||||
for history_file in self._get_backup_files_from_manifest(
|
||||
relative_path=SAFARI_HISTORY_BACKUP_RELPATH):
|
||||
history_path = self._get_backup_file_from_id(
|
||||
history_file["file_id"])
|
||||
|
||||
if not history_path:
|
||||
continue
|
||||
|
||||
self.log.info("Found Safari history database at path: %s", history_path)
|
||||
self.log.info("Found Safari history database at path: %s",
|
||||
history_path)
|
||||
|
||||
self._process_history_db(history_path)
|
||||
elif self.is_fs_dump:
|
||||
for history_path in self._get_fs_files_from_patterns(SAFARI_HISTORY_ROOT_PATHS):
|
||||
self.log.info("Found Safari history database at path: %s", history_path)
|
||||
for history_path in self._get_fs_files_from_patterns(
|
||||
SAFARI_HISTORY_ROOT_PATHS):
|
||||
self.log.info("Found Safari history database at path: %s",
|
||||
history_path)
|
||||
self._process_history_db(history_path)
|
||||
|
||||
self.log.info("Extracted a total of %d history records", len(self.results))
|
||||
self.log.info("Extracted a total of %d history records",
|
||||
len(self.results))
|
||||
|
||||
@@ -8,7 +8,7 @@ import itertools
|
||||
import logging
|
||||
import plistlib
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import check_for_links, convert_mactime_to_iso
|
||||
|
||||
@@ -25,10 +25,15 @@ SHORTCUT_ROOT_PATHS = [
|
||||
class Shortcuts(IOSExtraction):
|
||||
"""This module extracts all info about SMS/iMessage attachments."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -99,24 +104,29 @@ class Shortcuts(IOSExtraction):
|
||||
for index, value in enumerate(item):
|
||||
shortcut[names[index]] = value
|
||||
|
||||
action_data = plistlib.load(io.BytesIO(shortcut.pop("action_data", [])))
|
||||
action_data = plistlib.load(io.BytesIO(
|
||||
shortcut.pop("action_data", [])))
|
||||
actions = []
|
||||
for action_entry in action_data:
|
||||
action = {}
|
||||
action["identifier"] = action_entry["WFWorkflowActionIdentifier"]
|
||||
action["parameters"] = action_entry["WFWorkflowActionParameters"]
|
||||
|
||||
# URLs might be in multiple fields, do a simple regex search across the parameters.
|
||||
# URLs might be in multiple fields, do a simple regex search
|
||||
# across the parameters.
|
||||
extracted_urls = check_for_links(str(action["parameters"]))
|
||||
|
||||
# Remove quoting characters that may have been captured by the regex.
|
||||
# Remove quoting characters that may have been captured by the
|
||||
# regex.
|
||||
action["urls"] = [url.rstrip("',") for url in extracted_urls]
|
||||
actions.append(action)
|
||||
|
||||
shortcut["isodate"] = convert_mactime_to_iso(shortcut.pop("created_date"))
|
||||
shortcut["modified_date"] = convert_mactime_to_iso(shortcut["modified_date"])
|
||||
shortcut["parsed_actions"] = len(actions)
|
||||
shortcut["action_urls"] = list(itertools.chain(*[action["urls"] for action in actions]))
|
||||
shortcut["action_urls"] = list(itertools.chain(
|
||||
*[action["urls"] for action in actions]))
|
||||
|
||||
self.results.append(shortcut)
|
||||
|
||||
cur.close()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
import sqlite3
|
||||
from base64 import b64encode
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import check_for_links, convert_mactime_to_iso
|
||||
|
||||
@@ -23,10 +23,15 @@ SMS_ROOT_PATHS = [
|
||||
class SMS(IOSExtraction):
|
||||
"""This module extracts all SMS messages containing links."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -92,7 +97,8 @@ class SMS(IOSExtraction):
|
||||
for index, value in enumerate(item):
|
||||
# We base64 escape some of the attributes that could contain
|
||||
# binary data.
|
||||
if (names[index] == "attributedBody" or names[index] == "payload_data"
|
||||
if (names[index] == "attributedBody"
|
||||
or names[index] == "payload_data"
|
||||
or names[index] == "message_summary_info") and value:
|
||||
value = b64encode(value).decode()
|
||||
|
||||
@@ -108,9 +114,10 @@ class SMS(IOSExtraction):
|
||||
if not message.get("text", None):
|
||||
message["text"] = ""
|
||||
|
||||
if message.get("text", "").startswith("ALERT: State-sponsored attackers may be targeting your iPhone"):
|
||||
self.log.warn("Apple warning about state-sponsored attack "
|
||||
"received on the %s", message["isodate"])
|
||||
alert = "ALERT: State-sponsored attackers may be targeting your iPhone"
|
||||
if message.get("text", "").startswith(alert):
|
||||
self.log.warning("Apple warning about state-sponsored attack received on the %s",
|
||||
message["isodate"])
|
||||
self.results.append(message)
|
||||
else:
|
||||
# Extract links from the SMS message.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
import sqlite3
|
||||
from base64 import b64encode
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_mactime_to_iso
|
||||
|
||||
@@ -23,10 +23,15 @@ SMS_ROOT_PATHS = [
|
||||
class SMSAttachments(IOSExtraction):
|
||||
"""This module extracts all info about SMS/iMessage attachments."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -58,8 +63,10 @@ class SMSAttachments(IOSExtraction):
|
||||
message.service as "service",
|
||||
handle.id as "phone_number"
|
||||
FROM attachment
|
||||
LEFT JOIN message_attachment_join ON message_attachment_join.attachment_id = attachment.ROWID
|
||||
LEFT JOIN message ON message.ROWID = message_attachment_join.message_id
|
||||
LEFT JOIN message_attachment_join ON
|
||||
message_attachment_join.attachment_id = attachment.ROWID
|
||||
LEFT JOIN message ON
|
||||
message.ROWID = message_attachment_join.message_id
|
||||
LEFT JOIN handle ON handle.ROWID = message.handle_id;
|
||||
""")
|
||||
names = [description[0] for description in cur.description]
|
||||
@@ -74,8 +81,10 @@ class SMSAttachments(IOSExtraction):
|
||||
value = b64encode(value).decode()
|
||||
attachment[names[index]] = value
|
||||
|
||||
attachment["isodate"] = convert_mactime_to_iso(attachment["created_date"])
|
||||
attachment["start_date"] = convert_mactime_to_iso(attachment["start_date"])
|
||||
attachment["isodate"] = convert_mactime_to_iso(
|
||||
attachment["created_date"])
|
||||
attachment["start_date"] = convert_mactime_to_iso(
|
||||
attachment["start_date"])
|
||||
attachment["direction"] = ("sent" if attachment["is_outgoing"] == 1 else "received")
|
||||
attachment["has_user_info"] = attachment["user_info"] is not None
|
||||
attachment["service"] = attachment["service"] or "Unknown"
|
||||
@@ -84,8 +93,8 @@ class SMSAttachments(IOSExtraction):
|
||||
if (attachment["filename"].startswith("/var/tmp/")
|
||||
and attachment["filename"].endswith("-1")
|
||||
and attachment["direction"] == "received"):
|
||||
self.log.warn("Suspicious iMessage attachment %s on %s",
|
||||
attachment['filename'], attachment['isodate'])
|
||||
self.log.warning("Suspicious iMessage attachment %s on %s",
|
||||
attachment['filename'], attachment['isodate'])
|
||||
self.detected.append(attachment)
|
||||
|
||||
self.results.append(attachment)
|
||||
@@ -93,4 +102,5 @@ class SMSAttachments(IOSExtraction):
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
self.log.info("Extracted a total of %d SMS attachments", len(self.results))
|
||||
self.log.info("Extracted a total of %d SMS attachments",
|
||||
len(self.results))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from mvt.common.utils import convert_unix_to_iso
|
||||
|
||||
@@ -47,10 +47,15 @@ AUTH_REASONS = {
|
||||
class TCC(IOSExtraction):
|
||||
"""This module extracts records from the TCC.db SQLite database."""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -122,9 +127,9 @@ class TCC(IOSExtraction):
|
||||
|
||||
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
|
||||
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
|
||||
self.log.info("Found client \"%s\" with access %s to %s "
|
||||
"on %s by %s", client, auth_value_desc,
|
||||
device, last_modified, auth_reason_desc)
|
||||
self.log.info("Found client \"%s\" with access %s to %s on %s by %s",
|
||||
client, auth_value_desc, device,
|
||||
last_modified, auth_reason_desc)
|
||||
|
||||
self.results.append({
|
||||
"service": service,
|
||||
@@ -138,12 +143,16 @@ class TCC(IOSExtraction):
|
||||
allowed_value = row[3]
|
||||
allowed_desc = AUTH_VALUE_OLD.get(allowed_value, "")
|
||||
prompt_count = row[4]
|
||||
|
||||
if db_version == "v2":
|
||||
last_modified = convert_unix_to_iso(row[5])
|
||||
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
|
||||
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
|
||||
self.log.info("Found client \"%s\" with access %s to "
|
||||
"%s at %s", client, allowed_desc, device,
|
||||
device = "camera"
|
||||
if service == "kTCCServiceMicrophone":
|
||||
device = "microphone"
|
||||
|
||||
self.log.info("Found client \"%s\" with access %s to %s at %s",
|
||||
client, allowed_desc, device,
|
||||
last_modified)
|
||||
|
||||
self.results.append({
|
||||
@@ -156,7 +165,10 @@ class TCC(IOSExtraction):
|
||||
})
|
||||
else:
|
||||
if service in ["kTCCServiceMicrophone", "kTCCServiceCamera"]:
|
||||
device = "microphone" if service == "kTCCServiceMicrophone" else "camera"
|
||||
device = "camera"
|
||||
if service == "kTCCServiceMicrophone":
|
||||
device = "microphone"
|
||||
|
||||
self.log.info("Found client \"%s\" with access %s to %s",
|
||||
client, allowed_desc, device)
|
||||
|
||||
@@ -175,6 +187,7 @@ class TCC(IOSExtraction):
|
||||
self._find_ios_database(backup_ids=TCC_BACKUP_IDS,
|
||||
root_paths=TCC_ROOT_PATHS)
|
||||
self.log.info("Found TCC database at path: %s", self.file_path)
|
||||
|
||||
self.process_db(self.file_path)
|
||||
|
||||
self.log.info("Extracted a total of %d TCC items", len(self.results))
|
||||
|
||||
@@ -6,15 +6,16 @@
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.utils import convert_unix_to_iso
|
||||
|
||||
from ..base import IOSExtraction
|
||||
|
||||
WEBKIT_RESOURCELOADSTATICS_BACKUP_RELPATH = "Library/WebKit/WebsiteData/ResourceLoadStatistics/observations.db"
|
||||
WEBKIT_RESOURCELOADSTATICS_BACKUP_RELPATH = "Library/WebKit/WebsiteData/ResourceLoadStatistics/observations.db" # pylint: disable=line-too-long
|
||||
WEBKIT_RESOURCELOADSTATICS_ROOT_PATHS = [
|
||||
"private/var/mobile/Containers/Data/Application/*/Library/WebKit/WebsiteData/ResourceLoadStatistics/observations.db",
|
||||
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/observations.db",
|
||||
"private/var/mobile/Containers/Data/Application/*/Library/WebKit/WebsiteData/ResourceLoadStatistics/observations.db", # pylint: disable=line-too-long
|
||||
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/observations.db", # pylint: disable=line-too-long
|
||||
]
|
||||
|
||||
|
||||
@@ -23,10 +24,15 @@ class WebkitResourceLoadStatistics(IOSExtraction):
|
||||
observations.db."""
|
||||
# TODO: Add serialize().
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -49,8 +55,8 @@ class WebkitResourceLoadStatistics(IOSExtraction):
|
||||
self.detected[key].append(item)
|
||||
|
||||
def _process_observations_db(self, db_path, key):
|
||||
self.log.info("Found WebKit ResourceLoadStatistics observations.db "
|
||||
"file at path %s", db_path)
|
||||
self.log.info("Found WebKit ResourceLoadStatistics observations.db file at path %s",
|
||||
db_path)
|
||||
|
||||
self._recover_sqlite_db_if_needed(db_path)
|
||||
|
||||
@@ -81,14 +87,17 @@ class WebkitResourceLoadStatistics(IOSExtraction):
|
||||
def run(self) -> None:
|
||||
if self.is_backup:
|
||||
try:
|
||||
for backup_file in self._get_backup_files_from_manifest(relative_path=WEBKIT_RESOURCELOADSTATICS_BACKUP_RELPATH):
|
||||
for backup_file in self._get_backup_files_from_manifest(
|
||||
relative_path=WEBKIT_RESOURCELOADSTATICS_BACKUP_RELPATH):
|
||||
db_path = self._get_backup_file_from_id(backup_file["file_id"])
|
||||
|
||||
key = f"{backup_file['domain']}/{WEBKIT_RESOURCELOADSTATICS_BACKUP_RELPATH}"
|
||||
if db_path:
|
||||
self._process_observations_db(db_path=db_path, key=key)
|
||||
except Exception as exc:
|
||||
self.log.info("Unable to find WebKit observations.db: %s", exc)
|
||||
elif self.is_fs_dump:
|
||||
for db_path in self._get_fs_files_from_patterns(WEBKIT_RESOURCELOADSTATICS_ROOT_PATHS):
|
||||
for db_path in self._get_fs_files_from_patterns(
|
||||
WEBKIT_RESOURCELOADSTATICS_ROOT_PATHS):
|
||||
db_rel_path = os.path.relpath(db_path, self.target_path)
|
||||
self._process_observations_db(db_path=db_path, key=db_rel_path)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import logging
|
||||
import os
|
||||
import plistlib
|
||||
from typing import Optional
|
||||
|
||||
from mvt.common.utils import convert_datetime_to_iso
|
||||
|
||||
@@ -14,11 +15,11 @@ from ..base import IOSExtraction
|
||||
WEBKIT_SESSION_RESOURCE_LOG_BACKUP_IDS = [
|
||||
"a500ee38053454a02e990957be8a251935e28d3f",
|
||||
]
|
||||
WEBKIT_SESSION_RESOURCE_LOG_BACKUP_RELPATH = "Library/WebKit/WebsiteData/ResourceLoadStatistics/full_browsing_session_resourceLog.plist"
|
||||
WEBKIT_SESSION_RESOURCE_LOG_BACKUP_RELPATH = "Library/WebKit/WebsiteData/ResourceLoadStatistics/full_browsing_session_resourceLog.plist" # pylint: disable=line-too-long
|
||||
WEBKIT_SESSION_RESOURCE_LOG_ROOT_PATHS = [
|
||||
"private/var/mobile/Containers/Data/Application/*/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData/full_browsing_session_resourceLog.plist",
|
||||
"private/var/mobile/Containers/Data/Application/*/Library/WebKit/WebsiteData/ResourceLoadStatistics/full_browsing_session_resourceLog.plist",
|
||||
"private/var/mobile/Library/WebClips/*/Storage/full_browsing_session_resourceLog.plist",
|
||||
"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
|
||||
]
|
||||
|
||||
|
||||
@@ -30,10 +31,15 @@ class WebkitSessionResourceLog(IOSExtraction):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = None, target_path: str = None,
|
||||
results_path: str = None, fast_mode: bool = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: list = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
target_path: Optional[str] = None,
|
||||
results_path: Optional[str] = None,
|
||||
fast_mode: Optional[bool] = False,
|
||||
log: logging.Logger = logging.getLogger(__name__),
|
||||
results: Optional[list] = None
|
||||
) -> None:
|
||||
super().__init__(file_path=file_path, target_path=target_path,
|
||||
results_path=results_path, fast_mode=fast_mode,
|
||||
log=log, results=results)
|
||||
@@ -61,13 +67,20 @@ class WebkitSessionResourceLog(IOSExtraction):
|
||||
for _, entries in self.results.items():
|
||||
for entry in entries:
|
||||
source_domains = self._extract_domains(entry["redirect_source"])
|
||||
destination_domains = self._extract_domains(entry["redirect_destination"])
|
||||
destination_domains = self._extract_domains(
|
||||
entry["redirect_destination"])
|
||||
|
||||
# TODO: Currently not used.
|
||||
# subframe_origins = self._extract_domains(entry["subframe_under_origin"])
|
||||
# subresource_domains = self._extract_domains(entry["subresource_under_origin"])
|
||||
# subframe_origins = self._extract_domains(
|
||||
# entry["subframe_under_origin"])
|
||||
# subresource_domains = self._extract_domains(
|
||||
# entry["subresource_under_origin"])
|
||||
|
||||
all_origins = set([entry["origin"]] + source_domains + destination_domains)
|
||||
all_origins = set(
|
||||
[entry["origin"]]
|
||||
+ source_domains
|
||||
+ destination_domains
|
||||
)
|
||||
|
||||
ioc = self.indicators.check_domains(all_origins)
|
||||
if ioc:
|
||||
@@ -93,8 +106,8 @@ class WebkitSessionResourceLog(IOSExtraction):
|
||||
|
||||
redirect_path += ", ".join(destination_domains)
|
||||
|
||||
self.log.warning("Found HTTP redirect between suspicious "
|
||||
"domains: %s", redirect_path)
|
||||
self.log.warning("Found HTTP redirect between suspicious domains: %s",
|
||||
redirect_path)
|
||||
|
||||
def _extract_browsing_stats(self, log_path):
|
||||
items = []
|
||||
@@ -115,7 +128,8 @@ class WebkitSessionResourceLog(IOSExtraction):
|
||||
"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"]),
|
||||
"most_recent_interaction": convert_datetime_to_iso(
|
||||
item["mostRecentUserInteraction"]),
|
||||
"last_seen": convert_datetime_to_iso(item["lastSeen"]),
|
||||
})
|
||||
|
||||
@@ -123,20 +137,23 @@ class WebkitSessionResourceLog(IOSExtraction):
|
||||
|
||||
def run(self) -> None:
|
||||
if self.is_backup:
|
||||
for log_file in self._get_backup_files_from_manifest(relative_path=WEBKIT_SESSION_RESOURCE_LOG_BACKUP_RELPATH):
|
||||
for log_file in self._get_backup_files_from_manifest(
|
||||
relative_path=WEBKIT_SESSION_RESOURCE_LOG_BACKUP_RELPATH):
|
||||
log_path = self._get_backup_file_from_id(log_file["file_id"])
|
||||
|
||||
if not log_path:
|
||||
continue
|
||||
|
||||
self.log.info("Found Safari browsing session resource log at "
|
||||
"path: %s", log_path)
|
||||
self.log.info("Found Safari browsing session resource log at path: %s",
|
||||
log_path)
|
||||
self.results[log_path] = self._extract_browsing_stats(log_path)
|
||||
elif self.is_fs_dump:
|
||||
for log_path in self._get_fs_files_from_patterns(WEBKIT_SESSION_RESOURCE_LOG_ROOT_PATHS):
|
||||
self.log.info("Found Safari browsing session resource log at "
|
||||
"path: %s", log_path)
|
||||
for log_path in self._get_fs_files_from_patterns(
|
||||
WEBKIT_SESSION_RESOURCE_LOG_ROOT_PATHS):
|
||||
self.log.info("Found Safari browsing session resource log at path: %s",
|
||||
log_path)
|
||||
key = os.path.relpath(log_path, self.target_path)
|
||||
self.results[key] = self._extract_browsing_stats(log_path)
|
||||
|
||||
self.log.info("Extracted records from %d Safari browsing session "
|
||||
"resource logs", len(self.results))
|
||||
self.log.info("Extracted records from %d Safari browsing session resource logs",
|
||||
len(self.results))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user