Windowsでのクリップボードの仕組み

最近、Windowsでクリップボードをデバッグする機会があり、見つけた情報を共有するのがいいと判断しました。 クリップボードは、Windowsのコンポーネントであり、私たちの多くが1日に何十(何百?)を使用していますが、実際にそれについては考えていません。 このビジネスに着手する前に、私はすべてがどのように機能するかを考えたことがありませんでした。 結局のところ、想像もできないほど多くの興味深いことがあります。 最初に、アプリケーションがさまざまなタイプのデータをクリップボードに保存する方法とそこからデータを抽出する方法を説明し、次にアプリケーションがクリップボードに「フック」してその変更を追跡する方法を説明します。 どちらの場合も、デバッガーからデータにアクセスする方法を示すデバッグエントリが表示されます。



まず、 クリップボード形式について説明します 。 このような形式は、バッファリングできるデータの種類を記述するために使用されます。 ビットマップ、ANSIテキスト、Unicodeテキスト、TIFFなど、アプリケーションで使用できる定義済みの標準形式が多数あります。 Windowsでは、アプリケーションが独自の形式を設定することもできます。 たとえば、ワードプロセッサは、テキスト、書式、写真などの形式を登録できます。 もちろん、これは特定の問題につながります。データをテキストエディターからコピーしてメモ帳に貼り付けるとどうなりますか。メモ帳はこの書式設定をすべて理解せず、画像を表示しません。



解決策は、クリップボードのデータをいくつかの形式で同時に保存できるようにすることです。 クリップボードについて考えていたとき、そこに単一のオブジェクト(「私のテキスト」または「私の写真」)が格納されていると想像しましたが、実際には、私のデータはさまざまな形式でクリップボードに格納されます。 バッファから情報を取得するプログラムは、使用可能な形式で情報を受け取ります。



データはどのようにクリップボードに表示されますか? 非常に簡単で、アプリケーションはまずOpenClipboard関数を介してクリップボードの所有権を発表します。 その後、プログラムはEmptyClipboardコマンドでクリップボードをクリアし、最後にSetClipboardDataコマンドでデータをそこに配置できます。 SetClipboardDataは2つのパラメーターを受け入れます。 1つ目は、上記のクリップボード形式のいずれかの識別子です。 2番目は、この形式のデータを含むメモリ内のセグメント記述子です。 アプリケーションは、バッファリングする各フォーマットに対して、最良から最悪まで繰り返しSetClipboardDataコマンドを呼び出すことができます(データが挿入されるアプリケーションがリストから最初の適切なフォーマットを選択するため)。 開発者の作業を楽にするために、Windowsはいくつかの種類のクリップボード形式の変換を自動的に提供します。 プロセスの最後に、プログラムはCloseClipboardを呼び出します



ユーザーが[貼り付け]ボタンをクリックすると、ターゲットアプリケーションはOpenClipboardと次の関数のいずれかを呼び出して、使用可能なデータ形式を決定します: IsClipboardFormatAvailableGetPriorityClipboardFormatまたはEnumClipboardFormats 。 適切な形式が見つかった場合、データを取得するためのパラメーターとして目的の形式の識別子を使用してGetClipboardDataを呼び出します。 最後に、アプリケーションはCloseClipboardコマンドを使用してバッファーを閉じます。



デバッガーを使用して、クリップボードに書き込まれるデータを判別する方法を見てみましょう。 (私のエントリはすべてWin7 / 2008 R2システムで作成されていることに注意してください。したがって、OSの他のバージョンでは、外観が若干異なる場合があります)。 バッファーはWin32k.sysの一部であるため、カーネルデバッガーが必要になります。 win32k!InternalSetClipboardData+0xe4



を使用するのがwin32k!InternalSetClipboardData+0xe4



。 このオフセットの良いところは、RDIレジスタの後ろにあり、tagCLIPとして知られる構造のSetClipboardDataからのデータで満たされていることです。



  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 


メモ帳からSetClipboardDataを呼び出すと、次のようになります。



  kd> k
 Child-SP RetAddr呼び出しサイト
 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メモ帳!WinMain + 0x16f
 00000000`001dfdc0 00000000`77a2652dメモ帳!DisplayNonGenuineDlgWorker + 0x2da
 00000000`001dfe80 00000000`77b5c521 kernel32!BaseThreadInitThunk + 0xd
 00000000`001dfeb0 00000000`00000000ntdll!RtlUserThreadStart + 0x1d 


したがって、RDIの内容をtagCLIPとして表示し、バッファーに書き込まれる内容を確認できます。



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


Fmtはクリップボードの形式です。 0Xdは番号13で、Unicodeテキストに対応します。 ただし、 hData



の値によってdu



を開始することはできません。これは、データへの直接ポインターではなく記述子であるためです。 そのため、グローバルwin32k構造-gSharedInfoでそれを探す必要があります。



  kd>?win32k!gSharedInfo
式の評価:-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 


