📜 ⬆️ ⬇️

How the clipboard works in Windows

Recently, I had the opportunity to debug the clipboard in Windows, and I decided that it would be nice to share the information that I was able to find out. The clipboard is a component of Windows that many of us use dozens (hundreds?) Once a day, without really thinking about it. Before taking up this business, I never even thought about how things work. As it turned out, there are so many interesting things that you cannot even imagine. First I will describe how applications store different types of data in the clipboard and how to retrieve them from there, and then how applications can “cling” to the buffer and track changes in it. In both cases, you will see debug entries that show how to access the data from the debugger.

Let's start with a discussion of clipboard formats . Such formats are used to describe which types of data can be buffered. There are a number of predefined standard formats that an application can use, such as a bitmap, ANSI text, Unicode text and TIFF. Windows also allows the application to set its own format. For example, a word processor may register a format that includes text, formatting, and pictures. Of course, this leads to a certain problem: what happens if you copy the data from a text editor and paste it into Notepad, which does not understand all this formatting and does not display pictures?

Exit - allow simultaneous storage of data in the clipboard in multiple formats. When I used to think about the clipboard, I imagined that a single object was stored there (“my text” or “my picture”), but actually my data is stored in a buffer in different forms. A program that takes information from the buffer receives it in the format that it can use.

How does the data appear in the clipboard? Very simply, the application first declares ownership of the clipboard through the OpenClipboard function. After that, the program can clear the clipboard with the EmptyClipboard command and, finally, put its data there with the SetClipboardData command. SetClipboardData takes two parameters. The first is the identifier of one of the clipboard formats that we mentioned above. The second is an in-memory segment descriptor that contains data in this format. An application can repeatedly call the SetClipboardData command for each of the formats that it wants to put in the buffer, from best to worst (since the application where the data will be inserted will select the first suitable format from the list). To make life easier for the developer, Windows automatically converts certain types of formats to the clipboard. At the end of the process, the program calls CloseClipboard .
')
When the user clicks the Paste button, the target application calls OpenClipboard and one of the following functions to determine the available data formats: IsClipboardFormatAvailable , GetPriorityClipboardFormat or EnumClipboardFormats . If it finds a suitable format, then calls GetClipboardData with the identifier of the desired format as a parameter to get the data. At the end, the application uses the CloseClipboard command to close the buffer.

Now let's see how to use the debugger to determine which data is written to the clipboard. (Note that all my entries are made in Win7 / 2008 R2 - so on other versions of the OS they may look a little different). Since the buffer is part of Win32k.sys, you will need a kernel debugger. I like to use win32k!InternalSetClipboardData+0xe4 as a reference point. At such an offset, it is good that it is located behind the RDI register, filled with data from SetClipboardData in a structure known as tagCLIP.

  kd> u win32k! InternalSetClipboardData + 0xe4-c L5
 win32k! InternalSetClipboardData + 0xd8:
 fffff960`0011e278 894360 mov dword ptr [rbx + 60h], eax
 fffff960`0011e27b 8937 mov dword ptr [rdi], esi
 fffff960`0011e27d 4c896708 mov qword ptr [rdi + 8], r12
 fffff960`0011e281 896f10 mov dword ptr [rdi + 10h], ebp
 fffff960`0011e284 ff15667e1900 call qword ptr [win32k! _imp_PsGetCurrentProcessWin32Process (fffff960`002b60f0)] 

  kd> dt win32k! tagCLIP
    + 0x000 fmt: Uint4B
    + 0x008 hData: Ptr64 Void
    + 0x010fGlobalHandle: Int4B 

Here is the call to SetClipboardData from Notepad:

  kd> k
 Child-SP RetAddr Call Site
 fffff880`0513a940 fffff960`0011e14f win32k! InternalSetClipboardData + 0xe4
 fffff880`0513ab90 fffff960`000e9312 win32k! SetClipboardData + 0x57
 fffff880`0513abd0 fffff800`01482ed3 win32k! NtUserSetClipboardData + 0x9e
 fffff880`0513ac20 00000000`7792e30ant! KiSystemServiceCopyEnd + 0x13
 00000000`001dfad8 00000000`7792e494 USER32! ZwUserSetClipboardData + 0xa
 00000000`001dfae0 000007fe`fc5b892b USER32! SetClipboardData + 0xdf
 00000000`001dfb20 000007fe`fc5ba625 COMCTL32! Edit_Copy + 0xdf
 00000000`001dfb60 00000000`77929bd1 COMCTL32! Edit_WndProc + 0xec9
 00000000`001dfc00 00000000`779298da USER32! UserCallWinProcCheckWow + 0x1ad
 00000000`001dfcc0 00000000`ff5110bc USER32! DispatchMessageWorker + 0x3b5
 00000000`001dfd40 00000000`ff51133c notepad! WinMain + 0x16f
 00000000`001dfdc0 00000000`77a2652d notepad! DisplayNonGenuineDlgWorker + 0x2da
 00000000`001dfe80 00000000`77b5c521 kernel32! BaseThreadInitThunk + 0xd
 00000000`001dfeb0 00000000`00000000ntdll! RtlUserThreadStart + 0x1d 

