diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 33540bd6..96d548f1 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -51,7 +51,7 @@ jobs: - name: Test with pytest run: | python -m pip install black pytest sphinx sphinx-rtd-theme - python -m pip install coveralls codecov pytest-cov + python -m pip install coveralls pytest-cov pytest --cov=./ - name: Upload coverage to Codecov diff --git a/.github/workflows/smoke_test.yml b/.github/workflows/smoke_test.yml index 8b8794c2..b6c94818 100644 --- a/.github/workflows/smoke_test.yml +++ b/.github/workflows/smoke_test.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-10.15, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.8, 3.9] steps: @@ -47,10 +47,13 @@ jobs: - run: sudo apt-get -y install graphviz if: matrix.os == 'ubuntu-latest' - run: brew install graphviz - if: matrix.os == 'macOS-10.15' + if: matrix.os == 'macOS-latest' - run: choco install graphviz if: matrix.os == 'windows-latest' + - run: python -m pip install cython numpy versioneer pybind11 matplotlib lxml + if: matrix.os == 'macOS-latest' + - name: Install Quark-Engine run: | python setup.py build diff --git a/.travis.yml b/.travis.yml index f2f96cc9..9d8df957 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ before-install: # Installation install: - pipenv install -e . --skip-lock - - pipenv install coveralls codecov pytest-cov --skip-lock + - pipenv install coveralls pytest-cov --skip-lock # Run the unit test script: @@ -24,4 +24,3 @@ script: after_success: - coveralls - - codecov diff --git a/README.md b/README.md index 433e7b3f..ede6b3bb 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ * [CWE-020](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-20-in-android-application-diva-apk) Improper Input Validation * [CWE-022](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-22-in-android-application-ovaa-apk-and-insecurebankv2-apk) Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') * [CWE-023](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-23-in-android-application-ovaa-apk-and-insecurebankv2-apk) Relative Path Traversal +* [CWE-088](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-88-in-android-application-vuldroid-apk) Improper Neutralization of Argument Delimiters in a Command * [CWE-089](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-89-in-android-application-androgoat-apk) Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') * [CWE-094](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-94-in-android-application-ovaa-apk) Improper Control of Generation of Code ('Code Injection') * [CWE-295](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-295-in-android-application-insecureshop-apk) Improper Certificate Validation @@ -70,6 +71,7 @@ * [CWE-780](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-780-in-android-application-mstg-android-java-apk) Use of RSA Algorithm without OAEP * [CWE-798](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-798-in-android-application-ovaa-apk) Use of Hard-coded Credentials * [CWE-921](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-921-in-android-application-ovaa-apk) Storage of Sensitive Data in a Mechanism without Access Control +* [CWE-925](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-925-in-android-application-insecurebankv2-androgoat) Improper Verification of Intent by Broadcast Receiver * [CWE-926](https://quark-engine.readthedocs.io/en/latest/quark_script.html#detect-cwe-926-in-android-application-dvba-apk) Improper Export of Android Application Components # Quick Start diff --git a/docs/source/quark_method_reference.rst b/docs/source/quark_method_reference.rst index 7aa03218..cd1467d9 100644 --- a/docs/source/quark_method_reference.rst +++ b/docs/source/quark_method_reference.rst @@ -1,239 +1,403 @@ -+++++++++++++++++++++++ -Quark Method Reference -+++++++++++++++++++++++ - -quark.core.quark.py --------------------- - -find_previous_method -===================== - -**The algorithm of find_previous_method** - -The find_previous_method method uses a DFS algorithm to collect all MethodObjects called by the parent_method and add them to the specified wrapper. The search starts from the base_method and goes on recursively until there are no more levels or all candidates have been processed. - -.. code-block:: TEXT - - 1. Initialize an empty set "visited_methods" if it is not provided. - 2. Get a set "method_set" using "self.apkinfo.upperfunc(base_method)". - 3. Add "base_method" to the "visited_methods" set. - 4. If "method_set" is not None then check if "parent_function" is in "method_set". - - If yes, append "base_method" to "wrapper". - - If no, then iterate through each item in "method_set". - - If the item is in "visited_methods", skip it and continue to the next item. - - If not, call "find_previous_method" again with the current item, "parent_function", "wrapper", and "visited_methods". - -**The code of find_previous_method** - -.. code-block:: python - - def find_previous_method( - self, base_method, parent_function, wrapper, visited_methods=None - ): - """ - Find the method under the parent function, based on base_method before to parent_function. - This will append the method into wrapper. - - :param base_method: the base function which needs to be searched. - :param parent_function: the top-level function which calls the basic function. - :param wrapper: list is used to track each function. - :param visited_methods: set with tested method. - :return: None - """ - if visited_methods is None: - visited_methods = set() - - method_set = self.apkinfo.upperfunc(base_method) - visited_methods.add(base_method) - - if method_set is not None: - - if parent_function in method_set: - wrapper.append(base_method) - else: - for item in method_set: - # prevent to test the tested methods. - if item in visited_methods: - continue - self.find_previous_method( - item, parent_function, wrapper, visited_methods - ) - -find_intersection -===================== - -**The algorithm of find_intersection** - -The ``find_intersection`` method takes in two sets, ``first_method_set`` and ``second_method_set``, and finds their intersection using a recursive search algorithm. - -Here is the process of ``find_intersection``。 - -.. code-block:: TEXT - - 1. Check that the input sets are not empty. - If one of the sets is empty, raise a ValueError. - - 2. Use the & operator to find the intersection of the two sets. - If the intersection is not empty, return the resulting set. - - 3. If the intersection is empty, call the method_recursive_search - function with the input sets and a specified maximum depth. - - 4. The method_recursive_search function recursively searches for - the intersection of the two input sets up to the specified depth - by splitting the sets into subsets and comparing each subset's elements. - - If the intersection is found, return the resulting set. - - Otherwise, return None. - -**The code of find_intersection** - -.. code-block:: python - - def find_intersection(self, first_method_set, second_method_set, depth=1): - """ - Find the first_method_list ∩ second_method_list. - [MethodAnalysis, MethodAnalysis,...] - :param first_method_set: first list that contains each MethodAnalysis. - :param second_method_set: second list that contains each MethodAnalysis. - :param depth: maximum number of recursive search functions. - :return: a set of first_method_list ∩ second_method_list or None. - """ - # Check both lists are not null - if not first_method_set or not second_method_set: - raise ValueError("Set is Null") - # find ∩ - result = first_method_set & second_method_set - if result: - return result - else: - return self.method_recursive_search( - depth, first_method_set, second_method_set - ) - -method_recursive_search -======================= - -**The algorithm of method_recursive_search** - -The ``method_recursive_search`` algorithm finds the intersection between -two sets of methods. Specifically, the algorithm expands each set by -recursively adding their respective upper-level method objects until it -finds an intersection or the depth reaches ``MAX_SEARCH_LAYER``. - -Here is the process of ``method_recursive_search``. - -.. code:: text - - 1. The method_recursive_search function takes three arguments: - - depth, first_method_set, and second_method_set - 2. If the depth+1 > MAX_SEARCH_LAYER, return None. - 3. Create next_level_set_1 and next_level_set_2 that are the copies of first_method_set and second_method_set, respectively. - 4. Expand next_level_set_1 and next_level_set_2 by adding their respective upper-level methods. - 5. Calls find_intersection with the next_level_set_1, next_level_set_2 and depth+1 as arguments recursively. - - If an intersection is found, return the result. - - If no intersection is found, continue searching until depth > MAX_SEARCH_LAYER. - -**The code of method_recursive_search** - -.. code:: python - - def method_recursive_search( - self, depth, first_method_set, second_method_set - ): - # Not found same method usage, try to find the next layer. - depth += 1 - if depth > MAX_SEARCH_LAYER: - return None - - # Append first layer into next layer. - next_level_set_1 = first_method_set.copy() - next_level_set_2 = second_method_set.copy() - - # Extend the xref from function into next layer. - for method in first_method_set: - if self.apkinfo.upperfunc(method): - next_level_set_1 = ( - self.apkinfo.upperfunc(method) | next_level_set_1 - ) - for method in second_method_set: - if self.apkinfo.upperfunc(method): - next_level_set_2 = ( - self.apkinfo.upperfunc(method) | next_level_set_2 - ) - - return self.find_intersection( - next_level_set_1, next_level_set_2, depth - ) - -find_api_usage -============== - -**The algorithm of find_api_usage** - -``find_api_usage`` searches for methods with ``method_name`` and ``descriptor_name``, that belong to either the ``class_name`` or its subclass. It returns a list that contains matching methods. - -Here is the process of ``find_api_usage``. - -.. code-block:: TEXT - - 1. Initialize an empty "method_list". - 2. Search for an exact match of the method by its "class_name", "method_name", and "descriptor_name". - - If found, return a list with the matching methods. - 3. Create a list of potential methods with matching "method_name" and "descriptor_name". - 4. Filter the list of potential methods to include only those with bytecodes. - 5. Check if the class of each potential method is a subclass of the given "class_name". - - If yes, add the method to "method_list". - 6. Return "method_list". - -Here is the flowchart of ``find_api_usage``. - -.. image:: https://i.imgur.com/FZKRMgX.png - -**The code of find_api_usage** - -.. code-block:: python - - def find_api_usage(self, class_name, method_name, descriptor_name): - method_list = [] - - # Source method - source_method = self.apkinfo.find_method( - class_name, method_name, descriptor_name - ) - if source_method: - return [source_method] - - # Potential Method - potential_method_list = [ - method - for method in self.apkinfo.all_methods - if method.name == method_name - and method.descriptor == descriptor_name - ] - - potential_method_list = [ - method - for method in potential_method_list - if not next(self.apkinfo.get_method_bytecode(method), None) - ] - - # Check if each method's class is a subclass of the given class - for method in potential_method_list: - current_class_set = {method.class_name} - - while current_class_set and not current_class_set.intersection( - {class_name, "Ljava/lang/Object;"} - ): - next_class_set = set() - for clazz in current_class_set: - next_class_set.update( - self.apkinfo.superclass_relationships[clazz] - ) - - current_class_set = next_class_set - - current_class_set.discard("Ljava/lang/Object;") - if current_class_set: - method_list.append(method) - - return method_list ++++++++++++++++++++++++ +Quark Method Reference ++++++++++++++++++++++++ + +quark.core.quark.py +-------------------- + +find_previous_method +===================== + +**The algorithm of find_previous_method** + +The find_previous_method method uses a DFS algorithm to collect all MethodObjects called by the parent_method and add them to the specified wrapper. The search starts from the base_method and goes on recursively until there are no more levels or all candidates have been processed. + +.. code-block:: TEXT + + 1. Initialize an empty set "visited_methods" if it is not provided. + 2. Get a set "method_set" using "self.apkinfo.upperfunc(base_method)". + 3. Add "base_method" to the "visited_methods" set. + 4. If "method_set" is not None then check if "parent_function" is in "method_set". + - If yes, append "base_method" to "wrapper". + - If no, then iterate through each item in "method_set". + - If the item is in "visited_methods", skip it and continue to the next item. + - If not, call "find_previous_method" again with the current item, "parent_function", "wrapper", and "visited_methods". + +**The code of find_previous_method** + +.. code-block:: python + + def find_previous_method( + self, base_method, parent_function, wrapper, visited_methods=None + ): + """ + Find the method under the parent function, based on base_method before to parent_function. + This will append the method into wrapper. + + :param base_method: the base function which needs to be searched. + :param parent_function: the top-level function which calls the basic function. + :param wrapper: list is used to track each function. + :param visited_methods: set with tested method. + :return: None + """ + if visited_methods is None: + visited_methods = set() + + method_set = self.apkinfo.upperfunc(base_method) + visited_methods.add(base_method) + + if method_set is not None: + + if parent_function in method_set: + wrapper.append(base_method) + else: + for item in method_set: + # prevent to test the tested methods. + if item in visited_methods: + continue + self.find_previous_method( + item, parent_function, wrapper, visited_methods + ) + +find_intersection +===================== + +**The algorithm of find_intersection** + +The ``find_intersection`` method takes in two sets, ``first_method_set`` and ``second_method_set``, and finds their intersection using a recursive search algorithm. + +Here is the process of ``find_intersection``。 + +.. code-block:: TEXT + + 1. Check that the input sets are not empty. + If one of the sets is empty, raise a ValueError. + + 2. Use the & operator to find the intersection of the two sets. + If the intersection is not empty, return the resulting set. + + 3. If the intersection is empty, call the method_recursive_search + function with the input sets and a specified maximum depth. + + 4. The method_recursive_search function recursively searches for + the intersection of the two input sets up to the specified depth + by splitting the sets into subsets and comparing each subset's elements. + - If the intersection is found, return the resulting set. + - Otherwise, return None. + +**The code of find_intersection** + +.. code-block:: python + + def find_intersection(self, first_method_set, second_method_set, depth=1): + """ + Find the first_method_list ∩ second_method_list. + [MethodAnalysis, MethodAnalysis,...] + :param first_method_set: first list that contains each MethodAnalysis. + :param second_method_set: second list that contains each MethodAnalysis. + :param depth: maximum number of recursive search functions. + :return: a set of first_method_list ∩ second_method_list or None. + """ + # Check both lists are not null + if not first_method_set or not second_method_set: + raise ValueError("Set is Null") + # find ∩ + result = first_method_set & second_method_set + if result: + return result + else: + return self.method_recursive_search( + depth, first_method_set, second_method_set + ) + +method_recursive_search +======================= + +**The algorithm of method_recursive_search** + +The ``method_recursive_search`` algorithm finds the intersection between +two sets of methods. Specifically, the algorithm expands each set by +recursively adding their respective upper-level method objects until it +finds an intersection or the depth reaches ``MAX_SEARCH_LAYER``. + +Here is the process of ``method_recursive_search``. + +.. code:: text + + 1. The method_recursive_search function takes three arguments: + - depth, first_method_set, and second_method_set + 2. If the depth+1 > MAX_SEARCH_LAYER, return None. + 3. Create next_level_set_1 and next_level_set_2 that are the copies of first_method_set and second_method_set, respectively. + 4. Expand next_level_set_1 and next_level_set_2 by adding their respective upper-level methods. + 5. Calls find_intersection with the next_level_set_1, next_level_set_2 and depth+1 as arguments recursively. + - If an intersection is found, return the result. + - If no intersection is found, continue searching until depth > MAX_SEARCH_LAYER. + +**The code of method_recursive_search** + +.. code:: python + + def method_recursive_search( + self, depth, first_method_set, second_method_set + ): + # Not found same method usage, try to find the next layer. + depth += 1 + if depth > MAX_SEARCH_LAYER: + return None + + # Append first layer into next layer. + next_level_set_1 = first_method_set.copy() + next_level_set_2 = second_method_set.copy() + + # Extend the xref from function into next layer. + for method in first_method_set: + if self.apkinfo.upperfunc(method): + next_level_set_1 = ( + self.apkinfo.upperfunc(method) | next_level_set_1 + ) + for method in second_method_set: + if self.apkinfo.upperfunc(method): + next_level_set_2 = ( + self.apkinfo.upperfunc(method) | next_level_set_2 + ) + + return self.find_intersection( + next_level_set_1, next_level_set_2, depth + ) + +find_api_usage +============== + +**The algorithm of find_api_usage** + +``find_api_usage`` searches for methods with ``method_name`` and ``descriptor_name``, that belong to either the ``class_name`` or its subclass. It returns a list that contains matching methods. + +Here is the process of ``find_api_usage``. + +.. code-block:: TEXT + + 1. Initialize an empty "method_list". + 2. Search for an exact match of the method by its "class_name", "method_name", and "descriptor_name". + - If found, return a list with the matching methods. + 3. Create a list of potential methods with matching "method_name" and "descriptor_name". + 4. Filter the list of potential methods to include only those with bytecodes. + 5. Check if the class of each potential method is a subclass of the given "class_name". + - If yes, add the method to "method_list". + 6. Return "method_list". + +Here is the flowchart of ``find_api_usage``. + +.. image:: https://i.imgur.com/FZKRMgX.png + +**The code of find_api_usage** + +.. code-block:: python + + def find_api_usage(self, class_name, method_name, descriptor_name): + method_list = [] + + # Source method + source_method = self.apkinfo.find_method( + class_name, method_name, descriptor_name + ) + if source_method: + return [source_method] + + # Potential Method + potential_method_list = [ + method + for method in self.apkinfo.all_methods + if method.name == method_name + and method.descriptor == descriptor_name + ] + + potential_method_list = [ + method + for method in potential_method_list + if not next(self.apkinfo.get_method_bytecode(method), None) + ] + + # Check if each method's class is a subclass of the given class + for method in potential_method_list: + current_class_set = {method.class_name} + + while current_class_set and not current_class_set.intersection( + {class_name, "Ljava/lang/Object;"} + ): + next_class_set = set() + for clazz in current_class_set: + next_class_set.update( + self.apkinfo.superclass_relationships[clazz] + ) + + current_class_set = next_class_set + + current_class_set.discard("Ljava/lang/Object;") + if current_class_set: + method_list.append(method) + + return method_list + +_evaluate_method +===================== + +**The algorithm of _evaluate_method** + +The ``_evaluate_method`` method evaluates the execution of opcodes in the target method and returns a matrix representing the usage of each involved register. The method takes one parameter, method, which is the method to be evaluated. + +Here is the process of ``_evaluate_method``. + +.. code-block:: TEXT + + 1. Create a PyEval object with the apkinfo attribute of the instance. PyEval is presumably + a class that handles the evaluation of opcodes. + + 2. Loop through the bytecode objects in the target method by calling the get_method_bytecode + method of the apkinfo attribute. + + 3. Extract the mnemonic (opcode), registers, and parameter from the bytecode_obj and create + an instruction list containing these elements. + + 4. Convert all elements of the instruction list to strings (in case there are MUTF8String objects). + + 5. Check if the opcode (the first element of instruction) is in the eval dictionary of the pyeval object. + - If it is, call the corresponding function with the instruction as its argument. + + 6. Once the loop is finished, call the show_table method of the pyeval object to return the + matrix representing the usage of each involved register. + +Here is the flowchart of ``_evaluate_method``. + +.. image:: https://i.imgur.com/XCKrjjR.jpg + +**The code of _evaluate_method** + +.. code-block:: python + + def _evaluate_method(self, method) -> List[List[str]]: + """ + Evaluate the execution of the opcodes in the target method and return + the usage of each involved register. + :param method: Method to be evaluated + :return: Matrix that holds the usage of the registers + """ + pyeval = PyEval(self.apkinfo) + + for bytecode_obj in self.apkinfo.get_method_bytecode(method): + # ['new-instance', 'v4', Lcom/google/progress/SMSHelper;] + instruction = [bytecode_obj.mnemonic] + if bytecode_obj.registers is not None: + instruction.extend(bytecode_obj.registers) + if bytecode_obj.parameter is not None: + instruction.append(bytecode_obj.parameter) + + # for the case of MUTF8String + instruction = [str(x) for x in instruction] + + if instruction[0] in pyeval.eval.keys(): + pyeval.eval[instruction[0]](instruction) + + return pyeval.show_table() + +check_parameter_on_single_method +======================================= + +**The algorithm of check_parameter_on_single_method** + +The ``check_parameter_on_single_method`` function checks whether two methods use the same parameter. + +Here is the process of ``check_parameter_on_single_method``. + +.. code-block:: TEXT + + 1. Define a method named check_parameter_on_single_method, which takes 5 parameters: + * self: a reference to the current object, indicating that this method is defined in a class + * usage_table: a table for storing the usage of called functions + * first_method: the first API or the method calling the first API + * second_method: the second API or the method calling the second API + * keyword_item_list: a list of keywords used to determine if the parameter meets specific conditions + + 2. Define a Boolean variable regex, which is set to False by default. + + 3. Obtain the patterns of first_method and second_method based on the given input, and store them in + first_method_pattern and second_method_pattern, respectively. + + 4. Define a generator matched_records. Use the filter function to filter register_usage_records to + include only those matched records used by both first_method and second_method. + + 5. Use a for loop to process the matched records one by one. + + 6. Call method check_parameter_values to check if the matched records contain keywords in keyword_item_list. + - If True, add matched keywords to matched_keyword_list. + - If False, leave matched_keyword_list empty. + + 7. Use yield to return the matched record and matched_keyword_list. This method is a generator that processes + data and returns results at the same time. + +Here is the flowchart of ``check_parameter_on_single_method`` + +.. image:: https://i.imgur.com/BJf7oSg.png + +**The code of check_parameter_on_single_method** + +.. code:: python + + def check_parameter_on_single_method( + self, + usage_table, + first_method, + second_method, + keyword_item_list=None, + regex=False, + ) -> Generator[Tuple[str, List[str]], None, None]: + """Check the usage of the same parameter between two method. + + :param usage_table: the usage of the involved registers + :param first_method: the first API or the method calling the first APIs + :param second_method: the second API or the method calling the second + APIs + :param keyword_item_list: keywords required to be present in the usage + , defaults to None + :param regex: treat the keywords as regular expressions, defaults to + False + :yield: _description_ + """ + first_method_pattern = PyEval.get_method_pattern( + first_method.class_name, first_method.name, first_method.descriptor + ) + + second_method_pattern = PyEval.get_method_pattern( + second_method.class_name, + second_method.name, + second_method.descriptor, + ) + + register_usage_records = ( + c_func + for table in usage_table + for val_obj in table + for c_func in val_obj.called_by_func + ) + + matched_records = filter( + lambda r: first_method_pattern in r and second_method_pattern in r, + register_usage_records, + ) + + for record in matched_records: + if keyword_item_list and list(keyword_item_list): + matched_keyword_list = self.check_parameter_values( + record, + (first_method_pattern, second_method_pattern), + keyword_item_list, + regex, + ) + + if matched_keyword_list: + yield (record, matched_keyword_list) + + else: + yield (record, None) + + + diff --git a/docs/source/quark_script.rst b/docs/source/quark_script.rst index f2a83bca..039fb9f7 100644 --- a/docs/source/quark_script.rst +++ b/docs/source/quark_script.rst @@ -279,6 +279,25 @@ activityInstance.isExported(none) - **params**: none - **return**: True/False +getReceivers(samplePath) +========================== +- **Description**: Get receivers from a target sample. +- **params**: + 1. samplePath: target sample +- **return**: python list containing receivers + +receiverInstance.hasIntentFilter(none) +====================================== +- **Description**: Check if the receiver has an intent-filter. +- **params**: none +- **return**: True/False + +receiverInstance.isExported(none) +================================== +- **Description**: Check if the receiver is exported. +- **params**: none +- **return**: True/False + getApplication(samplePath) ========================== - **Description**: Get the application element from the manifest file of the target sample. @@ -1739,3 +1758,158 @@ Quark Script Result $ python CWE-338.py CWE-338 is detected in Lcom/htbridge/pivaa/EncryptionActivity$2; onClick (Landroid/view/View;)V + + + +Detect CWE-88 in Android Application (Vuldroid.apk) +------------------------------------------------------ + +This scenario seeks to find **Improper Neutralization of Argument Delimiters in a Command**. See `CWE-88 `_ for more details. + +Let‘s use this `APK `_ and the above APIs to show how the Quark script finds this vulnerability. + +First, we design a detection rule ``ExternalStringsCommands.json`` to spot on behavior using external strings as commands. + +Next, we use Quark API ``quarkResultInstance.findMethodInCaller(callerMethod, targetMethod)`` to check if any APIs in the caller method for string matching. + +If NO, the APK does not neutralize special elements within the argument, which may cause CWE-88 vulnerability. + +If YES, check if there are any delimiters used in string matching for a filter. If NO, the APK does not neutralize special elements within the argument, which may cause CWE-88 vulnerability. + + +Quark Script CWE-88.py +======================= + +The Quark Script below uses Vuldroid.apk to demonstrate. + +.. code-block:: python + + from quark.script import runQuarkAnalysis, Rule + + SAMPLE_PATH = "Vuldroid.apk" + RULE_PATH = "ExternalStringCommand.json" + + + STRING_MATCHING_API = [ + ["Ljava/lang/String;", "contains", "(Ljava/lang/CharSequence)Z"], + ["Ljava/lang/String;", "indexOf", "(I)I"], + ["Ljava/lang/String;", "indexOf", "(Ljava/lang/String;)I"], + ["Ljava/lang/String;", "matches", "(Ljava/lang/String;)Z"], + ["Ljava/lang/String;", "replaceAll", + "(Ljava/lang/String; Ljava/lang/String;)Ljava/lang/String;"], + ] + + delimiters = [' ', ';', '||', '|', ',', '>', '>>', '`'] + + ruleInstance = Rule(RULE_PATH) + quarkResult = runQuarkAnalysis(SAMPLE_PATH, ruleInstance) + + for ExternalStringCommand in quarkResult.behaviorOccurList: + + caller = ExternalStringCommand.methodCaller + + strMatchingAPIs = [ + api for api in STRING_MATCHING_API if + quarkResult.findMethodInCaller(caller, api) + ] + + if not strMatchingAPIs or \ + any(dlm not in strMatchingAPIs for dlm in delimiters): + print(f"CWE-88 is detected in method, {caller.fullName}") + + +Quark Rule: ExternalStringCommand.json +========================================= + +.. code-block:: json + + { + "crime": "Using external strings as commands", + "permission": [], + "api": [ + { + "class": "Landroid/content/Intent;", + "method": "getStringExtra", + "descriptor": "(Ljava/lang/String;)Ljava/lang/String" + }, + { + "class": "Ljava/lang/Runtime;", + "method": "exec", + "descriptor": "(Ljava/lang/String;)Ljava/lang/Process" + } + ], + "score": 1, + "label": [] + } + + +Quark Script Result +====================== +- **Vuldroid.apk** + +.. code-block:: TEXT + + $ python3 CWE-88.py + CWE-88 is detected in method, Lcom/vuldroid/application/RootDetection; onCreate (Landroid/os/Bundle;)V + +Detect CWE-925 in Android Application (InsecureBankv2, AndroGoat) +------------------------------------------------------------------ + +This scenario seeks to find **Improper Verification of Intent by +Broadcast Receiver**. See +`CWE-925 `__ for more +details. + +Let’s use both two of apks +(`InsecureBankv2 `__ +and `AndroGoat `__) to show +how the Quark script finds this vulnerability. + +In the first step, we use the ``getReceivers`` API to find all +``Receiver`` components defined in the Android application. Then, we +exclude any receivers that are not exported. + +In the second step, our goal is to verify the **intentAction** is +properly validated in each receiver which is identified in the previous +step. To do this, we use the ``checkMethodCalls`` function. + +Finally, if any receiver’s **onReceive** method exhibits improper +verification on **intentAction**, it could indicate a potential CWE-925 +vulnerability. + +Quark Script CWE-925.py +======================= + +.. code:: python + + from quark.script import checkMethodCalls, getReceivers + + SAMPLE_PATHS = ["AndroGoat.apk", "InsecureBankv2.apk"] + + TARGET_METHOD = [ + '', + 'onReceive', + '(Landroid/content/Context; Landroid/content/Intent;)V' + ] + + CHECK_METHODS = [ + ['Landroid/content/Intent;', 'getAction', '()Ljava/lang/String;'] + ] + + for filepath in SAMPLE_PATHS: + receivers = getReceivers(filepath) + for receiver in receivers: + if receiver.isExported(): + className = "L"+str(receiver).replace('.', '/')+';' + TARGET_METHOD[0] = className + if not checkMethodCalls(filepath, TARGET_METHOD, CHECK_METHODS): + print(f"CWE-925 is detected in method, {className}") + +Quark Script Result +=================== + +.. code-block:: TEXT + + $ python CWE-925.py + CWE-925 is detected in method, Lowasp/sat/agoat/ShowDataReceiver; + CWE-925 is detected in method, Lcom/android/insecurebankv2/MyBroadCastReceiver; diff --git a/quark/__init__.py b/quark/__init__.py index a3011f6e..0df16ace 100644 --- a/quark/__init__.py +++ b/quark/__init__.py @@ -1 +1 @@ -__version__ = "23.2.1" +__version__ = "23.4.1" diff --git a/quark/core/apkinfo.py b/quark/core/apkinfo.py index f01dca7c..9be77657 100644 --- a/quark/core/apkinfo.py +++ b/quark/core/apkinfo.py @@ -66,6 +66,21 @@ def activities(self) -> List[XMLElement]: return application.findall("activity") + @property + def receivers(self) -> List[XMLElement]: + """ + Return all receivers from the given APK. + + :return: a list of all receivers + """ + if self.ret_type == "DEX": + return [] + + manifest_root = self.apk.get_android_manifest_xml() + application = manifest_root.find("application") + + return application.findall("receiver") + @property def android_apis(self) -> Set[MethodObject]: apis = set() diff --git a/quark/core/interface/baseapkinfo.py b/quark/core/interface/baseapkinfo.py index 35e56d88..415bba8a 100644 --- a/quark/core/interface/baseapkinfo.py +++ b/quark/core/interface/baseapkinfo.py @@ -89,6 +89,16 @@ def activities(self) -> List[XMLElement]: """ pass + @property + @abstractmethod + def receivers(self) -> List[XMLElement]: + """ + Return all receivers from the given APK. + + :return: a list of all receivers + """ + pass + @property @abstractmethod def android_apis(self) -> Set[MethodObject]: diff --git a/quark/core/rzapkinfo.py b/quark/core/rzapkinfo.py index d61ad243..3304851b 100644 --- a/quark/core/rzapkinfo.py +++ b/quark/core/rzapkinfo.py @@ -268,6 +268,18 @@ def activities(self) -> List[XMLElement]: return root.findall("application/activity") + @functools.cached_property + def receivers(self) -> List[XMLElement]: + """ + Return all receivers from the given APK. + + :return: a list of all receivers + """ + axml = AxmlReader(self._manifest) + root = axml.get_xml_tree() + + return root.findall("application/receiver") + @property def android_apis(self) -> Set[MethodObject]: return { diff --git a/quark/script/__init__.py b/quark/script/__init__.py index cd81a2fd..f5bae45f 100644 --- a/quark/script/__init__.py +++ b/quark/script/__init__.py @@ -6,7 +6,7 @@ import functools from os import PathLike from os.path import abspath, isfile, join -from typing import Any, List, Tuple, Union +from typing import Any, Iterable, List, Tuple, Union from quark.config import DIR_PATH as QUARK_RULE_PATH from quark.core.analysis import QuarkAnalysis @@ -113,6 +113,46 @@ def isExported(self) -> bool: return exported +class Receiver: + def __init__(self, xml: XMLElement) -> None: + self.xml: XMLElement = xml + + def __str__(self) -> str: + return self._getAttribute("name") + + def _getAttribute( + self, attributeName: str, defaultValue: Any = None + ) -> Any: + realAttributeName = ( + f"{{http://schemas.android.com/apk/res/android}}{attributeName}" + ) + return self.xml.get(realAttributeName, defaultValue) + + def hasIntentFilter(self) -> bool: + """Check if the receiver has an intent filter. + + :return: True/False + """ + return self.xml.find("intent-filter") is not None + + def isExported(self) -> bool: + """Check if the receiver is exported. + + According to the documentation from Android Developer guide. + " + If the attribute exported is unspecified, the default value depends on whether + the broadcast receiver contains intent filters. + If the receiver contains at least one intent filter, + then the default value is "true". + Otherwise, the default value is "false". + " + + :return: True/False + """ + exported = self._getAttribute("exported", self.hasIntentFilter()) + return str(exported).lower() == 'true' + + class Method: def __init__( self, @@ -523,6 +563,18 @@ def getActivities(samplePath: PathLike) -> List[Activity]: return [Activity(xml) for xml in apkinfo.activities] +def getReceivers(samplePath: PathLike) -> List[Receiver]: + """Get receivers from a target sample. + + :param samplePath: target file + :return: python list containing receivers + """ + quark = _getQuark(samplePath) + apkinfo = quark.apkinfo + + return [Receiver(xml) for xml in apkinfo.receivers] + + def getApplication(samplePath: PathLike) -> Application: """Get the application element from the manifest file of the target sample. @@ -579,3 +631,41 @@ def _wrapMethodObject( for caller in list(caller_set) ] return caller_methods + + +def checkMethodCalls( + samplePath: PathLike, + targetMethod: Union[Tuple[str, str, str], MethodObject], + checkMethods: List[Tuple[str, str, str]]) -> bool: + """Check if any of the specific methods shown in the `targetMethod` + + :param samplePath: target file + :param targetMethod: python list contains the class name, + method name, and descriptor of the target method + or a Method Object. + :param checkMethods: python list contains the class name, + method name, and descriptor of the target method + + :return: bool that indicate specific methods can be called or defined within a `target method` or not. + """ + targetMethodSet = set() + checkMethodSet = set() + targetLowerFuncSet = set() + + quark = _getQuark(samplePath) + if isinstance(targetMethod, Iterable): + # Find the method in the APK with the given class name, method name, and descriptor + targetMethodSet.update(quark.apkinfo.find_method(*targetMethod)) + else: + # targetMethod is already a Method object + targetMethodSet.add(MethodObject) + + if not targetMethodSet: + return False + + for candidate in checkMethods: + checkMethodSet.update(quark.apkinfo.find_method(*candidate)) + + targetLowerFuncSet = {i for i, _ in quark.apkinfo.lowerfunc(targetMethodSet.pop())} + + return any(checkMethodSet.intersection(targetLowerFuncSet)) diff --git a/setup.py b/setup.py index edf3dcc8..60c5842e 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/quark-engine/quark-engine", - packages=setuptools.find_packages(), + packages=setuptools.find_packages(exclude=("tests",)), package_data={ "quark.core.axmlreader": ["axml_definition"], "quark.webreport": [ diff --git a/tests/core/test_apkinfo.py b/tests/core/test_apkinfo.py index b8a339a4..c8d1c873 100644 --- a/tests/core/test_apkinfo.py +++ b/tests/core/test_apkinfo.py @@ -123,6 +123,36 @@ def test_activities(apkinfo): == "com.example.google.service.MainActivity" ) + @staticmethod + def test_receivers(apkinfo): + receivers = apkinfo.receivers + + assert len(receivers) == 4 + assert ( + receivers[0].get( + "{http://schemas.android.com/apk/res/android}name" + ) + == "com.example.google.service.SMSServiceBootReceiver" + ) + assert ( + receivers[1].get( + "{http://schemas.android.com/apk/res/android}name" + ) + == "com.example.google.service.SMSReceiver" + ) + assert ( + receivers[2].get( + "{http://schemas.android.com/apk/res/android}name" + ) + == "TaskRequest" + ) + assert ( + receivers[3].get( + "{http://schemas.android.com/apk/res/android}name" + ) + == "com.example.google.service.MyDeviceAdminReceiver" + ) + def test_android_apis(self, apkinfo): api = { MethodObject( diff --git a/tests/script/test_script.py b/tests/script/test_script.py index 5ea7d6e8..6b464be3 100644 --- a/tests/script/test_script.py +++ b/tests/script/test_script.py @@ -11,7 +11,9 @@ Method, QuarkResult, Ruleset, + checkMethodCalls, getActivities, + getReceivers, getApplication, runQuarkAnalysis, findMethodInAPK, @@ -107,6 +109,28 @@ def testIsExported(SAMPLE_PATH_13667): assert activity.isExported() is True +class TestReceiver: + @staticmethod + def testHasNoIntentFilter(SAMPLE_PATH_13667): + receiver = getReceivers(SAMPLE_PATH_13667)[2] + assert receiver.hasIntentFilter() is False + + @staticmethod + def testHasIntentFilter(SAMPLE_PATH_13667): + receiver = getReceivers(SAMPLE_PATH_13667)[0] + assert receiver.hasIntentFilter() is True + + @staticmethod + def testIsNotExported(SAMPLE_PATH_13667): + receiver = getReceivers(SAMPLE_PATH_13667)[2] + assert receiver.isExported() is False + + @staticmethod + def testIsExported(SAMPLE_PATH_13667): + receiver = getReceivers(SAMPLE_PATH_13667)[0] + assert receiver.isExported() is True + + class TestMethod: @staticmethod def testInit(QUARK_ANALYSIS_RESULT_FOR_RULE_68): @@ -453,6 +477,13 @@ def testGetActivities(SAMPLE_PATH_14d9f) -> None: assert str(activities[0]) == "com.google.progress.BackGroundActivity" +def testGetReceivers(SAMPLE_PATH_14d9f) -> None: + receivers = getReceivers(SAMPLE_PATH_14d9f) + + assert len(receivers) == 1 + assert str(receivers[0]) == "com.google.progress.BootReceiver" + + def testfindMethodInAPK(SAMPLE_PATH_14d9f) -> None: method = findMethodInAPK(SAMPLE_PATH_14d9f, [ @@ -462,3 +493,22 @@ def testfindMethodInAPK(SAMPLE_PATH_14d9f) -> None: ) assert len(method) == 2 + + +def testCheckMethodCalls(SAMPLE_PATH_14d9f) -> None: + targetMethod = [ + "Lcom/google/progress/WifiCheckTask;", + "checkWifiCanOrNotConnectServer", + "([Ljava/lang/String;)Z" + ] + + checkMethods = [] + checkMethods.append(tuple([ + "Landroid/util/Log;", + "e", + "(Ljava/lang/String; Ljava/lang/String;)I" + ])) + + assert checkMethodCalls("14d9f1a92dd984d6040cc41ed06e273e.apk", targetMethod, checkMethods) is True + +