mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-24 23:22:23 +02:00
478 lines
17 KiB
Python
478 lines
17 KiB
Python
# References :
|
|
# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-XCA/%5bMS-XCA%5d.pdf
|
|
# https://github.com/libyal/libscca/blob/main/documentation/Windows%20Prefetch%20File%20(PF)%20format.asciidoc
|
|
# https://github.com/volatilityfoundation/volatility3/
|
|
# https://github.com/EricZimmerman/Prefetch/tree/master/Prefetch
|
|
import logging, pathlib, datetime, io, struct
|
|
from volatility3.framework import renderers, interfaces, exceptions, constants
|
|
from volatility3.framework.configuration import requirements
|
|
from volatility3.plugins.windows import filescan
|
|
from volatility3.framework.renderers import format_hints, conversion
|
|
|
|
vollog = logging.getLogger(__name__)
|
|
from typing import Tuple, List, Union
|
|
|
|
|
|
class BitStream:
|
|
def __init__(self, source: bytes, in_pos: int):
|
|
self.source = source
|
|
self.index = in_pos + 4
|
|
# read UInt16 little endian
|
|
mask = struct.unpack_from('<H', source, in_pos)[0] << 16
|
|
mask += struct.unpack_from('<H', source, in_pos + 2)[0]
|
|
self.mask = mask
|
|
self.bits = 32
|
|
|
|
def lookup(self, n: int) -> int:
|
|
if n == 0:
|
|
return 0
|
|
return self.mask >> (32 - n)
|
|
|
|
def skip(self, n: int) -> Union[None, Exception]:
|
|
self.mask = ((self.mask << n) & 0xFFFFFFFF)
|
|
self.bits -= n
|
|
if self.bits < 16:
|
|
if self.index + 2 > len(self.source):
|
|
return Exception("EOF Error")
|
|
# read UInt16 little endian
|
|
self.mask += ((struct.unpack_from('<H', self.source, self.index)[0]) << (16 - self.bits)) & 0xFFFFFFFF
|
|
self.index += 2
|
|
self.bits += 16
|
|
|
|
return None
|
|
|
|
def __str__(self):
|
|
return f"{self.id}: symbol {self.symbol} length {self.length}"
|
|
|
|
|
|
class PREFIX_CODE_NODE:
|
|
def __init__(self):
|
|
self.id = 0
|
|
self.symbol = 0
|
|
self.leaf = False
|
|
self.child = [None, None]
|
|
|
|
def __str__(self):
|
|
return f"Node {self.id}: symbol {self.symbol} leaf {self.leaf}"
|
|
|
|
|
|
class PREFIX_CODE_SYMBOL:
|
|
def __init__(self):
|
|
self.id = 0
|
|
self.symbol = 0
|
|
self.length = 0
|
|
|
|
def __str__(self):
|
|
return f"Symbol {self.id}: symbol {self.symbol} length {self.length}"
|
|
|
|
|
|
def prefix_code_tree_add_leaf(treeNodes: List[PREFIX_CODE_NODE], leafIndex: int, mask: int, bits: int) -> int:
|
|
node = treeNodes[0]
|
|
i = leafIndex + 1
|
|
childIndex = None
|
|
|
|
while bits > 1:
|
|
bits -= 1
|
|
childIndex = (mask >> bits) & 1
|
|
if node.child[childIndex] == None:
|
|
node.child[childIndex] = treeNodes[i]
|
|
treeNodes[i].leaf = False
|
|
i += 1
|
|
node = node.child[childIndex]
|
|
|
|
node.child[mask & 1] = treeNodes[leafIndex]
|
|
|
|
return i
|
|
|
|
|
|
def prefix_code_tree_rebuild(input: bytes) -> PREFIX_CODE_NODE:
|
|
treeNodes = [PREFIX_CODE_NODE() for _ in range(1024)]
|
|
symbolInfo = [PREFIX_CODE_SYMBOL() for _ in range(512)]
|
|
|
|
for i in range(256):
|
|
value = input[i]
|
|
|
|
symbolInfo[2 * i].id = 2 * i
|
|
symbolInfo[2 * i].symbol = 2 * i
|
|
symbolInfo[2 * i].length = value & 0xf
|
|
|
|
value >>= 4
|
|
|
|
symbolInfo[2 * i + 1].id = 2 * i + 1
|
|
symbolInfo[2 * i + 1].symbol = 2 * i + 1
|
|
symbolInfo[2 * i + 1].length = value & 0xf
|
|
|
|
symbolInfo = sorted(symbolInfo, key=lambda x: (x.length, x.symbol))
|
|
|
|
i = 0
|
|
while i < 512 and symbolInfo[i].length == 0:
|
|
i += 1
|
|
|
|
mask = 0
|
|
bits = 1
|
|
|
|
root = treeNodes[0]
|
|
root.leaf = False
|
|
|
|
j = 1
|
|
while i < 512:
|
|
treeNodes[j].id = j
|
|
treeNodes[j].symbol = symbolInfo[i].symbol
|
|
treeNodes[j].leaf = True
|
|
mask = mask << (symbolInfo[i].length - bits)
|
|
bits = symbolInfo[i].length
|
|
j = prefix_code_tree_add_leaf(treeNodes, j, mask, bits)
|
|
mask += 1
|
|
i += 1
|
|
|
|
return root
|
|
|
|
|
|
def prefix_code_tree_decode_symbol(bstr: BitStream, root: PREFIX_CODE_NODE) -> Tuple[int, Union[None, Exception]]:
|
|
node = root
|
|
i = 0
|
|
while True:
|
|
bit = bstr.lookup(1)
|
|
err = bstr.skip(1)
|
|
if err is not None:
|
|
return 0, err
|
|
|
|
node = node.child[bit]
|
|
if node == None:
|
|
return 0, Exception("Corruption detected")
|
|
|
|
if node.leaf:
|
|
break
|
|
return node.symbol, None
|
|
|
|
|
|
def lz77_huffman_decompress_chunck(in_idx: int,
|
|
input: bytes,
|
|
out_idx: int,
|
|
output: bytearray,
|
|
chunk_size: int) -> Tuple[int, int, Union[None, Exception]]:
|
|
# Ensure there are at least 256 bytes available to read
|
|
if in_idx + 256 > len(input):
|
|
return 0, 0, Exception("EOF Error")
|
|
|
|
root = prefix_code_tree_rebuild(input[in_idx:])
|
|
# print_tree(root)
|
|
bstr = BitStream(input, in_idx + 256)
|
|
|
|
i = out_idx
|
|
|
|
while i < out_idx + chunk_size:
|
|
symbol, err = prefix_code_tree_decode_symbol(bstr, root)
|
|
|
|
if err is not None:
|
|
return int(bstr.index), i, err
|
|
|
|
if symbol < 256:
|
|
output[i] = symbol
|
|
i += 1
|
|
else:
|
|
symbol -= 256
|
|
length = symbol & 15
|
|
symbol >>= 4
|
|
|
|
offset = 0
|
|
if symbol != 0:
|
|
offset = int(bstr.lookup(symbol))
|
|
|
|
offset |= 1 << symbol
|
|
offset = -offset
|
|
|
|
if length == 15:
|
|
length = bstr.source[bstr.index] + 15
|
|
bstr.index += 1
|
|
|
|
if length == 270:
|
|
length = struct.unpack_from('<H', bstr.source, bstr.index)[0]
|
|
bstr.index += 2
|
|
|
|
err = bstr.skip(symbol)
|
|
if err is not None:
|
|
return int(bstr.index), i, err
|
|
|
|
length += 3
|
|
while length > 0:
|
|
if i + offset < 0:
|
|
print(i + offset)
|
|
return int(bstr.index), i, Exception("Decompression Error")
|
|
|
|
output[i] = output[i + offset]
|
|
i += 1
|
|
length -= 1
|
|
if length == 0:
|
|
break
|
|
return int(bstr.index), i, None
|
|
|
|
|
|
def lz77_huffman_decompress(input: bytes, output_size: int) -> Tuple[bytes, Union[None, Exception]]:
|
|
output = bytearray(output_size)
|
|
err = None
|
|
|
|
# Index into the input buffer.
|
|
in_idx = 0
|
|
|
|
# Index into the output buffer.
|
|
out_idx = 0
|
|
|
|
while True:
|
|
# How much data belongs in the current chunk. Chunks
|
|
# are split into maximum 65536 bytes.
|
|
chunk_size = output_size - out_idx
|
|
if chunk_size > 65536:
|
|
chunk_size = 65536
|
|
|
|
in_idx, out_idx, err = lz77_huffman_decompress_chunck(
|
|
in_idx, input, out_idx, output, chunk_size)
|
|
if err is not None:
|
|
return output, err
|
|
if out_idx >= len(output) or in_idx >= len(input):
|
|
break
|
|
return output, None
|
|
|
|
|
|
class Prefetch(interfaces.plugins.PluginInterface):
|
|
"""Get and parse the prefetch files"""
|
|
_required_framework_version = (2, 0, 0)
|
|
_version = (1, 0, 0)
|
|
|
|
@classmethod
|
|
def get_requirements(cls):
|
|
return [requirements.ModuleRequirement(name='kernel', description='Windows kernel',
|
|
architectures=["Intel32", "Intel64"]),
|
|
requirements.PluginRequirement(name='filescan', plugin=filescan.FileScan, version=(0, 0, 0)), ]
|
|
|
|
@classmethod
|
|
def version_17(cls, prefetch_file):
|
|
"""Extract pf information for Version 17"""
|
|
stream = io.BytesIO(prefetch_file)
|
|
|
|
stream.seek(0x000C)
|
|
file_size = int.from_bytes(stream.read(4), "little")
|
|
|
|
stream.seek(0x0010)
|
|
executable_raw = stream.read(60).decode('utf-16')
|
|
executable_name = executable_raw.split('\u0000')[0]
|
|
|
|
stream.seek(0x004C)
|
|
prefetch_hash = int.from_bytes(stream.read(4), "little")
|
|
|
|
stream.seek(0x0078)
|
|
last_execution_filetime = int.from_bytes(stream.read(8), "little")
|
|
last_execution_filetime_human = conversion.wintime_to_datetime(last_execution_filetime)
|
|
|
|
stream.seek(0x0090)
|
|
execution_counter = int.from_bytes(stream.read(4), "little")
|
|
|
|
yield (
|
|
executable_name,
|
|
file_size,
|
|
format_hints.Hex(prefetch_hash),
|
|
last_execution_filetime_human,
|
|
execution_counter
|
|
)
|
|
|
|
@classmethod
|
|
def version_23(cls, prefetch_file):
|
|
"""Extract pf information for Version 23"""
|
|
stream = io.BytesIO(prefetch_file)
|
|
|
|
stream.seek(0x000C)
|
|
file_size = int.from_bytes(stream.read(4), "little")
|
|
|
|
stream.seek(0x0010)
|
|
executable_raw = stream.read(60).decode('utf-16')
|
|
executable_name = executable_raw.split('\u0000')[0]
|
|
|
|
stream.seek(0x004C)
|
|
prefetch_hash = int.from_bytes(stream.read(4), "little")
|
|
|
|
stream.seek(0x0080)
|
|
last_execution_filetime = int.from_bytes(stream.read(8), "little")
|
|
last_execution_filetime_human = conversion.wintime_to_datetime(last_execution_filetime)
|
|
|
|
stream.seek(0x0098)
|
|
execution_counter = int.from_bytes(stream.read(4), "little")
|
|
|
|
yield (
|
|
executable_name,
|
|
file_size,
|
|
format_hints.Hex(prefetch_hash),
|
|
last_execution_filetime_human,
|
|
execution_counter
|
|
)
|
|
|
|
@classmethod
|
|
def version_26(cls, prefetch_file):
|
|
"""Extract pf information for Version 26"""
|
|
stream = io.BytesIO(prefetch_file)
|
|
|
|
stream.seek(0x000C)
|
|
file_size = int.from_bytes(stream.read(4), "little")
|
|
|
|
stream.seek(0x0010)
|
|
executable_raw = stream.read(60).decode('utf-16')
|
|
executable_name = executable_raw.split('\u0000')[0]
|
|
|
|
stream.seek(0x004C)
|
|
prefetch_hash = int.from_bytes(stream.read(4), "little")
|
|
|
|
stream.seek(0x0080)
|
|
last_execution_filetime = int.from_bytes(stream.read(8), "little")
|
|
last_execution_filetime_human = conversion.wintime_to_datetime(last_execution_filetime)
|
|
|
|
stream.seek(0x00D0)
|
|
execution_counter = int.from_bytes(stream.read(4), "little")
|
|
|
|
yield (
|
|
executable_name,
|
|
file_size,
|
|
format_hints.Hex(prefetch_hash),
|
|
last_execution_filetime_human,
|
|
execution_counter
|
|
)
|
|
|
|
@classmethod
|
|
def version_30(cls, prefetch_file):
|
|
"""Extract pf information for Version 30"""
|
|
stream = io.BytesIO(prefetch_file)
|
|
|
|
stream.seek(0x000C)
|
|
file_size = int.from_bytes(stream.read(4), "little")
|
|
|
|
stream.seek(0x0010)
|
|
executable_raw = stream.read(60).decode('utf-16')
|
|
executable_name = executable_raw.split('\u0000')[0]
|
|
|
|
stream.seek(0x004C)
|
|
prefetch_hash = int.from_bytes(stream.read(4), "little")
|
|
|
|
stream.seek(0x0080)
|
|
# The first FILETIME is the most recent run time
|
|
last_execution_filetime = int.from_bytes(stream.read(8), "little")
|
|
last_execution_filetime_human = conversion.wintime_to_datetime(last_execution_filetime)
|
|
|
|
stream.seek(0x00C8) # Variant 1
|
|
execution_counter = int.from_bytes(stream.read(4), "little")
|
|
if execution_counter == 0:
|
|
stream.seek(0x00D0) # Variant 2
|
|
execution_counter = int.from_bytes(stream.read(4), "little")
|
|
|
|
yield (
|
|
executable_name,
|
|
file_size,
|
|
format_hints.Hex(prefetch_hash),
|
|
last_execution_filetime_human,
|
|
execution_counter
|
|
)
|
|
|
|
@classmethod
|
|
def parse_prefetch(cls, prefetch_file):
|
|
WinXpOrWin2K3 = 17
|
|
VistaOrWin7 = 23
|
|
Win8xOrWin2012x = 26
|
|
Win10OrWin11 = 30
|
|
stream = io.BytesIO(prefetch_file)
|
|
# First, we need to know if the prefetch is compressed (Win10/11)
|
|
signature = prefetch_file[:3].decode()
|
|
if signature == "MAM":
|
|
vollog.info("Windows 1X prefetch file detected.")
|
|
# The size of decompressed data is at offset 4
|
|
stream.seek(0x0004)
|
|
decompressed_size = int.from_bytes(stream.read(4), "little")
|
|
vollog.info(f"decompressed size : {decompressed_size}")
|
|
stream.seek(0x0008)
|
|
compressed_bytes = stream.read()
|
|
prefetch_file = lz77_huffman_decompress(bytearray(compressed_bytes), decompressed_size)[0]
|
|
try:
|
|
file_version = int.from_bytes(prefetch_file[:4], "little")
|
|
signature = prefetch_file[4:8].decode()
|
|
vollog.info(f'File version : {file_version}')
|
|
vollog.info(f"Signature : {signature}")
|
|
except:
|
|
# We can not even read the header
|
|
pass
|
|
|
|
if signature != "SCCA":
|
|
vollog.info("Wrong signature, should be SCCA")
|
|
return
|
|
if file_version == WinXpOrWin2K3:
|
|
for result in cls.version_17(prefetch_file):
|
|
yield result
|
|
elif file_version == VistaOrWin7:
|
|
for result in cls.version_23(prefetch_file):
|
|
yield result
|
|
elif file_version == Win8xOrWin2012x:
|
|
for result in cls.version_26(prefetch_file):
|
|
yield result
|
|
elif file_version == Win10OrWin11:
|
|
for result in cls.version_30(prefetch_file):
|
|
yield result
|
|
|
|
def _generator(self, files):
|
|
kernel = self.context.modules[self.config['kernel']]
|
|
offsets = []
|
|
for file_obj in files:
|
|
"""Get the prefetch recovered files from the “filescan” plugin; """
|
|
try:
|
|
file_name = file_obj.FileName.String
|
|
file_extension = pathlib.Path(file_name).suffix
|
|
if file_extension == ".pf":
|
|
"""If found, try to dump the prefetch file (inspired from the "DumpFiles" plugin)"""
|
|
memory_objects = []
|
|
memory_layer_name = self.context.layers[kernel.layer_name].config['memory_layer']
|
|
memory_layer = self.context.layers[memory_layer_name]
|
|
primary_layer = self.context.layers[kernel.layer_name]
|
|
for member_name in ["DataSectionObject", "ImageSectionObject"]:
|
|
try:
|
|
section_obj = getattr(file_obj.SectionObjectPointer, member_name)
|
|
control_area = section_obj.dereference().cast("_CONTROL_AREA")
|
|
if control_area.is_valid():
|
|
vollog.info(f"Found : {file_obj.FileName.String}")
|
|
memory_objects.append((control_area, memory_layer))
|
|
except exceptions.InvalidAddressException:
|
|
vollog.log(constants.LOGLEVEL_VVV,
|
|
f"{member_name} is unavailable for file {file_obj.vol.offset:#x}")
|
|
try:
|
|
scm_pointer = file_obj.SectionObjectPointer.SharedCacheMap
|
|
shared_cache_map = scm_pointer.dereference().cast("_SHARED_CACHE_MAP")
|
|
if shared_cache_map.is_valid():
|
|
memory_objects.append((shared_cache_map, primary_layer))
|
|
except exceptions.InvalidAddressException:
|
|
vollog.log(constants.LOGLEVEL_VVV,
|
|
f"SharedCacheMap is unavailable for file {file_obj.vol.offset:#x}")
|
|
vollog.info(f"memory_objects : {memory_objects}")
|
|
|
|
"""Now, read and parse our PF to retrieve our artifacts"""
|
|
for memory_object, layer in memory_objects:
|
|
bytes_read = 0
|
|
prefetch_raw = b''
|
|
try:
|
|
for mem_offset, _, datasize in memory_object.get_available_pages():
|
|
prefetch_raw += layer.read(mem_offset, datasize, pad=True)
|
|
bytes_read += len(prefetch_raw)
|
|
vollog.info(f"Read {bytes_read}")
|
|
if not bytes_read:
|
|
vollog.info(f"Prefetch is empty")
|
|
else:
|
|
"""Prefetch parsing"""
|
|
for result in self.parse_prefetch(prefetch_raw):
|
|
yield 0, result
|
|
|
|
except exceptions.InvalidAddressException:
|
|
vollog.debug(f"Unable to dump file at {file_obj.vol.offset:#x}")
|
|
pass
|
|
except exceptions.InvalidAddressException:
|
|
continue
|
|
|
|
def run(self):
|
|
kernel = self.context.modules[self.config['kernel']]
|
|
return renderers.TreeGrid([
|
|
("ExecutableName", str),
|
|
("FileSize", int),
|
|
("PrefetchHash", format_hints.Hex),
|
|
("LastExecution", datetime.datetime), ("ExecutionCounter", int)],
|
|
self._generator(filescan.FileScan.scan_files(self.context, kernel.layer_name, kernel.symbol_table_name))) |