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