From caeeec281645ec3228c7710c85aef20fb030e9e8 Mon Sep 17 00:00:00 2001 From: Rory Flynn <75283103+roaree@users.noreply.github.com> Date: Mon, 24 Jun 2024 19:00:07 +0200 Subject: [PATCH] Add packages module for androidqf (#506) * Add Packages module for androidqf * Update test --- mvt/android/modules/androidqf/__init__.py | 2 + mvt/android/modules/androidqf/packages.py | 97 +++++++++ mvt/android/utils.py | 12 ++ tests/android_androidqf/test_packages.py | 87 ++++++++ tests/artifacts/androidqf/packages.json | 233 ++++++++++++++++++++++ tests/common/test_utils.py | 2 +- tests/conftest.py | 30 +++ 7 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 mvt/android/modules/androidqf/packages.py create mode 100644 tests/android_androidqf/test_packages.py create mode 100644 tests/artifacts/androidqf/packages.json diff --git a/mvt/android/modules/androidqf/__init__.py b/mvt/android/modules/androidqf/__init__.py index e7bb765..eaa7546 100644 --- a/mvt/android/modules/androidqf/__init__.py +++ b/mvt/android/modules/androidqf/__init__.py @@ -12,6 +12,7 @@ from .dumpsys_dbinfo import DumpsysDBInfo from .dumpsys_packages import DumpsysPackages from .dumpsys_receivers import DumpsysReceivers from .getprop import Getprop +from .packages import Packages from .processes import Processes from .settings import Settings from .sms import SMS @@ -24,6 +25,7 @@ ANDROIDQF_MODULES = [ DumpsysDBInfo, DumpsysBatteryDaily, DumpsysBatteryHistory, + Packages, Processes, Getprop, Settings, diff --git a/mvt/android/modules/androidqf/packages.py b/mvt/android/modules/androidqf/packages.py new file mode 100644 index 0000000..8edcf3c --- /dev/null +++ b/mvt/android/modules/androidqf/packages.py @@ -0,0 +1,97 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 The MVT Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging +from typing import Optional +import json + +from mvt.android.utils import ( + ROOT_PACKAGES, + BROWSER_INSTALLERS, + PLAY_STORE_INSTALLERS, + THIRD_PARTY_STORE_INSTALLERS, +) + +from .base import AndroidQFModule + + +class Packages(AndroidQFModule): + """This module examines the installed packages in packages.json""" + + def __init__( + self, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, + module_options: Optional[dict] = None, + 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, + module_options=module_options, + log=log, + results=results, + ) + + def check_indicators(self) -> None: + for result in self.results: + if result["name"] in ROOT_PACKAGES: + self.log.warning( + "Found an installed package related to " + 'rooting/jailbreaking: "%s"', + result["name"], + ) + self.detected.append(result) + continue + + # Detections for apps installed via unusual methods + if result["installer"] in THIRD_PARTY_STORE_INSTALLERS: + self.log.warning( + 'Found a package installed via a third party store (installer="%s"): "%s"', + result["installer"], + result["name"], + ) + elif result["installer"] in BROWSER_INSTALLERS: + self.log.warning( + 'Found a package installed via a browser (installer="%s"): "%s"', + result["installer"], + result["name"], + ) + elif result["installer"] == "null" and result["system"] is False: + self.log.warning( + 'Found a non-system package installed via adb or another method: "%s"', + result["name"], + ) + elif result["installer"] in PLAY_STORE_INSTALLERS: + pass + + if not self.indicators: + continue + + ioc = self.indicators.check_app_id(result.get("name")) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + continue + + for package_file in result.get("files", []): + ioc = self.indicators.check_file_hash(package_file["sha256"]) + if ioc: + result["matched_indicator"] = ioc + self.detected.append(result) + + def run(self) -> None: + packages = self._get_files_by_pattern("*/packages.json") + if not packages: + self.log.error( + "packages.json file not found in this androidqf bundle. Possibly malformed?" + ) + return + + self.results = json.loads(self._get_file_content(packages[0])) + self.log.info("Found %d packages in packages.json", len(self.results)) diff --git a/mvt/android/utils.py b/mvt/android/utils.py index 955f704..2455959 100644 --- a/mvt/android/utils.py +++ b/mvt/android/utils.py @@ -91,3 +91,15 @@ SYSTEM_UPDATE_PACKAGES = [ "com.transsion.systemupdate", "com.wssyncmldm", ] + +# Apps installed from the Play store have this installer +PLAY_STORE_INSTALLERS = ["com.android.vending"] + +# Installer id for apps from common 3rd party stores +THIRD_PARTY_STORE_INSTALLERS = ["com.aurora.store", "org.fdroid.fdroid"] + +# Packages installed via a browser have these installers +BROWSER_INSTALLERS = [ + "com.google.android.packageinstaller", + "com.android.packageinstaller", +] diff --git a/tests/android_androidqf/test_packages.py b/tests/android_androidqf/test_packages.py new file mode 100644 index 0000000..c56d393 --- /dev/null +++ b/tests/android_androidqf/test_packages.py @@ -0,0 +1,87 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2023 The MVT Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import logging +import pytest +from pathlib import Path + +from mvt.android.modules.androidqf.packages import Packages +from mvt.common.module import run_module + +from ..utils import get_android_androidqf, list_files + + +@pytest.fixture() +def data_path(): + return get_android_androidqf() + + +@pytest.fixture() +def parent_data_path(data_path): + return Path(data_path).absolute().parent.as_posix() + + +@pytest.fixture() +def file_list(data_path): + return list_files(data_path) + + +@pytest.fixture() +def module(parent_data_path, file_list): + m = Packages(target_path=parent_data_path, log=logging) + m.from_folder(parent_data_path, file_list) + return m + + +class TestAndroidqfPackages: + def test_packages_list(self, module): + run_module(module) + + # There should just be 7 packages listed, no detections + assert len(module.results) == 7 + assert len(module.timeline) == 0 + assert len(module.detected) == 0 + + def test_non_appstore_warnings(self, caplog, module): + run_module(module) + + # Not a super test to be searching logs for this but heuristic detections not yet formalised + assert ( + 'Found a non-system package installed via adb or another method: "com.whatsapp"' + in caplog.text + ) + assert ( + 'Found a package installed via a browser (installer="com.google.android.packageinstaller"): ' + '"app.revanced.manager.flutter"' in caplog.text + ) + assert ( + 'Found a package installed via a third party store (installer="org.fdroid.fdroid"): "org.nuclearfog.apollo"' + in caplog.text + ) + + def test_packages_ioc_package_names(self, module, indicators_factory): + module.indicators = indicators_factory(app_ids=["com.malware.blah"]) + + run_module(module) + + assert len(module.detected) == 1 + assert module.detected[0]["name"] == "com.malware.blah" + assert module.detected[0]["matched_indicator"]["value"] == "com.malware.blah" + + def test_packages_ioc_sha256(self, module, indicators_factory): + module.indicators = indicators_factory( + files_sha256=[ + "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa" + ] + ) + + run_module(module) + + assert len(module.detected) == 1 + assert module.detected[0]["name"] == "com.malware.muahaha" + assert ( + module.detected[0]["matched_indicator"]["value"] + == "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa" + ) diff --git a/tests/artifacts/androidqf/packages.json b/tests/artifacts/androidqf/packages.json new file mode 100644 index 0000000..2182581 --- /dev/null +++ b/tests/artifacts/androidqf/packages.json @@ -0,0 +1,233 @@ +[ + { + "name": "com.whatsapp", + "files": [ + { + "path": "/data/app/~~/com.whatsapp-~~/base.apk", + "local_name": "", + "md5": "5870bd06e642de410c54705226ecfa9a", + "sha1": "6cb06e9ab5619345f930c2b2096b4dd013a10ec9", + "sha256": "744ed47f8176ec423840344c33e88bd2c96e8988cda0797f3415bb5229efc12b", + "sha512": "f222f742b0bd302c82e202bc78f7ff8b2de4acfc8d606994245ffa80998b003e215cad82cae023abe4f65c0da0a56fa9890e9bb3a753af6dac848a753ac07aee", + "error": "", + "verified_certificate": true, + "certificate": { + "Md5": "556c6019249bbc0cab70495178d3a9d1", + "Sha1": "38a0f7d505fe18fec64fbf343ecaaaf310dbd799", + "Sha256": "3987d043d10aefaf5a8710b3671418fe57e0e19b653c9df82558feb5ffce5d44", + "ValidFrom": "2010-06-25T23:07:16Z", + "ValidTo": "2044-02-15T23:07:16Z", + "Issuer": "C=US, ST=California, L=Santa Clara, O=WhatsApp Inc., OU=Engineering, CN=Brian Acton", + "Subject": "C=US, ST=California, L=Santa Clara, O=WhatsApp Inc., OU=Engineering, CN=Brian Acton", + "SignatureAlgorithm": "DSA-SHA1", + "SerialNumber": 1277507236 + }, + "certificate_error": "", + "trusted_certificate": true + } + ], + "installer": "null", + "uid": 10271, + "disabled": false, + "system": false, + "third_party": true + }, + { + "name": "app.revanced.manager.flutter", + "files": [ + { + "path": "/data/app/~~==/app.revanced.manager.flutter-==/base.apk", + "local_name": "", + "md5": "aae9b55c6f2592233518bb5a173e8505", + "sha1": "9185a83dae0fc8a0ba79f89f3c84fe8a038f93af", + "sha256": "6ddb76f6180ca8bc0a11d5b343ac9ad8f137a351f20c080e989ca4310973d319", + "sha512": "923a57d4cdf2e7d48539307abbd12f982d61f393a1d058ceef0f6109301d21fedf0fe73c667f8add37fb35da570ac35c6b911360d9bf0389aa0bbbd53103ff46", + "error": "", + "verified_certificate": true, + "certificate": { + "Md5": "f822d70f449d798f0688e2c7358a429c", + "Sha1": "93adc8e2bd1687644f1143e184bcbfd57912ff2c", + "Sha256": "b6362c6ea7888efd15c0800f480786ad0f5b133b4f84e12d46afba5f9eac1223", + "ValidFrom": "2022-09-14T11:45:44Z", + "ValidTo": "2050-01-30T11:45:44Z", + "Issuer": "C=Unknown, ST=Unknown, L=Unknown, O=ReVanced, OU=ReVanced, CN=ReVanced Manager", + "Subject": "C=Unknown, ST=Unknown, L=Unknown, O=ReVanced, OU=ReVanced, CN=ReVanced Manager", + "SignatureAlgorithm": "SHA256-RSA", + "SerialNumber": 710526530 + }, + "certificate_error": "", + "trusted_certificate": false + } + ], + "installer": "com.google.android.packageinstaller", + "uid": 10266, + "disabled": false, + "system": false, + "third_party": true + }, + { + "name": "com.google.android.youtube", + "files": [ + { + "path": "/data/app/~~==/com.google.android.youtube-==/base.apk", + "local_name": "", + "md5": "3ec11b187ec6195e9ca4b5be671eba34", + "sha1": "33a9a89836690966498ba106283e76eff430365b", + "sha256": "a81b6392ab855905763272cf1a248b0d09fc675a91eabe7ef4ed589356a35241", + "sha512": "c736fbd07fe52539d8e96f6489a49c915c2bac472f0203f6187d167e2e3623f07db9e70b0fbc0494f6eeffb66a4cf71da56ad70503dc8138512faa3c1e847174", + "error": "", + "verified_certificate": true, + "certificate": { + "Md5": "d046fc5d1fc3cd0e57c5444097cd5449", + "Sha1": "24bb24c05e47e0aefa68a58a766179d9b613a600", + "Sha256": "3d7a1223019aa39d9ea0e3436ab7c0896bfb4fb679f4de5fe7c23f326c8f994a", + "ValidFrom": "2008-12-02T02:07:58Z", + "ValidTo": "2036-04-19T02:07:58Z", + "Issuer": "C=US, ST=CA, L=Mountain View, O=Google, Inc, OU=Google, Inc, CN=Unknown", + "Subject": "C=US, ST=CA, L=Mountain View, O=Google, Inc, OU=Google, Inc, CN=Unknown", + "SignatureAlgorithm": "MD5-RSA", + "SerialNumber": 1228183678 + }, + "certificate_error": "", + "trusted_certificate": true + } + ], + "installer": "com.google.android.packageinstaller", + "uid": 10194, + "disabled": false, + "system": true, + "third_party": false + }, + { + "name": "org.fdroid.fdroid", + "files": [ + { + "path": "/data/app/~~-==/org.fdroid.fdroid-==/base.apk", + "local_name": "", + "md5": "1f7524d15b3d229e5e89af609551e640", + "sha1": "4ce271a8ac2afb9f584f1deb165f1ab4768c50b0", + "sha256": "dc3bb88f6419ee7dde7d1547a41569aa03282fe00e0dc43ce035efd7c9d27d75", + "sha512": "40e9bfaf6c2833078e370c85001adcb7493851a5146d2b4067a9909266a0d7904f80825f040c8c6e0cb59ec6e8c0825d522ff963f6db780b049a24d47f81b289", + "error": "", + "verified_certificate": true, + "certificate": { + "Md5": "17c55c628056e193e95644e989792786", + "Sha1": "05f2e65928088981b317fc9a6dbfe04b0fa13b4e", + "Sha256": "43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab", + "ValidFrom": "2010-07-23T17:10:24Z", + "ValidTo": "2037-12-08T17:10:24Z", + "Issuer": "C=UK, ST=Unknown, L=Wetherby, O=Unknown, OU=Unknown, CN=Ciaran Gultnieks", + "Subject": "C=UK, ST=Unknown, L=Wetherby, O=Unknown, OU=Unknown, CN=Ciaran Gultnieks", + "SignatureAlgorithm": "SHA1-RSA", + "SerialNumber": 1279905024 + }, + "certificate_error": "", + "trusted_certificate": false + } + ], + "installer": "com.google.android.packageinstaller", + "uid": 10267, + "disabled": false, + "system": false, + "third_party": true + }, + { + "name": "org.nuclearfog.apollo", + "files": [ + { + "path": "/data/app/~~==/org.nuclearfog.apollo-==/base.apk", + "local_name": "", + "md5": "69f611758cc911f472fcabad6151684a", + "sha1": "1f5e450ef1901e245d4828735e0e93f0f94fb4da", + "sha256": "00bdfc80a397b449bef89dd2051ddd3c9d2a64e954176420b40c90a2af956799", + "sha512": "2af8037e0e226cba9f32227f709afc32fd8871c0077f73d00d59353d67ab843cb6641a5e0101d494699aeb91dcd136767fe9d76b30df65e1a1153f3c5b51a837", + "error": "", + "verified_certificate": true, + "certificate": { + "Md5": "2bef3d492d62fad190a8b6b7d71d42a4", + "Sha1": "cad23563b5be0c33611d827ee0da6ad5ef3be39a", + "Sha256": "e1a418c51baa829917daa2e86d7509a8a10470e44280c20146b70ea550bfe1ab", + "ValidFrom": "2022-01-15T20:17:10Z", + "ValidTo": "2047-01-09T20:17:10Z", + "Issuer": "C=DE, ST=Saarland, CN=nuclearfog", + "Subject": "C=DE, ST=Saarland, CN=nuclearfog", + "SignatureAlgorithm": "SHA256-RSA", + "SerialNumber": 75365821 + }, + "certificate_error": "", + "trusted_certificate": false + } + ], + "installer": "org.fdroid.fdroid", + "uid": 10272, + "disabled": false, + "system": false, + "third_party": true + }, + { + "name": "com.malware.blah", + "files": [ + { + "path": "/data/app/~~-==/com.malware.blah-==/base.apk", + "local_name": "", + "md5": "349ba2de140fccaf2ed2ac20f66e711f", + "sha1": "2cc5b4a70ada9229fb50d30f525392f2d66f58d6", + "sha256": "79a3569fbb63a9167ad8a2dad963616bb01474c87d769c7640f6d6810c448eae", + "sha512": "df1bbbfa6e895054b36093548558ee0d9fbf61ef09e617d3b3b158ba9f9c11825dbbf7e84711331afb80fc24ea0e5aa07a9db1919932c109c34fefec3c02d184", + "error": "", + "verified_certificate": true, + "certificate": { + "Md5": "54d5b5aca1e7e76bb1a26c61a9381b93", + "Sha1": "4ba9d1f82adb7be841bcf53b03ddae857747199a", + "Sha256": "c3e8cafdcd10e7cd9b2ec67f7abd4447b840431126066f6b16ed42151d2b4d64", + "ValidFrom": "2021-01-15T22:03:53Z", + "ValidTo": "2051-01-15T22:03:53Z", + "Issuer": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android", + "Subject": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android", + "SignatureAlgorithm": "SHA256-RSA", + "SerialNumber": 955466096586930338769951715633687128507538251257 + }, + "certificate_error": "", + "trusted_certificate": false + } + ], + "installer": "null", + "uid": 10058, + "disabled": false, + "system": true, + "third_party": false + }, + { + "name": "com.malware.muahaha", + "files": [ + { + "path": "/data/app/~~-==/com.malware.meh-==/base.apk", + "local_name": "", + "md5": "349ba2de140fccaf2ed2ac20f66e711f", + "sha1": "2cc5b4a70ada9229fb50d30f525392f2d66f58d6", + "sha256": "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa", + "sha512": "df1bbbfa6e895054b36093548558ee0d9fbf61ef09e617d3b3b158ba9f9c11825dbbf7e84711331afb80fc24ea0e5aa07a9db1919932c109c34fefec3c02d184", + "error": "", + "verified_certificate": true, + "certificate": { + "Md5": "54d5b5aca1e7e76bb1a26c61a9381b93", + "Sha1": "4ba9d1f82adb7be841bcf53b03ddae857747199a", + "Sha256": "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa", + "ValidFrom": "2021-01-15T22:03:53Z", + "ValidTo": "2051-01-15T22:03:53Z", + "Issuer": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android", + "Subject": "C=US, ST=California, L=Mountain View, O=Google Inc., OU=Android, CN=Android", + "SignatureAlgorithm": "SHA256-RSA", + "SerialNumber": 955466096586930338769951715633687128507538251257 + }, + "certificate_error": "", + "trusted_certificate": false + } + ], + "installer": "null", + "uid": 10058, + "disabled": false, + "system": true, + "third_party": false + } +] \ No newline at end of file diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py index 4d03c8f..78f0edb 100644 --- a/tests/common/test_utils.py +++ b/tests/common/test_utils.py @@ -62,7 +62,7 @@ class TestHashes: def test_hash_from_folder(self): path = os.path.join(get_artifact_folder(), "androidqf") hashes = list(generate_hashes_from_path(path, logging)) - assert len(hashes) == 5 + assert len(hashes) == 6 # Sort the files to have reliable order for tests. hashes = sorted(hashes, key=lambda x: x["file_path"]) assert hashes[0]["file_path"] == os.path.join(path, "backup.ab") diff --git a/tests/conftest.py b/tests/conftest.py index a0e7674..22a4f14 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,8 @@ import os import pytest from .artifacts.generate_stix import generate_test_stix_file +import logging +from mvt.common.indicators import Indicators @pytest.fixture(scope="session", autouse=True) @@ -24,3 +26,31 @@ def clean_test_env(request, tmp_path_factory): del os.environ["MVT_STIX2"] except KeyError: pass + + +@pytest.fixture() +def indicators_factory(indicator_file): + def f( + domains=[], + emails=[], + file_names=[], + processes=[], + app_ids=[], + android_property_names=[], + files_sha256=[], + ): + + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + + ind.ioc_collections[0]["domains"].extend(domains) + ind.ioc_collections[0]["emails"].extend(emails) + ind.ioc_collections[0]["file_names"].extend(file_names) + ind.ioc_collections[0]["processes"].extend(processes) + ind.ioc_collections[0]["app_ids"].extend(app_ids) + ind.ioc_collections[0]["android_property_names"].extend(android_property_names) + ind.ioc_collections[0]["files_sha256"].extend(files_sha256) + + return ind + + return f