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