Пример #1
0
    def fn_extract_from_ext4(self, ext4_filepath=None):
        """Extracts Android application files from EXT4 image.
        
        This function calls multiple other functions to accomplish 
        the following:
        - First analyse superblock to get info regarding block size/count 
        and inode size/count.
        - Then analyse group descriptor table to get inode table locations.
        - Then go through each inode table, get directory listings, and 
        subsequently filenames.
        - Find the inodes corresponding to APK files and extract.
        
        Based on https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout
        
        Files are extracted to the same folder as the ext4 image. 
        Nothing is returned.
        
        :param ext4_filepath: string specifying path to ext4 file. If not 
            specified, the function will look for an ext4 file within the 
            app folder
        :raises JandroidException: an exception is thrown if more than one 
            ext4 file is found in the app folder, or if no files are found
        """
        # If no ext4 file is specified, then see if app folder contains any.
        if ext4_filepath == None:
            ext4_to_pull_from = []
            for root, dirs, filenames in os.walk(self.path_app_folder):
                for filename in fnmatch.filter(filenames, '*.ext4'):
                    ext4_to_pull_from.append(os.path.join(root, filename))
            # If no files are identified, then we have nothing to analyse.
            if len(ext4_to_pull_from) < 1:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) + ': Ext4Error',
                    'reason':
                    'No ext4 files found in app folder.'
                })
            # We don't support more than one ext4 analysis at a time.
            elif len(ext4_to_pull_from) > 1:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) + ': Ext4Error',
                    'reason':
                    'More than one ext4 files found ' + 'in app folder.'
                })
            self.ext4_filepath = ext4_to_pull_from[0]
        else:
            self.ext4_filepath = ext4_filepath

        logging.info('Extracting files from ext4: ' + self.ext4_filepath)

        ### Start analysis ###
        # Analyse superblock in block group 0.
        self.fn_analyse_super_block()
        # Analyse group descriptor in block group 0.
        self.fn_get_group_descriptor_table()
        # Analyse the inode tables to get file/dir info.
        self.fn_analyse_inode_tables()
Пример #2
0
    def fn_perform_initial_checks(self, bool_pull_apps, app_pull_src):
        """Checks for existence of required files and folders.
        
        :param bool_pull_apps: boolean indicating whether apps are to be 
            pulled from device/image
        :param app_pull_src: string specifying pull location. Can be one 
            of "device", "img" or "ext4"
        :returns: string specifying path to ADB platform tools
        :raises JandroidException: an exception is raised if the app folder 
            is not present, or if it is empty (only when bool_pull_apps is 
            False)
        """
        logging.info('Performing basic checks. Please wait.')
        self.bool_pull_apps = bool_pull_apps
        self.pull_source = app_pull_src

        # Check app folder.
        # First check if the folder exists at all.
        if not os.path.isdir(self.path_app_folder):
            raise JandroidException({
                'type':
                str(os.path.basename(__file__)) + ': DirectoryNotPresent',
                'reason':
                'App directory "' + self.path_app_folder + '" does not exist.'
            })

        # Check if the folder has APK or DEX files inside.
        # Only do this if we are not expected to pull apps.
        if self.bool_pull_apps == False:
            if [
                    f for f in os.listdir(self.path_app_folder)
                    if ((f.endswith('.apk') or f.endswith('.dex'))
                        and not f.startswith('.'))
            ] == []:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) + ': EmptyAppDirectory',
                    'reason':
                    'App directory "' + self.path_app_folder +
                    '" does not contain APK/DEX files.'
                })

        # If we *are* expected to pull apps, then if we are pulling
        #  from device, make sure we have Android platform-tools.
        else:
            if self.pull_source == 'device':
                self.fn_check_for_adb_executable()

        logging.info('Basic checks complete.')
        return self.path_platform_tools
