class CompressFileAnalyst(interface.Analyst): """ File compression digital forensic analyst. Parameters: analyst.compress-file.filename -- the name of the file to compress. analyst.compress-file.output -- the name of the output file. """ # File-based compression parameters. filename = interface.Parameter("analyst.compress-file.filename") output = interface.Parameter("analyst.compress-file.output") def analyse(self, execution, sample): """ Compress a file. """ target = self.output.format(execution=execution) logger.info("Compressing '%s' to '%s'", self.filename, target) compress(self.filename, target) def discard(self, execution): """ Discard the compressed file. """ target = self.output.format(execution=execution) try: os.unlink(target) except OSError: # The file does not exist, this error is expected and can be # safely ignored. pass
class CompressSlotAnalyst(interface.Analyst): """ Slot-based file compression digital forensic analyst. Parameters: analyst.compress-slot.slot -- the name of the slot. analyst.compress-slot.output -- the name of the output file. """ # Slot-based compression parameters. slot = interface.Parameter("analyst.compress-slot.slot") output = interface.Parameter("analyst.compress-slot.output") def analyse(self, execution, sample): """ Compress the file referenced in a sample slot. """ source = sample[self.slot] target = self.output.format(execution=execution) logger.info("Compressing '%s' to '%s'", source, target) compress(source, target) def discard(self, execution): """ Discard the compressed file. """ target = self.output.format(execution=execution) try: os.unlink(target) except OSError: # The file does not exist, this error is expected and can be # safely ignored. pass
class LogicalPageAnalyst(interface.Analyst): """ Logical memory page digital forensic analyst. Parameters: analyst.logical-page.image -- the name of the memory image file. analyst.logical-page.volatility -- the location of the volatility script. analyst.logical-page.output -- the name of the output file. """ # Memory image analysis parameters. image = interface.Parameter("analyst.logical-page.image") volatility = interface.Parameter("analyst.logical-page.volatility") # Output parameters. output = interface.Parameter("analyst.logical-page.output") def analyse(self, execution, sample): """ Analyse pages in a memory image. """ logger.info("Generating a hash of logical pages in '%s'", self.image) # Create a file to contain the memory page analysis results. output_filename = self.output.format(execution=execution) handle = bz2.BZ2File(output_filename, "wb") writer = csv.DictWriter(handle, PAGE_HEADERS) writer.writeheader() # Invoke the memory image analysis process. invocation = [self.volatility, "-f", sample[self.image], PAGE_COMMAND] process = subprocess.Popen(invocation, stdout=subprocess.PIPE) # Extract the information describing each page in the memory image. pattern = re.compile(PAGE_PATTERN) for line in process.stdout: match = pattern.match(line) if not match is None: writer.writerow(match.groupdict()) # Close the output handle and wait for the analysis process. logger.info("Finished logical memory page analysis") handle.close() process.wait() def discard(self, execution): """ Discard the memory image logical page hash database. """ output_filename = self.output.format(execution=execution) try: os.unlink(output_filename) except OSError: # The file does not exist, this error is expected and can be # safely ignored. pass
class InitialStateTechnique(interface.Technique): """ Sample the initial state of a virtual machine. Parameters: technique.domain.host -- the address of the virtualisation platform. technique.domain.name -- the name of the virtual machine domain. technique.domain.snapshot -- the name of the virtual machine snapshot. Slots: memory.image -- the virtual machine memory image. """ # Virtual machine domain parameters. domain_host = interface.Parameter("technique.domain.host") domain_name = interface.Parameter("technique.domain.name") domain_snapshot = interface.Parameter("technique.domain.snapshot") # Memory image slots. image = interface.Slot("memory.image") def allocate(self): """ Allocate temporary resources. """ self.image = tempfile.mktemp(".memory") def sample(self): """ Sample the initial state of a virtual machine. """ # Instantiate and revert the virtual machine to a snapshot. logger.info("Launching domain and reverting to snapshot") domain = emulator.Emulator(self.domain_host, self.domain_name) domain.revert(self.domain_snapshot) # Save an image of virtual machine memory. logger.info("Saving image of domain memory") domain.save_memory(self.image) # Terminate the virtual machine. logger.info("Terminating domain") domain.close() def release(self): """ Release temporary resources. """ logger.info("Unlinking memory image '%s'", self.image) os.unlink(self.image)
class MonitorInterventionBehaviour(interface.Behaviour): """ Monitoring live digital forensic intervention behaviour. Parameters: technique.invocation.command -- the command-line acquisition invocation. technique.invocation.interval -- the invocation sampling interval. technique.invocation.cwd -- the working-directory of the invocation. Slots: acquisition.samples -- the performance samples of the invocation. acquisition.standard-output -- the standard output of the invocation. acquisition.standard-error -- the standard error of the invocation. acquisition.return-code -- the return code of the information. """ # Memory image acquisition invocation parameters. invocation_command = interface.Parameter("technique.invocation.command") invocation_interval = interface.Parameter("technique.invocation.interval") invocation_cwd = interface.Parameter("technique.invocation.cwd") # Acquisition invocation outputs. samples = interface.Slot("acquisition.samples") standard_output = interface.Slot("acquisition.standard-output") standard_error = interface.Slot("acquisition.standard-error") return_code = interface.Slot("acquisition.return-code") def acquire(self, domain, agent): """ Acquire live digital forensic evidence by intervening in the execution of the domain and sampling the performance of the acquisition technique. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ reply = agent.monitor(self.invocation_command, self.invocation_interval, self.invocation_cwd) self.samples = reply["samples"] self.standard_output = reply["stdout"] self.standard_error = reply["stderr"] self.return_code = reply["return_code"]
class PhysicalPageAnalyst(interface.Analyst): """ Physical memory page digital forensic analyst. Parameters: analyst.physical-page.image -- the name of the memory image file. analyst.physical-page.page-size -- the page size of the memory image. analyst.physical-page.output -- the name of the output file. """ # Memory image analysis parameters. image = interface.Parameter("analyst.physical-page.image") page_size = interface.Parameter("analyst.physical-page.page-size") # Output parameters. output = interface.Parameter("analyst.physical-page.output") def analyse(self, execution, sample): """ Analyse a memory image. """ import pypette.analyst.storage as storage logger.info("Generating a hash of physical pages in '%s'", self.image) output_filename = self.output.format(execution=execution) handle = bz2.BZ2File(output_filename, "w") for block in storage.blocks(sample[self.image], self.page_size): checksum = hashlib.md5(block) handle.write(checksum.hexdigest()) handle.write("\n") logger.info("Finished physical memory page analysis") handle.close() def discard(self, execution): """ Discard the memory image physical page hash database. """ output_filename = self.output.format(execution=execution) try: os.unlink(output_filename) except OSError: # The file does not exist, this error is expected and can be # safely ignored. pass
class ExperimentMetadataAnalyst(interface.Analyst): """ Experiment metadata digital forensic analyst. """ # Metadata analysis parameters. slots = interface.Parameter("analyst.experiment-metadata.slots") output = interface.Parameter("analyst.experiment-metadata.output") def __init__(self, parameters): """ Initialise a new experiment metadata analyst. """ super(ExperimentMetadataAnalyst, self).__init__(parameters) self.table = {} def analyse(self, execution, sample): """ Collect metadata from a sample. """ self.table[execution] = sample def collate(self): """ Collate metadata from an experiment. """ logger.info("Collating metadata for experiment") document = yaml.dump(self.table) with open(self.output, "w") as handle: handle.write(document) def discard(self, execution): """ Discard the collected metadata. """ try: del self.table[execution] except KeyError: # The record does not exist, this error is expected and can be # safely ignored. pass
class RandomDelayBehaviour(interface.Behaviour): """ Random live digital forensic experiment delay behaviour. Parameters: technique.duration.mean -- the mean duration to idle the virtual machine. technique.duration.stdev -- the standard deviation of the duration. """ # Delay parameters. mean = interface.Parameter("technique.duration.mean") stdev = interface.Parameter("technique.duration.stdev") def idle(self): """ Allow the live digital forensic situation to execute without interaction for a period drawn from a normal distribution. """ duration = random.normalvariate(self.mean, self.stdev) logger.info("Idling virtual machine for %d seconds", duration) time.sleep(duration)
class OpticalAcquisitionToolkit(interface.Behaviour): """ Optical live digital forensic toolkit behaviour. Parameters: technique.toolkit.image -- the path of the toolkit image. technique.toolkit.device -- the device on which to attach the toolkit. technique.toolkit.tag -- the identification tag of the toolkit. """ # Optical acquisition toolkit parameters. toolkit_image = interface.Parameter("technique.toolkit.image") toolkit_device = interface.Parameter("technique.toolkit.device") toolkit_tag = interface.Parameter("technique.toolkit.tag") def attach_toolkit(self, domain, agent): """ Attach the optical live digital forensic acquisition toolkit to the domain. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ logger.info("Attaching optical acquisition toolkit to domain") domain.change_optical(self.toolkit_device, self.toolkit_image) agent.find(self.toolkit_tag) def detach_toolkit(self, domain, agent): """ Detach the optical live digital forensic acquisition toolkit from the domain. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ logger.info("Detaching optical acquisition toolkit from domain") domain.eject_optical(self.toolkit_device)
class SampleMetadataAnalyst(interface.Analyst): """ Sample metadata digital forensic analyst. Parameters: analyst.sample-metadata.slots -- the list of sample slots to record. analyst.sample-metadata.output -- the name of the output file. """ # Metadata analysis parameters. slots = interface.Parameter("analyst.sample-metadata.slots") output = interface.Parameter("analyst.sample-metadata.output") def analyse(self, execution, sample): """ Collect metadata from a sample. """ logger.info("Collecting metadata for execution '%s'", execution) # Encode the sample metadata in a document. document = yaml.dump({slot: sample[slot] for slot in self.slots}) # Save the metadata document to a file. output_filename = self.output.format(execution=execution) with open(output_filename, "w") as handle: handle.write(document) def discard(self, execution): """ Discard the collected metadata. """ output_filename = self.output.format(execution=execution) try: os.unlink(output_filename) except OSError: # The file does not exist, this error is expected and can be # safely ignored. pass
class FixedDelayBehaviour(interface.Behaviour): """ Fixed live digital forensic experiment delay behaviour. Parameters: technique.duration -- the number of seconds to idle the virtual machine. """ # Delay parameters. duration = interface.Parameter("technique.duration") def idle(self): """ Allow the live digital forensic situation to execute without interaction for a fixed period. """ logger.info("Idling virtual machine for %d seconds", self.duration) time.sleep(self.duration)
class MemoryFeatureAnalyst(interface.Analyst): """ Memory image semantic feature analyst. Parameters: analyst.memory-feature.image -- the name of the memory image. analyst.memory-feature.profile -- the name of the memory image profile. analyst.memory-feature.modules -- the list of memory analysis modules. analyst.memory-feature.volatility -- the location of the volatility script. analyst.memory-feature.output -- the name of the output path. """ # Memory image analysis parameters. image = interface.Parameter("analyst.memory-feature.image") profile = interface.Parameter("analyst.memory-feature.profile") modules = interface.Parameter("analyst.memory-feature.modules") volatility = interface.Parameter("analyst.memory-feature.volatility") # Output parameters. output = interface.Parameter("analyst.memory-feature.output") def analyse(self, execution, sample): """ Analyse semantic features in a memory image. """ path = self.output.format(execution=execution) os.mkdir(path) logger.info("Analysing semantic features from '%s'", self.image) for module in self.modules: command, headers, pattern = definitions[module] table = self.extract(sample[self.image], command, pattern) target = os.path.join(path, "{}.csv.bz2".format(module)) self.write(target, headers, table) logger.info("Finished analysing semantic features") def extract(self, source, command, pattern): """ Extract a table of semantic features from a memory image. Arguments: source -- the name of the memory image. command -- the name of the memory image analysis command. pattern -- the analysis command output pattern. """ invocation = [ self.volatility, "--profile", self.profile, "-f", source, command ] matcher = re.compile(pattern) try: data = subprocess.check_output(invocation) return [m.groupdict() for m in matcher.finditer(data)] except subprocess.CalledProcessError: logger.exception("Invocation of memory analysis command failed") raise def write(self, target, headers, table): """ Write a table of extracted semantic features to a file. Arguments: target -- the name of the target file. headers -- the headers of the semantic feature table. table -- the semantic feature table. """ handle = bz2.BZ2File(target, "wb") writer = csv.DictWriter(handle, headers) writer.writeheader() writer.writerows(table) handle.close() def discard(self, execution): """ Discard the tables of semantic features. """ path = self.output.format(execution=execution) try: shutil.rmtree(path) except OSError: # The file does not exist, this error is expected and can be # safely ignored. pass
class AcquisitionTechnique(interface.Technique): """ Abstract live digital forensic memory acquisition technique. Parameters: technique.domain.host -- the address of the virtualisation platform. technique.domain.name -- the name of the virtual machine domain. technique.domain.snapshot -- the name of the virtual machine snapshot. technique.domain.agent -- the address of the virtual machine guest agent. Slots: memory.final -- the final virtual machine state memory image. agent.initial -- the initial guest agent performance sample. agent.final -- the final guest agent performance sample. time.sample.start -- the time at which sampling started. time.sample.finish -- the time at which sampling finished. time.acquisition.start -- the time at which acquisition started. time.acquisition.finish -- the time at which acquisition finished. """ # Virtual machine domain parameters. domain_host = interface.Parameter("technique.domain.host") domain_name = interface.Parameter("technique.domain.name") domain_snapshot = interface.Parameter("technique.domain.snapshot") domain_agent = interface.Parameter("technique.domain.agent") # Memory image slots. image_final = interface.Slot("memory.final") # Guest agent performance slots. agent_initial = interface.Slot("agent.initial") agent_final = interface.Slot("agent.final") # Timing information slots. sample_start = interface.Slot("time.sample.start") sample_finish = interface.Slot("time.sample.finish") acquisition_start = interface.Slot("time.acquisition.start") acquisition_finish = interface.Slot("time.acquisition.finish") def allocate(self): """ Allocate temporary resources. """ logger.info("Allocating resources") self.image_final = tempfile.mktemp(".memory") self.allocate_behaviour() def sample(self): """ Sample the effects of a live digital forensic memory acquisition intervention on a virtual machine. """ self.sample_start = time.time() logger.info("Launching domain and reverting to snapshot") domain = emulator.Emulator(self.domain_host, self.domain_name) try: domain.revert(self.domain_snapshot) logger.info("Connecting to the virtual machine guest agent") agent = pypette.agent.Agent(self.domain_agent) self.acquisition_start = time.time() logger.info("Attaching acquisition toolkit and vector") domain.resume() self.agent_initial = agent.status() self.attach_toolkit(domain, agent) self.attach_vector(domain, agent) logger.info("Launching the live digital forensic acquisition") self.acquire(domain, agent) logger.info("Detaching acquisition toolkit and vector") self.detach_toolkit(domain, agent) self.detach_vector(domain, agent) self.acquisition_finish = time.time() self.agent_final = agent.status() logger.info("Collecting final memory image and terminating domain") domain.suspend() domain.save_memory(self.image_final) finally: domain.close() logger.info("Transferring the acquired memory image to the host") self.transfer_acquired_image() self.sample_finish = time.time() def attach_toolkit(self, domain, agent): """ Attach the live digital forensic acquisition toolkit to the domain. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ raise NotImplementedError() def detach_toolkit(self, domain, agent): """ Detach the live digital forensic acquisition toolkit from the domain. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ raise NotImplementedError() def attach_vector(self, domain, agent): """ Attach the live digital forensic acquisition vector to the domain. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ raise NotImplementedError() def acquire(self, domain, agent): """ Acquire live digital forensic evidence by intervening in the execution of the domain. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ pass def detach_vector(self, domain, agent): """ Detach the live digital forensic acquisition vector from the domain. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ raise NotImplementedError() def transfer_acquired_image(self): """ Transfer the acquired memory image from the acquisition vector to the host machine. """ raise NotImplementedError() def release(self): """ Release temporary resources. """ logger.info("Releasing resources") if os.path.exists(self.image_final): os.unlink(self.image_final) self.release_behaviour()
class ControlTechnique(interface.Technique): """ Live digital forensic control experiment technique. Parameters: technique.domain.host -- the address of the virtualisation platform. technique.domain.name -- the name of the virtual machine domain. technique.domain.snapshot -- the name of the virtual machine snapshot. Slots: memory.image -- the virtual machine memory image. """ # Virtual machine domain parameters. domain_host = interface.Parameter("technique.domain.host") domain_name = interface.Parameter("technique.domain.name") domain_snapshot = interface.Parameter("technique.domain.snapshot") # Memory image slots. image = interface.Slot("memory.image") def allocate(self): """ Allocate temporary resources. """ logger.info("Allocating file for memory image") self.image = tempfile.mktemp(".memory") def sample(self): """ Collect a control sample from a live digital forensic situation. """ # Instantiate and revert the virtual machine to a snapshot. logger.info("Launching domain and reverting to snapshot") domain = emulator.Emulator(self.domain_host, self.domain_name) try: domain.revert(self.domain_snapshot) # Execute the machine and allow it to idle. logger.info("Executing domain without user-interaction") domain.resume() self.idle() # Save an image of virtual machine memory. logger.info("Saving image of domain memory") domain.suspend() domain.save_memory(self.image) # Terminate the virtual machine. logger.info("Terminating domain") finally: domain.close() def idle(self): """ Allow the live digital forensic situation to execute without interaction for some period. """ raise NotImplementedError() def release(self): """ Release temporary resources. """ logger.info("Unlinking memory image '%s'", self.image) if os.path.exists(self.image): os.unlink(self.image)
class FixedDriveAcquisitionVector(interface.Behaviour): """ Fixed local drive live digital forensic acquisition vector behaviour. Parameters: technique.vector.filename -- the name of the disk image file. technique.vector.partition -- the index of the partition. technique.vector.tag -- the identification tag of the vector. technique.vector.image -- the filename of the memory image on the vector. Slots: memory.acquired -- the memory image acquired by the toolkit. """ # Fixed drive acquisition vector parameters. vector_filename = interface.Parameter("technique.vector.filename") vector_partition = interface.Parameter("technique.vector.partition") vector_tag = interface.Parameter("technique.vector.tag") vector_image = interface.Parameter("technique.vector.image") # Memory image slots. image_acquired = interface.Slot("memory.acquired") def allocate_behaviour(self): """ Allocate temporary resources. """ super(FixedDriveAcquisitionVector, self).allocate_behaviour() logger.info("Allocating resources") self.image_acquired = tempfile.mktemp(".memory") def attach_vector(self, domain, agent): """ Attach the fixed local drive acquisition vector to the domain. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ logger.info("Verifying that the fixed drive is attached to the domain") agent.find(self.vector_tag) def detach_vector(self, domain, agent): """ Detach the fixed local drive acquisition vector from the domain. Arguments: domain -- the virtual machine domain. agent -- the virtual machine guest agent. """ logger.info("Flushing the cache from the fixed drive") agent.flush(self.vector_tag) def transfer_acquired_image(self): """ Transfer the acquired memory image from the acquisition vector to the host machine. """ # Attach the disk image and mount the partition on the host machine. logger.info("Attaching and mounting the acquisition vector") device = volume.attach(self.vector_filename) partition_device = volume.partition(device, self.vector_partition) volume.wait_for_partition(partition_device) path = volume.mount(partition_device) # Copy the memory image from the vector partition. logger.info("Copying the memory image from the acquisition vector") image_filename = os.path.join(path, self.vector_image) shutil.copy(image_filename, self.image_acquired) # Unmount the partition and detach the disk image. logger.info("Unmounting and detaching the acquisition vector") volume.unmount(path) volume.detach(device) def release_behaviour(self): """ Release temporary resources. """ super(FixedDriveAcquisitionVector, self).release_behaviour() logger.info("Releasing resources") if os.path.exists(self.image_acquired): os.unlink(self.image_acquired)