Skip to content

Commit

Permalink
Add packages module for androidqf (#506)
Browse files Browse the repository at this point in the history
* Add Packages module for androidqf

* Update test
  • Loading branch information
roaree authored Jun 24, 2024
1 parent 9e19abb commit caeeec2
Show file tree
Hide file tree
Showing 7 changed files with 462 additions and 1 deletion.
2 changes: 2 additions & 0 deletions mvt/android/modules/androidqf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .dumpsys_packages import DumpsysPackages
from .dumpsys_receivers import DumpsysReceivers
from .getprop import Getprop
from .packages import Packages
from .processes import Processes
from .settings import Settings
from .sms import SMS
Expand All @@ -24,6 +25,7 @@
DumpsysDBInfo,
DumpsysBatteryDaily,
DumpsysBatteryHistory,
Packages,
Processes,
Getprop,
Settings,
Expand Down
97 changes: 97 additions & 0 deletions mvt/android/modules/androidqf/packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 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
import json

from mvt.android.utils import (
ROOT_PACKAGES,
BROWSER_INSTALLERS,
PLAY_STORE_INSTALLERS,
THIRD_PARTY_STORE_INSTALLERS,
)

from .base import AndroidQFModule


class Packages(AndroidQFModule):
"""This module examines the installed packages in packages.json"""

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 check_indicators(self) -> None:
for result in self.results:
if result["name"] in ROOT_PACKAGES:
self.log.warning(
"Found an installed package related to "
'rooting/jailbreaking: "%s"',
result["name"],
)
self.detected.append(result)
continue

# Detections for apps installed via unusual methods
if result["installer"] in THIRD_PARTY_STORE_INSTALLERS:
self.log.warning(
'Found a package installed via a third party store (installer="%s"): "%s"',
result["installer"],
result["name"],
)
elif result["installer"] in BROWSER_INSTALLERS:
self.log.warning(
'Found a package installed via a browser (installer="%s"): "%s"',
result["installer"],
result["name"],
)
elif result["installer"] == "null" and result["system"] is False:
self.log.warning(
'Found a non-system package installed via adb or another method: "%s"',
result["name"],
)
elif result["installer"] in PLAY_STORE_INSTALLERS:
pass

if not self.indicators:
continue

ioc = self.indicators.check_app_id(result.get("name"))
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)
continue

for package_file in result.get("files", []):
ioc = self.indicators.check_file_hash(package_file["sha256"])
if ioc:
result["matched_indicator"] = ioc
self.detected.append(result)

def run(self) -> None:
packages = self._get_files_by_pattern("*/packages.json")
if not packages:
self.log.error(
"packages.json file not found in this androidqf bundle. Possibly malformed?"
)
return

self.results = json.loads(self._get_file_content(packages[0]))
self.log.info("Found %d packages in packages.json", len(self.results))
12 changes: 12 additions & 0 deletions mvt/android/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,15 @@ def warn_android_patch_level(patch_level: str, log) -> bool:
"com.transsion.systemupdate",
"com.wssyncmldm",
]

# Apps installed from the Play store have this installer
PLAY_STORE_INSTALLERS = ["com.android.vending"]

# Installer id for apps from common 3rd party stores
THIRD_PARTY_STORE_INSTALLERS = ["com.aurora.store", "org.fdroid.fdroid"]

# Packages installed via a browser have these installers
BROWSER_INSTALLERS = [
"com.google.android.packageinstaller",
"com.android.packageinstaller",
]
87 changes: 87 additions & 0 deletions tests/android_androidqf/test_packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 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
import pytest
from pathlib import Path

from mvt.android.modules.androidqf.packages import Packages
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 = Packages(target_path=parent_data_path, log=logging)
m.from_folder(parent_data_path, file_list)
return m


class TestAndroidqfPackages:
def test_packages_list(self, module):
run_module(module)

# There should just be 7 packages listed, no detections
assert len(module.results) == 7
assert len(module.timeline) == 0
assert len(module.detected) == 0

def test_non_appstore_warnings(self, caplog, module):
run_module(module)

# Not a super test to be searching logs for this but heuristic detections not yet formalised
assert (
'Found a non-system package installed via adb or another method: "com.whatsapp"'
in caplog.text
)
assert (
'Found a package installed via a browser (installer="com.google.android.packageinstaller"): '
'"app.revanced.manager.flutter"' in caplog.text
)
assert (
'Found a package installed via a third party store (installer="org.fdroid.fdroid"): "org.nuclearfog.apollo"'
in caplog.text
)

def test_packages_ioc_package_names(self, module, indicators_factory):
module.indicators = indicators_factory(app_ids=["com.malware.blah"])

run_module(module)

assert len(module.detected) == 1
assert module.detected[0]["name"] == "com.malware.blah"
assert module.detected[0]["matched_indicator"]["value"] == "com.malware.blah"

def test_packages_ioc_sha256(self, module, indicators_factory):
module.indicators = indicators_factory(
files_sha256=[
"31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa"
]
)

run_module(module)

assert len(module.detected) == 1
assert module.detected[0]["name"] == "com.malware.muahaha"
assert (
module.detected[0]["matched_indicator"]["value"]
== "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa"
)
Loading

0 comments on commit caeeec2

Please sign in to comment.