Пример #3
0
    def fn_pull_apk(self, pkg, path_to_pkg):
        """Pulls an individual APK file.
        
        :param pkg: package name string
        :param path_to_pkg: string specifying path to package (on Android)
        :raises JandroidException: an exception is raised if any ADB 
            command should fail.
        """
        logging.debug('Pulling package ' + pkg + ' from ' + path_to_pkg)

        # Remove the "package:" string from path.
        path_to_apk = path_to_pkg.replace('package:', '').strip()

        # Pull APK (or raise error on failure).
        try:
            pull_result = self.fn_execute_adb(
                ['pull', path_to_apk, self.path_app_folder])
        except JandroidException as e:
            if (e.args[0]['type'] == 'ADBError'):
                # This is an error thrown when attempting to
                #  execute ADB. Probably fatal.
                raise JandroidException({
                    'type':
                    e.args[0]['type'],
                    'reason':
                    'Error pulling APK via ADB. ' + e.args[0]['reason']
                })
            elif (e.args[0]['type'] == 'ADBExecuteError'):
                # This might be a 'Permission Denied'.
                #  We ignore this type of error for now.
                pass
Пример #4
0
 def fn_get_attributes_labels(self, object):
     """Retrieves attributes and labels from object.
     
     This function takes as input a dictionary object containing 
     (at least) two keys: "attributes" and "labels".
     It retrieves the attributes and labels and returns them
     as lists within a list.
     
     :param object: dictionary object containing keys:
         "attributes" and "labels"
     :returns: list containing two lists: one of attributes,
         one of labels
     """
     if 'attributes' in object:
         node_attributes = object['attributes']
     else:
         node_attributes = []
     if 'labels' in object:
         node_labels = object['labels']
     else:
         node_labels = []
     if ((node_attributes == []) and (node_labels == [])):
         raise JandroidException({
             'type':
             str(os.path.basename(__file__)) +
             ': EmptyAttributeAndLabelList',
             'reason':
             'A node must have at least ' + 'one attribute or label.'
         })
     return [node_attributes, node_labels]
Пример #5
0
 def fn_pull_from_image(self):
     """Enumerate img files and initiate extraction."""
     logging.debug('Extracting files from image.')
     
     # Enumerate .img files within app folder.
     images_to_pull_from = []
     for root, dirs, filenames in os.walk(self.path_app_folder):
         for filename in fnmatch.filter(filenames, '*.img'):
             images_to_pull_from.append(os.path.join(root, filename))
     
     # For each .img file, extract different images (only sparse image 
     #  supported at present).
     for image_to_pull_from in images_to_pull_from:
         try:
             self.fn_identify_image_type_from_header(image_to_pull_from)
         except JandroidException:
             raise
         except Exception as e:
             raise JandroidException(
                 {
                     'type': str(os.path.basename(__file__))
                             + ': ImageExtractError',
                     'reason': str(e)
                 }
             )
Пример #6
0
 def fn_check_for_adb_executable(self):
     """Checks whether the ADB executable exists.
     
     This function identifies the execution platform, i.e., Windows, 
     Linux, or Mac (Darwin). It then creates the expected filepath to 
     the ADB executable and tests for whether the file is present in 
     the expected location (it expects the executable to be included 
     with the code, rather than being available somewhere on the system).
     
     :raises JandroidException: an exception is raised if the ADB 
         executable is not found at the expected location
     """
     # Get execution platform. ADB differs based on platform.
     run_platform = platform.system().lower().strip()
     logging.debug('Platform identified as "' + run_platform + '".')
     # Path to ADB.
     if run_platform == 'windows':
         executable = 'adb.exe'
     else:
         executable = 'adb'
     self.path_platform_tools = os.path.join(
         self.path_base_dir, 'libs', 'platform-tools',
         'platform-tools_' + run_platform, executable)
     if os.path.isfile(self.path_platform_tools):
         logging.debug('Using adb tool at ' + self.path_platform_tools +
                       '.')
     else:
         raise JandroidException({
             'type':
             str(os.path.basename(__file__)) + ': ADBNotPresent',
             'reason':
             'Could not find adb tool at ' + self.path_platform_tools + '.'
         })
