giovedì 5 settembre 2019

15 - Monitoraggio: Processi, Thread e Moduli

Arrivati a questo punto sarebbe interessante vedere quali strumenti il kernel offre per il monitoraggio e la notificazione. In questa lezione si vedrà come fare in modo che il proprio driver riceva notifiche quando un processo sta per essere creato o distrutto, un thread è stato creato o distrutto oppure quando un modulo (sia esso una DLL, un eseguibile o un driver) sta per essere caricato in memoria. Il codice di esempio di questa lezione è tratto da [1] e [2].




Innanzitutto è importante sapere che per usufruire di queste funzionalità da parte del sistema è necessario forzare i controlli di integrità sull'immagine del nostro driver. Anche se lavoriamo in Test Mode, e quindi con il DSE (Driver Signature Enforcement) disabilitato, si è comunque obbligati a rispettare tale restrizione durante la compilazione del nostro driver. A tale scopo si può passare l'opzione /integritycheck da riga di comando al linker attraverso le proprietà del progetto.




Per monitorare la creazione dei processi e la loro distruzione è possibile registrare la propria funzione di callback che verrà chiamata non appena si verificherà uno degli eventi appena citati (è un po' come ricevere una notifica dal sistema quando si verifica un particolare evento). Per i processi creati, il thread che eseguirà la funzione di callback è lo stesso che sta cercando di creare il processo. Per i processi distrutti, invece, il thread che eseguirà la funzione di callback è l'ultimo ad uscire prima che il processo venga distrutto. La registrazione avviene tramite la seguente funzione della Kernel API

NTSTATUS PsSetCreateProcessNotifyRoutineEx(
 PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
 BOOLEAN                           Remove
);

Remove è un valore booleano che permette di indicare se vogliamo registrare una funzione di callback (FALSE) o annullare una precedente registrazione (TRUE).

NotifyRoutine è 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 PCREATE_PROCESS_NOTIFY_ROUTINE_EX (deve essere quindi una funzione che non restituisce e che accetta 3 argomenti che possono poi essere utilizzati e analizzati per raccogliere le informazioni necessarie)

VOID(*PCREATE_PROCESS_NOTIFY_ROUTINE_EX) (
 _Inout_ PEPROCESS Process,
 _In_ HANDLE ProcessId,
 _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
 );

Process è un puntatore alla struttura EPROCESS collegata al processo che si sta creando o distruggendo.

ProcessId è l'ID del processo (PID) che si sta creando o distruggendo.

CreateInfo è un puntatore ad una struttura di tipo PS_CREATE_NOTIFY_INFO che contiene ulteriori informazioni se il processo è in creazione (NULL se è in distruzione)

typedef struct _PS_CREATE_NOTIFY_INFO {
 SIZE_T              Size;
 union {
  ULONG  Flags;
  struct {
   ULONG FileOpenNameAvailable : 1;
   ULONG Reserved : 31;
  };
 };
 HANDLE              ParentProcessId;
 CLIENT_ID           CreatingThreadId;
 struct _FILE_OBJECT  *FileObject;
 PCUNICODE_STRING    ImageFileName;
 PCUNICODE_STRING    CommandLine;
 NTSTATUS            CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
 
/*
typedef struct _CLIENT_ID
{
  PVOID UniqueProcess;
  PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
*/

FileOpenNameAvailable è un valore booleano che indica se il campo ImageFileName conterrà il nome esatto dell'eseguibile (TRUE) o solo un nome parziale (FALSE).

ParentProcessId è il PID del processo padre del processo che si sta creando.

CreatingThreadId è una struttura che contiene il PID (CreatingThreadId->UniqueProcess) del processo che si sta creando e il TID (CreatingThreadId->UniqueThread) del thread incaricato alla creazione del processo.

ImageFileName contiene il nome dell'eseguibile in formato esatto o parziale (in base al valore del campo FileOpenNameAvailable ).

CommandLine contiene la stringa con il comando usato per la creazione del processo. NULL se non è usato alcun comando.

CreationStatus è lo stato da passare all'operazione di creazione del processo. Passare il valore di uno stato di errore permette di far fallire la creazione del processo.

Di seguito è presentato il codice di una funzione di callback di esempio che, una volta registrata, verrà invocata durante la creazione o la distruzione di un processo. Inoltre, controlla se il processo in fase di creazione è Calculator.exe e, in caso affermativo, impedisce che tale operazione venga portata a termine con successo.

VOID MyCreateProcessNotifyEx
(
 __inout   PEPROCESS Process,
 __in      HANDLE ProcessId,
 __in_opt  PPS_CREATE_NOTIFY_INFO CreateInfo
)
{
 UNREFERENCED_PARAMETER(ProcessId);
 
 char xxx[16] = { 0 };
 
 if (CreateInfo != NULL)
 {
  PWCHAR wc = wcsrchr(CreateInfo->ImageFileName->Buffer, L'\\');
 
  DbgPrint("[PTM_Monitor][%ld]%s is creating Process: %ls",
   CreateInfo->ParentProcessId,
   GetProcessNameByProcessId(CreateInfo->ParentProcessId),
   ++wc);
 
  strcpy(xxx, PsGetProcessImageFileName(Process));
  if (!_stricmp(xxx, "Calculator.exe"))
  {
   DbgPrint("[PTM_Monitor]Try running Calculator.exe");
   CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL;
  }
 }
 else
 {
  DbgPrint("[PTM_Monitor]Process Exit: %s", PsGetProcessImageFileName(Process));
 }
}

PCHAR GetProcessNameByProcessId(HANDLE ProcessId)
{
 NTSTATUS st = STATUS_UNSUCCESSFUL;
 PEPROCESS ProcessObj = NULL;
 PCHAR string = NULL;
 st = PsLookupProcessByProcessId(ProcessId, &ProcessObj);
 if (NT_SUCCESS(st))
 {
  string = PsGetProcessImageFileName(ProcessObj);
  ObfDereferenceObject(ProcessObj);
 }
 return string;
}

Per monitorare la creazione e distruzione dei thread, invece, è possibile registrare la propria funzione di callback o annullare una registrazione esistente tramite le seguenti funzioni della Kernel API. Il thread che eseguirà tale funzione di callback sarà quello che ha creato o distrutto il thread.

NTSTATUS PsSetCreateThreadNotifyRoutine(
 PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);

NTSTATUS PsRemoveCreateThreadNotifyRoutine(
 PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);

NotifyRoutine è l'indirizzo della funzione di callback da registrare o di cui si vuole annullare la registrazione e che deve essere compatibile con la definizione del puntatore a funzione PCREATE_THREAD_NOTIFY_ROUTINE

typedef
VOID(*PCREATE_THREAD_NOTIFY_ROUTINE) (
 IN HANDLE  ProcessId,
 IN HANDLE  ThreadId,
 IN BOOLEAN  Create
 );

ProcessId è il PID del processo collegato al thread che si è creato o distrutto

ThreadId è il TID del thread che si è creato o distrutto.

Create è un valore booleano che è TRUE se il thread è stato creato, FALSE se è stato distrutto.

Il codice seguente mostra una funzione di callback di esempio che, una volta registrata, verrà invocata dopo che un thread è stato creato o distrutto.

VOID MyCreateThreadNotify
(
 IN HANDLE  ProcessId,
 IN HANDLE  ThreadId,
 IN BOOLEAN  Create
)
{
 if (Create)
  DbgPrint("[PTM_Monitor]Thread Created! PID=%ld;TID=%ld"ProcessIdThreadId);
 else
  DbgPrint("[PTM_Monitor]Thread Exit! PID=%ld;TID=%ld"ProcessIdThreadId);
}

Per quanto riguarda il monitoraggio del caricamento dei moduli in memoria, è possibile registrare la propria funzione di callback o annullare una registrazione esistente tramite le seguenti funzioni della Kernel API

NTSTATUS PsSetLoadImageNotifyRoutine(
 PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);

NTSTATUS PsRemoveLoadImageNotifyRoutine(
 PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);

NotifyRoutine è l'indirizzo della funzione di callback da registrare o di cui si vuole annullare la registrazione e che deve essere compatibile con la definizione del puntatore a funzione PLOAD_IMAGE_NOTIFY_ROUTINE

typedef
VOID(*PLOAD_IMAGE_NOTIFY_ROUTINE)(
 _In_opt_ PUNICODE_STRING FullImageName,
 _In_ HANDLE ProcessId,
 _In_ PIMAGE_INFO ImageInfo
 );

FullImageName contiene il nome completo del modulo. A volte il sistema non è in grado di recuperarlo e passa NULL come argomento per questo parametro.

ProcessId è il PID del processo in cui l'immagine del modulo viene mappata. Il sistema passa zero come argomento per questo parametro se il modulo è un driver.

ImageInfo è un puntatore ad una struttura di tipo IMAGE_INFO che contiene altre info utili sul modulo

typedef struct _IMAGE_INFO {
 union {
  ULONG Properties;
  struct {
   ULONG ImageAddressingMode : 8;
   ULONG SystemModeImage : 1;
   ULONG ImageMappedToAllPids : 1;
   ULONG ExtendedInfoPresent : 1;
   ULONG MachineTypeMismatch : 1;
   ULONG ImageSignatureLevel : 4;
   ULONG ImageSignatureType : 3;
   ULONG ImagePartialMap : 1;
   ULONG Reserved : 12;
  };
 };
 PVOID  ImageBase;
 ULONG  ImageSelector;
 SIZE_T ImageSize;
 ULONG  ImageSectionNumber;
} IMAGE_INFO, *PIMAGE_INFO;

Il codice seguente mostra una funzione di callback di esempio che, una volta registrata, verrà invocata poco prima che un driver sia caricato in memoria. Diversamente dalle notifiche che riguardano la creazione dei processi, però, in questo caso non esiste un modo canonico e documentato per impedire il caricamento di un modulo.

VOID LoadImageNotifyRoutine
(
 __in_opt PUNICODE_STRING  FullImageName,
 __in HANDLE  ProcessId,
 __in PIMAGE_INFO  ImageInfo
)
{
 PVOID pDrvEntry;
 
 if (FullImageName != NULL && MmIsAddressValid(FullImageName))
 {
  if (ProcessId == 0)
  {
   DbgPrint("[PTM_Monitor]Module Loaded: %wZ\n"FullImageName);
   pDrvEntry = GetDriverEntryByImageBase(ImageInfo->ImageBase);
   DbgPrint("[PTM_Monitor]Module Entrypoint: %p\n", pDrvEntry);
  }
 }
}

PVOID GetDriverEntryByImageBase(PVOID ImageBase)
{
 PIMAGE_DOS_HEADER pDOSHeader;
 PIMAGE_NT_HEADERS pNTHeader;
 PVOID pEntryPoint;
 pDOSHeader = (PIMAGE_DOS_HEADER)ImageBase;
 pNTHeader = (PIMAGE_NT_HEADERS)(PtrToUlong(ImageBase) + pDOSHeader->e_lfanew);
 pEntryPoint = (PVOID)(PtrToUlong(ImageBase) + pNTHeader->OptionalHeader.AddressOfEntryPoint);
 return pEntryPoint;
}

Infine il codice di DriverEntry, dove avviene la registrazione delle funzioni di callback, e di DriverUnload, dove avviene l'annullamento alle registrazioni corrispondenti.

VOID DriverUnload(PDRIVER_OBJECT pDriverObj)
{
 UNREFERENCED_PARAMETER(pDriverObj);
 
 // rimuove registrazione alla notificazione per creazione/distruzione di processi e thread
 PsSetCreateProcessNotifyRoutineEx(MyCreateProcessNotifyEx, TRUE);
 PsRemoveCreateThreadNotifyRoutine(MyCreateThreadNotify);
 
 // rimuove registrazione alla notificazione per caricamento di moduli
 PsRemoveLoadImageNotifyRoutine(LoadImageNotifyRoutine);
}
 
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObjPUNICODE_STRING pRegistryString)
{
 UNREFERENCED_PARAMETER(pRegistryString);
 
 NTSTATUS st = 0;
 
 pDriverObj->DriverUnload = DriverUnload;
 
 // registra funzioni di callback per la notifica di creazione/distruzione di processi e thread
 st = PsSetCreateProcessNotifyRoutineEx(MyCreateProcessNotifyEx, FALSE);
 DbgPrint("PsSetCreateProcessNotifyRoutineEx return: %x", st);
 st = PsSetCreateThreadNotifyRoutine(MyCreateThreadNotify);
 DbgPrint("PsSetCreateThreadNotifyRoutine return: %x", st);
 
 // registra funzione di callback per la notifica di caricamento dei moduli
 st = PsSetLoadImageNotifyRoutine(LoadImageNotifyRoutine);
 DbgPrint("PsSetLoadImageNotifyRoutine return: %x", st);
 
 return STATUS_SUCCESS;
}




Codice sorgente:
MonitorProcessThreadModuleDriver.zip



Riferimenti:
[1] https://github.com/andylau004/LookDrvCode/tree/master/WIN64驱动编程基础教程/代码/%5B4-1%5DMonitorCreateExitProcessThread/src
[2] https://github.com/andylau004/LookDrvCode/tree/master/WIN64驱动编程基础教程/代码/%5B4-2%5DMonitorLoadUnloadDllDriver/src

Nessun commento:

Posta un commento