From 02e77e3d0a048583ae59afde4066e85c6a47099b Mon Sep 17 00:00:00 2001 From: Irha batool Date: Thu, 25 Jun 2026 19:20:13 +0500 Subject: [PATCH 1/3] WIP: Add clipboard plugin and tagCLIPDATA/tagCLIP structures --- .../symbols/windows/extensions/gui.py | 54 +++++- volatility3/plugins/windows/clipboard.py | 160 ++++++++++++++++++ 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 volatility3/plugins/windows/clipboard.py diff --git a/volatility3/framework/symbols/windows/extensions/gui.py b/volatility3/framework/symbols/windows/extensions/gui.py index 000c50319f..1ab336b9b2 100644 --- a/volatility3/framework/symbols/windows/extensions/gui.py +++ b/volatility3/framework/symbols/windows/extensions/gui.py @@ -304,7 +304,7 @@ def get_window_procedure(self): # This is copy/paste from UNICODE_STRING in `symbols/windows/extensions/__init__.py` # The versioning of modules would get very ugly if we let different modules share implementations - # across different data structures + # across different data structure class LARGE_UNICODE_STRING(objects.StructType): """A class for Windows unicode string structures.""" @@ -324,6 +324,58 @@ def get_string(self) -> interfaces.objects.ObjectInterface: encoding="utf16", ) + 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""" + try: + size = self.cbData + if size == 0 or size > 0x100000: + return None + return bytes(bytearray(self.abData)) + 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 + try: + if "UNICODE" in fmt: + return data.decode("utf-16-le", errors="replace").rstrip("\x00") + else: + return data.decode("latin-1", errors="replace").rstrip("\x00") + except Exception: + return None + + class tagCLIP(objects.StructType): + """A class for clipboard format entries (one per clipboard item)""" + + # Common Windows clipboard format constants + 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_types = { "tagWINDOWSTATION": tagWINDOWSTATION, "tagDESKTOP": tagDESKTOP, diff --git a/volatility3/plugins/windows/clipboard.py b/volatility3/plugins/windows/clipboard.py new file mode 100644 index 0000000000..b4fb668a23 --- /dev/null +++ b/volatility3/plugins/windows/clipboard.py @@ -0,0 +1,160 @@ +# 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 +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist, vadinfo + +vollog = logging.getLogger(__name__) + + +class Clipboard(interfaces.plugins.PluginInterface): + """Extracts clipboard data from a Windows memory image.""" + + _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="pslist", + component=pslist.PsList, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="vadinfo", + component=vadinfo.VadInfo, + version=(2, 0, 0), + ), + ] + + def _generator(self): + """Scan csrss.exe VADs for clipboard data.""" + kernel_name = self.config["kernel"] + kernel = self.context.modules[kernel_name] + + for proc in pslist.PsList.list_processes( + context=self.context, + kernel_module_name=kernel_name, + ): + try: + proc_name = proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ).lower() + except exceptions.InvalidAddressException: + continue + + if proc_name not in ("csrss.exe", "rdpclip.exe"): + continue + + try: + proc_id = int(proc.UniqueProcessId) + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException: + continue + + proc_layer = self.context.layers[proc_layer_name] + + protect_values = vadinfo.VadInfo.protect_values( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + ) + + for vad in vadinfo.VadInfo.list_vads(proc): + try: + vad_start = vad.get_start() + vad_size = vad.get_size() + protection = vad.get_protection( + protect_values, + vadinfo.winnt_protections, + ) + tag = vad.get_tag() + except exceptions.InvalidAddressException: + continue + + if vad_size == 0 or vad_size > 0x200000: + continue + + if protection not in ( + "PAGE_READWRITE", + "PAGE_READONLY", + "PAGE_EXECUTE_READ", + "PAGE_EXECUTE_READWRITE", + ): + continue + + try: + data = proc_layer.read(vad_start, vad_size, pad=True) + except exceptions.InvalidAddressException: + continue + + if not data: + continue + + # Try UTF-16-LE (most Windows clipboard text) + try: + text = data.decode("utf-16-le", errors="ignore") + text = text.strip("\x00").strip() + if len(text) >= 4: + printable = "".join( + c for c in text if c.isprintable() or c in "\n\r\t" + ) + if len(printable) >= 4: + yield ( + 0, + ( + proc_id, + proc_name, + format_hints.Hex(vad_start), + "UTF16: " + printable[:256], + ), + ) + continue + except Exception: + pass + + # Try UTF-8 / ASCII + try: + text = data.decode("utf-8", errors="ignore").strip("\x00").strip() + if len(text) >= 4: + printable = "".join( + c for c in text if c.isprintable() or c in "\n\r\t" + ) + if len(printable) >= 4: + yield ( + 0, + ( + proc_id, + proc_name, + format_hints.Hex(vad_start), + "ASCII: " + printable[:256], + ), + ) + except Exception: + pass + + def run(self): + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Offset", format_hints.Hex), + ("Data", str), + ], + self._generator(), + ) \ No newline at end of file From b483f72dd1fa231a78ab43a1b37af649d11fa890 Mon Sep 17 00:00:00 2001 From: Irha batool Date: Thu, 25 Jun 2026 23:06:49 +0500 Subject: [PATCH 2/3] Add tagCLIPDATA to GUI JSON files and update clipboard plugin --- .../symbols/windows/extensions/gui.py | 2 + .../windows/gui/gui-win10-10586-x64.json | 26 ++- .../windows/gui/gui-win10-15063-x64.json | 26 ++- .../windows/gui/gui-win10-16299-x64.json | 26 ++- .../windows/gui/gui-win10-17134-x64.json | 26 ++- .../windows/gui/gui-win10-17763-x64.json | 26 ++- .../windows/gui/gui-win10-18362-x64.json | 26 ++- .../windows/gui/gui-win10-19041-x64.json | 26 ++- .../windows/gui/gui-win10-19577-x64.json | 26 ++- volatility3/plugins/windows/clipboard.py | 157 ++++++------------ 10 files changed, 252 insertions(+), 115 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/gui.py b/volatility3/framework/symbols/windows/extensions/gui.py index 1ab336b9b2..5a8335c942 100644 --- a/volatility3/framework/symbols/windows/extensions/gui.py +++ b/volatility3/framework/symbols/windows/extensions/gui.py @@ -381,4 +381,6 @@ def get_format_name(self) -> str: "tagDESKTOP": tagDESKTOP, "tagWND": tagWND, "_LARGE_UNICODE_STRING": LARGE_UNICODE_STRING, + "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..975bbce637 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-10586-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-10586-x64.json @@ -18027,6 +18027,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", + "count": 0, + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + } + } } }, "base_types": { @@ -18790,4 +18814,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..cd60a9253d 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-15063-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-15063-x64.json @@ -18027,6 +18027,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", + "count": 0, + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + } + } } }, "base_types": { @@ -18790,4 +18814,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..36511d03b7 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-16299-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-16299-x64.json @@ -18027,6 +18027,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", + "count": 0, + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + } + } } }, "base_types": { @@ -18790,4 +18814,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..650513167d 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-17134-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-17134-x64.json @@ -18070,6 +18070,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", + "count": 0, + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + } + } } }, "base_types": { @@ -18833,4 +18857,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..38590a84e2 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-17763-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-17763-x64.json @@ -18070,6 +18070,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", + "count": 0, + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + } + } } }, "base_types": { @@ -18833,4 +18857,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..8d22c74095 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-18362-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-18362-x64.json @@ -18070,6 +18070,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", + "count": 0, + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + } + } } }, "base_types": { @@ -18833,4 +18857,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..4c116af53c 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-19041-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-19041-x64.json @@ -18070,6 +18070,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", + "count": 0, + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + } + } } }, "base_types": { @@ -18833,4 +18857,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..23be6ad642 100644 --- a/volatility3/framework/symbols/windows/gui/gui-win10-19577-x64.json +++ b/volatility3/framework/symbols/windows/gui/gui-win10-19577-x64.json @@ -18070,6 +18070,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", + "count": 0, + "subtype": { + "kind": "base", + "name": "unsigned char" + } + } + } + } } }, "base_types": { @@ -18833,4 +18857,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 index b4fb668a23..7a48503269 100644 --- a/volatility3/plugins/windows/clipboard.py +++ b/volatility3/plugins/windows/clipboard.py @@ -9,7 +9,7 @@ from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist, vadinfo +from volatility3.plugins.windows import windowstations vollog = logging.getLogger(__name__) @@ -29,132 +29,75 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="pslist", - component=pslist.PsList, - version=(3, 0, 0), - ), - requirements.VersionRequirement( - name="vadinfo", - component=vadinfo.VadInfo, - version=(2, 0, 0), + name="windowstations", + component=windowstations.WindowStations, + version=(1, 0, 0), ), ] def _generator(self): - """Scan csrss.exe VADs for clipboard data.""" + """Walk window stations and extract clipboard data.""" kernel_name = self.config["kernel"] - kernel = self.context.modules[kernel_name] - for proc in pslist.PsList.list_processes( - context=self.context, - kernel_module_name=kernel_name, + for winsta, station_name, session_id in windowstations.WindowStations.scan_window_stations( + self.context, self.config_path, kernel_name ): try: - proc_name = proc.ImageFileName.cast( - "string", - max_length=proc.ImageFileName.vol.count, - errors="replace", - ).lower() - except exceptions.InvalidAddressException: - continue + clip_count = int(winsta.cNumClipFormats) + if clip_count == 0 or clip_count > 512: + continue - if proc_name not in ("csrss.exe", "rdpclip.exe"): - continue + clip_array = winsta.pClipBase.dereference() - try: - proc_id = int(proc.UniqueProcessId) - proc_layer_name = proc.add_process_layer() except exceptions.InvalidAddressException: + vollog.debug( + f"Could not read clipboard base for station {station_name}" + ) continue - proc_layer = self.context.layers[proc_layer_name] - - protect_values = vadinfo.VadInfo.protect_values( - self.context, - kernel.layer_name, - kernel.symbol_table_name, - ) - - for vad in vadinfo.VadInfo.list_vads(proc): + for i in range(clip_count): try: - vad_start = vad.get_start() - vad_size = vad.get_size() - protection = vad.get_protection( - protect_values, - vadinfo.winnt_protections, - ) - tag = vad.get_tag() - except exceptions.InvalidAddressException: - continue + clip = clip_array[i] + fmt_name = clip.get_format_name() + handle_val = int(clip.hData) + + clip_data_ptr = clip.hData.dereference() + data = clip_data_ptr.get_data() + + if data is None: + data_display = renderers.NotAvailableValue() + else: + text = clip_data_ptr.get_text(fmt_name) + if text: + data_display = text + else: + data_display = data.hex() - if vad_size == 0 or vad_size > 0x200000: - continue - - if protection not in ( - "PAGE_READWRITE", - "PAGE_READONLY", - "PAGE_EXECUTE_READ", - "PAGE_EXECUTE_READWRITE", - ): - continue - - try: - data = proc_layer.read(vad_start, vad_size, pad=True) except exceptions.InvalidAddressException: - continue + fmt_name = renderers.NotAvailableValue() + handle_val = 0 + data_display = renderers.NotAvailableValue() + + yield ( + 0, + ( + session_id, + station_name, + fmt_name, + format_hints.Hex(handle_val), + data_display, + ), + ) - if not data: - continue - - # Try UTF-16-LE (most Windows clipboard text) - try: - text = data.decode("utf-16-le", errors="ignore") - text = text.strip("\x00").strip() - if len(text) >= 4: - printable = "".join( - c for c in text if c.isprintable() or c in "\n\r\t" - ) - if len(printable) >= 4: - yield ( - 0, - ( - proc_id, - proc_name, - format_hints.Hex(vad_start), - "UTF16: " + printable[:256], - ), - ) - continue - except Exception: - pass - - # Try UTF-8 / ASCII - try: - text = data.decode("utf-8", errors="ignore").strip("\x00").strip() - if len(text) >= 4: - printable = "".join( - c for c in text if c.isprintable() or c in "\n\r\t" - ) - if len(printable) >= 4: - yield ( - 0, - ( - proc_id, - proc_name, - format_hints.Hex(vad_start), - "ASCII: " + printable[:256], - ), - ) - except Exception: - pass - def run(self): return renderers.TreeGrid( [ - ("PID", int), - ("Process", str), - ("Offset", format_hints.Hex), + ("Session", int), + ("WindowStation", str), + ("Format", str), + ("Handle", format_hints.Hex), ("Data", str), ], self._generator(), - ) \ No newline at end of file + ) + From 7eb595bb44474414ea442ebeb021e7d0b16b5df0 Mon Sep 17 00:00:00 2001 From: Irha batool Date: Sat, 27 Jun 2026 21:39:29 +0500 Subject: [PATCH 3/3] Add prototype support for Windows clipboard structures --- .../symbols/windows/extensions/gui.py | 70 ++++++----- .../windows/gui/gui-win10-10586-x64.json | 20 ++-- .../windows/gui/gui-win10-15063-x64.json | 20 ++-- .../windows/gui/gui-win10-16299-x64.json | 20 ++-- .../windows/gui/gui-win10-17134-x64.json | 20 ++-- .../windows/gui/gui-win10-17763-x64.json | 20 ++-- .../windows/gui/gui-win10-18362-x64.json | 20 ++-- .../windows/gui/gui-win10-19041-x64.json | 20 ++-- .../windows/gui/gui-win10-19577-x64.json | 20 ++-- volatility3/plugins/windows/clipboard.py | 112 +++++++++++------- 10 files changed, 178 insertions(+), 164 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/gui.py b/volatility3/framework/symbols/windows/extensions/gui.py index 5a8335c942..cdb4a387ba 100644 --- a/volatility3/framework/symbols/windows/extensions/gui.py +++ b/volatility3/framework/symbols/windows/extensions/gui.py @@ -304,7 +304,7 @@ def get_window_procedure(self): # This is copy/paste from UNICODE_STRING in `symbols/windows/extensions/__init__.py` # The versioning of modules would get very ugly if we let different modules share implementations - # across different data structure + # across different data structures class LARGE_UNICODE_STRING(objects.StructType): """A class for Windows unicode string structures.""" @@ -324,36 +324,10 @@ def get_string(self) -> interfaces.objects.ObjectInterface: encoding="utf16", ) - 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""" - try: - size = self.cbData - if size == 0 or size > 0x100000: - return None - return bytes(bytearray(self.abData)) - 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 - try: - if "UNICODE" in fmt: - return data.decode("utf-16-le", errors="replace").rstrip("\x00") - else: - return data.decode("latin-1", errors="replace").rstrip("\x00") - except Exception: - return None - class tagCLIP(objects.StructType): """A class for clipboard format entries (one per clipboard item)""" - # Common Windows clipboard format constants + # TODO: Extend clipboard format mapping if needed. CF_FORMATS = { 1: "CF_TEXT", 2: "CF_BITMAP", @@ -375,12 +349,52 @@ def get_format_name(self) -> str: 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 975bbce637..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": { @@ -18043,11 +18039,11 @@ "offset": 20, "type": { "kind": "array", - "count": 0, "subtype": { "kind": "base", "name": "unsigned char" - } + }, + "count": 0 } } } 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 cd60a9253d..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": { @@ -18043,11 +18039,11 @@ "offset": 20, "type": { "kind": "array", - "count": 0, "subtype": { "kind": "base", "name": "unsigned char" - } + }, + "count": 0 } } } 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 36511d03b7..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": { @@ -18043,11 +18039,11 @@ "offset": 20, "type": { "kind": "array", - "count": 0, "subtype": { "kind": "base", "name": "unsigned char" - } + }, + "count": 0 } } } 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 650513167d..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": { @@ -18086,11 +18082,11 @@ "offset": 20, "type": { "kind": "array", - "count": 0, "subtype": { "kind": "base", "name": "unsigned char" - } + }, + "count": 0 } } } 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 38590a84e2..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": { @@ -18086,11 +18082,11 @@ "offset": 20, "type": { "kind": "array", - "count": 0, "subtype": { "kind": "base", "name": "unsigned char" - } + }, + "count": 0 } } } 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 8d22c74095..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": { @@ -18086,11 +18082,11 @@ "offset": 20, "type": { "kind": "array", - "count": 0, "subtype": { "kind": "base", "name": "unsigned char" - } + }, + "count": 0 } } } 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 4c116af53c..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": { @@ -18086,11 +18082,11 @@ "offset": 20, "type": { "kind": "array", - "count": 0, "subtype": { "kind": "base", "name": "unsigned char" - } + }, + "count": 0 } } } 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 23be6ad642..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": { @@ -18086,11 +18082,11 @@ "offset": 20, "type": { "kind": "array", - "count": 0, "subtype": { "kind": "base", "name": "unsigned char" - } + }, + "count": 0 } } } diff --git a/volatility3/plugins/windows/clipboard.py b/volatility3/plugins/windows/clipboard.py index 7a48503269..6dadf848a0 100644 --- a/volatility3/plugins/windows/clipboard.py +++ b/volatility3/plugins/windows/clipboard.py @@ -1,12 +1,13 @@ # 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 +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 @@ -15,7 +16,13 @@ class Clipboard(interfaces.plugins.PluginInterface): - """Extracts clipboard data from a Windows memory image.""" + """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) @@ -36,58 +43,84 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ] def _generator(self): - """Walk window stations and extract clipboard data.""" + """ + 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( + 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) - if clip_count == 0 or clip_count > 512: - continue + clip_base_ptr = int(winsta.pClipBase) + except exceptions.InvalidAddressException: + vollog.debug(f"Cannot read clipboard fields for station {station_name}") + continue - clip_array = winsta.pClipBase.dereference() + 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"Could not read clipboard base for station {station_name}" - ) + vollog.debug(f"Cannot get tagCLIP size for {station_name}") continue for i in range(clip_count): try: - clip = clip_array[i] + 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) - clip_data_ptr = clip.hData.dereference() - data = clip_data_ptr.get_data() - - if data is None: - data_display = renderers.NotAvailableValue() - else: - text = clip_data_ptr.get_text(fmt_name) - if text: - data_display = text - else: - data_display = data.hex() - - except exceptions.InvalidAddressException: - fmt_name = renderers.NotAvailableValue() - handle_val = 0 - data_display = renderers.NotAvailableValue() - - yield ( - 0, - ( - session_id, - station_name, - fmt_name, - format_hints.Hex(handle_val), - data_display, - ), - ) + 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( @@ -100,4 +133,3 @@ def run(self): ], self._generator(), ) -