Esempio n. 1
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__)
Esempio n. 2
0
 def rename_method_invocations(self, smali_files: List[str], methods_to_rename: Set[str],
                               android_class_names: Set[str], interactive: bool = False):
     for smali_file in util.show_list_progress(smali_files,
                                               interactive=interactive,
                                               description='Renaming method invocations'):
         with util.inplace_edit_file(smali_file) as current_file:
             for line in current_file:
                 # Method invocation.
                 invoke_match = util.invoke_pattern.match(line)
                 if invoke_match:
                     method = '{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')
                     )
                     invoke_type = invoke_match.group('invoke_type')
                     class_name = invoke_match.group('invoke_object')
                     # Rename the method invocation only if is direct or static (we are renaming only direct methods)
                     # and if is called from a class that is not an Android API class.
                     if ('direct' in invoke_type or 'static' in invoke_type) and method in methods_to_rename and \
                             class_name not in android_class_names:
                         method_name = invoke_match.group('invoke_method')
                         print(line.replace(
                             '{0}('.format(method_name),
                             '{0}('.format(self.rename_method(method_name))
                         ), end='')
                     else:
                         print(line, end='')
                 else:
                     print(line, end='')
Esempio n. 3
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__)
Esempio n. 4
0
    def rename_class_usages_in_smali(self, smali_files: List[str], rename_transformations: dict,
                                     interactive: bool = False):
        dot_rename_transformations = self.slash_to_dot_notation_for_classes(rename_transformations)

        # Add package name.
        dot_rename_transformations[self.package_name] = self.encrypted_package_name

        for smali_file in util.show_list_progress(smali_files,
                                                  interactive=interactive,
                                                  description='Renaming class usages in smali files'):
            with util.inplace_edit_file(smali_file) as current_file:
                for line in current_file:
                    # Rename classes used as strings with . instead of /.
                    string_match = self.string_pattern.search(line)
                    if string_match and string_match.group('string_value') in dot_rename_transformations:
                        line = line.replace(string_match.group('string_value'),
                                            dot_rename_transformations[string_match.group('string_value')])

                    # Sometimes classes are used in annotations as strings without trailing ;
                    if string_match and '{0};'.format(string_match.group('string_value')) in rename_transformations:
                        line = line.replace(
                            string_match.group('string_value'),
                            rename_transformations['{0};'.format(string_match.group('string_value'))][:-1])

                    # Rename classes used with the "classic" syntax (leading L and trailing ;).
                    class_names = util.class_name_pattern.findall(line)
                    for class_name in class_names:
                        if class_name in rename_transformations:
                            line = line.replace(class_name, rename_transformations[class_name])

                    print(line, end='')
Esempio n. 5
0
 def rename_field_references(self,
                             fields_to_rename: Set[str],
                             smali_files: List[str],
                             sdk_classes: Set[str],
                             interactive: bool = False):
     for smali_file in util.show_list_progress(
             smali_files,
             interactive=interactive,
             description='Renaming field references'):
         with util.inplace_edit_file(smali_file) as current_smali_file:
             for line in current_smali_file:
                 # Field usage.
                 field_usage_match = util.field_usage_pattern.match(line)
                 if field_usage_match:
                     field = '{field_name}:{field_type}'.format(
                         field_name=field_usage_match.group('field_name'),
                         field_type=field_usage_match.group('field_type'))
                     class_name = field_usage_match.group('field_object')
                     field_name = field_usage_match.group('field_name')
                     if field in fields_to_rename and \
                             (not class_name.startswith(('Landroid', 'Ljava')) or class_name in sdk_classes):
                         # Rename field usage.
                         print(line.replace(
                             '{0}:'.format(field_name),
                             '{0}:'.format(self.rename_field(field_name))),
                               end='')
                     else:
                         print(line, end='')
                 else:
                     print(line, end='')
Esempio n. 6
0
    def add_method_overloads(
        self,
        smali_files: List[str],
        methods_to_ignore: Set[str],
        max_methods_to_add: int,
        interactive: bool = False,
    ):

        overloaded_method_body = util.get_smali_method_overload()
        added_methods = 0

        for smali_file in util.show_list_progress(
                smali_files,
                interactive=interactive,
                description="Inserting method overloads in smali files",
        ):
            self.logger.debug(
                'Inserting method overloads in file "{0}"'.format(smali_file))
            if added_methods < max_methods_to_add:
                added_methods += self.add_method_overloads_to_file(
                    smali_file, overloaded_method_body, methods_to_ignore)
            else:
                break

        self.logger.debug(
            "{0} new overloaded methods were added".format(added_methods))
