Драйвер-фильтр операций в реестре. Практика

от автора

Привет, Хабр!

Когда передо мной встала задача написать свой драйвер, осуществляющий мониторинг операций в реестре, я, конечно же, полезла искать на просторах интернета хоть какую-то информацию по этому поводу. Но единственное, что вылезало по запросу «Драйвер-фильтр реестра» — поток статей по написанию драйвера-фильтра (ура), НО все эти статьи касались только фильтра файловой системы (печаль).

К сожалению, единственное, что удалось найти — статью 2003 года, код из которой вы никогда не соберете в своей новенькой VS19.

К счастью же, есть прекрасный пример от Microsoft на GitHub (сразу кидаю ссылочку), на котором и будет строиться бОльшая часть этого разбора.

Возможно, суперпрограммистам хватит и ссылки на пример, чтобы за 5 минут во всем разобраться. Но есть и новички, студенты, как я, для которых, скорее всего, и будет данная статья. Надеюсь, кому-то это действительно поможет.

Окей. Погнали. Открываем примерчик. Внимание! Не пугаемся большого количества файлов, 80% нам не понадобится.

Мы видим в проекте 2 папки: exe и sys. В первой находится программа, запускающая драйвер, регистрирующая его в системе, а по завершению работы с драйвером, удаляющая его. С нее и начнем.

Открываем regctrl.c

Здесь и находится практически весь необходимый нам код программы.

Сразу идем к функции wmain. Что мы там видим? Загрузка драйвера функцией UtilLoadDriver(util.c), а затем указания по некоторым настройкам:

printf("\treg add \"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Debug Print Filter\" /v IHVDRIVER /t REG_DWORD /d 0x8\n\n");

Да, необходимо в реестр в указанную папку занести параметр (можно через cmd, а можно ручками). Это нужно для того, чтобы мы могли видеть больше сообщений от драйвера
Кстати говоря, не забудьте скачать приложение, которое позволяет вам просматривать отладочную информацию, я пользовалась DbgView.

Далее мы видим 2 интересные функции: DoKernelModeSamples и DoUserModeSamples — они нужны для демонстрации работы драйвера. Вот первая, например, отправляет драйверу IOCL запрос функцией DeviceIoControl, драйвер в свою очередь по второму параметру IOCTL_DO_KERNELMODE_SAMPLES запустит необходимые функции.

Из описания функции DeviceIoControl мы видим, что она может передавать драйверу буфер и также принимать его. Это нам понадобится в дальнейшем. А пока в этом файле ничего интересного для нас нет.

Перейдем в папку sys, файл driver.c

Начнем с функции DriverEntry. Там драйвер выводит какую-то отладочную информацию, затем функцией IoCreateDeviceSecure создает именованный объект устройства и применяет указанные параметры безопасности, интересный же кусочек ждет нас дальше:

DriverObject->MajorFunction[IRP_MJ_CREATE]         = DeviceCreate;     DriverObject->MajorFunction[IRP_MJ_CLOSE]          = DeviceClose;     DriverObject->MajorFunction[IRP_MJ_CLEANUP]        = DeviceCleanup;     DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceControl;     DriverObject->DriverUnload                         = DeviceUnload;

В скобках заключены основные коды функций для IRP. То есть это те типы пакетов, которые будут удостаиваться внимания нашего драйвера. После знака "=" указывается функция, которая будет обрабатывать поступивший пакет. Дальше опять-таки мало интересного. НО. Сюда необходимо будет добавить одну интересную функцию. Запомните это место, мы сюда еще вернемся
Итак, если с DeviceCreate, DeviceClose, DeviceCleanup и DeviceUnload все очевидно, то что же происходит в DeviceControl? А туда и прилетит запрос нашей программы, который мы отправляли функцией DeviceIoControl. Хватаем из стека запрос и изымаем (в данном примере) как раз тот второй параметр, о котором я говорила:

IrpStack = IoGetCurrentIrpStackLocation(Irp);     Ioctl = IrpStack->Parameters.DeviceIoControl.IoControlCode;

Основываясь на IoControlCode, драйвер отправится выполнять ту или иную функцию. Советую для понимания рассмотреть, например, файл pre.c и разобраться, что там происходит.

И закончим рассмотрение примера последним интересным моментом — конечно же, функция Callback.

Сюда и будут прилетать извещения об операциях, происходящих в реестре. Помните место, которое я просила запомнить? Оно чуть выше. Вот там бы нам оставить CmRegisterCallbackEx. Они и будет объявлять функцию Callback как «мешок», в который полетят IRP пакеты на обработку. CallbackCtx->Altitude будет определять уровень нашего драйвера (мы же не одни следим за реестром), то есть на какой высоте наш драйвер будет перехватывать пакеты и что-то с ними делать (Опять же в pre.c довольно понятно, что и как происходит: Регистрируем функцию, что-то делаем с реестром, все фиксируется, выводится информация драйвером и затем делаем обратное действие — CmUnRegisterCallback — чтобы нам больше ничего не прилетало).

Ах, да. Не паникуйте, когда в DbgView обнаружите нескончаемый поток сообщений от драйвера — в реестре постоянно какие-то тусовки.

