Hola Habr!
Cuando me enfrenté a la tarea de escribir mi propio controlador, que monitorea las operaciones en el registro, por supuesto, me puse a buscar en Internet al menos alguna información sobre esto. Pero lo único que surgió a pedido del "Filtro de controlador del registro" fue una corriente de artículos sobre cómo escribir un filtro de controlador (aplausos), PERO todos estos artículos solo se referían al filtro del
sistema de archivos (tristeza).
Desafortunadamente, lo único que se encontró fue el artículo de 2003, el código del que nunca recopilará en su nuevo VS19.
Afortunadamente, hay un gran ejemplo de Microsoft en GitHub (arrojaré
inmediatamente un enlace ), sobre el cual se desarrollará la mayor parte de este análisis.
Quizás un enlace a un ejemplo sea suficiente para que los superprogramadores resuelvan todo en 5 minutos. Pero también hay principiantes, estudiantes, como yo, para quienes, muy probablemente, será este artículo. Espero que esto realmente ayude a alguien.
Esta bien Perseguido. Abrimos un ejemplo. Atencion No tenemos miedo de una gran cantidad de archivos, el 80% no lo necesitamos.
Vemos 2 carpetas en el proyecto: exe y sys. El primero contiene un programa que inicia el controlador, lo registra en el sistema y, al finalizar el trabajo con el controlador, lo elimina. Comenzaremos con ella.
Regctrl.c abierto
Aquí está casi todo el código del programa que necesitamos.
Vaya inmediatamente a la función wmain. ¿Qué vemos allí? Carga del controlador con la función UtilLoadDriver (util.c), y luego instrucciones para algunas configuraciones:
printf("\treg add \"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Debug Print Filter\" /v IHVDRIVER /t REG_DWORD /d 0x8\n\n");
Sí, debe ingresar el parámetro en el registro en la carpeta especificada (puede usar cmd o bolígrafos). Esto es necesario para que podamos ver más mensajes del conductor
Por cierto, no olvides descargar una aplicación que te permite ver información de depuración, utilicé DbgView.Además, vemos 2 funciones interesantes: DoKernelModeSamples y DoUserModeSamples: son necesarias para demostrar el funcionamiento del controlador. Aquí está el primero, por ejemplo, envía una solicitud al controlador IOCL con la función DeviceIoControl, el controlador, a su vez, iniciará las funciones necesarias utilizando el segundo parámetro IOCTL_DO_KERNELMODE_SAMPLES.
A partir de la descripción de la función DeviceIoControl, vemos que puede pasar un búfer al controlador y también aceptarlo. Necesitaremos esto en el futuro. Mientras tanto, no hay nada interesante para nosotros en este archivo.
Vayamos a la carpeta sys, archivo
driver.cComencemos con la función DriverEntry. Allí, el controlador muestra algún tipo de información de depuración, luego la función IoCreateDeviceSecure crea un objeto de dispositivo con nombre y aplica los parámetros de seguridad especificados, un bit interesante nos espera aún más:
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;
Entre paréntesis se encuentran los códigos de función principales para el IRP. Es decir, estos son los tipos de paquetes que recibirán la atención de nuestro controlador. Después del signo "=", se indica la función que procesará el paquete entrante. Por otra parte, poco interesante. PERO Aquí deberá agregar una característica interesante.
Recuerda este lugar, volveremos aquíEntonces, si todo es obvio con DeviceCreate, DeviceClose, DeviceCleanup y DeviceUnload, ¿qué sucede en DeviceControl? Y allí volará la solicitud de nuestro programa, que enviamos con la función DeviceIoControl. Tomamos la solicitud de la pila y recuperamos (en este ejemplo) solo el segundo parámetro del que hablé:
IrpStack = IoGetCurrentIrpStackLocation(Irp); Ioctl = IrpStack->Parameters.DeviceIoControl.IoControlCode;
Basado en IoControlCode, el controlador irá a realizar una función particular. Le aconsejo que comprenda, por ejemplo, considere el archivo pre.c y descubra qué sucede allí.
Y terminaremos la consideración del ejemplo con el último punto interesante, por supuesto, la función de
devolución de llamada .
Aquí es donde volarán las notificaciones de operaciones que ocurren en el registro. ¿Recuerdas el lugar que pedí recordar? Es un poco más alto. Aquí dejaríamos CmRegisterCallbackEx. Declararán la función de devolución de llamada como una "bolsa" en la que volarán los paquetes IRP para su procesamiento. CallbackCtx-> Altitude determinará el nivel de nuestro controlador (no somos los únicos que monitoreamos el registro), es decir, a qué altura nuestro controlador interceptará los paquetes y hará algo con ellos (Nuevamente, en pre.c está bastante claro qué y cómo sucede : Registramos la función, hacemos algo con el registro, todo está arreglado, el controlador muestra la información y luego hacemos la acción opuesta: CmUnRegisterCallback, para que nada más nos llegue).
Oh si No entre en pánico cuando en DbgView encuentre un flujo interminable de mensajes del controlador; siempre hay algunos lugares de reunión en el registro.
En realidad, a partir de los argumentos de la función CallBack, puede extraer toda la información necesaria, tanto la operación realizada en alguna tecla (esto es solo el código - NotifyClass) como el nombre de la clave
Ahora alejémonos de este ejemplo. Considere lo que se puede hacer de manera interesante.
Tal tarea: tengamos los nombres de los programas y las claves de registro en un archivo, también especificamos los derechos de acceso al programa para una determinada clave (nos restringimos a simple: tiene / no tiene acceso).
Nuestro programa (el que está en la carpeta exe) leerá la configuración y la enviará al controlador mediante una solicitud IOCL. Es decir, en la función DeviceIoControl como tercer argumento, pasaremos el búfer. Puede transferir y organizar la configuración como lo desee.
El controlador obtiene estos derechos y se guarda en un búfer global. La matriz de entrada se puede obtener de esta manera:
in_buf = Irp->AssociatedIrp.SystemBuffer;
Ahora intente denegar algún acceso de programa a la clave
Vaya a la función de devolución de llamada.
Denotemos el nombre de nuestro programa y la clave a la que no tiene acceso, respectivamente MyProg y MyKey.Necesitamos averiguar qué programa está intentando acceder actualmente a la clave y comparar su nombre con los que están registrados en nuestra configuración. El nombre del proceso se puede obtener de esta manera:
PUNICODE_STRING processName = NULL; GetProcessImageName(PsGetCurrentProcess(), &processName); if (wcsstr(processName->Buffer, MyProg) != NULL) { <>}
La función GetProcessImageName no es una biblioteca (sino Internet), sus diversas variaciones se pueden encontrar en muchos foros. La dejaré aquí:
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; }
Descubrimos que ahora MyProg está accediendo al registro. Ahora necesita saber qué clave.
Del segundo argumento, extraemos información sobre la clave a la que se accede
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; } } }
Simplemente devuelva un valor que indique una prohibición. Y eso es todo.
Este artículo no tiene la intención de garantizar que todos los que leen después vean súper controladores.Esto, por así decirlo, es una introducción al curso de las cosas :) Como suele ser, en realidad no es suficiente cuando empiezas a entender.