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

        try:
            android_class_names: Set[str] = set(util.get_android_class_names())
            parent_class_names: Set[str] = self.get_parent_class_names(
                obfuscation_info.get_smali_files())

            # Methods in parent classes belonging to the Android framework should be ignored when renaming.
            classes_to_ignore: Set[str] = parent_class_names.intersection(
                android_class_names)
            methods_to_ignore: Set[str] = self.get_methods_to_ignore(
                obfuscation_info.get_smali_files(), classes_to_ignore)

            renamed_methods: Set[str] = self.rename_method_declarations(
                obfuscation_info.get_smali_files(), methods_to_ignore,
                obfuscation_info.interactive)

            self.rename_method_invocations(obfuscation_info.get_smali_files(),
                                           renamed_methods,
                                           android_class_names,
                                           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__)
예제 #2
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(self.__class__.__name__))

        try:
            # NOTE: only direct methods (methods that are by nature non-overridable,
            # namely private instance methods, constructors and static methods) will be
            # renamed.

            android_class_names: Set[str] = set(util.get_android_class_names())

            renamed_methods: Set[str] = self.rename_method_declarations(
                obfuscation_info.get_smali_files(),
                android_class_names,
                obfuscation_info.interactive,
            )

            self.rename_method_invocations(
                obfuscation_info.get_smali_files(),
                renamed_methods,
                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__)
예제 #3
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        # Get user defined ignore package list.
        self.ignore_package_names = obfuscation_info.get_ignore_package_names()

        try:
            sdk_class_declarations = self.get_sdk_class_names(
                obfuscation_info.get_smali_files())
            renamed_field_declarations: Set[str] = set()

            # There is a field limit for dex files.
            self.max_fields_to_add = (
                obfuscation_info.get_remaining_fields_per_obfuscator())
            self.added_fields = 0

            if obfuscation_info.is_multidex():
                for index, dex_smali_files in enumerate(
                        util.show_list_progress(
                            obfuscation_info.get_multidex_smali_files(),
                            interactive=obfuscation_info.interactive,
                            unit="dex",
                            description="Processing multidex",
                        )):
                    self.max_fields_to_add = (
                        obfuscation_info.get_remaining_fields_per_obfuscator()
                        [index])
                    self.added_fields = 0
                    renamed_field_declarations.update(
                        self.rename_field_declarations(
                            dex_smali_files, obfuscation_info.interactive))
            else:
                renamed_field_declarations = self.rename_field_declarations(
                    obfuscation_info.get_smali_files(),
                    obfuscation_info.interactive)

            # When renaming field references it makes no difference if this is a
            # multidex application, since at this point we are not introducing any new
            # field.
            self.rename_field_references(
                renamed_field_declarations,
                obfuscation_info.get_smali_files(),
                sdk_class_declarations,
                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__)
예제 #4
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            android_class_names: Set[str] = set(util.get_android_class_names())
            parent_class_names: Set[str] = self.get_parent_class_names(
                obfuscation_info.get_smali_files())

            # Methods in parent classes belonging to the Android framework should
            # be ignored.
            classes_to_ignore: Set[str] = parent_class_names.intersection(
                android_class_names)
            methods_to_ignore: Set[str] = self.get_methods_to_ignore(
                obfuscation_info.get_smali_files(), classes_to_ignore)

            # There is a method limit for dex files.
            max_methods_to_add = obfuscation_info.get_remaining_methods_per_obfuscator(
            )

            if obfuscation_info.is_multidex():
                for index, dex_smali_files in enumerate(
                        util.show_list_progress(
                            obfuscation_info.get_multidex_smali_files(),
                            interactive=obfuscation_info.interactive,
                            unit="dex",
                            description="Processing multidex",
                        )):
                    max_methods_to_add = obfuscation_info.get_remaining_methods_per_obfuscator(
                    )[index]
                    self.add_method_overloads(
                        dex_smali_files,
                        methods_to_ignore,
                        max_methods_to_add,
                        obfuscation_info.interactive,
                    )
            else:
                self.add_method_overloads(
                    obfuscation_info.get_smali_files(),
                    methods_to_ignore,
                    max_methods_to_add,
                    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__)
예제 #5
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(self.__class__.__name__))

        try:
            op_codes = util.get_nop_valid_op_codes()
            pattern = re.compile(r'\s+(?P<op_code>\S+)')

            for smali_file in util.show_list_progress(obfuscation_info.get_smali_files(),
                                                      interactive=obfuscation_info.interactive,
                                                      description='Inserting "nop" instructions in smali files'):
                self.logger.debug('Inserting "nop" instructions in file "{0}"'.format(smali_file))
                with util.inplace_edit_file(smali_file) as current_file:
                    for line in current_file:

                        # Print original instruction.
                        print(line, end='')

                        # Check if this line contains an op code at the beginning of the string.
                        match = pattern.match(line)
                        if match:
                            op_code = match.group('op_code')
                            # If this is a valid op code, insert some nop instructions after it.
                            if op_code in op_codes:
                                nop_count = util.get_random_int(1, 5)
                                print('\tnop\n' * nop_count, end='')

        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__)
예제 #6
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            # There is a method limit for dex files.
            max_methods_to_add = obfuscation_info.get_remaining_methods_per_obfuscator(
            )

            if obfuscation_info.is_multidex():
                for index, dex_smali_files in enumerate(
                        util.show_list_progress(
                            obfuscation_info.get_multidex_smali_files(),
                            interactive=obfuscation_info.interactive,
                            unit='dex',
                            description='Processing multidex')):
                    max_methods_to_add = obfuscation_info.get_remaining_methods_per_obfuscator(
                    )[index]
                    self.add_call_indirections(dex_smali_files,
                                               max_methods_to_add,
                                               obfuscation_info.interactive)
            else:
                self.add_call_indirections(obfuscation_info.get_smali_files(),
                                           max_methods_to_add,
                                           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__)
예제 #7
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(self.__class__.__name__))

        try:
            for smali_file in util.show_list_progress(obfuscation_info.get_smali_files(),
                                                      interactive=obfuscation_info.interactive,
                                                      description='Inserting arithmetic computations in smali files'):
                self.logger.debug('Inserting arithmetic computations in file "{0}"'.format(smali_file))
                with util.inplace_edit_file(smali_file) as current_file:
                    editing_method = False
                    start_label = None
                    end_label = None
                    for line in current_file:
                        if line.startswith('.method ') and ' abstract ' not in line and \
                                ' native ' not in line and not editing_method:
                            # Entering method.
                            print(line, end='')
                            editing_method = True

                        elif line.startswith('.end method') and editing_method:
                            # Exiting method.
                            if start_label and end_label:
                                print('\t:{0}'.format(end_label))
                                print('\tgoto/32 :{0}'.format(start_label))
                                start_label = None
                                end_label = None
                            print(line, end='')
                            editing_method = False

                        elif editing_method:
                            # Inside method.
                            print(line, end='')
                            match = util.locals_pattern.match(line)
                            if match and int(match.group('local_count')) >= 2:
                                # If there are at least 2 registers available, add a fake branch at the beginning of
                                # the method: one branch will continue from here, the other branch will go to the end
                                # of the method and then will return here through a "goto" instruction.
                                v0, v1 = util.get_random_int(1, 32), util.get_random_int(1, 32)
                                start_label = util.get_random_string(16)
                                end_label = util.get_random_string(16)
                                tmp_label = util.get_random_string(16)
                                print('\n\tconst v0, {0}'.format(v0))
                                print('\tconst v1, {0}'.format(v1))
                                print('\tadd-int v0, v0, v1')
                                print('\trem-int v0, v0, v1')
                                print('\tif-gtz v0, :{0}'.format(tmp_label))
                                print('\tgoto/32 :{0}'.format(end_label))
                                print('\t:{0}'.format(tmp_label))
                                print('\t:{0}'.format(start_label))

                        else:
                            print(line, end='')

        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__)
