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