mirror of
https://github.com/mvt-project/mvt.git
synced 2026-05-15 13:38:18 +02:00
Add initial parser for ADB state in Dumpsys (#547)
* Add initial parser for ADB dumpsys * Add ADBState tests and support for AndroidQF and check-adb * Handle case where ADB is not available in device dumpsys
This commit is contained in:
committed by
GitHub
parent
a03f4e55ff
commit
665806db98
@@ -2,22 +2,30 @@
|
||||
# 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/
|
||||
from typing import AnyStr
|
||||
|
||||
from mvt.common.artifact import Artifact
|
||||
|
||||
|
||||
class AndroidArtifact(Artifact):
|
||||
@staticmethod
|
||||
def extract_dumpsys_section(dumpsys: str, separator: str) -> str:
|
||||
def extract_dumpsys_section(
|
||||
dumpsys: AnyStr, separator: AnyStr, binary=False
|
||||
) -> AnyStr:
|
||||
"""
|
||||
Extract a section from a full dumpsys file.
|
||||
|
||||
:param dumpsys: content of the full dumpsys file (string)
|
||||
:param separator: content of the first line separator (string)
|
||||
:return: section extracted (string)
|
||||
:param dumpsys: content of the full dumpsys file (AnyStr)
|
||||
:param separator: content of the first line separator (AnyStr)
|
||||
:param binary: whether the dumpsys should be pared as binary or not (bool)
|
||||
:return: section extracted (string or bytes)
|
||||
"""
|
||||
lines = []
|
||||
in_section = False
|
||||
delimiter = "------------------------------------------------------------------------------"
|
||||
if binary:
|
||||
delimiter = delimiter.encode("utf-8")
|
||||
|
||||
for line in dumpsys.splitlines():
|
||||
if line.strip() == separator:
|
||||
in_section = True
|
||||
@@ -26,11 +34,9 @@ class AndroidArtifact(Artifact):
|
||||
if not in_section:
|
||||
continue
|
||||
|
||||
if line.strip().startswith(
|
||||
"------------------------------------------------------------------------------"
|
||||
):
|
||||
if line.strip().startswith(delimiter):
|
||||
break
|
||||
|
||||
lines.append(line)
|
||||
|
||||
return "\n".join(lines)
|
||||
return b"\n".join(lines) if binary else "\n".join(lines)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# 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 base64
|
||||
import hashlib
|
||||
|
||||
from .artifact import AndroidArtifact
|
||||
|
||||
|
||||
class DumpsysADBArtifact(AndroidArtifact):
|
||||
multiline_fields = ["user_keys"]
|
||||
|
||||
def indented_dump_parser(self, dump_data):
|
||||
"""
|
||||
Parse the indented dumpsys output, generated by DualDumpOutputStream in Android.
|
||||
"""
|
||||
res = {}
|
||||
stack = [res]
|
||||
cur_indent = 0
|
||||
in_multiline = False
|
||||
for line in dump_data.strip(b"\n").split(b"\n"):
|
||||
# Track the level of indentation
|
||||
indent = len(line) - len(line.lstrip())
|
||||
if indent < cur_indent:
|
||||
# If the current line is less indented than the previous one, back out
|
||||
stack.pop()
|
||||
cur_indent = indent
|
||||
else:
|
||||
cur_indent = indent
|
||||
|
||||
# Split key and value by '='
|
||||
vals = line.lstrip().split(b"=", 1)
|
||||
key = vals[0].decode("utf-8")
|
||||
current_dict = stack[-1]
|
||||
|
||||
# Annoyingly, some values are multiline and don't have a key on each line
|
||||
if in_multiline:
|
||||
if key == "":
|
||||
# If the line is empty, it's the terminator for the multiline value
|
||||
in_multiline = False
|
||||
stack.pop()
|
||||
else:
|
||||
current_dict.append(line.lstrip())
|
||||
continue
|
||||
|
||||
if key == "}":
|
||||
stack.pop()
|
||||
continue
|
||||
|
||||
if vals[1] == b"{":
|
||||
# If the value is a new dictionary, add it to the stack
|
||||
current_dict[key] = {}
|
||||
stack.append(current_dict[key])
|
||||
|
||||
# Handle continue multiline values
|
||||
elif key in self.multiline_fields:
|
||||
current_dict[key] = []
|
||||
current_dict[key].append(vals[1])
|
||||
|
||||
in_multiline = True
|
||||
stack.append(current_dict[key])
|
||||
else:
|
||||
# If the value something else, store it in the current dictionary
|
||||
current_dict[key] = vals[1]
|
||||
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def calculate_key_info(user_key: bytes) -> str:
|
||||
key_base64, user = user_key.split(b" ", 1)
|
||||
key_raw = base64.b64decode(key_base64)
|
||||
key_fingerprint = hashlib.md5(key_raw).hexdigest().upper()
|
||||
key_fingerprint_colon = ":".join(
|
||||
[key_fingerprint[i : i + 2] for i in range(0, len(key_fingerprint), 2)]
|
||||
)
|
||||
return {
|
||||
"user": user.decode("utf-8"),
|
||||
"fingerprint": key_fingerprint_colon,
|
||||
"key": key_base64,
|
||||
}
|
||||
|
||||
def check_indicators(self) -> None:
|
||||
if not self.results:
|
||||
return
|
||||
|
||||
for entry in self.results:
|
||||
for user_key in entry.get("user_keys", []):
|
||||
self.log.debug(
|
||||
f"Found trusted ADB key for user '{user_key['user']}' with fingerprint "
|
||||
f"'{user_key['fingerprint']}'"
|
||||
)
|
||||
|
||||
def parse(self, content: bytes) -> None:
|
||||
"""
|
||||
Parse the Dumpsys ADB section
|
||||
Adds results to self.results (List[Dict[str, str]])
|
||||
|
||||
:param content: content of the ADB section (string)
|
||||
"""
|
||||
if not content or b"Can't find service: adb" in content:
|
||||
self.log.error(
|
||||
"Could not load ADB data from dumpsys. "
|
||||
"It may not be supported on this device."
|
||||
)
|
||||
return
|
||||
|
||||
# TODO: Parse AdbDebuggingManager line in output.
|
||||
start_of_json = content.find(b"\n{") + 2
|
||||
end_of_json = content.rfind(b"}\n") - 2
|
||||
json_content = content[start_of_json:end_of_json].rstrip()
|
||||
|
||||
parsed = self.indented_dump_parser(json_content)
|
||||
if parsed.get("debugging_manager") is None:
|
||||
self.log.error("Unable to find expected ADB entries in dumpsys output") # noqa
|
||||
return
|
||||
else:
|
||||
parsed = parsed["debugging_manager"]
|
||||
|
||||
# Calculate key fingerprints for better readability
|
||||
key_info = []
|
||||
for user_key in parsed.get("user_keys"):
|
||||
user_info = self.calculate_key_info(user_key)
|
||||
key_info.append(user_info)
|
||||
|
||||
parsed["user_keys"] = key_info
|
||||
self.results = [parsed]
|
||||
@@ -10,6 +10,7 @@ from .dumpsys_appops import DumpsysAppOps
|
||||
from .dumpsys_battery_daily import DumpsysBatteryDaily
|
||||
from .dumpsys_battery_history import DumpsysBatteryHistory
|
||||
from .dumpsys_dbinfo import DumpsysDBInfo
|
||||
from .dumpsys_adbstate import DumpsysADBState
|
||||
from .dumpsys_full import DumpsysFull
|
||||
from .dumpsys_receivers import DumpsysReceivers
|
||||
from .files import Files
|
||||
@@ -37,6 +38,7 @@ ADB_MODULES = [
|
||||
DumpsysActivities,
|
||||
DumpsysAccessibility,
|
||||
DumpsysDBInfo,
|
||||
DumpsysADBState,
|
||||
DumpsysFull,
|
||||
DumpsysAppOps,
|
||||
Packages,
|
||||
|
||||
@@ -147,14 +147,14 @@ class AndroidExtraction(MVTModule):
|
||||
self._adb_disconnect()
|
||||
self._adb_connect()
|
||||
|
||||
def _adb_command(self, command: str) -> str:
|
||||
def _adb_command(self, command: str, decode: bool = True) -> str:
|
||||
"""Execute an adb shell command.
|
||||
|
||||
:param command: Shell command to execute
|
||||
:returns: Output of command
|
||||
|
||||
"""
|
||||
return self.device.shell(command, read_timeout_s=200.0)
|
||||
return self.device.shell(command, read_timeout_s=200.0, decode=decode)
|
||||
|
||||
def _adb_check_if_root(self) -> bool:
|
||||
"""Check if we have a `su` binary on the Android device.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# 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
|
||||
|
||||
from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact
|
||||
|
||||
from .base import AndroidExtraction
|
||||
|
||||
|
||||
class DumpsysADBState(DumpsysADBArtifact, AndroidExtraction):
|
||||
"""This module extracts ADB keystore state."""
|
||||
|
||||
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 run(self) -> None:
|
||||
self._adb_connect()
|
||||
output = self._adb_command("dumpsys adb", decode=False)
|
||||
self._adb_disconnect()
|
||||
|
||||
self.parse(output)
|
||||
if self.results:
|
||||
self.log.info(
|
||||
"Identified a total of %d trusted ADB keys",
|
||||
len(self.results[0].get("user_keys", [])),
|
||||
)
|
||||
@@ -11,6 +11,7 @@ from .dumpsys_battery_history import DumpsysBatteryHistory
|
||||
from .dumpsys_dbinfo import DumpsysDBInfo
|
||||
from .dumpsys_packages import DumpsysPackages
|
||||
from .dumpsys_receivers import DumpsysReceivers
|
||||
from .dumpsys_adb import DumpsysADBState
|
||||
from .getprop import Getprop
|
||||
from .packages import Packages
|
||||
from .processes import Processes
|
||||
@@ -26,6 +27,7 @@ ANDROIDQF_MODULES = [
|
||||
DumpsysDBInfo,
|
||||
DumpsysBatteryDaily,
|
||||
DumpsysBatteryHistory,
|
||||
DumpsysADBState,
|
||||
Packages,
|
||||
Processes,
|
||||
Getprop,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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
|
||||
|
||||
from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact
|
||||
|
||||
from .base import AndroidQFModule
|
||||
|
||||
|
||||
class DumpsysADBState(DumpsysADBArtifact, AndroidQFModule):
|
||||
"""This module extracts ADB keystore state."""
|
||||
|
||||
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 run(self) -> None:
|
||||
dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt")
|
||||
if not dumpsys_file:
|
||||
return
|
||||
|
||||
full_dumpsys = self._get_file_content(dumpsys_file[0])
|
||||
content = self.extract_dumpsys_section(
|
||||
full_dumpsys,
|
||||
b"DUMP OF SERVICE adb:",
|
||||
binary=True,
|
||||
)
|
||||
self.parse(content)
|
||||
if self.results:
|
||||
self.log.info(
|
||||
"Identified a total of %d trusted ADB keys",
|
||||
len(self.results[0].get("user_keys", [])),
|
||||
)
|
||||
@@ -12,6 +12,7 @@ from .dbinfo import DBInfo
|
||||
from .getprop import Getprop
|
||||
from .packages import Packages
|
||||
from .receivers import Receivers
|
||||
from .adb_state import DumpsysADBState
|
||||
|
||||
BUGREPORT_MODULES = [
|
||||
Accessibility,
|
||||
@@ -23,4 +24,5 @@ BUGREPORT_MODULES = [
|
||||
Getprop,
|
||||
Packages,
|
||||
Receivers,
|
||||
DumpsysADBState,
|
||||
]
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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
|
||||
|
||||
from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact
|
||||
|
||||
from .base import BugReportModule
|
||||
|
||||
|
||||
class DumpsysADBState(DumpsysADBArtifact, BugReportModule):
|
||||
"""This module extracts ADB key info."""
|
||||
|
||||
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 run(self) -> None:
|
||||
full_dumpsys = self._get_dumpstate_file()
|
||||
if not full_dumpsys:
|
||||
self.log.error(
|
||||
"Unable to find dumpstate file. "
|
||||
"Did you provide a valid bug report archive?"
|
||||
)
|
||||
return
|
||||
|
||||
content = self.extract_dumpsys_section(
|
||||
full_dumpsys,
|
||||
b"DUMP OF SERVICE adb:",
|
||||
binary=True,
|
||||
)
|
||||
self.parse(content)
|
||||
if self.results:
|
||||
self.log.info(
|
||||
"Identified a total of %d trusted ADB keys",
|
||||
len(self.results[0].get("user_keys", [])),
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
# 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/
|
||||
|
||||
from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact
|
||||
|
||||
from ..utils import get_artifact
|
||||
|
||||
|
||||
class TestDumpsysADBArtifact:
|
||||
def test_parsing(self):
|
||||
da_adb = DumpsysADBArtifact()
|
||||
file = get_artifact("android_data/dumpsys_adb.txt")
|
||||
with open(file, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
assert len(da_adb.results) == 0
|
||||
da_adb.parse(data)
|
||||
|
||||
assert len(da_adb.results) == 1
|
||||
adb_data = da_adb.results[0]
|
||||
assert "user_keys" in adb_data
|
||||
assert len(adb_data["user_keys"]) == 1
|
||||
|
||||
# Check key and fingerprint parsed successfully.
|
||||
user_key = adb_data["user_keys"][0]
|
||||
assert (
|
||||
user_key["fingerprint"] == "F0:A1:3D:8C:B3:F4:7B:09:9F:EE:8B:D8:38:2E:BD:C6"
|
||||
)
|
||||
assert user_key["user"] == "user@linux"
|
||||
@@ -0,0 +1,27 @@
|
||||
# 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/
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from mvt.android.modules.androidqf.dumpsys_adb import DumpsysADBState
|
||||
from mvt.common.module import run_module
|
||||
|
||||
from ..utils import get_android_androidqf, list_files
|
||||
|
||||
|
||||
class TestDumpsysADBModule:
|
||||
def test_parsing(self):
|
||||
data_path = get_android_androidqf()
|
||||
m = DumpsysADBState(target_path=data_path)
|
||||
files = list_files(data_path)
|
||||
parent_path = Path(data_path).absolute().parent.as_posix()
|
||||
m.from_folder(parent_path, files)
|
||||
run_module(m)
|
||||
assert len(m.results) == 1
|
||||
assert len(m.detected) == 0
|
||||
|
||||
adb_statedump = m.results[0]
|
||||
assert "user_keys" in adb_statedump
|
||||
assert len(adb_statedump["user_keys"]) == 1
|
||||
Binary file not shown.
@@ -249,7 +249,19 @@ Current AppOps Service state:
|
||||
READ_EXTERNAL_STORAGE (allow):
|
||||
WRITE_EXTERNAL_STORAGE (allow):
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
DUMP OF SERVICE adb:
|
||||
ADB MANAGER STATE (dumpsys adb):
|
||||
{
|
||||
debugging_manager={
|
||||
connected_to_adb=true
|
||||
last_key_received=F0:A1:3D:8C:B3:F4:7B:09:9F:EE:8B:D8:38:2E:BD:C6
|
||||
user_keys=QAAAAPUBsVijHeceeK3bETuudn5I+S+ndOZDsxQDvkjKqSZrM+35YWU+U1d7mayQoh5fghnSIChrg0UxNOjcCxMY5oabt5lWIY4hJ+i1viqcg0UcJFAvW4/9j2wEpYKlIbyBlg5WGqp/wkOXeVn6fLKHETBOZzWG+CMh382OOmiKi5+4b2jLd2WLmKd8fBDfyaONVDhTrpkbwt3ArRqQSRQRr9ufJCCwkLOnKWmRYoyuN+AA7DYAGn+9TuId6edoiu/uAc+f3e8t8t7Rav9ha6ZzwcUuU9k+HwqECnVHdTkwrQvdtxnguiOKuyN+RrjGB+ZUO6acDeGRrB22rvAj2QiT/ldN0wbXiaw22HET99G1id4NiNJwjTKylH3nu3UxvyevUt2s2QbmH4j5CTWKghKstcCSUn72eu7xarfyx49r++FU8TojNzMEZe3H6Z4C/qfU2nQ1DBaBqq9TEgj2eLDSzB8ob9TbvE481sSebS3SiFaS+6pj/wBoA2R+JSWxfdJi/T3jyhs4VcXTFBrHYGop4TrWUqw+FxPSMC0dXXVUcMvpSni0hxLgA8l5GJT0Vu8DjyXOXJpgQ9n4ldfHAM8yXx7NTVklZilAdAbwTryMuSlENcVTo1IVURs3+p3lvOm7kUSEEn4WD39mguRv8Q5Y6R5hLfHKO94oH6Hvbge059/4WVUoGgEAAQA= user@linux
|
||||
|
||||
keystore=ABX | ||||