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