Esempio n. 1
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(self.__class__.__name__))

        try:
            Xml.register_namespace('android', 'http://schemas.android.com/apk/res/android')

            xml_parser = Xml.XMLParser(encoding='utf-8')
            manifest_tree = Xml.parse(obfuscation_info.get_manifest_file(), parser=xml_parser)
            manifest_root = manifest_tree.getroot()

            self.package_name = manifest_root.get('package')
            if not self.package_name:
                raise Exception('Unable to extract package name from application manifest')

            # Get a mapping between class name and smali file path.
            for smali_file in util.show_list_progress(obfuscation_info.get_smali_files(),
                                                      interactive=obfuscation_info.interactive,
                                                      description='Class name to smali file mapping'):
                with open(smali_file, 'r', encoding='utf-8') as current_file:
                    class_name = None
                    for line in current_file:
                        if not class_name:
                            # Every smali file contains a class.
                            class_match = util.class_pattern.match(line)
                            if class_match:
                                self.class_name_to_smali_file[class_match.group('class_name')] = smali_file
                                break

            self.transform_package_name(manifest_root)

            # Write the changes into the manifest file.
            manifest_tree.write(obfuscation_info.get_manifest_file(), encoding='utf-8')

            xml_files: Set[str] = set(
                os.path.join(root, file_name)
                for root, dir_names, file_names in os.walk(obfuscation_info.get_resource_directory())
                for file_name in file_names if file_name.endswith('.xml') and 'layout' in root  # Only layout files.
            )
            xml_files.add(obfuscation_info.get_manifest_file())

            # TODO: use the following code to rename only the classes declared in application's package.
            # package_smali_files: Set[str] = set(
            #     smali_file for class_name, smali_file in self.class_name_to_smali_file.items()
            #     if class_name[1:].startswith(self.package_name.replace('.', '/'))
            # )
            #
            # # Rename the classes declared in the application's package.
            # class_rename_transformations = self.rename_class_declarations(list(package_smali_files),
            #                                                               obfuscation_info.interactive)

            # Rename all classes declared in smali files.
            class_rename_transformations = self.rename_class_declarations(obfuscation_info.get_smali_files(),
                                                                          obfuscation_info.interactive)

            # Update renamed classes through all the smali files.
            self.rename_class_usages_in_smali(obfuscation_info.get_smali_files(), class_rename_transformations,
                                              obfuscation_info.interactive)

            # Update renamed classes through all the xml files.
            self.rename_class_usages_in_xml(list(xml_files), class_rename_transformations,
                                            obfuscation_info.interactive)

        except Exception as e:
            self.logger.error('Error during execution of "{0}" obfuscator: {1}'.format(self.__class__.__name__, e))
            raise

        finally:
            obfuscation_info.used_obfuscators.append(self.__class__.__name__)
Esempio n. 2
0
 def test_get_resource_directory(self, tmp_demo_apk_v10_original_path: str):
     obfuscation = Obfuscation(tmp_demo_apk_v10_original_path)
     resource_dir = obfuscation.get_resource_directory()
     assert os.path.isdir(resource_dir)
     assert "drawable" in os.listdir(resource_dir)
