sabato 7 settembre 2019

16 - Monitoraggio: Registro

Per fare in modo che il proprio driver riceva notifiche quando c'è un'operazione che coinvolge il registro di sistema è necessario registrare una funzione di callback con CmRegisterCallback o CmRegisterCallbackEx ed annullare tale registrazione con CmUnRegisterCallback quando non si è più interessati a ricevere tali notifiche. Il thread che eseguirà tale funzione di callback sarà quello che sta per effettuare o ha effettuato l'operazione sul registro.

NTSTATUS CmRegisterCallback(
 PEX_CALLBACK_FUNCTION Function,
 PVOID                 Context,
 PLARGE_INTEGER        Cookie
);

NTSTATUS CmUnRegisterCallback(
 LARGE_INTEGER Cookie
);

Context è un puntatore ad un valore definito nel driver che verrà passato alla funzione di callback specificata dal primo parametro (Function).

Cookie è un puntatore ad un intero che identifica univocamente la funzione di callback da registrare passata come primo parametro (Function) e che servirà poi ad annullare la registrazione di tale funzione di callback con CmUnRegisterCallback. Per questo motivo è necessario che l'argomento passato a questo parametro sia una variabile globale.

Function è l'indirizzo della funzione di callback da registrare o di cui si vuole annullare una registrazione esistente e che deve essere compatibile con la definizione del puntatore a funzione PEX_CALLBACK_FUNCTION.

NTSTATUS(*PEX_CALLBACK_FUNCTION)(IN PVOID CallbackContext, IN PVOID Argument1, IN PVOID Argument2);

CallbackContext è il puntatore al valore passato a CmRegisterCallback nel parametro Context.

Argument1 è un valore di tipo REG_NOTIFY_CLASS (non è un puntatore come il tipo PVOID associato ad esso potrebbe far pensare) che identifica il tipo di operazione sul registro e se la funzione di callback è stata invocata prima o dopo il completamento di tale operazione. Si vedano [2] e [3] per maggiori informazioni su REG_NOTIFY_CLASS.

Argument2 è un puntatore ad una struttura che contiene informazioni sull'operazione per cui è stata invocata la funzione di callback. Il tipo di questa struttura dipende naturalmente dal valore di tipo REG_NOTIFY_CLASS contenuto nel parametro Argument1.

Se la funzione di callback viene invocata prima del completamento dell'operazione sul registro è possibile fare in modo che tale operazione non venga portata a termine. A tale scopo è sufficiente far restituire alla funzione di callback un valore che indica uno stato di errore. Al contrario, restituendo STATUS_SUCCESS l'operazione sarà completata normalmente. Il seguente codice è tratto da [1] e mostra una funzione di callback di esempio che verrà invocata poco prima della creazione o della cancellazione di una chiave o di un valore tramite regedit.exe e restituisce STATUS_ACCESS_DENIED impedendo così il completamento dell'operazione.