Пример #7
0
    def fn_pull_from_device(self):
        """Pulls APKs from an attached Android device via ADB.
        
        :raises JandroidException: an exception is raised if any ADB 
            command should fail.
        """
        logging.debug('Pulling APKs from device.')

        # First execute 'adb devices',
        #  to make sure there is a single device attached.
        try:
            self.fn_test_connected_devices()
        except JandroidException as e:
            raise

        # Get list of packages.
        try:
            result = self.fn_execute_adb(
                ['shell', 'pm', 'list', 'packages']
            )
        except JandroidException as e:
            raise JandroidException(
                {
                    'type': e.args[0]['type'],
                    'reason': 'Error getting package list via ADB. '
                              + e.args[0]['reason']
                }
            )

        for pkg in result.split('\n'):
            # Get path-to-APK.
            pkg = pkg.replace('package:', '')
            try:
                path_to_pkg = self.fn_execute_adb(
                    ['shell', 'pm', 'path', pkg]
                )
            except JandroidException as e:
                raise JandroidException(
                    {
                        'type': e.args[0]['type'],
                        'reason': 'Error getting package path '
                                  + 'via ADB. '
                                  + e.args[0]['reason']
                    }
                )
            # Pull the APK.
            self.fn_pull_apk(pkg, path_to_pkg)
Пример #8
0
 def fn_test_connected_devices(self):
     """Tests to make sure a single device is connected.
     
     This function runs 'adb devices' to make sure a device is attached. 
     Only one Android device (or VM) should be returned.
     
     :raises JandroidException: an exception is raised if more or less 
         than one device is returned by 'adb devices'
     """
     logging.debug('Testing for connected devices.')
     result = self.fn_execute_adb(['devices'])
     result_text_as_list = [
         f for f in result.split('\n')
         if 'daemon' not in f
     ]
     device_list = list(filter(None, result_text_as_list[1:]))
     logging.debug(
         'Device list: \n\t '
         + result.replace('\n','\n\t ')
     )
     if len(device_list) < 1:
         raise JandroidException(
             {
                 'type': str(os.path.basename(__file__))
                         + ': DeviceError',
                 'reason': 'No Android devices detected.'
             }
         )
     if len(device_list) > 1:
         raise JandroidException(
             {
                 'type': str(os.path.basename(__file__))
                         + ': DeviceError',
                 'reason': 'More than one Android device '
                           + 'attached.'
             }
         )
Пример #9
0
 def fn_execute_graph_query(self, cypher_query):
     """Executes the provided Cypher query against a neo4j graph.
     
     :param cypher_query: the Cypher query to execute against the 
         neo4j graph
     :raises JandroidException: an exception is raised if query fails to 
         execute
     """
     try:
         res = self.db.query(cypher_query)
     except Exception as e:
         raise JandroidException({
             'type': str(os.path.basename(__file__)) + ': DBQueryError',
             'reason': str(e)
         })
     logging.debug('Executed query "' + cypher_query +
                   '" with result stats: ' + str(res.stats) +
                   ' and values: ' + str(res.rows))
     return res
Пример #10
0
    def __init__(self, base_dir, app_dir,
                 adb_path, pull_location='device'):
        """Sets paths and app extraction location.
        
        :param base_dir: string specifying path to the base/root directory 
            (contains src/, templates/, etc)
        :param app_dir: string specifying path to the directory containing the 
            applications under analysis
        :param adb_path: string specifying path to the Android adb executable
        :param pull_location: string specifying location to pull apps from -
            can be one of "device", "ext4", "img"
        :raises JandroidException: an exception is raised if app folder cannot 
            be created
        """
        # Set paths.
        self.path_base_dir = base_dir        
        self.path_platform_tools = adb_path

        # Path to app folder
        #  (i.e., where apps should be stored after being pulled).
        #  Folder is created if it doesn't exist.
        self.path_app_folder = app_dir

        # Test if app folder exists, and if not, create it.
        if not os.path.isdir(self.path_app_folder):
            try:
                os.makedirs(self.path_app_folder)
            except Exception as e:
                raise JandroidException(
                    {
                        'type': str(os.path.basename(__file__))
                                + ': FolderCreateError',
                        'reason': str(e)
                    }
                )

        # Set pull location.
        self.pull_location = pull_location
