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

в 7:50, , рубрики: C, Драйвер, драйвер-фильтр, информационная безопасность, реестр windows

Привет!

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

К сожалению, единственное, что удалось найти — статью 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 0x8nn");

Да, необходимо в реестр в указанную папку занести параметр (можно через 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: %08xn", 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 ZwQueryInformationProcessn");
			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 = %xn", 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;
				}
                        }
                }

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

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

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

Автор: itiswhatitiss

Источник

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


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