diff --git a/docs/source/quark_script.rst b/docs/source/quark_script.rst index 7eaeeab1c..3e74ba43e 100644 --- a/docs/source/quark_script.rst +++ b/docs/source/quark_script.rst @@ -339,6 +339,19 @@ applicationInstance.isDebuggable(none) - **params**: none - **return**: True/False +getProviders(samplePath) +========================== +- **Description**: Get provider elements from the manifest file of the target sample. +- **params**: + 1. samplePath: the file path of target sample +- **return**: python list containing provider elements + +providerInstance.isExported(none) +================================== +- **Description**: Check if the provider element set ``android:exported=true``. +- **params**: none +- **return**: True/False + Analyzing real case (InstaStealer) using Quark Script ------------------------------------------------------ diff --git a/quark/core/interface/baseapkinfo.py b/quark/core/interface/baseapkinfo.py index 745408928..410f04065 100644 --- a/quark/core/interface/baseapkinfo.py +++ b/quark/core/interface/baseapkinfo.py @@ -152,6 +152,20 @@ def receivers(self) -> List[XMLElement] | None: return root.findall("application/receiver") + @property + def providers(self) -> List[XMLElement] | None: + """Get provider elements from the manifest file. + + :return: python list containing provider elements + """ + if self.ret_type != "APK": + return None + + with AxmlReader(self._manifest) as axml: + root = axml.get_xml_tree() + + return root.findall("application/provider") + @property @abstractmethod def android_apis(self) -> Set[MethodObject]: diff --git a/quark/script/__init__.py b/quark/script/__init__.py index 58b4b0d4d..273878240 100644 --- a/quark/script/__init__.py +++ b/quark/script/__init__.py @@ -154,6 +154,28 @@ def isExported(self) -> bool: return str(exported).lower() == 'true' +class Provider: + def __init__(self, xml: XMLElement) -> None: + self.xml: XMLElement = xml + + def __str__(self) -> str: + return self._getAttribute("name") + + def _getAttribute(self, attributeName: str) -> Any: + realAttributeName = ( + f"{{http://schemas.android.com/apk/res/android}}{attributeName}" + ) + return self.xml.get(realAttributeName, None) + + def isExported(self) -> bool: + """Check if the provider element set ``android:exported=true``. + + :return: True/False + """ + exported = self._getAttribute("exported") + return exported + + class Method: def __init__( self, @@ -634,6 +656,18 @@ def getApplication(samplePath: PathLike) -> Application: return Application(apkinfo.application) +def getProviders(samplePath: PathLike) -> List[Provider]: + """Get provider elements from the manifest file of the target sample. + + :param samplePath: the file path of target sample + :return: python list containing provider elements + """ + quark = _getQuark(samplePath) + apkinfo = quark.apkinfo + + return [Provider(xml) for xml in apkinfo.providers] + + def findMethodInAPK( samplePath: PathLike, targetMethod: Union[List[str], Method] diff --git a/tests/conftest.py b/tests/conftest.py index 39d3f6a9b..5857935cb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,13 @@ "/raw/master/vulnerable-samples/pivaa.apk" ), "fileName": "pivaa.apk" + }, + { + "sourceUrl": ( + "https://github.com/quark-engine/apk-samples" + "/raw/master/vulnerable-samples/Vuldroid.apk" + ), + "fileName": "Vuldroid.apk" } ] @@ -74,3 +81,7 @@ def SAMPLE_PATH_Ahmyth(tmp_path_factory: pytest.TempPathFactory) -> str: @pytest.fixture(scope="session") def SAMPLE_PATH_pivaa(tmp_path_factory: pytest.TempPathFactory) -> str: return downloadSample(tmp_path_factory, SAMPLES[3]) + +@pytest.fixture(scope="session") +def SAMPLE_PATH_Vuldroid(tmp_path_factory: pytest.TempPathFactory) -> str: + return downloadSample(tmp_path_factory, SAMPLES[4]) \ No newline at end of file diff --git a/tests/core/test_apkinfo.py b/tests/core/test_apkinfo.py index 0cf571183..558c7854f 100644 --- a/tests/core/test_apkinfo.py +++ b/tests/core/test_apkinfo.py @@ -30,6 +30,25 @@ def dex_file(SAMPLE_PATH_13667): os.remove(APK_NAME) +@pytest.fixture(scope="session") +def dex_file_pivaa(tmp_path_factory, SAMPLE_PATH_pivaa): + APK_NAME = SAMPLE_PATH_pivaa + DEX_NAME = "classes.dex" + DEX_DIR = tmp_path_factory.mktemp("dex_pivaa") + DEX_PATH = str(os.path.join(DEX_DIR, "classes.dex")) + + with zipfile.ZipFile(APK_NAME, "r") as zip: + zip.extract(DEX_NAME, path=DEX_DIR) + + yield DEX_PATH + + if os.path.exists(DEX_PATH): + os.remove(DEX_PATH) + + if os.path.exists(DEX_PATH): + os.remove(DEX_PATH) + + def __generateTestIDs(testInput: Tuple[BaseApkinfo, Literal["DEX", "APK"]]): return f"{testInput[0].__name__} with {testInput[1]}" @@ -80,6 +99,32 @@ def apkinfo_with_R2Imp_only(request, SAMPLE_PATH_13667, dex_file): yield apkinfo +@pytest.fixture( + scope="function", + params=( + (AndroguardImp, "DEX"), + (AndroguardImp, "APK"), + (RizinImp, "DEX"), + (RizinImp, "APK"), + (R2Imp, "DEX"), + (R2Imp, "APK"), + (ShurikenImp, "DEX"), + (ShurikenImp, "APK"), + ), + ids=__generateTestIDs, +) +def apkinfoPivaa(request, SAMPLE_PATH_pivaa, dex_file_pivaa): + apkinfoClass, fileType = request.param + + fileToBeAnalyzed = SAMPLE_PATH_pivaa + if fileType == "DEX": + fileToBeAnalyzed = dex_file_pivaa + + apkinfo = apkinfoClass(fileToBeAnalyzed) + + yield apkinfo + + class TestApkinfo: def test_init_with_invalid_type(self): filepath = None @@ -208,6 +253,22 @@ def test_receivers(apkinfo): == "com.example.google.service.MyDeviceAdminReceiver" ) + @staticmethod + def test_providers(apkinfoPivaa): + match apkinfoPivaa.ret_type: + case "DEX": + assert apkinfoPivaa.providers is None + case "APK": + providers= apkinfoPivaa.providers + + assert len(providers) == 1 + assert ( + providers[0].get( + "{http://schemas.android.com/apk/res/android}name" + ) + == "com.htbridge.pivaa.handlers.VulnerableContentProvider" + ) + def test_android_apis(self, apkinfo): api = { MethodObject( diff --git a/tests/script/test_script.py b/tests/script/test_script.py index 0c3fb0571..bdd741d8b 100644 --- a/tests/script/test_script.py +++ b/tests/script/test_script.py @@ -15,6 +15,7 @@ getActivities, getReceivers, getApplication, + getProviders, runQuarkAnalysis, findMethodInAPK, findMethodImpls, @@ -132,6 +133,16 @@ def testIsExported(SAMPLE_PATH_13667): receiver = getReceivers(SAMPLE_PATH_13667)[0] assert receiver.isExported() is True +class TestProvider: + @staticmethod + def testIsNotExported(SAMPLE_PATH_Vuldroid): + provider = getProviders(SAMPLE_PATH_Vuldroid)[0] + assert provider.isExported() is False + + @staticmethod + def testIsExported(SAMPLE_PATH_pivaa): + provider = getProviders(SAMPLE_PATH_pivaa)[0] + assert provider.isExported() is True class TestMethod: @staticmethod