Пример #11
0
    def fn_connect_to_graph(self):
        """Connects to Neo4j database.

        Creates a connection to the neo4j database, using the parameters 
        specified in the config file (or default values).

        :raises JandroidException: an exception is raised if connection to 
            neo4j database fails.
        """
        logging.info('Trying to connect to Neo4j graph DB.')
        try:
            self.db = GraphDatabase(self.neo4j_url,
                                    username=self.neo4j_username,
                                    password=self.neo4j_password)
            logging.info('Connected to graph DB.')
        except Exception as e:
            raise JandroidException({
                'type':
                str(os.path.basename(__file__)) + ': GraphConnectError',
                'reason':
                'Unable to connect to Neo4j ' + 'graph database. ' +
                'Are you sure it\'s running? ' + 'Returned error is: ' + str(e)
            })
Пример #12
0
    def fn_process_exported(self,
                            lookfor_tags,
                            lookfor_value,
                            current_xml_tree,
                            is_match=True):
        """Processes the "exported" tag.
        
        :param lookfor_tags: a list of tags (namespace variants) to look for
        :param lookfor_value: string value (either "true" or "false")
        :param current_xml_tree: lxml Element
        :param is_match: boolean indicating whether the requirement is to 
            check for match or no-match
        """
        # Make sure we are at the correct level in the XML tree.
        # That is, the exported tag is only used with activities, services,
        #  receivers and providers.
        current_tag = current_xml_tree.tag
        exported_tag_options = [
            'activity', 'activity-alias', 'receiver', 'service', 'provider'
        ]
        if current_tag not in exported_tag_options:
            raise JandroidException({
                'type':
                str(os.path.basename(__file__)) + ': InvalidTag',
                'reason':
                'Exported tag must belong to one of [' +
                '"activity", "activity-alias", "receiver", ' +
                '"service", "provider"' + '].'
            })

        # First check if exported is explicitly defined.
        # If it is, then we needn't do much more processing.
        tag_present = False
        tag_value_in_manifest = None
        for tag in lookfor_tags:
            if tag in current_xml_tree.attrib:
                tag_present = True
                tag_value_in_manifest = current_xml_tree.attrib[tag]

        # If exported isn't explicitly defined, then consider default values.
        if tag_present == False:
            # For activities, receivers and services, the presence of an
            #  intent-filter means exported defaults to True.
            # Else it defaults to False.
            if current_tag in [
                    'activity', 'activity-alias', 'receiver', 'service'
            ]:
                intent_filters = current_xml_tree.findall('intent-filter')
                if intent_filters == []:
                    tag_value_in_manifest = 'false'
                else:
                    tag_value_in_manifest = 'true'

            # For providers, if sdkversion >= 17, defaults to False.
            # Else, defaults to True.
            elif current_tag in ['provider']:
                target_sdk_version = None
                uses_sdk = self.apk_manifest_root.findall('uses-sdk')
                if uses_sdk != []:
                    possible_targetsdktags = \
                        self.fn_generate_namespace_variants(
                            '<NAMESPACE>:targetSdkVersion'
                        )
                    for uses_sdk_element in uses_sdk:
                        for targetsdktag in possible_targetsdktags:
                            if targetsdktag in uses_sdk_element.attrib:
                                target_sdk_version = \
                                    int(uses_sdk_element.attrib[targetsdktag])
                if target_sdk_version != None:
                    if target_sdk_version >= 17:
                        tag_value_in_manifest = 'false'
                    else:
                        tag_value_in_manifest = 'true'
                # This is a non-ideal way to handle the situation where there
                #  is a provider with no explicit export, and no
                #  uses-sdk/targetSdkVersion element.
                if tag_value_in_manifest == None:
                    return False

        # If the values match, then if the goal was
        #  to have the values match, return True. Else, return False.
        if tag_value_in_manifest == lookfor_value:
            if is_match == True:
                return True
            else:
                return False
        # If the values match, and the goal was that they should *not* match,
        #  return False. Else, return True.
        else:
            if is_match == True:
                return False
            else:
                return True
