domenica 22 settembre 2019

20 - Monitoraggio: File (Minifilter)

Parlare di File System, Filter Manager e Minifilter in maniera approfondita richiederebbe, come minimo, un corso separato ma questo non esclude il fatto che si possano introdurre i concetti generali e scrivere un esempio di codice funzionante come quello proposto in questa lezione e che permette di monitorare (e volendo anche impedire) le operazioni di I/O sui file. I principi generali non sono difficili da comprendere e l'ostacolo del copioso codice iniziale da scrivere viene agevolmente superato grazie all'uso di un template di Visual Studio specifico per i Minifilter che genera quasi più del 90% del codice necessario.



File System, Filter Manager e Minifilter

Il ruolo dei Minifilter è quello di prendere in gestione le richieste fatte al disco fisico prima che queste vengano intercettate dal driver del File System che si occupa di processare ed inviare tali richieste ai driver che dialogano direttamente con tale disco fisico.




Se non ci sono Minifilter nello scenario l'I/O Manager dialoga direttamente con il driver del File System. Quando invece c'è almeno un Minifilter entra in gioco il Filter Manager che si occupa di dirottare le richieste dell'I/O Manager ai vari Minifilter di cui esso stesso si occupa. Le richieste vengono inviate al Minifilter che ha il valore di Altitude più alto. A questo punto il Minifilter che ha preso in gestione la richiesta può decidere se completare l'operazione e soddisfare la richiesta oppure può decidere di far completare la richiesta a qualcun altro e passare la richiesta (eventualmente modificata) nuovamente al Filter Manager che la inoltrerà al Minifilter con valore di Altitude successivo più alto, e così via. Da notare che ogni Minifilter può intercettare la richiesta di una operazione di I/O sui file sia prima che dopo che questa venga portata a termine.



Installazione

Un Minifilter è molto simile ad un normale driver ma necessita di chiavi e valori aggiuntivi nel registro di sistema che vanno oltre a quelli che vengono normalmente creati dai tool utilizzati fino a questo momento (sc.exe o KdManager). Per installare un Minifilter esistono altri metodi che comprendono: alcune specifiche funzioni dell'API (sia in user che in kernel mode), il tool fltmc.exe e OSR Driver Loader. In questa lezione useremo fltmc.exe poiché Visual Studio genera tutto il codice necessario (a scrivere il registro di sistema e a posizionare il file del Minifilter in %windir%\system32\drivers) e lo mette in un file con estensione .INF. Per fare in modo che il file con estensione .INF entri in azione e porti a termine le operazioni preliminari è sufficiente cliccarci sopra con il tasto destro del mouse e selezionare la voce Installa. A questo punto tutto è pronto per caricare il Minifilter in memoria ed invocare il suo entrypoint con

fltmc load Nome_del_Minifilter

Quando si vuole togliere il Minifilter dalla gestione del Filter Manager è sufficiente il comando

fltmc unload Nome_del_Minifilter



API

Per registrare un Minifilter nel Filter Manager è necessaria una chiamata alla funzione FltRegisterFilter. Per avviare l'azione del Minifilter ed iniziare ad intercettare richieste si usa la funzione FltStartFiltering. Infine per annullare una registrazione esistente si usa la funzione FltUnregisterFilter.

NTSTATUS FLTAPI FltRegisterFilter(
 PDRIVER_OBJECT         Driver,
 const FLT_REGISTRATION *Registration,
 PFLT_FILTER            *RetFilter
);

NTSTATUS FLTAPI FltStartFiltering(
 PFLT_FILTER Filter
);

VOID FLTAPI FltUnregisterFilter(
 PFLT_FILTER Filter
);

Driver è il puntatore al oggetto driver passato a DriverEntry.

RetFilter è un puntatore a puntatore che verrà inizializzato per fare riferimento ad una istanza di tipo FLT_FILTER, il cui indirizzo verrà usato con FltStartFiltering e FltUnregisterFilter.

Registration è un puntatore ad una istanza di tipo FLT_REGISTRATION, una struttura che conserva praticamente tutte le info importanti del Minifilter. Di seguito è riportata la definizione di tale struttura con il dettaglio dei campi più importanti

