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:
Donncha Ó Cearbhaill
2024-10-18 15:31:25 +02:00
committed by GitHub
parent a03f4e55ff
commit 665806db98
14 changed files with 373 additions and 11 deletions
+14 -8
View File
@@ -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)
+128
View File
@@ -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]
+2
View File
@@ -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,
+2 -2
View File
@@ -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.
+12
View File
@@ -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=ABX2keyStoreoversion2adbKey/keyQAAAAPUBsVijHeceeK3bETuudn5I+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@linuxlastConnection`xY]33
}
}
--------- 0.001s was the duration of dumpsys adb, ending at: 2024-03-21 11:07:22
-------------------------------------------------------------------------------
DUMP OF SERVICE dbinfo:
Applications Database Info:
+3 -1
View File
@@ -71,9 +71,11 @@ class TestHashes:
== "f0e32fe8a7fd5ac0e2de19636d123c0072e979396986139ba2bc49ec385dc325"
)
assert hashes[1]["file_path"] == os.path.join(path, "dumpsys.txt")
# This needs to be updated when we add or edit files in AndroidQF folder
assert (
hashes[1]["sha256"]
== "cfae0e04ef139b5a2ae1e2b3d400ce67eb98e67ff66f56ba2a580fe41bc120d0"
== "1bd255f656a7f9d5647a730f0f0cc47053115576f11532d41bf28c16635b193d"
)