Пример #13
0
    def fn_analyse_lookfor(self, lookfor_object, current_xml_tree):
        """Analyses LOOKFOR elements.
        
        :param lookfor_object: dictionary object specifying the parameters 
            to look for
        :param current_xml_tree: lxml Element
        :returns: boolean indicating whether the LOOKFOR was satisfied
        """
        # Initialise variables to keep track of how many things we are
        #  supposed to bechecking and how many have been satisfied.
        expected_lookfors = 0
        satisfied_lookfors = 0

        # There are different LOOKFOR types, each with a corresponding function.
        fn_to_execute = None
        for lookfor_key in lookfor_object:
            expected_lookfors += 1
            if lookfor_key == 'TAGEXISTS':
                fn_to_execute = self.fn_analyse_tag_exists
            elif lookfor_key == 'TAGNOTEXISTS':
                fn_to_execute = self.fn_analyse_tag_not_exists
            elif lookfor_key == 'TAGVALUEMATCH':
                fn_to_execute = self.fn_analyse_tag_value_match
            elif lookfor_key == 'TAGVALUENOMATCH':
                fn_to_execute = self.fn_analyse_tag_value_no_match
            else:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) + ': IncorrectLookforKey',
                    'reason':
                    'Unrecognised LOOKFOR key.'
                })

            # A single LOOKFOR object may have a number of elements to
            #  satisfy (specified as a list).
            all_lookfors = self.fn_process_lookfor_lists(
                lookfor_object[lookfor_key])
            # We have to keep track of these individual elements as well.
            expected_per_tag_lookfors = len(all_lookfors)
            satisfied_per_tag_lookfors = 0
            # Check each individual element.
            for single_lookfor in all_lookfors:
                lookfor_output = fn_to_execute(single_lookfor,
                                               current_xml_tree)
                if lookfor_output == True:
                    satisfied_per_tag_lookfors += 1
                # If even one fails, the whole thing fails.
                else:
                    break
            # Check if this one LOOKFOR check was fully satisfied.
            if expected_per_tag_lookfors == satisfied_per_tag_lookfors:
                satisfied_lookfors += 1
            # If even one fails, the whole thing fails.
            else:
                break

        # Finally, check if all expected lookfor elements were satisfied.
        if expected_lookfors == satisfied_lookfors:
            return True
        else:
            return False