typedef struct _FLT_REGISTRATION {
 USHORT                                      Size;
 USHORT                                      Version;
 FLT_REGISTRATION_FLAGS                      Flags;
 const FLT_CONTEXT_REGISTRATION              *ContextRegistration;
 const FLT_OPERATION_REGISTRATION            *OperationRegistration;
 PFLT_FILTER_UNLOAD_CALLBACK                 FilterUnloadCallback;
 PFLT_INSTANCE_SETUP_CALLBACK                InstanceSetupCallback;
 PFLT_INSTANCE_QUERY_TEARDOWN_CALLBACK       InstanceQueryTeardownCallback;
 PFLT_INSTANCE_TEARDOWN_CALLBACK             InstanceTeardownStartCallback;
 PFLT_INSTANCE_TEARDOWN_CALLBACK             InstanceTeardownCompleteCallback;
 PFLT_GENERATE_FILE_NAME                     GenerateFileNameCallback;
 PFLT_NORMALIZE_NAME_COMPONENT               NormalizeNameComponentCallback;
 PFLT_NORMALIZE_CONTEXT_CLEANUP              NormalizeContextCleanupCallback;
 PFLT_TRANSACTION_NOTIFICATION_CALLBACK      TransactionNotificationCallback;
 PFLT_NORMALIZE_NAME_COMPONENT_EX            NormalizeNameComponentExCallback;
 PFLT_SECTION_CONFLICT_NOTIFICATION_CALLBACK SectionNotificationCallback;
} FLT_REGISTRATION, *PFLT_REGISTRATION;

Size è la dimensione di una istanza della struttura stessa. Per questo motivo a questo parametro si passa sempre sizeof(FLT_REGISTRATION).

Version è un valore numerico a cui si passa sempre FLT_REGISTRATION_VERSION.

OperationRegistration è un puntatore ad un array di istanze di tipo FLT_OPERATION_REGISTRATION.

typedef struct _FLT_OPERATION_REGISTRATION {
 UCHAR                            MajorFunction;
 FLT_OPERATION_REGISTRATION_FLAGS Flags;
 PFLT_PRE_OPERATION_CALLBACK      PreOperation;
 PFLT_POST_OPERATION_CALLBACK     PostOperation;
 PVOID                            Reserved1;
} FLT_OPERATION_REGISTRATION, *PFLT_OPERATION_REGISTRATION;

MajorFunction è l'indice major function (IRP_MJ_CREATE, IRP_MJ_READ, IRP_MJ_WRITE, ecc.) dell'operazione a cui il Minifilter è interessato e che vuole intercettare e prendere in carico.

