Adds support for zip files in check-androidqf command

This commit is contained in:
tek 2023-07-24 00:57:01 +02:00
parent 15ce1b7e64
commit cfd0baaab0
28 changed files with 235 additions and 124 deletions

View File

@ -9,13 +9,13 @@ import click
from mvt.common.cmd_check_iocs import CmdCheckIOCS
from mvt.common.help import (
HELP_MSG_ANDROID_BACKUP_PASSWORD,
HELP_MSG_FAST,
HELP_MSG_HASHES,
HELP_MSG_IOC,
HELP_MSG_LIST_MODULES,
HELP_MSG_MODULE,
HELP_MSG_NONINTERACTIVE,
HELP_MSG_ANDROID_BACKUP_PASSWORD,
HELP_MSG_OUTPUT,
HELP_MSG_SERIAL,
HELP_MSG_VERBOSE,
@ -32,8 +32,8 @@ from .cmd_download_apks import DownloadAPKs
from .modules.adb import ADB_MODULES
from .modules.adb.packages import Packages
from .modules.backup import BACKUP_MODULES
from .modules.bugreport import BUGREPORT_MODULES
from .modules.backup.helpers import cli_load_android_backup_password
from .modules.bugreport import BUGREPORT_MODULES
init_logging()
log = logging.getLogger("mvt")

View File

@ -4,7 +4,10 @@
# https://license.mvt.re/1.1/
import logging
from typing import Optional
import os
import zipfile
from pathlib import Path
from typing import List, Optional
from mvt.common.command import Command
@ -37,3 +40,28 @@ class CmdAndroidCheckAndroidQF(Command):
self.name = "check-androidqf"
self.modules = ANDROIDQF_MODULES
self.format: Optional[str] = None
self.archive: Optional[zipfile.ZipFile] = None
self.files: List[str] = []
def init(self):
if os.path.isdir(self.target_path):
self.format = "dir"
parent_path = Path(self.target_path).absolute().parent.as_posix()
target_abs_path = os.path.abspath(self.target_path)
for root, subdirs, subfiles in os.walk(target_abs_path):
for fname in subfiles:
file_path = os.path.relpath(os.path.join(root, fname), parent_path)
self.files.append(file_path)
elif os.path.isfile(self.target_path):
self.format = "zip"
self.archive = zipfile.ZipFile(self.target_path)
self.files = self.archive.namelist()
def module_init(self, module):
if self.format == "zip":
module.from_zip_file(self.archive, self.files)
else:
parent_path = Path(self.target_path).absolute().parent.as_posix()
module.from_folder(parent_path, self.files)

View File

@ -12,13 +12,13 @@ from pathlib import Path
from typing import List, Optional
from mvt.android.modules.backup.base import BackupExtraction
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.android.parsers.backup import (
AndroidBackupParsingError,
InvalidBackupPassword,
parse_ab_header,
parse_backup_file,
)
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.common.command import Command
from .modules.backup import BACKUP_MODULES

View File

@ -24,12 +24,12 @@ from adb_shell.exceptions import (
)
from usb1 import USBErrorAccess, USBErrorBusy
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.android.parsers.backup import (
InvalidBackupPassword,
parse_ab_header,
parse_backup_file,
)
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.common.module import InsufficientPrivileges, MVTModule
ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey")

View File

@ -6,6 +6,7 @@
import fnmatch
import logging
import os
import zipfile
from typing import Any, Dict, List, Optional, Union
from mvt.common.module import MVTModule
@ -31,13 +32,28 @@ class AndroidQFModule(MVTModule):
log=log,
results=results,
)
self._path: str = target_path
self.files: List[str] = []
self.archive: Optional[zipfile.ZipFile] = None
self._path = target_path
self._files = []
def from_folder(self, parent_path: str, files: List[str]):
self.parent_path = parent_path
self.files = files
for root, dirs, files in os.walk(target_path):
for name in files:
self._files.append(os.path.join(root, name))
def from_zip_file(self, archive: zipfile.ZipFile, files: List[str]):
self.archive = archive
self.files = files
def _get_files_by_pattern(self, pattern):
return fnmatch.filter(self._files, pattern)
def _get_files_by_pattern(self, pattern: str):
return fnmatch.filter(self.files, pattern)
def _get_file_content(self, file_path):
if self.archive:
handle = self.archive.open(file_path)
else:
handle = open(os.path.join(self.parent_path, file_path), "rb")
data = handle.read()
handle.close()
return data