Пример #14
0
    def fn_analyse_super_block(self):
        """Analyses the superblock in block group 0.
        
        The superblock contains a lot of information that we will require 
        later on. These are set as class attributes. 
        We do also read a lot of data that we don't use.
        
        Does not return anything.
        
        :raises JandroidException: an exception is raised if unsupported 
            modes are identified.
        """
        # Open the file in binary read mode.
        ext4_file = open(self.ext4_filepath, "rb")
        # First 1024 bytes in BG0 are padding.
        ext4_file.read(1024)
        ### Read superblock ###
        # A superblock has 1024 bytes of data.
        ext4_super_block = ext4_file.read(1024)
        s_inodes_count = \
            struct.unpack('<I', ext4_super_block[0:4])[0] # Total inode count.
        s_blocks_count_lo = \
            struct.unpack('<I', ext4_super_block[4:8])[0] # Total block count.
        s_r_blocks_count_lo = \
            struct.unpack('<I', ext4_super_block[8:12])[0]
        s_free_blocks_count_lo = \
            struct.unpack('<I', ext4_super_block[12:16])[0]
        s_free_inodes_count = \
            struct.unpack('<I', ext4_super_block[16:20])[0]
        s_first_data_block = \
            struct.unpack('<I', ext4_super_block[20:24])[0]
        s_log_block_size = \
            struct.unpack('<I', ext4_super_block[24:28])[0]
        self.block_size = 2**(10 + s_log_block_size)
        if self.block_size == 1024:
            raise JandroidException({
                'type': str(os.path.basename(__file__)) + ': Ext4Error',
                'reason': 'Unsupported block size.'
            })
        s_log_cluster_size = \
            struct.unpack('<I', ext4_super_block[28:32])[0]
        s_blocks_per_group = \
            struct.unpack('<I', ext4_super_block[32:36])[0]
        self.block_group_size = \
            self.block_size * s_blocks_per_group
        self.num_block_groups = \
            int(os.path.getsize(self.ext4_filepath)/self.block_group_size)
        s_clusters_per_group = \
            struct.unpack('<I', ext4_super_block[36:40])[0]
        s_inodes_per_group = \
            struct.unpack('<I', ext4_super_block[40:44])[0]
        self.inodes_per_group = \
            s_inodes_per_group
        s_mtime = \
            struct.unpack('<I', ext4_super_block[44:48])[0]
        s_wtime = \
            struct.unpack('<I', ext4_super_block[48:52])[0]
        s_mnt_count = \
            struct.unpack('<H', ext4_super_block[52:54])[0]
        s_max_mnt_count = \
            struct.unpack('<H', ext4_super_block[54:56])[0]
        s_magic = \
            struct.unpack('<H', ext4_super_block[56:58])[0]
        if s_magic != 0xEF53:
            raise JandroidException({
                'type':
                str(os.path.basename(__file__)) + ': Ext4Error',
                'reason':
                'Imvalid magic number in superblock.'
            })
        s_state = \
            struct.unpack('<H', ext4_super_block[58:60])[0]
        s_errors = \
            struct.unpack('<H', ext4_super_block[60:62])[0]
        s_minor_rev_level = \
            struct.unpack('<H', ext4_super_block[62:64])[0]
        s_lastcheck = \
            struct.unpack('<I', ext4_super_block[64:68])[0]
        s_checkinterval = \
            struct.unpack('<I', ext4_super_block[68:72])[0]
        s_creator_os = \
            struct.unpack('<I', ext4_super_block[72:76])[0]
        s_rev_level = \
            struct.unpack('<I', ext4_super_block[76:80])[0]
        s_def_resuid = \
            struct.unpack('<H', ext4_super_block[80:82])[0]
        s_def_resgid = \
            struct.unpack('<H', ext4_super_block[82:84])[0]
        s_first_ino = \
            struct.unpack('<I', ext4_super_block[84:88])[0]
        s_inode_size = \
            struct.unpack('<H', ext4_super_block[88:90])[0]
        self.inode_size = \
            s_inode_size
        s_block_group_nr = \
            struct.unpack('<H', ext4_super_block[90:92])[0]
        s_feature_compat = \
            struct.unpack('<I', ext4_super_block[92:96])[0]
        if ((s_feature_compat & 0x10) == 0x10):
            self.has_reserved_gdt = 1
            self.num_reserved_gdt_entries = \
                struct.unpack('<H', ext4_super_block[206:208])[0]
        else:
            self.has_reserved_gdt = 0
        # Next section (compatibility).
        s_feature_incompat = \
            struct.unpack('<I', ext4_super_block[96:100])[0]
        # Support for 64-bit.
        self.INCOMPAT_64BIT = 0
        if ((s_feature_incompat & 0x80) == 0x80):
            self.INCOMPAT_64BIT = 1
        if self.INCOMPAT_64BIT != 0:
            raise JandroidException({
                'type': str(os.path.basename(__file__)) + ': Ext4Error',
                'reason': 'No support for 64-bit.'
            })
        # Directories store file type info.
        self.INCOMPAT_FILETYPE = 0
        if ((s_feature_incompat & 0x2) == 0x2):
            self.INCOMPAT_FILETYPE = 1
        # Data in inode.
        self.INCOMPAT_INLINE_DATA = 0
        if ((s_feature_incompat & 0x8000) == 0x8000):
            self.INCOMPAT_INLINE_DATA = 1

        # Readonly-compatible feature set.
        s_feature_ro_compat = \
            struct.unpack('<I', ext4_super_block[100:104])[0]
        # Check for large files.
        if ((s_feature_ro_compat & 0x8) == 0x8):
            self.RO_COMPAT_HUGE_FILE = 1
        else:
            self.RO_COMPAT_HUGE_FILE = 0

        logging.debug('Superblock details:\n\t ' + 'Total inode count ' +
                      str(s_inodes_count) + '\n\t ' + 'Total block count ' +
                      str(s_blocks_count_lo) + '\n\t ' + 'Log block size ' +
                      str(s_log_block_size) + '\n\t ' + 'Block size ' +
                      str(self.block_size) + '\n\t ' + 'Blocks per group ' +
                      str(s_blocks_per_group) + '\n\t ' + 'Inode size ' +
                      str(s_inode_size) + '\n\t ' + 'Inodes per group ' +
                      str(s_inodes_per_group) + '\n\t ' + 'Block group size ' +
                      str(self.block_group_size) + '\n\t ' +
                      'Number of block groups ' + str(self.num_block_groups) +
                      '\n\t ' + 'Size of inode structure (bytes) ' +
                      str(s_inode_size) + '\n\t ' +
                      'Current block group number ' + str(s_block_group_nr) +
                      '\n\t ')
        ext4_file.close()