NTSTATUS RegistryCallback
(
 IN PVOID CallbackContext,
 IN PVOID Argument1,
 IN PVOID Argument2
)
{
 UNREFERENCED_PARAMETER(CallbackContext);
 
 long type;
 NTSTATUS CallbackStatus = STATUS_SUCCESS;
 UNICODE_STRING registryPath;
 registryPath.Length = 0;
 registryPath.MaximumLength = 2048 * sizeof(WCHAR);
 registryPath.Buffer = ExAllocatePoolWithTag(NonPagedPool, registryPath.MaximumLength, REGISTRY_POOL_TAG);
 
 if (registryPath.Buffer == NULL)
  return STATUS_SUCCESS;
 
 type = (REG_NOTIFY_CLASS)PtrToInt(Argument1);
 switch (type)
 {
 case RegNtPreCreateKeyEx:  // Un thread sta cercando di creare una chiave
 {
  // PsGetCurrentProcess ritorna un puntatore all'EPROCESS del processo collegato al thread
  // che sta eseguendo la funzione di callback. Come già detto all'inizio della lezione
  // il thread che esegue la funzione di callback è quello che sta effettuando o ha effettuato
  // un operazione sul registro.
  if (IsProcessName("regedit.exe"PsGetCurrentProcess()))
  {
   GetRegistryObjectCompleteName(&registryPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject);
 
   DbgPrint("[RegNtPreCreateKeyEx]KeyPath: %wZ", &registryPath);
   DbgPrint("[RegNtPreCreateKeyEx]KeyName: %wZ",
    ((PREG_CREATE_KEY_INFORMATION)Argument2)->CompleteName);
 
   // Restituisce un valore di stato erroneo impedendo il completamento dell'operazione sul registro.
   CallbackStatus = STATUS_ACCESS_DENIED;
  }
  break;
 }
 case RegNtPreDeleteKey:     // Un thread sta cercando di eliminare una chiave
 {
  if (IsProcessName("regedit.exe"PsGetCurrentProcess()))
  {
   GetRegistryObjectCompleteName(&registryPath, ((PREG_DELETE_KEY_INFORMATION)Argument2)->Object);
   DbgPrint("[RegNtPreDeleteKey]%wZ", &registryPath);
 
   CallbackStatus = STATUS_ACCESS_DENIED;
  }
  break;
 }
 case RegNtPreSetValueKey:    // Un thread sta cercando di creare una valore
 {
  if (IsProcessName("regedit.exe"PsGetCurrentProcess()))
  {
   GetRegistryObjectCompleteName(&registryPath, ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->Object);
 
   DbgPrint("[RegNtPreSetValueKey]KeyPath: %wZ", &registryPath);
   DbgPrint("[RegNtPreSetValueKey]ValName: %wZ", ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->ValueName);
 
   CallbackStatus = STATUS_ACCESS_DENIED;
  }
  break;
 }
 case RegNtPreDeleteValueKey// Un thread sta cercando di eliminare un valore
 {
  if (IsProcessName("regedit.exe"PsGetCurrentProcess()))
  {
   GetRegistryObjectCompleteName(&registryPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->Object);
 
   DbgPrint("[RegNtPreDeleteValueKey]KeyPath: %wZ", &registryPath);
   DbgPrint("[RegNtPreDeleteValueKey]ValName: %wZ", ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->ValueName);
 
   CallbackStatus = STATUS_ACCESS_DENIED;
  }
  break;
 }
 case RegNtPreRenameKey:     // Un thread sta cercando di rinominare una chiave
 {
  if (IsProcessName("regedit.exe"PsGetCurrentProcess()))
  {
   GetRegistryObjectCompleteName(&registryPath, ((PREG_RENAME_KEY_INFORMATION)Argument2)->Object);
 
   DbgPrint("[RegNtPreRenameKey]KeyPath: %wZ", &registryPath);
   DbgPrint("[RegNtPreRenameKey]NewName: %wZ", ((PREG_RENAME_KEY_INFORMATION)Argument2)->NewName);
 
   CallbackStatus = STATUS_ACCESS_DENIED;
  }
  break;
 }
 default:
  break;
 }
 
 if (registryPath.Buffer != NULL)
  ExFreePoolWithTag(registryPath.Buffer, REGISTRY_POOL_TAG);
 
 return CallbackStatus;
}

BOOLEAN IsProcessName(char *stringPEPROCESS eprocess)
{
 char xx[260] = { 0 };
 strcpy(xx, (PCCHAR)PsGetProcessImageFileName(eprocess));
 if (!_stricmp(xx, string))
  return TRUE;
 else
  return FALSE;
}

