Compare commits

...

31 Commits

Author SHA1 Message Date
tek
e4e1716729 Bumped version 2022-01-20 15:28:42 +01:00
tek
083bc12351 Merge branch 'feature/check-file-path' 2022-01-20 15:19:37 +01:00
tek
cf6d392460 Adds more details on the download-iocs command 2022-01-20 13:29:50 +01:00
tek
95205d8e17 Adds indicators check to iOS TCC module 2022-01-18 17:12:20 +01:00
Nex
1460828c30 Uniforming style in test units 2022-01-18 16:33:13 +01:00
Nex
fa84b3f296 Revert "Testing with slightly older version of iOSbackup"
This reverts commit e1efaa5467.
2022-01-18 16:32:22 +01:00
Nex
e1efaa5467 Testing with slightly older version of iOSbackup 2022-01-18 16:27:14 +01:00
Nex
696d42fc6e Disabling tests for 3.7 due to iOSbackup requirements of >= 3.8 2022-01-18 16:22:29 +01:00
Nex
a0e1662726 Somehow mysteriously with >= pip doesn't find the version, with == does 2022-01-18 16:16:03 +01:00
Nex
51645bdbc0 Adding pip install for deps 2022-01-18 16:10:59 +01:00
Nex
bb1b108fd7 Cleaning build workflow 2022-01-18 16:09:01 +01:00
Nex
92f9dcb8a5 Tring to fix build 2022-01-18 16:08:14 +01:00
Nex
a6fd5fe1f3 Bumped version 2022-01-18 16:06:14 +01:00
Nex
3e0ef20fcd . 2022-01-18 16:05:01 +01:00
Nex
01f3acde2e Merge branch 'main' of github.com:mvt-project/mvt 2022-01-18 16:00:52 +01:00
Nex
b697874f56 Conforming the test files 2022-01-18 16:00:03 +01:00
Donncha Ó Cearbhaill
41d699f457 Add PyTest to Github actions 2022-01-18 15:59:16 +01:00
Donncha Ó Cearbhaill
6fcd40f6b6 Fix use of global list instance as self.results variable 2022-01-18 15:53:05 +01:00
tek
38bb583a9e Improves management of file path indicators 2022-01-18 15:50:31 +01:00
Donncha Ó Cearbhaill
48ec2d8fa8 Merge branch 'main' into tests 2022-01-18 15:30:40 +01:00
tek
798805c583 Improves Shortcut output 2022-01-18 13:06:35 +01:00
Nex
24be9e9570 Use default list of indicators files now that some default ones are automatically loaded 2022-01-14 16:26:14 +01:00
Nex
adbd95c559 Dots 2022-01-14 02:01:59 +01:00
Nex
8a707c288a Bumped version 2022-01-14 01:53:10 +01:00
Nex
4c906ad52e Renamed download iocs function 2022-01-14 01:52:57 +01:00
Nex
a2f8030cce Added new iOS versions 2022-01-14 01:41:48 +01:00
Donncha Ó Cearbhaill
b2e9f0361b Fix repeated results due to global results[] variable 2022-01-07 18:24:24 +01:00
Donncha Ó Cearbhaill
e85c70c603 Generate stix2 for each test run 2022-01-07 17:51:21 +01:00
Donncha Ó Cearbhaill
3f8dade610 Move backup binary artifact to seperate folder 2022-01-07 17:08:46 +01:00
Donncha Ó Cearbhaill
54963b0b59 Update test PR to work with latest code, fix flake8 2022-01-07 17:03:53 +01:00
tek
513e2cc704 First test structure 2022-01-07 16:41:19 +01:00
30 changed files with 348 additions and 49 deletions

View File

@@ -16,7 +16,8 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.7, 3.8, 3.9]
# python-version: [3.7, 3.8, 3.9]
python-version: [3.8, 3.9]
steps:
- uses: actions/checkout@v2
@@ -27,8 +28,9 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest safety
python -m pip install flake8 pytest safety stix2
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
python -m pip install .
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
@@ -37,7 +39,5 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Safety checks
run: safety check
# - name: Test with pytest
# run: |
# pytest
- name: Test with pytest
run: pytest

View File