Esempio n. 7
0
    def rename_class_usages_in_xml(self, xml_files: List[str], rename_transformations: dict,
                                   interactive: bool = False):
        dot_rename_transformations = self.slash_to_dot_notation_for_classes(rename_transformations)

        # Add package name.
        dot_rename_transformations[self.package_name] = self.encrypted_package_name

        for xml_file in util.show_list_progress(xml_files,
                                                interactive=interactive,
                                                description='Renaming class usages in xml files'):
            with open(xml_file, 'r', encoding='utf-8') as current_file:
                file_content = current_file.read()

            # Replace strings from longest to shortest (to avoid replacing partial strings).
            for old_name in sorted(dot_rename_transformations, reverse=True, key=lambda x: len(x)):
                file_content = file_content.replace(old_name, dot_rename_transformations[old_name])

                # Activity without package name (".ActivityName")
                if '"{0}"'.format(old_name.replace(self.package_name, '')) in file_content:
                    file_content = file_content.replace(
                        '"{0}"'.format(old_name.replace(self.package_name, '')),
                        '"{0}"'.format(dot_rename_transformations[old_name].replace(self.encrypted_package_name, '')))

            with open(xml_file, 'w', encoding='utf-8') as current_file:
                current_file.write(file_content)
Esempio n. 8
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__)
Esempio n. 9
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__)
Esempio n. 10
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 "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__)
Esempio n. 11
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__)
Esempio n. 12
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__)
Esempio n. 13
0
    def rename_field_declarations(self,
                                  smali_files: List[str],
                                  interactive: bool = False) -> Set[str]:
        renamed_fields: Set[str] = set()

        # Search for field definitions that can be renamed.
        for smali_file in util.show_list_progress(
                smali_files,
                interactive=interactive,
                description="Renaming field declarations",
        ):
            with util.inplace_edit_file(smali_file) as (in_file, out_file):
                for line in in_file:
                    # Field declared in class.
                    field_match = util.field_pattern.match(line)

                    if field_match:
                        field_name = field_match.group("field_name")
                        # Avoid sub-fields.
                        if "$" not in field_name:
                            # Rename field declaration (usages of this field will be
                            # renamed later) and add some random fields.
                            line = line.replace(
                                "{0}:".format(field_name),
                                "{0}:".format(self.rename_field(field_name)),
                            )
                            out_file.write(line)

                            # Add random fields.
                            if self.added_fields < self.max_fields_to_add:
                                for _ in range(util.get_random_int(1, 4)):
                                    out_file.write("\n")
                                    out_file.write(
                                        line.replace(
                                            ":",
                                            "{0}:".format(
                                                util.get_random_string(8)),
                                        ))
                                    self.added_fields += 1

                            field = "{field_name}:{field_type}".format(
                                field_name=field_match.group("field_name"),
                                field_type=field_match.group("field_type"),
                            )
                            renamed_fields.add(field)
                        else:
                            out_file.write(line)
                    else:
                        out_file.write(line)

        return renamed_fields
Esempio n. 14
0
    def add_call_indirections(self, smali_files: List[str], max_methods_to_add: int, interactive: bool = False):
        added_methods = 0
        for smali_file in util.show_list_progress(smali_files,
                                                  interactive=interactive,
                                                  description='Inserting call indirections in smali files'):
            self.logger.debug('Inserting call indirections in file "{0}"'.format(smali_file))
            if added_methods < max_methods_to_add:
                with StringIO() as new_method:
                    self.update_method(smali_file, new_method)
                    self.add_method(smali_file, new_method)
                    added_methods += self.get_declared_method_number_in_text(new_method.getvalue())
            else:
                break

        self.logger.debug('{0} new methods were added'.format(added_methods))
Esempio n. 15
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__)
Esempio n. 16
0
 def rename_method_invocations(
     self,
     smali_files: List[str],
     methods_to_rename: Set[str],
     interactive: bool = False,
 ):
     for smali_file in util.show_list_progress(
         smali_files,
         interactive=interactive,
         description="Renaming method invocations",
     ):
         with util.inplace_edit_file(smali_file) as (in_file, out_file):
             for line in in_file:
                 # Method invocation.
                 invoke_match = util.invoke_pattern.match(line)
                 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"),
                         )
                     )
                     invoke_type = invoke_match.group("invoke_type")
                     # Rename the method invocation only if is direct or static (we
                     # are renaming only direct methods). The list of methods to
                     # rename already contains the class name of each method, since
                     # here we have a list of methods whose declarations were already
                     # renamed.
                     if (
                         "direct" in invoke_type or "static" in invoke_type
                     ) and method in methods_to_rename:
                         method_name = invoke_match.group("invoke_method")
                         out_file.write(
                             line.replace(
                                 "->{0}(".format(method_name),
                                 "->{0}(".format(self.rename_method(method_name)),
                             )
                         )
                     else:
                         out_file.write(line)
                 else:
                     out_file.write(line)
