How the Clipboard Works, Part 2

Last time, we discussed how applications place data on the clipboard, and how to access that data using the debugger. Today, we'll take a look at how an application can monitor the clipboard for changes.  Understanding this is important because it is a place where Windows allows 3rd-party code to "hook" into the system.  If you experience unexpected behavior with copying and pasting, a program using these hooks may be misbehaving.  We’ll start by covering the hooking mechanisms for clipboard, and then review how to identify what applications, if any, are using these hooks in the debugger.


There are three ways to monitor the clipboard for changes - clipboard viewers, clipboard format listeners, and querying the clipboard sequence number.  We will focus on the first two as these allow an application to register for notifications whenever the clipboard is updated.  The third method simply allows an application to check and see if a change has occurred, and should not be used in a polling loop.


The Clipboard Viewer functionality has been around since Windows 2000, if not earlier.  The way it works is pretty simple - an application interested in receiving clipboard change notifications calls SetClipboardViewer and passes a handle to its window.  Windows then stores that handle in a per-session win32k global, and anytime the clipboard is changed Windows sends a WM_DRAWCLIPBOARD message to the registered window.


Of course, multiple applications are allowed to register their windows as clipboard viewers - so how does Windows handle that?  Well, if an application calls SetClipboardViewer and another clipboard viewer was already registered, Windows returns the handle value of the previous viewer's window to the new viewer.  It is then the responsibility of the new viewer to call SendMessage every time it receives a WM_DRAWCLIPBOARD to notify the next viewer in the chain. Each clipboard viewer also needs to handle the WM_CHANGECBCHAIN message, which notifies  all viewers when one of the viewers in the chain is removed, and specifies what the next viewer in the chain is.  This allows the chain to be maintained.


An obvious problem with this design is it relies on each clipboard viewer application to behave correctly, not to terminate unexpectedly, and to generally be a good citizen.  If any viewer decided not to be friendly, it could simply skip notifying the next viewer in line about an update, rendering the next viewer and all subsequent viewers impotent.


To address these problems, the Clipboard Format Listener mechanism was added in Windows Vista. This works in much the same way as the clipboard viewer functionality except in this case Windows maintains the list of listeners, instead of depending on each application to preserve a chain.


If an application wishes to become a clipboard format listener,it calls the AddClipboardFormatListener function and passes in a handle to its window. After that, its window message handler will receive WM_CLIPBOARDUPDATE messages.  When the application is ready to exit or no longer wishes to receive notifications, it can call RemoveClipboardFormatListener.


Now that we've covered the ways to register a viewer/listener, let's take a look at how to identify them using the debugger.  First, you'll need to identify a process in the session you are interested in checking for clipboard monitors.  It can be any win32 process in that session -we just need to use it to locate a pointer to the Window Station.  In this case, I'll use the Notepad window I used in part 1: 

kd> !process 0 0 notepad.exe

PROCESS fffff980366ecb30

    SessionId: 1  Cid: 0374   Peb: 7fffffd8000  ParentCid: 0814

    DirBase: 1867e000  ObjectTable: fffff9803d28ef90  HandleCount: 52.

    Image: notepad.exe


If you are doing this in a live kernel debug, you'll need to change context into the process interactively (using .process /I< address> then hit g and wait for the debugger to break back in).  Now DT the process address as an _EPROCESS and look for the Win32Process field: 