gSharedInfoのaheListには、記述子を持つ配列が含まれ、最後の2つのhDataバイトに記述子レコードのサイズを乗算すると、記述子レコードのアドレスが表示されます。



  kd>?0x00000000`00270235&FFFF
式の評価:565 = 00000000`00000235
 kd> ?? sizeof(win32k!_HANDLEENTRY)
符号なしint64 0x18
 kd>?  0xfffff900`c0800000 +(0x235 * 0x18)
式の評価:-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 


14のオフセットでpheadを見ると、データが得られます(このオフセットはプラットフォームによって異なる場合があります)。



  kd> du fffff900`c0de0fb0 + 0x14
 fffff900`c0de0fc4 "こんにちはNTDebuggingリーダー!" 


別のシナリオを想像してください。 ワードパッドからテキストをコピーし、SetClipboardDataコマンドが特定の回数動作して、データを異なる形式で配置しました。 Unicodeエントリは次のようになります。



 ブレークポイント0ヒット
 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はゼロです! なぜそう クリップボードにより、アプリケーションは特定の形式のSetClipboardDataパラメーターとしてゼロを渡すことができます。 これは、アプリケーションがこの形式でデータを提供できるが、必要に応じて後で提供することを意味します。 バッファーにUnicodeテキストが必要なテキストをメモ帳に貼り付ける場合、WindowsはWM_RENDERFORMATメッセージをワードパッドウィンドウに送信し、ワードパッドはデータを新しい形式で提供します。 もちろん、すべての形式のデータを提供する前にアプリケーションを閉じると、Windowsはすべての形式を必要とします。 この場合、WindowsはWM_RENDERALLFORMATSメッセージを送信して、他のアプリケーションが親アプリケーションを閉じた後にクリップボードのデータを使用できるようにします。



次に、アプリケーションがクリップボードの変更を追跡する方法を見てみましょう。 この時点で、Windowsはサードパーティのアプリケーションがシステムに接続できるようにするため、これは知っておくことが重要です。 コピーアンドペーストで不明瞭な不具合がある場合、原因はこれらのプログラムの一部の不適切な動作である可能性があります。 まず、クリップボードに接続するためのメカニズムを検討します。 次に、デバッガーを使用してこのようなフックを使用してアプリケーションを識別できるかどうかを調べます。



クリップボードの変更を監視するには、バッファーの表示、バッファー形式のリッスン、およびバッファーシーケンス番号の要求の 3つの方法があります。 最初の2つのメソッドに焦点を当てます。これらのメソッドは、バッファーの内容が更新されたときに通知を提供するためです。 3番目の方法では、アプリケーション自体がバッファが変更されたかどうかを毎回確認する必要があり、この方法はポーリングサイクルでは使用できません。



クリップボードビューアの機能は、Windows 2000バージョンで登場しました(以前のバージョンではない場合)。 操作の原理は非常に簡単です。バッファの変更の通知を受信することに関心のあるアプリケーションはSetClipboardViewerを呼び出し、そのウィンドウにハンドルを渡します。 Windowsはこのハンドルをwin32k構造に格納し、クリップボードが変更されるたびに、Windowsは登録されたウィンドウにWM_DRAWCLIPBOARDメッセージを送信します。



もちろん、いくつかのウィンドウを登録してバッファを表示できます-Windowsはこれをどのように処理しますか? アプリケーションがSetClipboardViewerを呼び出し、クリップボードを表示するために別のウィンドウが以前に登録されている場合、Windowsは前のウィンドウのハンドルを新しいウィンドウに返します。 バッファを監視する新しいウィンドウは、WM_DRAWCLIPBOARDを受信するたびにSendMessageを呼び出し、バッファを監視する人のチェーン内の次のウィンドウについてバッファに通知する必要があります。 各バッファ監視ウィンドウは、WM_CHANGECBCHAINメッセージも処理する必要があります。 このようなメッセージは、チェーン内の1つのリンクの削除を他のすべてのウィンドウに通知し、どのリンクがキュー内の次になるかを通知します。 これにより、チェーンを保存できます。



このようなアーキテクチャの明らかな問題は次のとおりです。バッファを監視するすべてのアプリケーションが正しく動作し、予期せず終了せず、全体がシステムの良き市民になることを期待しています。 アプリケーションのいずれかが不親切に動作し始めた場合、クリップボードの変更に関する通知をチェーン内の次のアプリケーションに送信しません。その結果、チェーン全体が通知なしで残ります。



このような問題に対処するため、Windows Vistaでは、クリップボード形式をリッスンするクリップボード形式のリスナーを追加しました。 Windows自体がクリップボードをリッスンするアプリケーションのリストを保持し、チェーンを保持する必要があるアプリケーションの整合性に依存しないことを除いて、クリップボードの表示とほぼ同じように機能します。



アプリケーションがバッファをリッスンする場合、 AddClipboardFormatListener関数を呼び出して、ウィンドウにハンドルを渡します。 その後、ウィンドウメッセージハンドラーはWM_CLIPBOARDUPDATEメッセージを受け取ります。 アプリケーションがシャットダウンしようとしているとき、または通知を受け取りたくないときは、 RemoveClipboardFormatListenerを呼び出します



クリップボードの視聴を登録する方法を見ました。 次に、デバッガを使用して、これらのプロセスに関係するプログラムを特定する方法を見てみましょう。 最初に、クリップボードの監視を確認するセッションのプロセスを特定する必要があります。 このセッションの任意のwin32プロセスを使用できます。WindowStationへのポインタを見つけるために必要なだけです。 この場合、以前のようにメモ帳ウィンドウを使用します。



  kd>!process 0 0 notepad.exe
 PROCESS fffff980366ecb30
     SessionId:1 Cid:0374 Peb:7fffffd8000 ParentCid:0814
     DirBase:1867e000 ObjectTable:fffff9803d28ef90 HandleCount:52。
    画像:notepad.exe 


カーネルのデバッグ中にこれを行う場合、コンテキストを対話的に変更する必要があります( .process /I<address>



使用してからg



を押し、デバッガーがブレークバックするのを待ちます)。 次に、プロセスアドレスで_EPROCESS



としてDT



を実行し、Win32Processフィールドを確認します。



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


次に、Win32Processアドレスをwin32k!TagPROCESSINFOとして見て、rpwinstaの値を見つけます。



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


これがウィンドウステーションです。 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 


spwndClipViewer、spwndClipboardListener、およびspwndClipOwnerfieldsフィールドに注目してください。 ここで、spwndClipViewerは、クリップボードを表示しているユーザーのチェーン内で最後に登録されたウィンドウです。 また、spwndClipboardListenerは、Clipboard Format Listenerで最後に登録されたバッファリッスンウィンドウです。 spwndClipOwnerウィンドウは、クリップボードにデータを配置したウィンドウです。



ウィンドウがわかっている場合は、そのプロセスが属するプロセスを見つけるための手順がいくつか残っています。 forspwndClipViewer、spwndClipboardListener、spwndClipOwnerに興味があります。 まず、dtを実行してtagWNDの値を見つけます。 このデモでは、spwndClipViewerを使用します。



  kd> dt 0xfffff900`c1a4ca70 tagWND
 win32k!tagWND
    + 0x000ヘッド:_THRDESKHEAD
    + 0x028状態:0x40020008
    + 0x028 bHasMeun:0y0
    + 0x028 bHasVerticalScrollbar:0y0
 ... 