Esempio n. 17
0
 def rename_method_invocations(
     self,
     smali_files: List[str],
     methods_to_rename: Set[str],
     android_class_names: Set[str],
     interactive: bool = False,
 ):
     for smali_file in util.show_list_progress(
             smali_files,
             interactive=interactive,
             description="Renaming method invocations",
     ):
         with util.inplace_edit_file(smali_file) as (in_file, out_file):
             for line in in_file:
                 # Method invocation.
                 invoke_match = util.invoke_pattern.match(line)
                 if invoke_match:
                     method = "{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"),
                     )
                     invoke_type = invoke_match.group("invoke_type")
                     class_name = invoke_match.group("invoke_object")
                     # Rename the method invocation only if is direct or static (we
                     # are renaming only direct methods) and if is called from a
                     # class that is not an Android API class.
                     if (("direct" in invoke_type
                          or "static" in invoke_type)
                             and method in methods_to_rename
                             and class_name not in android_class_names):
                         method_name = invoke_match.group("invoke_method")
                         out_file.write(
                             line.replace(
                                 "{0}(".format(method_name),
                                 "{0}(".format(
                                     self.rename_method(method_name)),
                             ))
                     else:
                         out_file.write(line)
                 else:
                     out_file.write(line)
Esempio n. 18
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__)
Esempio n. 19
0
 def rename_field_references(
     self,
     fields_to_rename: Set[str],
     smali_files: List[str],
     sdk_classes: Set[str],
     interactive: bool = False,
 ):
     for smali_file in util.show_list_progress(
             smali_files,
             interactive=interactive,
             description="Renaming field references",
     ):
         with util.inplace_edit_file(smali_file) as (in_file, out_file):
             for line in in_file:
                 # Field usage.
                 field_usage_match = util.field_usage_pattern.match(line)
                 if field_usage_match:
                     field = "{field_name}:{field_type}".format(
                         field_name=field_usage_match.group("field_name"),
                         field_type=field_usage_match.group("field_type"),
                     )
                     class_name = field_usage_match.group("field_object")
                     field_name = field_usage_match.group("field_name")
                     if field in fields_to_rename and (
                             not class_name.startswith(
                                 ("Landroid", "Ljava"))
                             or class_name in sdk_classes):
                         # Rename field usage.
                         out_file.write(
                             line.replace(
                                 "{0}:".format(field_name),
                                 "{0}:".format(
                                     self.rename_field(field_name)),
                             ))
                     else:
                         out_file.write(line)
                 else:
                     out_file.write(line)
Esempio n. 20
0
    def rename_field_declarations(self,
                                  smali_files: List[str],
                                  interactive: bool = False) -> Set[str]:
        renamed_fields: Set[str] = set()

        # Search for field definitions that can be renamed.
        for smali_file in util.show_list_progress(
                smali_files,
                interactive=interactive,
                description='Renaming field declarations'):
            with util.inplace_edit_file(smali_file) as current_smali_file:
                for line in current_smali_file:
                    # Field declared in class.
                    field_match = util.field_pattern.match(line)

                    if field_match:
                        field_name = field_match.group('field_name')
                        # Avoid sub-fields.
                        if '$' not in field_name:
                            # Rename field declaration (usages of this field will be renamed later) and add some
                            # random fields.
                            line = line.replace(
                                '{0}:'.format(field_name),
                                '{0}:'.format(self.rename_field(field_name)))
                            print(line, end='')
                            self.add_random_fields(line)

                            field = '{field_name}:{field_type}'.format(
                                field_name=field_match.group('field_name'),
                                field_type=field_match.group('field_type'))
                            renamed_fields.add(field)
                        else:
                            print(line, end='')
                    else:
                        print(line, end='')

        return renamed_fields