kd> dt _EPROCESS fffff980366ecb30 Win32Process


   +0x258 Win32Process : 0xfffff900`c18c0ce0 Void


Now DT the Win32Process address as a win32k!tagPROCESSINFO and identify the rpwinsta value: 

kd> dt win32k!tagPROCESSINFO 0xfffff900`c18c0ce0 rpwinsta

   +0x258 rpwinsta : 0xfffff980`0be2af60 tagWINDOWSTATION


This is our Window Station. Dump it using dt: 

kd> dt 0xfffff980`0be2af60 tagWINDOWSTATION


   +0x000 dwSessionId      : 1

   +0x008 rpwinstaNext     : (null)

   +0x010 rpdeskList       : 0xfffff980`0c5e2f20 tagDESKTOP

   +0x018 pTerm            : 0xfffff960`002f5560 tagTERMINAL

   +0x020 dwWSF_Flags      : 0

   +0x028 spklList         : 0xfffff900`c192cf80 tagKL

   +0x030 ptiClipLock      : (null)

   +0x038 ptiDrawingClipboard: (null)

   +0x040 spwndClipOpen    : (null)

   +0x048 spwndClipViewer  : 0xfffff900`c1a4ca70 tagWND

   +0x050 spwndClipOwner   : 0xfffff900`c1a3ef70 tagWND

   +0x058 pClipBase        : 0xfffff900`c5512fa0 tagCLIP

   +0x060 cNumClipFormats  : 4

   +0x064 iClipSerialNumber : 0x16

   +0x068 iClipSequenceNumber : 0xc1

   +0x070 spwndClipboardListener : 0xfffff900`c1a53440 tagWND

   +0x078 pGlobalAtomTable: 0xfffff980`0bd56c70 Void

   +0x080 luidEndSession   : _LUID

   +0x088 luidUser         : _LUID

   +0x090 psidUser         : 0xfffff900`c402afe0 Void


Note the spwndClipViewer, spwndClipboardListener, and spwndClipOwnerfields.  spwndClipViewer is the most-recently-registered window in the clipboard viewer chain.  Similarly, spwndClipboardListener is the most recent listener in our Clipboard Format Listener list.  spwndClipOwner is the window that set the content in the clipboard.


Given the window, it is just a few steps to determine the process.  This would work forspwndClipViewer, spwndClipboardListener, and spwndClipOwner.  First, dt the value as a tagWND.  We'll use the spwndClipViewer for this demonstration: 

kd> dt 0xfffff900`c1a4ca70 tagWND


   +0x000 head             : _THRDESKHEAD

   +0x028 state            : 0x40020008

   +0x028 bHasMeun         : 0y0

   +0x028 bHasVerticalScrollbar : 0y0


We only care about the head - so since it is at offset 0, dt the same address as a _THRDESKHEAD: 

kd> dt 0xfffff900`c1a4ca70 _THRDESKHEAD


   +0x000 h                : 0x00000000`000102ae Void

   +0x008 cLockObj         : 6

   +0x010 pti              : 0xfffff900`c4f26c20tagTHREADINFO

   +0x018 rpdesk           : 0xfffff980`0c5e2f20 tagDESKTOP

   +0x020 pSelf            : 0xfffff900`c1a4ca70  "???"


Now, dt the address in pti as a tagTHREADINFO: 

kd> dt 0xfffff900`c4f26c20 tagTHREADINFO


   +0x000 pEThread         : 0xfffff980`0ef6cb10 _ETHREAD

   +0x008 RefCount         : 1

   +0x010 ptlW32           : (null)

   +0x018 pgdiDcattr       : 0x00000000`000f0d00 Void


Here, we only care about the value of pEThread, which we can pass to !thread: 

kd> !thread 0xfffff980`0ef6cb10 e

THREAD fffff9800ef6cb10 Cid 087c.07ec  Teb: 000007fffffde000 Win32Thread: fffff900c4f26c20 WAIT: (WrUserRequest) UserModeNon-Alertable

    fffff9801c01efe0  SynchronizationEvent

Not impersonating

DeviceMap                 fffff980278a0fc0

Owning Process            fffff98032e18b30       Image:         viewer02.exe

Attached Process          N/A            Image:         N/A

Wait Start TickCount     5435847        Ticks: 33 (0:00:00:00.515)

Context Switch Count     809            IdealProcessor: 0                 LargeStack

UserTime                  00:00:00.000

KernelTime                00:00:00.062

Win32 Start Address 0x000000013f203044

Stack Init fffff880050acdb0 Current fffff880050ac6f0

Base fffff880050ad000 Limit fffff880050a3000 Call 0

Priority 11 BasePriority 8 UnusualBoost 0 ForegroundBoost 2IoPriority 2 PagePriority 5

Child-SP          RetAddr           Call Site

fffff880`050ac730 fffff800`01488f32 nt!KiSwapContext+0x7a

fffff880`050ac870 fffff800`0148b74f nt!KiCommitThreadWait+0x1d2

fffff880`050ac900 fffff960`000dc8e7 nt!KeWaitForSingleObject+0x19f

fffff880`050ac9a0 fffff960`000dc989 win32k!xxxRealSleepThread+0x257

fffff880`050aca40 fffff960`000dafc0 win32k!xxxSleepThread+0x59

fffff880`050aca70 fffff960`000db0c5 win32k!xxxRealInternalGetMessage+0x7dc

fffff880`050acb50 fffff960`000dcab5 win32k!xxxInternalGetMessage+0x35

fffff880`050acb90 fffff800`01482ed3 win32k!NtUserGetMessage+0x75

fffff880`050acc20 00000000`77929e6a nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ fffff880`050acc20)