예제 #8
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            debug_op_codes = [
                '.source ', '.line ', '.prologue', '.epilogue', '.local ',
                '.end local', '.restart local', '.param '
            ]

            param_pattern = re.compile(r'\s+\.param\s(?P<register>[vp0-9]+)')

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

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

                with open(smali_file, 'w', encoding='utf-8') as current_file:
                    # Keep only the lines not containing debug op codes.
                    # ".param <annotation> .end param" shouldn't be removed.
                    reversed_lines_to_keep = []
                    inside_param_declaration = False
                    for line in reversed(
                            file_content.splitlines(keepends=True)):
                        if line.strip().startswith('.end param'):
                            inside_param_declaration = True
                            reversed_lines_to_keep.append(line)
                        elif line.strip().startswith(
                                '.param ') and inside_param_declaration:
                            inside_param_declaration = False
                            # Remove unnecessary data from param (name and type comment).
                            line = '{0}\n'.format(
                                param_pattern.match(line).group())
                            reversed_lines_to_keep.append(line)
                        elif not inside_param_declaration:
                            if not any(line.strip().startswith(op_code)
                                       for op_code in debug_op_codes):
                                reversed_lines_to_keep.append(line)
                        else:
                            reversed_lines_to_keep.append(line)

                    current_file.writelines(
                        list(reversed(reversed_lines_to_keep)))

        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__)
예제 #9
0
파일: goto.py 프로젝트: zulJ96/Obfuscapk
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            for smali_file in util.show_list_progress(
                    obfuscation_info.get_smali_files(),
                    interactive=obfuscation_info.interactive,
                    description='Inserting "goto" instructions in smali files',
            ):
                self.logger.debug(
                    'Inserting "goto" instructions in file "{0}"'.format(
                        smali_file))
                with util.inplace_edit_file(smali_file) as (in_file, out_file):
                    editing_method = False
                    for line in in_file:
                        if (line.startswith(".method ")
                                and " abstract " not in line
                                and " native " not in line
                                and not editing_method):
                            # If at the beginning of a non abstract/native method
                            # (after the .locals instruction), insert a "goto" to the
                            # label at the end of the method and a label to the first
                            # instruction of the method.
                            out_file.write(line)
                            editing_method = True

                        elif editing_method and util.locals_pattern.match(
                                line):
                            out_file.write(line)
                            out_file.write(
                                "\n\tgoto/32 :after_last_instruction\n\n")
                            out_file.write("\t:before_first_instruction\n")

                        elif line.startswith(".end method") and editing_method:
                            # If at the end of the method, insert a label after the
                            # last instruction of the method and a "goto" to the label
                            # at the beginning of the method. This will not cause an
                            # endless loop because the method will return at some point
                            # and the second "goto" won't be called again when the
                            # method finishes.
                            out_file.write("\n\t:after_last_instruction\n\n")
                            out_file.write(
                                "\tgoto/32 :before_first_instruction\n\n")
                            out_file.write(line)
                            editing_method = False

                        else:
                            out_file.write(line)

        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__)
예제 #10
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            # NOTE: only direct methods (methods that are by nature non-overridable,
            # namely private instance methods, constructors and static methods) will be
            # overloaded.

            android_class_names: Set[str] = set(util.get_android_class_names())

            # There is a method limit for dex files.
            max_methods_to_add = obfuscation_info.get_remaining_methods_per_obfuscator(
            )

            if obfuscation_info.is_multidex():
                for index, dex_smali_files in enumerate(
                        util.show_list_progress(
                            obfuscation_info.get_multidex_smali_files(),
                            interactive=obfuscation_info.interactive,
                            unit="dex",
                            description="Processing multidex",
                        )):
                    max_methods_to_add = (
                        obfuscation_info.get_remaining_methods_per_obfuscator(
                        )[index])
                    self.add_method_overloads(
                        dex_smali_files,
                        android_class_names,
                        max_methods_to_add,
                        obfuscation_info.interactive,
                    )
            else:
                self.add_method_overloads(
                    obfuscation_info.get_smali_files(),
                    android_class_names,
                    max_methods_to_add,
                    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__)
예제 #11
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(self.__class__.__name__))

        try:
            debug_op_codes = ['.source ', '.line ', '.prologue', '.epilogue',
                              '.local ', '.end local', '.restart local',
                              '.param ']

            param_with_annotation_pattern = re.compile(r'\.param.+?'
                                                       r'\.annotation.+?'
                                                       r'\.end annotation\n'
                                                       r'\s+\.end param\n', re.UNICODE | re.MULTILINE | re.DOTALL)

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

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

                with open(smali_file, 'w', encoding='utf-8') as current_file:
                    # Remove .param <annotation> .end param.
                    file_content = param_with_annotation_pattern.sub('', file_content)

                    # Keep only the lines not containing debug op codes.
                    lines_to_keep = []
                    for line in file_content.splitlines(keepends=True):
                        if not any(line.strip().startswith(op_code) for op_code in debug_op_codes):
                            lines_to_keep.append(line)
                    current_file.writelines(lines_to_keep)

        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__)
예제 #12
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__)
예제 #13
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__)
예제 #14
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            # This instruction takes 2 registers, the latter contains the name of the asset file to load.
            open_asset_invoke_pattern = re.compile(
                r'\s+invoke-virtual\s'
                r'{[vp0-9]+,\s(?P<param_register>[vp0-9]+)},\s'
                r'Landroid/content/res/AssetManager;->'
                r'open\(Ljava/lang/String;\)Ljava/io/InputStream;')

            assets_dir = obfuscation_info.get_assets_directory()
            already_encrypted_files: Set[str] = set()

            # Continue only if there are assets file to encrypt.
            if os.path.isdir(assets_dir):
                for smali_file in util.show_list_progress(
                        obfuscation_info.get_smali_files(),
                        interactive=obfuscation_info.interactive,
                        description='Encrypting asset files'):
                    self.logger.debug(
                        'Encrypting asset files used in smali file "{0}"'.
                        format(smali_file))

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

                    # Line numbers where an asset file is opened.
                    asset_index: List[int] = []

                    # Registers containing the strings with the names of the opened asset files.
                    asset_register: List[str] = []

                    # Names of the opened asset files.
                    asset_names: List[str] = []

                    # Each time an asset name is added to asset_names, the line number of the asset
                    # open instruction is added to this list, so the element in position n in asset_names
                    # is opened at the line in position n in asset_index_for_asset_names. So each time an
                    # asset file is encrypted, the corresponding line is changed to open the encrypted file.
                    # A new variable is needed because asset_index could have different indices than asset_names
                    # because there might be assets loaded from other variables and not constant strings.
                    asset_index_for_asset_names: List[int] = []

                    for line_number, line in enumerate(lines):
                        invoke_match = open_asset_invoke_pattern.match(line)
                        if invoke_match:
                            # Asset file open instruction.
                            asset_index.append(line_number)
                            asset_register.append(
                                invoke_match.group('param_register'))

                    # Iterate the lines backwards (until the method declaration is reached) and for each asset
                    # file open instruction find the constant string containing the name of the opened file (if any).
                    for asset_number, index in enumerate(asset_index):
                        for line_number in range(index - 1, 0, -1):
                            if lines[line_number].startswith('.method '):
                                # Method declaration reached, no constant string found for this asset file so
                                # proceed with the next (if any).
                                break

                            # NOTE: if an asset is opened using a constant string, it will be encrypted. If other
                            # code opens the same assets but using a variable instead of a constant string, it
                            # won't work anymore and this case is not handled by this obfuscator.

                            string_match = util.const_string_pattern.match(
                                lines[line_number])
                            if string_match and string_match.group(
                                    'register'
                            ) == asset_register[asset_number]:
                                # Asset file name string declaration.
                                asset_names.append(
                                    string_match.group('string'))
                                asset_index_for_asset_names.append(
                                    asset_index[asset_number])

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

                    # Encrypt the the loaded asset files and replace the old code with new code to decrypt
                    # the encrypted asset files.
                    for index, asset_name in enumerate(asset_names):
                        asset_file = os.path.join(assets_dir, asset_name)
                        if os.path.isfile(asset_file):

                            # Encrypt the asset file (if not already encrypted).
                            if asset_file not in already_encrypted_files:
                                with open(asset_file,
                                          'rb') as original_asset_file:
                                    encrypted_content = AES \
                                        .new(key=self.encryption_secret.encode(), mode=AES.MODE_ECB) \
                                        .encrypt(pad(original_asset_file.read(), AES.block_size))

                                with open(asset_file,
                                          'wb') as encrypted_asset_file:
                                    encrypted_asset_file.write(
                                        encrypted_content)

                                already_encrypted_files.add(asset_file)

                            # Replace the old code with new code to decrypt the encrypted asset file.
                            lines[asset_index_for_asset_names[index]] = \
                                lines[asset_index_for_asset_names[index]].replace(
                                    'invoke-virtual', 'invoke-static').replace(
                                    'Landroid/content/res/AssetManager;->open(Ljava/lang/String;)Ljava/io/InputStream;',
                                    'Lcom/decryptassetmanager/DecryptAsset;->decryptAsset('
                                    'Landroid/content/res/AssetManager;Ljava/lang/String;)Ljava/io/InputStream;')

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

                if not obfuscation_info.decrypt_asset_smali_file_added_flag and already_encrypted_files:
                    # Add to the app the code for decrypting the encrypted assets. 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,
                                                    'DecryptAsset.smali')
                    with open(destination_file, 'w',
                              encoding='utf-8') as decrypt_asset_smali:
                        decrypt_asset_smali.write(
                            util.get_decrypt_asset_smali_code())
                        obfuscation_info.decrypt_asset_smali_file_added_flag = True

            else:
                self.logger.debug('No assets found')

        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__)
