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

от автора

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

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

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

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


На следующей схеме в упрощенном виде показано как функционирует Filter Manager.

Более подробную теоретическую информацию Вы можете получить на сайте 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 - %08x\n", 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 - %08x\n", 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 рассматривать не буду, скажу только, что она возвращает полный путь до файла в следующем виде: \Device\HarddiskVolume4\Windows\notepad.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 : \Device\HarddiskVolume4\Windows\notepad.exe 		{ 			#rule : redirect 			{ 				#from : \test.txt 				#to   : \data\test.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_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter с именем DEFAULT и значением 8.

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

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

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

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

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

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

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

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/176739/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *