Operações de filtro de driver no registro. Prática

Olá Habr!

Quando me deparei com a tarefa de escrever meu próprio driver que monitora as operações no registro, é claro que comecei a procurar pelo menos algumas informações na Internet na Internet. Mas a única coisa que saiu a pedido do "Driver-filter of the Registry" foi um fluxo de artigos sobre como escrever um driver-filter (aplausos), MAS todos esses artigos diziam respeito apenas ao filtro do sistema de arquivos (tristeza).

Infelizmente, a única coisa encontrada foi o artigo de 2003, o código do qual você nunca coletará em seu novo VS19.

Felizmente, há um ótimo exemplo da Microsoft no GitHub (vou lançar imediatamente um link ), no qual a maior parte dessa análise será construída.

Talvez um link para um exemplo seja suficiente para os superprogramadores descobrirem tudo em 5 minutos. Mas também existem iniciantes, estudantes, como eu, para quem, provavelmente, este artigo será. Espero que isso realmente ajude alguém.

Ok Perseguido. Abrimos um exemplo. Atenção! Não temos medo de um grande número de arquivos, 80% não precisamos.

Vemos duas pastas no projeto: exe e sys. O primeiro contém um programa que inicia o driver, registra-o no sistema e, após a conclusão do trabalho com o driver, o remove. Vamos começar com ela.

Abra regctrl.c

Aqui está quase todo o código do programa que precisamos.

Vá imediatamente para a função wmain. O que vemos lá? Carregando o driver com a função UtilLoadDriver (util.c) e instruções para algumas configurações:

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

Sim, você precisa inserir o parâmetro no registro na pasta especificada (você pode usar cmd ou canetas). Isso é necessário para que possamos ver mais mensagens do driver
By the way, não se esqueça de baixar um aplicativo que permite visualizar informações de depuração, eu usei DbgView.

Além disso, vemos duas funções interessantes: DoKernelModeSamples e DoUserModeSamples - são necessárias para demonstrar a operação do driver. Aqui está o primeiro, por exemplo, envia uma solicitação ao driver IOCL com a função DeviceIoControl, o driver, por sua vez, inicia as funções necessárias usando o segundo parâmetro IOCTL_DO_KERNKERMELODE_SAMPLES.

A partir da descrição da função DeviceIoControl, vemos que ela pode passar um buffer para o driver e também aceitá-lo. Nós precisaremos disso no futuro. Enquanto isso, não há nada interessante para nós neste arquivo.

Vamos para a pasta sys, arquivo driver.c

Vamos começar com a função DriverEntry. Lá, o driver exibe algum tipo de informação de depuração e, em seguida, a função IoCreateDeviceSecure cria um objeto de dispositivo nomeado e aplica os parâmetros de segurança especificados, um pouco interessante ainda nos espera:

 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ênteses estão os principais códigos de função do IRP. Ou seja, esses são os tipos de pacotes que receberão a atenção do nosso driver. Após o sinal "=", a função que processará o pacote recebido é indicada. Então, novamente, pouco interessante. MAS. Aqui você precisará adicionar um recurso interessante. Lembre-se deste lugar, vamos voltar aqui
Portanto, se tudo é óbvio com DeviceCreate, DeviceClose, DeviceCleanup e DeviceUnload, o que acontece no DeviceControl? E a solicitação do nosso programa será executada, que enviamos com a função DeviceIoControl. Pegamos a solicitação da pilha e recuperamos (neste exemplo) apenas o segundo parâmetro sobre o qual falei:

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

Com base no IoControlCode, o driver executará uma função específica. Aconselho que você compreenda, por exemplo, o arquivo pre.c e descubra o que acontece lá.

E terminaremos a consideração do exemplo com o último ponto interessante - é claro, a função de retorno de chamada .

É aqui que as notificações de operações que ocorrem no registro serão executadas. Lembra do lugar que eu pedi para lembrar? É um pouco mais alto. Aqui deixaríamos CmRegisterCallbackEx. Eles declararão a função de retorno de chamada como um "saco" no qual os pacotes IRP voarão para processamento. CallbackCtx-> Altitude determinará o nível de nosso driver (não somos os únicos que monitoram o registro), ou seja, em que altura nosso driver interceptará pacotes e fará algo com eles (novamente, no pre.c, é bem claro o que e como isso acontece : Registramos a função, fazemos algo com o registro, tudo é corrigido, as informações são exibidas pelo driver e, em seguida, fazemos a ação oposta - CmUnRegisterCallback - para que nada mais chegue até nós).

Ah sim. Não entre em pânico quando no DbgView você encontra um fluxo interminável de mensagens do driver - sempre há alguns hangouts no registro.

Na verdade, a partir dos argumentos da função CallBack, você pode extrair todas as informações necessárias - tanto a operação executada em alguma chave (isso é apenas no código - NotifyClass) quanto o nome da chave

Agora vamos nos afastar deste exemplo. Considere o que pode ser feito de maneira interessante.

Tal tarefa: vamos ter os nomes dos programas e chaves do registro em um arquivo, também especificamos os direitos de acesso ao programa para uma determinada chave (nos restringimos ao simples: ele tem / não tem acesso).

Nosso programa (aquele na pasta exe) lerá a configuração e a enviará ao driver usando uma solicitação IOCL. Ou seja, na função DeviceIoControl como terceiro argumento, passaremos o buffer. Você pode transferir e organizar a configuração como desejar.

O driver obtém esses direitos e se salva em algum buffer global. A matriz de entrada pode ser obtida desta maneira:

 in_buf = Irp->AssociatedIrp.SystemBuffer; 

Agora tente negar algum acesso ao programa à chave
Vá para a função de retorno de chamada.

Vamos denotar o nome do nosso programa e a chave à qual ele não tem acesso, respectivamente MyProg e MyKey.

Precisamos descobrir qual programa está atualmente tentando acessar a chave e comparar seu nome com os registrados em nossa configuração. O nome do processo pode ser obtido desta maneira:

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

A função GetProcessImageName não é uma biblioteca (mas a Internet); suas várias variações podem ser encontradas em muitos fóruns. Vou deixá-la aqui:

 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; } 

Descobrimos que agora o MyProg está acessando o registro. Agora você precisa descobrir qual chave.

A partir do segundo argumento, extraímos informações sobre a chave que está sendo acessada

 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; } } } 

Basta retornar um valor indicando uma proibição. E isso é tudo.

Este artigo não tem como objetivo garantir que todos que leem depois assistam a super drivers.

Isto, por assim dizer, é uma introdução ao curso das coisas :) Como geralmente não é suficiente quando você começa a entender.

Source: https://habr.com/ru/post/pt485606/


All Articles