Разработка MiniFilter драйвера

в 11:37, , рубрики: драйверы, Песочница, системное программирование, файловая система, метки: , ,

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

Решил разрабатывать MiniFilter драйвер, конфигурируемый при помощи текстового файла.

Рассмотрим, что из себя в общем виде представляет MiniFilter:

Фильтрация осуществляется через так называемый Filter Manager, который поставляется с операционной системой Windows, активируется только при загрузке мини фильтров. Filter Manager подключается напрямую к стеку файловой системы. Мини фильтры регистрируются на обработку данных по операциям ввода/вывода при помощи функционала Filter Manager, получая, таким образом, косвенный доступ к файловой системе. После регистрации и запуска мини фильтр получает набор данных по операциям ввода/вывода, которые были указаны при конфигурировании, при необходимости может вносить изменения в эти данные, таким образом влияя на работу файловой системы.


На следующей схеме в упрощенном виде показано как функционирует Filter Manager.
Разработка MiniFilter драйвера
Более подробную теоретическую информацию Вы можете получить на сайте MSDN, воспользовавшись ссылкой в конце статьи. Достаточно не плохо все разобрано.

Мы же двинемся в сторону разработки и рассмотрим некоторые базовые структуры, которые необходимо заполнить.

Общие глобальные данные.

typedef struct _MINIFILTER
{
	PDRIVER_OBJECT pDriverObject;
	PFLT_FILTER pFilter;
} MINIFILTER, *PMINIFILTER;
MINIFILTER fileManager;

В этой структуре будем хранить ссылку на объект нашего драйвера и ссылку на экземпляр фильтра. Хочу заметить, что PFLT_FILTER уникально идентифицирует мини фильтр и остается константой на все время работы драйвера. Используется при активации или остановке процесса фильтрации.

Регистрируем фильтр

CONST FLT_REGISTRATION FilterRegistration = {

    sizeof( FLT_REGISTRATION ),         //  Size
    FLT_REGISTRATION_VERSION,           //  Version
    0,                                  //  Flags

    NULL,                               //  Context
    Callbacks,                          //  Operation callbacks

    FilterUnload,                     //  FilterUnload

    FilterLoad,                    //  InstanceSetup
    NULL,            //  InstanceQueryTeardown
    NULL,            //  InstanceTeardownStart
    NULL,         //  InstanceTeardownComplete

    NULL,                 //  GenerateFileName
    NULL            //  NormalizeNameComponent
};

Тут стоит остановиться на нескольких поляx:

  1. Callbacks – ссылка на структуру, определяющую, что и при помощи каких функций мы собираемся обрабатывать.
  2. FilterUnload – функция, которая будет вызвана при отключении фильтра.
  3. FilterLoad – функция, которая будет вызвана при инициализации фильтра.

Далее рассмотрим структуру Callbacks:

const FLT_OPERATION_REGISTRATION Callbacks[] = {

    { IRP_MJ_CREATE,								
      0,											
      PreFileOperationCallback,
      PostFileOperationCallback },
    
    { IRP_MJ_OPERATION_END }
};

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

Далее привожу код функций, которые вызываются при инициализации и отключении фильтра.

NTSTATUS FilterLoad (IN PCFLT_RELATED_OBJECTS  FltObjects,
	IN FLT_INSTANCE_SETUP_FLAGS  Flags,
	IN DEVICE_TYPE  VolumeDeviceType,
	IN FLT_FILESYSTEM_TYPE  VolumeFilesystemType)
{
	if (VolumeDeviceType == FILE_DEVICE_NETWORK_FILE_SYSTEM) {
       return STATUS_FLT_DO_NOT_ATTACH;
    }

    return STATUS_SUCCESS;
}

NTSTATUS FilterUnload ( IN FLT_FILTER_UNLOAD_FLAGS Flags )
{
	return STATUS_SUCCESS;
}

Думаю, код не нуждается в дополнительных комментариях, так как все достаточно стандартно. Замечу только, что наш драйвер не будет работать для сети.

Теперь давайте рассмотрим функцию инициализации драйвера:

NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath )
{
  int i;
  NTSTATUS status;
  PCHAR ConfigInfo;
  UNICODE_STRING test;

  DbgPrint("MiniFilter: Started.");

  // Register a dispatch function
  for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) 
  {
      theDriverObject->MajorFunction[i] = OnStubDispatch;
  }

  theDriverObject->DriverUnload  = OnUnload; 

  fileManager.pDriverObject = theDriverObject;

  status = FltRegisterFilter(theDriverObject, &FilterRegistration, &fileManager.pFilter);

  if (!NT_SUCCESS(status))
  {
	   DbgPrint("MiniFilter:  Driver not started. ERROR FltRegisterFilter - %08xn", status); 
	   return status;
  }

  ConfigInfo = ReadConfigurationFile();
  if(ConfigInfo != NULL && NT_SUCCESS(ParseConfigurationFile(ConfigInfo)))
  {
		ExFreePool(ConfigInfo);
		DbgPrint("MiniFilter: Configuration finished.");
  }else
  {
	    if(ConfigInfo != NULL)ExFreePool(ConfigInfo);
	    FltUnregisterFilter( fileManager.pFilter );
	    DbgPrint("MiniFilter: Driver configuration was failed. Driver not started.");
		return STATUS_DEVICE_CONFIGURATION_ERROR;
  }

  status = FltStartFiltering( fileManager.pFilter );

  if (!NT_SUCCESS( status )) {
         FltUnregisterFilter( fileManager.pFilter );
		 FreeConfigInfo();
		 DbgPrint("MiniFilter:  Driver not started. ERROR FltStartFiltering - %08xn", status);
		 return status;
  }

   DbgPrint("MiniFilter: Filter was started and configured.");
   return STATUS_SUCCESS;
}

Регистрация мини фильтра осуществляется посредством вызова функции FltRegisterFilter, в которую мы передаем полученный на входе theDriverObject, структуру FilterRegistration, описанную ранее и ссылку на переменную, куда будет помещен созданный экземпляр фильтра fileManager.pFilter. Для запуска процесса фильтрации нужно вызвать функцию FltStartFiltering( fileManager.pFilter ).

Так же обращу внимание, что загрузка файла конфигурации и его обработка выполняется посредством следующих вызовов ConfigInfo = ReadConfigurationFile(); и ParseConfigurationFile(ConfigInfo) соответственно.

Данные из конфигурационного файла преобразуются в следующий набор структур.

typedef struct FILE_REDIRECT_RULE
{
    UNICODE_STRING From;
    UNICODE_STRING To;
	struct FILE_REDIRECT_RULE *NextRule;
}FileRedirectRule, *PFileRedirectRule;

struct PROCESS_CONFIGURATION_RULE
{
    UNICODE_STRING ProcessName;
	struct FILE_REDIRECT_RULE *Rule;
};

typedef struct CONFIGURATION_MAP
{
	struct PROCESS_CONFIGURATION_RULE ProcessRule;
	struct REDIRECT_MAP *NextItem; 
}ConfigurationMap ,*PConfigurationMap;

Головной структурой выступает CONFIGURATION_MAP, которая хранит в себе ссылку на описание процесса ProcessRule, а так же указатель на следующий элемент. В свою очередь PROCESS_CONFIGURATION_RULE хранит ссылку на имя процесса и непосредственно на структуру правил перенаправления ввода/вывода, которая так же, как и REDIRECT_MAP является связным списком.

Рассмотрим функцию выгрузки драйвера, она достаточно проста:

VOID OnUnload( IN PDRIVER_OBJECT DriverObject )
{
	FltUnregisterFilter(fileManager.pFilter);
    FreeConfigInfo();
	DbgPrint("MiniFilter: Unloaded");
} 

Здесь мы лишь удаляем регистрацию фильтра и высвобождаем все наши конфигурационные структуры.

Теперь давайте обратимся к самой интересной части, а именно к функции, которая занимается перенаправлением операций ввода/вывода. Так как у нас достаточно простой драйвер, делать это мы будем прямо в PreFileOperationCallback.

