Довелось мне как-то на работе столкнуться с задачей управления доступа и перенаправления запросов к файловой системе в рамках определенных процессов. Реализовать необходимо было простое, легко конфигурируемое решение.
Решил разрабатывать 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:
- Callbacks – ссылка на структуру, определяющую, что и при помощи каких функций мы собираемся обрабатывать.
- FilterUnload – функция, которая будет вызвана при отключении фильтра.
- 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 Вы должны наблюдать нечто подобное.
А так наш драйвер будет выглядеть в DeviceTree
Могу добавить, что код пока еще достаточно сырой и требует доработок, однако в целом функционирует нормально. Собственно, если у Вас будет BSOD, я не виноват). Тестировал только на Windows 7 X86 и Windows 7 IA64.
Ссылка на исходники и утилиты: publish.rar
Что почитать:
PS. Хочу заметить, что не являюсь профессионалом в системном программировании, так что данная статья не претендует на полноту. По роду своей деятельности занимаюсь разработкой под Microsoft Dynamics CRM (.net, asp.net и прочее).
Буду рад Вашим комментариям.
Автор: LrdSpr