Пример #15
0
    def fn_convert_sparse_to_ext4(self, path_to_img_file):
        """Extracts ext4 files from Android sparse image.
        
        Extraction process is based on 
        https://android.googlesource.com/platform/system/core/+/master/ \
        libsparse/sparse_format.h
        
        Ext4 file will be written to same directory as the sparse image.
        
        :param path_to_img_file: string specifying path to sparse image file.
        :raises JandroidException: an exception is raised if the magic header 
            is incorrect, or if any subsequent byte values are not as expected.
        """
        logging.debug('Extracting files from Android sparse image.')

        # Open an output file in Binary Write mode.
        out_file_path = os.path.join(self.path_app_folder, 'temp.ext4')
        out_file = open(out_file_path, 'wb')

        # Open the sparse image file in Binary Read mode.
        try:
            image_file = open(path_to_img_file, "rb")
        except Exception as e:
            raise JandroidException({
                'type':
                str(os.path.basename(__file__)) + ': ImageOpenError',
                'reason':
                'Unable to open file ' + path_to_img_file +
                ' in binary read mode: ' + str(e)
            })

        # Unpack bytes and analyse. This will result in an Ext4 file.
        try:
            magic_header = struct.unpack('<I', image_file.read(4))[0]
            if magic_header != 0xed26ff3a:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) + ': InvalidMagicHeader',
                    'reason':
                    'Error reading header bytes ' + 'or invalid magic header.'
                })
            major_version = struct.unpack('<H', image_file.read(2))[0]
            if major_version != 0x01:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) + ': InvalidVersionHeader',
                    'reason':
                    'Error reading header bytes ' +
                    'or unsupported major version.'
                })
            minor_version = struct.unpack('<H', image_file.read(2))[0]
            if minor_version != 0x00:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) + ': InvalidVersionHeader',
                    'reason':
                    'Error reading header bytes ' +
                    'or unsupported minor version.'
                })
            file_hdr_sz = struct.unpack('<H', image_file.read(2))[0]
            if file_hdr_sz != 28:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) +
                    ': InvalidNumFileHeaderSize',
                    'reason':
                    'Invalid file header size.'
                })
            chunk_hdr_sz = struct.unpack('<H', image_file.read(2))[0]
            if chunk_hdr_sz != 12:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) +
                    ': InvalidNumChunkHeaderSize',
                    'reason':
                    'Invalid chunk header size.'
                })
            blk_sz = struct.unpack('<I', image_file.read(4))[0]
            if blk_sz % 4 > 0:
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) + ': InvalidBlkSize',
                    'reason':
                    'Invalid block size (not ' + 'multiple of 4).'
                })
            total_blks = struct.unpack('<I', image_file.read(4))[0]
            total_chunks = struct.unpack('<I', image_file.read(4))[0]
            image_checksum = struct.unpack('<I', image_file.read(4))[0]

            logging.debug('Read header info from sparse image file:\n\t ' +
                          'Major version: ' + str(major_version) + '\n\t ' +
                          'Minor version: ' + str(minor_version) + '\n\t ' +
                          'File header size: ' + str(file_hdr_sz) + '\n\t ' +
                          'Chunk header size: ' + str(chunk_hdr_sz) + '\n\t ' +
                          'Block size in bytes: ' + str(blk_sz) + '\n\t ' +
                          'Total blocks: ' + str(total_blks) + '\n\t ' +
                          'Total chunks: ' + str(total_chunks) + '\n\t ' +
                          'Image checksum: ' + str(image_checksum) + '\n\t ')

            chunk_offset = 0
            for i in range(1, total_chunks + 1):
                # Chunk header.
                chunk_type = struct.unpack('<H', image_file.read(2))[0]
                reserved1 = struct.unpack('<H', image_file.read(2))[0]
                chunk_sz = struct.unpack('<I', image_file.read(4))[0]
                total_sz = struct.unpack('<I', image_file.read(4))[0]

                logging.debug('Header information from chunk:\n\t ' +
                              'Chunk_type: ' + str(hex(chunk_type)) + '\n\t ' +
                              'Chunk size: ' + str(chunk_sz) + '\n\t ' +
                              'Total size: ' + str(total_sz) + '\n\t ' +
                              'Data size: ' + str(total_sz - 12))
                if chunk_type == 0xCAC1:
                    #raw
                    data_size = chunk_sz * blk_sz
                    data_bytes = image_file.read(data_size)
                    out_file.seek(chunk_offset * blk_sz)
                    out_file.write(data_bytes)
                elif chunk_type == 0xCAC2:
                    #fill
                    # TODO: Check for correctness.
                    data_size = 4
                    data_bytes = image_file.read(data_size)
                    out_file.seek(chunk_offset * blk_sz)
                    out_file.write(data_bytes)
                elif chunk_type == 0xCAC3:
                    #don't care
                    if total_sz - 12 != 0:
                        logging.error('Don\'t care chunk has non-zero bytes!')
                        break
                elif chunk_type == 0xCAC4:
                    #crc32
                    data_size = 4
                    data_bytes = image_file.read(data_size)
                # Increment offset.
                chunk_offset += chunk_sz
        except Exception as e:
            raise JandroidException({
                'type':
                str(os.path.basename(__file__)) + ': ByteReadError',
                'reason':
                'Error analysing sparse image ' + path_to_img_file + ': ' +
                str(e)
            })
        out_file.close()

        # Now extract files from ext4.
        inst_ext4_extractor = Ext4Extractor(self.path_app_folder)
        inst_ext4_extractor.fn_extract_from_ext4(out_file_path)