FLT_PREOP_CALLBACK_STATUS
PreFileOperationCallback (
    __inout PFLT_CALLBACK_DATA Data,
    __in PCFLT_RELATED_OBJECTS FltObjects,
    __deref_out_opt PVOID *CompletionContext
    )
{

	NTSTATUS status;
	PFILE_OBJECT FileObject;
	PFileRedirectRule redirectRuleItem;
	PFLT_FILE_NAME_INFORMATION pFileNameInformation;
	PConfigurationMap rule;
	UNICODE_STRING fullPath;
	UNICODE_STRING processName;
	PWCHAR Volume;
 
	FLT_PREOP_CALLBACK_STATUS returnStatus = FLT_PREOP_SUCCESS_NO_CALLBACK;
	
	if(FLT_IS_FS_FILTER_OPERATION(Data))
	{
		return FLT_PREOP_SUCCESS_NO_CALLBACK;
	}

Определяем основные переменные, а также проверим, не пришло ли нам уже что-то отфильтрованное, и если так, то эту операцию нужно пропустить, в противном случае мы можем получить рекурсию вызовов, что может повлечь BSOD.

if (FltObjects->FileObject != NULL && Data != NULL) {
		FileObject = Data->Iopb->TargetFileObject;
                if(FileObject != NULL && Data->Iopb->MajorFunction == IRP_MJ_CREATE)
				{

Здесь обращаемся к данным структур полученных от FilterManager. Структура PFLT_CALLBACK_DATA – хранит данные по текущей операции ввода/вывода, FilterManager руководствуется полями этой структуры при обращении к файловой системе. Соответственно, если мы хотим изменить поведение Windows при обращении к файлам или каталогам, мы должны отразить это в PFLT_CALLBACK_DATA. Более конкретно, нас интересует поле Data->Iopb->TargetFileObject, используя его мы сможем получить путь до файла в текущем разделе и позже изменить его при необходимости, изменив таким образом поведение ОС. PCFLT_RELATED_OBJECTS — содержит объекты связанные с данной операцией ввода/вывода, такие как ссылку на файл, раздел и прочее. Проверим, что нужные нам элементы структуры заполнены. Также проверим, что функция в контексте которой мы выполняемся действительно MJ_CREATE.

processName.Length = 0;
processName.MaximumLength = NTSTRSAFE_UNICODE_STRING_MAX_CCH * sizeof(WCHAR);
processName.Buffer = ExAllocatePoolWithTag(NonPagedPool, processName.MaximumLength,CURRENT_PROCESS_TAG);
RtlZeroMemory(processName.Buffer, processName.MaximumLength);
status =  GetProcessImageName(&processName);

В этом участке кода мы выделяем память для пути и имени процесса. Не представляю какого размера будет строка, так что выделяем максимально возможную строку WCHAR. Исходный код GetProcessImageName рассматривать не буду, скажу только, что она возвращает полный путь до файла в следующем виде: DeviceHarddiskVolume4Windowsnotepad.exe. т.е раздел, ну и собственно, путь до файла.

	if(NT_SUCCESS(status))
					{
						if(LoggingEnabled()== 1)
						{
							DbgPrint("MiniFilter: Process: %ws", processName.Buffer);
						}
					}
					else
					{
						return FLT_PREOP_SUCCESS_NO_CALLBACK;
					}
				    rule = FindRuleByProcessName(&processName,GetRedirectionMap());

Функция FindRuleByProcessName в случае успеха возвращает первый элемент связанного списка содержащего правила перенаправления по текущему процессу, в противном случае NULL.

ExFreePool(processName.Buffer); 

					if(rule != NULL){
						if(LoggingEnabled() == 1)
						{
							DbgPrint("MiniFilter: File name %ws", FileObject->FileName.Buffer);
						}

						redirectRuleItem = rule->ProcessRule.Rule;

Высвобождаем ненужную память и проверяем то, что мы получили какой-то объект, а не NULL. redirectRuleItem = rule->ProcessRule.Rule — обращение к первому правилу для данного процесса.


	while(redirectRuleItem)
						{
				if(RtlCompareUnicodeString(&FileObject->FileName ,&redirectRuleItem->From, FALSE) == 0)
							{
								status = FltGetFileNameInformation( Data,
												FLT_FILE_NAME_NORMALIZED |
												FLT_FILE_NAME_QUERY_ALWAYS_ALLOW_CACHE_LOOKUP,
												&pFileNameInformation );

Начинаем проход по всем правилам для данного процесса, сравниваем ссылку на текущий файл с тем, что у нас есть в конфигурации. Если совпало, пытаемся получить дополнительную информацию о файле, например, к какому разделу он принадлежит. Для этого используем функцию FltGetFileNameInformation.

if(NT_SUCCESS(status))
								{

									fullPath.Length = 0;
								        fullPath.MaximumLength = NTSTRSAFE_UNICODE_STRING_MAX_CCH
                                                                    * sizeof(WCHAR);
									fullPath.Buffer = ExAllocatePoolWithTag(NonPagedPool, 
                                                                    fullPath.MaximumLength, FULL_PATH_TAG);
									RtlZeroMemory(fullPath.Buffer, fullPath.MaximumLength);

									Volume = wcssplt(pFileNameInformation->Volume.Buffer, 
                                                                     redirectRuleItem->From.Buffer );

									RtlAppendUnicodeToString(&fullPath, Volume);  
									RtlAppendUnicodeToString(&fullPath, redirectRuleItem->To.Buffer); 

									ExFreePool(Volume);
									ExFreePool(FileObject->FileName.Buffer);

Если все ок, пытаемся выделить раздел, после чего формируем итоговую строку. Итоговый путь = Текущий раздел + Куда направить запрос ввода/вывода.

									FileObject->FileName.Length = fullPath.Length; 
									FileObject->FileName.MaximumLength = fullPath.MaximumLength; 
									FileObject->FileName.Buffer = fullPath.Buffer;

							
									Data->Iopb->TargetFileObject->RelatedFileObject = NULL;
									Data->IoStatus.Information = IO_REPARSE; 
									Data->IoStatus.Status = STATUS_REPARSE;
							
									DbgPrint("MiniFilter: Redirect done %ws", fullPath.Buffer);

									return FLT_PREOP_COMPLETE;	

Далее, конфигурируем системные структуры, так чтобы File Manager еще раз обработал этот запрос, но только теперь уже по другому пути. Для этого важно проставить следующие значения полей Data->IoStatus.Information = IO_REPARSE и Data->IoStatus.Status = STATUS_REPARSE;, а так же указать новый путь до файла FileObject->FileName.Buffer = fullPath.Buffer;. В качестве результата функции возвращаем FLT_PROP_COMPLETE.

	}
							}

							redirectRuleItem = redirectRuleItem->NextRule;
						}
					}
				}
	}

	return FLT_PREOP_SUCCESS_NO_CALLBACK;
}