Собственно, из аргументов функции CallBack можно извлечь всю необходимую информацию — и операцию, совершаемую над каким-то ключом (это как раз есть в коде — NotifyClass), и имя ключа

А теперь отойдем от данного примера. Рассмотрим, что можно интересного сделать.

Такая задачка: пусть у нас в каком-то файле перечислены названия программ и ключей реестра, там же мы прописываем права доступа программы к определенному ключу (ограничимся простым: имеет/не имеет доступ).

Наша программа (та, что в папке exe) будет считывать конфигурацию и отправлять драйверу с помощью IOCL запроса. То есть в функции DeviceIoControl в качестве третьего аргумента и будем передавать буфер. Передавать и оформлять конфигурацию можно, как вам удобно.

Драйвер получает эти права и сохраняет себе в какой-нибудь глобальный буфер. Входной массив можно получить таким образом:

in_buf = Irp->AssociatedIrp.SystemBuffer;

Теперь попробуем запретить какой-нибудь программе доступ к ключу
Идем в функцию Callback.

Давайте обозначим имя нашей программы и ключа, к которой у нее нет доступа соответственно MyProg и MyKey.

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

 PUNICODE_STRING processName = NULL; GetProcessImageName(PsGetCurrentProcess(), &processName);  if (wcsstr(processName->Buffer, MyProg) != NULL) { <блаблабла>}

Функция GetProcessImageName не библиотечная (а интернечная), ее различные вариации можно встретить на многих форумах. Оставлю ее здесь:

 typedef NTSTATUS(*QUERY_INFO_PROCESS) ( 	__in HANDLE ProcessHandle, 	__in PROCESSINFOCLASS ProcessInformationClass, 	__out_bcount(ProcessInformationLength) PVOID ProcessInformation, 	__in ULONG ProcessInformationLength, 	__out_opt PULONG ReturnLength 	); QUERY_INFO_PROCESS ZwQueryInformationProcess;  NTSTATUS GetProcessImageName( 	PEPROCESS eProcess, 	PUNICODE_STRING* ProcessImageName ) { 	NTSTATUS status = STATUS_UNSUCCESSFUL; 	ULONG returnedLength; 	HANDLE hProcess = NULL;  	PAGED_CODE(); // this eliminates the possibility of the IDLE Thread/Process  	if (eProcess == NULL) 	{ 		return STATUS_INVALID_PARAMETER_1; 	}  	status = ObOpenObjectByPointer(eProcess, 		0, NULL, 0, 0, KernelMode, &hProcess); 	if (!NT_SUCCESS(status)) 	{ 		DbgPrint("ObOpenObjectByPointer Failed: %08x\n", status); 		return status; 	}  	if (ZwQueryInformationProcess == NULL) 	{ 		UNICODE_STRING routineName = RTL_CONSTANT_STRING(L"ZwQueryInformationProcess");  		ZwQueryInformationProcess = 			(QUERY_INFO_PROCESS)MmGetSystemRoutineAddress(&routineName);  		if (ZwQueryInformationProcess == NULL) 		{ 			DbgPrint("Cannot resolve ZwQueryInformationProcess\n"); 			status = STATUS_UNSUCCESSFUL; 			goto cleanUp; 		} 	}  	/* Query the actual size of the process path */ 	status = ZwQueryInformationProcess(hProcess, 		ProcessImageFileName, 		NULL, // buffer 		0,    // buffer size 		&returnedLength);  	if (STATUS_INFO_LENGTH_MISMATCH != status) { 		DbgPrint("ZwQueryInformationProcess status = %x\n", status); 		goto cleanUp; 	}  	*ProcessImageName = ExAllocatePoolWithTag(NonPagedPoolNx, returnedLength, '2gat');  	if (ProcessImageName == NULL) 	{ 		status = STATUS_INSUFFICIENT_RESOURCES; 		goto cleanUp; 	} 	/* Retrieve the process path from the handle to the process */ 	status = ZwQueryInformationProcess(hProcess, 		ProcessImageFileName, 		*ProcessImageName, 		returnedLength, 		&returnedLength);  	if (!NT_SUCCESS(status)) ExFreePoolWithTag(*ProcessImageName, '2gat');  cleanUp:  	ZwClose(hProcess); 	return status; } 

Мы обнаружили, что сейчас именно MyProg обращается к реестру. Теперь необходимо узнать, к какому ключу.

Из второго аргумента вынимаем информацию о ключе, к которому производится доступ

 	REG_PRE_OPEN_KEY_INFORMATION* pRegPreCreateKey = (REG_PRE_OPEN_KEY_INFORMATION*)Argument2; 		if (pRegPreCreateKey != NULL)  		{ 			if (wcscmp(pRegPreCreateKey->CompleteName->Buffer, MyKey) == 0) 			{ 				if (){//можно 					return STATUS_SUCCESS; 				} 				else {//нельзя 					return STATUS_ACCESS_DENIED; 				}                         }                 } 

Просто возвращаем значение, указывающее на запрет. И все.

Эта статья не нацелена на то, чтобы каждый прочитавший после пошел пилить супердрайверы.

Это, так скажем, ввод в курс дела 🙂 Так как именно его обычно очень не хватает, когда только начинаешь разбираться.

ссылка на оригинал статьи https://habr.com/ru/post/485606/


Комментарии

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

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