PreOperation è un puntatore ad una funzione di callback che verrà invocata prima che l'operazione sul file indicata dal parametro MajorFunction venga portata a termine. Tale funzione di callback verrà eseguita ad un IRQL $\le$ APC_LEVEL e generalmente dal thread che ha richiesto l'operazione sul file. Se la funzione di callback decide di soddisfare la richiesta ritorna FLT_PREOP_COMPLETE altrimenti, se vuole passare la richiesta a qualcun altro, ritorna FLT_PREOP_SUCCESS_NO_CALLBACK o FLT_PREOP_SUCCESS_WITH_CALLBACK (a seconda che si voglia o meno che la funzione di callback indicata dal membro PostOperation debba essere invocata una volta terminata l'operazione). Inoltre, la funzione di callback deve essere compatibile con la definizione del puntatore PFLT_PRE_OPERATION_CALLBACK.

PostOperation è un puntatore ad una funzione di callback che verrà invocata dopo che l'operazione sul file indicata dal parametro MajorFunction è stata portata a termine. Tale funzione di callback verrà eseguita un IRQL $>$ APC_LEVEL e generalmente da un thread arbitrario. Inoltre, il valore restituito è in genere FLT_POSTOP_FINISHED_PROCESSING per indicare che il processamento della richiesta è terminato. Infine, deve essere compatibile con la definizione del puntatore PFLT_PRE_OPERATION_CALLBACK.

typedef FLT_PREOP_CALLBACK_STATUS
(FLTAPI *PFLT_PRE_OPERATION_CALLBACK) (
 _Inout_ PFLT_CALLBACK_DATA Data,
 _In_ PCFLT_RELATED_OBJECTS FltObjects,
 _Outptr_result_maybenull_ PVOID *CompletionContext
 );

typedef FLT_POSTOP_CALLBACK_STATUS
(FLTAPI *PFLT_POST_OPERATION_CALLBACK) (
 _Inout_ PFLT_CALLBACK_DATA Data,
 _In_ PCFLT_RELATED_OBJECTS FltObjects,
 _In_opt_ PVOID CompletionContext,
 _In_ FLT_POST_OPERATION_FLAGS Flags
 );

FltObjects è un puntatore ad una istanza di tipo CFLT_RELATED_OBJECTS che contiene informazioni sul file su cui è in atto l'operazione (non rilevante al momento perché le informazioni sul file utili per questo articolo verranno ricavate in un altro modo).

Data è un puntatore ad una istanza di tipo FLT_CALLBACK_DATA.

typedef struct _FLT_CALLBACK_DATA {
 FLT_CALLBACK_DATA_FLAGS     Flags;
 PETHREAD                    Thread;
 PFLT_IO_PARAMETER_BLOCK     Iopb;
 IO_STATUS_BLOCK             IoStatus;
 struct _FLT_TAG_DATA_BUFFER *TagData;
 union {
  struct {
   LIST_ENTRY QueueLinks;
   PVOID      QueueContext[2];
  };
  PVOID FilterContext[4];
 };
 KPROCESSOR_MODE             RequestorMode;
} FLT_CALLBACK_DATA, *PFLT_CALLBACK_DATA;

Thread è un puntatore all'ETHREAD del thread che ha effettuato la richiesta di operazione di I/O sul file. Può essere NULL.

IoStatus è un valore che può essere impostato solo se il minifilter decide di soddisfare la richiesta o per indicare che il processamento della richiesta è terminato. Quindi solo quando si ritorna FLT_PREOP_COMPLETE o FLT_POSTOP_FINISHED_PROCESSING nelle rispettive funzioni di callback.

RequestorMode è un valore che indica la modalità di esecuzione del processo che ha avviato la richiesta di I/O sul file. Tale valore può essere UserMode o KernelMode.

Iopb è un puntatore ad una istanza di tipo FLT_IO_PARAMETER_BLOCK.

typedef struct _FLT_IO_PARAMETER_BLOCK {
 ULONG          IrpFlags;
 UCHAR          MajorFunction;
 UCHAR          MinorFunction;
 UCHAR          OperationFlags;
 UCHAR          Reserved;
 PFILE_OBJECT   TargetFileObject;
 PFLT_INSTANCE  TargetInstance;
 FLT_PARAMETERS Parameters;
} FLT_IO_PARAMETER_BLOCK, *PFLT_IO_PARAMETER_BLOCK;

TargetFileObject è un puntatore al file object relativo al file su cui è in atto l'operazione di I/O (anche in questo caso non è rilevante perché le info sul file utili per questo articolo verranno ricavate in un altro modo).

Parameters è l'unione di un gran numero di strutture, ognuna valida nel contesto di una operazione specifica. Ad esempio, per una operazione in lettura (che coinvolge quindi l'indice major function IRP_MJ_READ) la struttura da usare è Parameters.Read. Per certi versi è molto simile al campo Parameters di IO_STACK_LOCATION visto nelle precedenti lezioni.

Dopo aver spiegato a grandi linee le funzioni e le strutture coinvolte è arrivato il momento di presentare un esempio concreto. Il codice di questa lezione è tratto da [1] ed implementa un Minifilter che monitora le operazioni su tutti i file con nome readme.txt. Il codice da scrivere partendo da un progetto vuoto sarebbe parecchio ma fortunatamente Visual Studio offre un template chiamato Filter Driver: Filesystem Mini-filter che si occupa di generare praticamente tutto il codice necessario, persino quello del file con estensione .INF. A quel punto a noi non resta che fixare un paio di cose e dedicarci esclusivamente al codice delle funzioni di callback.




