diff --git a/dissect/target/plugins/os/windows/regf/shellbags.py b/dissect/target/plugins/os/windows/regf/shellbags.py index 14be6320e..5811bcebb 100644 --- a/dissect/target/plugins/os/windows/regf/shellbags.py +++ b/dissect/target/plugins/os/windows/regf/shellbags.py @@ -1,6 +1,10 @@ +from __future__ import annotations + import io import logging import uuid +from datetime import datetime +from typing import Any, Iterator from dissect.cstruct import cstruct from dissect.util.ts import dostimestamp @@ -13,7 +17,9 @@ UserRecordDescriptorExtension, ) from dissect.target.helpers.record import create_extended_descriptor +from dissect.target.helpers.regutil import RegistryKey from dissect.target.plugin import Plugin, export +from dissect.target.target import Target log = logging.getLogger(__name__) @@ -170,30 +176,30 @@ }; struct SHITEM_MTP_VOLUME { - uint16 size; - uint8 type; - uint8 unk0; - uint16 data_size; - uint32 data_signature; - uint32 unk1; - uint16 unk2; - uint16 unk3; - uint16 unk4; - uint16 unk5; - uint32 unk6; - uint64 unk7; - uint32 unk8; - uint32 name_size; - uint32 identifier_size; - uint32 filesystem_size; - uint32 num_guid; - wchar name[name_size]; - wchar identifier[identifier_size]; - wchar filesystem[filesystem_size]; - SHITEM_MTP_VOLUME_GUID guids[num_guid]; - uint32 unk9; - char class_identifier[16]; - uint32 num_properties; + uint16 size; + uint8 type; + uint8 unk0; + uint16 data_size; + uint32 data_signature; + uint32 unk1; + uint16 unk2; + uint16 unk3; + uint16 unk4; + uint16 unk5; + uint32 unk6; + uint64 unk7; + uint32 unk8; + uint32 name_size; + uint32 identifier_size; + uint32 filesystem_size; + uint32 num_guid; + wchar name[name_size]; + wchar identifier[identifier_size]; + wchar filesystem[filesystem_size]; + SHITEM_MTP_VOLUME_GUID guids[num_guid]; + uint32 unk9; + char class_identifier[16]; + uint32 num_properties; }; struct SHITEM_USERS_PROPERTY_VIEW { @@ -248,267 +254,6 @@ DELEGATE_ITEM_IDENTIFIER = b"\x74\x1a\x59\x5e\x96\xdf\xd3\x48\x8d\x67\x17\x33\xbc\xee\x28\xba" -ShellBagRecord = create_extended_descriptor([RegistryRecordDescriptorExtension, UserRecordDescriptorExtension])( - "windows/shellbag", - [ - ("path", "path"), - ("datetime", "creation_time"), - ("datetime", "modification_time"), - ("datetime", "access_time"), - ("datetime", "regf_modification_time"), - ], -) - - -class ShellBagsPlugin(Plugin): - """Windows Shellbags plugin. - - References: - - https://github.com/libyal/libfwsi - """ - - KEYS = [ - "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell", - "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\ShellNoRoam", - "HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\Shell", - "HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\ShellNoRoam", - "HKEY_CURRENT_USER\\Software\\Classes\\Wow6432Node\\Local Settings\\Software\\Microsoft\\Windows\\Shell", - "HKEY_CURRENT_USER\\Software\\Classes\\Wow6432Node\\Local Settings\\Software\\Microsoft\\Windows\\ShellNoRoam", - "HKEY_CURRENT_USER\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU", - ] - - def __init__(self, target): - super().__init__(target) - self.bagkeys = list(self.target.registry.keys(self.KEYS)) - - def check_compatible(self) -> None: - if not len(self.bagkeys) > 0: - raise UnsupportedPluginError("No shellbags found") - - @export(record=ShellBagRecord) - def shellbags(self): - """Return Windows Shellbags. - - Shellbags are registry keys to improve user experience when using Windows Explorer. It stores information about - for example file/folder creation time and access time. - - References: - - https://www.hackingarticles.in/forensic-investigation-shellbags/ - """ - for regkey in self.bagkeys: - try: - bagsmru = regkey.subkey("BagMRU") - - for r in self._walk_bags(bagsmru, None): - yield r - except RegistryKeyNotFoundError: - continue - except Exception: # noqa - self.target.log.exception("Exception while parsing shellbags") - continue - - def _walk_bags(self, key, path_prefix): - path_prefix = [] if path_prefix is None else [path_prefix] - - user = self.target.registry.get_user(key) - - for reg_val in key.values(): - name, value = reg_val.name, reg_val.value - if not name.isdigit(): - continue - path = None - - for item in parse_shell_item_list(value): - path = "\\".join(path_prefix + [item.name]) - yield ShellBagRecord( - path=windows_path(path), - creation_time=item.creation_time, - modification_time=item.modification_time, - access_time=item.access_time, - regf_modification_time=key.ts, - _target=self.target, - _user=user, - _key=key, - ) - - for r in self._walk_bags(key.subkey(name), path): - yield r - - -def parse_shell_item_list(buf): - offset = 0 - end = len(buf) - list_buf = memoryview(buf) - - parent = None - while offset < end: - size = c_bag.uint16(list_buf[offset : offset + 2]) - - if size == 0: - break - - item_buf = list_buf[offset : offset + size] - - entry = None - if size >= 8: - signature = c_bag.uint32(item_buf[4:8]) - if signature == 0x39DE2184: - entry = CONTROL_PANEL_CATEGORY - elif signature == 0x4D677541: - entry = CDBURN - elif signature == 0x49534647: - entry = GAME_FOLDER - elif signature == 0xFFFFFF38: - entry = CONTROL_PANEL_CPL_FILE - - if size >= 10 and not entry: - signature = c_bag.uint32(item_buf[6:10]) - if signature == 0x07192006: - entry = MTP_FILE_ENTRY - elif signature == 0x10312005: - entry = MTP_VOLUME - elif signature in (0x10141981, 0x23A3DFD5, 0x23FEBBEE, 0x3B93AFBB, 0xBEEBEE00): - entry = USERS_PROPERTY_VIEW - elif signature == 0x46534643: - entry = UNKNOWN_0x74 - - if size >= 38 and not entry: - if item_buf[size - 32 : size] == DELEGATE_ITEM_IDENTIFIER: - entry = DELEGATE - - if size >= 3 and not entry: - class_type = item_buf[2] - mask_type = class_type & 0x70 - - if mask_type == 0x00: - if class_type == 0x00: - entry = UNKNOWN0 - elif class_type == 0x01: - entry = UNKNOWN1 - - elif mask_type == 0x10: - if class_type == 0x1F: - entry = ROOT_FOLDER - - elif mask_type == 0x20: - if class_type in (0x23, 0x25, 0x29, 0x2A, 0x2E, 0x2F): - entry = VOLUME - - elif mask_type == 0x30: - if class_type in (0x30, 0x31, 0x32, 0x35, 0x36, 0xB1): - entry = FILE_ENTRY - - elif mask_type == 0x40: - if class_type in (0x41, 0x42, 0x46, 0x47, 0x4C, 0xC3): - entry = NETWORK - - elif mask_type == 0x50: - if class_type == 0x52: - entry = COMPRESSED_FOLDER - - elif mask_type == 0x60: - if class_type == 0x61: - entry = URI - - elif mask_type == 0x70: - if class_type == 0x71: - entry = CONTROL_PANEL - else: - if not entry: - log.debug("No supported shell item found for size 0x%04x and type 0x%02x", size, class_type) - entry = UNKNOWN - - if not entry: - log.debug("No supported shell item found for size 0x%04x", size) - entry = UNKNOWN - - entry = entry(item_buf) - entry.parent = parent - - first_extension_block_offset = c_bag.uint16(item_buf[-2:]) - if 4 <= first_extension_block_offset < size - 2: - extension_offset = first_extension_block_offset - while extension_offset < size - 2: - extension_size = c_bag.uint16(item_buf[extension_offset : extension_offset + 2]) - - if extension_size == 0: - break - - if extension_size > size - extension_offset: - log.debug( - "Extension size exceeds item size: 0x%04x > 0x%04x - 0x%04x", - extension_size, - size, - extension_offset, - ) - break # Extension size too large - - extension_buf = item_buf[extension_offset : extension_offset + extension_size] - extension_signature = c_bag.uint32(extension_buf[4:8]) - - ext = None - - if extension_signature >> 16 != 0xBEEF: - log.debug("Got unsupported extension signature 0x%08x from item %r", extension_signature, entry) - pass # Unsupported - - elif extension_signature == 0xBEEF0000: - pass - - elif extension_signature == 0xBEEF0001: - pass - - elif extension_signature == 0xBEEF0003: - ext = EXTENSION_BLOCK_BEEF0004 - - elif extension_signature == 0xBEEF0004: - ext = EXTENSION_BLOCK_BEEF0004 - - elif extension_signature == 0xBEEF0005: - ext = EXTENSION_BLOCK_BEEF0005 - - elif extension_signature == 0xBEEF0006: - pass - - elif extension_signature == 0xBEEF000A: - pass - - elif extension_signature == 0xBEEF0013: - pass - - elif extension_signature == 0xBEEF0014: - pass - - elif extension_signature == 0xBEEF0019: - pass - - elif extension_signature == 0xBEEF0025: - pass - - elif extension_signature == 0xBEEF0026: - pass - - else: - log.debug( - "Got unsupported beef extension signature 0x%08x from item %r", extension_signature, entry - ) - pass - - if ext is None: - ext = EXTENSION_BLOCK - log.debug("Unimplemented extension signature 0x%08x from item %r", extension_signature, entry) - - ext = ext(extension_buf) - - entry.extensions.append(ext) - extension_offset += extension_size - - parent = entry - yield entry - - offset += size - - class SHITEM: STRUCT = None @@ -521,43 +266,43 @@ def __init__(self, buf): self.parent = None self.extensions = [] + def __repr__(self) -> str: + return f"<{self.__class__.__name__}>" + @property - def name(self): + def name(self) -> str: return f"" @property - def creation_time(self): + def creation_time(self) -> None: return None @property - def modification_time(self): + def modification_time(self) -> None: return None @property - def access_time(self): + def access_time(self) -> None: return None @property - def file_size(self): + def file_size(self) -> None: return None @property - def file_reference(self): + def file_reference(self) -> None: return None - def extension(self, cls): + def extension(self, cls: Any) -> Any | None: for ext in self.extensions: if isinstance(ext, cls): return ext return None - def __repr__(self): - return f"<{self.__class__.__name__}>" - class UNKNOWN(SHITEM): @property - def name(self): + def name(self) -> str: type_number = hex(self.type) if self.type else self.type return f"" @@ -573,7 +318,7 @@ def __init__(self, fh): self.guid = uuid.UUID(bytes_le=fh.read(16)) @property - def name(self): + def name(self) -> str: if self.guid: GUID_name = shell_folder_ids.DESCRIPTIONS.get(str(self.guid)) return GUID_name or f"" @@ -585,11 +330,11 @@ class UNKNOWN1(SHITEM): STRUCT = c_bag.SHITEM_UNKNOWN1 @property - def name(self): + def name(self) -> str: return f"" -class ROOT_FOLDER(SHITEM): # noqa +class ROOT_FOLDER(SHITEM): STRUCT = c_bag.SHITEM_ROOT_FOLDER def __init__(self, fh): @@ -601,7 +346,7 @@ def __init__(self, fh): self.extension = None @property - def name(self): + def name(self) -> str: GUID_name = shell_folder_ids.DESCRIPTIONS.get(str(self.guid)) return GUID_name or f"{{{self.item.folder_id.name}: {self.guid}}}" @@ -616,12 +361,12 @@ def __init__(self, buf): if self.type == 0x2E: self.identifier = uuid.UUID(bytes_le=buf[4:20].tobytes()) else: - self.volume_name = self.fh.read(20).rstrip(b"\x00").decode() + self.volume_name = self.fh.read(20).rstrip(b"\x00").decode(errors="surrogateescape") if self.size >= 41: self.identifier = uuid.UUID(bytes_le=buf[25:41].tobytes()) @property - def name(self): + def name(self) -> str: if self.volume_name: return self.volume_name if self.identifier: @@ -630,7 +375,7 @@ def name(self): return f"" -class FILE_ENTRY(SHITEM): # noqa +class FILE_ENTRY(SHITEM): STRUCT = c_bag.SHITEM_FILE_ENTRY def __init__(self, buf): @@ -644,7 +389,7 @@ def __init__(self, buf): self.primary_name = c_bag.wchar[None](self.fh) self.is_unicode = True else: - self.primary_name = c_bag.char[None](self.fh).decode() + self.primary_name = c_bag.char[None](self.fh).decode(errors="surrogateescape") self.is_unicode = False if self.fh.tell() % 2: @@ -659,17 +404,17 @@ def __init__(self, buf): if self.is_unicode: self.secondary_name = c_bag.wchar[None](self.fh) else: - self.secondary_name = c_bag.char[None](self.fh).decode() + self.secondary_name = c_bag.char[None](self.fh).decode(errors="surrogateescape") @property - def name(self): + def name(self) -> str: ext = self.extension(EXTENSION_BLOCK_BEEF0004) if ext and ext.long_name: return ext.long_name return self.primary_name @property - def modification_time(self): + def modification_time(self) -> datetime | None: ts = self.item.modification_time if ts > 0: return dostimestamp(ts, swap=True) @@ -691,15 +436,15 @@ def __init__(self, buf): self.comments = c_bag.char[None](self.fh) @property - def name(self): - return self.item.location.decode() + def name(self) -> str: + return self.item.location.decode(errors="surrogateescape") -class COMPRESSED_FOLDER(SHITEM): # noqa +class COMPRESSED_FOLDER(SHITEM): STRUCT = c_bag.SHITEM_COMPRESSED_FOLDER @property - def name(self): + def name(self) -> str: return "" @@ -714,14 +459,14 @@ def __init__(self, buf): if self.item.flags & 0x80: self.uri = c_bag.wchar[None](self.fh) else: - self.uri = c_bag.char[None](self.fh).decode() + self.uri = c_bag.char[None](self.fh).decode(errors="surrogateescape") @property - def name(self): + def name(self) -> str: return self.uri or "" -class CONTROL_PANEL(SHITEM): # noqa +class CONTROL_PANEL(SHITEM): STRUCT = c_bag.SHITEM_CONTROL_PANEL def __init__(self, buf): @@ -729,12 +474,12 @@ def __init__(self, buf): self.guid = uuid.UUID(bytes_le=self.item.guid) @property - def name(self): + def name(self) -> str: GUID_name = shell_folder_ids.DESCRIPTIONS.get(str(self.guid)) return GUID_name or f"" -class CONTROL_PANEL_CATEGORY(SHITEM): # noqa +class CONTROL_PANEL_CATEGORY(SHITEM): STRUCT = c_bag.SHITEM_CONTROL_PANEL_CATEGORY CATEGORIES = { 0: "All Control Panel Items", @@ -752,7 +497,7 @@ class CONTROL_PANEL_CATEGORY(SHITEM): # noqa } @property - def name(self): + def name(self) -> str: categ_str = self.CATEGORIES.get(self.item.category) if categ_str: return categ_str @@ -763,11 +508,11 @@ class CDBURN(SHITEM): STRUCT = c_bag.SHITEM_CDBURN @property - def name(self): + def name(self) -> str: return "" -class GAME_FOLDER(SHITEM): # noqa +class GAME_FOLDER(SHITEM): STRUCT = c_bag.SHITEM_GAME_FOLDER def __init__(self, buf): @@ -775,43 +520,43 @@ def __init__(self, buf): self.guid = uuid.UUID(bytes_le=self.item.identifier) @property - def name(self): + def name(self) -> str: return f"" -class CONTROL_PANEL_CPL_FILE(SHITEM): # noqa +class CONTROL_PANEL_CPL_FILE(SHITEM): STRUCT = c_bag.SHITEM_CONTROL_PANEL_CPL_FILE @property - def name(self): + def name(self) -> str: return f"" -class MTP_FILE_ENTRY(SHITEM): # noqa +class MTP_FILE_ENTRY(SHITEM): STRUCT = c_bag.SHITEM_MTP_FILE_ENTRY @property - def name(self): + def name(self) -> str: return "" @property - def creation_time(self): + def creation_time(self) -> datetime: return self.item.creation_time @property - def modification_time(self): + def modification_time(self) -> datetime: return self.item.modification_time -class MTP_VOLUME(SHITEM): # noqa +class MTP_VOLUME(SHITEM): STRUCT = c_bag.SHITEM_MTP_FILE_ENTRY @property - def name(self): + def name(self) -> str: return "" -class USERS_PROPERTY_VIEW(SHITEM): # noqa +class USERS_PROPERTY_VIEW(SHITEM): STRUCT = c_bag.SHITEM_USERS_PROPERTY_VIEW def __init__(self, buf): @@ -823,13 +568,13 @@ def __init__(self, buf): self.guid = uuid.UUID(bytes_le=self.item.identifier) @property - def name(self): + def name(self) -> str: # As we don't know how to handle identifier_size other than 16 bytes, we fall back to data_signature property_view = self.guid or self.identifier return f"" -class UNKNOWN_0x74(SHITEM): # noqa +class UNKNOWN_0x74(SHITEM): STRUCT = c_bag.SHITEM_UNKNOWN_0x74 def __init__(self, buf): @@ -839,11 +584,11 @@ def __init__(self, buf): self.subitem = c_bag.SHITEM_UNKNOWN_0x74_SUBITEM(self.fh) @property - def name(self): - return self.subitem.primary_name.decode() if self.subitem else "" + def name(self) -> str: + return self.subitem.primary_name.decode(errors="surrogateescape") if self.subitem else "" @property - def modification_time(self): + def modification_time(self) -> datetime | None: if self.subitem.modification_time > 0: return dostimestamp(self.subitem.modification_time, swap=True) if self.subitem else None return None @@ -858,38 +603,38 @@ def __init__(self, buf): self.shell_identifier = uuid.UUID(bytes_le=self.item.shell_identifier) @property - def name(self): + def name(self) -> str: GUID_name = shell_folder_ids.DESCRIPTIONS.get(str(self.shell_identifier)) return GUID_name if GUID_name else f"{{{self.shell_identifier}}}" -class EXTENSION_BLOCK: # noqa +class EXTENSION_BLOCK: def __init__(self, buf): self.buf = buf self.fh = io.BytesIO(buf) self.header = c_bag.EXTENSION_BLOCK_HEADER(self.fh) + def __repr__(self) -> str: + return f"" + @property - def size(self): + def size(self) -> int: return self.header.size @property - def data_size(self): + def data_size(self) -> int: return self.size - 8 # minus header @property - def version(self): + def version(self) -> int: return self.header.version @property - def signature(self): + def signature(self) -> int: return self.header.signature - def __repr__(self): - return f"" - -class EXTENSION_BLOCK_BEEF0004(EXTENSION_BLOCK): # noqa +class EXTENSION_BLOCK_BEEF0004(EXTENSION_BLOCK): def __init__(self, buf): super().__init__(buf) fh = self.fh @@ -923,8 +668,269 @@ def __init__(self, buf): self.localized_name = c_bag.wchar[None](fh) -class EXTENSION_BLOCK_BEEF0005(EXTENSION_BLOCK): # noqa +class EXTENSION_BLOCK_BEEF0005(EXTENSION_BLOCK): def __init__(self, buf): super().__init__(buf) c_bag.char[16](self.fh) # GUID? self.shell_items = self.fh.read(self.data_size - 18) + + +ShellBagRecord = create_extended_descriptor([RegistryRecordDescriptorExtension, UserRecordDescriptorExtension])( + "windows/shellbag", + [ + ("datetime", "ts_mtime"), + ("datetime", "ts_atime"), + ("datetime", "ts_btime"), + ("string", "type"), + ("path", "path"), + ("datetime", "regf_mtime"), + ], +) + + +class ShellBagsPlugin(Plugin): + """Windows Shellbags plugin.""" + + KEYS = [ + "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell", + "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\ShellNoRoam", + "HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\Shell", + "HKEY_CURRENT_USER\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\ShellNoRoam", + "HKEY_CURRENT_USER\\Software\\Classes\\Wow6432Node\\Local Settings\\Software\\Microsoft\\Windows\\Shell", + "HKEY_CURRENT_USER\\Software\\Classes\\Wow6432Node\\Local Settings\\Software\\Microsoft\\Windows\\ShellNoRoam", + "HKEY_CURRENT_USER\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU", + ] + + def __init__(self, target: Target): + super().__init__(target) + self.bagkeys: list[RegistryKey] = list(self.target.registry.keys(self.KEYS)) + + def check_compatible(self) -> None: + if not self.bagkeys: + raise UnsupportedPluginError("No shellbags found") + + @export(record=ShellBagRecord) + def shellbags(self) -> Iterator[ShellBagRecord]: + """Yields Windows Shellbags. + + Shellbags are registry keys to improve user experience when using Windows Explorer. + They contain information such as file and folder creation time and access time. + + References: + - https://github.com/libyal/libfwsi + - https://www.giac.org/paper/gcfa/9576/windows-shellbag-forensics-in-depth/128522 + - https://www.hackingarticles.in/forensic-investigation-shellbags/ + """ + for regkey in self.bagkeys: + try: + yield from self._walk_bags(regkey.subkey("BagMRU"), None) + + except RegistryKeyNotFoundError: + continue + + except Exception as e: + self.target.log.error("Exception while parsing shellbags") + self.target.log.debug("", exc_info=e) + continue + + def _walk_bags(self, key: RegistryKey, path_prefix: str | None) -> Iterator[ShellBagRecord]: + """Recursively walk shellbags from the given RegistryKey location.""" + path_prefix = [] if path_prefix is None else [path_prefix] + user = self.target.registry.get_user(key) + + for reg_val in key.values(): + name, value = reg_val.name, reg_val.value + if not name.isdigit(): + continue + path = None + + for item in parse_shell_item_list(value): + path = "\\".join(path_prefix + [item.name]) + yield ShellBagRecord( + ts_mtime=item.modification_time, + ts_atime=item.access_time, + ts_btime=item.creation_time, + type=item.__class__.__name__, + path=windows_path(path), + regf_mtime=key.ts, + _key=key, + _user=user, + _target=self.target, + ) + + yield from self._walk_bags(key.subkey(name), path) + + +def parse_shell_item_list(buf: bytes) -> Iterator[SHITEM]: + """Parse a shellbag item from the given bytes.""" + + offset = 0 + end = len(buf) + list_buf = memoryview(buf) + parent = None + + while offset < end: + size = c_bag.uint16(list_buf[offset : offset + 2]) + + if size == 0: + break + + item_buf = list_buf[offset : offset + size] + + entry = None + if size >= 8: + signature = c_bag.uint32(item_buf[4:8]) + if signature == 0x39DE2184: + entry = CONTROL_PANEL_CATEGORY + elif signature == 0x4D677541: + entry = CDBURN + elif signature == 0x49534647: + entry = GAME_FOLDER + elif signature == 0xFFFFFF38: + entry = CONTROL_PANEL_CPL_FILE + + if size >= 10 and not entry: + signature = c_bag.uint32(item_buf[6:10]) + if signature == 0x07192006: + entry = MTP_FILE_ENTRY + elif signature == 0x10312005: + entry = MTP_VOLUME + elif signature in (0x10141981, 0x23A3DFD5, 0x23FEBBEE, 0x3B93AFBB, 0xBEEBEE00): + entry = USERS_PROPERTY_VIEW + elif signature == 0x46534643: + entry = UNKNOWN_0x74 + + if size >= 38 and not entry: + if item_buf[size - 32 : size] == DELEGATE_ITEM_IDENTIFIER: + entry = DELEGATE + + if size >= 3 and not entry: + class_type = item_buf[2] + mask_type = class_type & 0x70 + + if mask_type == 0x00: + if class_type == 0x00: + entry = UNKNOWN0 + elif class_type == 0x01: + entry = UNKNOWN1 + + elif mask_type == 0x10: + if class_type == 0x1F: + entry = ROOT_FOLDER + + elif mask_type == 0x20: + if class_type in (0x23, 0x25, 0x29, 0x2A, 0x2E, 0x2F): + entry = VOLUME + + elif mask_type == 0x30: + if class_type in (0x30, 0x31, 0x32, 0x35, 0x36, 0xB1): + entry = FILE_ENTRY + + elif mask_type == 0x40: + if class_type in (0x41, 0x42, 0x46, 0x47, 0x4C, 0xC3): + entry = NETWORK + + elif mask_type == 0x50: + if class_type == 0x52: + entry = COMPRESSED_FOLDER + + elif mask_type == 0x60: + if class_type == 0x61: + entry = URI + + elif mask_type == 0x70: + if class_type == 0x71: + entry = CONTROL_PANEL + else: + if not entry: + log.debug("No supported shell item found for size 0x%04x and type 0x%02x", size, class_type) + entry = UNKNOWN + + if not entry: + log.debug("No supported shell item found for size 0x%04x", size) + entry = UNKNOWN + + entry = entry(item_buf) + entry.parent = parent + + first_extension_block_offset = c_bag.uint16(item_buf[-2:]) + if 4 <= first_extension_block_offset < size - 2: + extension_offset = first_extension_block_offset + while extension_offset < size - 2: + extension_size = c_bag.uint16(item_buf[extension_offset : extension_offset + 2]) + + if extension_size == 0: + break + + if extension_size > size - extension_offset: + log.debug( + "Extension size exceeds item size: 0x%04x > 0x%04x - 0x%04x", + extension_size, + size, + extension_offset, + ) + break # Extension size too large + + extension_buf = item_buf[extension_offset : extension_offset + extension_size] + extension_signature = c_bag.uint32(extension_buf[4:8]) + + ext = None + + if extension_signature >> 16 != 0xBEEF: + log.debug("Got unsupported extension signature 0x%08x from item %r", extension_signature, entry) + pass # Unsupported + + elif extension_signature == 0xBEEF0000: + pass + + elif extension_signature == 0xBEEF0001: + pass + + elif extension_signature == 0xBEEF0003: + ext = EXTENSION_BLOCK_BEEF0004 + + elif extension_signature == 0xBEEF0004: + ext = EXTENSION_BLOCK_BEEF0004 + + elif extension_signature == 0xBEEF0005: + ext = EXTENSION_BLOCK_BEEF0005 + + elif extension_signature == 0xBEEF0006: + pass + + elif extension_signature == 0xBEEF000A: + pass + + elif extension_signature == 0xBEEF0013: + pass + + elif extension_signature == 0xBEEF0014: + pass + + elif extension_signature == 0xBEEF0019: + pass + + elif extension_signature == 0xBEEF0025: + pass + + elif extension_signature == 0xBEEF0026: + pass + + else: + log.debug( + "Got unsupported beef extension signature 0x%08x from item %r", extension_signature, entry + ) + pass + + if ext is None: + ext = EXTENSION_BLOCK + log.debug("Unimplemented extension signature 0x%08x from item %r", extension_signature, entry) + + ext = ext(extension_buf) + + entry.extensions.append(ext) + extension_offset += extension_size + + parent = entry + offset += size + yield entry diff --git a/tests/plugins/os/windows/regf/test_shellbags.py b/tests/plugins/os/windows/regf/test_shellbags.py index 03795558b..78822aea6 100644 --- a/tests/plugins/os/windows/regf/test_shellbags.py +++ b/tests/plugins/os/windows/regf/test_shellbags.py @@ -4,7 +4,12 @@ import pytest -from dissect.target.plugins.os.windows.regf.shellbags import parse_shell_item_list +from dissect.target.helpers.regutil import VirtualHive, VirtualKey +from dissect.target.plugins.os.windows.regf.shellbags import ( + ShellBagsPlugin, + parse_shell_item_list, +) +from dissect.target.target import Target @pytest.mark.parametrize( @@ -32,9 +37,9 @@ "@shell32.dll,-21813", ), ], - ids=["char", "wchar"], + ids=("char", "wchar"), ) -def test_parse_shell_item_list( +def test_shellbags_parser( shellbag: bytes, name: str, modification_time: datetime.datetime, localized_name: str | bytes ) -> None: bag = next(parse_shell_item_list(shellbag)) @@ -46,3 +51,100 @@ def test_parse_shell_item_list( assert extension.long_name == name assert extension.localized_name == localized_name + + +@pytest.mark.parametrize( + "bags, expected_type, expected_path", + [ + # single VOLUME with path `A:\` + ( + [("", "1", "19002f413a5c000000000000000000000000000000000000000000")], + ["VOLUME"], + ["A:\\"], + ), + # single VOLUME with invalid unicode character + ( + [("", "0", "19002f4d7920436f6d70757465725c52c3a9ea6d79000000000000")], + ["VOLUME"], + ["My Computer\\R\u00e9\udceamy"], + ), + # nested path leading to "My Computer\\C:\\Users\\Administrator\\Downloads" + ( + [ + # ROOT_FOLDER + ("", "1", "14001f50e04fd020ea3a6910a2d808002b30309d0000"), + # VOLUME + ("1", "0", "19002f433a5c000000000000000000000000000000000000000000"), + # FILE_ENTRY + ( + "1\\0", + "0", + ( + "7800310000000000875747ab1100557365727300640009000400efbe2f4d2e31" + "875747ab2e000000d00100000000010000000000000000003a00000000004899" + "e90055007300650072007300000040007300680065006c006c00330032002e00" + "64006c006c002c002d0032003100380031003300000014000000" + ), + ), + # FILE_ENTRY + ( + "1\\0\\0", + "0", + ( + "6400310000000000875748ab100041444d494e497e3100004c0009000400efbe" + "875747ab875748ab2e000000b9f5010000000200000000000000000000000000" + "0000688f5a00410064006d0069006e006900730074007200610074006f007200" + "000018000000" + ), + ), + # FILE_ENTRY + ( + "1\\0\\0\\0", + "0", + ( + "8400310000000000875748ab1100444f574e4c4f7e3100006c0009000400efbe" + "875747ab875748ab2e000000c1f5010000000200000000000000000042000000" + "0000192d580044006f0077006e006c006f006100640073000000400073006800" + "65006c006c00330032002e0064006c006c002c002d0032003100370039003800" + "000018000000" + ), + ), + ], + ["ROOT_FOLDER", "VOLUME", "FILE_ENTRY", "FILE_ENTRY", "FILE_ENTRY"], + [ + "My Computer", + "My Computer\\C:", + "My Computer\\C:\\Users", + "My Computer\\C:\\Users\\Administrator", + "My Computer\\C:\\Users\\Administrator\\Downloads", + ], + ), + ], + ids=( + "single-volume", + "single-volume-invalid-unicode", + "nested-path", + ), +) +def test_shellbags_plugin( + target_win_users: Target, + hive_hku: VirtualHive, + bags: list[tuple[str, str, str]], + expected_type: list[str], + expected_path: list[str], +) -> None: + """test if shellbags mapped to a registry hive are found and parsed correctly.""" + + key_name = "Software\\Microsoft\\Windows\\Shell\\BagMRU" + + for bag_key, bag_name, bag_value in bags: + key = VirtualKey(hive_hku, f"{key_name}\\{bag_key}") + key.add_value(bag_name, bytes.fromhex(bag_value)) + hive_hku.map_key(f"{key_name}\\{bag_key}", key) + + target_win_users.add_plugin(ShellBagsPlugin) + results = list(target_win_users.shellbags()) + + assert len(results) == len(bags) + assert [r.type for r in results] == expected_type + assert [r.path for r in results] == expected_path