giovedì 12 settembre 2019

17 - Monitoraggio: Handle (Processi e Thread)

In una lezione precedente si è visto come fosse possibile monitorare eventi come la creazione o distruzione di thread e processi ma queste non sono le uniche operazioni per cui è possibile essere notificati. Quando si parla di thread e processi è possibile monitorare anche le operazioni sugli handle a tali oggetti: in particolare, si possono ricevere notifiche quando un handle, ad un thread o processo, viene creato, aperto o duplicato. Per fare in modo che il nostro driver riceva tali notifiche è necessario registrare una o più funzioni di callback tramite il metodo ObRegisterCallbacks ed annullare poi tale registrazione con ObUnRegisterCallbacks quando non si è più interessati a ricevere tali notifiche. Il fatto che si possano registrare una o più funzioni di callback può risultare alquanto bizzarro all'inizio ma il fatto è che ObRegisterCallbacks permette di registrare in un colpo solo funzioni di callback per operazioni diverse (creazione, apertura o copia di handle), per oggetti diversi (thread e processi) e, per ogni combinazione possibile di queste ultime due, perfino indicare due funzioni di callback che vengono invocate una prima e l'altra dopo che l'operazione sull'handle venga portata a termine. Il fatto che tutto ciò possa sembrare complicato non deve destare preoccupazione in quanto spesso si usa ObRegisterCallbacks per registrare una sola callback per una sola operazione e per un solo tipo di oggetto, che riduce il caso a quello visto nelle lezioni precedenti. Il thread che eseguirà le funzione di callback è (contrariamente a quello che si può intuire leggendo la documentazione ufficiale) quello che da il via all'operazione di creazione, apertura o copia dell'handle.

NTSTATUS ObRegisterCallbacks(
 POB_CALLBACK_REGISTRATION CallbackRegistration,
 PVOID                     *RegistrationHandle
);

void ObUnRegisterCallbacks(
 PVOID RegistrationHandle
);

RegistrationHandle è un puntatore ad una variabile che riceverà un valore che identifica l'insieme delle funzioni di callback registrate e che verrà usato per rimuovere la registrazione tramite ObUnRegisterCallbacks. A tal fine, è necessario che tale variabile sia globale.

CallbackRegistration è puntatore ad un'istanza di tipo OB_CALLBACK_REGISTRATION, una struttura incaricata di conservare i parametri della registrazione.

