From b5d7e528defad0f17c676fbf21df717d24191c47 Mon Sep 17 00:00:00 2001 From: tek Date: Wed, 29 Mar 2023 12:57:41 +0200 Subject: [PATCH] Adds indicators for android properties --- mvt/android/modules/adb/getprop.py | 19 +++++++++++++---- mvt/android/modules/androidqf/getprop.py | 26 ++++++++++++++++-------- mvt/android/parsers/getprop.py | 13 +++++++----- mvt/common/indicators.py | 26 ++++++++++++++++++++++++ tests/android_androidqf/test_getprop.py | 14 +++++++++++++ tests/artifacts/generate_stix.py | 6 ++++++ tests/common/test_indicators.py | 11 ++++++++-- 7 files changed, 96 insertions(+), 19 deletions(-) diff --git a/mvt/android/modules/adb/getprop.py b/mvt/android/modules/adb/getprop.py index d603dd0..5574913 100644 --- a/mvt/android/modules/adb/getprop.py +++ b/mvt/android/modules/adb/getprop.py @@ -30,6 +30,16 @@ class Getprop(AndroidExtraction): self.results = {} if not results else results + def check_indicators(self) -> None: + if not self.indicators: + return + + for result in self.results: + ioc = self.indicators.check_android_property_name(result.get("name", "")) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + def run(self) -> None: self._adb_connect() output = self._adb_command("getprop") @@ -38,13 +48,14 @@ class Getprop(AndroidExtraction): self.results = parse_getprop(output) # Alert if phone is outdated. - security_patch = self.results.get("ro.build.version.security_patch", "") - if security_patch: - patch_date = datetime.strptime(security_patch, "%Y-%m-%d") + for entry in self.results: + if entry.get("name", "") != "ro.build.version.security_patch": + continue + patch_date = datetime.strptime(entry["value"], "%Y-%m-%d") if (datetime.now() - patch_date) > timedelta(days=6*30): self.log.warning("This phone has not received security updates " "for more than six months (last update: %s)", - security_patch) + entry["value"]) self.log.info("Extracted %d Android system properties", len(self.results)) diff --git a/mvt/android/modules/androidqf/getprop.py b/mvt/android/modules/androidqf/getprop.py index 988e2a7..4866422 100644 --- a/mvt/android/modules/androidqf/getprop.py +++ b/mvt/android/modules/androidqf/getprop.py @@ -7,7 +7,7 @@ import logging from datetime import datetime, timedelta from typing import Optional -from mvt.android.parsers import getprop +from mvt.android.parsers.getprop import parse_getprop from .base import AndroidQFModule @@ -41,7 +41,17 @@ class Getprop(AndroidQFModule): super().__init__(file_path=file_path, target_path=target_path, results_path=results_path, fast_mode=fast_mode, log=log, results=results) - self.results = {} + self.results = [] + + def check_indicators(self) -> None: + if not self.indicators: + return + + for result in self.results: + ioc = self.indicators.check_android_property_name(result.get("name", "")) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) def run(self) -> None: getprop_files = self._get_files_by_pattern("*/getprop.txt") @@ -52,15 +62,15 @@ class Getprop(AndroidQFModule): with open(getprop_files[0]) as f: data = f.read() - self.results = getprop.parse_getprop(data) + self.results = 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 entry["name"] in INTERESTING_PROPERTIES: + self.log.info("%s: %s", entry["name"], entry["value"]) + if entry["name"] == "ro.build.version.security_patch": + last_patch = datetime.strptime(entry["value"], "%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]) + "(last update: %s)", entry["value"]) self.log.info("Extracted a total of %d properties", len(self.results)) diff --git a/mvt/android/parsers/getprop.py b/mvt/android/parsers/getprop.py index 0e3857e..eadf298 100644 --- a/mvt/android/parsers/getprop.py +++ b/mvt/android/parsers/getprop.py @@ -4,10 +4,11 @@ # https://license.mvt.re/1.1/ import re +from typing import Dict, List -def parse_getprop(output: str) -> dict: - results = {} +def parse_getprop(output: str) -> List[Dict[str, str]]: + results = [] rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]") for line in output.splitlines(): @@ -19,8 +20,10 @@ def parse_getprop(output: str) -> dict: if not matches or len(matches[0]) != 2: continue - key = matches[0][0] - value = matches[0][1] - results[key] = value + entry = { + "name": matches[0][0], + "value": matches[0][1] + } + results.append(entry) return results diff --git a/mvt/common/indicators.py b/mvt/common/indicators.py index b27f0ca..4b47bb0 100644 --- a/mvt/common/indicators.py +++ b/mvt/common/indicators.py @@ -72,6 +72,7 @@ class Indicators: "files_sha256": [], "app_ids": [], "ios_profile_ids": [], + "android_property_names": [], "count": 0, } @@ -121,6 +122,11 @@ class Indicators: ioc_coll=collection, ioc_coll_list=collection["ios_profile_ids"]) + elif key == "android-property:name": + self._add_indicator(ioc=value, + ioc_coll=collection, + ioc_coll_list=collection["android_property_names"]) + def parse_stix2(self, file_path: str) -> None: """Extract indicators from a STIX2 file. @@ -519,3 +525,23 @@ class Indicators: return ioc return None + + def check_android_property_name(self, property_name: str) -> Optional[dict]: + """Check the android property name against the list of indicators. + + :param property_name: Name of the Android property + :type property_name: str + :returns: Indicator details if matched, otherwise None + + """ + if property_name is None: + return None + + for ioc in self.get_iocs("android_property_names"): + if property_name.lower() == ioc["value"].lower(): + self.log.warning("Found a known suspicious Android property \"%s\" " + "matching indicators from \"%s\"", property_name, + ioc["name"]) + return ioc + + return None diff --git a/tests/android_androidqf/test_getprop.py b/tests/android_androidqf/test_getprop.py index f9c99a9..0e87c00 100644 --- a/tests/android_androidqf/test_getprop.py +++ b/tests/android_androidqf/test_getprop.py @@ -7,6 +7,7 @@ import logging import os from mvt.android.modules.androidqf.getprop import Getprop +from mvt.common.indicators import Indicators from mvt.common.module import run_module from ..utils import get_artifact_folder @@ -18,5 +19,18 @@ class TestAndroidqfGetpropAnalysis: m = Getprop(target_path=os.path.join(get_artifact_folder(), "androidqf"), log=logging) run_module(m) assert len(m.results) == 10 + assert m.results[0]["name"] == "dalvik.vm.appimageformat" + assert m.results[0]["value"] == "lz4" assert len(m.timeline) == 0 assert len(m.detected) == 0 + + def test_androidqf_getprop_detection(self, indicator_file): + m = Getprop(target_path=os.path.join(get_artifact_folder(), "androidqf"), log=logging) + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + ind.ioc_collections[0]["android_property_names"].append("dalvik.vm.heapmaxfree") + m.indicators = ind + run_module(m) + assert len(m.results) == 10 + assert len(m.detected) == 1 + assert m.detected[0]["name"] == "dalvik.vm.heapmaxfree" diff --git a/tests/artifacts/generate_stix.py b/tests/artifacts/generate_stix.py index 229289c..57db7d9 100644 --- a/tests/artifacts/generate_stix.py +++ b/tests/artifacts/generate_stix.py @@ -16,6 +16,7 @@ def generate_test_stix_file(file_path): processes = ["Launch"] emails = ["foobar@example.org"] filenames = ["/var/foobar/txt"] + android_property = ["sys.foobar"] res = [] malware = Malware(name="TestMalware", is_family=False, description="") @@ -40,6 +41,11 @@ def generate_test_stix_file(file_path): res.append(i) res.append(Relationship(i, "indicates", malware)) + for p in android_property: + i = Indicator(indicator_types=["malicious-activity"], pattern="[android-property:name='{}']".format(p), pattern_type="stix") + res.append(i) + res.append(Relationship(i, "indicates", malware)) + bundle = Bundle(objects=res) with open(file_path, "w+", encoding="utf-8") as f: f.write(bundle.serialize(pretty=True)) diff --git a/tests/common/test_indicators.py b/tests/common/test_indicators.py index 79e4481..4d469c2 100644 --- a/tests/common/test_indicators.py +++ b/tests/common/test_indicators.py @@ -14,11 +14,12 @@ 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_collections[0]["count"] == 4 + assert ind.ioc_collections[0]["count"] == 5 assert len(ind.ioc_collections[0]["domains"]) == 1 assert len(ind.ioc_collections[0]["emails"]) == 1 assert len(ind.ioc_collections[0]["file_names"]) == 1 assert len(ind.ioc_collections[0]["processes"]) == 1 + assert len(ind.ioc_collections[0]["android_property_names"]) == 1 def test_check_domain(self, indicator_file): ind = Indicators(log=logging) @@ -26,8 +27,14 @@ class TestIndicators: assert ind.check_domain("https://www.example.org/foobar") assert ind.check_domain("http://example.org:8080/toto") + def test_check_android_property(self, indicator_file): + ind = Indicators(log=logging) + ind.load_indicators_files([indicator_file], load_default=False) + assert ind.check_android_property_name("sys.foobar") + assert ind.check_android_property_name("sys.soundsokay") is None + def test_env_stix(self, indicator_file): os.environ["MVT_STIX2"] = indicator_file ind = Indicators(log=logging) ind.load_indicators_files([], load_default=False) - assert ind.total_ioc_count == 4 + assert ind.total_ioc_count == 5