예제 #15
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            op_codes = util.get_code_block_valid_op_codes()
            op_code_pattern = re.compile(r'\s+(?P<op_code>\S+)')
            if_pattern = re.compile(
                r'\s+(?P<if_op_code>\S+)\s(?P<register>[vp0-9,\s]+?),\s:(?P<goto_label>\S+)'
            )

            for smali_file in util.show_list_progress(
                    obfuscation_info.get_smali_files(),
                    interactive=obfuscation_info.interactive,
                    description='Code reordering'):
                self.logger.debug(
                    'Reordering code in file "{0}"'.format(smali_file))
                with util.inplace_edit_file(smali_file) as current_file:
                    editing_method = False
                    inside_try_catch = False
                    jump_count = 0
                    for line in current_file:
                        if line.startswith('.method ') and ' abstract ' not in line and \
                                ' native ' not in line and not editing_method:
                            # If at the beginning of a non abstract/native method
                            print(line, end='')
                            editing_method = True
                            inside_try_catch = False
                            jump_count = 0

                        elif line.startswith('.end method') and editing_method:
                            # If a the end of the method.
                            print(line, end='')
                            editing_method = False
                            inside_try_catch = False

                        elif editing_method:
                            # Inside method. Check if this line contains an op code at the beginning of the string.
                            match = op_code_pattern.match(line)
                            if match:
                                op_code = match.group('op_code')

                                # Check if we are entering or leaving a try-catch block of code.
                                if op_code.startswith(':try_start_'):
                                    print(line, end='')
                                    inside_try_catch = True
                                elif op_code.startswith(':try_end_'):
                                    print(line, end='')
                                    inside_try_catch = False

                                # If this is a valid op code, and we are not inside a try-catch block, mark this
                                # section with a special label that will be used later and invert the if conditions
                                # (if any).
                                elif op_code in op_codes and not inside_try_catch:
                                    jump_name = util.get_random_string(16)
                                    print('\tgoto/32 :l_{label}_{count}\n'.
                                          format(label=jump_name,
                                                 count=jump_count))
                                    print('\tnop\n')
                                    print('#!code_block!#')
                                    print('\t:l_{label}_{count}'.format(
                                        label=jump_name, count=jump_count))
                                    jump_count += 1

                                    new_if = self.if_mapping.get(op_code, None)
                                    if new_if:
                                        if_match = if_pattern.match(line)
                                        random_label_name = util.get_random_string(
                                            16)
                                        print(
                                            '\t{if_cond} {register}, :gl_{new_label}\n'
                                            .format(
                                                if_cond=new_if,
                                                register=if_match.group(
                                                    'register'),
                                                new_label=random_label_name))
                                        print('\tgoto/32 :{0}\n'.format(
                                            if_match.group('goto_label')))
                                        print('\t:gl_{0}'.format(
                                            random_label_name),
                                              end='')
                                    else:
                                        print(line, end='')
                                else:
                                    print(line, end='')
                            else:
                                print(line, end='')

                        else:
                            print(line, end='')

                # Reorder code blocks randomly.
                with util.inplace_edit_file(smali_file) as current_file:
                    editing_method = False
                    block_count = 0
                    code_blocks: List[CodeBlock] = []
                    current_code_block = None
                    for line in current_file:
                        if line.startswith('.method ') and ' abstract ' not in line and \
                                ' native ' not in line and not editing_method:
                            # If at the beginning of a non abstract/native method
                            print(line, end='')
                            editing_method = True
                            block_count = 0
                            code_blocks = []
                            current_code_block = None

                        elif line.startswith('.end method') and editing_method:
                            # If a the end of the method.
                            editing_method = False
                            random.shuffle(code_blocks)
                            for code_block in code_blocks:
                                print(code_block.smali_code, end='')
                            print(line, end='')

                        elif editing_method:
                            # Inside method. Check if this line is marked with a special label.
                            if line.startswith('#!code_block!#'):
                                block_count += 1
                                current_code_block = CodeBlock(block_count, '')
                                code_blocks.append(current_code_block)
                            else:
                                if block_count > 0 and current_code_block:
                                    current_code_block.add_smali_code_to_block(
                                        line)
                                else:
                                    print(line, end='')

                        else:
                            print(line, end='')

        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__)
예제 #16
0
 def test_get_smali_files(self, tmp_demo_apk_v10_original_path: str):
     obfuscation = Obfuscation(tmp_demo_apk_v10_original_path)
     smali_files = obfuscation.get_smali_files()
     assert len(smali_files) > 5
     assert all(os.path.isfile(smali_file) for smali_file in smali_files)