Partendo dalle cose da fixare nel codice generato da Visual Studio. Nel file con estensione .inf presente nel progetto all'interno della cartella Driver Files è necessario andare nella categoria [Version] (solitamente è all'inizio del file) e togliere i commenti alle righe che incominciano con Class e ClassGuid ed eliminare le righe immediatamente sotto di queste che contengono valori fittizi per le chiavi Class e ClassGuid. Il risultato dovrebbe essere simile questo

[Version]
Signature   = "$Windows NT$"
; TODO - Change the Class and ClassGuid to match the Load Order Group value, see https://...
Class       = "ActivityMonitor"                         ;This is determined by ...
ClassGuid   = {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2}    ;This value is determined by ..
Provider    = %ManufacturerName%
DriverVer   = 
CatalogFile = MonitorFileMiniFilter.cat

Successivamente è necessario andare nella categoria [Strings] (di solito alla fine del file) ed impostare una valore numerico (tra virgolette) per Instance1.Altitude che stia nell'intervallo della chiave Class selezionata in precedenza (si veda [3]). Ad esempio

Instance1.Altitude       = "370101"

Dopo aver salvato le modifiche si può passare al file con estensione .c generato da Visual Studio e che contiene tutto il codice necessario già pronto: entrypoint, funzione di unload, array di FLT_OPERATION_REGISTRATION, dichiarazioni e definizioni delle funzioni di callback (vuote naturalmente). Prima di tutto è necessario modificare l'array di FLT_OPERATION_REGISTRATION in modo che il risultato finale sia

CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
 
 { IRP_MJ_CREATE,
   0,
   MonitorFileMiniFilterPreOperation,
   MonitorFileMiniFilterPostOperation },
 
 { IRP_MJ_CLOSE,
   0,
   MonitorFileMiniFilterPreOperation,
   MonitorFileMiniFilterPostOperation },
 
 { IRP_MJ_READ,
   0,
   MonitorFileMiniFilterPreOperation,
   MonitorFileMiniFilterPostOperation },
 
 { IRP_MJ_WRITE,
   0,
   MonitorFileMiniFilterPreOperation,
   MonitorFileMiniFilterPostOperation },
 
 { IRP_MJ_SET_INFORMATION,
   0,
   MonitorFileMiniFilterPreOperation,
   MonitorFileMiniFilterPostOperation },
 
 { IRP_MJ_OPERATION_END }
};

Fatto questo non resta che scrivere il codice all'interno delle funzioni di callback di interesse già generate da Visual Studio, più altre eventuali funzioni accessorie.

/*************************************************************************
    MiniFilter callback routines.
*************************************************************************/
FLT_PREOP_CALLBACK_STATUS
MonitorFileMiniFilterPreOperation (
    _Inout_ PFLT_CALLBACK_DATA Data,
    _In_ PCFLT_RELATED_OBJECTS FltObjects,
    _Flt_CompletionContext_Outptr_ PVOID *CompletionContext
    )
 
// ...