headの値のみに関心があるため、オフセットが0の場合は、_THRDESKHEADの同じアドレスに対してdtを実行します。



  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 "???" 


次に、ptiフィールドにtagTHREADINFOとして示されているアドレスに対してdtを実行します。



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


これで、pEThreadフィールドの値にのみ興味があり、それを渡すことができます!スレッド:



  kd>!thread 0xfffff980`0ef6cb10 e
 THREAD fffff9800ef6cb10 Cid 087c.07ec Teb:000007fffffde000 Win32Thread:fffff900c4f26c20 WAIT:(WrUserRequest)UserModeNon-Alertable
     fffff9801c01efe0 SynchronizationEvent
なりすましではない
 DeviceMap fffff980278a0fc0
所有プロセスfffff98032e18b30画像:viewer02.exe
添付プロセスN / A画像:N / A
待機開始TickCount 5435847ティック:33(0:00:00:00.515)
コンテキストスイッチ数809 IdealProcessor:0 LargeStack
 UserTime 00:00:00.000
 KernelTime 00:00:00.062
 Win32開始アドレス0x000000013f203044
 Stack Init fffff880050acdb0現在のfffff880050ac6f0
基本fffff880050ad000制限fffff880050a3000コール0
優先度11 BasePriority 8 UnusualBoost 0 ForegroundBoost 2IoPriority 2 PagePriority 5
 Child-SP RetAddr呼び出しサイト
 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 


ご覧のとおり、クリップボードビューはviewer02.exeプロセスに代わって登録されました。 ブラウジングはチェーン内にあるため、チェーン内の次のプロセスを決定するのは簡単ではありません。 しかし、バッファを聞く人のためにこれを行うことができます。 ウィンドウステーションをもう一度見てください。



  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 


spwndClipboardListenerでdtを開始すると、次のリスニングプロセスを含むspwndClipboardListenerNextフィールドが表示されます。



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


tagWNDバッファーのリスナーのリストの最後のプロセスに到達すると、そのspwndClipboardListenerNextフィールドはゼロになります。



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


ウィンドウのアドレスを使用して、同じメソッドを使用してプロセスの名前を取得できます。 前述のように、tagWNDはカーネル構造であるため、OS自体がspwndClipboardListener / spwndClipboardListenerNextポインターを格納するため、ウォッチチェーンのようなバッファートラッキングの問題につながることはありません。



これで、Windowsクリップボードのレビューは終了です。 あなたにとって有益なものになったことを願っています。 クリップボードの監視について詳しく知りたいですか? これに関するMSDNの良い記事をご覧ください



All Articles