mirror of
https://github.com/mvt-project/mvt
synced 2025-10-21 22:42:15 +02:00
Compare commits
19 Commits
fix/safari
...
root_binar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b44944aecf | ||
|
|
514c400017 | ||
|
|
41d22a8fdc | ||
|
|
d90654e74a | ||
|
|
86a0772eb2 | ||
|
|
7d0be9db4f | ||
|
|
4e120b2640 | ||
|
|
dbe9e5db9b | ||
|
|
0b00398729 | ||
|
|
87034d2c7a | ||
|
|
595a2f6536 | ||
|
|
8ead44a31e | ||
|
|
5c19d02a73 | ||
|
|
14ebc9ee4e | ||
|
|
de53cc07f8 | ||
|
|
242052b8ec | ||
|
|
10915f250c | ||
|
|
c60cef4009 | ||
|
|
49108e67e2 |
@@ -1,5 +1,5 @@
|
||||
mkdocs==1.6.1
|
||||
mkdocs-autorefs==1.4.2
|
||||
mkdocs-material==9.6.14
|
||||
mkdocs-material==9.6.16
|
||||
mkdocs-material-extensions==1.3.1
|
||||
mkdocstrings==0.29.1
|
||||
mkdocstrings==0.30.0
|
||||
@@ -20,7 +20,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"click==8.2.1",
|
||||
"rich==14.0.0",
|
||||
"rich==14.1.0",
|
||||
"tld==0.13.1",
|
||||
"requests==2.32.4",
|
||||
"simplejson==3.20.1",
|
||||
@@ -29,12 +29,12 @@ dependencies = [
|
||||
"iOSbackup==0.9.925",
|
||||
"adb-shell[usb]==0.4.4",
|
||||
"libusb1==3.3.1",
|
||||
"cryptography==45.0.5",
|
||||
"cryptography==45.0.6",
|
||||
"PyYAML>=6.0.2",
|
||||
"pyahocorasick==2.2.0",
|
||||
"betterproto==1.2.5",
|
||||
"pydantic==2.11.7",
|
||||
"pydantic-settings==2.9.1",
|
||||
"pydantic-settings==2.10.1",
|
||||
"NSKeyedUnArchiver==1.5.2",
|
||||
"python-dateutil==2.9.0.post0",
|
||||
]
|
||||
|
||||
@@ -51,11 +51,6 @@ ANDROID_DANGEROUS_SETTINGS = [
|
||||
"key": "send_action_app_error",
|
||||
"safe_value": "1",
|
||||
},
|
||||
{
|
||||
"description": "enabled installation of non Google Play apps",
|
||||
"key": "install_non_market_apps",
|
||||
"safe_value": "0",
|
||||
},
|
||||
{
|
||||
"description": "enabled accessibility services",
|
||||
"key": "accessibility_enabled",
|
||||
|
||||
@@ -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
|
||||
|
||||
ANDROIDQF_MODULES = [
|
||||
DumpsysActivities,
|
||||
@@ -37,4 +38,5 @@ ANDROIDQF_MODULES = [
|
||||
SMS,
|
||||
DumpsysPackages,
|
||||
Files,
|
||||
RootBinaries,
|
||||
]
|
||||
|
||||
121
src/mvt/android/modules/androidqf/root_binaries.py
Normal file
121
src/mvt/android/modules/androidqf/root_binaries.py
Normal file
@@ -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))
|
||||
@@ -1131,5 +1131,9 @@
|
||||
{
|
||||
"version": "18.5",
|
||||
"build": "22F76"
|
||||
},
|
||||
{
|
||||
"version": "18.6",
|
||||
"build": "22G86"
|
||||
}
|
||||
]
|
||||
@@ -116,13 +116,16 @@ class TCC(IOSExtraction):
|
||||
)
|
||||
db_version = "v2"
|
||||
except sqlite3.OperationalError:
|
||||
cur.execute(
|
||||
"""SELECT
|
||||
service, client, client_type, allowed,
|
||||
prompt_count
|
||||
FROM access;"""
|
||||
)
|
||||
db_version = "v1"
|
||||
try:
|
||||
cur.execute(
|
||||
"""SELECT
|
||||
service, client, client_type, allowed,
|
||||
prompt_count
|
||||
FROM access;"""
|
||||
)
|
||||
db_version = "v1"
|
||||
except sqlite3.OperationalError as e:
|
||||
self.log.error(f"Error parsing TCC database: {e}")
|
||||
|
||||
for row in cur:
|
||||
service = row[0]
|
||||
|
||||
116
tests/android_androidqf/test_root_binaries.py
Normal file
116
tests/android_androidqf/test_root_binaries.py
Normal file
@@ -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
|
||||
6
tests/artifacts/androidqf/root_binaries.json
Normal file
6
tests/artifacts/androidqf/root_binaries.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
"/system/bin/su",
|
||||
"/system/xbin/busybox",
|
||||
"/data/local/tmp/magisk",
|
||||
"/system/bin/magiskhide"
|
||||
]
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user