예제 #17
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            native_libs = obfuscation_info.get_native_lib_files()

            native_lib_invoke_pattern = re.compile(
                r'\s+invoke-static\s{(?P<invoke_pass>[vp0-9]+)},\s'
                r'Ljava/lang/System;->loadLibrary\(Ljava/lang/String;\)V')

            encrypted_libs: Set[str] = set()

            if native_libs:
                for smali_file in util.show_list_progress(
                        obfuscation_info.get_smali_files(),
                        interactive=obfuscation_info.interactive,
                        description='Encrypting native libraries'):
                    self.logger.debug(
                        'Replacing native libraries with encrypted native libraries '
                        '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 native library is loaded.
                    lib_index: List[int] = []

                    # Registers containing the strings with the names of the loaded libraries.
                    lib_register: List[str] = []

                    # Names of the loaded libraries.
                    lib_names: List[str] = []

                    editing_constructor = False
                    start_index = 0
                    for line_number, line in enumerate(lines):
                        if line.startswith(
                                '.method static constructor <clinit>()V'
                        ) and not editing_constructor:
                            # Entering static constructor.
                            editing_constructor = True
                            start_index = line_number

                        elif line.startswith(
                                '.end method') and editing_constructor:
                            # Only one static constructor per class.
                            break

                        elif editing_constructor:
                            # Inside static constructor.
                            invoke_match = native_lib_invoke_pattern.match(
                                line)
                            if invoke_match:
                                # Native library load instruction.
                                lib_index.append(line_number)
                                lib_register.append(
                                    invoke_match.group('invoke_pass'))

                    # Iterate the constructor lines backwards and for each library load instruction
                    # find the string containing the name of the loaded library.
                    for lib_number, index in enumerate(lib_index):
                        for line_number in range(index - 1, start_index, -1):
                            string_match = util.const_string_pattern.match(
                                lines[line_number])
                            if string_match and string_match.group(
                                    'register') == lib_register[lib_number]:
                                # Native library string declaration.
                                lib_names.append(string_match.group('string'))

                                # Change the library string since it won't be used anymore.
                                lines[line_number] = lines[
                                    line_number].replace(
                                        '"{0}"'.format(
                                            string_match.group('string')),
                                        '"removed"')

                                # Proceed with the next native library (if any).
                                break

                    # Remove current native library invocations (new invocations to the encrypted version
                    # of the libraries will be added later). The const-string references to the libraries
                    # are just renamed and not removed, to avoid errors in case there is a surrounding
                    # try/catch block.
                    lines = [
                        line for index, line in enumerate(lines)
                        if index not in lib_index
                    ]

                    # Insert invocations to the encrypted native libraries (if any).
                    if lib_names:
                        editing_method = False
                        after_invoke_super = False
                        for line in lines:
                            if line.startswith('.method protected attachBaseContext(Landroid/content/Context;)V') \
                                    and not editing_method:
                                # Entering method.
                                editing_method = True

                            elif line.startswith(
                                    '.end method') and editing_method:
                                # Only one method with this signature per class.
                                break

                            elif editing_method and not after_invoke_super:
                                # Inside method, before the call to the parent constructor.
                                # Look for the call to the parent constructor.
                                invoke_match = util.invoke_pattern.match(line)
                                if invoke_match and invoke_match.group(
                                        'invoke_type') == 'invoke-super':
                                    after_invoke_super = True

                            elif editing_method and after_invoke_super:
                                # Inside method, after the call to the parent constructor. We'll insert here
                                # the invocations of the encrypted native libraries.
                                for lib_name in lib_names:
                                    line += '\n\tconst-string/jumbo p0, "{name}"\n'.format(name=lib_name) + \
                                        '\n\tinvoke-static {p1, p0}, ' \
                                        'Lcom/decryptassetmanager/DecryptAsset;->' \
                                        'loadEncryptedLibrary(Landroid/content/Context;Ljava/lang/String;)V\n'

                        # No existing attachBaseContext method was found, we have to declare it.
                        if not editing_method:
                            # Look for the virtual methods section (if present, otherwise add it).
                            virtual_methods_line = next(
                                (line_number
                                 for line_number, line in enumerate(lines)
                                 if line.startswith('# virtual methods')),
                                None)
                            if not virtual_methods_line:
                                lines.append('\n# virtual methods')

                            lines.append(
                                '\n.method protected attachBaseContext(Landroid/content/Context;)V\n'
                                '\t.locals 0\n'
                                '\n\tinvoke-super {p0, p1}, '
                                'Landroid/support/v7/app/AppCompatActivity;->'
                                'attachBaseContext(Landroid/content/Context;)V\n'
                            )

                            for lib_name in lib_names:
                                lines.append(
                                    '\n\tconst-string/jumbo p0, "{name}"\n'.
                                    format(name=lib_name) +
                                    '\n\tinvoke-static {p1, p0}, '
                                    'Lcom/decryptassetmanager/DecryptAsset;->'
                                    'loadEncryptedLibrary(Landroid/content/Context;Ljava/lang/String;)V\n'
                                )

                            lines.append('\n\treturn-void' '\n.end method\n')

                        # Encrypt the native libraries used in code and put them in asset folder.
                        assets_dir = obfuscation_info.get_assets_directory()
                        os.makedirs(assets_dir, exist_ok=True)
                        for native_lib in native_libs:
                            for lib_name in lib_names:
                                if native_lib.endswith(
                                        '{0}.so'.format(lib_name)):
                                    arch = os.path.basename(
                                        os.path.dirname(native_lib))
                                    encrypted_lib_path = os.path.join(
                                        assets_dir,
                                        'lib,{arch},{lib_name}.so'.format(
                                            arch=arch, lib_name=lib_name))

                                    with open(native_lib,
                                              'rb') as native_lib_file:
                                        encrypted_lib = AES \
                                            .new(key=self.encryption_secret.encode(), mode=AES.MODE_ECB) \
                                            .encrypt(pad(native_lib_file.read(), AES.block_size))

                                    with open(encrypted_lib_path,
                                              'wb') as encrypted_lib_file:
                                        encrypted_lib_file.write(encrypted_lib)

                                    encrypted_libs.add(encrypted_lib_path)

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

                if not obfuscation_info.decrypt_asset_smali_file_added_flag and encrypted_libs:
                    # Add to the app the code for decrypting the encrypted native libraries. 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,
                                                    'DecryptAsset.smali')
                    with open(destination_file, 'w',
                              encoding='utf-8') as decrypt_asset_smali:
                        decrypt_asset_smali.write(
                            util.get_decrypt_asset_smali_code())
                        obfuscation_info.decrypt_asset_smali_file_added_flag = True

            else:
                self.logger.debug('No native libraries found')

        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__)
