Merge pull request #1044 from forensicxlab/feature/ADS

Feature: Alternate Data Streams.
This commit is contained in:
ikelos
2023-12-03 15:20:53 +00:00
committed by GitHub
3 changed files with 186 additions and 2 deletions
@@ -173,3 +173,140 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
],
self._generator(),
)
class ADS(interfaces.plugins.PluginInterface):
"""Scans for Alternate Data Stream"""
_required_framework_version = (2, 0, 0)
@classmethod
def get_requirements(cls):
return [
requirements.TranslationLayerRequirement(
name="primary",
description="Memory layer for the kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
),
]
def _generator(self):
layer = self.context.layers[self.config["primary"]]
# Yara Rule to scan for MFT Header Signatures
rules = yarascan.YaraScan.process_yara_options(
{"yara_rules": "/FILE0|FILE\*|BAAD/"}
)
# Read in the Symbol File
symbol_table = intermed.IntermediateSymbolTable.create(
context=self.context,
config_path=self.config_path,
sub_path="windows",
filename="mft",
class_types={
"MFT_ENTRY": mft.MFTEntry,
"FILE_NAME_ENTRY": mft.MFTFileName,
"ATTRIBUTE": mft.MFTAttribute,
},
)
# get each of the individual Field Sets
mft_object = symbol_table + constants.BANG + "MFT_ENTRY"
attribute_object = symbol_table + constants.BANG + "ATTRIBUTE"
fn_object = symbol_table + constants.BANG + "FILE_NAME_ENTRY"
# Scan the layer for Raw MFT records and parse the fields
for offset, _rule_name, _name, _value in layer.scan(
context=self.context, scanner=yarascan.YaraScanner(rules=rules)
):
with contextlib.suppress(exceptions.PagedInvalidAddressException):
mft_record = self.context.object(
mft_object, offset=offset, layer_name=layer.name
)
# We will update this on each pass in the next loop and use it as the new offset.
attr_base_offset = mft_record.FirstAttrOffset
attr = self.context.object(
attribute_object,
offset=offset + attr_base_offset,
layer_name=layer.name,
)
# There is no field that has a count of Attributes
# Keep Attempting to read attributes until we get an invalid attr.AttrType
is_ads = False
file_name = renderers.NotAvailableValue
# The First $DATA Attr is the 'principal' file itself not the ADS
while attr.Attr_Header.AttrType.is_valid_choice:
if attr.Attr_Header.AttrType.lookup() == "FILE_NAME":
attr_data = attr.Attr_Data.cast(fn_object)
file_name = attr_data.get_full_name()
if attr.Attr_Header.AttrType.lookup() == "DATA":
if is_ads:
if not attr.Attr_Header.NonResidentFlag:
# Resident files are the most interesting.
if attr.Attr_Header.NameLength > 0:
ads_name = attr.get_resident_filename()
if not ads_name:
ads_name = renderers.NotAvailableValue
content = attr.get_resident_filecontent()
if content:
# Preparing for Disassembly
disasm = interfaces.renderers.BaseAbsentValue
architecture = layer.metadata.get(
"architecture", None
)
if architecture:
disasm = interfaces.renderers.Disassembly(
content, 0, architecture.lower()
)
else:
content = renderers.NotAvailableValue
disasm = interfaces.renderers.BaseAbsentValue
yield 0, (
format_hints.Hex(attr_data.vol.offset),
mft_record.get_signature(),
mft_record.RecordNumber,
attr.Attr_Header.AttrType.lookup(),
file_name,
ads_name,
format_hints.HexBytes(content),
disasm,
)
else:
is_ads = True
# If there's no advancement the loop will never end, so break it now
if attr.Attr_Header.Length == 0:
break
# Update the base offset to point to the next attribute
attr_base_offset += attr.Attr_Header.Length
# Get the next attribute
attr = self.context.object(
attribute_object,
offset=offset + attr_base_offset,
layer_name=layer.name,
)
def run(self):
return renderers.TreeGrid(
[
("Offset", format_hints.Hex),
("Record Type", str),
("Record Number", int),
("MFT Type", str),
("Filename", str),
("ADS Filename", str),
("Hexdump", format_hints.HexBytes),
("Disasm", interfaces.renderers.Disassembly),
],
self._generator(),
)
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from volatility3.framework import objects
from volatility3.framework import objects, constants, exceptions
class MFTEntry(objects.StructType):
@@ -21,3 +21,36 @@ class MFTFileName(objects.StructType):
"string", encoding="utf16", max_length=self.NameLength * 2, errors="replace"
)
return output
class MFTAttribute(objects.StructType):
"""This represents an MFT ATTRIBUTE"""
def get_resident_filename(self) -> str:
# To get the resident name, we jump to relative name offset and read name length * 2 bytes of data
try:
name = self._context.object(
self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "string",
layer_name=self.vol.layer_name,
offset=self.vol.offset + self.Attr_Header.NameOffset,
max_length=self.Attr_Header.NameLength * 2,
errors="replace",
encoding="utf16",
)
return name
except exceptions.InvalidAddressException:
return None
def get_resident_filecontent(self) -> bytes:
# To get the resident content, we jump to relative content offset and read name length * 2 bytes of data
try:
bytesobj = self._context.object(
self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "bytes",
layer_name=self.vol.layer_name,
offset=self.vol.offset + self.Attr_Header.ContentOffset,
native_layer_name=self.vol.native_layer_name,
length=self.Attr_Header.ContentLength,
)
return bytesobj
except exceptions.InvalidAddressException:
return None
+15 -1
View File
@@ -300,10 +300,24 @@
"kind": "base",
"name": "unsigned short"
}
},
"ContentLength": {
"offset": 16,
"type": {
"kind": "base",
"name": "unsigned int"
}
},
"ContentOffset": {
"offset": 20,
"type": {
"kind": "base",
"name": "unsigned short"
}
}
},
"kind": "struct",
"size": 16
"size": 24
},"RESIDENT_HEADER": {
"fields": {
"AttrSize": {