diff --git a/mvt/android/artifacts/dumpsys_packages.py b/mvt/android/artifacts/dumpsys_packages.py new file mode 100644 index 0000000..2ca7e4c --- /dev/null +++ b/mvt/android/artifacts/dumpsys_packages.py @@ -0,0 +1,203 @@ +# 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 re +from typing import Any, Dict, List, Union + +from mvt.android.utils import ROOT_PACKAGES + +from .artifact import AndroidArtifact + + +class DumpsysPackagesArtifact(AndroidArtifact): + 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 serialize(self, record: dict) -> Union[dict, list]: + records = [] + + timestamps = [ + {"event": "package_install", "timestamp": record["timestamp"]}, + { + "event": "package_first_install", + "timestamp": record["first_install_time"], + }, + {"event": "package_last_update", "timestamp": record["last_update_time"]}, + ] + + for timestamp in timestamps: + records.append( + { + "timestamp": timestamp["timestamp"], + "module": self.__class__.__name__, + "event": timestamp["event"], + "data": f"Install or update of package {record['package_name']}", + } + ) + + return records + + @staticmethod + def parse_dumpsys_package_for_details(output: str) -> Dict[str, Any]: + """ + 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(self, output: str) -> List[Dict[str, Any]]: + """ + 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 = self.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 = self.parse_dumpsys_package_for_details("\n".join(lines)) + package.update(details) + results.append(package) + + return results + + def parse(self, content: str): + """ + Parse the Dumpsys Package section for activities + Adds results to self.results + + :param content: content of the package section (string) + """ + self.results = [] + package = [] + + in_package_list = False + for line in content.split("\n"): + if line.startswith("Packages:"): + in_package_list = True + continue + + if not in_package_list: + continue + + if line.strip() == "": + break + + package.append(line) + + self.results = self.parse_dumpsys_packages("\n".join(package)) diff --git a/mvt/android/modules/adb/packages.py b/mvt/android/modules/adb/packages.py index 183aced..078d8dc 100644 --- a/mvt/android/modules/adb/packages.py +++ b/mvt/android/modules/adb/packages.py @@ -4,86 +4,25 @@ # https://license.mvt.re/1.1/ import logging -from typing import List, Optional, 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.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact +from mvt.android.utils import ( + DANGEROUS_PERMISSIONS, + DANGEROUS_PERMISSIONS_THRESHOLD, + ROOT_PACKAGES, + SECURITY_PACKAGES, + SYSTEM_UPDATE_PACKAGES, +) from mvt.common.virustotal import VTNoKey, VTQuotaExceeded, virustotal_lookup from .base import AndroidExtraction -DANGEROUS_PERMISSIONS_THRESHOLD = 10 -DANGEROUS_PERMISSIONS = [ - "android.permission.ACCESS_COARSE_LOCATION", - "android.permission.ACCESS_FINE_LOCATION", - "android.permission.AUTHENTICATE_ACCOUNTS", - "android.permission.CAMERA", - "android.permission.DISABLE_KEYGUARD", - "android.permission.PROCESS_OUTGOING_CALLS", - "android.permission.READ_CALENDAR", - "android.permission.READ_CALL_LOG", - "android.permission.READ_CONTACTS", - "android.permission.READ_PHONE_STATE", - "android.permission.READ_SMS", - "android.permission.RECEIVE_MMS", - "android.permission.RECEIVE_SMS", - "android.permission.RECEIVE_WAP_PUSH", - "android.permission.RECORD_AUDIO", - "android.permission.SEND_SMS", - "android.permission.SYSTEM_ALERT_WINDOW", - "android.permission.USE_CREDENTIALS", - "android.permission.USE_SIP", - "com.android.browser.permission.READ_HISTORY_BOOKMARKS", -] -ROOT_PACKAGES: List[str] = [ - "com.noshufou.android.su", - "com.noshufou.android.su.elite", - "eu.chainfire.supersu", - "com.koushikdutta.superuser", - "com.thirdparty.superuser", - "com.yellowes.su", - "com.koushikdutta.rommanager", - "com.koushikdutta.rommanager.license", - "com.dimonvideo.luckypatcher", - "com.chelpus.lackypatch", - "com.ramdroid.appquarantine", - "com.ramdroid.appquarantinepro", - "com.devadvance.rootcloak", - "com.devadvance.rootcloakplus", - "de.robv.android.xposed.installer", - "com.saurik.substrate", - "com.zachspong.temprootremovejb", - "com.amphoras.hidemyroot", - "com.amphoras.hidemyrootadfree", - "com.formyhm.hiderootPremium", - "com.formyhm.hideroot", - "me.phh.superuser", - "eu.chainfire.supersu.pro", - "com.kingouser.com", - "com.topjohnwu.magisk", -] -SECURITY_PACKAGES = [ - "com.policydm", - "com.samsung.android.app.omcagent", - "com.samsung.android.securitylogagent", - "com.sec.android.soagent", -] -SYSTEM_UPDATE_PACKAGES = [ - "com.android.updater", - "com.google.android.gms", - "com.huawei.android.hwouc", - "com.lge.lgdmsclient", - "com.motorola.ccc.ota", - "com.oneplus.opbackup", - "com.oppo.ota", - "com.transsion.systemupdate", - "com.wssyncmldm", -] - class Packages(AndroidExtraction): """This module extracts the list of installed packages.""" @@ -234,7 +173,9 @@ class Packages(AndroidExtraction): if line.strip() == "Packages:": in_packages = True - return parse_dumpsys_package_for_details("\n".join(lines)) + return DumpsysPackagesArtifact.parse_dumpsys_package_for_details( + "\n".join(lines) + ) def _get_files_for_package(self, package_name: str) -> list: command = f"pm path {package_name}" diff --git a/mvt/android/modules/androidqf/dumpsys_packages.py b/mvt/android/modules/androidqf/dumpsys_packages.py index bbd5bad..8df7144 100644 --- a/mvt/android/modules/androidqf/dumpsys_packages.py +++ b/mvt/android/modules/androidqf/dumpsys_packages.py @@ -4,19 +4,18 @@ # https://license.mvt.re/1.1/ import logging -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional +from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact 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 AndroidQFModule -class DumpsysPackages(AndroidQFModule): +class DumpsysPackages(DumpsysPackagesArtifact, AndroidQFModule): """This module analyse dumpsys packages""" def __init__( @@ -37,70 +36,15 @@ class DumpsysPackages(AndroidQFModule): 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 - data = self._get_file_content(dumpsys_file[0]) - - package = [] - in_service = False - in_package_list = False - for line in data.decode("utf-8", errors="ignore").split("\n"): - 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)) + data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace") + content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:") + self.parse(content) for result in self.results: dangerous_permissions_count = 0 diff --git a/mvt/android/modules/bugreport/packages.py b/mvt/android/modules/bugreport/packages.py index ba50993..f1b9d63 100644 --- a/mvt/android/modules/bugreport/packages.py +++ b/mvt/android/modules/bugreport/packages.py @@ -4,19 +4,15 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional -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.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact +from mvt.android.utils import DANGEROUS_PERMISSIONS, DANGEROUS_PERMISSIONS_THRESHOLD from .base import BugReportModule -class Packages(BugReportModule): +class Packages(DumpsysPackagesArtifact, BugReportModule): """This module extracts details on receivers for risky activities.""" def __init__( @@ -37,83 +33,18 @@ class Packages(BugReportModule): results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: - records = [] - - timestamps = [ - {"event": "package_install", "timestamp": record["timestamp"]}, - { - "event": "package_first_install", - "timestamp": record["first_install_time"], - }, - {"event": "package_last_update", "timestamp": record["last_update_time"]}, - ] - - for timestamp in timestamps: - records.append( - { - "timestamp": timestamp["timestamp"], - "module": self.__class__.__name__, - "event": timestamp["event"], - "data": f"Install or update of package {record['package_name']}", - } - ) - - return records - - 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) - continue - def run(self) -> None: - content = self._get_dumpstate_file() - if not content: + data = self._get_dumpstate_file() + if not data: self.log.error( "Unable to find dumpstate file. " "Did you provide a valid bug report archive?" ) return - in_package = False - in_packages_list = False - lines = [] - for line in content.decode(errors="ignore").splitlines(): - if line.strip() == "DUMP OF SERVICE package:": - in_package = True - continue - - if not in_package: - continue - - if line.strip() == "Packages:": - in_packages_list = True - continue - - if not in_packages_list: - continue - - if line.strip() == "": - break - - lines.append(line) - - self.results = parse_dumpsys_packages("\n".join(lines)) + data = data.decode("utf-8", errors="replace") + content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:") + self.parse(content) for result in self.results: dangerous_permissions_count = 0 diff --git a/mvt/android/parsers/dumpsys.py b/mvt/android/parsers/dumpsys.py deleted file mode 100644 index 6338c86..0000000 --- a/mvt/android/parsers/dumpsys.py +++ /dev/null @@ -1,131 +0,0 @@ -# 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 re -from typing import Any, Dict, List - - -def parse_dumpsys_package_for_details(output: str) -> Dict[str, Any]: - """ - 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[Dict[str, Any]]: - """ - 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 diff --git a/mvt/android/utils.py b/mvt/android/utils.py index 043593f..955f704 100644 --- a/mvt/android/utils.py +++ b/mvt/android/utils.py @@ -3,6 +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 datetime import datetime, timedelta +from typing import List def warn_android_patch_level(patch_level: str, log) -> bool: @@ -17,3 +18,76 @@ def warn_android_patch_level(patch_level: str, log) -> bool: return True return False + + +ROOT_PACKAGES: List[str] = [ + "com.noshufou.android.su", + "com.noshufou.android.su.elite", + "eu.chainfire.supersu", + "com.koushikdutta.superuser", + "com.thirdparty.superuser", + "com.yellowes.su", + "com.koushikdutta.rommanager", + "com.koushikdutta.rommanager.license", + "com.dimonvideo.luckypatcher", + "com.chelpus.lackypatch", + "com.ramdroid.appquarantine", + "com.ramdroid.appquarantinepro", + "com.devadvance.rootcloak", + "com.devadvance.rootcloakplus", + "de.robv.android.xposed.installer", + "com.saurik.substrate", + "com.zachspong.temprootremovejb", + "com.amphoras.hidemyroot", + "com.amphoras.hidemyrootadfree", + "com.formyhm.hiderootPremium", + "com.formyhm.hideroot", + "me.phh.superuser", + "eu.chainfire.supersu.pro", + "com.kingouser.com", + "com.topjohnwu.magisk", +] + +DANGEROUS_PERMISSIONS_THRESHOLD = 10 + +DANGEROUS_PERMISSIONS = [ + "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.ACCESS_FINE_LOCATION", + "android.permission.AUTHENTICATE_ACCOUNTS", + "android.permission.CAMERA", + "android.permission.DISABLE_KEYGUARD", + "android.permission.PROCESS_OUTGOING_CALLS", + "android.permission.READ_CALENDAR", + "android.permission.READ_CALL_LOG", + "android.permission.READ_CONTACTS", + "android.permission.READ_PHONE_STATE", + "android.permission.READ_SMS", + "android.permission.RECEIVE_MMS", + "android.permission.RECEIVE_SMS", + "android.permission.RECEIVE_WAP_PUSH", + "android.permission.RECORD_AUDIO", + "android.permission.SEND_SMS", + "android.permission.SYSTEM_ALERT_WINDOW", + "android.permission.USE_CREDENTIALS", + "android.permission.USE_SIP", + "com.android.browser.permission.READ_HISTORY_BOOKMARKS", +] + +SECURITY_PACKAGES = [ + "com.policydm", + "com.samsung.android.app.omcagent", + "com.samsung.android.securitylogagent", + "com.sec.android.soagent", +] + +SYSTEM_UPDATE_PACKAGES = [ + "com.android.updater", + "com.google.android.gms", + "com.huawei.android.hwouc", + "com.lge.lgdmsclient", + "com.motorola.ccc.ota", + "com.oneplus.opbackup", + "com.oppo.ota", + "com.transsion.systemupdate", + "com.wssyncmldm", +] diff --git a/tests/android/test_artifact_dumpsys_packages.py b/tests/android/test_artifact_dumpsys_packages.py new file mode 100644 index 0000000..6300f17 --- /dev/null +++ b/tests/android/test_artifact_dumpsys_packages.py @@ -0,0 +1,42 @@ +# 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 mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact +from mvt.common.indicators import Indicators + +from ..utils import get_artifact + + +class TestDumpsysPackagesArtifact: + def test_parsing(self): + dpa = DumpsysPackagesArtifact() + file = get_artifact("android_data/dumpsys_packages.txt") + with open(file) as f: + data = f.read() + + assert len(dpa.results) == 0 + dpa.parse(data) + assert len(dpa.results) == 2 + assert ( + dpa.results[0]["package_name"] + == "com.samsung.android.provider.filterprovider" + ) + assert dpa.results[0]["version_name"] == "5.0.07" + + def test_ioc_check(self, indicator_file): + dpa = DumpsysPackagesArtifact() + file = get_artifact("android_data/dumpsys_packages.txt") + with open(file) as f: + data = f.read() + dpa.parse(data) + + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + ind.ioc_collections[0]["app_ids"].append("com.sec.android.app.DataCreate") + dpa.indicators = ind + assert len(dpa.detected) == 0 + dpa.check_indicators() + assert len(dpa.detected) == 1 diff --git a/tests/ios_fs/test_filesystem.py b/tests/ios_fs/test_filesystem.py index 4e6cbb6..062713f 100644 --- a/tests/ios_fs/test_filesystem.py +++ b/tests/ios_fs/test_filesystem.py @@ -4,7 +4,6 @@ # 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.fs.filesystem import Filesystem