00000000`002ffb18 00000000`00000000 0x77929e6a


As you can see, we have a clipboard viewer registered from process viewer02.exe.  Because of viewer's process-maintained chain architecture, it isn't easy to see the next process in the chain.  However, we can do this for clipboard listeners.  Let's look back at our window station: 

kd> dt 0xfffff980`0be2af60 tagWINDOWSTATION


   +0x000 dwSessionId      : 1

   +0x008 rpwinstaNext     : (null)

   +0x010 rpdeskList       : 0xfffff980`0c5e2f20 tagDESKTOP

   +0x018 pTerm            : 0xfffff960`002f5560 tagTERMINAL

   +0x020 dwWSF_Flags      : 0

   +0x028 spklList         : 0xfffff900`c192cf80 tagKL

   +0x030 ptiClipLock      : (null)

   +0x038 ptiDrawingClipboard : (null)

   +0x040 spwndClipOpen    : (null)

   +0x048 spwndClipViewer  : 0xfffff900`c1a4ca70tagWND

   +0x050 spwndClipOwner   : 0xfffff900`c1a3ef70tagWND

   +0x058 pClipBase        : 0xfffff900`c5512fa0 tagCLIP

   +0x060 cNumClipFormats  : 4

   +0x064 iClipSerialNumber : 0x16

   +0x068 iClipSequenceNumber : 0xc1

   +0x070 spwndClipboardListener: 0xfffff900`c1a53440 tagWND

   +0x078 pGlobalAtomTable: 0xfffff980`0bd56c70 Void

   +0x080 luidEndSession   : _LUID

   +0x088 luidUser         : _LUID

   +0x090 psidUser         : 0xfffff900`c402afe0 Void


If we dt the spwndClipboardListener, there is a field that shows the next listener named spwndClipboardListenerNext: 

kd> dt 0xfffff900`c1a53440 tagWND spwndClipboardListenerNext


   +0x118 spwndClipboardListenerNext : 0xfffff900`c1a50080 tagWND


When you reach the last clipboard format listener's tagWND, its spwndClipboardListenerNext value will be null: 

kd> dt 0xfffff900`c1a50080 tagWND spwndClipboardListenerNext


   +0x118 spwndClipboardListenerNext : (null)


Using this window address, we can go through the same steps as above to identify this listener's process name. As mentioned earlier, since tagWND is a kernel structure, the OS is maintaining these spwndClipboardListener/spwndClipboardListenerNext pointers, so they aren't susceptible to the chain problems of clipboard viewers.


That wraps up our clipboard coverage. I hope you found it informative.  Want to learn more about monitoring the clipboard?  This MSDN article is a good resource.


-Matt Burrough

Comments (1)

  1. Narasinga says:

    Both yours articles have been very helpful to me in getting a good understanding on the topic.

    I have a question, there are multiple clipboards that are shown to Users of Excel application, and you can you pick which one of the cell to paste. How does this work internally in sharing the global clipboard data ? I believe the format is the same for all these multiple clipboard contents.

    [This is an Office specific implementation, it is not a feature built into the Windows clipboard functionality.  Unfortunately as part of the Windows team we have limited exposure to Office architecture.  It appears that Office monitors the clipboard and retains a copy of the data placed on the clipboard.]

Skip to main content