Esempio n. 21
0
def perform_obfuscation(
    input_apk_path: str,
    obfuscator_list: List[str],
    working_dir_path: str = None,
    obfuscated_apk_path: str = None,
    interactive: bool = False,
    ignore_libs: bool = False,
    virus_total_api_key: List[str] = None,
):
    """
    Apply the obfuscation techniques to an input application and generate an obfuscated
    apk file.

    :param input_apk_path: The path to the input application file to obfuscate.
    :param obfuscator_list: A list containing the names of the obfuscation techniques
                            to apply.
    :param working_dir_path: The working directory where to store the intermediate
                             files. By default a directory will be created in the same
                             directory as the input application. If the specified
                             directory doesn't exist, it will be created.
    :param obfuscated_apk_path: The path where to save the obfuscated apk file. By
                                default the file will be saved in the working directory.
    :param interactive: If True, show a progress bar with the obfuscation progress.
    :param ignore_libs: If True, exclude known third party libraries from the
                        obfuscation operations.
    :param virus_total_api_key: A list containing Virus Total API keys, needed only
                                when using Virus Total obfuscator.
    """

    check_external_tool_dependencies()

    if not os.path.isfile(input_apk_path):
        logger.critical(
            'Unable to find application file "{0}"'.format(input_apk_path))
        raise FileNotFoundError(
            'Unable to find application file "{0}"'.format(input_apk_path))

    obfuscation = Obfuscation(
        input_apk_path,
        working_dir_path,
        obfuscated_apk_path,
        interactive=interactive,
        ignore_libs=ignore_libs,
        virus_total_api_key=virus_total_api_key,
    )

    manager = ObfuscatorManager()
    obfuscator_name_to_obfuscator_object = {
        ob.name: ob.plugin_object
        for ob in manager.get_all_obfuscators()
    }
    obfuscator_name_to_function = {
        ob.name: ob.plugin_object.obfuscate
        for ob in manager.get_all_obfuscators()
    }
    valid_obfuscators = manager.get_obfuscators_names()

    # Check how many obfuscators in list will add new fields/methods.
    for obfuscator_name in obfuscator_list:
        # Make sure all the provided obfuscator names are valid.
        if obfuscator_name not in valid_obfuscators:
            raise ValueError(
                'There is no obfuscator named "{0}"'.format(obfuscator_name))
        if obfuscator_name_to_obfuscator_object[
                obfuscator_name].is_adding_fields:
            obfuscation.obfuscators_adding_fields += 1
        if obfuscator_name_to_obfuscator_object[
                obfuscator_name].is_adding_methods:
            obfuscation.obfuscators_adding_methods += 1

    obfuscator_progress = util.show_list_progress(
        obfuscator_list,
        interactive=interactive,
        unit="obfuscator",
        description="Running obfuscators",
    )

    for obfuscator_name in obfuscator_progress:
        try:
            if interactive:
                obfuscator_progress.set_description(
                    "Running obfuscators ({0})".format(obfuscator_name))
            (obfuscator_name_to_function[obfuscator_name])(obfuscation)
        except Exception as e:
            logger.critical("Error during obfuscation: {0}".format(e),
                            exc_info=True)
            raise
Esempio n. 22
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__)
Esempio n. 23
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__)
Esempio n. 24
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__)
Esempio n. 25
0
    def rename_class_declarations(self, smali_files: List[str], interactive: bool = False) -> dict:
        renamed_classes = {}

        # Search for class declarations that can be renamed.
        for smali_file in util.show_list_progress(smali_files,
                                                  interactive=interactive,
                                                  description='Renaming class declarations'):
            annotation_flag = False
            with util.inplace_edit_file(smali_file) as current_file:

                skip_remaining_lines = False
                class_name = None
                r_class = False
                for line in current_file:

                    if skip_remaining_lines:
                        print(line, end='')
                        continue

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

                            # Split class name to its components and encrypt them.
                            class_tokens = self.split_class_pattern.split(class_name[1:-1])

                            encrypted_class_name = 'L'
                            separator_index = 1
                            for token in class_tokens:
                                separator_index += len(token)
                                if token == 'R':
                                    r_class = True
                                if token.isdigit():
                                    encrypted_class_name += token + class_name[separator_index]
                                elif not r_class:
                                    encrypted_class_name += self.encrypt_identifier(token) + \
                                                            class_name[separator_index]
                                else:
                                    encrypted_class_name += token + class_name[separator_index]
                                separator_index += 1

                            print(line.replace(class_name, encrypted_class_name), end='')

                            renamed_classes[class_name] = encrypted_class_name
                            continue

                    if line.strip() == '.annotation system Ldalvik/annotation/InnerClass;':
                        annotation_flag = True
                        print(line, end='')
                        continue

                    if annotation_flag and 'name = "' in line:
                        # Subclasses have to be renamed as well.
                        subclass_match = self.subclass_name_pattern.match(line)
                        if subclass_match and not r_class:
                            subclass_name = subclass_match.group('subclass_name')
                            print(line.replace(subclass_name, self.encrypt_identifier(subclass_name)), end='')
                        else:
                            print(line, end='')
                        continue

                    if line.strip() == '.end annotation':
                        annotation_flag = False
                        print(line, end='')
                        continue

                    # Method declaration reached, no more class definitions in this file.
                    if line.startswith('.method '):
                        skip_remaining_lines = True
                        print(line, end='')
                    else:
                        print(line, end='')

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

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

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

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

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

            self.transform_package_name(manifest_root)

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

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

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

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

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

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

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

        finally:
            obfuscation_info.used_obfuscators.append(self.__class__.__name__)
Esempio n. 27
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__)
Esempio n. 28
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__)
Esempio n. 29
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__)
Esempio n. 30
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__)