{
    NTSTATUS status;
 
    UNREFERENCED_PARAMETERFltObjects );
    UNREFERENCED_PARAMETERCompletionContext );
 
    PT_DBG_PRINTPTDBG_TRACE_ROUTINES,
                  ("MonitorFileMiniFilter!MonitorFileMiniFilterPreOperation: Entered\n") );
 
 UCHAR MajorFunction = Data->Iopb->MajorFunction;
 PFLT_FILE_NAME_INFORMATION lpNameInfo = NULL;

 status = FltGetFileNameInformation(
              DataFLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, 
              &lpNameInfo);

 if (NT_SUCCESS(status))
 {
  status = FltParseFileNameInformation(lpNameInfo);
  if (NT_SUCCESS(status))
  {
   // CREATE
   if (MajorFunction == IRP_MJ_CREATE)
   {
    if (IsProtectionFile(lpNameInfo))
    {
     KdPrint(("[IRP_MJ_CREATE]%wZ", &lpNameInfo->Name));
 
     // Permette l'operazione lasciando passare la richiesta
     return FLT_PREOP_SUCCESS_NO_CALLBACK;
 
     // Impedisce l'operazione indicando accesso non permesso e completando l'operazione
     //Data->IoStatus.Status = STATUS_ACCESS_DENIED;
     //return FLT_PREOP_COMPLETE;
    }
   }
   // READ
   else if (MajorFunction == IRP_MJ_READ)
   {
    if (IsProtectionFile(lpNameInfo))
    {
     KdPrint(("[IRP_MJ_READ]%wZ", &lpNameInfo->Name));
 
     // Permette l'operazione lasciando passare la richiesta
     return FLT_PREOP_SUCCESS_NO_CALLBACK;
 
     // Impedisce l'operazione indicando accesso non permesso e completando l'operazione
     //Data->IoStatus.Status = STATUS_ACCESS_DENIED;
     //return FLT_PREOP_COMPLETE;
    }
   }
   // WRITE
   else if (MajorFunction == IRP_MJ_WRITE)
   {
    if (IsProtectionFile(lpNameInfo))
    {
     KdPrint(("[IRP_MJ_WRITE]%wZ", &lpNameInfo->Name));
 
     // Permette l'operazione lasciando passare la richiesta
     return FLT_PREOP_SUCCESS_NO_CALLBACK;
 
     // Impedisce l'operazione indicando accesso non permesso e completando l'operazione
     //Data->IoStatus.Status = STATUS_ACCESS_DENIED;
     //return FLT_PREOP_COMPLETE;
    }
   }
   // SET
   else if (MajorFunction == IRP_MJ_SET_INFORMATION)
   {
    if (IsProtectionFile(lpNameInfo))
    {
     KdPrint(("[IRP_MJ_SET_INFORMATION]%wZ", &lpNameInfo->Name));
 
     // Permette l'operazione lasciando passare la richiesta
     return FLT_PREOP_SUCCESS_NO_CALLBACK;
 
     // Impedisce l'operazione indicando accesso non permesso e completando l'operazione
     //Data->IoStatus.Status = STATUS_ACCESS_DENIED;
     //return FLT_PREOP_COMPLETE;
    }
   }
  }
 
  if (lpNameInfo != NULL)
   FltReleaseFileNameInformation(lpNameInfo);
 }
 
// ...
 
    return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}

BOOLEAN IsProtectionFile(PFLT_FILE_NAME_INFORMATION lpNameInfo)
{
 BOOLEAN bProtect = FALSE;
 PWCHAR lpszProtectionFileName, lpszFileName;
 
 lpszProtectionFileName = (PWCHAR)ExAllocatePool(NonPagedPool, 256);
 lpszFileName = (PWCHAR)ExAllocatePool(NonPagedPool, 512);
 
 RtlZeroMemory(lpszProtectionFileName, 256);
 RtlZeroMemory(lpszFileName, 512);
 
 RtlCopyMemory(lpszFileName, lpNameInfo->Name.Buffer, (sizeof(WCHAR) + lpNameInfo->Name.Length));
 RtlCopyMemory(lpszProtectionFileName, L"readme.txt", (sizeof(WCHAR) + wcslen(L"readme.txt")));
 
 if (wcsstr(lpszFileName, lpszProtectionFileName) != NULL)
 {
  bProtect = TRUE;
 }
 
 ExFreePool(lpszProtectionFileName);
 ExFreePool(lpszFileName);
 
 return bProtect;
}

Di particolare interesse è il codice che recupera il nome del file. Come detto in precedenza, le informazioni sul file su cui è in atto l'operazione di I/O si possono trovare in più di una struttura dati (ad esempio il campo TargetFileObject di FLT_IO_PARAMETER_BLOCK ed il parametro FltObjects delle funzioni di callback). Il problema è che non ci si può fidare troppo di questi dati per una serie di motivi: i file possono essere aperti con percorsi completi o relativi, le informazioni sul nome di un file spesso vengono conservate in cache per comodità, possono verificarsi operazioni simultanee di rinomina del file, ecc. Per questi e altri motivi quando si vuole recuperare il nome di un file in questo contesto si usano le funzioni FltGetFileNameInformation, FltParseFileNameInformation e FltReleaseFileNameInformation.

