From a9a4cb5a386ad8abe9fac0f5b16bb3c533b038b4 Mon Sep 17 00:00:00 2001 From: zinwang Date: Wed, 20 Aug 2025 15:55:50 +0800 Subject: [PATCH 1/9] Add Quark Script APIs to detect CWE-359 --- quark/core/interface/baseapkinfo.py | 14 ++++++++++++ quark/script/__init__.py | 34 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) 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..b6280bb66 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 Providers: + 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, "") + + 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[Receiver]: + """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 [Providers(xml) for xml in apkinfo.providers] + + def findMethodInAPK( samplePath: PathLike, targetMethod: Union[List[str], Method] From 185eb2d4b8480eabb5a4a828b78764f64d8ff93a Mon Sep 17 00:00:00 2001 From: zinwang Date: Wed, 20 Aug 2025 16:31:13 +0800 Subject: [PATCH 2/9] Add tests --- tests/conftest.py | 11 ++++++++++ tests/core/test_apkinfo.py | 43 +++++++++++++++++++++++++++++++++++++ tests/script/test_script.py | 11 ++++++++++ 3 files changed, 65 insertions(+) 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..778224563 100644 --- a/tests/core/test_apkinfo.py +++ b/tests/core/test_apkinfo.py @@ -723,3 +723,46 @@ def test_get_wrapper_smali(self, apkinfo): for key, expected in expected_result.items(): assert result[key] == expected + + +@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): + apkinfoClass, fileType = request.param + + fileToBeAnalyzed = SAMPLE_PATH_pivaa + if fileType == "DEX": + fileToBeAnalyzed = dex_file + + apkinfo = apkinfoClass(fileToBeAnalyzed) + + yield apkinfo + +class AnotherTestApkinfo: + @staticmethod + def test_providers(apkinfoPivaa): + match apkinfo.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" + ) \ No newline at end of file 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 From 6c34cf11694861ab237ada196ef5741094328f43 Mon Sep 17 00:00:00 2001 From: zinwang Date: Wed, 20 Aug 2025 16:55:19 +0800 Subject: [PATCH 3/9] Update docs for quark script --- docs/source/quark_script.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 ------------------------------------------------------ From e0c37c2192f1d71958ce4592a9f96ad3a3d0ee4e Mon Sep 17 00:00:00 2001 From: zinwang Date: Wed, 20 Aug 2025 17:05:05 +0800 Subject: [PATCH 4/9] Adjust tests --- tests/core/test_apkinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/test_apkinfo.py b/tests/core/test_apkinfo.py index 778224563..86fe58c67 100644 --- a/tests/core/test_apkinfo.py +++ b/tests/core/test_apkinfo.py @@ -750,7 +750,7 @@ def apkinfoPivaa(request, SAMPLE_PATH_pivaa, dex_file): yield apkinfo -class AnotherTestApkinfo: +class TestAnotherApkinfo: @staticmethod def test_providers(apkinfoPivaa): match apkinfo.ret_type: From 20758c42966e8f9c047fad8ca3158a7863356509 Mon Sep 17 00:00:00 2001 From: zinwang Date: Wed, 20 Aug 2025 17:17:34 +0800 Subject: [PATCH 5/9] Adjust tests --- tests/core/test_apkinfo.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/core/test_apkinfo.py b/tests/core/test_apkinfo.py index 86fe58c67..db2bb3546 100644 --- a/tests/core/test_apkinfo.py +++ b/tests/core/test_apkinfo.py @@ -724,6 +724,21 @@ def test_get_wrapper_smali(self, apkinfo): for key, expected in expected_result.items(): assert result[key] == expected +@pytest.fixture(scope="session") +def dex_file_pivaa(SAMPLE_PATH_pivaa): + APK_NAME = SAMPLE_PATH_pivaa + DEX_NAME = "classes.dex" + + with zipfile.ZipFile(APK_NAME, "r") as zip: + zip.extract(DEX_NAME) + + yield DEX_NAME + + if os.path.exists(DEX_NAME): + os.remove(DEX_NAME) + + if os.path.exists(APK_NAME): + os.remove(APK_NAME) @pytest.fixture( scope="function", @@ -739,12 +754,12 @@ def test_get_wrapper_smali(self, apkinfo): ), ids=__generateTestIDs, ) -def apkinfoPivaa(request, SAMPLE_PATH_pivaa, dex_file): +def apkinfoPivaa(request, SAMPLE_PATH_pivaa, dex_file_pivaa): apkinfoClass, fileType = request.param fileToBeAnalyzed = SAMPLE_PATH_pivaa if fileType == "DEX": - fileToBeAnalyzed = dex_file + fileToBeAnalyzed = dex_file_pivaa apkinfo = apkinfoClass(fileToBeAnalyzed) @@ -753,7 +768,7 @@ def apkinfoPivaa(request, SAMPLE_PATH_pivaa, dex_file): class TestAnotherApkinfo: @staticmethod def test_providers(apkinfoPivaa): - match apkinfo.ret_type: + match apkinfoPivaa.ret_type: case "DEX": assert apkinfoPivaa.providers is None case "APK": From 0567eedafe83a7ff31aeda2c6c9abd178efe9537 Mon Sep 17 00:00:00 2001 From: zinwang Date: Wed, 20 Aug 2025 18:20:41 +0800 Subject: [PATCH 6/9] Fix small mistakes --- quark/script/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quark/script/__init__.py b/quark/script/__init__.py index b6280bb66..273878240 100644 --- a/quark/script/__init__.py +++ b/quark/script/__init__.py @@ -154,7 +154,7 @@ def isExported(self) -> bool: return str(exported).lower() == 'true' -class Providers: +class Provider: def __init__(self, xml: XMLElement) -> None: self.xml: XMLElement = xml @@ -165,7 +165,7 @@ def _getAttribute(self, attributeName: str) -> Any: realAttributeName = ( f"{{http://schemas.android.com/apk/res/android}}{attributeName}" ) - return self.xml.get(realAttributeName, "") + return self.xml.get(realAttributeName, None) def isExported(self) -> bool: """Check if the provider element set ``android:exported=true``. @@ -656,7 +656,7 @@ def getApplication(samplePath: PathLike) -> Application: return Application(apkinfo.application) -def getProviders(samplePath: PathLike) -> List[Receiver]: +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 @@ -665,7 +665,7 @@ def getProviders(samplePath: PathLike) -> List[Receiver]: quark = _getQuark(samplePath) apkinfo = quark.apkinfo - return [Providers(xml) for xml in apkinfo.providers] + return [Provider(xml) for xml in apkinfo.providers] def findMethodInAPK( From 7bd27aa5a3ba9bd1a0ec816e309c7de1c4f9fead Mon Sep 17 00:00:00 2001 From: zinwang Date: Thu, 21 Aug 2025 22:17:46 +0800 Subject: [PATCH 7/9] Adjust tests --- tests/core/test_apkinfo.py | 84 +++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/tests/core/test_apkinfo.py b/tests/core/test_apkinfo.py index db2bb3546..52cce4b8d 100644 --- a/tests/core/test_apkinfo.py +++ b/tests/core/test_apkinfo.py @@ -80,6 +80,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 +234,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( @@ -739,45 +781,3 @@ def dex_file_pivaa(SAMPLE_PATH_pivaa): if os.path.exists(APK_NAME): os.remove(APK_NAME) - -@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 TestAnotherApkinfo: - @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" - ) \ No newline at end of file From 40a8369b769f3238cbf85b3fc59934c4de8e4046 Mon Sep 17 00:00:00 2001 From: zinwang Date: Thu, 21 Aug 2025 22:30:51 +0800 Subject: [PATCH 8/9] Adjust tests --- tests/core/test_apkinfo.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/tests/core/test_apkinfo.py b/tests/core/test_apkinfo.py index 52cce4b8d..e7a03d4f7 100644 --- a/tests/core/test_apkinfo.py +++ b/tests/core/test_apkinfo.py @@ -30,6 +30,23 @@ def dex_file(SAMPLE_PATH_13667): os.remove(APK_NAME) +@pytest.fixture(scope="session") +def dex_file_pivaa(SAMPLE_PATH_pivaa): + APK_NAME = SAMPLE_PATH_pivaa + DEX_NAME = "classes.dex" + + with zipfile.ZipFile(APK_NAME, "r") as zip: + zip.extract(DEX_NAME) + + yield DEX_NAME + + if os.path.exists(DEX_NAME): + os.remove(DEX_NAME) + + if os.path.exists(APK_NAME): + os.remove(APK_NAME) + + def __generateTestIDs(testInput: Tuple[BaseApkinfo, Literal["DEX", "APK"]]): return f"{testInput[0].__name__} with {testInput[1]}" @@ -765,19 +782,3 @@ def test_get_wrapper_smali(self, apkinfo): for key, expected in expected_result.items(): assert result[key] == expected - -@pytest.fixture(scope="session") -def dex_file_pivaa(SAMPLE_PATH_pivaa): - APK_NAME = SAMPLE_PATH_pivaa - DEX_NAME = "classes.dex" - - with zipfile.ZipFile(APK_NAME, "r") as zip: - zip.extract(DEX_NAME) - - yield DEX_NAME - - if os.path.exists(DEX_NAME): - os.remove(DEX_NAME) - - if os.path.exists(APK_NAME): - os.remove(APK_NAME) From cb9bf9b8d72dfcc9e7dd54cdfd3353e074fb1099 Mon Sep 17 00:00:00 2001 From: zinwang Date: Fri, 22 Aug 2025 14:20:17 +0800 Subject: [PATCH 9/9] Fix unit test failure --- tests/core/test_apkinfo.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/core/test_apkinfo.py b/tests/core/test_apkinfo.py index e7a03d4f7..558c7854f 100644 --- a/tests/core/test_apkinfo.py +++ b/tests/core/test_apkinfo.py @@ -31,20 +31,22 @@ def dex_file(SAMPLE_PATH_13667): @pytest.fixture(scope="session") -def dex_file_pivaa(SAMPLE_PATH_pivaa): +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) + zip.extract(DEX_NAME, path=DEX_DIR) - yield DEX_NAME + yield DEX_PATH - if os.path.exists(DEX_NAME): - os.remove(DEX_NAME) + if os.path.exists(DEX_PATH): + os.remove(DEX_PATH) - if os.path.exists(APK_NAME): - os.remove(APK_NAME) + if os.path.exists(DEX_PATH): + os.remove(DEX_PATH) def __generateTestIDs(testInput: Tuple[BaseApkinfo, Literal["DEX", "APK"]]):