How the Clipboard Works, Part 2

This is an archive post of content I wrote for the NTDebugging Blog on MSDN. Since the MSDN blogs are being retired, I’m transferring my posts here so they aren’t lost. The post has been back-dated to its original publication date.

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 – so 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 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 the viewers when one of the viewers in the chain was 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 new viewer in line about an update, rendering that 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
nt!_EPROCESS
   +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
win32k!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 spwndClipOwner fields.  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 for spwndClipViewer, spwndClipboardListener, and spwndClipOwner.  First, dt the value as a tagWND.  We’ll use the spwndClipViewer for this demonstration:

kd> dt 0xfffff900`c1a4ca70 tagWND
win32k!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
win32k!_THRDESKHEAD
   +0x000 h                : 0x00000000`000102ae Void
   +0x008 cLockObj         : 6
   +0x010 pti              : 0xfffff900`c4f26c20 tagTHREADINFO
   +0x018 rpdesk           : 0xfffff980`0c5e2f20 tagDESKTOP
   +0x020 pSelf            : 0xfffff900`c1a4ca70  "???"

Now, dt the address in pti as a tagTHREADINFO:

kd> dt 0xfffff900`c4f26c20 tagTHREADINFO
win32k!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 
THREAD fffff9800ef6cb10  Cid 087c.07ec  Teb: 000007fffffde000 Win32Thread: fffff900c4f26c20 WAIT: (WrUserRequest) UserMode Non-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 2 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
fffff880`050ac730 fffff800`01488f32 : fffff980`0ef6cb10 fffff980`0ef6cb10 fffff980`0ef6cbd0 00000000`0000000b : nt!KiSwapContext+0x7a
fffff880`050ac870 fffff800`0148b74f : 00000000`00000021 fffff800`015b1975 00000000`00000000 00000000`6d717355 : nt!KiCommitThreadWait+0x1d2
fffff880`050ac900 fffff960`000dc8e7 : fffff900`c4f26c00 fffff880`0000000d fffff900`c40a6f01 fffff960`000dcc00 : nt!KeWaitForSingleObject+0x19f
fffff880`050ac9a0 fffff960`000dc989 : 00000000`00000000 00000000`00000000 00000000`00000001 00000000`00000000 : win32k!xxxRealSleepThread+0x257
fffff880`050aca40 fffff960`000dafc0 : fffff900`c4f26c20 fffff880`050acca0 00000000`00000001 00000000`00000000 : win32k!xxxSleepThread+0x59
fffff880`050aca70 fffff960`000db0c5 : 00000000`00000000 fffff800`000025ff 00000000`00000000 fffff980`ffffffff : win32k!xxxRealInternalGetMessage+0x7dc
fffff880`050acb50 fffff960`000dcab5 : 00000000`021503bd 00000000`021503bd fffff880`050acbc8 fffff800`01482ed3 : win32k!xxxInternalGetMessage+0x35
fffff880`050acb90 fffff800`01482ed3 : fffff980`0ef6cb10 00000000`00000000 00000000`00000020 fffff980`1c394e60 : win32k!NtUserGetMessage+0x75
fffff880`050acc20 00000000`77929e6a : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ fffff880`050acc20)
00000000`002ffb18 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 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
win32k!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

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

kd> dt 0xfffff900`c1a53440 tagWND spwndClipboardListenerNext
win32k!tagWND
   +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
win32k!tagWND
   +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