NTSTATUS FLTAPI FltGetFileNameInformation(
 PFLT_CALLBACK_DATA         CallbackData,
 FLT_FILE_NAME_OPTIONS      NameOptions,
 PFLT_FILE_NAME_INFORMATION *FileNameInformation
);

NTSTATUS FLTAPI FltParseFileNameInformation(
 PFLT_FILE_NAME_INFORMATION FileNameInformation
);

VOID FLTAPI FltReleaseFileNameInformation(
 PFLT_FILE_NAME_INFORMATION FileNameInformation
);

CallbackData è un puntatore all'istanza di tipo FLT_CALLBACK_DATA ricevuta dalla funzione di callback attraverso il parametro Data.

NameOptions è un valore (o una combinazione di valori) che specifica il formato del nome del file che si vuole recuperare e le modalità di richiesta nel recupero delle informazioni sul nome del file. Passando, ad esempio, la combinazione FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT significa che l'ultimo parametro (FileNameInformation) riceverà l'indirizzo di una istanza di tipo FLT_FILE_NAME_INFORMATION che contiene il nome in forma normalizzata prendendo le info sul nome del file dalla cache (se già disponibile). Se tali info non sono già in cache viene fatta richiesta al file system (se possibile) di recuperare tali info per metterle in cache. Altrimenti ritorna errore. Un esempio di nome di file normalizato è "\Device\HarddiskVolume1\Documents and Settings\MyUser\My Documents\Test Results.txt"

FileNameInformation è, in FltGetFileNameInformation, un puntatore a puntatore che verrà inizializzato per fare riferimento ad una istanza di tipo FLT_FILE_NAME_INFORMATION che all'inizio contiene solo le info sul nome del file nel campo Name. Per popolare gli altri campi della struttura a partire da Name si usa la funzione FltParseFileNameInformation mentre FltReleaseFileNameInformation serve per rilasciare le risorse impiegate per costruire tale struttura. Entrambe queste funzioni accettano come argomento l'indirizzo all'istanza di tipo FLT_FILE_NAME_INFORMATION ottenuto attraverso il membro di output FileNameInformation di FltGetFileNameInformation.

typedef struct _FLT_FILE_NAME_INFORMATION {
 USHORT                     Size;
 FLT_FILE_NAME_PARSED_FLAGS NamesParsed;
 FLT_FILE_NAME_OPTIONS      Format;
 UNICODE_STRING             Name;
 UNICODE_STRING             Volume;
 UNICODE_STRING             Share;
 UNICODE_STRING             Extension;
 UNICODE_STRING             Stream;
 UNICODE_STRING             FinalComponent;
 UNICODE_STRING             ParentDir;
} FLT_FILE_NAME_INFORMATION, *PFLT_FILE_NAME_INFORMATION;

Size è la dimensione di una istanza di FLT_FILE_NAME_INFORMATION. Il significato degli altri campi è facilmente intuibile oppure è irrilevante.

L'immagine sotto mostra il risultato dell'esempio se si prova a commentare

return FLT_PREOP_SUCCESS_NO_CALLBACK;

e a rimuovere il commento a

//Data->IoStatus.Status = STATUS_ACCESS_DENIED;
//return FLT_PREOP_COMPLETE;

Come si può notare, il nostro semplice monitor delle operazioni sui file di nome readme.txt si trasformerebbe in un driver in grado di proteggere ed impedire la cancellazione di tutti i file con questo nome.





Codice sorgente:
MonitorFileMiniFilter.zip



Riferimenti:
[1] https://github.com/dearfuture/WindowsHack/tree/master/内核层/5/Minifilter文件监控
[2] https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/file-system-minifilter-drivers
[3] https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/load-order-groups-and-altitudes-for-minifilter-drivers

Nessun commento:

Posta un commento