예제 #18
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            encrypted_strings: Set[str] = set()

            # .field <other_optional_stuff> <string_name>:Ljava/lang/String; = "<string_value>"
            static_string_pattern = re.compile(
                r'\.field.+?static.+?(?P<string_name>\S+?):'
                r'Ljava/lang/String;\s=\s"(?P<string_value>.+)"', re.UNICODE)

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

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

                class_name = None

                # Line numbers where a static string is declared.
                static_string_index: List[int] = []

                # Names of the static strings.
                static_string_name: List[str] = []

                # Values of the static strings.
                static_string_value: List[str] = []

                direct_methods_line = -1
                static_constructor_line = -1

                # Line numbers where a constant string is declared.
                string_index: List[int] = []

                # Registers containing the constant strings.
                string_register: List[str] = []

                # Values of the constant strings.
                string_value: List[str] = []

                current_local_count = 0
                for line_number, line in enumerate(lines):

                    if not class_name:
                        class_match = util.class_pattern.match(line)
                        if class_match:
                            class_name = class_match.group('class_name')
                            continue

                    if line.startswith('# direct methods'):
                        direct_methods_line = line_number
                        continue

                    if line.startswith(
                            '.method static constructor <clinit>()V'):
                        static_constructor_line = line_number
                        continue

                    static_string_match = static_string_pattern.match(line)
                    if static_string_match and static_string_match.group(
                            'string_value'):
                        # A static non empty string initialization was found.
                        static_string_index.append(line_number)
                        static_string_name.append(
                            static_string_match.group('string_name'))
                        static_string_value.append(
                            static_string_match.group('string_value'))

                    # 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. When we'll encounter a constant string later in the
                    # body of the method, we'll look at its register value and if it's greater than 15 we won't encrypt
                    # it (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

                    # If the constant string has 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.
                    string_match = util.const_string_pattern.match(line)
                    if string_match and string_match.group('string'):
                        reg_type = string_match.group('register')[:1]
                        reg_number = int(string_match.group('register')[1:])
                        if (reg_type == 'v' and reg_number <= 15) or (
                                reg_type == 'p'
                                and reg_number + current_local_count <= 15):
                            # A non empty string was found in a register <= 15.
                            string_index.append(line_number)
                            string_register.append(
                                string_match.group('register'))
                            string_value.append(string_match.group('string'))

                # Const string encryption.

                for string_number, index in enumerate(string_index):
                    lines[index] = '\tconst-string/jumbo {register}, "{enc_string}"\n' \
                                   '\n\tinvoke-static {{{register}}}, Lcom/decryptstringmanager/DecryptString' \
                                   ';->decryptString(Ljava/lang/String;)Ljava/lang/String;\n' \
                                   '\n\tmove-result-object {register}\n'.format(
                        register=string_register[string_number],
                        enc_string=self.encrypt_string(string_value[string_number]))

                    encrypted_strings.add(string_value[string_number])

                # Static string encryption.

                static_string_encryption_code = ''
                for string_number, index in enumerate(static_string_index):
                    # Remove the original initialization.
                    lines[index] = '{0}\n'.format(lines[index].split(' = ')[0])

                    # Initialize the static string from an encrypted string.
                    static_string_encryption_code += '\tconst-string/jumbo v0, "{enc_string}"\n' \
                        '\n\tinvoke-static {{v0}}, Lcom/decryptstringmanager/DecryptString' \
                        ';->decryptString(Ljava/lang/String;)Ljava/lang/String;\n' \
                        '\n\tmove-result-object v0\n' \
                        '\n\tsput-object v0, {class_name}->{string_name}:Ljava/lang/String;\n\n'.format(
                            enc_string=self.encrypt_string(static_string_value[string_number]),
                            class_name=class_name, string_name=static_string_name[string_number])

                    encrypted_strings.add(static_string_value[string_number])

                if static_constructor_line != -1:
                    # Add static string encryption to the existing static constructor.
                    local_match = util.locals_pattern.match(
                        lines[static_constructor_line + 1])
                    if local_match:
                        # At least one register is needed.
                        local_count = int(local_match.group('local_count'))
                        if local_count == 0:
                            lines[static_constructor_line +
                                  1] = '\t.locals 1\n'
                        lines[static_constructor_line + 2] = '\n{0}'.format(
                            static_string_encryption_code)
                else:
                    # Add a new static constructor for the static string encryption.
                    if direct_methods_line != -1:
                        new_constructor_line = direct_methods_line
                    else:
                        new_constructor_line = len(lines) - 1

                    lines[new_constructor_line] = '{original}' \
                        '.method static constructor <clinit>()V\n' \
                        '\t.locals 1\n\n' \
                        '{encryption_code}' \
                        '\treturn-void\n' \
                        '.end method\n\n'.format(original=lines[new_constructor_line],
                                                 encryption_code=static_string_encryption_code)

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

            if not obfuscation_info.decrypt_string_smali_file_added_flag and encrypted_strings:
                # 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())
                    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__)
예제 #19
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:
            native_libs = obfuscation_info.get_native_lib_files()

            native_lib_invoke_pattern = re.compile(
                r"\s+invoke-static\s{(?P<invoke_pass>[vp0-9]+)},\s"
                r"Ljava/lang/System;->loadLibrary\(Ljava/lang/String;\)V"
            )

            encrypted_to_original_mapping: Dict[str, str] = {}

            if native_libs:
                for smali_file in util.show_list_progress(
                    obfuscation_info.get_smali_files(),
                    interactive=obfuscation_info.interactive,
                    description="Encrypting native libraries",
                ):
                    self.logger.debug(
                        "Replacing native libraries with encrypted native libraries "
                        'in file "{0}"'.format(smali_file)
                    )

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

                    class_name = None

                    local_count = 16

                    # Names of the loaded libraries.
                    lib_names: List[str] = []

                    editing_constructor = False
                    start_index = 0
                    for line_number, line in enumerate(lines):

                        if not class_name:
                            class_match = util.class_pattern.match(line)
                            if class_match:
                                class_name = class_match.group("class_name")
                                continue

                        # Native libraries should be loaded inside static constructors.
                        if (
                            line.startswith(".method static constructor <clinit>()V")
                            and not editing_constructor
                        ):
                            # Entering static constructor.
                            editing_constructor = True
                            start_index = line_number + 1
                            local_match = util.locals_pattern.match(
                                lines[line_number + 1]
                            )
                            if local_match:
                                local_count = int(local_match.group("local_count"))
                                if local_count <= 15:
                                    # An additional register is needed for the
                                    # encryption.
                                    local_count += 1
                                    lines[line_number + 1] = "\t.locals {0}\n".format(
                                        local_count
                                    )
                                    continue

                            # For some reason the locals declaration was not found where
                            # it should be, so assume the local registers are all used
                            # (we can't add any instruction here).
                            break

                        elif line.startswith(".end method") and editing_constructor:
                            # Only one static constructor per class.
                            break

                        elif editing_constructor:
                            # Inside static constructor.
                            invoke_match = native_lib_invoke_pattern.match(line)
                            if invoke_match:
                                # Native library load instruction. Iterate the
                                # constructor lines backwards in order to find the
                                # string containing the name of the loaded library.
                                for l_num in range(line_number - 1, start_index, -1):
                                    string_match = util.const_string_pattern.match(
                                        lines[l_num]
                                    )
                                    if string_match and string_match.group(
                                        "register"
                                    ) == invoke_match.group("invoke_pass"):
                                        # Native library string declaration.
                                        lib_names.append(string_match.group("string"))

                                # Static constructors take no parameters, so the highest
                                # register is v<local_count - 1>.
                                lines[line_number] = (
                                    "\tconst-class v{class_register_num}, "
                                    "{class_name}\n\n"
                                    "\tinvoke-static {{v{class_register_num}, "
                                    "{original_register}}}, "
                                    "Lcom/decryptassetmanager/DecryptAsset;->"
                                    "loadEncryptedLibrary("
                                    "Ljava/lang/Class;Ljava/lang/String;)V\n".format(
                                        class_name=class_name,
                                        original_register=invoke_match.group(
                                            "invoke_pass"
                                        ),
                                        class_register_num=local_count - 1,
                                    )
                                )

                        # Encrypt the native libraries used in code and put them
                        # in assets folder.
                        assets_dir = obfuscation_info.get_assets_directory()
                        os.makedirs(assets_dir, exist_ok=True)
                        for native_lib in native_libs:
                            for lib_name in lib_names:
                                if native_lib.endswith("{0}.so".format(lib_name)):
                                    arch = os.path.basename(os.path.dirname(native_lib))
                                    encrypted_lib_path = os.path.join(
                                        assets_dir,
                                        "lib.{arch}.{lib_name}.so".format(
                                            arch=arch, lib_name=lib_name
                                        ),
                                    )

                                    with open(native_lib, "rb") as native_lib_file:
                                        encrypted_lib = AES.new(
                                            key=self.encryption_secret.encode(),
                                            mode=AES.MODE_ECB,
                                        ).encrypt(
                                            pad(native_lib_file.read(), AES.block_size)
                                        )

                                    with open(
                                        encrypted_lib_path, "wb"
                                    ) as encrypted_lib_file:
                                        encrypted_lib_file.write(encrypted_lib)

                                    encrypted_to_original_mapping[
                                        encrypted_lib_path
                                    ] = native_lib

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

                if (
                    not obfuscation_info.decrypt_asset_smali_file_added_flag
                    and encrypted_to_original_mapping
                ):
                    # Add to the app the code for decrypting the encrypted native
                    # libraries. 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, "DecryptAsset.smali"
                    )
                    with open(
                        destination_file, "w", encoding="utf-8"
                    ) as decrypt_asset_smali:
                        decrypt_asset_smali.write(
                            util.get_decrypt_asset_smali_code(self.encryption_secret)
                        )
                        obfuscation_info.decrypt_asset_smali_file_added_flag = True

                # Remove the original native libraries that were encrypted (the
                # encrypted ones will be used instead).
                for _, original_lib in encrypted_to_original_mapping.items():
                    try:
                        os.remove(original_lib)
                    except OSError as e:
                        self.logger.warning(
                            'Unable to delete native library "{0}": {1}'.format(
                                original_lib, e
                            )
                        )

            else:
                self.logger.debug("No native libraries found")

        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__)
