mirror of
https://github.com/mvt-project/mvt.git
synced 2024-06-26 14:28:54 +00:00
Merge branch 'main' into feature/androidqf-zip
This commit is contained in:
commit
8262ce4f84
0
mvt/android/artifacts/__init__.py
Normal file
0
mvt/android/artifacts/__init__.py
Normal file
9
mvt/android/artifacts/artifact.py
Normal file
9
mvt/android/artifacts/artifact.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
from mvt.common.artifact import Artifact
|
||||||
|
|
||||||
|
|
||||||
|
class AndroidArtifact(Artifact):
|
||||||
|
pass
|
59
mvt/android/artifacts/getprop.py
Normal file
59
mvt/android/artifacts/getprop.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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 Dict, List
|
||||||
|
|
||||||
|
from mvt.android.utils import warn_android_patch_level
|
||||||
|
|
||||||
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
INTERESTING_PROPERTIES = [
|
||||||
|
"gsm.sim.operator.alpha",
|
||||||
|
"gsm.sim.operator.iso-country",
|
||||||
|
"persist.sys.timezone",
|
||||||
|
"ro.boot.serialno",
|
||||||
|
"ro.build.version.sdk",
|
||||||
|
"ro.build.version.security_patch",
|
||||||
|
"ro.product.cpu.abi",
|
||||||
|
"ro.product.locale",
|
||||||
|
"ro.product.vendor.manufacturer",
|
||||||
|
"ro.product.vendor.model",
|
||||||
|
"ro.product.vendor.name",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GetProp(AndroidArtifact):
|
||||||
|
def parse(self, entry: str) -> None:
|
||||||
|
self.results: List[Dict[str, str]] = []
|
||||||
|
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
|
||||||
|
|
||||||
|
for line in entry.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
matches = re.findall(rxp, line)
|
||||||
|
if not matches or len(matches[0]) != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry = {"name": matches[0][0], "value": matches[0][1]}
|
||||||
|
self.results.append(entry)
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
for entry in self.results:
|
||||||
|
if entry["name"] in INTERESTING_PROPERTIES:
|
||||||
|
self.log.info("%s: %s", entry["name"], entry["value"])
|
||||||
|
|
||||||
|
if entry["name"] == "ro.build.version.security_patch":
|
||||||
|
warn_android_patch_level(entry["value"], self.log)
|
||||||
|
|
||||||
|
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)
|
69
mvt/android/artifacts/processes.py
Normal file
69
mvt/android/artifacts/processes.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
|
||||||
|
class Processes(AndroidArtifact):
|
||||||
|
def parse(self, entry: str) -> None:
|
||||||
|
for line in entry.split("\n")[1:]:
|
||||||
|
proc = line.split()
|
||||||
|
|
||||||
|
# Skip empty lines
|
||||||
|
if len(proc) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sometimes WCHAN is empty.
|
||||||
|
if len(proc) == 8:
|
||||||
|
proc = proc[:5] + [""] + proc[5:]
|
||||||
|
|
||||||
|
# Sometimes there is the security label.
|
||||||
|
if proc[0].startswith("u:r"):
|
||||||
|
label = proc[0]
|
||||||
|
proc = proc[1:]
|
||||||
|
else:
|
||||||
|
label = ""
|
||||||
|
|
||||||
|
# Sometimes there is no WCHAN.
|
||||||
|
if len(proc) < 9:
|
||||||
|
proc = proc[:5] + [""] + proc[5:]
|
||||||
|
|
||||||
|
self.results.append(
|
||||||
|
{
|
||||||
|
"user": proc[0],
|
||||||
|
"pid": int(proc[1]),
|
||||||
|
"ppid": int(proc[2]),
|
||||||
|
"virtual_memory_size": int(proc[3]),
|
||||||
|
"resident_set_size": int(proc[4]),
|
||||||
|
"wchan": proc[5],
|
||||||
|
"aprocress": proc[6],
|
||||||
|
"stat": proc[7],
|
||||||
|
"proc_name": proc[8].strip("[]"),
|
||||||
|
"label": label,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
if not self.indicators:
|
||||||
|
return
|
||||||
|
|
||||||
|
for result in self.results:
|
||||||
|
proc_name = result.get("proc_name", "")
|
||||||
|
if not proc_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skipping this process because of false positives.
|
||||||
|
if result["proc_name"] == "gatekeeperd":
|
||||||
|
continue
|
||||||
|
|
||||||
|
ioc = self.indicators.check_app_id(proc_name)
|
||||||
|
if ioc:
|
||||||
|
result["matched_indicator"] = ioc
|
||||||
|
self.detected.append(result)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ioc = self.indicators.check_process(proc_name)
|
||||||
|
if ioc:
|
||||||
|
result["matched_indicator"] = ioc
|
||||||
|
self.detected.append(result)
|
71
mvt/android/artifacts/settings.py
Normal file
71
mvt/android/artifacts/settings.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
from .artifact import AndroidArtifact
|
||||||
|
|
||||||
|
ANDROID_DANGEROUS_SETTINGS = [
|
||||||
|
{
|
||||||
|
"description": "disabled Google Play Services apps verification",
|
||||||
|
"key": "verifier_verify_adb_installs",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled Google Play Protect",
|
||||||
|
"key": "package_verifier_enable",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled Google Play Protect",
|
||||||
|
"key": "package_verifier_user_consent",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled Google Play Protect",
|
||||||
|
"key": "upload_apk_enable",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled confirmation of adb apps installation",
|
||||||
|
"key": "adb_install_need_confirm",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled sharing of security reports",
|
||||||
|
"key": "send_security_reports",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled sharing of crash logs with manufacturer",
|
||||||
|
"key": "samsung_errorlog_agree",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "disabled applications errors reports",
|
||||||
|
"key": "send_action_app_error",
|
||||||
|
"safe_value": "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "enabled installation of non Google Play apps",
|
||||||
|
"key": "install_non_market_apps",
|
||||||
|
"safe_value": "0",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(AndroidArtifact):
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
for namespace, settings in self.results.items():
|
||||||
|
for key, value in settings.items():
|
||||||
|
for danger in ANDROID_DANGEROUS_SETTINGS:
|
||||||
|
# Check if one of the dangerous settings is using an unsafe
|
||||||
|
# value (different than the one specified).
|
||||||
|
if danger["key"] == key and danger["safe_value"] != value:
|
||||||
|
self.log.warning(
|
||||||
|
'Found suspicious "%s" setting "%s = %s" (%s)',
|
||||||
|
namespace,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
danger["description"],
|
||||||
|
)
|
||||||
|
break
|
|
@ -4,15 +4,14 @@
|
||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.android.parsers import parse_getprop
|
from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
from .base import AndroidExtraction
|
||||||
|
|
||||||
|
|
||||||
class Getprop(AndroidExtraction):
|
class Getprop(GetPropArtifact, AndroidExtraction):
|
||||||
"""This module extracts device properties from getprop command."""
|
"""This module extracts device properties from getprop command."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -35,33 +34,10 @@ class Getprop(AndroidExtraction):
|
||||||
|
|
||||||
self.results = {} if not results else results
|
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:
|
def run(self) -> None:
|
||||||
self._adb_connect()
|
self._adb_connect()
|
||||||
output = self._adb_command("getprop")
|
output = self._adb_command("getprop")
|
||||||
self._adb_disconnect()
|
self._adb_disconnect()
|
||||||
|
|
||||||
self.results = parse_getprop(output)
|
self.parse(output)
|
||||||
|
|
||||||
# Alert if phone is outdated.
|
|
||||||
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)",
|
|
||||||
entry["value"],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.log.info("Extracted %d Android system properties", len(self.results))
|
self.log.info("Extracted %d Android system properties", len(self.results))
|
||||||
|
|
|
@ -6,10 +6,12 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from mvt.android.artifacts.processes import Processes as ProcessesArtifact
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
from .base import AndroidExtraction
|
||||||
|
|
||||||
|
|
||||||
class Processes(AndroidExtraction):
|
class Processes(ProcessesArtifact, AndroidExtraction):
|
||||||
"""This module extracts details on running processes."""
|
"""This module extracts details on running processes."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -30,61 +32,11 @@ class Processes(AndroidExtraction):
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_indicators(self) -> None:
|
|
||||||
if not self.indicators:
|
|
||||||
return
|
|
||||||
|
|
||||||
for result in self.results:
|
|
||||||
proc_name = result.get("proc_name", "")
|
|
||||||
if not proc_name:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skipping this process because of false positives.
|
|
||||||
if result["proc_name"] == "gatekeeperd":
|
|
||||||
continue
|
|
||||||
|
|
||||||
ioc = self.indicators.check_app_id(proc_name)
|
|
||||||
if ioc:
|
|
||||||
result["matched_indicator"] = ioc
|
|
||||||
self.detected.append(result)
|
|
||||||
continue
|
|
||||||
|
|
||||||
ioc = self.indicators.check_process(proc_name)
|
|
||||||
if ioc:
|
|
||||||
result["matched_indicator"] = ioc
|
|
||||||
self.detected.append(result)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
self._adb_connect()
|
self._adb_connect()
|
||||||
|
|
||||||
output = self._adb_command("ps -A")
|
output = self._adb_command("ps -A")
|
||||||
|
self.parse(output)
|
||||||
for line in output.splitlines()[1:]:
|
|
||||||
line = line.strip()
|
|
||||||
if line == "":
|
|
||||||
continue
|
|
||||||
|
|
||||||
fields = line.split()
|
|
||||||
proc = {
|
|
||||||
"user": fields[0],
|
|
||||||
"pid": fields[1],
|
|
||||||
"parent_pid": fields[2],
|
|
||||||
"vsize": fields[3],
|
|
||||||
"rss": fields[4],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Sometimes WCHAN is empty, so we need to re-align output fields.
|
|
||||||
if len(fields) == 8:
|
|
||||||
proc["wchan"] = ""
|
|
||||||
proc["pc"] = fields[5]
|
|
||||||
proc["name"] = fields[7]
|
|
||||||
elif len(fields) == 9:
|
|
||||||
proc["wchan"] = fields[5]
|
|
||||||
proc["pc"] = fields[6]
|
|
||||||
proc["name"] = fields[8]
|
|
||||||
|
|
||||||
self.results.append(proc)
|
|
||||||
|
|
||||||
self._adb_disconnect()
|
self._adb_disconnect()
|
||||||
|
|
||||||
self.log.info("Extracted records on a total of %d processes", len(self.results))
|
self.log.info("Extracted records on a total of %d processes", len(self.results))
|
||||||
|
|
|
@ -6,58 +6,12 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from mvt.android.artifacts.settings import Settings as SettingsArtifact
|
||||||
|
|
||||||
from .base import AndroidExtraction
|
from .base import AndroidExtraction
|
||||||
|
|
||||||
ANDROID_DANGEROUS_SETTINGS = [
|
|
||||||
{
|
|
||||||
"description": "disabled Google Play Services apps verification",
|
|
||||||
"key": "verifier_verify_adb_installs",
|
|
||||||
"safe_value": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "disabled Google Play Protect",
|
|
||||||
"key": "package_verifier_enable",
|
|
||||||
"safe_value": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "disabled Google Play Protect",
|
|
||||||
"key": "package_verifier_user_consent",
|
|
||||||
"safe_value": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "disabled Google Play Protect",
|
|
||||||
"key": "upload_apk_enable",
|
|
||||||
"safe_value": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "disabled confirmation of adb apps installation",
|
|
||||||
"key": "adb_install_need_confirm",
|
|
||||||
"safe_value": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "disabled sharing of security reports",
|
|
||||||
"key": "send_security_reports",
|
|
||||||
"safe_value": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "disabled sharing of crash logs with manufacturer",
|
|
||||||
"key": "samsung_errorlog_agree",
|
|
||||||
"safe_value": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "disabled applications errors reports",
|
|
||||||
"key": "send_action_app_error",
|
|
||||||
"safe_value": "1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "enabled installation of non Google Play apps",
|
|
||||||
"key": "install_non_market_apps",
|
|
||||||
"safe_value": "0",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
class Settings(SettingsArtifact, AndroidExtraction):
|
||||||
class Settings(AndroidExtraction):
|
|
||||||
"""This module extracts Android system settings."""
|
"""This module extracts Android system settings."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -80,21 +34,6 @@ class Settings(AndroidExtraction):
|
||||||
|
|
||||||
self.results = {} if not results else results
|
self.results = {} if not results else results
|
||||||
|
|
||||||
def check_indicators(self) -> None:
|
|
||||||
for _, settings in self.results.items():
|
|
||||||
for key, value in settings.items():
|
|
||||||
for danger in ANDROID_DANGEROUS_SETTINGS:
|
|
||||||
# Check if one of the dangerous settings is using an unsafe
|
|
||||||
# value (different than the one specified).
|
|
||||||
if danger["key"] == key and danger["safe_value"] != value:
|
|
||||||
self.log.warning(
|
|
||||||
'Found suspicious setting "%s = %s" (%s)',
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
danger["description"],
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
self._adb_connect()
|
self._adb_connect()
|
||||||
|
|
||||||
|
|
|
@ -4,29 +4,14 @@
|
||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.android.parsers.getprop import parse_getprop
|
from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
|
||||||
|
|
||||||
from .base import AndroidQFModule
|
from .base import AndroidQFModule
|
||||||
|
|
||||||
INTERESTING_PROPERTIES = [
|
|
||||||
"gsm.sim.operator.alpha",
|
|
||||||
"gsm.sim.operator.iso-country",
|
|
||||||
"persist.sys.timezone",
|
|
||||||
"ro.boot.serialno",
|
|
||||||
"ro.build.version.sdk",
|
|
||||||
"ro.build.version.security_patch",
|
|
||||||
"ro.product.cpu.abi",
|
|
||||||
"ro.product.locale",
|
|
||||||
"ro.product.vendor.manufacturer",
|
|
||||||
"ro.product.vendor.model",
|
|
||||||
"ro.product.vendor.name",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
class Getprop(GetPropArtifact, AndroidQFModule):
|
||||||
class Getprop(AndroidQFModule):
|
|
||||||
"""This module extracts data from get properties."""
|
"""This module extracts data from get properties."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -48,16 +33,6 @@ class Getprop(AndroidQFModule):
|
||||||
)
|
)
|
||||||
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:
|
def run(self) -> None:
|
||||||
getprop_files = self._get_files_by_pattern("*/getprop.txt")
|
getprop_files = self._get_files_by_pattern("*/getprop.txt")
|
||||||
if not getprop_files:
|
if not getprop_files:
|
||||||
|
@ -66,18 +41,5 @@ class Getprop(AndroidQFModule):
|
||||||
|
|
||||||
data = self._get_file_content(getprop_files[0]).decode("utf-8")
|
data = self._get_file_content(getprop_files[0]).decode("utf-8")
|
||||||
|
|
||||||
self.results = parse_getprop(data)
|
self.parse(data)
|
||||||
for entry in self.results:
|
|
||||||
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)",
|
|
||||||
entry["value"],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.log.info("Extracted a total of %d properties", len(self.results))
|
self.log.info("Extracted a total of %d properties", len(self.results))
|
||||||
|
|
|
@ -6,10 +6,12 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from mvt.android.artifacts.processes import Processes as ProcessesArtifact
|
||||||
|
|
||||||
from .base import AndroidQFModule
|
from .base import AndroidQFModule
|
||||||
|
|
||||||
|
|
||||||
class Processes(AndroidQFModule):
|
class Processes(ProcessesArtifact, AndroidQFModule):
|
||||||
"""This module analyse running processes"""
|
"""This module analyse running processes"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -30,68 +32,10 @@ class Processes(AndroidQFModule):
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_indicators(self) -> None:
|
|
||||||
if not self.indicators:
|
|
||||||
return
|
|
||||||
|
|
||||||
for result in self.results:
|
|
||||||
proc_name = result.get("proc_name", "")
|
|
||||||
if not proc_name:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skipping this process because of false positives.
|
|
||||||
if result["proc_name"] == "gatekeeperd":
|
|
||||||
continue
|
|
||||||
|
|
||||||
ioc = self.indicators.check_app_id(proc_name)
|
|
||||||
if ioc:
|
|
||||||
result["matched_indicator"] = ioc
|
|
||||||
self.detected.append(result)
|
|
||||||
continue
|
|
||||||
|
|
||||||
ioc = self.indicators.check_process(proc_name)
|
|
||||||
if ioc:
|
|
||||||
result["matched_indicator"] = ioc
|
|
||||||
self.detected.append(result)
|
|
||||||
|
|
||||||
def _parse_ps(self, data):
|
|
||||||
for line in data.split("\n")[1:]:
|
|
||||||
proc = line.split()
|
|
||||||
|
|
||||||
# Sometimes WCHAN is empty.
|
|
||||||
if len(proc) == 8:
|
|
||||||
proc = proc[:5] + [""] + proc[5:]
|
|
||||||
|
|
||||||
# Sometimes there is the security label.
|
|
||||||
if proc[0].startswith("u:r"):
|
|
||||||
label = proc[0]
|
|
||||||
proc = proc[1:]
|
|
||||||
else:
|
|
||||||
label = ""
|
|
||||||
|
|
||||||
# Sometimes there is no WCHAN.
|
|
||||||
if len(proc) < 9:
|
|
||||||
proc = proc[:5] + [""] + proc[5:]
|
|
||||||
|
|
||||||
self.results.append(
|
|
||||||
{
|
|
||||||
"user": proc[0],
|
|
||||||
"pid": int(proc[1]),
|
|
||||||
"ppid": int(proc[2]),
|
|
||||||
"virtual_memory_size": int(proc[3]),
|
|
||||||
"resident_set_size": int(proc[4]),
|
|
||||||
"wchan": proc[5],
|
|
||||||
"aprocress": proc[6],
|
|
||||||
"stat": proc[7],
|
|
||||||
"proc_name": proc[8].strip("[]"),
|
|
||||||
"label": label,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
ps_files = self._get_files_by_pattern("*/ps.txt")
|
ps_files = self._get_files_by_pattern("*/ps.txt")
|
||||||
if not ps_files:
|
if not ps_files:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._parse_ps(self._get_file_content(ps_files[0]).decode("utf-8"))
|
self.parse(self._get_file_content(ps_files[0]).decode("utf-8"))
|
||||||
self.log.info("Identified %d running processes", len(self.results))
|
self.log.info("Identified %d running processes", len(self.results))
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.android.modules.adb.settings import ANDROID_DANGEROUS_SETTINGS
|
from mvt.android.artifacts.settings import Settings as SettingsArtifact
|
||||||
|
|
||||||
from .base import AndroidQFModule
|
from .base import AndroidQFModule
|
||||||
|
|
||||||
|
|
||||||
class Settings(AndroidQFModule):
|
class Settings(SettingsArtifact, AndroidQFModule):
|
||||||
"""This module analyse setting files"""
|
"""This module analyse setting files"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -51,16 +51,6 @@ class Settings(AndroidQFModule):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for danger in ANDROID_DANGEROUS_SETTINGS:
|
|
||||||
if danger["key"] == key and danger["safe_value"] != value:
|
|
||||||
self.log.warning(
|
|
||||||
'Found suspicious setting "%s = %s" (%s)',
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
danger["description"],
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"Identified %d settings", sum([len(val) for val in self.results.values()])
|
"Identified %d settings", sum([len(val) for val in self.results.values()])
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,15 +4,14 @@
|
||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from mvt.android.parsers import parse_getprop
|
from mvt.android.artifacts.getprop import GetProp as GetPropArtifact
|
||||||
|
|
||||||
from .base import BugReportModule
|
from .base import BugReportModule
|
||||||
|
|
||||||
|
|
||||||
class Getprop(BugReportModule):
|
class Getprop(GetPropArtifact, BugReportModule):
|
||||||
"""This module extracts device properties from getprop command."""
|
"""This module extracts device properties from getprop command."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -33,7 +32,7 @@ class Getprop(BugReportModule):
|
||||||
results=results,
|
results=results,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.results = {} if not results else results
|
self.results = [] if not results else results
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
content = self._get_dumpstate_file()
|
content = self._get_dumpstate_file()
|
||||||
|
@ -60,18 +59,5 @@ class Getprop(BugReportModule):
|
||||||
|
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
|
|
||||||
self.results = parse_getprop("\n".join(lines))
|
self.parse("\n".join(lines))
|
||||||
|
|
||||||
# Alert if phone is outdated.
|
|
||||||
for entry in self.results:
|
|
||||||
if entry["name"] == "ro.build.version.security_patch":
|
|
||||||
security_patch = entry["value"]
|
|
||||||
patch_date = datetime.strptime(security_patch, "%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,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.log.info("Extracted %d Android system properties", len(self.results))
|
self.log.info("Extracted %d Android system properties", len(self.results))
|
||||||
|
|
|
@ -12,4 +12,3 @@ from .dumpsys import (
|
||||||
parse_dumpsys_dbinfo,
|
parse_dumpsys_dbinfo,
|
||||||
parse_dumpsys_receiver_resolver_table,
|
parse_dumpsys_receiver_resolver_table,
|
||||||
)
|
)
|
||||||
from .getprop import parse_getprop
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Mobile Verification Toolkit (MVT)
|
|
||||||
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
|
||||||
# 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 Dict, List
|
|
||||||
|
|
||||||
|
|
||||||
def parse_getprop(output: str) -> List[Dict[str, str]]:
|
|
||||||
results = []
|
|
||||||
rxp = re.compile(r"\[(.+?)\]: \[(.+?)\]")
|
|
||||||
|
|
||||||
for line in output.splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line == "":
|
|
||||||
continue
|
|
||||||
|
|
||||||
matches = re.findall(rxp, line)
|
|
||||||
if not matches or len(matches[0]) != 2:
|
|
||||||
continue
|
|
||||||
|
|
||||||
entry = {"name": matches[0][0], "value": matches[0][1]}
|
|
||||||
results.append(entry)
|
|
||||||
|
|
||||||
return results
|
|
19
mvt/android/utils.py
Normal file
19
mvt/android/utils.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
def warn_android_patch_level(patch_level: str, log) -> bool:
|
||||||
|
"""Alert if Android patch level out-of-date"""
|
||||||
|
patch_date = datetime.strptime(patch_level, "%Y-%m-%d")
|
||||||
|
if (datetime.now() - patch_date) > timedelta(days=6 * 31):
|
||||||
|
log.warning(
|
||||||
|
"This phone has not received security updates "
|
||||||
|
"for more than six months (last update: %s)",
|
||||||
|
patch_level,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
28
mvt/common/artifact.py
Normal file
28
mvt/common/artifact.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
|
||||||
|
class Artifact:
|
||||||
|
"""
|
||||||
|
Main artifact class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.results = []
|
||||||
|
self.detected = []
|
||||||
|
self.indicators = None
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def parse(self, entry: str):
|
||||||
|
"""
|
||||||
|
Parse the artifact, adds the parsed information to self.results
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def check_indicators(self) -> None:
|
||||||
|
"""Check the results of this module against a provided list of
|
||||||
|
indicators coming from self.indicators
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
|
@ -871,6 +871,10 @@
|
||||||
"version": "15.7.7",
|
"version": "15.7.7",
|
||||||
"build": "19H357"
|
"build": "19H357"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"version": "15.7.8",
|
||||||
|
"build": "19H364"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"build": "20A362",
|
"build": "20A362",
|
||||||
"version": "16.0"
|
"version": "16.0"
|
||||||
|
@ -927,5 +931,9 @@
|
||||||
{
|
{
|
||||||
"version": "16.5.1",
|
"version": "16.5.1",
|
||||||
"build": "20F75"
|
"build": "20F75"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "16.6",
|
||||||
|
"build": "20G75"
|
||||||
}
|
}
|
||||||
]
|
]
|
41
tests/android/test_artifact_getprop.py
Normal file
41
tests/android/test_artifact_getprop.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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.getprop import GetProp
|
||||||
|
from mvt.common.indicators import Indicators
|
||||||
|
|
||||||
|
from ..utils import get_artifact
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetPropArtifact:
|
||||||
|
def test_parsing(self):
|
||||||
|
gp = GetProp()
|
||||||
|
file = get_artifact("android_data/getprop.txt")
|
||||||
|
with open(file) as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
assert len(gp.results) == 0
|
||||||
|
gp.parse(data)
|
||||||
|
assert len(gp.results) == 13
|
||||||
|
assert gp.results[0]["name"] == "af.fast_track_multiplier"
|
||||||
|
assert gp.results[0]["value"] == "1"
|
||||||
|
|
||||||
|
def test_ioc_check(self, indicator_file):
|
||||||
|
gp = GetProp()
|
||||||
|
file = get_artifact("android_data/getprop.txt")
|
||||||
|
with open(file) as f:
|
||||||
|
data = f.read()
|
||||||
|
gp.parse(data)
|
||||||
|
|
||||||
|
ind = Indicators(log=logging.getLogger())
|
||||||
|
ind.parse_stix2(indicator_file)
|
||||||
|
ind.ioc_collections[0]["android_property_names"].append(
|
||||||
|
"dalvik.vm.appimageformat"
|
||||||
|
)
|
||||||
|
gp.indicators = ind
|
||||||
|
assert len(gp.detected) == 0
|
||||||
|
gp.check_indicators()
|
||||||
|
assert len(gp.detected) == 1
|
38
tests/android/test_artifact_processes.py
Normal file
38
tests/android/test_artifact_processes.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2023 Claudio Guarnieri.
|
||||||
|
# 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.processes import Processes
|
||||||
|
from mvt.common.indicators import Indicators
|
||||||
|
|
||||||
|
from ..utils import get_artifact
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessesArtifact:
|
||||||
|
def test_parsing(self):
|
||||||
|
p = Processes()
|
||||||
|
file = get_artifact("android_data/ps.txt")
|
||||||
|
with open(file) as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
assert len(p.results) == 0
|
||||||
|
p.parse(data)
|
||||||
|
assert len(p.results) == 17
|
||||||
|
assert p.results[0]["proc_name"] == "init"
|
||||||
|
|
||||||
|
def test_ioc_check(self, indicator_file):
|
||||||
|
p = Processes()
|
||||||
|
file = get_artifact("android_data/ps.txt")
|
||||||
|
with open(file) as f:
|
||||||
|
data = f.read()
|
||||||
|
p.parse(data)
|
||||||
|
|
||||||
|
ind = Indicators(log=logging.getLogger())
|
||||||
|
ind.parse_stix2(indicator_file)
|
||||||
|
ind.ioc_collections[0]["processes"].append("lru-add-drain")
|
||||||
|
p.indicators = ind
|
||||||
|
assert len(p.detected) == 0
|
||||||
|
p.check_indicators()
|
||||||
|
assert len(p.detected) == 1
|
14
tests/artifacts/android_data/getprop.txt
Normal file
14
tests/artifacts/android_data/getprop.txt
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[af.fast_track_multiplier]: [1]
|
||||||
|
[audio.deep_buffer.media]: [true]
|
||||||
|
[audio.offload.min.duration.secs]: [30]
|
||||||
|
[audio.offload.video]: [true]
|
||||||
|
[av.debug.disable.pers.cache]: [1]
|
||||||
|
[dalvik.vm.appimageformat]: [lz4]
|
||||||
|
[dalvik.vm.dex2oat-Xms]: [64m]
|
||||||
|
[dalvik.vm.dex2oat-Xmx]: [512m]
|
||||||
|
|
||||||
|
[dalvik.vm.dex2oat-max-image-block-size]: [524288]
|
||||||
|
[dalvik.vm.dex2oat-minidebuginfo]: [true]
|
||||||
|
[dalvik.vm.dex2oat-resolve-startup-strings]: [true]
|
||||||
|
[dalvik.vm.dexopt.secondary]: [true]
|
||||||
|
[dalvik.vm.heapgrowthlimit]: [192m]
|
18
tests/artifacts/android_data/ps.txt
Normal file
18
tests/artifacts/android_data/ps.txt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
USER PID PPID VSZ RSS WCHAN ADDR S NAME
|
||||||
|
root 1 0 68696 2864 0 0 S init
|
||||||
|
root 2 0 0 0 0 0 S [kthreadd]
|
||||||
|
root 4 2 0 0 0 0 S [kworker/0:0H]
|
||||||
|
root 5 2 0 0 0 0 S [kworker/u16:0]
|
||||||
|
root 6 2 0 0 0 0 S [ksoftirqd/0]
|
||||||
|
root 7 2 0 0 0 0 S [rcu_preempt]
|
||||||
|
root 8 2 0 0 0 0 S [rcu_sched]
|
||||||
|
root 9 2 0 0 0 0 S [rcu_bh]
|
||||||
|
root 10 2 0 0 0 0 S [rcuop/0]
|
||||||
|
root 11 2 0 0 0 0 S [rcuos/0]
|
||||||
|
root 12 2 0 0 0 0 S [rcuob/0]
|
||||||
|
root 13 2 0 0 0 0 S [migration/0]
|
||||||
|
root 14 2 0 0 0 0 S [lru-add-drain]
|
||||||
|
root 15 2 0 0 0 0 S [cpuhp/0]
|
||||||
|
root 16 2 0 0 0 0 S [cpuhp/1]
|
||||||
|
root 17 2 0 0 0 0 S [migration/1]
|
||||||
|
root 18 2 0 0 0 0 S [ksoftirqd/1]
|
Loading…
Reference in New Issue
Block a user