
Перехват буфера обмена на Ассемблере
Сегодня разберем интересную тему - перехват буфера обмена. Применений этому перехвату можно найти много: менеджеры буфера обмена, хранящие последние несколько скопированных текстов; программы, выполняющие заданные действия, если в буфере обмена появилось кодовое слово; кейлоггеры, перехватывающие пароли и тексты пользователя; одно время были популярны трояны, подменяющие в буфере обмена номера электронных кошельков на свои собственные. Область применения любой технологии, как обычно, ограничивается только вашей фантазией. Но это все лирика, пора переходить к программированию.
Для того, чтобы ваше приложение узнавало об изменении содержимого буфера обмена, оно должно встроиться в цепочку обработчиков буфера обмена ("наблюдателей"). Существует два основных способа это сделать. Первый способ поддерживается операционными системами от Windows 2000 и выше, он основан на использовании функции SetClipboardViewer.
wm_init:
; Добавить наше окно в список наблюдателей
invoke SetClipboardViewer, [hwnddlg]
mov [hNextW], eax
...
wm_close:
; Убрать наше окно из списка наблюдателей
invoke ChangeClipboardChain, [hwnddlg], [hNextW]
...
При инициализации диалогового окна-перехватчика вызывается функция SetClipboardViewer, которая добавляет его в цепочку наблюдателей за буфером обмена. После этого при любом изменении буфера обмена, окну будет посылаться сообщение WM_DRAWCLIPBOARD. Но тут есть один важный момент. Сразу после того, как наше окно было встроено в цепочку наблюдателей, возвращается хэндл окна, следующего в цепочке. Когда наше приложение заканчивает работу с буфером обмена, оно обязано послать сообщение WM_DRAWCLIPBOARD следующему в цепочке окну.
; Отправить сообщение следующему окну
invoke SendMessage, [hNextW], WM_DRAWCLIPBOARD, 0, 0
А при закрытии окна-перехватчика, надо обязательно уведомить об этом систему, сообщив ей через функцию ChangeClipboardChain хэндл закрываемого окна и хэндл окна, следующего по цепочке наблюдателей. Точно так же должны поступать и остальные перехватчики. При любом изменении цепочки, всем ее членам системой отправляется сообщение
WM_CHANGECBCHAIN. Наше приложение также должно его обработать примерно следующим образом:
; Удаляется известное нам окно?
mov eax,[wparam]
cmp eax,[hNextW]
jne unknown_handle
; Запомнить новый хэндл следующего обработчика
mov eax,[lparam]
mov [hNextW],eax
Если удаляется обработчик, который мы знали в качестве следующего в цепочке, то нам надо запомнить у себя новый хэндл окна, которое теперь будет являться следующим. Если удаляемое окно нам неизвестно, то просто оповещаем об изменении цепочки следующее окно, отправив ему сообщение WM_CHANGECBCHAIN с теми же параметрами, что были получены.
unknown_handle:
; Передать сообщение следующему окну
invoke SendMessage, [hNextW], WM_CHANGECBCHAIN, [wparam], [lparam]
Этот способ универсальный, но несколько сложен в реализации. Также слабым звеном тут является предположение, что все программы-перехватчики, встроенные в цепочку наблюдения за буфером обмена, всегда работают корректно и всегда отсылают другим участникам правильные сообщения. На деле это может быть совсем не так. Операционная система, конечно, пытается корректировать такие ситуации, но это не всегда получается. Если в исходнике из приложения к статье закомментировать строчку с отправкой сообщения WM_DRAWCLIPBOARD и запустить две копии программы, то перехватывать буфер обмена сможет только одна из них.
Второй, более современный способ, доступен в операционных системах, начиная с Windows Vista. Здесь используется функция AddClipboardFormatListener.
wm_init:
; Добавить наше окно в список наблюдателей
invoke AddClipboardFormatListener,[hwnddlg]
...
wm_close:
; Убрать наше окно из списка наблюдателей
invoke RemoveClipboardFormatListener,[hwnddlg]
...
Немного похоже на первый способ, но гораздо проще в реализации. После добавления окна в список наблюдателей, при каждом изменении буфера обмена, окну будет приходить сообщение WM_CLIPBOARDUPDATE. Все просто, никаких цепочек, никого не надо ни о чем уведомлять, достаточно только убрать окно из списка наблюдателей при его закрытии при помощи функции RemoveClipboardFormatListener.
При написании статьи я попробовал одновременно запустить два приложения, одно из которых использует первый (старый) способ перехвата, а другой второй способ (новый). Как ни странно, в этом случае победил первый способ перехвата. Второму приложению не доставалось ничего до того момента, как было закрыто первое. Дальше я запустил параллельно две копии первого перехватчика, они оба отрабатывали перехват без каких-либо проблем. А вот при запуске двух копии приложения с новым перехватом, данные доставались только тому, которое было запущено позднее. Такие дела.
Осталось выяснить еще один вопрос. Как узнать, что именно содержится в буфере обмена? Например, нас интересуют только текстовые строчки, а картинки и всякие эксельные таблицы должны пролетать мимо. Для этого есть функция IsClipboardFormatAvailable.
; В буфере обмена находится текст?
invoke IsClipboardFormatAvailable,CF_TEXT
or eax,eax
jz not_text
В приложении пример двух программ, реализующих оба метода перехвата буфера обмена. Если в буфер обмена копируется текст, то он сразу же появится в окне перехватчика.