예제 #20
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            op_codes = util.get_code_block_valid_op_codes()
            op_code_pattern = re.compile(r"\s+(?P<op_code>\S+)")
            if_pattern = re.compile(
                r"\s+(?P<if_op_code>\S+)"
                r"\s(?P<register>[vp0-9,\s]+?),\s:(?P<goto_label>\S+)")

            for smali_file in util.show_list_progress(
                    obfuscation_info.get_smali_files(),
                    interactive=obfuscation_info.interactive,
                    description="Code reordering",
            ):
                self.logger.debug(
                    'Reordering code in file "{0}"'.format(smali_file))
                with util.inplace_edit_file(smali_file) as (in_file, out_file):
                    editing_method = False
                    inside_try_catch = False
                    jump_count = 0
                    for line in in_file:
                        if (line.startswith(".method ")
                                and " abstract " not in line
                                and " native " not in line
                                and not editing_method):
                            # If at the beginning of a non abstract/native method
                            out_file.write(line)
                            editing_method = True
                            inside_try_catch = False
                            jump_count = 0

                        elif line.startswith(".end method") and editing_method:
                            # If a the end of the method.
                            out_file.write(line)
                            editing_method = False
                            inside_try_catch = False

                        elif editing_method:
                            # Inside method. Check if this line contains an op code at
                            # the beginning of the string.
                            match = op_code_pattern.match(line)
                            if match:
                                op_code = match.group("op_code")

                                # Check if we are entering or leaving a try-catch
                                # block of code.
                                if op_code.startswith(":try_start_"):
                                    out_file.write(line)
                                    inside_try_catch = True
                                elif op_code.startswith(":try_end_"):
                                    out_file.write(line)
                                    inside_try_catch = False

                                # If this is a valid op code, and we are not inside a
                                # try-catch block, mark this section with a special
                                # label that will be used later and invert the if
                                # conditions (if any).
                                elif op_code in op_codes and not inside_try_catch:
                                    jump_name = util.get_random_string(16)
                                    out_file.write(
                                        "\tgoto/32 :l_{label}_{count}\n\n".
                                        format(label=jump_name,
                                               count=jump_count))
                                    out_file.write("\tnop\n\n")
                                    out_file.write("#!code_block!#\n")
                                    out_file.write(
                                        "\t:l_{label}_{count}\n".format(
                                            label=jump_name, count=jump_count))
                                    jump_count += 1

                                    new_if = self.if_mapping.get(op_code, None)
                                    if new_if:
                                        if_match = if_pattern.match(line)
                                        random_label_name = util.get_random_string(
                                            16)
                                        out_file.write(
                                            "\t{if_cond} {register}, "
                                            ":gl_{new_label}\n\n".format(
                                                if_cond=new_if,
                                                register=if_match.group(
                                                    "register"),
                                                new_label=random_label_name,
                                            ))
                                        out_file.write(
                                            "\tgoto/32 :{0}\n\n".format(
                                                if_match.group("goto_label")))
                                        out_file.write("\t:gl_{0}".format(
                                            random_label_name))
                                    else:
                                        out_file.write(line)
                                else:
                                    out_file.write(line)
                            else:
                                out_file.write(line)

                        else:
                            out_file.write(line)

                # Reorder code blocks randomly.
                with util.inplace_edit_file(smali_file) as (in_file, out_file):
                    editing_method = False
                    block_count = 0
                    code_blocks: List[CodeBlock] = []
                    current_code_block = None
                    for line in in_file:
                        if (line.startswith(".method ")
                                and " abstract " not in line
                                and " native " not in line
                                and not editing_method):
                            # If at the beginning of a non abstract/native method
                            out_file.write(line)
                            editing_method = True
                            block_count = 0
                            code_blocks = []
                            current_code_block = None

                        elif line.startswith(".end method") and editing_method:
                            # If a the end of the method.
                            editing_method = False
                            random.shuffle(code_blocks)
                            for code_block in code_blocks:
                                out_file.write(code_block.smali_code)
                            out_file.write(line)

                        elif editing_method:
                            # Inside method. Check if this line is marked with
                            # a special label.
                            if line.startswith("#!code_block!#"):
                                block_count += 1
                                current_code_block = CodeBlock(block_count, "")
                                code_blocks.append(current_code_block)
                            else:
                                if block_count > 0 and current_code_block:
                                    current_code_block.add_smali_code_to_block(
                                        line)
                                else:
                                    out_file.write(line)

                        else:
                            out_file.write(line)

        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__)
예제 #21
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            native_libs = obfuscation_info.get_native_lib_files()

            native_lib_invoke_pattern = re.compile(
                r'\s+invoke-static\s{(?P<invoke_pass>[vp0-9]+)},\s'
                r'Ljava/lang/System;->loadLibrary\(Ljava/lang/String;\)V')

            encrypted_libs: Set[str] = set()

            if native_libs:
                for smali_file in util.show_list_progress(
                        obfuscation_info.get_smali_files(),
                        interactive=obfuscation_info.interactive,
                        description='Encrypting native libraries'):
                    self.logger.debug(
                        'Replacing native libraries with encrypted native libraries '
                        'in file "{0}"'.format(smali_file))

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

                    class_name = None

                    local_count = 16

                    # Names of the loaded libraries.
                    lib_names: List[str] = []

                    editing_constructor = False
                    start_index = 0
                    for line_number, line in enumerate(lines):

                        if not class_name:
                            class_match = util.class_pattern.match(line)
                            if class_match:
                                class_name = class_match.group('class_name')
                                continue

                        # Native libraries should be loaded inside static constructors.
                        if line.startswith(
                                '.method static constructor <clinit>()V'
                        ) and not editing_constructor:
                            # Entering static constructor.
                            editing_constructor = True
                            start_index = line_number + 1
                            local_match = util.locals_pattern.match(
                                lines[line_number + 1])
                            if local_match:
                                local_count = int(
                                    local_match.group('local_count'))
                                if local_count <= 15:
                                    # An additional register is needed for the encryption.
                                    local_count += 1
                                    lines[line_number +
                                          1] = '\t.locals {0}\n'.format(
                                              local_count)
                                    continue

                            # For some reason the locals declaration was not found where it should be, so assume the
                            # local registers are all used (we can't add any instruction here).
                            break

                        elif line.startswith(
                                '.end method') and editing_constructor:
                            # Only one static constructor per class.
                            break

                        elif editing_constructor:
                            # Inside static constructor.
                            invoke_match = native_lib_invoke_pattern.match(
                                line)
                            if invoke_match:
                                # Native library load instruction. Iterate the constructor lines backwards in order to
                                # find the string containing the name of the loaded library.
                                for l_num in range(line_number - 1,
                                                   start_index, -1):
                                    string_match = util.const_string_pattern.match(
                                        lines[l_num])
                                    if string_match and \
                                            string_match.group('register') == invoke_match.group('invoke_pass'):
                                        # Native library string declaration.
                                        lib_names.append(
                                            string_match.group('string'))

                                # Static constructors take no parameters, so the highest register is v<local_count - 1>.
                                lines[line_number] = '\tconst-class v{class_register_num}, {class_name}\n\n' \
                                    '\tinvoke-static {{v{class_register_num}, {original_register}}}, ' \
                                    'Lcom/decryptassetmanager/DecryptAsset;->loadEncryptedLibrary(' \
                                    'Ljava/lang/Class;Ljava/lang/String;)V\n'.format(
                                        class_name=class_name, original_register=invoke_match.group('invoke_pass'),
                                        class_register_num=local_count - 1)

                        # Encrypt the native libraries used in code and put them in assets folder.
                        assets_dir = obfuscation_info.get_assets_directory()
                        os.makedirs(assets_dir, exist_ok=True)
                        for native_lib in native_libs:
                            for lib_name in lib_names:
                                if native_lib.endswith(
                                        '{0}.so'.format(lib_name)):
                                    arch = os.path.basename(
                                        os.path.dirname(native_lib))
                                    encrypted_lib_path = os.path.join(
                                        assets_dir,
                                        'lib.{arch}.{lib_name}.so'.format(
                                            arch=arch, lib_name=lib_name))

                                    with open(native_lib,
                                              'rb') as native_lib_file:
                                        encrypted_lib = AES \
                                            .new(key=self.encryption_secret.encode(), mode=AES.MODE_ECB) \
                                            .encrypt(pad(native_lib_file.read(), AES.block_size))

                                    with open(encrypted_lib_path,
                                              'wb') as encrypted_lib_file:
                                        encrypted_lib_file.write(encrypted_lib)

                                    encrypted_libs.add(encrypted_lib_path)

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

                if not obfuscation_info.decrypt_asset_smali_file_added_flag and encrypted_libs:
                    # Add to the app the code for decrypting the encrypted native libraries. 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,
                                                    'DecryptAsset.smali')
                    with open(destination_file, 'w',
                              encoding='utf-8') as decrypt_asset_smali:
                        decrypt_asset_smali.write(
                            util.get_decrypt_asset_smali_code())
                        obfuscation_info.decrypt_asset_smali_file_added_flag = True

                # Remove the original native libraries (the encrypted ones will be used instead).
                for native_lib in native_libs:
                    try:
                        os.remove(native_lib)
                    except OSError as e:
                        self.logger.warning(
                            'Unable to delete native library "{0}": {1}'.
                            format(native_lib, e))

            else:
                self.logger.debug('No native libraries found')

        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__)