Не забываем перейти к следующему элементу списка перенаправлений. FLT_PREOP_SUCCESS_NO_CALLBACK возвращаем если делать с текущей операцией Filter Manger ничего не должен.

На данный момент переопределение ввода/вывода работает только в рамках одного раздела, как только отлажу вариант с поддержкой нескольких разделов, выложу.

Устанавливать мини фильтр необходимо при помощи специально оформленного inf файла, пример, которого Вы найдете в исходниках к данной статье.

Конфигурационный файл имеет следующий вид:

#minifilter config start
{
	#logging : off

		#process : DeviceHarddiskVolume4Windowsnotepad.exe
		{
			#rule : redirect
			{
				#from : test.txt
				#to   : datatest.txt
			}

			#rule : redirect
			{
				#from : ioman.log
				#to   : IRCCL.ini
			}
		}
}

Файл должен располагаться в корне диска C, имя должно быть: minifilter.conf.

Итак мы имеем возможность перенаправления запросов файлового ввода/вывода, однако реализовать в дополнение, скажем, механизм запрета доступа к файлу достаточно просто. Необходимо выделить файл, доступ к которому нужно запретить и указать следующее значение для поля системной структуры Data->IoStatus.Status = STATUS_ACCESS_DENIED;. Не забыть вернуть FLT_PROP_COMPLETE в качестве результата функции.

Чтобы стартовать или остановить сервис я использую KMD Manager. Для анализа утечек памяти PoolTag. Что касается отладки, то можно использовать DbgView, однако для Windows Vista и выше отладочные сообщения необходимо активировать, для этого нужно создать DWORD ключ в реестре по следующему пути HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerDebug Print Filter с именем DEFAULT и значением 8.

Для запуска драйвера в 64 битной версии Windows 7 нужно будет отключить проверку подписи драйверов, для этого нужно перезагрузить компьютер, при старте системы нажать F8 и выбрать пункт Disable Driver Signature Enforcement, либо воспользоваться утилитой Driver Signature Enforcement Overrider(DSEO). Данная утилита позволит активировать тестовый режим отладки драйверов и подписать нужный драйвер фейковым сертификатом, что в конечном итоге позволит без проблем его использовать.

В не зависимости от того, включено логирование или нет, после запуска сервиса в DbgView Вы должны наблюдать нечто подобное.
Разработка MiniFilter драйвера

А так наш драйвер будет выглядеть в DeviceTree
Разработка MiniFilter драйвера

Могу добавить, что код пока еще достаточно сырой и требует доработок, однако в целом функционирует нормально. Собственно, если у Вас будет BSOD, я не виноват). Тестировал только на Windows 7 X86 и Windows 7 IA64.

Ссылка на исходники и утилиты: publish.rar

Что почитать:

  1. Документация MSDN
  2. Блог о файловых системах и фильтрах

PS. Хочу заметить, что не являюсь профессионалом в системном программировании, так что данная статья не претендует на полноту. По роду своей деятельности занимаюсь разработкой под Microsoft Dynamics CRM (.net, asp.net и прочее).

Буду рад Вашим комментариям.

Автор: LrdSpr

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js