-
Notifications
You must be signed in to change notification settings - Fork 691
Add bounded Windows clipboard format enumeration #2008
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AmirGhiassian
wants to merge
8
commits into
volatilityfoundation:develop
Choose a base branch
from
AmirGhiassian:fix-pr-2001-clipboard-review
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
15df80a
WIP: Add clipboard plugin and tagCLIPDATA/tagCLIP structures
IB-Mustafa b288462
Add tagCLIPDATA to GUI JSON files and update clipboard plugin
IB-Mustafa 4ff9c24
Add prototype support for Windows clipboard structures
IB-Mustafa ab93e24
Fix Windows clipboard format enumeration
AmirGhiassian 54d1397
Harden clipboard format name handling
AmirGhiassian 125e6cf
Align clipboard format test behavior
AmirGhiassian 742b408
Exclude broken leechcorepyc release
AmirGhiassian 5a0a2f8
Potential fix for pull request finding
AmirGhiassian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| # This file is Copyright 2026 Volatility Foundation and licensed under the Volatility Software License 1.0 | ||
| # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 | ||
|
|
||
| from types import SimpleNamespace | ||
|
|
||
| from volatility3.framework.symbols.windows.extensions import gui | ||
| from volatility3.plugins.windows import clipboard, windowstations | ||
|
|
||
|
|
||
| class _FakeFormat(int): | ||
| def __new__(cls, value, name=None): | ||
| result = int.__new__(cls, value) | ||
| result._name = name | ||
| return result | ||
|
|
||
| def lookup(self): | ||
| if self._name is None: | ||
| raise ValueError | ||
| return self._name | ||
|
|
||
|
|
||
| class _FakePointer: | ||
| def __init__(self, value, target): | ||
| self._value = value | ||
| self._target = target | ||
|
|
||
| def __int__(self): | ||
| return self._value | ||
|
|
||
| def __bool__(self): | ||
| return bool(self._value) | ||
|
|
||
| def dereference(self): | ||
| return self._target | ||
|
|
||
|
|
||
| def test_clipboard_format_names(): | ||
| get_format_name = gui.GUIExtensions.tagCLIP.get_format_name | ||
|
|
||
| assert get_format_name(SimpleNamespace(fmt=_FakeFormat(4, "CF_SYLK"))) == "CF_SYLK" | ||
| assert ( | ||
| get_format_name(SimpleNamespace(fmt=_FakeFormat(0xC001))) | ||
| == "REGISTERED_FORMAT(0xc001)" | ||
| ) | ||
| assert ( | ||
| get_format_name(SimpleNamespace(fmt=_FakeFormat(0x123))) == "CF_UNKNOWN(0x123)" | ||
| ) | ||
|
|
||
|
|
||
| def test_list_clipboard_formats_uses_bounded_symbol_array(monkeypatch): | ||
| clips = [ | ||
| SimpleNamespace( | ||
| get_format_name=lambda index=index: f"FORMAT_{index}", | ||
| hData=0x1000 + index, | ||
| ) | ||
| for index in range(104) | ||
| ] | ||
| station = SimpleNamespace( | ||
| cNumClipFormats=512, | ||
| pClipBase=_FakePointer(0x2000, clips), | ||
| ) | ||
| monkeypatch.setattr( | ||
| windowstations.WindowStations, | ||
| "scan_window_stations", | ||
| lambda context, config_path, kernel_module_name: iter( | ||
| [(station, "WinSta0", 1)] | ||
| ), | ||
| ) | ||
|
|
||
| results = list( | ||
| clipboard.Clipboard.list_clipboard_formats( | ||
| SimpleNamespace(), "plugins.Clipboard", "kernel" | ||
| ) | ||
| ) | ||
|
|
||
| assert len(results) == 104 | ||
| assert results[0] == (1, "WinSta0", "FORMAT_0", 0x1000) | ||
| assert results[-1] == (1, "WinSta0", "FORMAT_103", 0x1067) | ||
|
|
||
|
|
||
| def test_list_clipboard_formats_skips_null_pointer(monkeypatch): | ||
| station = SimpleNamespace( | ||
| cNumClipFormats=1, | ||
| pClipBase=_FakePointer(0, None), | ||
| ) | ||
| monkeypatch.setattr( | ||
| windowstations.WindowStations, | ||
| "scan_window_stations", | ||
| lambda context, config_path, kernel_module_name: iter( | ||
| [(station, "WinSta0", 1)] | ||
| ), | ||
| ) | ||
|
|
||
| results = list( | ||
| clipboard.Clipboard.list_clipboard_formats( | ||
| SimpleNamespace(), "plugins.Clipboard", "kernel" | ||
| ) | ||
| ) | ||
|
|
||
| assert results == [] | ||
|
|
||
|
|
||
| def test_clipboard_requires_64_bit_kernel(): | ||
| kernel_requirement = clipboard.Clipboard.get_requirements()[0] | ||
|
|
||
| assert kernel_requirement.requirements["layer_name"].architectures == ["Intel64"] | ||
|
|
||
|
|
||
| def test_clipboard_format_handles_non_int_fmt_with_lookup(): | ||
| """If int(self.fmt) raises, use a valid non-decimal lookup name.""" | ||
|
|
||
| class _NonIntFmt: | ||
| def __int__(self): | ||
|
AmirGhiassian marked this conversation as resolved.
|
||
| raise ValueError("non-int") | ||
|
|
||
| def lookup(self): | ||
| return "CF_LOOKUP_FORMAT" | ||
|
|
||
| get_format_name = gui.GUIExtensions.tagCLIP.get_format_name | ||
| assert get_format_name(SimpleNamespace(fmt=_NonIntFmt())) == "CF_LOOKUP_FORMAT" | ||
|
|
||
|
|
||
| def test_clipboard_format_handles_missing_lookup(): | ||
| """If the fmt object lacks lookup(), fall back to numeric-based logic.""" | ||
|
|
||
| class _NumericFmt: | ||
| def __int__(self): | ||
| return 0xC001 | ||
|
|
||
| # no lookup() method -> AttributeError when accessed | ||
|
|
||
| get_format_name = gui.GUIExtensions.tagCLIP.get_format_name | ||
| assert ( | ||
| get_format_name(SimpleNamespace(fmt=_NumericFmt())) | ||
| == "REGISTERED_FORMAT(0xc001)" | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| # 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 enumerate clipboard formats from Windows memory dumps.""" | ||
|
|
||
| import logging | ||
| from typing import Iterable, List, Tuple | ||
|
|
||
| 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 windowstations | ||
|
|
||
| vollog = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class Clipboard(interfaces.plugins.PluginInterface): | ||
| """Enumerates clipboard formats and USER handles for each Window Station. | ||
|
|
||
| Clipboard contents are not recovered because the GUI symbol tables do not | ||
| currently expose the session's ``gSharedInfo`` symbol needed to resolve | ||
| USER handles to ``tagCLIPDATA`` objects. | ||
| """ | ||
|
|
||
| _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=["Intel64"], | ||
| ), | ||
| requirements.VersionRequirement( | ||
| name="windowstations", | ||
| component=windowstations.WindowStations, | ||
| version=(1, 0, 0), | ||
| ), | ||
| ] | ||
|
|
||
| @classmethod | ||
| def list_clipboard_formats( | ||
| cls, | ||
| context: interfaces.context.ContextInterface, | ||
| config_path: str, | ||
| kernel_module_name: str, | ||
| ) -> Iterable[Tuple[int, str, str, int]]: | ||
| """Yields session, Window Station, format name, and USER handle.""" | ||
| for ( | ||
| winsta, | ||
| station_name, | ||
| session_id, | ||
| ) in windowstations.WindowStations.scan_window_stations( | ||
| context, config_path, kernel_module_name | ||
| ): | ||
| try: | ||
| clip_count = int(winsta.cNumClipFormats) | ||
| clip_base = winsta.pClipBase | ||
| except exceptions.InvalidAddressException: | ||
| vollog.debug( | ||
| "Cannot read clipboard fields for station %s", station_name | ||
| ) | ||
| continue | ||
|
|
||
| if clip_count <= 0 or not clip_base: | ||
| continue | ||
|
|
||
| try: | ||
| clip_array = clip_base.dereference() | ||
| except exceptions.InvalidAddressException: | ||
| vollog.debug("Cannot read clipboard array for station %s", station_name) | ||
| continue | ||
|
|
||
| vollog.debug( | ||
| "Station=%s Session=%s cNumClipFormats=%s pClipBase=%#x", | ||
| station_name, | ||
| session_id, | ||
| clip_count, | ||
| int(clip_base), | ||
| ) | ||
|
|
||
| for index, clip in enumerate(clip_array): | ||
| if index >= clip_count: | ||
| break | ||
| try: | ||
| fmt_name = clip.get_format_name() | ||
| handle_val = int(clip.hData) | ||
| vollog.debug( | ||
| "clip[%s]: fmt=%s hData=%#x", | ||
| index, | ||
| fmt_name, | ||
| handle_val, | ||
| ) | ||
| yield session_id, station_name, fmt_name, handle_val | ||
| except exceptions.InvalidAddressException as e: | ||
| vollog.debug("clip[%s]: %s", index, e) | ||
| continue | ||
|
|
||
| def _generator(self): | ||
| for ( | ||
| session_id, | ||
| station_name, | ||
| fmt_name, | ||
| handle_val, | ||
| ) in self.list_clipboard_formats( | ||
| self.context, self.config_path, self.config["kernel"] | ||
| ): | ||
| yield ( | ||
| 0, | ||
| ( | ||
| session_id, | ||
| station_name, | ||
| fmt_name, | ||
| format_hints.Hex(handle_val), | ||
| ), | ||
| ) | ||
|
|
||
| def run(self): | ||
| return renderers.TreeGrid( | ||
| [ | ||
| ("Session", int), | ||
| ("WindowStation", str), | ||
| ("Format", str), | ||
| ("Handle", format_hints.Hex), | ||
| ], | ||
| self._generator(), | ||
| ) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.