View File

@ -49,21 +49,21 @@ class DumpsysAccessibility(AndroidQFModule):
lines = []
in_accessibility = False
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.strip().startswith("DUMP OF SERVICE accessibility:"):
in_accessibility = True
continue
data = self._get_file_content(dumpsys_file[0])
for line in data.decode("utf-8").split("\n"):
if line.strip().startswith("DUMP OF SERVICE accessibility:"):
in_accessibility = True
continue
if not in_accessibility:
continue
if not in_accessibility:
continue
if line.strip().startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
if line.strip().startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
lines.append(line.rstrip())
self.results = parse_dumpsys_accessibility("\n".join(lines))

View File

@ -52,21 +52,21 @@ class DumpsysActivities(AndroidQFModule):
lines = []
in_package = False
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.strip() == "DUMP OF SERVICE package:":
in_package = True
continue
data = self._get_file_content(dumpsys_file[0])
for line in data.decode("utf-8").split("\n"):
if line.strip() == "DUMP OF SERVICE package:":
in_package = True
continue
if not in_package:
continue
if not in_package:
continue
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
lines.append(line.rstrip())
self.results = parse_dumpsys_activity_resolver_table("\n".join(lines))

View File

@ -76,19 +76,19 @@ class DumpsysAppops(AndroidQFModule):
lines = []
in_package = False
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.startswith("DUMP OF SERVICE appops:"):
in_package = True
continue
data = self._get_file_content(dumpsys_file[0])
for line in data.decode("utf-8").split("\n"):
if line.startswith("DUMP OF SERVICE appops:"):
in_package = True
continue
if in_package:
if line.startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
if in_package:
if line.startswith(
"-------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
lines.append(line.rstrip())
self.results = parse_dumpsys_appops("\n".join(lines))
self.log.info("Identified %d applications in AppOps Manager", len(self.results))

View File

@ -78,13 +78,12 @@ class DumpsysPackages(AndroidQFModule):
self.log.info("Dumpsys file not found")
return
with open(dumpsys_file[0]) as handle:
data = handle.read().split("\n")
data = self._get_file_content(dumpsys_file[0])
package = []
in_service = False
in_package_list = False
for line in data:
for line in data.decode("utf-8").split("\n"):
if line.strip().startswith("DUMP OF SERVICE package:"):
in_service = True
continue

View File

@ -86,21 +86,21 @@ class DumpsysReceivers(AndroidQFModule):
in_receivers = False
lines = []
with open(dumpsys_file[0]) as handle:
for line in handle:
if line.strip() == "DUMP OF SERVICE package:":
in_receivers = True
continue
data = self._get_file_content(dumpsys_file[0])
for line in data.decode("utf-8").split("\n"):
if line.strip() == "DUMP OF SERVICE package:":
in_receivers = True
continue
if not in_receivers:
continue
if not in_receivers:
continue
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
if line.strip().startswith(
"------------------------------------------------------------------------------"
): # pylint: disable=line-too-long
break
lines.append(line.rstrip())
lines.append(line.rstrip())
self.results = parse_dumpsys_receiver_resolver_table("\n".join(lines))

View File

@ -64,8 +64,7 @@ class Getprop(AndroidQFModule):
self.log.info("getprop.txt file not found")
return
with open(getprop_files[0]) as f:
data = f.read()
data = self._get_file_content(getprop_files[0]).decode("utf-8")
self.results = parse_getprop(data)
for entry in self.results:

View File

@ -93,7 +93,5 @@ class Processes(AndroidQFModule):
if not ps_files:
return
with open(ps_files[0]) as handle:
self._parse_ps(handle.read())
self._parse_ps(self._get_file_content(ps_files[0]).decode("utf-8"))
self.log.info("Identified %d running processes", len(self.results))

View File

@ -38,29 +38,28 @@ class Settings(AndroidQFModule):
namespace = setting_file[setting_file.rfind("_") + 1 : -4]
self.results[namespace] = {}
data = self._get_file_content(setting_file)
for line in data.decode("utf-8").split("\n"):
line = line.strip()
try:
key, value = line.split("=", 1)
except ValueError:
continue
with open(setting_file) as handle:
for line in handle:
line = line.strip()
try:
key, value = line.split("=", 1)
except ValueError:
continue
try:
self.results[namespace][key] = value
except IndexError:
continue
try:
self.results[namespace][key] = value
except IndexError:
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
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(
"Identified %d settings", sum([len(val) for val in self.results.values()])

View File

@ -6,6 +6,7 @@
import logging
from typing import Optional
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from mvt.android.parsers.backup import (
AndroidBackupParsingError,
InvalidBackupPassword,
@ -13,7 +14,6 @@ from mvt.android.parsers.backup import (
parse_backup_file,
parse_tar_for_sms,
)
from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password
from .base import AndroidQFModule
@ -96,8 +96,5 @@ class SMS(AndroidQFModule):
self.log.info("No backup data found")
return
with open(files[0], "rb") as handle:
data = handle.read()
self.parse_backup(data)
self.parse_backup(self._get_file_content(files[0]))
self.log.info("Identified %d SMS in backup data", len(self.results))

View File

@ -7,7 +7,6 @@ import os
from rich.prompt import Prompt
MVT_ANDROID_BACKUP_PASSWORD = "MVT_ANDROID_BACKUP_PASSWORD"

View File

@ -5,7 +5,7 @@
import logging
import sqlite3
from typing import Union, Optional
from typing import Optional, Union
from mvt.common.utils import convert_mactime_to_iso

View File

@ -3,16 +3,21 @@
# 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_accessibility import DumpsysAccessibility
from mvt.common.module import run_module
from ..utils import get_android_androidqf
from ..utils import get_android_androidqf, list_files
class TestDumpsysAccessibilityModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysAccessibility(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) == 4
assert len(m.detected) == 0

View File

@ -3,16 +3,21 @@
# 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_appops import DumpsysAppops
from mvt.common.module import run_module
from ..utils import get_android_androidqf
from ..utils import get_android_androidqf, list_files
class TestDumpsysAppOpsModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysAppops(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) == 12
assert len(m.timeline) == 16

View File

@ -4,18 +4,22 @@
# https://license.mvt.re/1.1/
import logging
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_packages import DumpsysPackages
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
from ..utils import get_android_androidqf
from ..utils import get_android_androidqf, list_files
class TestDumpsysPackagesModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysPackages(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) == 2
assert len(m.detected) == 0
@ -28,6 +32,9 @@ class TestDumpsysPackagesModule:
def test_detection_pkgname(self, indicator_file):
data_path = get_android_androidqf()
m = DumpsysPackages(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)
ind.ioc_collections[0]["app_ids"].append("com.sec.android.app.DataCreate")

View File

@ -3,16 +3,21 @@
# 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_receivers import DumpsysReceivers
from mvt.common.module import run_module
from ..utils import get_android_androidqf
from ..utils import get_android_androidqf, list_files
class TestDumpsysReceiversModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysReceivers(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) == 4
assert len(m.detected) == 0

View File

@ -4,20 +4,35 @@
# https://license.mvt.re/1.1/
import logging
import os
import zipfile
from pathlib import Path
from mvt.android.modules.androidqf.getprop import Getprop
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
from ..utils import get_artifact_folder
from ..utils import get_android_androidqf, get_artifact, list_files
class TestAndroidqfGetpropAnalysis:
def test_androidqf_getprop(self):
m = Getprop(
target_path=os.path.join(get_artifact_folder(), "androidqf"), log=logging
)
data_path = get_android_androidqf()
m = Getprop(target_path=data_path, log=logging)
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) == 10
assert m.results[0]["name"] == "dalvik.vm.appimageformat"
assert m.results[0]["value"] == "lz4"
assert len(m.timeline) == 0
assert len(m.detected) == 0
def test_getprop_parsing_zip(self):
fpath = get_artifact("androidqf.zip")
m = Getprop(target_path=fpath, log=logging)
archive = zipfile.ZipFile(fpath)
m.from_zip_file(archive, archive.namelist())
run_module(m)
assert len(m.results) == 10
assert m.results[0]["name"] == "dalvik.vm.appimageformat"
@ -26,9 +41,11 @@ class TestAndroidqfGetpropAnalysis:
assert len(m.detected) == 0
def test_androidqf_getprop_detection(self, indicator_file):
m = Getprop(
target_path=os.path.join(get_artifact_folder(), "androidqf"), log=logging
)
data_path = get_android_androidqf()
m = Getprop(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)
ind.ioc_collections[0]["android_property_names"].append("dalvik.vm.heapmaxfree")

View File

@ -4,19 +4,21 @@
# https://license.mvt.re/1.1/
import logging
import os
from pathlib import Path
from mvt.android.modules.androidqf.processes import Processes
from mvt.common.module import run_module
from ..utils import get_artifact_folder
from ..utils import get_android_androidqf, list_files
class TestAndroidqfProcessesAnalysis:
def test_androidqf_processes(self):
m = Processes(
target_path=os.path.join(get_artifact_folder(), "androidqf"), log=logging
)
data_path = get_android_androidqf()
m = Processes(target_path=data_path, log=logging)
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) == 15
assert len(m.timeline) == 0

View File

@ -3,16 +3,21 @@
# 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.settings import Settings
from mvt.common.module import run_module
from ..utils import get_android_androidqf
from ..utils import get_android_androidqf, list_files
class TestSettingsModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = Settings(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 "random" in m.results.keys()

View File

@ -5,65 +5,84 @@
import logging
import os
from pathlib import Path
from mvt.android.modules.androidqf.sms import SMS
from mvt.common.module import run_module
from ..utils import get_artifact_folder
from ..utils import get_android_androidqf, get_artifact_folder, list_files
TEST_BACKUP_PASSWORD = "123456"
class TestAndroidqfSMSAnalysis:
def test_androidqf_sms(self):
m = SMS(
target_path=os.path.join(get_artifact_folder(), "androidqf"), log=logging
)
data_path = get_android_androidqf()
m = SMS(target_path=data_path, log=logging)
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) == 2
assert len(m.timeline) == 0
assert len(m.detected) == 0
def test_androidqf_sms_encrypted_password_valid(self):
data_path = os.path.join(get_artifact_folder(), "androidqf_encrypted")
m = SMS(
target_path=os.path.join(get_artifact_folder(), "androidqf_encrypted"),
target_path=data_path,
log=logging,
module_options={"backup_password": TEST_BACKUP_PASSWORD},
)
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
def test_androidqf_sms_encrypted_password_prompt(self, mocker):
data_path = os.path.join(get_artifact_folder(), "androidqf_encrypted")
prompt_mock = mocker.patch(
"rich.prompt.Prompt.ask", return_value=TEST_BACKUP_PASSWORD
)
m = SMS(
target_path=os.path.join(get_artifact_folder(), "androidqf_encrypted"),
target_path=data_path,
log=logging,
module_options={},
)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert prompt_mock.call_count == 1
assert len(m.results) == 1
def test_androidqf_sms_encrypted_password_invalid(self, caplog):
data_path = os.path.join(get_artifact_folder(), "androidqf_encrypted")
with caplog.at_level(logging.CRITICAL):
m = SMS(
target_path=os.path.join(get_artifact_folder(), "androidqf_encrypted"),
target_path=data_path,
log=logging,
module_options={"backup_password": "invalid_password"},
)
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) == 0
assert "Invalid backup password" in caplog.text
def test_androidqf_sms_encrypted_no_interactive(self, caplog):
data_path = os.path.join(get_artifact_folder(), "androidqf_encrypted")
with caplog.at_level(logging.CRITICAL):
m = SMS(
target_path=os.path.join(get_artifact_folder(), "androidqf_encrypted"),
target_path=data_path,
log=logging,
module_options={"interactive": False},
)
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) == 0
assert (

Binary file not shown.

View File

@ -11,7 +11,6 @@ from mvt.android.cli import check_androidqf
from .utils import get_artifact_folder
TEST_BACKUP_PASSWORD = "123456"

View File

@ -3,8 +3,8 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import os
import logging
import os
from click.testing import CliRunner
@ -12,7 +12,6 @@ from mvt.android.cli import check_backup
from .utils import get_artifact_folder
TEST_BACKUP_PASSWORD = "123456"

View File

@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import os
from pathlib import Path
def get_artifact(fname):
@ -34,3 +35,15 @@ def get_android_androidqf():
def get_indicator_file():
print("PYTEST env", os.getenv("PYTEST_CURRENT_TEST"))
def list_files(path: str):
files = []
parent_path = Path(path).absolute().parent.as_posix()
target_abs_path = os.path.abspath(path)
for root, subdirs, subfiles in os.walk(target_abs_path):
for fname in subfiles:
file_path = os.path.relpath(os.path.join(root, fname), parent_path)
files.append(file_path)
return files