typedef struct _OB_CALLBACK_REGISTRATION {
 USHORT                    Version;
 USHORT                    OperationRegistrationCount;
 UNICODE_STRING            Altitude;
 PVOID                     RegistrationContext;
 OB_OPERATION_REGISTRATION *OperationRegistration;
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;

Version è un valore numerico costante che può essere inizializzato con ObGetFilterVersion() o direttamente tramite il valore OB_FLT_REGISTRATION_VERSION.

OperationRegistrationCount è un valore numerico che indica il numero di elementi di tipo OB_OPERATION_REGISTRATION che compongono l'array passato al membro OperationRegistration (vedi sotto).

Altitude è un valore numerico a virgola mobile passato come stringa di testo (per avere precisione infinita) e che indica la priorità di invocazione delle funzioni di callback nel caso in cui ci siano più driver interessati alla stessa notifica e che abbiano registrato una funzione di callback a questo scopo. Ad esempio, se due driver hanno registrato una funzione di callback che viene invocata quando viene creato un handle ad un processo quale delle due viene invocata per prima? La risposta è quella che ha il valore Altitude più alto.

RegistrationContext è un puntatore ad un valore definito dal driver che verrà passato alle funzioni di callback.

OperationRegistration è l'indirizzo di un array composto da una o più istanze di tipo OB_OPERATION_REGISTRATION, una struttura che contiene i puntatori alle due funzioni di callback che è possibile registrare per combinazione possibile tra di tipo di operazione e tipo di oggetto (anche queste ultime due informazioni sono conservate in questa struttura).

typedef struct _OB_OPERATION_REGISTRATION {
 POBJECT_TYPE                *ObjectType;
 OB_OPERATION                Operations;
 POB_PRE_OPERATION_CALLBACK  PreOperation;
 POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;

ObjectType è un puntatore a puntatore che identifica tipo di oggetto per cui si è interessati a ricevere notifiche sugli handle: quindi thread o processi. Questi puntatori a puntatori sono in realtà variabili globali definite ed esportate in wdm.h e si possono utilizzare per fare riferimento, rispettivamente, al tipo che identifica i thread (PsThreadType) ed al tipo che identifica i processi (PsProcessType).

Operations è un valore (o una combinazione di valori) che identifica i tipi di operazioni per cui si vogliono ricevere notifiche. OB_OPERATION_HANDLE_CREATE indica la creazione o l'apertura di un handle tramite, ad esempio, funzioni usermode tipo CreateProcess, OpenProcess, CreateThread e Openthread. OB_OPERATION_HANDLE_DUPLICATE invece indica la copia di un handle tramite, ad esempio, la funzione usermode DuplicateHandle.

PreOperation e PostOperation sono gli indirizzi delle funzioni di callback che vengono invocate, rispettivamente, prima e dopo che il completamento dell'operazione. Tali funzioni di callback devono essere compatibili con le definizioni dei puntatori a funzione corrispondenti (quindi PreOperation devevessere compatibile con POB_PRE_OPERATION_CALLBACK e PostOperation deve essere compatibile con POB_POST_OPERATION_CALLBACK).

Si comincia con POB_PRE_OPERATION_CALLBACK e man mano si esamina tutto ciò che è collegato advesso. A quel punto l'analisi di POB_POST_OPERATION_CALLBACK diventa banale.

typedef OB_PREOP_CALLBACK_STATUS
(*POB_PRE_OPERATION_CALLBACK) (
 _In_ PVOID RegistrationContext,
 _Inout_ POB_PRE_OPERATION_INFORMATION OperationInformation
 );

RegistrationContext è un puntatore al valore passato alla funzione di callback corrente attraverso il membro RegistrationContext della struttura OB_CALLBACK_REGISTRATION vista in precedenza.

OperationInformation è un puntatore ad una istanza di tipo OB_PRE_OPERATION_INFORMATION che contiene molte informazioni utili da usare durante l'invocazione della callback.

typedef struct _OB_PRE_OPERATION_INFORMATION {
 OB_OPERATION                 Operation;
 union {
  ULONG Flags;
  struct {
   ULONG KernelHandle : 1;
   ULONG Reserved : 31;
  };
 };
 PVOID                        Object;
 POBJECT_TYPE                 ObjectType;
 PVOID                        CallContext;
 POB_PRE_OPERATION_PARAMETERS Parameters;
} OB_PRE_OPERATION_INFORMATION, *POB_PRE_OPERATION_INFORMATION;

Operation è un valore che indica il tipo di operazione per cui la funzione di callback è stata invocata (OB_OPERATION_HANDLE_CREATE o OB_OPERATION_HANDLE_DUPLICATE).

KernelHandle è un valore booleano che indica se l'handle è del kernel. Gli handle del kernel possono essere creati e usati solo in kernel mode. Si può eventualmente usare questo valore per filtrare e ignorare le operazioni provenienti dal kernel e da altri driver.

Object è un puntatore all'oggetto a cui fa riferimento l'handle su cui è (o è stata) richiesta una operazione di creazione, apertura o copia. Per i processi questo è un puntatore ad un EPROCESS mentre per i thread è un puntatore ad un ETHREAD.

ObjectType è un puntatore al tipo dell'oggetto indicato precedentemente attraverso il membro Object appena visto. Come detto in precedenza i puntatori a tali tipi di oggetti sono disponibili come variabili globali definite in wdm.h. Si noti che, in questo caso, il parametro è un puntatore al tipo di oggetto e non un puntatore a puntatore quindi è necessario deferenziare per riferirsi a thread (*PsThreadType) e processi (*PsProcessType).

CallContext è un puntatore ad un valore definito dal driver che verrà passato alla funzione di callback indicata dal membro PostOperation OB_OPERATION_REGISTRATION (se tale membro è stato effettivamente inizializzato con un valore diverso da NULL).

Parameters è un puntatore ad una istanza di tipo OB_PRE_OPERATION_PARAMETERS che contiene informazioni da utilizzare in base al valore passato al membro Object visto sopra.

typedef union _OB_PRE_OPERATION_PARAMETERS {
 OB_PRE_CREATE_HANDLE_INFORMATION    CreateHandleInformation;
 OB_PRE_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation;
} OB_PRE_OPERATION_PARAMETERS, *POB_PRE_OPERATION_PARAMETERS;

CreateHandleInformation è una istanza di tipo OB_PRE_CREATE_HANDLE_INFORMATION che contiene informazioni specifiche per handle creati e aperti.

DuplicateHandleInformation è una istanza di tipo OB_PRE_DUPLICATE_HANDLE_INFORMATION che contiene informazioni specifiche per handle copiati.


typedef struct _OB_PRE_DUPLICATE_HANDLE_INFORMATION {
 ACCESS_MASK DesiredAccess;
 ACCESS_MASK OriginalDesiredAccess;
 PVOID       SourceProcess;
 PVOID       TargetProcess;
} OB_PRE_DUPLICATE_HANDLE_INFORMATION, *POB_PRE_DUPLICATE_HANDLE_INFORMATION;

DesiredAccess è un valore (o una combinazionedi valori) che indica i permessi di accesso associati all'handle. Ad esempio, se il client richiede un handle per un oggetto con permessi in lettura e scrittura queste informazioni verranno passata in questo parametro. La funzione di callback può modificare questo parametro per limitare i permessi di accesso associati all'handle. Quindi se all'handle erano associati permessi in lettura e scrittura si può, volendo, togliere i permessi in scrittura o in lettura. Non è possibile invece aggiungere permessi (si può solo rimuoverli).

OriginalDesiredAccess è un valore simile al membro DesiredAccess visto sopra ma solo in lettura: non è possibile apportare modifiche a questo membro.

SourceProcess è un puntatore all'oggetto processo di cui si vuole effettuare una copia dell'handle.

TargetProcess è un puntatore all'oggetto processo in cui un nuovo handle riceverà la copia dell'handle.

Dopo aver visto POB_PRE_OPERATION_CALLBACK, insieme a tutte le strutture ad esso collegate, esaminare POB_POST_OPERATION_CALLBACK ed il resto non dovrebbe comportare alcuna difficoltà.

typedef VOID
(*POB_POST_OPERATION_CALLBACK) (
 _In_ PVOID RegistrationContext,
 _In_ POB_POST_OPERATION_INFORMATION OperationInformation
 );

typedef struct _OB_POST_OPERATION_INFORMATION {
 OB_OPERATION                  Operation;
 union {
  ULONG Flags;
  struct {
   ULONG KernelHandle : 1;
   ULONG Reserved : 31;
  };
 };
 PVOID                         Object;
 POBJECT_TYPE                  ObjectType;
 PVOID                         CallContext;
 NTSTATUS                      ReturnStatus;
 POB_POST_OPERATION_PARAMETERS Parameters;
} OB_POST_OPERATION_INFORMATION, *POB_POST_OPERATION_INFORMATION;

ReturnStatus è un valore che indica lo stato finale dell'operazione. In altre parole indica l'esito che ha avuto l'operazione. Se si tratta di uno stato di successo il client riceverà un handle valido, con permessi eventualmente ridotti (come visto quando si è parlato del campo DesiredAccess di OB_PRE_DUPLICATE_HANDLE_INFORMATION).

typedef union _OB_POST_OPERATION_PARAMETERS {
 OB_POST_CREATE_HANDLE_INFORMATION    CreateHandleInformation;
 OB_POST_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation;
} OB_POST_OPERATION_PARAMETERS, *POB_POST_OPERATION_PARAMETERS;

typedef struct _OB_POST_CREATE_HANDLE_INFORMATION {
 ACCESS_MASK GrantedAccess;
} OB_POST_CREATE_HANDLE_INFORMATION, *POB_POST_CREATE_HANDLE_INFORMATION;

typedef struct _OB_POST_DUPLICATE_HANDLE_INFORMATION {
 ACCESS_MASK GrantedAccess;
} OB_POST_DUPLICATE_HANDLE_INFORMATION, *POB_POST_DUPLICATE_HANDLE_INFORMATION;

GrantedAccess è un valore (o una combinazione di valori) che indica il tipo di permessi di accesso associati all'handle e quindi garantiti al client.

Dopo questa lunga premessa per spiegare funzioni e strutture coinvolte nella registrazione di funzioni di callback per il monitoraggio di operazioni sugli handle è arrivato il momento di presentare un esempio concreto. Il codice di questa lezione è tratto da [1] e [2] ed implementa un driver che rimuove i permessi di terminazione dagli handle creati, aperti e copiati che fanno riferimento ad un determinato processo ed ai sui thread. In questo modo ogni tentativo di terminare il thread o processo collegato all'handle fallirà perché non si possiedono i permessi richiesti per tale operazione. Si noti che tale esempio funziona solo con metodi che usano l'handle per terminare il thread o il processo collegato. Ad esempio, l'immagine seguente mostra che chiudere Calculator.exe con Task Manager (Gestione attività) funziona utilizzando la tab dei Dettagli mentre lo stesso non avviene se si usa la tab dei Processi. Esistono molti modi, alcuni anche abbastanza brutali, per chiudere o terminare un processo o un thread ma questo non è un argomento di questa lezione. Si parte con le funzioni di callback da registrare per il monitoraggio di operazioni su handle di thread e processi. Si ricorda che anche in questo caso è richiesta l'opzione /integritycheck passata come comando al linker.




OB_PREOP_CALLBACK_STATUS ProcessPreCall(PVOID RegistrationContextPOB_PRE_OPERATION_INFORMATION pObPreOperationInfo)
{
 UNREFERENCED_PARAMETER(RegistrationContext);
 
 // Nonostante PROCESS_TERMINATE sia un valore specificato nella documentazione ufficiale
 // non è definito o esposto da nessun file header.
 #define PROCESS_TERMINATE 0x0001
 
 PEPROCESS pEProcess = NULL;
 HANDLE pid = NULL;
 
 if (pObPreOperationInfo->ObjectType != *PsProcessType)
 {
  return OB_PREOP_SUCCESS;
 }
 
 pEProcess = (PEPROCESS)pObPreOperationInfo->Object;
 
 // recupera PID da EPROCESS
 pid = PsGetProcessId(pEProcess);
 
 //PCHAR pProcessName = PsGetProcessImageFileName(pEProcess);
 //DbgPrint("[OBCALLBACK][Process] %s PID=%ld\n", pProcessName, pid);
 
 // Verifica se si tratta di un processo che si vuole non venga terminato tramite handle
 if (IsProtectProcess(pEProcess))
 {
  if (pObPreOperationInfo->Operation == OB_OPERATION_HANDLE_CREATE)
  {
   if ((pObPreOperationInfo->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_TERMINATE) == PROCESS_TERMINATE )
   {
    pObPreOperationInfo->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_TERMINATE;
   }
  }
  else if (pObPreOperationInfo->Operation == OB_OPERATION_HANDLE_DUPLICATE)
  {
   if ((pObPreOperationInfo->Parameters->DuplicateHandleInformation.OriginalDesiredAccess & PROCESS_TERMINATE) == PROCESS_TERMINATE)
   {
    pObPreOperationInfo->Parameters->DuplicateHandleInformation.DesiredAccess &= ~PROCESS_TERMINATE;
   }
  }
 }
 
 // OB_PREOP_SUCCESS è il valore di ritorno che ogni funzione di callback passata al membro 
 // PreOperation di OB_OPERATION_REGISTRATION deve restituire.
 return OB_PREOP_SUCCESS;
}

OB_PREOP_CALLBACK_STATUS ThreadPreCall(PVOID RegistrationContextPOB_PRE_OPERATION_INFORMATION pObPreOperationInfo)
{
 UNREFERENCED_PARAMETER(RegistrationContext);
 
 PEPROCESS pEProcess = NULL;
 PETHREAD pEThread = NULL;
 //HANDLE pid;
 
 if (pObPreOperationInfo->ObjectType != *PsThreadType)
 {
  return OB_PREOP_SUCCESS;
 }
 
 pEThread = (PETHREAD)pObPreOperationInfo->Object;
 
 // recupera puntatore aEPROCESS da puntatore a ETHREAD
 pEProcess = IoThreadToProcess(pEThread);
 
 //pid = PsGetProcessId(pEProcess);
 //DbgPrint("[OBCALLBACK][Thread]PID=%ld; TID=%ld\n", pid, PsGetThreadId((PETHREAD)pObPreOperationInfo->Object));
 
 if (IsProtectProcess(pEProcess))
 {
  if (pObPreOperationInfo->Operation == OB_OPERATION_HANDLE_CREATE)
  {
   if ((pObPreOperationInfo->Parameters->CreateHandleInformation.OriginalDesiredAccess & THREAD_TERMINATE) == THREAD_TERMINATE)
   {
    pObPreOperationInfo->Parameters->CreateHandleInformation.DesiredAccess &= ~THREAD_TERMINATE;
   }
  }
  else if (pObPreOperationInfo->Operation == OB_OPERATION_HANDLE_DUPLICATE)
  {
   if ((pObPreOperationInfo->Parameters->DuplicateHandleInformation.OriginalDesiredAccess & THREAD_TERMINATE) == THREAD_TERMINATE)
   {
    pObPreOperationInfo->Parameters->DuplicateHandleInformation.DesiredAccess &= ~THREAD_TERMINATE;
   }
  }
 }
 
 return OB_PREOP_SUCCESS;
}

BOOLEAN IsProtectProcess(PEPROCESS pEProcess)
{
 PCHAR pProcessName = PsGetProcessImageFileName(pEProcess);
 if (pProcessName != NULL)
 {
  if (_stricmp(pProcessName, "Calculator.exe") == 0)
  {
   DbgPrint("[Protect]");
   return TRUE;
  }
 }
 
 return FALSE;
}

NTSTATUS SetProcessCallbacks()
{
 NTSTATUS status = STATUS_SUCCESS;
 OB_CALLBACK_REGISTRATION obCallbackReg;
 OB_OPERATION_REGISTRATION obOperationReg;
 
 RtlZeroMemory(&obCallbackReg, sizeof(OB_CALLBACK_REGISTRATION));
 RtlZeroMemory(&obOperationReg, sizeof(OB_OPERATION_REGISTRATION));
 
 obCallbackReg.Version = OB_FLT_REGISTRATION_VERSION;
 obCallbackReg.OperationRegistrationCount = 1;
 obCallbackReg.RegistrationContext = NULL;
 RtlInitUnicodeString(&obCallbackReg.Altitude, L"321000");
 obCallbackReg.OperationRegistration = &obOperationReg;
 
 obOperationReg.ObjectType = PsProcessType;
 obOperationReg.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
 obOperationReg.PreOperation = (POB_PRE_OPERATION_CALLBACK)ProcessPreCall;
 
 status = ObRegisterCallbacks(&obCallbackReg, &g_obProcessHandle);
 if (!NT_SUCCESS(status))
 {
  DbgPrint("ObRegisterCallbacks Error[0x%X]\n", status);
  return status;
 }
 
 return status;
}

NTSTATUS SetThreadCallbacks()
{
 NTSTATUS status = STATUS_SUCCESS;
 OB_CALLBACK_REGISTRATION obCallbackReg;
 OB_OPERATION_REGISTRATION obOperationReg;
 
 RtlZeroMemory(&obCallbackReg, sizeof(OB_CALLBACK_REGISTRATION));
 RtlZeroMemory(&obOperationReg, sizeof(OB_OPERATION_REGISTRATION));
 
 obCallbackReg.Version = OB_FLT_REGISTRATION_VERSION;
 obCallbackReg.OperationRegistrationCount = 1;
 obCallbackReg.RegistrationContext = NULL;
 RtlInitUnicodeString(&obCallbackReg.Altitude, L"321001");
 obCallbackReg.OperationRegistration = &obOperationReg;
 
 obOperationReg.ObjectType = PsThreadType;
 obOperationReg.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
 obOperationReg.PreOperation = (POB_PRE_OPERATION_CALLBACK)ThreadPreCall;
 
 status = ObRegisterCallbacks(&obCallbackReg, &g_obThreadHandle);
 if (!NT_SUCCESS(status))
 {
  DbgPrint("ObRegisterCallbacks Error[0x%X]\n", status);
  return status;
 }
 
 return status;
}

VOID RemoveProcessCallbacks()
{
 if (g_obProcessHandle != NULL)
 {
  ObUnRegisterCallbacks(g_obProcessHandle);
  g_obProcessHandle = NULL;
 }
}

VOID RemoveThreadCallbacks()
{
 if (g_obThreadHandle != NULL)
 {
  ObUnRegisterCallbacks(g_obThreadHandle);
  g_obThreadHandle = NULL;
 }
}

#include <ntddk.h>
 
// Variabili globali
PVOID g_obProcessHandle;
PVOID g_obThreadHandle;
 
// Esportate da Ntoskrnl.exe ed esposte in Ntoskrnl.lib
NTKERNELAPI PCHAR PsGetProcessImageFileName(PEPROCESS Process);
NTKERNELAPI PEPROCESS IoThreadToProcess(PETHREAD Thread);
 
 
VOID DriverUnload(PDRIVER_OBJECT pDriverObj)
{
 UNREFERENCED_PARAMETER(pDriverObj);
 
 // Rimuove funzioni di callback
 RemoveProcessCallbacks();
 RemoveThreadCallbacks();
 
 DbgPrint("[MyDriver]Unloaded!\n");
}
 
 
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObjectPUNICODE_STRING pRegPath)
{
 UNREFERENCED_PARAMETER(pRegPath);
 DbgPrint("Enter DriverEntry\n");
 
 NTSTATUS status = STATUS_SUCCESS;
 pDriverObject->DriverUnload = DriverUnload;
 
 // Imposta funzioni di callback
 SetProcessCallbacks();
 SetThreadCallbacks();
 
 DbgPrint("Leave DriverEntry\n");
 return status;
}




Codice sorgente:
MonitorHandleDriver.zip



Riferimenti:
[1] https://github.com/dearfuture/WindowsHack/tree/master/内核层/5/对象监控/ObRegisterCallbacks_Test/ObRegisterCallbacks_Test
[2] https://github.com/andylau004/LookDrvCode/tree/master/WIN64驱动编程基础教程/代码/%5B4-5%5DMonitorProcessThreadHandle

Nessun commento:

Posta un commento