From b795ea3129a211dc1d344d7d24bc8fe2014f075b Mon Sep 17 00:00:00 2001 From: besendorf Date: Thu, 23 Oct 2025 15:12:01 +0200 Subject: [PATCH] Add root_binaries androidqf module (#676) * Add root_binaries androidqf module * Fix AndroidQF file count test * fix ruff --------- Co-authored-by: User --- src/mvt/android/modules/androidqf/__init__.py | 2 + .../modules/androidqf/root_binaries.py | 121 ++++++++++++++++++ tests/android_androidqf/test_root_binaries.py | 116 +++++++++++++++++ tests/artifacts/androidqf/root_binaries.json | 6 + tests/common/test_utils.py | 2 +- 5 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 src/mvt/android/modules/androidqf/root_binaries.py create mode 100644 tests/android_androidqf/test_root_binaries.py create mode 100644 tests/artifacts/androidqf/root_binaries.json diff --git a/src/mvt/android/modules/androidqf/__init__.py b/src/mvt/android/modules/androidqf/__init__.py index dd68457..bcb1e32 100644 --- a/src/mvt/android/modules/androidqf/__init__.py +++ b/src/mvt/android/modules/androidqf/__init__.py @@ -19,6 +19,7 @@ from .processes import Processes from .settings import Settings from .sms import SMS from .files import Files +from .root_binaries import RootBinaries from .mounts import Mounts ANDROIDQF_MODULES = [ @@ -38,5 +39,6 @@ ANDROIDQF_MODULES = [ SMS, DumpsysPackages, Files, + RootBinaries, Mounts, ] diff --git a/src/mvt/android/modules/androidqf/root_binaries.py b/src/mvt/android/modules/androidqf/root_binaries.py new file mode 100644 index 0000000..c5df729 --- /dev/null +++ b/src/mvt/android/modules/androidqf/root_binaries.py @@ -0,0 +1,121 @@ +# 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 json +import logging +from typing import Optional + +from .base import AndroidQFModule + + +class RootBinaries(AndroidQFModule): + """This module analyzes root_binaries.json for root binaries found by androidqf.""" + + 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 serialize(self, record: dict) -> dict: + return { + "timestamp": record.get("timestamp"), + "module": self.__class__.__name__, + "event": "root_binary_found", + "data": f"Root binary found: {record['path']} (binary: {record['binary_name']})", + } + + def check_indicators(self) -> None: + """Check for indicators of device rooting.""" + if not self.results: + return + + # All found root binaries are considered indicators of rooting + for result in self.results: + self.log.warning( + 'Found root binary "%s" at path "%s"', + result["binary_name"], + result["path"], + ) + self.detected.append(result) + + if self.detected: + self.log.warning( + "Device shows signs of rooting with %d root binaries found", + len(self.detected), + ) + + def run(self) -> None: + """Run the root binaries analysis.""" + root_binaries_files = self._get_files_by_pattern("*/root_binaries.json") + + if not root_binaries_files: + self.log.info("No root_binaries.json file found") + return + + rawdata = self._get_file_content(root_binaries_files[0]).decode( + "utf-8", errors="ignore" + ) + + try: + root_binary_paths = json.loads(rawdata) + except json.JSONDecodeError as e: + self.log.error("Failed to parse root_binaries.json: %s", e) + return + + if not isinstance(root_binary_paths, list): + self.log.error("Expected root_binaries.json to contain a list of paths") + return + + # Known root binary names that might be found and their descriptions + # This maps the binary name to a human-readable description + known_root_binaries = { + "su": "SuperUser binary", + "busybox": "BusyBox utilities", + "supersu": "SuperSU root management", + "Superuser.apk": "Superuser app", + "KingoUser.apk": "KingRoot app", + "SuperSu.apk": "SuperSU app", + "magisk": "Magisk root framework", + "magiskhide": "Magisk hide utility", + "magiskinit": "Magisk init binary", + "magiskpolicy": "Magisk policy binary", + } + + for path in root_binary_paths: + if not path or not isinstance(path, str): + continue + + # Extract binary name from path + binary_name = path.split("/")[-1].lower() + + # Check if this matches a known root binary by exact name match + description = "Unknown root binary" + for known_binary in known_root_binaries: + if binary_name == known_binary.lower(): + description = known_root_binaries[known_binary] + break + + result = { + "path": path.strip(), + "binary_name": binary_name, + "description": description, + } + + self.results.append(result) + + self.log.info("Found %d root binaries", len(self.results)) diff --git a/tests/android_androidqf/test_root_binaries.py b/tests/android_androidqf/test_root_binaries.py new file mode 100644 index 0000000..a59ecf5 --- /dev/null +++ b/tests/android_androidqf/test_root_binaries.py @@ -0,0 +1,116 @@ +# 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 pathlib import Path + +import pytest + +from mvt.android.modules.androidqf.root_binaries import RootBinaries +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 = RootBinaries(target_path=parent_data_path, log=logging) + m.from_folder(parent_data_path, file_list) + return m + + +class TestAndroidqfRootBinaries: + def test_root_binaries_detection(self, module): + run_module(module) + + # Should find 4 root binaries from the test file + assert len(module.results) == 4 + assert len(module.detected) == 4 + + # Check that all results are detected as indicators + binary_paths = [result["path"] for result in module.results] + assert "/system/bin/su" in binary_paths + assert "/system/xbin/busybox" in binary_paths + assert "/data/local/tmp/magisk" in binary_paths + assert "/system/bin/magiskhide" in binary_paths + + def test_root_binaries_descriptions(self, module): + run_module(module) + + # Check that binary descriptions are correctly identified + su_result = next((r for r in module.results if "su" in r["binary_name"]), None) + assert su_result is not None + assert "SuperUser binary" in su_result["description"] + + busybox_result = next( + (r for r in module.results if "busybox" in r["binary_name"]), None + ) + assert busybox_result is not None + assert "BusyBox utilities" in busybox_result["description"] + + magisk_result = next( + (r for r in module.results if r["binary_name"] == "magisk"), None + ) + assert magisk_result is not None + assert "Magisk root framework" in magisk_result["description"] + + magiskhide_result = next( + (r for r in module.results if "magiskhide" in r["binary_name"]), None + ) + assert magiskhide_result is not None + assert "Magisk hide utility" in magiskhide_result["description"] + + def test_root_binaries_warnings(self, caplog, module): + run_module(module) + + # Check that warnings are logged for each root binary found + assert 'Found root binary "su" at path "/system/bin/su"' in caplog.text + assert ( + 'Found root binary "busybox" at path "/system/xbin/busybox"' in caplog.text + ) + assert ( + 'Found root binary "magisk" at path "/data/local/tmp/magisk"' in caplog.text + ) + assert ( + 'Found root binary "magiskhide" at path "/system/bin/magiskhide"' + in caplog.text + ) + assert "Device shows signs of rooting with 4 root binaries found" in caplog.text + + def test_serialize_method(self, module): + run_module(module) + + # Test that serialize method works correctly + if module.results: + serialized = module.serialize(module.results[0]) + assert serialized["module"] == "RootBinaries" + assert serialized["event"] == "root_binary_found" + assert "Root binary found:" in serialized["data"] + + def test_no_root_binaries_file(self, parent_data_path): + # Test behavior when no root_binaries.json file is present + empty_file_list = [] + m = RootBinaries(target_path=parent_data_path, log=logging) + m.from_folder(parent_data_path, empty_file_list) + + run_module(m) + + assert len(m.results) == 0 + assert len(m.detected) == 0 diff --git a/tests/artifacts/androidqf/root_binaries.json b/tests/artifacts/androidqf/root_binaries.json new file mode 100644 index 0000000..37a3ccc --- /dev/null +++ b/tests/artifacts/androidqf/root_binaries.json @@ -0,0 +1,6 @@ +[ + "/system/bin/su", + "/system/xbin/busybox", + "/data/local/tmp/magisk", + "/system/bin/magiskhide" +] diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py index d1058e5..4dbe5c0 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) == 7 + assert len(hashes) == 8 # 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")