def load_disktype_data(self): """Calls the :command:`disktype` command and obtains the disk GUID from GPT volume systems. As we are running the tool anyway, the label is also extracted from the tool if it is not yet set. The disktype data is only loaded and not assigned to volumes yet. """ if not _util.command_exists('disktype'): logger.warning("disktype not installed, could not detect volume type") return None disktype = _util.check_output_(['disktype', self.parent.get_raw_path()]).strip() current_partition = None for line in disktype.splitlines(): if not line: continue # noinspection PyBroadException try: line = line.strip() find_partition_nr = re.match(r"^Partition (\d+):", line) if find_partition_nr: current_partition = int(find_partition_nr.group(1)) elif current_partition is not None: if line.startswith("Type ") and "GUID" in line: self._disktype[current_partition]['guid'] = \ line[line.index('GUID') + 5:-1].strip() # output is between () elif line.startswith("Partition Name "): self._disktype[current_partition]['label'] = \ line[line.index('Name ') + 6:-1].strip() # output is between "" except Exception: logger.exception("Error while parsing disktype output") return
def _determine_auto_detection_method(): """Return the detection method to use when the detection method is 'auto'""" if _util.module_exists('pytsk3'): return 'pytsk3' elif _util.command_exists('mmls'): return 'mmls' else: return 'parted'
def carve(self, freespace=True): """Call this method to carve the free space of the volume for (deleted) files. Note that photorec has its own interface that temporarily takes over the shell. :param freespace: indicates whether the entire volume should be carved (False) or only the free space (True) :type freespace: bool :return: string to the path where carved data is available :raises CommandNotFoundError: if the underlying command does not exist :raises SubsystemError: if the underlying command fails :raises NoMountpointAvailableError: if there is no mountpoint available :raises NoLoopbackAvailableError: if there is no loopback available (only when volume has no slot number) """ if not _util.command_exists('photorec'): logger.warning("photorec is not installed, could not carve volume") raise CommandNotFoundError("photorec") self._make_mountpoint(var_name='carve', suffix="carve", in_paths=True) # if no slot, we need to make a loopback that we can use to carve the volume loopback_was_created_for_carving = False if not self.slot: if not self.loopback: self._find_loopback() #Can't carve if volume has no slot number and can't be mounted on loopback. loopback_was_created_for_carving = True # noinspection PyBroadException try: _util.check_call_(["photorec", "/d", self._paths['carve'] + os.sep, "/cmd", self.loopback, ("freespace," if freespace else "") + "search"]) # clean out the loop device if we created it specifically for carving if loopback_was_created_for_carving: # noinspection PyBroadException try: _util.check_call_(['losetup', '-d', self.loopback]) except Exception: pass else: self.loopback = "" return self._paths['carve'] except Exception as e: logger.exception("Failed carving the volume.") raise SubsystemError(e) else: # noinspection PyBroadException try: _util.check_call_(["photorec", "/d", self._paths['carve'] + os.sep, "/cmd", self.get_raw_path(), str(self.slot) + (",freespace" if freespace else "") + ",search"]) return self._paths['carve'] except Exception as e: logger.exception("Failed carving the volume.") raise SubsystemError(e)
def _check_command(self, command, package="", why=""): if _util.command_exists(command): print(" INSTALLED {}".format(command)) elif why and package: print(" MISSING {:<20}needed for {}, part of the {} package".format(command, why, package)) elif why: print(" MISSING {:<20}needed for {}".format(command, why)) elif package: print(" MISSING {:<20}part of the {} package".format(command, package)) else: print(" MISSING {}".format(command))
def carve(self, freespace=True): """Call this method to carve the free space of the volume for (deleted) files. Note that photorec has its own interface that temporarily takes over the shell. :param freespace: indicates whether the entire volume should be carved (False) or only the free space (True) :type freespace: bool :return: boolean indicating whether the command succeeded """ if not _util.command_exists('photorec'): logger.warning("photorec is not installed, could not carve volume") return False if not self._make_mountpoint(var_name='carvepoint', suffix="carve"): return False # if no slot, we need to make a loopback that we can use to carve the volume loopback_was_created_for_carving = False if not self.slot: if not self.loopback: if not self._find_loopback(): logger.error("Can't carve if volume has no slot number and can't be mounted on loopback.") return False loopback_was_created_for_carving = True try: _util.check_call_(["photorec", "/d", self.carvepoint + os.sep, "/cmd", self.loopback, ("freespace," if freespace else "") + "search"]) # clean out the loop device if we created it specifically for carving if loopback_was_created_for_carving: try: _util.check_call_(['losetup', '-d', self.loopback]) except Exception: pass else: self.loopback = "" return True except Exception: logger.exception("Failed carving the volume.") return False else: try: _util.check_call_(["photorec", "/d", self.carvepoint + os.sep, "/cmd", self.get_raw_base_path(), str(self.slot) + (",freespace" if freespace else "") + ",search"]) return True except Exception: logger.exception("Failed carving the volume.") return False
def vshadowmount(self): """Method to call vshadowmount and mount NTFS volume shadow copies. :return: string representing the path to the volume shadow copies :raises CommandNotFoundError: if the underlying command does not exist :raises SubSystemError: if the underlying command fails :raises NoMountpointAvailableError: if there is no mountpoint available """ if not _util.command_exists('vshadowmount'): logger.warning("vshadowmount is not installed, could not mount volume shadow copies") raise CommandNotFoundError('vshadowmount') self._make_mountpoint(var_name='vss', suffix="vss", in_paths=True) try: _util.check_call_(["vshadowmount", "-o", str(self.offset), self.get_raw_path(), self._paths['vss']]) return self._paths['vss'] except Exception as e: logger.exception("Failed mounting the volume shadow copies.") raise SubsystemError(e)
def is_raid(self): """Tests whether this image (was) part of a RAID array. Requires :command:`mdadm` to be installed.""" if not _util.command_exists('mdadm'): logger.info("mdadm not installed, could not detect RAID") return False # Scan for new lvm volumes # noinspection PyBroadException try: result = _util.check_output_(["mdadm", "--examine", self.get_raw_path()], stderr=subprocess.STDOUT) for l in result.splitlines(): if 'Raid Level' in l: logger.debug("Detected RAID level " + l[l.index(':') + 2:]) break else: return False except Exception: return False return True
def detect_volume_shadow_copies(self): """Method to call vshadowmount and mount NTFS volume shadow copies. :return: iterable with the :class:`Volume` objects of the VSS :raises CommandNotFoundError: if the underlying command does not exist :raises SubSystemError: if the underlying command fails :raises NoMountpointAvailableError: if there is no mountpoint available """ if not _util.command_exists('vshadowmount'): logger.warning("vshadowmount is not installed, could not mount volume shadow copies") raise CommandNotFoundError('vshadowmount') self._make_mountpoint(var_name='vss', suffix="vss", in_paths=True) try: _util.check_call_(["vshadowmount", "-o", str(self.offset), self.get_raw_path(), self._paths['vss']]) except Exception as e: logger.exception("Failed mounting the volume shadow copies.") raise SubsystemError(e) else: return self.volumes.detect_volumes(vstype='vss')
def _load_disktype_data(self): """Calls the :command:`disktype` command and obtains the disk GUID from GPT volume systems. As we are running the tool anyway, the label is also extracted from the tool if it is not yet set. The disktype data is only loaded and not assigned to volumes yet. """ if not _util.command_exists('disktype'): logger.warning( "disktype not installed, could not detect volume type") return None disktype = _util.check_output_( ['disktype', self.parent.get_raw_path()]).strip() current_partition = None for line in disktype.splitlines(): if not line: continue # noinspection PyBroadException try: line = line.strip() find_partition_nr = re.match(r"^Partition (\d+):", line) if find_partition_nr: current_partition = int(find_partition_nr.group(1)) elif current_partition is not None: if line.startswith("Type ") and "GUID" in line: self._disktype[current_partition]['guid'] = \ line[line.index('GUID') + 5:-1].strip() # output is between () elif line.startswith("Partition Name "): self._disktype[current_partition]['label'] = \ line[line.index('Name ') + 6:-1].strip() # output is between "" except Exception: logger.exception("Error while parsing disktype output") return
def main(): class MyParser(argparse.ArgumentParser): def error(self, message): sys.stderr.write('error: {0}\n'.format(message)) self.print_help() sys.exit(2) parser = MyParser( description='Utility to mount volumes in Encase and dd images locally.' ) parser.add_argument( 'images', nargs='*', help= 'path(s) to the image(s) that you want to mount; generally just the first file (e.g. ' 'the .E01 or .001 file) or the folder containing the files is enough in the case of ' 'split files') # Special options parser.add_argument('--version', action='version', version=__version__, help='display version and exit') parser.add_argument( '--check', action=CheckAction, nargs=0, help='do a system check and list which tools are installed') parser.add_argument('-i', '--interactive', action='store_true', default=False, help='enter the interactive shell') # Utility specific parser.add_argument( '-u', '--unmount', action='store_true', default=False, help= 'try to unmount left-overs of previous imount runs; may occasionally not be able to ' 'detect all mountpoints or detect too much mountpoints; use --casename to limit ' 'the unmount options') parser.add_argument('-w', '--wait', action='store_true', default=False, help='pause on some additional warnings') parser.add_argument('-k', '--keep', action='store_true', default=False, help='keep volumes mounted after program exits') parser.add_argument('--no-interaction', action='store_true', default=False, help="do not ask for any user input, implies --keep") parser.add_argument( '-o', '--only-mount', default=None, help="specify which volume(s) you want to mount, comma-separated") parser.add_argument('-v', '--verbose', action='count', default=False, help='enable verbose output') parser.add_argument('-c', '--color', action='store_true', default=False, help='force colorizing the output') parser.add_argument('--no-color', action='store_true', default=False, help='prevent colorizing the output') # Additional options parser.add_argument( '-r', '--reconstruct', action='store_true', default=False, help= 'attempt to reconstruct the full filesystem tree; implies -s and mounts all partitions ' 'at once') parser.add_argument( '--carve', action='store_true', default=False, help= 'automatically carve the free space of a mounted volume for deleted files' ) parser.add_argument('--vshadow', action='store_true', default=False, help='automatically mount volume shadow copies') # Specify options to the subsystem parser.add_argument('-md', '--mountdir', default=None, help='specify other directory for volume mountpoints') parser.add_argument( '-p', '--pretty', action='store_true', default=False, help= 'use pretty names for mount points; useful in combination with --mountdir' ) parser.add_argument( '-cn', '--casename', default=None, help= 'name to add to the --mountdir, often used in conjunction with --pretty' ) parser.add_argument( '-rw', '--read-write', action='store_true', default=False, help= 'mount image read-write by creating a local write-cache file in a temp directory; ' 'implies --disk-mounter=xmount') parser.add_argument( '-m', '--disk-mounter', choices=DISK_MOUNTERS, default='auto', help= 'use other tool to mount the initial images; results may vary between methods and if ' 'something doesn\'t work, try another method; dummy can be used when base should not be ' 'mounted (default: auto)') parser.add_argument( '-d', '--volume-detector', choices=['pytsk3', 'mmls', 'parted', 'auto'], default='auto', help= 'use other volume detection method; pytsk3 and mmls should provide identical results, ' 'though pytsk3 is using the direct C API of mmls, but requires pytsk3 to be installed; ' 'auto distinguishes between pytsk3 and mmls only ' '(default: auto)') parser.add_argument( '--vstypes', action=AppendDictAction, default={'*': 'detect'}, help= 'specify type of volume system (partition table); if you don\'t know, ' 'use "detect" to try to detect (default: detect)') parser.add_argument( '--fstypes', action=AppendDictAction, default={'?': 'unknown'}, help= "allows the specification of the file system type per volume number; format: 0.1=lvm,...; " "use volume number ? for all undetected file system types and * for all file systems; " "accepted file systems types are {}".format( ", ".join(FILE_SYSTEM_TYPES)) + ", and none only for the ? volume (defaults to unknown)") parser.add_argument( '--keys', action=AppendDictAction, default={}, help= "allows the specification of key material per volume number; format: 0.1=p:pass,...; " "exact format depends on volume type") # Toggles for default settings you may perhaps want to override toggroup = parser.add_argument_group('toggles') toggroup.add_argument( '--single', action='store_true', default=False, help= "do not try to find a volume system, but assume the image contains a single volume" ) toggroup.add_argument( '--no-single', action='store_true', default=False, help= "prevent trying to mount the image as a single volume if no volume system was found" ) args = parser.parse_args() col = get_coloring_func(color=args.color, no_color=args.color) # Set logging level for internal Python handler = logging.StreamHandler() handler.setFormatter(ImageMounterFormatter(col, verbosity=args.verbose)) logger = logging.getLogger("imagemounter") logger.setLevel({ 0: logging.CRITICAL, 1: logging.WARNING, 2: logging.INFO }.get(args.verbose, logging.DEBUG)) logger.addHandler(handler) # Check some prerequisites if os.geteuid(): # Not run as root print(col('[!] Not running as root!', 'yellow')) if 'a' in __version__ or 'b' in __version__: print( col("Development release v{0}. Please report any bugs you encounter." .format(__version__), attrs=['dark'])) print( col("Bug reports: use -vvvv to get maximum verbosity and include imount --check output in your report", attrs=['dark'])) print( col("Critical bug? Use git tag to list all versions and use git checkout <version>", attrs=['dark'])) # Make args.single default to None if args.single == args.no_single: args.single = None elif args.single: args.single = True elif args.no_single: args.single = False # If --no-interaction is specified, imply --keep and not --wait if args.no_interaction: args.keep = True if args.wait: print( col( "[!] --no-interaction can't be used in conjunction with --wait", 'yellow')) args.wait = False # Check if mount method supports rw if args.disk_mounter not in ('xmount', 'auto') and args.read_write: print( col( "[!] {0} does not support mounting read-write! Will mount read-only." .format(args.disk_mounter), 'yellow')) args.read_write = False # Check if mount method is available mount_command = 'avfsd' if args.disk_mounter == 'avfs' else args.disk_mounter if args.disk_mounter not in ( 'auto', 'dummy') and not _util.command_exists(mount_command): print(col("[-] {0} is not installed!".format(args.disk_mounter), 'red')) sys.exit(1) elif args.disk_mounter == 'auto' and not any( map(_util.command_exists, ('xmount', 'affuse', 'ewfmount', 'vmware-mount', 'avfsd'))): print( col( "[-] No tools installed to mount the image base! Please install xmount, affuse (afflib-tools), " "ewfmount (ewf-tools), vmware-mount or avfs first.", 'red')) sys.exit(1) # Check if detection method is available if args.volume_detector == 'pytsk3' and not _util.module_exists('pytsk3'): print(col("[-] pytsk3 module does not exist!", 'red')) sys.exit(1) elif args.volume_detector in ('mmls', 'parted') and not _util.command_exists( args.volume_detector): print( col("[-] {0} is not installed!".format(args.volume_detector), 'red')) sys.exit(1) elif args.volume_detector == 'auto' and not any( (_util.module_exists('pytsk3'), _util.command_exists('mmls'), _util.command_exists('parted'))): print( col( "[-] No tools installed to detect volumes! Please install mmls (sleuthkit), pytsk3 or parted first.", 'red')) sys.exit(1) if args.fstypes: for k, v in args.fstypes.items(): if v.strip() not in FILE_SYSTEM_TYPES and v.strip() not in VOLUME_SYSTEM_TYPES \ and not (k == '?' and v.strip().lower() == 'none'): print( "[!] Error while parsing --fstypes: {} is invalid".format( v)) sys.exit(1) if '*' in args.fstypes: print( "[!] You are forcing the file system type to {0}. This may cause unexpected results." .format(args.fstypes['*'])) elif '?' in args.fstypes and args.fstypes['?'] not in ('unknown', 'none'): print( "[!] You are using the file system type {0} as fallback. This may cause unexpected results." .format(args.fstypes['?'])) if args.only_mount: args.only_mount = args.only_mount.split(',') if args.vstypes: for k, v in args.vstypes.items(): if v.strip() not in VOLUME_SYSTEM_TYPES: print( "[!] Error while parsing --vstypes: {} is invalid".format( v)) sys.exit(1) if args.carve and not _util.command_exists('photorec'): print( col( "[-] The photorec command (part of testdisk package) is required to carve, but is not " "installed. Carving will be disabled.", 'yellow')) args.carve = False if args.vshadow and not _util.command_exists('vshadowmount'): print( col( "[-] The vhadowmount command is required to mount volume shadow copies, but is not " "installed. Mounting volume shadow copies will be disabled.", 'yellow')) args.vshadow = False if (args.interactive or not args.images) and not args.unmount: from imagemounter.cli.shell import main main() return if args.unmount: unmounter = Unmounter(**vars(args)) commands = unmounter.preview_unmount() if not commands: print("[+] Nothing to do") parser.exit() print( "[!] --unmount will rigorously clean anything that looks like a mount or volume group originating " "from this utility. You may regret using this if you have other mounts or volume groups that are " "similarly named. The following commands will be executed:") for c in commands: print(" {0}".format(c)) try: input(">>> Press [enter] to continue or ^C to cancel... ") unmounter.unmount() except KeyboardInterrupt: print("\n[-] Aborted.") sys.exit(0) # Enumerate over all images in the CLI images = [] for num, image in enumerate(args.images): # If is a directory, find a E01 file in the directory if os.path.isdir(image): for f in glob.glob(os.path.join(image, '*.[E0]01')): images.append(f) break else: print( col( "[-] {0} is a directory not containing a .001 or .E01 file, aborting!" .format(image), "red")) break continue elif not os.path.exists(image): print( col("[-] Image {0} does not exist, aborting!".format(image), "red")) break images.append(image) else: p = None try: p = ImageParser(images, **vars(args)) num = 0 # Mount all disks. We could use .init, but where's the fun in that? for disk in p.disks: num += 1 print('[+] Mounting image {0} using {1}...'.format( disk.paths[0], disk.disk_mounter)) # Mount the base image using the preferred method try: disk.mount() except ImageMounterError: print( col( "[-] Failed mounting base image. Perhaps try another mount method than {0}?" .format(disk.disk_mounter), "red")) return if args.read_write: print('[+] Created read-write cache at {0}'.format( disk.rwpath)) disk.volumes.preload_volume_data() print('[+] Mounted raw image [{num}/{total}]'.format( num=num, total=len(args.images))) sys.stdout.write("[+] Mounting volume...\r") sys.stdout.flush() has_left_mounted = False for volume in p.init_volumes(args.single, args.only_mount, swallow_exceptions=True): try: # something failed? if not volume.mountpoint and not volume.loopback: if volume.exception and volume.size is not None and volume.size <= 1048576: print( col( '[-] Exception while mounting small volume {0}' .format(volume.get_description()), 'yellow')) if args.wait: input( col('>>> Press [enter] to continue... ', attrs=['dark'])) elif isinstance(volume.exception, UnsupportedFilesystemError ) and volume.fstype == 'swap': print( col( '[-] Exception while mounting swap volume {0}' .format(volume.get_description()), 'yellow')) if args.wait: input( col('>>> Press [enter] to continue... ', attrs=['dark'])) elif volume.exception: print( col( '[-] Exception while mounting {0}'.format( volume.get_description()), 'red')) if not args.no_interaction: input( col('>>> Press [enter] to continue... ', attrs=['dark'])) elif volume.flag != 'alloc': if args.wait or args.verbose: # do not show skipped messages by default print( col( '[-] Skipped {0} {1} volume'.format( volume.get_description(), volume.flag), 'yellow')) if args.wait: input( col('>>> Press [enter] to continue... ', attrs=['dark'])) elif not volume._should_mount(args.only_mount): print( col( '[-] Skipped {0}'.format( volume.get_description()), 'yellow')) else: print( col( '[-] Could not mount volume {0}'.format( volume.get_description()), 'yellow')) if args.wait: input( col('>>> Press [enter] to continue... ', attrs=['dark'])) if args.carve and volume.flag in ('alloc', 'unalloc'): sys.stdout.write("[+] Carving volume...\r") sys.stdout.flush() try: path = volume.carve(freespace=False) except ImageMounterError: print(col('[-] Carving failed.', 'red')) else: print('[+] Carved data is available at {0}.'. format(col(path, 'green', attrs=['bold']))) else: continue # we do not need the unmounting sequence else: # it all was ok if volume.mountpoint: print('[+] Mounted volume {0} on {1}.'.format( col(volume.get_description(), attrs=['bold']), col(volume.mountpoint, 'green', attrs=['bold']))) elif volume.loopback: # fallback, generally indicates error. print('[+] Mounted volume {0} as loopback on {1}.'. format( col(volume.get_description(), attrs=['bold']), col(volume.loopback, 'green', attrs=['bold']))) print( col( '[-] Could not detect further volumes in the loopback device.', 'red')) if args.carve: sys.stdout.write("[+] Carving volume...\r") sys.stdout.flush() try: path = volume.carve() except ImageMounterError: print(col('[-] Carving failed.', 'red')) else: print('[+] Carved data is available at {0}.'. format(col(path, 'green', attrs=['bold']))) if args.vshadow and volume.fstype == 'ntfs': sys.stdout.write( "[+] Mounting volume shadow copies...\r") sys.stdout.flush() try: volumes = volume.detect_volume_shadow_copies() except ImageMounterError: print( col( '[-] Volume shadow copies could not be mounted.', 'red')) else: for v in volumes: try: v.init_volume() except ImageMounterError: print( col( '[-] Volume shadow copy {} not mounted' .format(v), 'red')) else: print( '[+] Volume shadow copy available at {0}.' .format( col(v.mountpoint, 'green', attrs=['bold']))) # Do not offer unmount when reconstructing if args.reconstruct or args.keep: has_left_mounted = True continue input( col('>>> Press [enter] to unmount the volume, or ^C to keep mounted... ', attrs=['dark'])) # Case where image should be unmounted, but has failed to do so. Keep asking whether the user wants # to unmount. while True: try: volume.unmount() break except ImageMounterError: try: print( col( "[-] Error unmounting volume. Perhaps files are still open?", "red")) input( col('>>> Press [enter] to retry unmounting, or ^C to skip... ', attrs=['dark'])) except KeyboardInterrupt: has_left_mounted = True print("") break except KeyboardInterrupt: has_left_mounted = True print("") sys.stdout.write("[+] Mounting volume...\r") sys.stdout.flush() for disk in p.disks: if [x for x in disk.volumes if x.was_mounted] == 0: if disk.vstype != 'detect': print( col( '[?] Could not determine volume information of {0}. Image may be empty, ' 'or volume system type {0} was incorrect.'. format(disk.vstype.upper()), 'yellow')) elif args.single is False: print( col( '[?] Could not determine volume information. Image may be empty, or volume system ' 'type could not be detected. Try explicitly providing the volume system type with ' '--vstypes or mounting as a single volume with --single', 'yellow')) else: print( col( '[?] Could not determine volume information. Image may be empty, or volume system ' 'type could not be detected. Try explicitly providing the volume system type with ' '--vstypes.', 'yellow')) if args.wait: input( col('>>> Press [enter] to continue... ', attrs=['dark'])) print('[+] Parsed all volumes!') # Perform reconstruct if required if args.reconstruct: # Reverse order so '/' gets unmounted last print("[+] Performing reconstruct... ") try: root = p.reconstruct() except NoRootFoundError: print( col( "[-] Failed reconstructing filesystem: could not find root directory.", 'red')) else: failed = [] for disk in p.disks: failed.extend([ x for x in disk.volumes if 'bindmounts' not in x._paths and x.mountpoint and x != root ]) if failed: print( "[+] Parts of the filesystem are reconstructed in {0}." .format( col(root.mountpoint, "green", attrs=["bold"]))) for m in failed: print(" {0} was not reconstructed".format( m.mountpoint)) else: print( "[+] The entire filesystem is reconstructed in {0}." .format( col(root.mountpoint, "green", attrs=["bold"]))) if not args.keep: input( col(">>> Press [enter] to unmount all volumes... ", attrs=['dark'])) elif has_left_mounted and not args.keep: input( col(">>> Some volumes were left mounted. Press [enter] to unmount all... ", attrs=['dark'])) except KeyboardInterrupt: print('\n[+] User pressed ^C, aborting...') return except Exception as e: import traceback traceback.print_exc() print(col("[-] {0}".format(e), 'red')) if not args.no_interaction: input(col(">>> Press [enter] to continue.", attrs=['dark'])) finally: if args.keep: print("[+] Analysis complete.") elif not p: print("[-] Crashed before creating parser") else: print('[+] Analysis complete, unmounting...') # All done with this image, unmount it try: remove_rw = p.rw_active() and 'y' in input( '>>> Delete the rw cache file? [y/N] ').lower() except KeyboardInterrupt: remove_rw = False while True: try: p.clean(remove_rw) print("[+] All cleaned up") break except ImageMounterError: try: print( col( "[-] Error unmounting base image. Perhaps volumes are still open?", 'red')) input( col('>>> Press [enter] to retry unmounting, or ^C to cancel... ', attrs=['dark'])) except KeyboardInterrupt: print("") # ^C does not print \n break
def is_available(self): """Whether the command is available on the system. :rtype: bool """ return _util.command_exists(self.name)
def add_method_if_exists(method): if (method == 'avfs' and _util.command_exists('avfsd')) or \ (method == 'nbd' and _util.command_exists('qemu-nbd')) or \ _util.command_exists(method): methods.append(method)
def main(): class MyParser(argparse.ArgumentParser): def error(self, message): sys.stderr.write('error: {0}\n'.format(message)) self.print_help() sys.exit(2) parser = MyParser(description='Utility to mount volumes in Encase and dd images locally.') parser.add_argument('images', nargs='*', help='path(s) to the image(s) that you want to mount; generally just the first file (e.g. ' 'the .E01 or .001 file) or the folder containing the files is enough in the case of ' 'split files') # Special options parser.add_argument('--version', action='version', version=__version__, help='display version and exit') parser.add_argument('--check', action=CheckAction, nargs=0, help='do a system check and list which tools are installed') parser.add_argument('-i', '--interactive', action='store_true', default=False, help='enter the interactive shell') # Utility specific parser.add_argument('-u', '--unmount', action='store_true', default=False, help='try to unmount left-overs of previous imount runs; may occasionally not be able to ' 'detect all mountpoints or detect too much mountpoints; use --casename to limit ' 'the unmount options') parser.add_argument('-w', '--wait', action='store_true', default=False, help='pause on some additional warnings') parser.add_argument('-k', '--keep', action='store_true', default=False, help='keep volumes mounted after program exits') parser.add_argument('--no-interaction', action='store_true', default=False, help="do not ask for any user input, implies --keep") parser.add_argument('-o', '--only-mount', default=None, help="specify which volume(s) you want to mount, comma-separated") parser.add_argument('--skip', default=None, help="specify which volume(s) you do not want to mount, comma-separated") parser.add_argument('-v', '--verbose', action='count', default=False, help='enable verbose output') parser.add_argument('-c', '--color', action='store_true', default=False, help='force colorizing the output') parser.add_argument('--no-color', action='store_true', default=False, help='prevent colorizing the output') # Additional options parser.add_argument('-r', '--reconstruct', action='store_true', default=False, help='attempt to reconstruct the full filesystem tree; implies -s and mounts all partitions ' 'at once') parser.add_argument('--carve', action='store_true', default=False, help='automatically carve the free space of a mounted volume for deleted files') parser.add_argument('--vshadow', action='store_true', default=False, help='automatically mount volume shadow copies') # Specify options to the subsystem parser.add_argument('-md', '--mountdir', default=None, help='specify other directory for volume mountpoints') parser.add_argument('-p', '--pretty', action='store_true', default=False, help='use pretty names for mount points; useful in combination with --mountdir') parser.add_argument('-cn', '--casename', default=None, help='name to add to the --mountdir, often used in conjunction with --pretty') parser.add_argument('-rw', '--read-write', action='store_true', default=False, help='mount image read-write by creating a local write-cache file in a temp directory; ' 'implies --disk-mounter=xmount') parser.add_argument('-m', '--disk-mounter', choices=DISK_MOUNTERS, default='auto', help='use other tool to mount the initial images; results may vary between methods and if ' 'something doesn\'t work, try another method; dummy can be used when base should not be ' 'mounted (default: auto)') parser.add_argument('-d', '--volume-detector', choices=['pytsk3', 'mmls', 'parted', 'auto'], default='auto', help='use other volume detection method; pytsk3 and mmls should provide identical results, ' 'though pytsk3 is using the direct C API of mmls, but requires pytsk3 to be installed; ' 'auto distinguishes between pytsk3 and mmls only ' '(default: auto)') parser.add_argument('--vstypes', action=AppendDictAction, default={'*': 'detect'}, help='specify type of volume system (partition table); if you don\'t know, ' 'use "detect" to try to detect (default: detect)') parser.add_argument('--fstypes', action=AppendDictAction, default={'?': 'unknown'}, help="allows the specification of the file system type per volume number; format: 0.1=lvm,...; " "use volume number ? for all undetected file system types and * for all file systems; " "accepted file systems types are {}".format(", ".join(FILE_SYSTEM_TYPES)) + ", and none only for the ? volume (defaults to unknown)") parser.add_argument('--keys', action=AppendDictAction, default={}, help="allows the specification of key material per volume number; format: 0.1=p:pass,...; " "exact format depends on volume type", allow_commas=False) parser.add_argument('--lazy-unmount', action='store_true', default=False, help="enables lazily unmounting volumes and disks if direct unmounting fails") # Toggles for default settings you may perhaps want to override toggroup = parser.add_argument_group('toggles') toggroup.add_argument('--single', action='store_true', default=False, help="do not try to find a volume system, but assume the image contains a single volume") toggroup.add_argument('--no-single', action='store_true', default=False, help="prevent trying to mount the image as a single volume if no volume system was found") args = parser.parse_args() col = get_coloring_func(color=args.color, no_color=args.color) # Set logging level for internal Python handler = ImageMounterStreamHandler(col, args.verbose) logger = logging.getLogger("imagemounter") logger.setLevel({0: logging.CRITICAL, 1: logging.WARNING, 2: logging.INFO}.get(args.verbose, logging.DEBUG)) logger.addHandler(handler) # Check some prerequisites if os.geteuid(): # Not run as root print(col('[!] Not running as root!', 'yellow')) if 'a' in __version__ or 'b' in __version__: print(col("Development release v{0}. Please report any bugs you encounter.".format(__version__), attrs=['dark'])) print(col("Bug reports: use -vvvv to get maximum verbosity and include imount --check output in your report", attrs=['dark'])) print(col("Critical bug? Use git tag to list all versions and use git checkout <version>", attrs=['dark'])) # Make args.single default to None if args.single == args.no_single: args.single = None elif args.single: args.single = True elif args.no_single: args.single = False # If --no-interaction is specified, imply --keep and not --wait if args.no_interaction: args.keep = True if args.wait: print(col("[!] --no-interaction can't be used in conjunction with --wait", 'yellow')) args.wait = False # Check if mount method supports rw if args.disk_mounter not in ('xmount', 'auto') and args.read_write: print(col("[!] {0} does not support mounting read-write! Will mount read-only.".format(args.disk_mounter), 'yellow')) args.read_write = False # Check if mount method is available mount_command = 'avfsd' if args.disk_mounter == 'avfs' else args.disk_mounter if args.disk_mounter not in ('auto', 'dummy') and not _util.command_exists(mount_command): print(col("[-] {0} is not installed!".format(args.disk_mounter), 'red')) sys.exit(1) elif args.disk_mounter == 'auto' and not any(map(_util.command_exists, ('xmount', 'affuse', 'ewfmount', 'vmware-mount', 'avfsd'))): print(col("[-] No tools installed to mount the image base! Please install xmount, affuse (afflib-tools), " "ewfmount (ewf-tools), vmware-mount or avfs first.", 'red')) sys.exit(1) # Check if detection method is available if args.volume_detector == 'pytsk3' and not _util.module_exists('pytsk3'): print(col("[-] pytsk3 module does not exist!", 'red')) sys.exit(1) elif args.volume_detector in ('mmls', 'parted') and not _util.command_exists(args.volume_detector): print(col("[-] {0} is not installed!".format(args.volume_detector), 'red')) sys.exit(1) elif args.volume_detector == 'auto' and not any((_util.module_exists('pytsk3'), _util.command_exists('mmls'), _util.command_exists('parted'))): print(col("[-] No tools installed to detect volumes! Please install mmls (sleuthkit), pytsk3 or parted first.", 'red')) sys.exit(1) if args.fstypes: for k, v in args.fstypes.items(): if v.strip() not in FILE_SYSTEM_TYPES and v.strip() not in VOLUME_SYSTEM_TYPES \ and not (k == '?' and v.strip().lower() == 'none'): print("[!] Error while parsing --fstypes: {} is invalid".format(v)) sys.exit(1) if '*' in args.fstypes: print("[!] You are forcing the file system type to {0}. This may cause unexpected results." .format(args.fstypes['*'])) elif '?' in args.fstypes and args.fstypes['?'] not in ('unknown', 'none'): print("[!] You are using the file system type {0} as fallback. This may cause unexpected results." .format(args.fstypes['?'])) if args.only_mount: args.only_mount = args.only_mount.split(',') if args.skip: args.skip = args.skip.split(',') if args.vstypes: for k, v in args.vstypes.items(): if v.strip() not in VOLUME_SYSTEM_TYPES: print("[!] Error while parsing --vstypes: {} is invalid".format(v)) sys.exit(1) if args.carve and not _util.command_exists('photorec'): print(col("[-] The photorec command (part of testdisk package) is required to carve, but is not " "installed. Carving will be disabled.", 'yellow')) args.carve = False if args.vshadow and not _util.command_exists('vshadowmount'): print(col("[-] The vhadowmount command is required to mount volume shadow copies, but is not " "installed. Mounting volume shadow copies will be disabled.", 'yellow')) args.vshadow = False if (args.interactive or not args.images) and not args.unmount: from imagemounter.cli.shell import main main() return if args.unmount: unmounter = Unmounter(**vars(args)) commands = unmounter.preview_unmount() if not commands: print("[+] Nothing to do") parser.exit() print("[!] --unmount will rigorously clean anything that looks like a mount or volume group originating " "from this utility. You may regret using this if you have other mounts or volume groups that are " "similarly named. The following commands will be executed:") for c in commands: print(" {0}".format(c)) try: input(">>> Press [enter] to continue or ^C to cancel... ") unmounter.unmount() except KeyboardInterrupt: print("\n[-] Aborted.") sys.exit(0) # Enumerate over all images in the CLI images = [] for num, image in enumerate(args.images): # If is a directory, find a E01 file in the directory if os.path.isdir(image): for f in glob.glob(os.path.join(image, '*.[Ee0]01')): images.append(f) break else: print(col("[-] {0} is a directory not containing a .001 or .E01 file, aborting!".format(image), "red")) break continue elif not os.path.exists(image): print(col("[-] Image {0} does not exist, aborting!".format(image), "red")) break images.append(image) else: p = None try: p = ImageParser(images, **vars(args)) num = 0 # Mount all disks. We could use .init, but where's the fun in that? for disk in p.disks: num += 1 print('[+] Mounting image {0} using {1}...'.format(disk.paths[0], disk.disk_mounter)) # Mount the base image using the preferred method try: disk.mount() except ImageMounterError: print(col("[-] Failed mounting base image. Perhaps try another mount method than {0}?" .format(disk.disk_mounter), "red")) return if args.read_write: print('[+] Created read-write cache at {0}'.format(disk.rwpath)) disk.volumes.preload_volume_data() print('[+] Mounted raw image [{num}/{total}]'.format(num=num, total=len(args.images))) sys.stdout.write("[+] Mounting volume...\r") sys.stdout.flush() has_left_mounted = False for volume in p.init_volumes(args.single, args.only_mount, args.skip, swallow_exceptions=True): try: # something failed? if not volume.mountpoint and not volume.loopback: if volume.exception and volume.size is not None and volume.size <= 1048576: print(col('[-] Exception while mounting small volume {0}'.format(volume.get_description()), 'yellow')) if args.wait: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) elif isinstance(volume.exception, UnsupportedFilesystemError) and volume.fstype == 'swap': print(col('[-] Exception while mounting swap volume {0}'.format(volume.get_description()), 'yellow')) if args.wait: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) elif volume.exception: print(col('[-] Exception while mounting {0}'.format(volume.get_description()), 'red')) if not args.no_interaction: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) elif volume.flag != 'alloc': if args.wait or args.verbose: # do not show skipped messages by default print(col('[-] Skipped {0} {1} volume' .format(volume.get_description(), volume.flag), 'yellow')) if args.wait: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) elif not volume._should_mount(args.only_mount): print(col('[-] Skipped {0}'.format(volume.get_description()), 'yellow')) else: print(col('[-] Could not mount volume {0}'.format(volume.get_description()), 'yellow')) if args.wait: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) if args.carve and volume.flag in ('alloc', 'unalloc'): sys.stdout.write("[+] Carving volume...\r") sys.stdout.flush() try: path = volume.carve(freespace=False) except ImageMounterError: print(col('[-] Carving failed.', 'red')) else: print('[+] Carved data is available at {0}.'.format(col(path, 'green', attrs=['bold']))) else: continue # we do not need the unmounting sequence else: # it all was ok if volume.mountpoint: print('[+] Mounted volume {0} on {1}.'.format(col(volume.get_description(), attrs=['bold']), col(volume.mountpoint, 'green', attrs=['bold']))) elif volume.loopback: # fallback, generally indicates error. print('[+] Mounted volume {0} as loopback on {1}.'.format(col(volume.get_description(), attrs=['bold']), col(volume.loopback, 'green', attrs=['bold']))) print(col('[-] Could not detect further volumes in the loopback device.', 'red')) if args.carve: sys.stdout.write("[+] Carving volume...\r") sys.stdout.flush() try: path = volume.carve() except ImageMounterError: print(col('[-] Carving failed.', 'red')) else: print('[+] Carved data is available at {0}.'.format(col(path, 'green', attrs=['bold']))) if args.vshadow and volume.fstype == 'ntfs': sys.stdout.write("[+] Mounting volume shadow copies...\r") sys.stdout.flush() try: volumes = volume.detect_volume_shadow_copies() except ImageMounterError: print(col('[-] Volume shadow copies could not be mounted.', 'red')) else: for v in volumes: try: v.init_volume() except ImageMounterError: print(col('[-] Volume shadow copy {} not mounted'.format(v), 'red')) else: print('[+] Volume shadow copy available at {0}.'.format(col(v.mountpoint, 'green', attrs=['bold']))) # Do not offer unmount when reconstructing if args.reconstruct or args.keep: has_left_mounted = True continue input(col('>>> Press [enter] to unmount the volume, or ^C to keep mounted... ', attrs=['dark'])) # Case where image should be unmounted, but has failed to do so. Keep asking whether the user wants # to unmount. while True: try: volume.unmount(allow_lazy=args.lazy_unmount) break except ImageMounterError: try: print(col("[-] Error unmounting volume. Perhaps files are still open?", "red")) input(col('>>> Press [enter] to retry unmounting, or ^C to skip... ', attrs=['dark'])) except KeyboardInterrupt: has_left_mounted = True print("") break except KeyboardInterrupt: has_left_mounted = True print("") sys.stdout.write("[+] Mounting volume...\r") sys.stdout.flush() for disk in p.disks: if [x for x in disk.volumes if x.was_mounted] == 0: if disk.vstype != 'detect': print(col('[?] Could not determine volume information of {0}. Image may be empty, ' 'or volume system type {0} was incorrect.'.format(disk.vstype.upper()), 'yellow')) elif args.single is False: print(col('[?] Could not determine volume information. Image may be empty, or volume system ' 'type could not be detected. Try explicitly providing the volume system type with ' '--vstypes or mounting as a single volume with --single', 'yellow')) else: print(col('[?] Could not determine volume information. Image may be empty, or volume system ' 'type could not be detected. Try explicitly providing the volume system type with ' '--vstypes.', 'yellow')) if args.wait: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) print('[+] Parsed all volumes!') # Perform reconstruct if required if args.reconstruct: # Reverse order so '/' gets unmounted last print("[+] Performing reconstruct... ") try: root = p.reconstruct() except NoRootFoundError: print(col("[-] Failed reconstructing filesystem: could not find root directory.", 'red')) else: failed = [] for disk in p.disks: failed.extend([x for x in disk.volumes if 'bindmounts' not in x._paths and x.mountpoint and x != root]) if failed: print("[+] Parts of the filesystem are reconstructed in {0}.".format(col(root.mountpoint, "green", attrs=["bold"]))) for m in failed: print(" {0} was not reconstructed".format(m.mountpoint)) else: print("[+] The entire filesystem is reconstructed in {0}.".format(col(root.mountpoint, "green", attrs=["bold"]))) if not args.keep: input(col(">>> Press [enter] to unmount all volumes... ", attrs=['dark'])) elif has_left_mounted and not args.keep: input(col(">>> Some volumes were left mounted. Press [enter] to unmount all... ", attrs=['dark'])) except KeyboardInterrupt: print('\n[+] User pressed ^C, aborting...') return except Exception as e: import traceback traceback.print_exc() print(col("[-] {0}".format(e), 'red')) if not args.no_interaction: input(col(">>> Press [enter] to continue.", attrs=['dark'])) finally: if args.keep: print("[+] Analysis complete.") elif not p: print("[-] Crashed before creating parser") else: print('[+] Analysis complete, unmounting...') # All done with this image, unmount it try: remove_rw = p.rw_active() and 'y' in input('>>> Delete the rw cache file? [y/N] ').lower() except KeyboardInterrupt: remove_rw = False while True: try: p.clean(remove_rw, allow_lazy=args.lazy_unmount) print("[+] All cleaned up") break except ImageMounterError: try: print(col("[-] Error unmounting base image. Perhaps volumes are still open?", 'red')) input(col('>>> Press [enter] to retry unmounting, or ^C to cancel... ', attrs=['dark'])) except KeyboardInterrupt: print("") # ^C does not print \n break
def __init__(self, parser, path, offset=0, vstype='detect', read_write=False, method='auto', detection='auto', multifile=True, index=None, mount_directories=True, **args): """Instantiation of this class does not automatically mount, detect or analyse the disk. You will need the :func:`init` method for this. :param parser: the parent parser :type parser: :class:`ImageParser` :param int offset: offset of the disk where the volume (system) resides :param str vstype: the volume system type :param bool read_write: indicates whether the disk should be mounted with a read-write cache enabled :param str method: the method to mount the base image with :param str detection: the method to detect volumes in the volume system with :param bool multifile: indicates whether :func:`mount` should attempt to call the underlying mount method with all files of a split file when passing a single file does not work :param str index: the base index of this Disk :param bool mount_directories: indicates whether directories should also be 'mounted' :param args: arguments that should be passed down to :class:`Volume` objects """ self.parser = parser # Find the type and the paths path = os.path.expandvars(os.path.expanduser(path)) if _util.is_encase(path): self.type = 'encase' elif _util.is_vmware(path): self.type = 'vmdk' elif _util.is_compressed(path): self.type = 'compressed' else: self.type = 'dd' self.paths = sorted(_util.expand_path(path)) self.offset = offset self.vstype = vstype.lower() self.block_size = BLOCK_SIZE self.read_write = read_write self.method = method if detection == 'auto': if _util.module_exists('pytsk3'): self.detection = 'pytsk3' elif _util.command_exists('mmls'): self.detection = 'mmls' else: self.detection = 'parted' else: self.detection = detection self.read_write = read_write self.rwpath = "" self.multifile = multifile self.index = index self.mount_directories = mount_directories self.args = args self.name = os.path.split(path)[1] self.mountpoint = '' self.avfs_mountpoint = '' self.volumes = [] self.volume_source = "" self._disktype = defaultdict(dict) self.loopback = "" self.md_device = ""
def main(): class MyParser(argparse.ArgumentParser): def error(self, message): sys.stderr.write('error: {0}\n'.format(message)) self.print_help() sys.exit(2) class CheckAction(argparse.Action): def _check_command(self, command, package="", why=""): if _util.command_exists(command): print(" INSTALLED {}".format(command)) elif why and package: print(" MISSING {:<20}needed for {}, part of the {} package".format(command, why, package)) elif why: print(" MISSING {:<20}needed for {}".format(command, why)) elif package: print(" MISSING {:<20}part of the {} package".format(command, package)) else: print(" MISSING {}".format(command)) def _check_module(self, module, pip_name="", why=""): if not pip_name: pip_name = module if module == "magic" and _util.module_exists(module): import magic if hasattr(magic, 'from_file'): print(" INSTALLED {:<20}(Python package)".format(pip_name)) elif hasattr(magic, 'open'): print(" INSTALLED {:<20}(system package)".format(pip_name)) else: print(" ERROR {:<20}expecting {}, found other module named magic".format(pip_name, pip_name)) elif module != "magic" and _util.module_exists(module): print(" INSTALLED {}".format(pip_name)) elif why: print(" MISSING {:<20}needed for {}, install using pip".format(pip_name, why)) else: print(" MISSING {:<20}install using pip".format(pip_name, why)) # noinspection PyShadowingNames def __call__(self, parser, namespace, values, option_string=None): print("The following commands are used by imagemounter internally. Without most commands, imagemounter " "works perfectly fine, but may lack some detection or mounting capabilities.") print("-- Mounting base disk images (at least one required, first three recommended) --") self._check_command("xmount", "xmount", "several types of disk images") self._check_command("ewfmount", "ewf-tools", "EWF images (partially covered by xmount)") self._check_command("affuse", "afflib-tools", "AFF images (partially covered by xmount)") self._check_command("vmware-mount", why="VMWare disks") print("-- Detecting volumes and volume types (at least one required) --") self._check_command("mmls", "sleuthkit") self._check_module("pytsk3") self._check_command("parted", "parted") print("-- Detecting volume types (all recommended, first two highly recommended) --") self._check_command("fsstat", "sleuthkit") self._check_command("file", "libmagic1") self._check_module("magic", "python-magic") self._check_command("disktype", "disktype") print("-- Enhanced mounting and detecting disks (install when needed) --") self._check_command("mdadm", "mdadm", "RAID disks") self._check_command("cryptsetup", "cryptsetup", "LUKS containers") self._check_command("mountavfs", "avfs", "compressed disk images") print("-- Mounting volumes (install when needed) --") self._check_command("mount.xfs", "xfsprogs", "XFS volumes") self._check_command("mount.ntfs", "ntfs-3g", "NTFS volumes") self._check_command("lvm", "lvm2", "LVM volumes") self._check_command("vmfs-fuse", "vmfs-tools", "VMFS volumes") self._check_command("mount.jffs2", "mtd-tools", "JFFS2 volumes") self._check_command("mount.squashfs", "squashfs-tools", "SquashFS volumes") parser.exit() parser = MyParser(description='Utility to mount volumes in Encase and dd images locally.') parser.add_argument('images', nargs='*', help='path(s) to the image(s) that you want to mount; generally just the first file (e.g. ' 'the .E01 or .001 file) or the folder containing the files is enough in the case of ' 'split files') # Special options parser.add_argument('--version', action='version', version=__version__, help='display version and exit') parser.add_argument('--check', action=CheckAction, nargs=0, help='do a system check and list which tools are installed') # Utility specific parser.add_argument('-u', '--unmount', action='store_true', default=False, help='try to unmount left-overs of previous imount runs; may occasionally not be able to ' 'detect all mountpoints or detect too much mountpoints; use --casename to limit ' 'the unmount options') parser.add_argument('-w', '--wait', action='store_true', default=False, help='pause on some additional warnings') parser.add_argument('-k', '--keep', action='store_true', default=False, help='keep volumes mounted after program exits') parser.add_argument('--no-interaction', action='store_true', default=False, help="do not ask for any user input, implies --keep") parser.add_argument('-v', '--verbose', action='count', default=False, help='enable verbose output') parser.add_argument('-c', '--color', action='store_true', default=False, help='force colorizing the output') parser.add_argument('--no-color', action='store_true', default=False, help='prevent colorizing the output') # Additional options parser.add_argument('-r', '--reconstruct', action='store_true', default=False, help='attempt to reconstruct the full filesystem tree; implies -s and mounts all partitions ' 'at once') parser.add_argument('--carve', action='store_true', default=False, help='automatically carve the free space of a mounted volume for deleted files') # Specify options to the subsystem parser.add_argument('-md', '--mountdir', default=None, help='specify other directory for volume mountpoints') parser.add_argument('-p', '--pretty', action='store_true', default=False, help='use pretty names for mount points; useful in combination with --mountdir') parser.add_argument('-cn', '--casename', default=None, help='name to add to the --mountdir, often used in conjunction with --pretty') parser.add_argument('-rw', '--read-write', action='store_true', default=False, help='mount image read-write by creating a local write-cache file in a temp directory; ' 'implies --method=xmount') parser.add_argument('-m', '--method', choices=['xmount', 'affuse', 'ewfmount', 'vmware-mount', 'avfs', 'auto', 'dummy'], default='auto', help='use other tool to mount the initial images; results may vary between methods and if ' 'something doesn\'t work, try another method; dummy can be used when base should not be ' 'mounted (default: auto)') parser.add_argument('-d', '--detection', choices=['pytsk3', 'mmls', 'parted', 'auto'], default='auto', help='use other volume detection method; pytsk3 and mmls should provide identical results, ' 'though pytsk3 is using the direct C API of mmls, but requires pytsk3 to be installed; ' 'auto distinguishes between pytsk3 and mmls only ' '(default: auto)') parser.add_argument('--vstype', choices=VOLUME_SYSTEM_TYPES, default="detect", help='specify type of volume system (partition table); if you don\'t know, ' 'use "detect" to try to detect (default: detect)') parser.add_argument('--fsfallback', choices=FILE_SYSTEM_TYPES + ('none', ), default='unknown', help="specify fallback type of the filesystem, which is used when it could not be detected or " "is unsupported; use unknown to mount without specifying type") parser.add_argument('--fsforce', action='store_true', default=False, help="force the use of the filesystem type specified with --fsfallback for all volumes") parser.add_argument('--fstypes', default=None, help="allows the specification of the filesystem type per volume number; format: 0.1=lvm, ...") # Toggles for default settings you may perhaps want to override parser.add_argument('--stats', action='store_true', default=False, help='show limited information from fsstat, which will slow down mounting and may cause ' 'random issues such as partitions being unreadable (default)') parser.add_argument('--no-stats', action='store_true', default=False, help='do not show limited information from fsstat') parser.add_argument('--disktype', action='store_true', default=False, help='use the disktype command to get even more information about the volumes (default)') parser.add_argument('--no-disktype', action='store_true', default=False, help='do not use disktype to get more information') parser.add_argument('--raid', action='store_true', default=False, help="try to detect whether the volume is part of a RAID array (default)") parser.add_argument('--no-raid', action='store_true', default=False, help="prevent trying to mount the volume in a RAID array") parser.add_argument('--single', action='store_true', default=False, help="do not try to find a volume system, but assume the image contains a single volume") parser.add_argument('--no-single', action='store_true', default=False, help="prevent trying to mount the image as a single volume if no volume system was found") args = parser.parse_args() # Colorize the output by default if the terminal supports it if not args.color and args.no_color: args.color = False elif args.color: args.color = True else: args.color = _util.terminal_supports_color() if not args.color: # noinspection PyUnusedLocal,PyShadowingNames def col(s, *args, **kwargs): return s else: from termcolor import colored col = colored class ImageMounterFormatter(logging.Formatter): def format(self, record): msg = record.getMessage() if args.verbose >= 4 and record.exc_info: if not record.exc_text: record.exc_text = self.formatException(record.exc_info) if msg[-1:] != "\n": msg += "\n" msg += record.exc_text if record.levelno >= logging.WARNING: return col("[-] " + msg, 'cyan') elif record.levelno == logging.INFO: return col("[+] " + msg, 'cyan') elif msg.startswith('$'): return col(" " + msg, 'cyan') else: return col(" " + msg, 'cyan') # Set logging level for internal Python handler = logging.StreamHandler() handler.setFormatter(ImageMounterFormatter()) logger = logging.getLogger("imagemounter") logger.setLevel({0: logging.CRITICAL, 1: logging.WARNING, 2: logging.INFO}.get(args.verbose, logging.DEBUG)) logger.addHandler(handler) # Check some prerequisites if os.geteuid(): # Not run as root print(col('[!] Not running as root!', 'yellow')) if 'a' in __version__ or 'b' in __version__: print(col("Development release v{0}. Please report any bugs you encounter.".format(__version__), attrs=['dark'])) print(col("Critical bug? Use git tag to list all versions and use git checkout <version>", attrs=['dark'])) # Always assume stats, except when --no-stats is present, and --stats is not. if not args.stats and args.no_stats: args.stats = False else: args.stats = True # Make args.disktype default to True explicit_disktype = False if not args.disktype and args.no_disktype: args.disktype = False else: if args.disktype: explicit_disktype = True args.disktype = True # Make args.raid default to True explicit_raid = False if not args.raid and args.no_raid: args.raid = False else: if args.raid: explicit_raid = True args.raid = True # Make args.single default to None if args.single == args.no_single: args.single = None elif args.single: args.single = True elif args.no_single: args.single = False # If --no-interaction is specified, imply --keep and not --wait if args.no_interaction: args.keep = True if args.wait: print(col("[!] --no-interaction can't be used in conjunction with --wait", 'yellow')) args.wait = False # Check if mount method supports rw if args.method not in ('xmount', 'auto') and args.read_write: print(col("[!] {0} does not support mounting read-write! Will mount read-only.".format(args.method), 'yellow')) args.read_write = False # Check if mount method is available mount_command = 'avfsd' if args.method == 'avfs' else args.method if args.method not in ('auto', 'dummy') and not _util.command_exists(mount_command): print(col("[-] {0} is not installed!".format(args.method), 'red')) sys.exit(1) elif args.method == 'auto' and not any(map(_util.command_exists, ('xmount', 'affuse', 'ewfmount', 'vmware-mount', 'avfsd'))): print(col("[-] No tools installed to mount the image base! Please install xmount, affuse (afflib-tools), " "ewfmount (ewf-tools), vmware-mount or avfs first.", 'red')) sys.exit(1) # Check if detection method is available if args.detection == 'pytsk3' and not _util.module_exists('pytsk3'): print(col("[-] pytsk3 module does not exist!", 'red')) sys.exit(1) elif args.detection in ('mmls', 'parted') and not _util.command_exists(args.detection): print(col("[-] {0} is not installed!".format(args.detection), 'red')) sys.exit(1) elif args.detection == 'auto' and not any((_util.module_exists('pytsk3'), _util.command_exists('mmls'), _util.command_exists('parted'))): print(col("[-] No tools installed to detect volumes! Please install mmls (sleuthkit), pytsk3 or parted first.", 'red')) sys.exit(1) # Check if raid is available if args.raid and not _util.command_exists('mdadm'): if explicit_raid: print(col("[!] RAID mount requires the mdadm command.", 'yellow')) args.raid = False if args.reconstruct and not args.stats: # Reconstruct implies use of fsstat print("[!] You explicitly disabled stats, but --reconstruct implies the use of stats. Stats are re-enabled.") args.stats = True # Check if raid is available if args.disktype and not _util.command_exists('disktype'): if explicit_disktype: print(col("[-] The disktype command can not be used in this session, as it is not installed.", 'yellow')) args.disktype = False if args.stats and not _util.command_exists('fsstat'): print(col("[-] The fsstat command (part of sleuthkit package) is required to obtain stats, but is not " "installed. Stats can not be obtained during this session.", 'yellow')) args.stats = False if args.reconstruct: print(col("[-] Reconstruction requires stats to be obtained, but stats can not be enabled.", 'red')) sys.exit(1) if args.fsforce: if not args.fsfallback or args.fsfallback == 'none': print("[-] You are forcing a file system type, but have not specified the type to use. Ignoring force.") args.fsforce = False else: print("[!] You are forcing the file system type to {0}. This may cause unexpected results." .format(args.fsfallback)) elif args.fsfallback and args.fsfallback not in ('unknown', 'none'): print("[!] You are using the file system type {0} as fallback. This may cause unexpected results." .format(args.fsfallback)) if args.fstypes: try: fstypes = {} # noinspection PyUnresolvedReferences types = args.fstypes.split(',') for typ in types: idx, fstype = typ.split('=', 1) if fstype.strip() not in FILE_SYSTEM_TYPES: print("[!] Error while parsing --fstypes: {} is invalid".format(fstype)) else: fstypes[idx.strip()] = fstype.strip() args.fstypes = fstypes except Exception as e: print("[!] Failed to parse --fstypes: {}".format(e)) if args.vstype != 'detect' and args.single: print("[!] There's no point in using --single in combination with --vstype.") if args.carve and not _util.command_exists('photorec'): print(col("[-] The photorec command (part of testdisk package) is required to carve, but is not " "installed. Carving will be disabled.", 'yellow')) args.carve = False if not args.images and not args.unmount: print(col("[-] You must specify at least one path to a disk image", 'red')) sys.exit(1) if args.unmount: unmounter = Unmounter(**vars(args)) commands = unmounter.preview_unmount() if not commands: print("[+] Nothing to do") parser.exit() print("[!] --unmount will rigorously clean anything that looks like a mount or volume group originating " "from this utility. You may regret using this if you have other mounts or volume groups that are " "similarly named. The following commands will be executed:") for c in commands: print(" {0}".format(c)) try: input(">>> Press [enter] to continue or ^C to cancel... ") unmounter.unmount() except KeyboardInterrupt: print("\n[-] Aborted.") sys.exit(0) # Enumerate over all images in the CLI images = [] for num, image in enumerate(args.images): # If is a directory, find a E01 file in the directory if os.path.isdir(image): for f in glob.glob(os.path.join(image, '*.[E0]01')): images.append(f) break else: print(col("[-] {0} is a directory not containing a .001 or .E01 file, aborting!".format(image), "red")) break continue elif not os.path.exists(image): print(col("[-] Image {0} does not exist, aborting!".format(image), "red")) break images.append(image) else: p = None try: p = ImageParser(images, **vars(args)) num = 0 found_raid = False # Mount all disks. We could use .init, but where's the fun in that? for disk in p.disks: num += 1 print('[+] Mounting image {0} using {1}...'.format(p.paths[0], disk.method)) # Mount the base image using the preferred method if not disk.mount(): print(col("[-] Failed mounting base image. Perhaps try another mount method than {0}?" .format(disk.method), "red")) return if args.raid: if disk.add_to_raid(): found_raid = True if args.read_write: print('[+] Created read-write cache at {0}'.format(disk.rwpath)) if args.disktype: disk.load_disktype_data() print('[+] Mounted raw image [{num}/{total}]'.format(num=num, total=len(args.images))) sys.stdout.write("[+] Mounting volume...\r") sys.stdout.flush() has_left_mounted = False for volume in p.mount_volumes(args.single): try: # something failed? if not volume.mountpoint and not volume.loopback: if volume.exception and volume.size is not None and volume.size <= 1048576: print(col('[-] Exception while mounting small volume {0}'.format(volume.get_description()), 'yellow')) if args.wait: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) elif volume.exception: print(col('[-] Exception while mounting {0}'.format(volume.get_description()), 'red')) if not args.no_interaction: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) elif volume.flag != 'alloc': if args.wait or args.verbose: # do not show skipped messages by default print(col('[-] Skipped {0} {1} volume' .format(volume.get_description(), volume.flag), 'yellow')) if args.wait: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) else: print(col('[-] Could not mount volume {0}'.format(volume.get_description()), 'yellow')) if args.wait: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) if args.carve and volume.flag in ('alloc', 'unalloc'): sys.stdout.write("[+] Carving volume...\r") sys.stdout.flush() if volume.carve(freespace=False): print('[+] Carved data is available at {0}.'.format(col(volume.carvepoint, 'green', attrs=['bold']))) else: print(col('[-] Carving failed.', 'red')) else: continue # we do not need the unmounting sequence else: # it all was ok if volume.mountpoint: print('[+] Mounted volume {0} on {1}.'.format(col(volume.get_description(), attrs=['bold']), col(volume.mountpoint, 'green', attrs=['bold']))) elif volume.loopback: # fallback, generally indicates error. print('[+] Mounted volume {0} as loopback on {1}.'.format(col(volume.get_description(), attrs=['bold']), col(volume.loopback, 'green', attrs=['bold']))) print(col('[-] Could not detect further volumes in the loopback device.', 'red')) if args.carve: sys.stdout.write("[+] Carving volume...\r") sys.stdout.flush() if volume.carve(): print('[+] Carved data is available at {0}.'.format(col(volume.carvepoint, 'green', attrs=['bold']))) else: print(col('[-] Carving failed.', 'red')) # Do not offer unmount when reconstructing if args.reconstruct or args.keep: has_left_mounted = True continue input(col('>>> Press [enter] to unmount the volume, or ^C to keep mounted... ', attrs=['dark'])) # Case where image should be unmounted, but has failed to do so. Keep asking whether the user wants # to unmount. while True: if volume.unmount(): break else: try: print(col("[-] Error unmounting volume. Perhaps files are still open?", "red")) input(col('>>> Press [enter] to retry unmounting, or ^C to skip... ', attrs=['dark'])) except KeyboardInterrupt: has_left_mounted = True print("") break except KeyboardInterrupt: has_left_mounted = True print("") sys.stdout.write("[+] Mounting volume...\r") sys.stdout.flush() for disk in p.disks: if [x for x in disk.volumes if x.was_mounted] == 0: if args.vstype != 'detect': print(col('[?] Could not determine volume information of {0}. Image may be empty, ' 'or volume system type {0} was incorrect.'.format(args.vstype.upper()), 'yellow')) elif found_raid: print(col('[?] Could not determine volume information. Image may be empty, or volume system ' 'type could not be detected. Try explicitly providing the volume system type with ' '--vstype, or providing more volumes to complete the RAID array.', 'yellow')) elif not args.raid or args.single is False: print(col('[?] Could not determine volume information. Image may be empty, or volume system ' 'type could not be detected. Try explicitly providing the volume system type with ' '--vstype, mounting as RAID with --raid and/or mounting as a single volume with ' '--single', 'yellow')) else: print(col('[?] Could not determine volume information. Image may be empty, or volume system ' 'type could not be detected. Try explicitly providing the volume system type with ' '--vstype.', 'yellow')) if args.wait: input(col('>>> Press [enter] to continue... ', attrs=['dark'])) print('[+] Parsed all volumes!') # Perform reconstruct if required if args.reconstruct: # Reverse order so '/' gets unmounted last print("[+] Performing reconstruct... ") root = p.reconstruct() if not root: print(col("[-] Failed reconstructing filesystem: could not find root directory.", 'red')) else: failed = [] for disk in p.disks: failed.extend([x for x in disk.volumes if not x.bindmountpoint and x.mountpoint and x != root]) if failed: print("[+] Parts of the filesystem are reconstructed in {0}.".format(col(root.mountpoint, "green", attrs=["bold"]))) for m in failed: print(" {0} was not reconstructed".format(m.mountpoint)) else: print("[+] The entire filesystem is reconstructed in {0}.".format(col(root.mountpoint, "green", attrs=["bold"]))) if not args.keep: input(col(">>> Press [enter] to unmount all volumes... ", attrs=['dark'])) elif has_left_mounted and not args.keep: input(col(">>> Some volumes were left mounted. Press [enter] to unmount all... ", attrs=['dark'])) except KeyboardInterrupt: print('\n[+] User pressed ^C, aborting...') return except Exception as e: import traceback traceback.print_exc() print(col("[-] {0}".format(e), 'red')) if not args.no_interaction: input(col(">>> Press [enter] to continue.", attrs=['dark'])) finally: if args.keep: print("[+] Analysis complete.") elif not p: print("[-] Crashed before creating parser") else: print('[+] Analysis complete, unmounting...') # All done with this image, unmount it try: remove_rw = p.rw_active() and 'y' in input('>>> Delete the rw cache file? [y/N] ').lower() except KeyboardInterrupt: remove_rw = False while True: if p.clean(remove_rw): break else: try: print(col("[-] Error unmounting base image. Perhaps volumes are still open?", 'red')) input(col('>>> Press [enter] to retry unmounting, or ^C to cancel... ', attrs=['dark'])) except KeyboardInterrupt: print("") # ^C does not print \n break print("[+] All cleaned up")
def _load_fsstat_data(self): """Using :command:`fsstat`, adds some additional information of the volume to the Volume.""" if not _util.command_exists('fsstat'): logger.warning("fsstat is not installed, could not mount volume shadow copies") return process = None def stats_thread(): try: cmd = ['fsstat', self.get_raw_path(), '-o', str(self.offset // self.disk.block_size)] logger.debug('$ {0}'.format(' '.join(cmd))) # noinspection PyShadowingNames process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) for line in iter(process.stdout.readline, b''): line = line.decode() if line.startswith("File System Type:"): self.info['statfstype'] = line[line.index(':') + 2:].strip() elif line.startswith("Last Mount Point:") or line.startswith("Last mounted on:"): self.info['lastmountpoint'] = line[line.index(':') + 2:].strip().replace("//", "/") elif line.startswith("Volume Name:") and not self.info.get('label'): self.info['label'] = line[line.index(':') + 2:].strip() elif line.startswith("Version:"): self.info['version'] = line[line.index(':') + 2:].strip() elif line.startswith("Source OS:"): self.info['version'] = line[line.index(':') + 2:].strip() elif 'CYLINDER GROUP INFORMATION' in line: # noinspection PyBroadException try: process.terminate() # some attempt except Exception: pass break if self.info.get('lastmountpoint') and self.info.get('label'): self.info['label'] = "{0} ({1})".format(self.info['lastmountpoint'], self.info['label']) elif self.info.get('lastmountpoint') and not self.info.get('label'): self.info['label'] = self.info['lastmountpoint'] elif not self.info.get('lastmountpoint') and self.info.get('label') and \ self.info['label'].startswith("/"): # e.g. /boot1 if self.info['label'].endswith("1"): self.info['lastmountpoint'] = self.info['label'][:-1] else: self.info['lastmountpoint'] = self.info['label'] except Exception as e: # ignore any exceptions here. logger.exception("Error while obtaining stats.") pass thread = threading.Thread(target=stats_thread) thread.start() duration = 5 # longest possible duration for fsstat. thread.join(duration) if thread.is_alive(): # noinspection PyBroadException try: process.terminate() except Exception: pass thread.join() logger.debug("Killed fsstat after {0}s".format(duration))
def add_method_if_exists(method): if (method == "avfs" and _util.command_exists("avfsd")) or _util.command_exists(method): methods.append(method)
def add_method_if_exists(method): if (method == 'avfs' and _util.command_exists('avfsd')) or _util.command_exists(method): methods.append(method)
def add_method_if_exists(method): if (method == 'avfs' and _util.command_exists('avfsd') ) or _util.command_exists(method): methods.append(method)