diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 4cdbd26e8..35ad84011 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -19,6 +19,7 @@ import os import sys import tempfile import traceback +from datetime import datetime from typing import Any, Dict, Type, Union from urllib import parse, request @@ -157,6 +158,10 @@ class CommandLine: help = "Write configuration JSON file out to config.json", default = False, action = 'store_true') + parser.add_argument("--save-config", + help = "Save configuration JSON file to a file", + default = None, + type = str) parser.add_argument("--clear-cache", help = "Clears out all short-term cached items", default = False, @@ -320,8 +325,13 @@ class CommandLine: self.file_handler_class_factory()) if args.write_config: - vollog.debug("Writing out configuration data to config.json") - with open("config.json", "w") as f: + vollog.warning('Use of --write-config has been deprecated, replaced by --save-config ') + args.save_config = 'config.json' + if args.save_config: + vollog.debug("Writing out configuration data to {args.save_config}") + if os.path.exists(os.path.abspath(args.save_config)): + parser.error(f"Cannot write configuration: file {args.save_config} already exists") + with open(args.save_config, "w") as f: json.dump(dict(constructed.build_configuration()), f, sort_keys = True, indent = 2) except exceptions.UnsatisfiedException as excp: self.process_unsatisfied_exceptions(excp) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 8e07d58d1..1ddfcca84 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -101,7 +101,7 @@ def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str: disasm: Input disassembly objects Returns: - A string as rendererd by capstone where available, otherwise output as if it were just bytes + A string as rendered by capstone where available, otherwise output as if it were just bytes """ if CAPSTONE_PRESENT: diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index 812d44337..30fe75e06 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -85,6 +85,10 @@ class VolShell(cli.CommandLine): help = "Write configuration JSON file out to config.json", default = False, action = 'store_true') + parser.add_argument("--save-config", + help = "Save configuration JSON file to a file", + default = None, + type = str) parser.add_argument("--clear-cache", help = "Clears out all short-term cached items", default = False, @@ -234,8 +238,13 @@ class VolShell(cli.CommandLine): self.file_handler_class_factory()) if args.write_config: - vollog.debug("Writing out configuration data to config.json") - with open("config.json", "w") as f: + vollog.warning('Use of --write-config has been deprecated, replaced by --save-config ') + args.save_config = 'config.json' + if args.save_config: + vollog.debug("Writing out configuration data to {args.save_config}") + if os.path.exists(os.path.abspath(args.save_config)): + parser.error(f"Cannot write configuration: file {args.save_config} already exists") + with open(args.save_config, "w") as f: json.dump(dict(constructed.build_configuration()), f, sort_keys = True, indent = 2) except exceptions.UnsatisfiedException as excp: self.process_unsatisfied_exceptions(excp) diff --git a/volatility3/framework/constants/__init__.py b/volatility3/framework/constants/__init__.py index 665e62d30..badec946b 100644 --- a/volatility3/framework/constants/__init__.py +++ b/volatility3/framework/constants/__init__.py @@ -9,7 +9,7 @@ volatility This includes default scanning block sizes, etc. import enum import os.path import sys -from typing import Optional, Callable +from typing import Callable, Optional import volatility3.framework.constants.linux import volatility3.framework.constants.windows @@ -63,7 +63,7 @@ LOGLEVEL_VVVV = 6 CACHE_PATH = os.path.join(os.path.expanduser("~"), ".cache", "volatility3") """Default path to store cached data""" -if sys.platform == 'windows': +if sys.platform == 'win32': CACHE_PATH = os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3") os.makedirs(CACHE_PATH, exist_ok = True) @@ -80,6 +80,7 @@ ProgressCallback = Optional[Callable[[float, str], None]] OS_CATEGORIES = ['windows', 'mac', 'linux'] + class Parallelism(enum.IntEnum): """An enumeration listing the different types of parallelism applied to volatility.""" diff --git a/volatility3/framework/plugins/mac/__init__.py b/volatility3/framework/plugins/mac/__init__.py index e69de29bb..ef6762bee 100644 --- a/volatility3/framework/plugins/mac/__init__.py +++ b/volatility3/framework/plugins/mac/__init__.py @@ -0,0 +1,8 @@ +# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""All core mac plugins. + +These modules should only be imported from volatility3.plugins NOT +volatility3.framework.plugins +""" diff --git a/volatility3/framework/plugins/mac/ifconfig.py b/volatility3/framework/plugins/mac/ifconfig.py index c366a19f0..330c13f07 100644 --- a/volatility3/framework/plugins/mac/ifconfig.py +++ b/volatility3/framework/plugins/mac/ifconfig.py @@ -9,7 +9,7 @@ from volatility3.framework.symbols import mac class Ifconfig(plugins.PluginInterface): - """Lists loaded kernel modules""" + """Lists network interface information for all devices""" _required_framework_version = (2, 0, 0) diff --git a/volatility3/framework/plugins/mac/mount.py b/volatility3/framework/plugins/mac/mount.py index 398559446..ba3ab83c8 100644 --- a/volatility3/framework/plugins/mac/mount.py +++ b/volatility3/framework/plugins/mac/mount.py @@ -12,7 +12,7 @@ from volatility3.framework.symbols import mac class Mount(plugins.PluginInterface): """A module containing a collection of plugins that produce data typically - foundin Mac's mount command""" + found in Mac's mount command""" _required_framework_version = (2, 0, 0) diff --git a/volatility3/framework/plugins/windows/mbrscan.py b/volatility3/framework/plugins/windows/mbrscan.py new file mode 100644 index 000000000..d064e7d29 --- /dev/null +++ b/volatility3/framework/plugins/windows/mbrscan.py @@ -0,0 +1,200 @@ +# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +import hashlib + +from typing import Iterator, List, Tuple + +from volatility3.framework import constants, exceptions, interfaces, renderers, symbols +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import scanners +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.windows.extensions import mbr + +vollog = logging.getLogger(__name__) + +class MBRScan(interfaces.plugins.PluginInterface): + """Scans for and parses potential Master Boot Records (MBRs)""" + + _required_framework_version = (2, 0, 1) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls)-> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', + architectures = ["Intel32", "Intel64"]), + requirements.BooleanRequirement(name = 'full', + description ="It analyzes and provides all the information in the partition entry and bootcode hexdump. (It returns a lot of information, so we recommend you render it in CSV.)", + default = False, + optional = True) + ] + + @classmethod + def get_hash(cls, data:bytes) -> str: + return hashlib.md5(data).hexdigest() + + def _generator(self) -> Iterator[Tuple]: + kernel = self.context.modules[self.config['kernel']] + physical_layer_name = self.context.layers[kernel.layer_name].config.get('memory_layer', None) + + # Decide of Memory Dump Architecture + layer = self.context.layers[physical_layer_name] + architecture = "intel" if not symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name) else "intel64" + + # Read in the Symbol File + symbol_table = intermed.IntermediateSymbolTable.create(context = self.context, + config_path = self.config_path, + sub_path = "windows", + filename = "mbr", + class_types = { + 'PARTITION_TABLE': mbr.PARTITION_TABLE, + 'PARTITION_ENTRY': mbr.PARTITION_ENTRY + }) + + partition_table_object = symbol_table + constants.BANG + "PARTITION_TABLE" + + # Define Signature and Data Length + mbr_signature = b"\x55\xAA" + mbr_length = 0x200 + bootcode_length = 0x1B8 + + # Scan the Layer for Raw Master Boot Record (MBR) and parse the fields + for offset, _value in layer.scan(context = self.context, scanner = scanners.MultiStringScanner(patterns = [mbr_signature])): + try: + mbr_start_offset = offset - (mbr_length - len(mbr_signature)) + partition_table = self.context.object(partition_table_object, offset = mbr_start_offset, layer_name = layer.name) + + # Extract only BootCode + full_mbr = layer.read(mbr_start_offset, mbr_length, pad = True) + bootcode = full_mbr[:bootcode_length] + + all_zeros = None + + if bootcode: + all_zeros = bootcode.count(b"\x00") == len(bootcode) + + if not all_zeros: + + partition_entries = [ + partition_table.FirstEntry, partition_table.SecondEntry, + partition_table.ThirdEntry, partition_table.FourthEntry + ] + + if not self.config.get("full", True): + yield (0, ( + format_hints.Hex(offset), + partition_table.get_disk_signature(), + self.get_hash(bootcode), + self.get_hash(full_mbr), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + interfaces.renderers.Disassembly(bootcode, 0, architecture) + )) + else: + yield (0, ( + format_hints.Hex(offset), + partition_table.get_disk_signature(), + self.get_hash(bootcode), + self.get_hash(full_mbr), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + interfaces.renderers.Disassembly(bootcode, 0, architecture), + format_hints.HexBytes(bootcode) + )) + + for partition_index, partition_entry_object in enumerate(partition_entries, start=1): + + if not self.config.get("full", True): + yield (1, ( + format_hints.Hex(offset), + partition_table.get_disk_signature(), + self.get_hash(bootcode), + self.get_hash(full_mbr), + partition_index, + partition_entry_object.is_bootable(), + partition_entry_object.get_partition_type(), + format_hints.Hex(partition_entry_object.get_size_in_sectors()), + renderers.NotApplicableValue() + )) + else: + yield (1, ( + format_hints.Hex(offset), + partition_table.get_disk_signature(), + self.get_hash(bootcode), + self.get_hash(full_mbr), + partition_index, + partition_entry_object.is_bootable(), + format_hints.Hex(partition_entry_object.get_bootable_flag()), + partition_entry_object.get_partition_type(), + format_hints.Hex(partition_entry_object.PartitionType), + format_hints.Hex(partition_entry_object.get_starting_lba()), + partition_entry_object.get_starting_cylinder(), + partition_entry_object.get_starting_chs(), + partition_entry_object.get_starting_sector(), + partition_entry_object.get_ending_cylinder(), + partition_entry_object.get_ending_chs(), + partition_entry_object.get_ending_sector(), + format_hints.Hex(partition_entry_object.get_size_in_sectors()), + renderers.NotApplicableValue(), + renderers.NotApplicableValue() + )) + else: + vollog.log(constants.LOGLEVEL_VVVV, f"Not a valid MBR: Data all zeroed out : {format_hints.Hex(offset)}") + continue + + except exceptions.PagedInvalidAddressException as excp: + vollog.log(constants.LOGLEVEL_VVVV, f"Invalid address identified in guessed MBR: {hex(excp.invalid_address)}") + continue + + def run(self)-> renderers.TreeGrid: + if not self.config.get("full", True): + return renderers.TreeGrid([ + ("Potential MBR at Physical Offset", format_hints.Hex), + ("Disk Signature", str), + ("Bootcode MD5", str), + ("Full MBR MD5", str), + ("PartitionIndex", int), + ("Bootable", bool), + ("PartitionType", str), + ("SectorInSize", format_hints.Hex), + ("Disasm", interfaces.renderers.Disassembly) + ], self._generator()) + else: + return renderers.TreeGrid([ + ("Potential MBR at Physical Offset", format_hints.Hex), + ("Disk Signature", str), + ("Bootcode MD5", str), + ("Full MBR MD5", str), + ("PartitionIndex", int), + ("Bootable", bool), + ("BootFlag", format_hints.Hex), + ("PartitionType", str), + ("PartitionTypeRaw", format_hints.Hex), + ("StartingLBA", format_hints.Hex), + ("StartingCylinder", int), + ("StartingCHS", int), + ("StartingSector", int), + ("EndingCylinder", int), + ("EndingCHS", int), + ("EndingSector", int), + ("SectorInSize", format_hints.Hex), + ("Disasm", interfaces.renderers.Disassembly), + ("Bootcode", format_hints.HexBytes) + ], self._generator()) diff --git a/volatility3/framework/symbols/windows/extensions/mbr.py b/volatility3/framework/symbols/windows/extensions/mbr.py new file mode 100644 index 000000000..fc7996c52 --- /dev/null +++ b/volatility3/framework/symbols/windows/extensions/mbr.py @@ -0,0 +1,62 @@ +# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from volatility3.framework import objects + +class PARTITION_TABLE(objects.StructType): + + def get_disk_signature(self) -> str: + """Get Disk Signature (GUID).""" + return "{0:02x}-{1:02x}-{2:02x}-{3:02x}".format( + self.DiskSignature[0], + self.DiskSignature[1], + self.DiskSignature[2], + self.DiskSignature[3] + ) + +class PARTITION_ENTRY(objects.StructType): + + def get_bootable_flag(self) -> int: + """Get Bootable Flag.""" + return self.BootableFlag + + def is_bootable(self) -> bool: + """Check Bootable Partition.""" + return False if not (self.get_bootable_flag() == 0x80) else True + + def get_partition_type(self) -> str: + """Get Partition Type.""" + return self.PartitionType.lookup() if self.PartitionType.is_valid_choice else "Not Defined PartitionType" + + def get_starting_chs(self) -> int: + """Get Starting CHS (Cylinder Header Sector) Address.""" + return self.StartingCHS[0] + + def get_ending_chs(self) -> int: + """Get Ending CHS (Cylinder Header Sector) Address.""" + return self.EndingCHS[0] + + def get_starting_sector(self) -> int: + """Get Starting Sector.""" + return self.StartingCHS[1] % 64 + + def get_ending_sector(self) -> int: + """Get Ending Sector.""" + return self.EndingCHS[1] % 64 + + def get_starting_cylinder(self) -> int: + """Get Starting Cylinder.""" + return (self.StartingCHS[1] - self.get_starting_sector()) * 4 + self.StartingCHS[2] + + def get_ending_cylinder(self) -> int: + """Get Ending Cylinder.""" + return (self.EndingCHS[1] - self.get_ending_sector()) * 4 + self.EndingCHS[2] + + def get_starting_lba(self) -> int: + """Get Starting LBA (Logical Block Addressing).""" + return self.StartingLBA + + def get_size_in_sectors(self) -> int: + """Get Size in Sectors.""" + return self.SizeInSectors diff --git a/volatility3/framework/symbols/windows/mbr.json b/volatility3/framework/symbols/windows/mbr.json new file mode 100644 index 000000000..2a6ec7779 --- /dev/null +++ b/volatility3/framework/symbols/windows/mbr.json @@ -0,0 +1,240 @@ +{ + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Donghyun Kim (@digitalisx99)", + "comment": "Using structures defined in File System Forensic Analysis pg 88+", + "datetime": "2022-03-05T10:53:00" + }, + "format": "6.1.0" + }, + "base_types": { + "unsigned long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned long long": { + "kind": "int", + "size": 8, + "signed": false, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": true, + "endian": "little" + }, + "unsigned int": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "int": { + "kind": "int", + "size": 4, + "signed": true, + "endian": "little" + }, + "unsigned short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "little" + }, + "unsigned char": { + "kind": "int", + "size": 1, + "signed": false, + "endian": "little" + }, + "wchar": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + } + }, + "symbols": {}, + "enums": { + "PartitionTypes": { + "base": "unsigned char", + "constants": { + "Empty": 0, + "FAT12,CHS": 1, + "FAT16 16-32MB,CHS": 4, + "Microsoft Extended": 5, + "FAT16 32MB,CHS": 6, + "NTFS": 7, + "FAT32,CHS": 11, + "FAT32,LBA": 12, + "FAT16, 32MB-2GB,LBA": 14, + "Microsoft Extended, LBA": 15, + "Hidden FAT12,CHS": 17, + "Hidden FAT16,16-32MB,CHS": 20, + "Hidden FAT16,32MB-2GB,CHS": 22, + "AST SmartSleep Partition": 24, + "Hidden FAT32,CHS": 27, + "Hidden FAT32,LBA": 28, + "Hidden FAT16,32MB-2GB,LBA": 30, + "PQservice": 39, + "Plan 9 partition": 57, + "PartitionMagic recovery partition": 60, + "Microsoft MBR,Dynamic Disk": 66, + "GoBack partition": 68, + "Novell": 81, + "CP/M": 82, + "Unix System V": 99, + "PC-ARMOUR protected partition": 100, + "Solaris x86 or Linux Swap": 130, + "Linux": 131, + "Hibernation": 132, + "Linux Extended": 133, + "NTFS Volume Set": 134, + "NTFS Volume Set": 135, + "BSD/OS": 159, + "Hibernation": 160, + "Hibernation": 161, + "FreeBSD": 165, + "OpenBSD": 166, + "Mac OSX": 168, + "NetBSD": 169, + "Mac OSX Boot": 171, + "MacOS X HFS": 175, + "BSDI": 183, + "BSDI Swap": 184, + "Boot Wizard hidden": 187, + "Solaris 8 boot partition": 190, + "CP/M-86": 216, + "Dell PowerEdge Server utilities (FAT fs)": 222, + "DG/UX virtual disk manager partition": 223, + "BeOS BFS": 235, + "EFI GPT Disk": 238, + "EFI System Partition": 239, + "VMWare File System": 251, + "VMWare Swap": 252 + }, + "size": 1 + } + }, + "user_types": { + "PARTITION_ENTRY":{ + "fields": { + "BootableFlag": { + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned char" + } + }, + "StartingCHS": { + "offset": 1, + "type": { + "count": 3, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "PartitionType": { + "offset": 4, + "type": { + "kind": "enum", + "name": "PartitionTypes" + } + }, + "EndingCHS": { + "offset": 5, + "type": { + "count": 3, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "StartingLBA": { + "offset": 8, + "type": { + "kind": "base", + "name": "unsigned int" + } + }, + "SizeInSectors": { + "offset": 12, + "type": { + "kind": "base", + "name": "unsigned int" + } + } + }, + "kind": "struct", + "size": 16 + }, + "PARTITION_TABLE":{ + "fields":{ + "DiskSignature": { + "offset": 440, + "type": { + "count": 4, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + }, + "Unused": { + "offset": 444, + "type": { + "kind": "base", + "name": "unsigned short" + } + }, + "FirstEntry":{ + "offset": 446, + "type": { + "kind": "struct", + "name": "PARTITION_ENTRY" + } + }, + "SecondEntry":{ + "offset": 462, + "type": { + "kind": "struct", + "name": "PARTITION_ENTRY" + } + }, + "ThirdEntry":{ + "offset": 478, + "type": { + "kind": "struct", + "name": "PARTITION_ENTRY" + } + }, + "FourthEntry":{ + "offset": 494, + "type": { + "kind": "struct", + "name": "PARTITION_ENTRY" + } + }, + "Signature":{ + "offset": 510, + "type": { + "kind": "base", + "name": "unsigned short" + } + } + }, + "kind": "struct", + "size": 512 + } + } +}