So now we can view the contents of the RDI as tagCLIP and see what is written to the buffer:

  kd> dt win32k! tagCLIP @rdi
    + 0x000 fmt: 0xd
    + 0x008 hData: 0x00000000`00270235 Void
    + 0x010fGlobalHandle: 0n1 

Fmt is a clipboard format. 0Xd is the number 13, which corresponds to the text in Unicode format. However, we cannot just run du by hData because it is a descriptor, not a direct pointer to the data. So you need to look for it in the global structure win32k - gSharedInfo:

  kd>? win32k! gSharedInfo
 Evaluate expression: -7284261440224 = fffff960`002f3520
 kd> dt win32k! tagSHAREDINFO fffff960`002f3520
    + 0x000 psi: 0xfffff900`c0980a70 tagSERVERINFO
    + 0x008 aheList: 0xfffff900`c0800000 _HANDLEENTRY
    + 0x010 HeEntrySize: 0x18
    + 0x018 pDispInfo: 0xfffff900`c0981e50 tagDISPLAYINFO
    + 0x020ulSharedDelta: 0
    + 0x028 awmControl: [31] _WNDMSG
    + 0x218DefWindowMsgs: _WNDMSG
    + 0x228DefWindowSpecMsgs: _WNDMSG 

aheList in gSharedInfo contains an array with descriptors, and the last two bytes of hData, multiplied by the size of the record of the descriptor, show the address of the record of our descriptor:

  kd>? 0x00000000`00270235 & FFFF
 Evaluate expression: 565 = 00000000`00000235
 kd> ?? sizeof (win32k! _HANDLEENTRY)
 unsigned int64 0x18
 kd>?  0xfffff900`c0800000 + (0x235 * 0x18)
 Evaluate expression: -7693351766792 = fffff900`c08034f8
 kd> dt win32k! _HANDLEENTRY fffff900`c08034f8
    + 0x000 phead: 0xfffff900`c0de0fb0 _HEAD
    + 0x008 pOwner: (null)
    + 0x010 bType: 0x6 ''
    + 0x011 bFlags: 0 ''
    + 0x012 wUniq: 0x27 

If you look at phead with offset 14, then we get our data (this offset may differ on different platforms):

  kd> du fffff900`c0de0fb0 + 0x14
 fffff900`c0de0fc4 "Hi NTDebugging readers!" 

Imagine another scenario. I copied some text from Wordpad, and the SetClipboardData team worked a certain number of times to place the data in different formats. Record in Unicode format looks like this ::

  Breakpoint 0 hit
 win32k! InternalSetClipboardData + 0xe4:
 fffff960`0011e284 ff15667e1900 call qword ptr [win32k! _imp_PsGetCurrentProcessWin32Process (fffff960`002b60f0)]
 kd> dt win32k! tagCLIP @rdi
    + 0x000 fmt: 0xd
    + 0x008 hData: (null)
    + 0x010fGlobalHandle: 0n0 

hData is zero! Why is that? It turns out that the clipboard allows an application to pass null as a SetClipboardData parameter for a particular format. This means that the application is able to provide data in this format, but will do so later, if necessary. If I want to insert text into Notepad, for which there must be Unicode text in the buffer, Windows will send the WM_RENDERFORMAT message to the WordPad window, and then WordPad will provide the data in a new format. Of course, if the application is closed before it has provided data in all formats, Windows will need all the formats. In this case, Windows will send a WM_RENDERALLFORMATS message so that other applications can use the data from the clipboard after the parent application is closed.

Now let's see how the application can monitor the clipboard for changes. This is important to know, because in this place Windows allows third-party applications to connect to the system. If you have obscure glitches with copying and pasting, the reason may be the incorrect behavior of some of these programs. Let's start with the fact that we consider the mechanisms for connecting to the clipboard. Then, consider whether using the debugger to identify applications that use such hooks.

There are three ways to monitor the clipboard for changes: view the buffer, listen for buffer formats and query the buffer sequence number . We will focus on the first two methods, because they provide for receiving notifications when the contents of the buffer is updated. In the third method, the application should itself check each time whether the buffer has changed, and this method cannot be used in the polling cycle.

The functionality of the Clipboard Viewer appeared in the version of Windows 2000, if not earlier. The principle of operation is quite simple: an application that is interested in receiving notifications about changes in the buffer calls SetClipboardViewer and passes the handle to its window. Windows stores this handle in a win32k structure, and each time the clipboard changes, Windows sends a WM_DRAWCLIPBOARD message to the registered window.

Of course, several windows can register to view the buffer - how will Windows cope with this? Well, if an application calls SetClipboardViewer, and another window has previously registered to view the clipboard, then Windows returns the value of the handle of the previous window to the new window. And now the new window, which monitors the buffer, is obliged to call SendMessage each time it receives WM_DRAWCLIPBOARD, and notify the next window in the chain of those who monitor the buffer when the buffer changes. Each of the buffer-tracking windows must also process WM_CHANGECBCHAIN ​​messages. Such messages notify all other windows about the deletion of one link in the chain and report which link becomes the next in the queue. This allows you to save the chain.