Пример #16
0
    def fn_get_class_method_desc_from_string(self, input_string):
        """Gets class/method/descriptor parts from a string.
        
        A method call in smali is of the format
            classname->methodname descriptor (without the space)
        For example:
            Landroid/util/ArrayMap;->get(Ljava/lang/Object;)Ljava/lang/Object;
        In the above example:
            The class part is           Landroid/util/ArrayMap;
            The method part is          get
            The descriptor part is      (Ljava/lang/Object;)Ljava/lang/Object;
            
        The method part is separated from the class by "->" and from the 
        descriptor by an opening parenthesis.
        
        This function takes as input a string in the smali format and splits 
        it into the class, method and descriptor parts. If "->" is not present 
        in the string, then the entire string is assumed to be the class name 
        and the method and descriptor are assigned values of ".*" (which is 
        considered in Androguard as "any" or "don't care".
        If an opening parenthesis is not present, then it is assumed that 
        there is no descriptor part, and only the descriptor is assigned ".*".
        
        :param input_string: a string representation of a class/method
        :returns: a list containing (in order) the class, method and 
            descriptor parts obtained from the string
        """
        # Assign default values of "don't care" to method and descriptor.
        method_part = '.*'
        desc_part = '.*'

        # In a smali method specification, the class and method must be
        #  separated using '->'.
        if '->' in input_string:
            # There must be some string fragment after the '->'.
            split_string = input_string.split('->')
            if ((len(split_string) != 2) or (split_string[1] == '')):
                raise JandroidException({
                    'type':
                    str(os.path.basename(__file__)) + ': IncorrectMethodCall',
                    'reason':
                    'Call to method specified incorrectly in ' + 'string: "' +
                    input_string +
                    '". Ensure that correct smali format is used.'
                })

            # The class part is easy: it's the part preceding the '->'.
            class_part = split_string[0]

            # The part following the '->' may comprise the method *and* descriptor.
            method_desc_part = split_string[1]

            # However, it's possible that the descriptor part is not specified.
            # If the descriptor *is* included, it will begin with an
            #  opening parenthesis.
            if '(' in method_desc_part:
                method_part = method_desc_part.split('(')[0]
                desc_part = '(' + method_desc_part.split('(')[1]
            # If no opening parenthesis exists, we assume the descriptor hasn't
            #  been provided, i.e., the entire string is the method name.
            else:
                method_part = method_desc_part
                desc_part = '.'
        # If there is no "->" then assume that the entire string is the
        #  class name.
        else:
            class_part = input_string
        return [class_part, method_part, desc_part]