def test_get_multidex_smali_files(self, tmp_demo_apk_v10_original_path: str): obfuscation = Obfuscation(tmp_demo_apk_v10_original_path) smali_files = obfuscation.get_multidex_smali_files() # This test application is not multidex. assert len(smali_files) == 0
def test_get_manifest_file(self, tmp_demo_apk_v10_original_path: str): obfuscation = Obfuscation(tmp_demo_apk_v10_original_path) manifest = obfuscation.get_manifest_file() assert os.path.isfile(manifest)
def test_get_smali_files(self, tmp_demo_apk_v10_original_path: str): obfuscation = Obfuscation(tmp_demo_apk_v10_original_path) smali_files = obfuscation.get_smali_files() assert len(smali_files) > 5 assert all(os.path.isfile(smali_file) for smali_file in smali_files)
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: for smali_file in util.show_list_progress( obfuscation_info.get_smali_files(), interactive=obfuscation_info.interactive, description="Class name to smali file mapping", ): with open(smali_file, "r", encoding="utf-8") as current_file: class_name = None for line in current_file: if not class_name: # Every smali file contains a class. class_match = util.class_pattern.match(line) if class_match: self.class_name_to_smali_file[ class_match.group( "class_name")] = smali_file break obfuscator_smali_code: str = "" move_result_pattern = re.compile( r"\s+move-result.*?\s(?P<register>[vp0-9]+)") for smali_file in util.show_list_progress( obfuscation_info.get_smali_files(), interactive=obfuscation_info.interactive, description="Obfuscating using reflection", ): self.logger.debug( 'Obfuscating using reflection in file "{0}"'.format( smali_file)) # There is no space for further reflection instructions. if (self.obfuscator_instructions_length >= self.obfuscator_instructions_limit): break with open(smali_file, "r", encoding="utf-8") as current_file: lines = current_file.readlines() # Line numbers where a method is declared. method_index: List[int] = [] # For each method in method_index, True if there are enough registers # to perform some operations by using reflection, False otherwise. method_is_reflectable: List[bool] = [] # The number of local registers of each method in method_index. method_local_count: List[int] = [] # Find the method declarations in this smali file. for line_number, line in enumerate(lines): method_match = util.method_pattern.match(line) if method_match: method_index.append(line_number) param_count = self.count_needed_registers( self.split_method_params( method_match.group("method_param"))) # Save the number of local registers of this method. local_count = 16 local_match = util.locals_pattern.match( lines[line_number + 1]) if local_match: local_count = int(local_match.group("local_count")) method_local_count.append(local_count) else: # For some reason the locals declaration was not found where # it should be, so assume the local registers are all used. method_local_count.append(local_count) # If there are enough registers available we can perform some # reflection operations. if param_count + local_count <= 11: method_is_reflectable.append(True) else: method_is_reflectable.append(False) # Look for method invocations inside the methods declared in this # smali file, and change normal invocations with invocations through # reflection. for method_number, index in enumerate(method_index): # If there are enough registers for reflection operations, look for # method invocations inside each method's body. if method_is_reflectable[method_number]: current_line_number = index while not lines[current_line_number].startswith( ".end method"): # There is no space for further reflection instructions. if (self.obfuscator_instructions_length >= self.obfuscator_instructions_limit): break current_line_number += 1 invoke_match = util.invoke_pattern.match( lines[current_line_number]) if (invoke_match and "<init>" not in lines[current_line_number]): # The method belongs to an Android class or is # invoked on an array. if invoke_match.group( "invoke_object" ) in self.android_class_names or invoke_match.group( "invoke_object").startswith("["): continue method_signature = ( "{method_name}({method_param})" "{method_return}".format( method_name=invoke_match.group( "invoke_method"), method_param=invoke_match.group( "invoke_param"), method_return=invoke_match.group( "invoke_return"), )) # The method to reflect has to be public, has to be # declared in a public class and all its parameters # have to be public. if not self.method_is_all_public( invoke_match.group("invoke_object"), method_signature, invoke_match.group("invoke_param"), ): continue if (invoke_match.group("invoke_type") == "invoke-virtual"): tmp_is_virtual = True elif (invoke_match.group("invoke_type") == "invoke-static"): tmp_is_virtual = False else: continue tmp_register = invoke_match.group( "invoke_pass") tmp_class_name = invoke_match.group( "invoke_object") tmp_method = invoke_match.group( "invoke_method") tmp_param = invoke_match.group("invoke_param") tmp_return_type = invoke_match.group( "invoke_return") # Check if the method invocation result is used in # the following lines. for move_result_index in range( current_line_number + 1, min(current_line_number + 10, len(lines) - 1), ): if "invoke-" in lines[move_result_index]: # New method invocation, the previous method # result is not used. break move_result_match = move_result_pattern.match( lines[move_result_index]) if move_result_match: tmp_result_register = move_result_match.group( "register") # Fix the move-result instruction after the # method invocation. new_move_result = "" if tmp_return_type in self.primitive_types: new_move_result += ( "\tmove-result-object " "{result_register}\n\n" "\tcheck-cast {result_register}, " "{result_class}\n\n".format( result_register= tmp_result_register, result_class=self. type_dict[tmp_return_type], )) new_move_result += "\tinvoke-virtual " "{{{result_register}}}, {cast}\n\n".format( result_register= tmp_result_register, cast=self.reverse_cast_dict[ tmp_return_type], ) if (tmp_return_type == "J" or tmp_return_type == "D"): new_move_result += ( "\tmove-result-wide " "{result_register}\n". format( result_register= tmp_result_register)) else: new_move_result += ( "\tmove-result " "{result_register}\n". format( result_register= tmp_result_register)) else: new_move_result += ( "\tmove-result-object " "{result_register}\n\n" "\tcheck-cast {result_register}, " "{return_type}\n".format( result_register= tmp_result_register, return_type=tmp_return_type, )) lines[ move_result_index] = new_move_result # Add the original method to the list of methods # using reflection. obfuscator_smali_code += self.add_smali_reflection_code( tmp_class_name, tmp_method, tmp_param) # Change the original code with code using reflection. lines[ current_line_number] = self.create_reflection_method( self.methods_with_reflection, method_local_count[method_number], tmp_is_virtual, tmp_register, tmp_param, ) self.methods_with_reflection += 1 # Add the registers needed for performing reflection. lines[index + 1] = "\t.locals {0}\n".format( method_local_count[method_number] + 4) with open(smali_file, "w", encoding="utf-8") as current_file: current_file.writelines(lines) # Add to the app the code needed for the reflection obfuscator. The code # can be put in any smali directory, since it will be moved to the correct # directory when rebuilding the application. destination_dir = os.path.dirname( obfuscation_info.get_smali_files()[0]) destination_file = os.path.join(destination_dir, "ApiReflection.smali") with open(destination_file, "w", encoding="utf-8") as api_reflection_smali: reflection_code = util.get_api_reflection_smali_code().replace( "#!code_to_replace!#", obfuscator_smali_code) api_reflection_smali.write(reflection_code) except Exception as e: self.logger.error( 'Error during execution of "{0}" obfuscator: {1}'.format( self.__class__.__name__, e)) raise finally: obfuscation_info.used_obfuscators.append(self.__class__.__name__)
def test_get_resource_directory(self, tmp_demo_apk_v10_original_path: str): obfuscation = Obfuscation(tmp_demo_apk_v10_original_path) resource_dir = obfuscation.get_resource_directory() assert os.path.isdir(resource_dir) assert "drawable" in os.listdir(resource_dir)
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 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__)) 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: 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__)
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: native_libs = obfuscation_info.get_native_lib_files() native_lib_invoke_pattern = re.compile( r'\s+invoke-static\s{(?P<invoke_pass>[vp0-9]+)},\s' r'Ljava/lang/System;->loadLibrary\(Ljava/lang/String;\)V') encrypted_libs: Set[str] = set() if native_libs: for smali_file in util.show_list_progress( obfuscation_info.get_smali_files(), interactive=obfuscation_info.interactive, description='Encrypting native libraries'): self.logger.debug( 'Replacing native libraries with encrypted native libraries ' 'in file "{0}"'.format(smali_file)) with open(smali_file, 'r', encoding='utf-8') as current_file: lines = current_file.readlines() class_name = None local_count = 16 # Names of the loaded libraries. lib_names: List[str] = [] editing_constructor = False start_index = 0 for line_number, line in enumerate(lines): if not class_name: class_match = util.class_pattern.match(line) if class_match: class_name = class_match.group('class_name') continue # Native libraries should be loaded inside static constructors. if line.startswith( '.method static constructor <clinit>()V' ) and not editing_constructor: # Entering static constructor. editing_constructor = True start_index = line_number + 1 local_match = util.locals_pattern.match( lines[line_number + 1]) if local_match: local_count = int( local_match.group('local_count')) if local_count <= 15: # An additional register is needed for the encryption. local_count += 1 lines[line_number + 1] = '\t.locals {0}\n'.format( local_count) continue # For some reason the locals declaration was not found where it should be, so assume the # local registers are all used (we can't add any instruction here). break elif line.startswith( '.end method') and editing_constructor: # Only one static constructor per class. break elif editing_constructor: # Inside static constructor. invoke_match = native_lib_invoke_pattern.match( line) if invoke_match: # Native library load instruction. Iterate the constructor lines backwards in order to # find the string containing the name of the loaded library. for l_num in range(line_number - 1, start_index, -1): string_match = util.const_string_pattern.match( lines[l_num]) if string_match and \ string_match.group('register') == invoke_match.group('invoke_pass'): # Native library string declaration. lib_names.append( string_match.group('string')) # Static constructors take no parameters, so the highest register is v<local_count - 1>. lines[line_number] = '\tconst-class v{class_register_num}, {class_name}\n\n' \ '\tinvoke-static {{v{class_register_num}, {original_register}}}, ' \ 'Lcom/decryptassetmanager/DecryptAsset;->loadEncryptedLibrary(' \ 'Ljava/lang/Class;Ljava/lang/String;)V\n'.format( class_name=class_name, original_register=invoke_match.group('invoke_pass'), class_register_num=local_count - 1) # Encrypt the native libraries used in code and put them in assets folder. assets_dir = obfuscation_info.get_assets_directory() os.makedirs(assets_dir, exist_ok=True) for native_lib in native_libs: for lib_name in lib_names: if native_lib.endswith( '{0}.so'.format(lib_name)): arch = os.path.basename( os.path.dirname(native_lib)) encrypted_lib_path = os.path.join( assets_dir, 'lib.{arch}.{lib_name}.so'.format( arch=arch, lib_name=lib_name)) with open(native_lib, 'rb') as native_lib_file: encrypted_lib = AES \ .new(key=self.encryption_secret.encode(), mode=AES.MODE_ECB) \ .encrypt(pad(native_lib_file.read(), AES.block_size)) with open(encrypted_lib_path, 'wb') as encrypted_lib_file: encrypted_lib_file.write(encrypted_lib) encrypted_libs.add(encrypted_lib_path) with open(smali_file, 'w', encoding='utf-8') as current_file: current_file.writelines(lines) if not obfuscation_info.decrypt_asset_smali_file_added_flag and encrypted_libs: # Add to the app the code for decrypting the encrypted native libraries. The code # for decrypting can be put in any smali directory, since it will be moved to the # correct directory when rebuilding the application. destination_dir = os.path.dirname( obfuscation_info.get_smali_files()[0]) destination_file = os.path.join(destination_dir, 'DecryptAsset.smali') with open(destination_file, 'w', encoding='utf-8') as decrypt_asset_smali: decrypt_asset_smali.write( util.get_decrypt_asset_smali_code()) obfuscation_info.decrypt_asset_smali_file_added_flag = True # Remove the original native libraries (the encrypted ones will be used instead). for native_lib in native_libs: try: os.remove(native_lib) except OSError as e: self.logger.warning( 'Unable to delete native library "{0}": {1}'. format(native_lib, e)) else: self.logger.debug('No native libraries found') except Exception as e: self.logger.error( 'Error during execution of "{0}" obfuscator: {1}'.format( self.__class__.__name__, e)) raise finally: obfuscation_info.used_obfuscators.append(self.__class__.__name__)
def obfuscate(self, obfuscation_info: Obfuscation): self.logger.info('Running "{0}" obfuscator'.format(self.__class__.__name__)) self.encryption_secret = obfuscation_info.encryption_secret try: native_libs = obfuscation_info.get_native_lib_files() native_lib_invoke_pattern = re.compile( r"\s+invoke-static\s{(?P<invoke_pass>[vp0-9]+)},\s" r"Ljava/lang/System;->loadLibrary\(Ljava/lang/String;\)V" ) encrypted_to_original_mapping: Dict[str, str] = {} if native_libs: for smali_file in util.show_list_progress( obfuscation_info.get_smali_files(), interactive=obfuscation_info.interactive, description="Encrypting native libraries", ): self.logger.debug( "Replacing native libraries with encrypted native libraries " 'in file "{0}"'.format(smali_file) ) with open(smali_file, "r", encoding="utf-8") as current_file: lines = current_file.readlines() class_name = None local_count = 16 # Names of the loaded libraries. lib_names: List[str] = [] editing_constructor = False start_index = 0 for line_number, line in enumerate(lines): if not class_name: class_match = util.class_pattern.match(line) if class_match: class_name = class_match.group("class_name") continue # Native libraries should be loaded inside static constructors. if ( line.startswith(".method static constructor <clinit>()V") and not editing_constructor ): # Entering static constructor. editing_constructor = True start_index = line_number + 1 local_match = util.locals_pattern.match( lines[line_number + 1] ) if local_match: local_count = int(local_match.group("local_count")) if local_count <= 15: # An additional register is needed for the # encryption. local_count += 1 lines[line_number + 1] = "\t.locals {0}\n".format( local_count ) continue # For some reason the locals declaration was not found where # it should be, so assume the local registers are all used # (we can't add any instruction here). break elif line.startswith(".end method") and editing_constructor: # Only one static constructor per class. break elif editing_constructor: # Inside static constructor. invoke_match = native_lib_invoke_pattern.match(line) if invoke_match: # Native library load instruction. Iterate the # constructor lines backwards in order to find the # string containing the name of the loaded library. for l_num in range(line_number - 1, start_index, -1): string_match = util.const_string_pattern.match( lines[l_num] ) if string_match and string_match.group( "register" ) == invoke_match.group("invoke_pass"): # Native library string declaration. lib_names.append(string_match.group("string")) # Static constructors take no parameters, so the highest # register is v<local_count - 1>. lines[line_number] = ( "\tconst-class v{class_register_num}, " "{class_name}\n\n" "\tinvoke-static {{v{class_register_num}, " "{original_register}}}, " "Lcom/decryptassetmanager/DecryptAsset;->" "loadEncryptedLibrary(" "Ljava/lang/Class;Ljava/lang/String;)V\n".format( class_name=class_name, original_register=invoke_match.group( "invoke_pass" ), class_register_num=local_count - 1, ) ) # Encrypt the native libraries used in code and put them # in assets folder. assets_dir = obfuscation_info.get_assets_directory() os.makedirs(assets_dir, exist_ok=True) for native_lib in native_libs: for lib_name in lib_names: if native_lib.endswith("{0}.so".format(lib_name)): arch = os.path.basename(os.path.dirname(native_lib)) encrypted_lib_path = os.path.join( assets_dir, "lib.{arch}.{lib_name}.so".format( arch=arch, lib_name=lib_name ), ) with open(native_lib, "rb") as native_lib_file: encrypted_lib = AES.new( key=self.encryption_secret.encode(), mode=AES.MODE_ECB, ).encrypt( pad(native_lib_file.read(), AES.block_size) ) with open( encrypted_lib_path, "wb" ) as encrypted_lib_file: encrypted_lib_file.write(encrypted_lib) encrypted_to_original_mapping[ encrypted_lib_path ] = native_lib with open(smali_file, "w", encoding="utf-8") as current_file: current_file.writelines(lines) if ( not obfuscation_info.decrypt_asset_smali_file_added_flag and encrypted_to_original_mapping ): # Add to the app the code for decrypting the encrypted native # libraries. The code for decrypting can be put in any smali # directory, since it will be moved to the correct directory # when rebuilding the application. destination_dir = os.path.dirname( obfuscation_info.get_smali_files()[0] ) destination_file = os.path.join( destination_dir, "DecryptAsset.smali" ) with open( destination_file, "w", encoding="utf-8" ) as decrypt_asset_smali: decrypt_asset_smali.write( util.get_decrypt_asset_smali_code(self.encryption_secret) ) obfuscation_info.decrypt_asset_smali_file_added_flag = True # Remove the original native libraries that were encrypted (the # encrypted ones will be used instead). for _, original_lib in encrypted_to_original_mapping.items(): try: os.remove(original_lib) except OSError as e: self.logger.warning( 'Unable to delete native library "{0}": {1}'.format( original_lib, e ) ) else: self.logger.debug("No native libraries found") except Exception as e: self.logger.error( 'Error during execution of "{0}" obfuscator: {1}'.format( self.__class__.__name__, e ) ) raise finally: obfuscation_info.used_obfuscators.append(self.__class__.__name__)
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 (in_file, out_file): editing_method = False start_label = None end_label = None for line in in_file: if ( line.startswith(".method ") and " abstract " not in line and " native " not in line and not editing_method ): # Entering method. out_file.write(line) editing_method = True elif line.startswith(".end method") and editing_method: # Exiting method. if start_label and end_label: out_file.write("\t:{0}\n".format(end_label)) out_file.write("\tgoto/32 :{0}\n".format(start_label)) start_label = None end_label = None out_file.write(line) editing_method = False elif editing_method: # Inside method. out_file.write(line) 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) out_file.write("\n\tconst v0, {0}\n".format(v0)) out_file.write("\tconst v1, {0}\n".format(v1)) out_file.write("\tadd-int v0, v0, v1\n") out_file.write("\trem-int v0, v0, v1\n") out_file.write("\tif-gtz v0, :{0}\n".format(tmp_label)) out_file.write("\tgoto/32 :{0}\n".format(end_label)) out_file.write("\t:{0}\n".format(tmp_label)) out_file.write("\t:{0}\n".format(start_label)) 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 test_get_native_lib_files(self, tmp_demo_apk_v10_original_path: str): obfuscation = Obfuscation(tmp_demo_apk_v10_original_path) native_libs = obfuscation.get_native_lib_files() assert len(native_libs) > 0 assert all(os.path.isfile(native_lib) for native_lib in native_libs)
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 test_get_assets_directory(self, tmp_demo_apk_v10_original_path: str): obfuscation = Obfuscation(tmp_demo_apk_v10_original_path) assets_dir = obfuscation.get_assets_directory() assert os.path.isdir(assets_dir) assert "message.txt" in os.listdir(assets_dir)
def test_obfuscation_get_total_methods( self, tmp_demo_apk_v10_original_path: str): obfuscation = Obfuscation(tmp_demo_apk_v10_original_path) total_methods = obfuscation._get_total_methods() assert isinstance(total_methods, int) assert total_methods > 10
def test_obfuscation_error_invalid_apk_path(self): with pytest.raises(FileNotFoundError): Obfuscation("invalid.apk.path")
def test_obfuscation_get_remaining_methods( self, tmp_demo_apk_v10_original_path: str): obfuscation = Obfuscation(tmp_demo_apk_v10_original_path) remaining_methods = obfuscation._get_remaining_methods() assert isinstance(remaining_methods, int) assert remaining_methods > 63500
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 test_is_multidex(self, tmp_demo_apk_v10_original_path: str): obfuscation = Obfuscation(tmp_demo_apk_v10_original_path) is_multidex = obfuscation.is_multidex() assert is_multidex is False
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: 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__)