@@ -41,6 +41,6 @@ export MVT_STIX2="/home/user/IOC1.stix2:/home/user/IOC2.stix2"
- [Predator from Cytrox](https://citizenlab.ca/2021/12/pegasus-vs-predator-dissidents-doubly-infected-iphone-reveals-cytrox-mercenary-spyware/) ([STIX2](https://raw.githubusercontent.com/AmnestyTech/investigations/master/2021-12-16_cytrox/cytrox.stix2))
- [This repository](https://github.com/Te-k/stalkerware-indicators) contains IOCs for Android stalkerware including [a STIX MVT-compatible file](https://raw.githubusercontent.com/Te-k/stalkerware-indicators/master/stalkerware.stix2).
You can automaticallly download the latest public indicator files with the command `mvt-ios download-iocs` or `mvt-android download-iocs`.
You can automaticallly download the latest public indicator files with the command `mvt-ios download-iocs` or `mvt-android download-iocs`. These commands download the list of indicators listed [here](https://github.com/mvt-project/mvt/blob/main/public_indicators.json) and store them in the [appdir](https://pypi.org/project/appdirs/) folder. They are then loaded automatically by mvt.
Please [open an issue](https://github.com/mvt-project/mvt/issues/) to suggest new sources of STIX-formatted IOCs.

View File

@@ -89,10 +89,6 @@ class Files(AndroidExtraction):
return
for result in self.results:
if self.indicators.check_file_name(result["path"]):
self.log.warning("Found a known suspicous filename at path: \"%s\"", result["path"])
self.detected.append(result)
if self.indicators.check_file_path(result["path"]):
self.log.warning("Found a known suspicous file at path: \"%s\"", result["path"])
self.detected.append(result)

View File

@@ -25,6 +25,7 @@ class Indicators:
self.ioc_processes = []
self.ioc_emails = []
self.ioc_files = []
self.ioc_file_paths = []
self.ioc_files_sha256 = []
self.ioc_app_ids = []
self.ios_profile_ids = []
@@ -45,7 +46,7 @@ class Indicators:
def _check_stix2_env_variable(self):
"""
Checks if a variable MVT_STIX2 contains path to STIX Files
Checks if a variable MVT_STIX2 contains path to STIX Files.
"""
if "MVT_STIX2" not in os.environ:
return False
@@ -57,9 +58,9 @@ class Indicators:
else:
self.log.info("Invalid STIX2 path %s in MVT_STIX2 environment variable", path)
def load_indicators_files(self, files):
def load_indicators_files(self, files, load_default=True):
"""
Load a list of indicators files
Load a list of indicators files.
"""
for file_path in files:
if os.path.isfile(file_path):
@@ -68,7 +69,8 @@ class Indicators:
self.log.warning("This indicators file %s does not exist", file_path)
# Load downloaded indicators and any indicators from env variable
self._load_downloaded_indicators()
if load_default:
self._load_downloaded_indicators()
self._check_stix2_env_variable()
self.log.info("Loaded a total of %d unique indicators", self.ioc_count)
@@ -108,6 +110,9 @@ class Indicators:
elif key == "file:name":
self._add_indicator(ioc=value,
iocs_list=self.ioc_files)
elif key == "file:path":
self._add_indicator(ioc=value,
iocs_list=self.ioc_file_paths)
elif key == "app:id":
self._add_indicator(ioc=value,
iocs_list=self.ioc_app_ids)
@@ -271,30 +276,26 @@ class Indicators:
return False
def check_file_name(self, file_path) -> bool:
"""Check the provided file path against the list of file indicators.
def check_file_name(self, file_name) -> bool:
"""Check the provided file name against the list of file indicators.
:param file_path: File path or file name to check against file
:param file_name: File name to check against file
indicators
:type file_path: str
:returns: True if the file path matched an indicator, otherwise False
:type file_name: str
:returns: True if the file name matched an indicator, otherwise False
:rtype: bool
"""
if not file_path:
if not file_name:
return False
file_name = os.path.basename(file_path)
if file_name in self.ioc_files:
return True
return False
# TODO: The difference between check_file_name() and check_file_path()
# needs to be more explicit and clear. Probably, the two should just
# be combined into one function.
def check_file_path(self, file_path) -> bool:
"""Check the provided file path against the list of file indicators.
"""Check the provided file path against the list of file indicators (both path and name).
:param file_path: File path or file name to check against file
indicators
@@ -306,7 +307,10 @@ class Indicators:
if not file_path:
return False
for ioc_file in self.ioc_files:
if self.check_file_name(os.path.basename(file_path)):
return True
for ioc_file in self.ioc_file_paths:
# Strip any trailing slash from indicator paths to match directories.
if file_path.startswith(ioc_file.rstrip("/")):
return True
@@ -330,7 +334,7 @@ class Indicators:
def download_indicators_files(log):
"""
Download indicators from repo into MVT app data directory
Download indicators from repo into MVT app data directory.
"""
data_dir = user_data_dir("mvt")
if not os.path.isdir(data_dir):

View File

@@ -30,7 +30,7 @@ class MVTModule(object):
slug = None
def __init__(self, file_path=None, base_folder=None, output_folder=None,
fast_mode=False, log=None, results=[]):
fast_mode=False, log=None, results=None):
"""Initialize module.
:param file_path: Path to the module's database file, if there is any
@@ -51,7 +51,7 @@ class MVTModule(object):
self.fast_mode = fast_mode
self.log = log
self.indicators = None
self.results = results
self.results = results if results else []
self.detected = []
self.timeline = []
self.timeline_detected = []

View File

@@ -6,7 +6,7 @@
import requests
from packaging import version
MVT_VERSION = "1.4.2"
MVT_VERSION = "1.4.5"
def check_for_updates():

View File

@@ -298,5 +298,5 @@ def check_iocs(ctx, iocs, list_modules, module, folder):
# Command: download-iocs
#==============================================================================
@cli.command("download-iocs", help="Download public STIX2 indicators")
def download_indicators():
def download_iocs():
download_indicators_files(log)

View File

@@ -83,7 +83,7 @@ class Manifest(IOSExtraction):
self.detected.append(result)
continue
if self.indicators.check_file_name(result["relative_path"]):
if self.indicators.check_file_path("/" + result["relative_path"]):
self.log.warning("Found a known malicious file at path: %s", result["relative_path"])
self.detected.append(result)
continue

View File

@@ -37,10 +37,6 @@ class Filesystem(IOSExtraction):
return
for result in self.results:
if self.indicators.check_file(result["path"]):
self.log.warning("Found a known malicious file name at path: %s", result["path"])
self.detected.append(result)
if self.indicators.check_file_path(result["path"]):
self.log.warning("Found a known malicious file path at path: %s", result["path"])
self.detected.append(result)

View File

@@ -34,12 +34,19 @@ class ShutdownLog(IOSExtraction):
return
for result in self.results:
if self.indicators.check_file_path(result["client"]):
self.log.warning("Found mention of a known malicious file \"%s\" in shutdown.log",
result["client"])
self.detected.append(result)
continue
for ioc in self.indicators.ioc_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.detected.append(result)
continue
def process_shutdownlog(self, content):
current_processes = []

View File

@@ -41,13 +41,13 @@ class LocationdClients(IOSExtraction):
def serialize(self, record):
records = []
for ts in self.timestamps:
if ts in record.keys():
for timestamp in self.timestamps:
if timestamp in record.keys():
records.append({
"timestamp": record[ts],
"timestamp": record[timestamp],
"module": self.__class__.__name__,
"event": ts,
"data": f"{ts} from {record['package']}"
"event": timestamp,
"data": f"{timestamp} from {record['package']}"
})
return records
@@ -61,7 +61,31 @@ class LocationdClients(IOSExtraction):
proc_name = parts[len(parts)-1]
if self.indicators.check_process(proc_name):
self.log.warning("Found a suspicious process name in LocationD entry %s",
result["package"])
self.detected.append(result)
continue
if "BundlePath" in result:
if self.indicators.check_file_path(result["BundlePath"]):
self.log.warning("Found a suspicious file path in Location D: %s",
result["BundlePath"])
self.detected.append(result)
continue
if "Executable" in result:
if self.indicators.check_file_path(result["Executable"]):
self.log.warning("Found a suspicious file path in Location D: %s",
result["Executable"])
self.detected.append(result)
continue
if "Registered" in result:
if self.indicators.check_file_path(result["Registered"]):
self.log.warning("Found a suspicious file path in Location D: %s",
result["Registered"])
self.detected.append(result)
continue
def _extract_locationd_entries(self, file_path):
with open(file_path, "rb") as handle:

View File

@@ -34,13 +34,21 @@ class Shortcuts(IOSExtraction):
found_urls = ""
if record["action_urls"]:
found_urls = "- URLs in actions: {}".format(", ".join(record["action_urls"]))
desc = ""
if record["description"]:
desc = record["description"].decode('utf-8', errors='ignore')
return {
return [{
"timestamp": record["isodate"],
"module": self.__class__.__name__,
"event": "shortcut",
"data": f"iOS Shortcut '{record['shortcut_name']}': {record['description']} {found_urls}"
}
"event": "shortcut_created",
"data": f"iOS Shortcut '{record['shortcut_name'].decode('utf-8')}': {desc} {found_urls}"
}, {
"timestamp": record["modified_date"],
"module": self.__class__.__name__,
"event": "shortcut_modified",
"data": f"iOS Shortcut '{record['shortcut_name'].decode('utf-8')}': {desc} {found_urls}"
}]
def check_indicators(self):
if not self.indicators:
@@ -92,14 +100,13 @@ class Shortcuts(IOSExtraction):
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)
# pprint.pprint(actions)
shortcut["isodate"] = convert_timestamp_to_iso(convert_mactime_to_unix(shortcut.pop("created_date")))
shortcut["modified_date"] = convert_timestamp_to_iso(convert_mactime_to_unix(shortcut["modified_date"]))
shortcut["parsed_actions"] = len(actions)

View File

@@ -66,6 +66,15 @@ class TCC(IOSExtraction):
"data": msg
}
def check_indicators(self):
if not self.indicators:
return
for result in self.results:
if self.indicators.check_process(result["client"]):
self.log.warning("Found malicious process in TCC database: %s", result["client"])
self.detected.append(result)
def process_db(self, file_path):
conn = sqlite3.connect(file_path)
cur = conn.cursor()

View File

@@ -234,6 +234,8 @@ IPHONE_IOS_VERSIONS = [
{"build": "19A404", "version": "15.0.2"},
{"build": "19B74", "version": "15.1"},
{"build": "19B81", "version": "15.1.1"},
{"build": "19C56", "version": "15.2"},
{"build": "19C63", "version": "15.2.1"},
]

0
tests/__init__.py Normal file
View File

1
tests/artifacts/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
test.stix2

View File

@@ -0,0 +1,50 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
from stix2.v21 import Bundle, Indicator, Malware, Relationship
def generate_test_stix_file(file_path):
if os.path.isfile(file_path):
os.remove(file_path)
domains = ["example.org"]
processes = ["Launch"]
emails = ["foobar@example.org"]
filenames = ["/var/foobar/txt"]
res = []
malware = Malware(name="TestMalware", is_family=False, description="")
res.append(malware)
for d in domains:
i = Indicator(indicator_types=["malicious-activity"], pattern="[domain-name:value='{}']".format(d), pattern_type="stix")
res.append(i)
res.append(Relationship(i, "indicates", malware))
for p in processes:
i = Indicator(indicator_types=["malicious-activity"], pattern="[process:name='{}']".format(p), pattern_type="stix")
res.append(i)
res.append(Relationship(i, "indicates", malware))
for f in filenames:
i = Indicator(indicator_types=["malicious-activity"], pattern="[file:name='{}']".format(f), pattern_type="stix")
res.append(i)
res.append(Relationship(i, "indicates", malware))
for e in emails:
i = Indicator(indicator_types=["malicious-activity"], pattern="[email-addr:value='{}']".format(e), pattern_type="stix")
res.append(i)
res.append(Relationship(i, "indicates", malware))
bundle = Bundle(objects=res)
with open(file_path, "w+") as f:
f.write(bundle.serialize(pretty=True))
if __name__ == "__main__":
generate_test_stix_file("test.stix2")
print("test.stix2 file created")

Binary file not shown.

Binary file not shown.

0
tests/common/__init__.py Normal file
View File

View File

@@ -0,0 +1,32 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import os
from mvt.common.indicators import Indicators
class TestIndicators:
def test_parse_stix2(self, indicator_file):
ind = Indicators(log=logging)
ind.load_indicators_files([indicator_file], load_default=False)
assert ind.ioc_count == 4
assert len(ind.ioc_domains) == 1
assert len(ind.ioc_emails) == 1
assert len(ind.ioc_files) == 1
assert len(ind.ioc_processes) == 1
def test_check_domain(self, indicator_file):
ind = Indicators(log=logging)
ind.load_indicators_files([indicator_file], load_default=False)
assert ind.check_domain("https://www.example.org/foobar")
assert ind.check_domain("http://example.org:8080/toto")
def test_env_stix(self, indicator_file):
os.environ["MVT_STIX2"] = indicator_file
ind = Indicators(log=logging)
ind.load_indicators_files([indicator_file], load_default=False)
assert ind.ioc_count == 4

26
tests/conftest.py Normal file
View File

@@ -0,0 +1,26 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import pytest
from .artifacts.generate_stix import generate_test_stix_file
@pytest.fixture(scope="session", autouse=True)
def indicator_file(request, tmp_path_factory):
indicator_dir = tmp_path_factory.mktemp("indicators")
stix_path = indicator_dir / "indicators.stix2"
generate_test_stix_file(stix_path)
return str(stix_path)
@pytest.fixture(scope="session", autouse=True)
def clean_test_env(request, tmp_path_factory):
try:
del os.environ["MVT_STIX2"]
except KeyError:
pass

0
tests/ios/__init__.py Normal file
View File

View File

@@ -0,0 +1,19 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.common.module import run_module
from mvt.ios.modules.backup.backup_info import BackupInfo
from ..utils import get_backup_folder
class TestBackupInfoModule:
def test_manifest(self):
m = BackupInfo(base_folder=get_backup_folder(), log=logging)
run_module(m)
assert m.results["Build Version"] == "18C66"
assert m.results["IMEI"] == "42"

View File

@@ -0,0 +1,31 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
from mvt.ios.modules.mixed.net_datausage import Datausage
from ..utils import get_backup_folder
class TestDatausageModule:
def test_datausage(self):
m = Datausage(base_folder=get_backup_folder(), log=logging, results=[])
run_module(m)
assert len(m.results) == 42
assert len(m.timeline) == 60
assert len(m.detected) == 0
def test_detection(self, indicator_file):
m = Datausage(base_folder=get_backup_folder(), log=logging, results=[])
ind = Indicators(log=logging)
ind.parse_stix2(indicator_file)
# Adds a file that exists in the manifest.
ind.ioc_processes[0] = "CumulativeUsageTracker"
m.indicators = ind
run_module(m)
assert len(m.detected) == 2

View File

@@ -0,0 +1,31 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
from mvt.ios.modules.backup.manifest import Manifest
from ..utils import get_backup_folder
class TestManifestModule:
def test_manifest(self):
m = Manifest(base_folder=get_backup_folder(), log=logging, results=[])
run_module(m)
assert len(m.results) == 3721
assert len(m.timeline) == 5881
assert len(m.detected) == 0
def test_detection(self, indicator_file):
m = Manifest(base_folder=get_backup_folder(), log=logging, results=[])
ind = Indicators(log=logging)
ind.parse_stix2(indicator_file)
# Adds a file that exists in the manifest
ind.ioc_files[0] = "com.apple.CoreBrightness.plist"
m.indicators = ind
run_module(m)
assert len(m.detected) == 1

36
tests/ios/test_tcc.py Normal file
View File

@@ -0,0 +1,36 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
from mvt.ios.modules.mixed.tcc import TCC
from ..utils import get_backup_folder
class TestTCCtModule:
def test_tcc(self):
m = TCC(base_folder=get_backup_folder(), log=logging, results=[])
run_module(m)
assert len(m.results) == 11
assert len(m.timeline) == 11
assert len(m.detected) == 0
assert m.results[0]["service"] == "kTCCServiceUbiquity"
assert m.results[0]["client"] == "com.apple.Preferences"
assert m.results[0]["auth_value"] == "allowed"
def test_tcc_detection(self, indicator_file):
m = TCC(base_folder=get_backup_folder(), log=logging, results=[])
ind = Indicators(log=logging)
ind.parse_stix2(indicator_file)
m.indicators = ind
run_module(m)
assert len(m.results) == 11
assert len(m.timeline) == 11
assert len(m.detected) == 1
assert m.detected[0]["service"] == "kTCCServiceLiverpool"
assert m.detected[0]["client"] == "Launch"

28
tests/utils.py Normal file
View File

@@ -0,0 +1,28 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021 The MVT Project Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
def get_artifact(fname):
"""
Return the artifact path in the artifact folder
"""
fpath = os.path.join(get_artifact_folder(), fname)
if os.path.isfile(fpath):
return fpath
return
def get_artifact_folder():
return os.path.join(os.path.dirname(__file__), "artifacts")
def get_backup_folder():
return os.path.join(os.path.dirname(__file__), "artifacts", "ios_backup")
def get_indicator_file():
print("PYTEST env", os.getenv("PYTEST_CURRENT_TEST"))