BOOLEAN GetRegistryObjectCompleteName(PUNICODE_STRING pRegistryPathPVOID pRegistryObject)
{
 BOOLEAN foundCompleteName = FALSE;
 
 if (!foundCompleteName)
 {
  NTSTATUS status;
  ULONG returnedLength;
  PUNICODE_STRING pObjectName = NULL;
 
  // ObQueryNameString restituisce nel secondo parametro il nome dell'oggetto passato come primo argomento.
  // Se l'oggetto in questione è un oggetto senza nome verrà restituito NULL nel secondo parametro.
  // OBJECT_NAME_INFORMATION è praticamente solo una wrapper di UNICODE_STRING.
  // Il terzo parametro indica la dimensione in byte del buffer specificato dal secondo parametro
  // che conterrà il nome del'oggetto. Se si passa zero come argomento per il terzo parametro il
  // quarto parametro riceverà la dimesione in byte necessaria a contenere il nome dell'oggetto.
  // Infine, se il terzo parametro è zero o un valore più piccolo della dimensione necessaria a contenere
  // il nome dell'oggetto ObQueryNameString restituisce STATUS_INFO_LENGTH_MISMATCH.
  status = ObQueryNameString(pRegistryObject, (POBJECT_NAME_INFORMATION)pObjectName, 0, &returnedLength);
  if (status == STATUS_INFO_LENGTH_MISMATCH)
  {
   pObjectName = ExAllocatePoolWithTag(NonPagedPool, returnedLength, REGISTRY_POOL_TAG);
 
   status = ObQueryNameString(pRegistryObject, (POBJECT_NAME_INFORMATION)pObjectName, returnedLength, &returnedLength);
   if (NT_SUCCESS(status))
   {
    RtlCopyUnicodeString(pRegistryPath, pObjectName);
    foundCompleteName = TRUE;
   }
 
   ExFreePoolWithTag(pObjectName, REGISTRY_POOL_TAG);
  }
 }
 
 return foundCompleteName;
}

Insieme all'entrypoint vengono anche dichiarate le variabili globali e le funzioni esterne necessarie. Infine, è bene notare che l'opzione /integritycheck da passare al linker è necessaria anche in questo caso.

#include <ntddk.h>
 
#define REGISTRY_POOL_TAG 'pRE'
 
// Esportata da NtosKrnl.exe e esposta da NtosKrnl.lib.
// Dichiarata in alcuni header ufficiali ma di tali header serve solo questa funzione
// quindi è più semplice dichiararla ed usarla in questo modo.
NTKERNELAPI NTSTATUS ObQueryNameString
(
 IN  PVOID Object,
 OUT POBJECT_NAME_INFORMATION ObjectNameInfo,
 IN  ULONG Length,
 OUT PULONG ReturnLength
);
 
// Esportata da NtosKrnl.exe e esposta da NtosKrnl.lib.
NTKERNELAPI UCHAR* PsGetProcessImageFileName(PEPROCESS Process);
 
// Variabili globali
LARGE_INTEGER CmHandle;
NTSTATUS CmSt;
 
VOID DriverUnload(PDRIVER_OBJECT pDriverObj)
{
 UNREFERENCED_PARAMETER(pDriverObj);
 
 // Cancella registrazione di callback usando variabile globale CmHandle che
 // identifica la callback registrata in DriverEntry.
 if (NT_SUCCESS(CmSt))
  CmUnRegisterCallback(CmHandle);
}
 
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObjPUNICODE_STRING pRegistryString)
{
 UNREFERENCED_PARAMETER(pRegistryString);
 
 pDriverObj->DriverUnload = DriverUnload;
 
 // Registra funzione di callback ed usa variabile globale CmHandle per
 // identificarla univocamente.
 CmSt = CmRegisterCallback(RegistryCallback, NULL, &CmHandle);
 if (NT_SUCCESS(CmSt))
  DbgPrint("CmRegisterCallback SUCCESS!");
 else
  DbgPrint("CmRegisterCallback Failed!");

 return STATUS_SUCCESS;
}




Codice sorgente:
MonitorRegistryDriver.zip



Riferimenti:
[1] https://github.com/andylau004/LookDrvCode/tree/master/WIN64驱动编程基础教程/代码/%5B4-1%5DMonitorCreateExitProcessThread/src
[2] https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/nf-wdm-cmregistercallback
[3] https://docs.microsoft.com/it-it/windows-hardware/drivers/ddi/content/wdm/nc-wdm-ex_callback_function

Nessun commento:

Posta un commento