예제 #22
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            dangerous_api: Set[str] = set(util.get_dangerous_api())

            obfuscator_smali_code: str = ""

            move_result_pattern = re.compile(
                r"\s+move-result.*?\s(?P<register>[vp0-9]+)")

            for smali_file in util.show_list_progress(
                    obfuscation_info.get_smali_files(),
                    interactive=obfuscation_info.interactive,
                    description="Obfuscating dangerous APIs using reflection",
            ):
                self.logger.debug(
                    'Obfuscating dangerous APIs using reflection in file "{0}"'
                    .format(smali_file))

                # There is no space for further reflection instructions.
                if (self.obfuscator_instructions_length >=
                        self.obfuscator_instructions_limit):
                    break

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

                # Line numbers where a method is declared.
                method_index: List[int] = []

                # For each method in method_index, True if there are enough registers
                # to perform some operations by using reflection, False otherwise.
                method_is_reflectable: List[bool] = []

                # The number of local registers of each method in method_index.
                method_local_count: List[int] = []

                # Find the method declarations in this smali file.
                for line_number, line in enumerate(lines):
                    method_match = util.method_pattern.match(line)
                    if method_match:
                        method_index.append(line_number)

                        param_count = self.count_needed_registers(
                            self.split_method_params(
                                method_match.group("method_param")))

                        # Save the number of local registers of this method.
                        local_count = 16
                        local_match = util.locals_pattern.match(
                            lines[line_number + 1])
                        if local_match:
                            local_count = int(local_match.group("local_count"))
                            method_local_count.append(local_count)
                        else:
                            # For some reason the locals declaration was not found where
                            # it should be, so assume the local registers are all used.
                            method_local_count.append(local_count)

                        # If there are enough registers available we can perform some
                        # reflection operations.
                        if param_count + local_count <= 11:
                            method_is_reflectable.append(True)
                        else:
                            method_is_reflectable.append(False)

                # Look for method invocations of dangerous APIs inside the methods
                # declared in this smali file and change normal invocations with
                # invocations through reflection.
                for method_number, index in enumerate(method_index):

                    # If there are enough registers for reflection operations, look for
                    # method invocations inside each method's body.
                    if method_is_reflectable[method_number]:
                        current_line_number = index
                        while not lines[current_line_number].startswith(
                                ".end method"):

                            # There is no space for further reflection instructions.
                            if (self.obfuscator_instructions_length >=
                                    self.obfuscator_instructions_limit):
                                break

                            current_line_number += 1

                            invoke_match = util.invoke_pattern.match(
                                lines[current_line_number])
                            if invoke_match:
                                method = (
                                    "{class_name}->{method_name}"
                                    "({method_param}){method_return}".format(
                                        class_name=invoke_match.group(
                                            "invoke_object"),
                                        method_name=invoke_match.group(
                                            "invoke_method"),
                                        method_param=invoke_match.group(
                                            "invoke_param"),
                                        method_return=invoke_match.group(
                                            "invoke_return"),
                                    ))

                                # Use reflection only if this method belongs to
                                # dangerous APIs.
                                if method not in dangerous_api:
                                    continue

                                if (invoke_match.group("invoke_type") ==
                                        "invoke-virtual"):
                                    tmp_is_virtual = True
                                elif (invoke_match.group("invoke_type") ==
                                      "invoke-static"):
                                    tmp_is_virtual = False
                                else:
                                    continue

                                tmp_register = invoke_match.group(
                                    "invoke_pass")
                                tmp_class_name = invoke_match.group(
                                    "invoke_object")
                                tmp_method = invoke_match.group(
                                    "invoke_method")
                                tmp_param = invoke_match.group("invoke_param")
                                tmp_return_type = invoke_match.group(
                                    "invoke_return")

                                # Check if the method invocation result is used in the
                                # following lines.
                                for move_result_index in range(
                                        current_line_number + 1,
                                        min(current_line_number + 10,
                                            len(lines) - 1),
                                ):
                                    if "invoke-" in lines[move_result_index]:
                                        # New method invocation, the previous method
                                        # result is not used.
                                        break

                                    move_result_match = move_result_pattern.match(
                                        lines[move_result_index])
                                    if move_result_match:
                                        tmp_result_register = move_result_match.group(
                                            "register")

                                        # Fix the move-result instruction after the
                                        # method invocation.
                                        new_move_result = ""
                                        if tmp_return_type in self.primitive_types:
                                            new_move_result += (
                                                "\tmove-result-object "
                                                "{result_register}\n\n"
                                                "\tcheck-cast {result_register}, "
                                                "{result_class}\n\n".format(
                                                    result_register=
                                                    tmp_result_register,
                                                    result_class=self.
                                                    type_dict[tmp_return_type],
                                                ))

                                            new_move_result += "\tinvoke-virtual " "{{{result_register}}}, {cast}\n\n".format(
                                                result_register=
                                                tmp_result_register,
                                                cast=self.reverse_cast_dict[
                                                    tmp_return_type],
                                            )

                                            if (tmp_return_type == "J"
                                                    or tmp_return_type == "D"):
                                                new_move_result += (
                                                    "\tmove-result-wide "
                                                    "{result_register}\n".
                                                    format(
                                                        result_register=
                                                        tmp_result_register))
                                            else:
                                                new_move_result += (
                                                    "\tmove-result "
                                                    "{result_register}\n".
                                                    format(
                                                        result_register=
                                                        tmp_result_register))

                                        else:
                                            new_move_result += (
                                                "\tmove-result-object "
                                                "{result_register}\n\n"
                                                "\tcheck-cast {result_register}, "
                                                "{return_type}\n".format(
                                                    result_register=
                                                    tmp_result_register,
                                                    return_type=tmp_return_type,
                                                ))

                                        lines[
                                            move_result_index] = new_move_result

                                # Add the original method to the list of methods using
                                # reflection.
                                obfuscator_smali_code += self.add_smali_reflection_code(
                                    tmp_class_name, tmp_method, tmp_param)

                                # Change the original code with code using reflection.
                                lines[
                                    current_line_number] = self.create_reflection_method(
                                        self.methods_with_reflection,
                                        method_local_count[method_number],
                                        tmp_is_virtual,
                                        tmp_register,
                                        tmp_param,
                                    )

                                self.methods_with_reflection += 1

                                # Add the registers needed for performing reflection.
                                lines[index + 1] = "\t.locals {0}\n".format(
                                    method_local_count[method_number] + 4)

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

            # Add to the app the code needed for the reflection obfuscator. The code
            # 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,
                                            "AdvancedApiReflection.smali")
            with open(destination_file, "w",
                      encoding="utf-8") as api_reflection_smali:
                reflection_code = util.get_advanced_api_reflection_smali_code(
                ).replace("#!code_to_replace!#", obfuscator_smali_code)
                api_reflection_smali.write(reflection_code)

        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__)
