Привет, Хабр!
Когда передо мной встала задача написать свой драйвер, осуществляющий мониторинг операций в реестре, я, конечно же, полезла искать на просторах интернета хоть какую-то информацию по этому поводу. Но единственное, что вылезало по запросу «Драйвер-фильтр реестра» — поток статей по написанию драйвера-фильтра (ура), НО все эти статьи касались только фильтра файловой системы (печаль).
К сожалению, единственное, что удалось найти — статью 2003 года, код из которой вы никогда не соберете в своей новенькой VS19.
К счастью же, есть прекрасный пример от Microsoft на GitHub (сразу кидаю ссылочку), на котором и будет строиться бОльшая часть этого разбора.
Возможно, суперпрограммистам хватит и ссылки на пример, чтобы за 5 минут во всем разобраться. Но есть и новички, студенты, как я, для которых, скорее всего, и будет данная статья. Надеюсь, кому-то это действительно поможет.
Окей. Погнали. Открываем примерчик. Внимание! Не пугаемся большого количества файлов, 80% нам не понадобится.
Мы видим в проекте 2 папки: exe и sys. В первой находится программа, запускающая драйвер, регистрирующая его в системе, а по завершению работы с драйвером, удаляющая его. С нее и начнем.
Открываем regctrl.c
Здесь и находится практически весь необходимый нам код программы.
Сразу идем к функции wmain. Что мы там видим? Загрузка драйвера функцией UtilLoadDriver(util.c), а затем указания по некоторым настройкам:
Да, необходимо в реестр в указанную папку занести параметр (можно через cmd, а можно ручками). Это нужно для того, чтобы мы могли видеть больше сообщений от драйвера
Кстати говоря, не забудьте скачать приложение, которое позволяет вам просматривать отладочную информацию, я пользовалась DbgView.
Далее мы видим 2 интересные функции: DoKernelModeSamples и DoUserModeSamples — они нужны для демонстрации работы драйвера. Вот первая, например, отправляет драйверу IOCL запрос функцией DeviceIoControl, драйвер в свою очередь по второму параметру IOCTL_DO_KERNELMODE_SAMPLES запустит необходимые функции.
Из описания функции DeviceIoControl мы видим, что она может передавать драйверу буфер и также принимать его. Это нам понадобится в дальнейшем. А пока в этом файле ничего интересного для нас нет.
Перейдем в папку sys, файл driver.c
Начнем с функции DriverEntry. Там драйвер выводит какую-то отладочную информацию, затем функцией IoCreateDeviceSecure создает именованный объект устройства и применяет указанные параметры безопасности, интересный же кусочек ждет нас дальше:
В скобках заключены основные коды функций для IRP. То есть это те типы пакетов, которые будут удостаиваться внимания нашего драйвера. После знака "=" указывается функция, которая будет обрабатывать поступивший пакет. Дальше опять-таки мало интересного. НО. Сюда необходимо будет добавить одну интересную функцию. Запомните это место, мы сюда еще вернемся
Итак, если с DeviceCreate, DeviceClose, DeviceCleanup и DeviceUnload все очевидно, то что же происходит в DeviceControl? А туда и прилетит запрос нашей программы, который мы отправляли функцией DeviceIoControl. Хватаем из стека запрос и изымаем (в данном примере) как раз тот второй параметр, о котором я говорила:
Основываясь на IoControlCode, драйвер отправится выполнять ту или иную функцию. Советую для понимания рассмотреть, например, файл pre.c и разобраться, что там происходит.
И закончим рассмотрение примера последним интересным моментом — конечно же, функция Callback.
Сюда и будут прилетать извещения об операциях, происходящих в реестре. Помните место, которое я просила запомнить? Оно чуть выше. Вот там бы нам оставить CmRegisterCallbackEx. Они и будет объявлять функцию Callback как «мешок», в который полетят IRP пакеты на обработку. CallbackCtx->Altitude будет определять уровень нашего драйвера (мы же не одни следим за реестром), то есть на какой высоте наш драйвер будет перехватывать пакеты и что-то с ними делать (Опять же в pre.c довольно понятно, что и как происходит: Регистрируем функцию, что-то делаем с реестром, все фиксируется, выводится информация драйвером и затем делаем обратное действие — CmUnRegisterCallback — чтобы нам больше ничего не прилетало).
Ах, да. Не паникуйте, когда в DbgView обнаружите нескончаемый поток сообщений от драйвера — в реестре постоянно какие-то тусовки.
Собственно, из аргументов функции CallBack можно извлечь всю необходимую информацию — и операцию, совершаемую над каким-то ключом (это как раз есть в коде — NotifyClass), и имя ключа
А теперь отойдем от данного примера. Рассмотрим, что можно интересного сделать.
Такая задачка: пусть у нас в каком-то файле перечислены названия программ и ключей реестра, там же мы прописываем права доступа программы к определенному ключу (ограничимся простым: имеет/не имеет доступ).
Наша программа (та, что в папке exe) будет считывать конфигурацию и отправлять драйверу с помощью IOCL запроса. То есть в функции DeviceIoControl в качестве третьего аргумента и будем передавать буфер. Передавать и оформлять конфигурацию можно, как вам удобно.
Драйвер получает эти права и сохраняет себе в какой-нибудь глобальный буфер. Входной массив можно получить таким образом:
Теперь попробуем запретить какой-нибудь программе доступ к ключу
Идем в функцию Callback.
Давайте обозначим имя нашей программы и ключа, к которой у нее нет доступа соответственно MyProg и MyKey.
Нам необходимо узнать, какая программа в данный момент попыталась обратиться к ключу и сравнить ее название с теми, которые у нас прописаны в конфигурации. Имя процесса можно получить таким образом:
Функция GetProcessImageName не библиотечная (а интернечная), ее различные вариации можно встретить на многих форумах. Оставлю ее здесь:
Мы обнаружили, что сейчас именно MyProg обращается к реестру. Теперь необходимо узнать, к какому ключу.
Из второго аргумента вынимаем информацию о ключе, к которому производится доступ
Просто возвращаем значение, указывающее на запрет. И все.
Эта статья не нацелена на то, чтобы каждый прочитавший после пошел пилить супердрайверы.
Это, так скажем, ввод в курс дела :) Так как именно его обычно очень не хватает, когда только начинаешь разбираться.
Когда передо мной встала задача написать свой драйвер, осуществляющий мониторинг операций в реестре, я, конечно же, полезла искать на просторах интернета хоть какую-то информацию по этому поводу. Но единственное, что вылезало по запросу «Драйвер-фильтр реестра» — поток статей по написанию драйвера-фильтра (ура), НО все эти статьи касались только фильтра файловой системы (печаль).
К сожалению, единственное, что удалось найти — статью 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;
}
}
}
Просто возвращаем значение, указывающее на запрет. И все.
Эта статья не нацелена на то, чтобы каждый прочитавший после пошел пилить супердрайверы.
Это, так скажем, ввод в курс дела :) Так как именно его обычно очень не хватает, когда только начинаешь разбираться.