diff --git a/volatility3/framework/symbols/windows/extensions/gui.py b/volatility3/framework/symbols/windows/extensions/gui.py index 000c50319f..cdb4a387ba 100644 --- a/volatility3/framework/symbols/windows/extensions/gui.py +++ b/volatility3/framework/symbols/windows/extensions/gui.py @@ -324,9 +324,77 @@ def get_string(self) -> interfaces.objects.ObjectInterface: encoding="utf16", ) + class tagCLIP(objects.StructType): + """A class for clipboard format entries (one per clipboard item)""" + + # TODO: Extend clipboard format mapping if needed. + CF_FORMATS = { + 1: "CF_TEXT", + 2: "CF_BITMAP", + 3: "CF_METAFILEPICT", + 7: "CF_OEMTEXT", + 8: "CF_DIB", + 13: "CF_UNICODETEXT", + 14: "CF_ENHMETAFILE", + 15: "CF_HDROP", + 16: "CF_LOCALE", + 17: "CF_DIBV5", + } + + def get_format_name(self) -> str: + """Returns the clipboard format as a human-readable string""" + try: + fmt_val = int(self.fmt) + return self.CF_FORMATS.get(fmt_val, f"CF_UNKNOWN({fmt_val:#x})") + except exceptions.InvalidAddressException: + return "CF_UNKNOWN" + + class tagCLIPDATA(objects.StructType): + """A class for clipboard data objects stored in Windows memory""" + + def get_data(self) -> Optional[bytes]: + """Returns the raw clipboard data as bytes""" + + # TODO: Verify abData consistency; future Windows may change tagCLIPDATA layout. + try: + size = int(self.cbData) + + # TODO: Improve clipboard size validation if needed. + MAX_CLIPBOARD_SIZE = 50 * 1024 * 1024 # 50 MiB + if size == 0 or size > MAX_CLIPBOARD_SIZE: + return None + + layer = self._context.layers[self.vol.layer_name] + return layer.read(self.abData.vol.offset, size, pad=True) + + except exceptions.InvalidAddressException: + return None + + def get_text(self, fmt: str) -> Optional[str]: + """Returns clipboard data as readable text if format is text-based""" + data = self.get_data() + + if data is None: + return None + + # TODO: Extend support for more clipboard formats as needed. + + if fmt == "CF_UNICODETEXT": + return data.decode("utf-16-le", errors="replace").rstrip("\x00") + elif fmt in ("CF_TEXT", "CF_OEMTEXT"): + return data.decode("latin-1", errors="replace").rstrip("\x00") + + elif fmt == "CF_HDROP": + # File paths stored as null-separated UTF-16 list + return data.decode("utf-16-le", errors="replace").rstrip("\x00") + return None + class_types = { "tagWINDOWSTATION": tagWINDOWSTATION, "tagDESKTOP": tagDESKTOP, "tagWND": tagWND, "_LARGE_UNICODE_STRING": LARGE_UNICODE_STRING, + # Clipboard plugin extensions + "tagCLIPDATA": tagCLIPDATA, + "tagCLIP": tagCLIP, } diff --git a/volatility3/framework/symbols/windows/gui/gui-win10-10586-x64.json b/volatility3/framework/symbols/windows/gui/gui-win10-10586-x64.json index 0308f4c7d3..27cb700512 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-10586-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-10586-x64.json @@ -13653,18 +13653,14 @@ "tagWINDOWSTATION": { "fields": { "pClipBase": { + "offset": 96, "type": { + "kind": "pointer", "subtype": { - "count": 104, - "subtype": { - "kind": "struct", - "name": "tagCLIP" - }, - "kind": "array" - }, - "kind": "pointer" - }, - "offset": 96 + "kind": "struct", + "name": "tagCLIP" + } + } }, "dwSessionId": { "type": { @@ -18027,6 +18023,30 @@ }, "kind": "struct", "size": 6 + }, + "tagCLIPDATA": { + "kind": "struct", + "size": 24, + "fields": { + "cbData": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "abData": { + "offset": 20, + "type": { + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "count": 0 + } + } + } } }, "base_types": { @@ -18790,4 +18810,4 @@ }, "format": "4.0.0" } -} +} \ No newline at end of file diff --git a/volatility3/framework/symbols/windows/gui/gui-win10-15063-x64.json b/volatility3/framework/symbols/windows/gui/gui-win10-15063-x64.json index a69cbb7c32..ea0991ea16 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-15063-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-15063-x64.json @@ -13653,18 +13653,14 @@ "tagWINDOWSTATION": { "fields": { "pClipBase": { + "offset": 96, "type": { + "kind": "pointer", "subtype": { - "count": 104, - "subtype": { - "kind": "struct", - "name": "tagCLIP" - }, - "kind": "array" - }, - "kind": "pointer" - }, - "offset": 96 + "kind": "struct", + "name": "tagCLIP" + } + } }, "dwSessionId": { "type": { @@ -18027,6 +18023,30 @@ }, "kind": "struct", "size": 6 + }, + "tagCLIPDATA": { + "kind": "struct", + "size": 24, + "fields": { + "cbData": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "abData": { + "offset": 20, + "type": { + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "count": 0 + } + } + } } }, "base_types": { @@ -18790,4 +18810,4 @@ }, "format": "4.0.0" } -} +} \ No newline at end of file diff --git a/volatility3/framework/symbols/windows/gui/gui-win10-16299-x64.json b/volatility3/framework/symbols/windows/gui/gui-win10-16299-x64.json index 54e8aeec32..65fb617eaf 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-16299-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-16299-x64.json @@ -13653,18 +13653,14 @@ "tagWINDOWSTATION": { "fields": { "pClipBase": { + "offset": 96, "type": { + "kind": "pointer", "subtype": { - "count": 104, - "subtype": { - "kind": "struct", - "name": "tagCLIP" - }, - "kind": "array" - }, - "kind": "pointer" - }, - "offset": 96 + "kind": "struct", + "name": "tagCLIP" + } + } }, "dwSessionId": { "type": { @@ -18027,6 +18023,30 @@ }, "kind": "struct", "size": 6 + }, + "tagCLIPDATA": { + "kind": "struct", + "size": 24, + "fields": { + "cbData": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "abData": { + "offset": 20, + "type": { + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "count": 0 + } + } + } } }, "base_types": { @@ -18790,4 +18810,4 @@ }, "format": "4.0.0" } -} +} \ No newline at end of file diff --git a/volatility3/framework/symbols/windows/gui/gui-win10-17134-x64.json b/volatility3/framework/symbols/windows/gui/gui-win10-17134-x64.json index 48b97a1c2b..1328e27814 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-17134-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-17134-x64.json @@ -13696,18 +13696,14 @@ "tagWINDOWSTATION": { "fields": { "pClipBase": { + "offset": 96, "type": { + "kind": "pointer", "subtype": { - "count": 104, - "subtype": { - "kind": "struct", - "name": "tagCLIP" - }, - "kind": "array" - }, - "kind": "pointer" - }, - "offset": 96 + "kind": "struct", + "name": "tagCLIP" + } + } }, "dwSessionId": { "type": { @@ -18070,6 +18066,30 @@ }, "kind": "struct", "size": 6 + }, + "tagCLIPDATA": { + "kind": "struct", + "size": 24, + "fields": { + "cbData": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "abData": { + "offset": 20, + "type": { + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "count": 0 + } + } + } } }, "base_types": { @@ -18833,4 +18853,4 @@ }, "format": "4.0.0" } -} +} \ No newline at end of file diff --git a/volatility3/framework/symbols/windows/gui/gui-win10-17763-x64.json b/volatility3/framework/symbols/windows/gui/gui-win10-17763-x64.json index 33db6c28d9..98cb254d36 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-17763-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-17763-x64.json @@ -13696,18 +13696,14 @@ "tagWINDOWSTATION": { "fields": { "pClipBase": { + "offset": 96, "type": { + "kind": "pointer", "subtype": { - "count": 104, - "subtype": { - "kind": "struct", - "name": "tagCLIP" - }, - "kind": "array" - }, - "kind": "pointer" - }, - "offset": 96 + "kind": "struct", + "name": "tagCLIP" + } + } }, "dwSessionId": { "type": { @@ -18070,6 +18066,30 @@ }, "kind": "struct", "size": 6 + }, + "tagCLIPDATA": { + "kind": "struct", + "size": 24, + "fields": { + "cbData": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "abData": { + "offset": 20, + "type": { + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "count": 0 + } + } + } } }, "base_types": { @@ -18833,4 +18853,4 @@ }, "format": "4.0.0" } -} +} \ No newline at end of file diff --git a/volatility3/framework/symbols/windows/gui/gui-win10-18362-x64.json b/volatility3/framework/symbols/windows/gui/gui-win10-18362-x64.json index bb1c74f7f8..22281d6302 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-18362-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-18362-x64.json @@ -13696,18 +13696,14 @@ "tagWINDOWSTATION": { "fields": { "pClipBase": { + "offset": 96, "type": { + "kind": "pointer", "subtype": { - "count": 104, - "subtype": { - "kind": "struct", - "name": "tagCLIP" - }, - "kind": "array" - }, - "kind": "pointer" - }, - "offset": 96 + "kind": "struct", + "name": "tagCLIP" + } + } }, "dwSessionId": { "type": { @@ -18070,6 +18066,30 @@ }, "kind": "struct", "size": 6 + }, + "tagCLIPDATA": { + "kind": "struct", + "size": 24, + "fields": { + "cbData": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "abData": { + "offset": 20, + "type": { + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "count": 0 + } + } + } } }, "base_types": { @@ -18833,4 +18853,4 @@ }, "format": "4.0.0" } -} +} \ No newline at end of file diff --git a/volatility3/framework/symbols/windows/gui/gui-win10-19041-x64.json b/volatility3/framework/symbols/windows/gui/gui-win10-19041-x64.json index 74868f1a78..c6cf3a1c52 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-19041-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-19041-x64.json @@ -13696,18 +13696,14 @@ "tagWINDOWSTATION": { "fields": { "pClipBase": { + "offset": 96, "type": { + "kind": "pointer", "subtype": { - "count": 104, - "subtype": { - "kind": "struct", - "name": "tagCLIP" - }, - "kind": "array" - }, - "kind": "pointer" - }, - "offset": 96 + "kind": "struct", + "name": "tagCLIP" + } + } }, "dwSessionId": { "type": { @@ -18070,6 +18066,30 @@ }, "kind": "struct", "size": 6 + }, + "tagCLIPDATA": { + "kind": "struct", + "size": 24, + "fields": { + "cbData": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "abData": { + "offset": 20, + "type": { + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "count": 0 + } + } + } } }, "base_types": { @@ -18833,4 +18853,4 @@ }, "format": "4.0.0" } -} +} \ No newline at end of file diff --git a/volatility3/framework/symbols/windows/gui/gui-win10-19577-x64.json b/volatility3/framework/symbols/windows/gui/gui-win10-19577-x64.json index e718710cf5..5615912c4d 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-19577-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-19577-x64.json @@ -13696,18 +13696,14 @@ "tagWINDOWSTATION": { "fields": { "pClipBase": { + "offset": 96, "type": { + "kind": "pointer", "subtype": { - "count": 104, - "subtype": { - "kind": "struct", - "name": "tagCLIP" - }, - "kind": "array" - }, - "kind": "pointer" - }, - "offset": 96 + "kind": "struct", + "name": "tagCLIP" + } + } }, "dwSessionId": { "type": { @@ -18070,6 +18066,30 @@ }, "kind": "struct", "size": 6 + }, + "tagCLIPDATA": { + "kind": "struct", + "size": 24, + "fields": { + "cbData": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned long" + } + }, + "abData": { + "offset": 20, + "type": { + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "count": 0 + } + } + } } }, "base_types": { @@ -18833,4 +18853,4 @@ }, "format": "4.0.0" } -} +} \ No newline at end of file diff --git a/volatility3/plugins/windows/clipboard.py b/volatility3/plugins/windows/clipboard.py new file mode 100644 index 0000000000..6dadf848a0 --- /dev/null +++ b/volatility3/plugins/windows/clipboard.py @@ -0,0 +1,135 @@ +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 + + +"""Plugin to extract clipboard data from Windows memory dumps.""" + +import logging +from typing import List + +from volatility3.framework import interfaces, renderers, exceptions, constants +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import windowstations + +vollog = logging.getLogger(__name__) + + +class Clipboard(interfaces.plugins.PluginInterface): + """Reads clipboard data from a Windows memory dump. + It looks at the tagCLIP list inside each WindowStation (pClipBase) + and shows the clipboard formats and data stored there. + + Note: This is just a prototype. Right now it only works through + WindowStation. In the future, support for USER handle table + (tagSHAREDINFO) can be added when pClipBase is missing or zero""" + + _required_framework_version = (2, 0, 0) + _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.VersionRequirement( + name="windowstations", + component=windowstations.WindowStations, + version=(1, 0, 0), + ), + ] + + def _generator(self): + """ + TODO: Later add USER handle table (tagSHAREDINFO) support + to recover clipboard data when pClipBase is missing. + """ + + kernel_name = self.config["kernel"] + + for ( + winsta, + station_name, + session_id, + ) in windowstations.WindowStations.scan_window_stations( + self.context, self.config_path, kernel_name + ): + gui_table_name = winsta.vol.type_name.split(constants.BANG)[0] + layer_name = winsta.vol.layer_name + + try: + clip_count = int(winsta.cNumClipFormats) + clip_base_ptr = int(winsta.pClipBase) + except exceptions.InvalidAddressException: + vollog.debug(f"Cannot read clipboard fields for station {station_name}") + continue + + vollog.debug( + f"Station={station_name} Session={session_id} " + f"cNumClipFormats={clip_count} pClipBase={clip_base_ptr:#x}" + ) + + if clip_count == 0 or clip_count > 512: + continue + + if clip_base_ptr <= 0xFFFF: + continue + + try: + tagclip_size = self.context.symbol_space.get_type( + gui_table_name + constants.BANG + "tagCLIP" + ).size + except exceptions.InvalidAddressException: + vollog.debug(f"Cannot get tagCLIP size for {station_name}") + continue + + for i in range(clip_count): + try: + clip = self.context.object( + gui_table_name + constants.BANG + "tagCLIP", + layer_name=layer_name, + offset=clip_base_ptr + i * tagclip_size, + ) + + fmt_name = clip.get_format_name() + handle_val = int(clip.hData) + + vollog.debug(f" clip[{i}]: fmt={fmt_name} hData={handle_val:#x}") + + # hData is a USER handle — not a direct pointer. + # Resolving it requires walking tagSHAREDINFO.aheList + # which is not yet implemented (see TODO above). + # For now we report format and handle without data. + data_display: interfaces.renderers.BaseAbsentValue | str = ( + renderers.NotAvailableValue() + ) + + yield ( + 0, + ( + session_id, + station_name, + fmt_name, + format_hints.Hex(handle_val), + data_display, + ), + ) + + except exceptions.InvalidAddressException as e: + vollog.debug(f" clip[{i}]: {e}") + continue + + def run(self): + return renderers.TreeGrid( + [ + ("Session", int), + ("WindowStation", str), + ("Format", str), + ("Handle", format_hints.Hex), + ("Data", str), + ], + self._generator(), + )