예제 #23
0
    def obfuscate(self, obfuscation_info: Obfuscation):
        self.logger.info('Running "{0}" obfuscator'.format(
            self.__class__.__name__))

        try:
            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

            obfuscator_smali_code: str = ''

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

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

                # There is no space for further reflection instructions.
                if self.obfuscator_instructions_length >= self.obfuscator_instructions_limit:
                    break

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

                # Line numbers where a method is declared.
                method_index: List[int] = []

                # For each method in method_index, True if there are enough registers to perform some
                # operations by using reflection, False otherwise.
                method_is_reflectable: List[bool] = []

                # The number of local registers of each method in method_index.
                method_local_count: List[int] = []

                # Find the method declarations in this smali file.
                for line_number, line in enumerate(lines):
                    method_match = util.method_pattern.match(line)
                    if method_match:
                        method_index.append(line_number)

                        param_count = self.count_needed_registers(
                            self.split_method_params(
                                method_match.group('method_param')))

                        # Save the number of local registers of this method.
                        local_count = 16
                        local_match = util.locals_pattern.match(
                            lines[line_number + 1])
                        if local_match:
                            local_count = int(local_match.group('local_count'))
                            method_local_count.append(local_count)
                        else:
                            # For some reason the locals declaration was not found where it should be, so assume the
                            # local registers are all used.
                            method_local_count.append(local_count)

                        # If there are enough registers available we can perform some reflection operations.
                        if param_count + local_count <= 11:
                            method_is_reflectable.append(True)
                        else:
                            method_is_reflectable.append(False)

                # Look for method invocations inside the methods declared in this smali file and change normal
                # invocations with invocations through reflection.
                for method_number, index in enumerate(method_index):

                    # If there are enough registers for reflection operations, look for method invocations inside
                    # each method's body.
                    if method_is_reflectable[method_number]:
                        current_line_number = index
                        while not lines[current_line_number].startswith(
                                '.end method'):

                            # There is no space for further reflection instructions.
                            if self.obfuscator_instructions_length >= self.obfuscator_instructions_limit:
                                break

                            current_line_number += 1

                            invoke_match = util.invoke_pattern.match(
                                lines[current_line_number])

                            if invoke_match and '<init>' not in lines[
                                    current_line_number]:

                                # The method belongs to an Android class or is invoked on an array.
                                if invoke_match.group('invoke_object') in self.android_class_names or \
                                        invoke_match.group('invoke_object').startswith('['):
                                    continue

                                method_signature = '{method_name}({method_param}){method_return}'.format(
                                    method_name=invoke_match.group(
                                        'invoke_method'),
                                    method_param=invoke_match.group(
                                        'invoke_param'),
                                    method_return=invoke_match.group(
                                        'invoke_return'))

                                # The method to reflect has to be public, has to be declared in a public class and all
                                # its parameters have to be public.
                                if not self.method_is_all_public(
                                        invoke_match.group('invoke_object'),
                                        method_signature,
                                        invoke_match.group('invoke_param')):
                                    continue

                                if invoke_match.group(
                                        'invoke_type') == 'invoke-virtual':
                                    tmp_is_virtual = True
                                elif invoke_match.group(
                                        'invoke_type') == 'invoke-static':
                                    tmp_is_virtual = False
                                else:
                                    continue

                                tmp_register = invoke_match.group(
                                    'invoke_pass')
                                tmp_class_name = invoke_match.group(
                                    'invoke_object')
                                tmp_method = invoke_match.group(
                                    'invoke_method')
                                tmp_param = invoke_match.group('invoke_param')
                                tmp_return_type = invoke_match.group(
                                    'invoke_return')

                                # Check if the method invocation result is used in the following lines.
                                for move_result_index in range(
                                        current_line_number + 1,
                                        min(current_line_number + 10,
                                            len(lines) - 1)):
                                    if 'invoke-' in lines[move_result_index]:
                                        # New method invocation, the previous method result is not used.
                                        break

                                    move_result_match = move_result_pattern.match(
                                        lines[move_result_index])
                                    if move_result_match:
                                        tmp_result_register = move_result_match.group(
                                            'register')

                                        # Fix the move-result instruction after the method invocation.
                                        new_move_result = ''
                                        if tmp_return_type in self.primitive_types:
                                            new_move_result += '\tmove-result-object {result_register}\n\n' \
                                                               '\tcheck-cast {result_register}, {result_class}\n\n' \
                                                .format(result_register=tmp_result_register,
                                                        result_class=self.type_dict[tmp_return_type])

                                            new_move_result += '\tinvoke-virtual {{{result_register}}}, {cast}\n\n' \
                                                .format(result_register=tmp_result_register,
                                                        cast=self.reverse_cast_dict[tmp_return_type])

                                            if tmp_return_type == 'J' or tmp_return_type == 'D':
                                                new_move_result += '\tmove-result-wide {result_register}\n'.format(
                                                    result_register=
                                                    tmp_result_register)
                                            else:
                                                new_move_result += '\tmove-result {result_register}\n'.format(
                                                    result_register=
                                                    tmp_result_register)

                                        else:
                                            new_move_result += '\tmove-result-object {result_register}\n\n' \
                                                               '\tcheck-cast {result_register}, {return_type}\n' \
                                                .format(result_register=tmp_result_register,
                                                        return_type=tmp_return_type)

                                        lines[
                                            move_result_index] = new_move_result

                                # Add the original method to the list of methods using reflection.
                                obfuscator_smali_code += self.add_smali_reflection_code(
                                    tmp_class_name, tmp_method, tmp_param)

                                # Change the original code with code using reflection.
                                lines[
                                    current_line_number] = self.create_reflection_method(
                                        self.methods_with_reflection,
                                        method_local_count[method_number],
                                        tmp_is_virtual, tmp_register,
                                        tmp_param)

                                self.methods_with_reflection += 1

                                # Add the registers needed for performing reflection.
                                lines[index + 1] = '\t.locals {0}\n'.format(
                                    method_local_count[method_number] + 4)

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

            # Add to the app the code needed for the reflection obfuscator. The code
            # 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,
                                            'ApiReflection.smali')
            with open(destination_file, 'w',
                      encoding='utf-8') as api_reflection_smali:
                reflection_code = util.get_api_reflection_smali_code().replace(
                    '#!code_to_replace!#', obfuscator_smali_code)
                api_reflection_smali.write(reflection_code)

        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__)