Esempio n. 3
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(self.__class__.__name__))

        self.encryption_secret = obfuscation_info.encryption_secret
        try:
            string_res_field_pattern = re.compile(r'\.field\spublic\sstatic\sfinal\s(?P<string_name>\S+?):I\s=\s'
                                                  r'(?P<string_id>[0-9a-fA-FxX]+)', re.UNICODE)

            string_id_pattern = re.compile(r'\s+const\s(?P<register>[vp0-9]+),\s(?P<id>\S+)')

            string_array_id_pattern = re.compile(r'\s+const/high16\s(?P<register>[vp0-9]+),\s(?P<id>\S+)')

            load_string_res_pattern = re.compile(r'\s+invoke-virtual\s'
                                                 r'{[vp0-9]+,\s(?P<param_register>[vp0-9]+)},\s'
                                                 r'(Landroid/content/res/Resources;->getString\(I\)Ljava/lang/String;'
                                                 r'|Landroid/content/Context;->getString\(I\)Ljava/lang/String;)')

            load_string_array_res_pattern = re.compile(r'\s+invoke-virtual\s'
                                                       r'{[vp0-9]+,\s(?P<param_register>[vp0-9]+)},\s'
                                                       r'Landroid/content/res/Resources;->'
                                                       r'getStringArray\(I\)\[Ljava/lang/String;')

            move_result_obj_pattern = re.compile(r'\s+move-result-object\s(?P<register>[vp0-9]+)')

            # Set with the names of the encrypted string and string array resources.
            encrypted_res_strings: Set[str] = set()
            encrypted_res_string_arrays: Set[str] = set()

            # Find the mappings between string name and string id.
            string_id_to_string_name: dict = {}
            string_array_id_to_string_name: dict = {}
            for smali_file in obfuscation_info.get_smali_files():
                if smali_file.endswith('R$string.smali'):
                    with open(smali_file, 'r', encoding='utf-8') as current_file:
                        for line in current_file:
                            if line.startswith('.method '):
                                # Method declaration reached, no more field declarations from now on.
                                break
                            field_match = string_res_field_pattern.match(line)
                            if field_match:
                                # String name and id declaration.
                                string_id_to_string_name[field_match.group('string_id')] = \
                                    field_match.group('string_name')

                elif smali_file.endswith('R$array.smali'):
                    with open(smali_file, 'r', encoding='utf-8') as current_file:
                        for line in current_file:
                            if line.startswith('.method '):
                                # Method declaration reached, no more field declarations from now on.
                                break
                            field_match = string_res_field_pattern.match(line)
                            if field_match:
                                # String array name and id declaration.
                                string_array_id_to_string_name[field_match.group('string_id')] = \
                                    field_match.group('string_name')

            for smali_file in util.show_list_progress(obfuscation_info.get_smali_files(),
                                                      interactive=obfuscation_info.interactive,
                                                      description='Encrypting string resources'):
                self.logger.debug('Encrypting string resources in file "{0}"'.format(smali_file))

                with open(smali_file, 'r', encoding='utf-8') as current_file:
                    lines = current_file.readlines()

                # Line numbers where a string is loaded from resources.
                string_index: List[int] = []

                # Registers containing the strings loaded from resources.
                string_register: List[str] = []

                # The number of local registers in the method where a string resource is loaded.
                string_local_count: List[int] = []

                # Line numbers where a string array is loaded from resources.
                string_array_index: List[int] = []

                # Registers containing the string arrays loaded from resources.
                string_array_register: List[str] = []

                # The number of local registers in the method where a string array resource is loaded.
                string_array_local_count: List[int] = []

                # Look for resource strings that can be encrypted.
                current_local_count = 0
                for line_number, line in enumerate(lines):
                    # We are iterating the lines in order, so each time we enter a method we'll find the declaration
                    # with the number of local registers available. We need this information because the invoke
                    # instruction that we need later won't take registers with values greater than 15.
                    match = util.locals_pattern.match(line)
                    if match:
                        current_local_count = int(match.group('local_count'))
                        continue

                    string_res_match = load_string_res_pattern.match(line)
                    if string_res_match:
                        string_index.append(line_number)
                        string_register.append(string_res_match.group('param_register'))
                        string_local_count.append(current_local_count)
                        continue

                    string_array_res_match = load_string_array_res_pattern.match(line)
                    if string_array_res_match:
                        string_array_index.append(line_number)
                        string_array_register.append(string_array_res_match.group('param_register'))
                        string_array_local_count.append(current_local_count)

                # Iterate the lines backwards (until the method declaration is reached) and find the id of each
                # string resource.
                for string_number, index in enumerate(string_index):
                    for line_number in range(index - 1, 0, -1):
                        if lines[line_number].startswith('.method '):
                            # Method declaration reached, no string resource found so proceed with the next (if any).
                            # If we are here it means that the string was loaded from a variable and not from a
                            # constant reference, so this string should not be encrypted. We set the corresponding
                            # string_index to -1 and we won't insert any decryption code for this string.
                            string_index[string_number] = -1
                            break

                        # NOTE: if a string is loaded from resources, it will be encrypted. If other code loads
                        # the same string but using a variable instead of the resource id, it won't work anymore
                        # and this case is not handled by this obfuscator.

                        id_match = string_id_pattern.match(lines[line_number])
                        if id_match and id_match.group('register') == string_register[string_number]:
                            # String id declaration, get the name corresponding to the id and add it to
                            # the list of string resources to be encrypted.
                            if id_match.group('id') in string_id_to_string_name:
                                encrypted_res_strings.add(string_id_to_string_name[id_match.group('id')])

                            # Proceed with the next asset file (if any).
                            break

                # Iterate the lines backwards (until the method declaration is reached) and find the id of each
                # string array resource.
                for string_array_number, index in enumerate(string_array_index):
                    for line_number in range(index - 1, 0, -1):
                        if lines[line_number].startswith('.method '):
                            # Method declaration reached, no string array resource found so proceed
                            # with the next (if any).
                            # If we are here it means that the string was loaded from a variable and not from a
                            # constant reference, so this string should not be encrypted. We set the corresponding
                            # string_array_index to -1 and we won't insert any decryption code for this string.
                            string_array_index[string_array_number] = -1
                            break

                        # NOTE: if a string array is loaded from resources, it will be encrypted. If other code loads
                        # the same string array but using a variable instead of the resource id, it won't work anymore
                        # and this case is not handled by this obfuscator.

                        id_match = string_array_id_pattern.match(lines[line_number])
                        if id_match and id_match.group('register') == string_array_register[string_array_number]:
                            # String array id declaration, get the name corresponding to the id and add it to
                            # the list of string array resources to be encrypted.
                            if id_match.group('id') in string_array_id_to_string_name:
                                encrypted_res_string_arrays.add(string_array_id_to_string_name[id_match.group('id')])

                            # Proceed with the next asset file (if any).
                            break

                # After each string resource is loaded, decrypt it (the string resource will be encrypted
                # directly in the xml file).
                for string_number, index in enumerate(i for i in string_index if i != -1):
                    # For each resource string loaded, look for the next move-result-object instruction to see
                    # in which register the string is saved, in order to add a new instruction to decrypt it.
                    for line_number in range(index + 1, len(lines)):
                        if lines[line_number].startswith('.end method'):
                            # Method end reached, no move-result-object instruction found for this string
                            # resource (the loaded string is not used), so proceed with the next (if any).
                            break

                        # If the string resource is put into a register v0-v15 we can proceed with the encryption,
                        # but if it uses a p<number> register, before encrypting we have to check if
                        # <number> + locals <= 15.
                        move_result_match = move_result_obj_pattern.match(lines[line_number])
                        if move_result_match:
                            reg_type = move_result_match.group('register')[:1]
                            reg_number = int(move_result_match.group('register')[1:])
                            if (reg_type == 'v' and reg_number <= 15) or \
                                    (reg_type == 'p' and reg_number + string_local_count[string_number] <= 15):
                                # Add string decrypt instruction.
                                lines[line_number] += '\n\tinvoke-static {{{register}}}, ' \
                                    'Lcom/decryptstringmanager/DecryptString;->' \
                                    'decryptString(Ljava/lang/String;)Ljava/lang/String;\n\n'.format(
                                    register=move_result_match.group('register')) + lines[line_number]

                            # Proceed with the next string resource (if any).
                            break

                # After each string array resource is loaded, decrypt it (the string array resource will be encrypted
                # directly in the xml file).
                for string_array_number, index in enumerate(i for i in string_array_index if i != -1):
                    # For each resource string array loaded, look for the next move-result-object instruction to see
                    # in which register the string array is saved, in order to add a new instruction to decrypt it.
                    for line_number in range(index + 1, len(lines)):
                        if lines[line_number].startswith('.end method'):
                            # Method end reached, no move-result-object instruction found for this string array
                            # resource (the loaded string array is not used), so proceed with the next (if any).
                            break

                        # If the string array resource is put into a register v0-v15 we can proceed with the encryption,
                        # but if it uses a p<number> register, before encrypting we have to check if
                        # <number> + locals <= 15.
                        move_result_match = move_result_obj_pattern.match(lines[line_number])
                        if move_result_match:
                            reg_type = move_result_match.group('register')[:1]
                            reg_number = int(move_result_match.group('register')[1:])
                            if (reg_type == 'v' and reg_number <= 15) or \
                                    (reg_type == 'p' and reg_number +
                                     string_array_local_count[string_array_number] <= 15):
                                # Add string array decrypt instruction.
                                lines[line_number] += '\n\tinvoke-static {{{register}}}, ' \
                                    'Lcom/decryptstringmanager/DecryptString;->' \
                                    'decryptStringArray([Ljava/lang/String;)[Ljava/lang/String;\n\n'.format(
                                    register=move_result_match.group('register')) + lines[line_number]

                            # Proceed with the next string array resource (if any).
                            break

                with open(smali_file, 'w', encoding='utf-8') as current_file:
                    current_file.writelines(lines)

            # Encrypt the strings and the string arrays in the resource files.
            strings_xml_path = os.path.join(obfuscation_info.get_resource_directory(), 'values', 'strings.xml')
            string_arrays_xml_path = os.path.join(obfuscation_info.get_resource_directory(), 'values', 'arrays.xml')
            if os.path.isfile(strings_xml_path):
                self.encrypt_string_resources(strings_xml_path, encrypted_res_strings)
            if os.path.isfile(string_arrays_xml_path):
                self.encrypt_string_array_resources(string_arrays_xml_path, encrypted_res_string_arrays)

            if not obfuscation_info.decrypt_string_smali_file_added_flag and (encrypted_res_strings or
                                                                              encrypted_res_string_arrays):
                # Add to the app the code for decrypting the encrypted strings. The code
                # for decrypting can be put in any smali directory, since it will be moved to the
                # correct directory when rebuilding the application.
                destination_dir = os.path.dirname(obfuscation_info.get_smali_files()[0])
                destination_file = os.path.join(destination_dir, 'DecryptString.smali')
                with open(destination_file, 'w', encoding='utf-8') as decrypt_string_smali:
                    decrypt_string_smali.write(util.get_decrypt_string_smali_code(self.encryption_secret))
                    obfuscation_info.decrypt_string_smali_file_added_flag = True

        except Exception as e:
            self.logger.error('Error during execution of "{0}" obfuscator: {1}'.format(self.__class__.__name__, e))
            raise

        finally:
            obfuscation_info.used_obfuscators.append(self.__class__.__name__)