The obvious problem with this architecture is as follows: it expects that every application that monitors the buffer will behave correctly, do not unexpectedly complete the work, and in general will be a good citizen in the system. If any of the applications starts to behave unfriendly, then it will not send a notification about the change of the clipboard to the next application in the chain, with the result that the entire chain will be left without notification.

To cope with such problems, in Windows Vista added a mechanism for listening to the clipboard format - Clipboard Format Listener. It works in much the same way as viewing the clipboard, except that Windows itself keeps a list of applications that listen to the buffer, and does not rely on the integrity of applications that should keep the chain.

If the application wants to listen to the buffer, it calls the AddClipboardFormatListener function and passes its window handle. After this, the window's message handler will receive WM_CLIPBOARDUPDATE messages. When an application is about to exit, or no longer wants to receive notifications, it calls RemoveClipboardFormatListener .

We looked at how to register a clipboard view / listen. Now let's see how to use the debugger to determine which programs are involved in these processes. First you need to identify the process in the session where we want to check the clipboard monitoring. This can be any win32 process in this session — we just need it to find a pointer to Window Station . In this case, I would use the Notepad window, as before:

  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 do this while debugging the kernel, you will need to interactively change the context (using .process /I<address> , then press g and wait for the debugger to break back). Now we run DT to the process address as _EPROCESS , and look at the Win32Process field:

  kd> dt _EPROCESS fffff980366ecb30 Win32Process
 nt! _EPROCESS
    + 0x258 Win32Process: 0xfffff900`c18c0ce0 Void 

Next, let's look at the Win32Process address as win32k! TagPROCESSINFO and find out the value of rpwinsta:

  kd> dt win32k! tagPROCESSINFO 0xfffff900`c18c0ce0 rpwinsta
    + 0x258 rpwinsta: 0xfffff980`0be2af60 tagWINDOWSTATION 

This is our Window Station. Merge the contents through 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 

Notice the spwndClipViewer, spwndClipboardListener, and spwndClipOwnerfields fields. Here, spwndClipViewer is the last registered window in the chain of those who view the clipboard. Also spwndClipboardListener is the latest registered buffer listening window in the Clipboard Format Listener list. The spwndClipOwner window is the window that placed the data on the clipboard.

If we know the window, then there are a few steps left to find out which process it belongs to. We are interested in forspwndClipViewer, spwndClipboardListener and spwndClipOwner. First, run dt to find out the value of tagWND. For this demonstration, we use spwndClipViewer:

  kd> dt 0xfffff900`c1a4ca70 tagWND
 win32k! tagWND
    + 0x000 head: _THRDESKHEAD
    + 0x028 state: 0x40020008
    + 0x028 bHasMeun: 0y0
    + 0x028 bHasVerticalScrollbar: 0y0
 ... 

We are only interested in the value of head - so if the offset is 0, we do dt for the same address on _THRDESKHEAD:

  kd> dt 0xfffff900`c1a4ca70 _THRDESKHEAD
 win32k! _THRDESKHEAD
    + 0x000 h: 0x00000000`000102ae Void
    + 0x008 cLockObj: 6
    + 0x010 pti: 0xfffff900`c4f26c20tagTHREADINFO
    + 0x018 rpdesk: 0xfffff980`0c5e2f20 tagDESKTOP
    + 0x020 pSelf: 0xfffff900`c1a4ca70 "???" 

Now run dt for the address specified in the pti field as tagTHREADINFO:

  kd> dt 0xfffff900`c4f26c20 tagTHREADINFO
 win32k! tagTHREADINFO
    + 0x000 pEThread: 0xfffff980`0ef6cb10 _ETHREAD
    + 0x008 RefCount: 1
    + 0x010 ptlW32: (null)
    + 0x018 pgdiDcattr: 0x00000000`000f0d00 Void 

Now we are only interested in the value of the pEThread field, 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, the clipboard view is registered on behalf of the process viewer02.exe. Since the review is on the chain, to determine the next process in the chain will not be easy. But we can do this for those who listen to the buffer. Take another look 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`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 you run dt on spwndClipboardListener, you will see the spwndClipboardListenerNext field with the following listening process:

  kd> dt 0xfffff900`c1a53440 tagWND spwndClipboardListenerNext
 win32k! tagWND
    + 0x118 spwndClipboardListenerNext: 0xfffff900`c1a50080 tagWND 

When reaching the last process in the tagWND buffer listening list, the value of its spwndClipboardListenerNext field will be zero:

  kd> dt 0xfffff900`c1a50080 tagWND spwndClipboardListenerNext
 win32k! tagWND
    + 0x118 spwndClipboardListenerNext: (null) 

Using the window address, we can get to the process name using the same method. As mentioned earlier, because the tagWND is a kernel structure, the OS itself stores the spwndClipboardListener / spwndClipboardListenerNext pointers, so that they cannot lead to buffer tracking problems such as browsing chains.

This ends our review of the Windows clipboard. I hope for you he became informative. Want to know more about clipboard monitoring? Here is a good MSDN article about it.

Source: https://habr.com/ru/post/339442/


All Articles