giovedì 1 aprile 2021

26 - Diagnostica: Event Tracing for Windows (ETW)

Nella scorsa lezione si è visto come gli sviluppatori possono sfruttare WPP (Windows Software Trace Preprocessor) per il tracciamento nei programmi in fase di debug. Se quello che si vuole è un tracciamento più generale di eventi che avvengono nel programma (cioè che abbia una utilità anche al di fuori della fase di debug), si dovrebbe considerare ETW. WPP si basa su ETW e ne semplifica l'utilizzo quindi una lezione esaustiva su tale argomento è al di fuori degli scopi di questa lezione (per maggiori informazioni si vedano i riferimenti a fine lezione). Quello che si esaminerà in questo contesto è invece un caso d'uso molto semplice e lineare. Innanzitutto è utile sapere che le componenti del tracciamento tramite ETW sono praticamente le stesse viste nella lezione su WPP: provider, controller, buffer, sessioni, file di log, consumer, ecc. 




Aver visto WPP, quindi, ci da già un bel vantaggio in partenza che ci permette di passare ad esaminare direttamente un esempio pratico di test diagnostico.




Test diagnostico

Prima di esaminare il codice sorgente del progetto EvntDrv (fornito da Microsoft; si veda [1]) è utile vedere un esempio pratico di test diagnostico. Innanzitutto è necessario aver già i file SYS,  EXE ed XML, rispettivamente di EventDrv (il provider), EvntCtrl (programma user-mode che interroga il provider) ed il manifesto (si vedrà a breve cosa contiene e a cosa serve). Ad ogni modo, al momento ci si limiti a seguire leggendo, senza mettere mano a Visual Studio (si vedrà in seguito come compilare i due progetti).

- Ci si assicuri che il file SYS abbia i permessi in lettura per tutti (Everyone). In caso contrario, si apra una command prompt da amministratore, si vada nella cartella che contiene il suddetto file e si dia il seguente comando
 icacls eventdrv.sys /grant Everyone:R

- Si installi il manifesto con il seguente comando
  wevtutil.exe im evntdrv.xml

- Si esegua il Visualizzatore Eventi di Windows (Event Viewer). Per farlo è possibile cercarlo nella barra di ricerca o scrivendo eventvwr nella stessa barra di ricerca, in una command prompt o nell'app Esegui (Run) di Windows.

- Si avvii EventCtrl.exe con privilegi di amministratore. Si prema un paio di volte un pulsante qualsiasi per inviare una richiesta a EvntDrv ed infine il tasto "q" per uscire. 

- Infine dare il seguente comando per disinstallare il manifesto
 wevtutil.exe um evntdrv.xml




- Passando ora al Visualizzatore Eventi, si selezioni la voce Sistema, sotto Registri di Windows (nella parte sinistra). Quello che si nota (nella parte centrale) è che sono stati catturati alcuni eventi generati da un provider chiamato Sample Driver (il nome dato al provider EvntDrv all'interno del manifesto; il relativo codice verrà esaminato a breve). Si noti che insieme al nome del provider ci sono anche un ID che identifica l'evento, il suo livello e data e ora in cui è stato generato. Nella parte inferiore, invece, si può vedere una stringa associata all'evento selezionato. In questo caso il messaggio dice "Driver Loaded".




- Per vedere quali altre informazioni, oltre al messaggio, sono associate all'evento si selezioni la tab Dettagli. Questa mostrerà, sotto la voce EventData, alcune informazioni passate dal provider al momento della generazione dell'evento.




- In Visualizzatore Eventi è anche possibile filtrare gli eventi da visualizzare in base al provider e/o all'ID dell'evento. Per farlo ci sono due modi (entrambi selezionabili nella parte destra): modificare la visualizzazione corrente (Filtra registro corrente) o crearne una nuova (Crea visualizzazione personalizzata). Si provi, ad esempio, a creare una visualizzazione personalizzata scegliendo un filtro "Per Origine" e mettendo una spunta su Sample Driver nella casella a discesa nominata "Origine Eventi". Come risultato, si dovrebbero avere a schermo solo eventi generati da Sample Driver.






Workflow

Quando si vogliono aggiungere funzionalità di tracciamento tramite ETW ad un driver i passi da fare sono quelli raffigurati nell'immagine seguente. Ognuno di questi verrà esaminato nelle rispettive sezioni.





1. Decidere i tipi di eventi da generare

La prima cosa da fare è pensare a quali eventi si desidera generare. In particolare, si deve decidere dove pubblicare l'evento, il suo livello, il messaggio e le altre eventuali informazioni associate. Come visto nel programma Visualizzatore Eventi, gli eventi si possono trovare in varie voci all'interno della chiave Registri di Windows. Tali voci, all'interno di ETW, prendono il nome di canali. In questa lezioni si discuterà di eventi pubblicati esclusivamente sul canale Sistema (System) ma il discorso non cambia quando si parla di altri canali.



2. Creare il manifesto

Decisi gli eventi da generare, è necessario costruire un manifesto (un semplice file XML) con una descrizione completa del provider e degli eventi che esso andrà a generare. Per la creazione del manifesto si può usare un tool specifico come ECManGen (si veda [5]). In questa lezione ci si limiterà ad esaminare il manifesto già pronto all'interno del progetto EvntDrv. Il resto della sezione è dedicato alla descrizione sommaria degli elementi principali che compongono tale manifesto, evntdrv.xml, il cui codice è quello mostrato di seguito. Per una discussione esaustiva si veda [4].

<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<instrumentationManifest
    xmlns="http://schemas.microsoft.com/win/2004/08/events"
    xmlns:win="http://manifests.microsoft.com/win/2004/08/windows/events"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://schemas.microsoft.com/win/2004/08/events eventman.xsd"
    >
  <instrumentation>
    <events>
      <provider
          guid="{b5a0bda9-50fe-4d0e-a83d-bae3f58c94d6}"
          messageFileName="%USERPROFILE%\Desktop\Eventdrv\Eventdrv.sys"
          name="Sample Driver"
          resourceFileName="%USERPROFILE%\Desktop\Eventdrv\Eventdrv.sys"
          symbol="DriverControlGuid"
          >
        <channels>
          <importChannel
              chid="SYSTEM"
              name="System"
              />
        </channels>
        <templates>
          <template tid="tid_load_template">
            <data
                inType="win:UInt16"
                name="DeviceNameLength"
                outType="xs:unsignedShort"
                />
            <data
                inType="win:UnicodeString"
                name="name"
                outType="xs:string"
                />
            <data
                inType="win:UInt32"
                name="Status"
                outType="xs:unsignedInt"
                />
          </template>
          <template tid="tid_unload_template">
            <data
                inType="win:Pointer"
                name="DeviceObjPtr"
                outType="win:HexInt64"
                />
          </template>
        </templates>
        <events>
          <event
              channel="SYSTEM"
              level="win:Informational"
              message="$(string.StartEvent.EventMessage)"
              opcode="win:Start"
              symbol="StartEvent"
              template="tid_load_template"
              value="1"
              />
          <event
              channel="SYSTEM"
              level="win:Informational"
              message="$(string.SampleEventA.EventMessage)"
              opcode="win:Info"
              symbol="SampleEventA"
              value="2"
              />
          <event
              channel="SYSTEM"
              level="win:Informational"
              message="$(string.UnloadEvent.EventMessage)"
              opcode="win:Stop"
              symbol="UnloadEvent"
              template="tid_unload_template"
              value="3"
              />
        </events>
      </provider>
    </events>
  </instrumentation>
  <localization xmlns="http://schemas.microsoft.com/win/2004/08/events">
    <resources culture="en-US">
      <stringTable>
        <string
            id="StartEvent.EventMessage"
            value="Driver Loaded"
            />
        <string
            id="SampleEventA.EventMessage"
            value="IRP A Occurred"
            />
        <string
            id="UnloadEvent.EventMessage"
            value="Driver Unloaded"
            />
      </stringTable>
    </resources>
  </localization>
</instrumentationManifest>

provider indica GUID e nome che si vuole dare al provider (si può dare qualsiasi nome, non deve essere per forza uguale a quello del driver). Oltre a queste due informazioni si indica anche dove ETW può trovare la risorsa necessaria a codificare le informazioni associate agli eventi. E' possibile anche fornire la risorsa che contiene la string table con le versioni localizzate dei messaggi associati agli eventi ed altre stringhe.  Nel manifesto di questa lezione, in entrambi i casi si indica il file SYS del driver perché le risorse saranno incorporate in esso. Tale argomento verrà ripreso a breve.

channels indica in quali canali pubblicare gli eventi generati dal provider. E' possibile pubblicare gli eventi in più canali ma, in questo caso, si indica solo System (Sistema).

template (all'interno di templates) indica il tipo ed il nome dei parametri di una particolare funzione che genera un evento di tracciamento. Si tornerà su tale argomento a breve.

event (all'interno di events) indica varie informazioni che riguardano un particolare evento generato dal provider: livello, simbolo, valore (inteso come ID), messaggio, ecc. Si noti che si può indicare anche un template per indicare i parametri della relativa funzione di tracciamento. In caso contrario la funzione semplicemente non prenderà argomenti.

string (all'interno di stringTable) definisce una stringa ed un ID associato ad essa. L'attributo message, visto in event, può far riferimento a tale ID per indicare la stringa associata all'evento.



3. Compilare il manifesto

Il manifesto appena creato serve a due scopi. Il primo è quello di generare l'header con il codice delle funzioni di tracciamento e le risorse per la codifica degli eventi e la string table con le varie localizzazioni. Il secondo scopo del manifesto verrà visto in un'altra sezione. Per avviare la compilazione del manifesto prima della compilazione vera e propria si imposti Si nelle proprietà "Generate Kernel Mode Logging Macros" e "Use base Name Of Input" di Message Compiler ->General del progetto EventDrv.




La prima serve a generare alcune macro necessarie al provider. La seconda serve ad informare il compilatore di usare il nome di base per nominare i file che andrà a generare. Il nome di base ed altre informazioni si possono trovare in File Options, come si può vedere nella seguente immagine. In questo caso il nome di base è evntdrvEvents.




Questo sarebbe il metodo più semplice e generale per indicare la compilazione del manifesto prima della compilazione vera e propria. Nel caso di EventDrv, però, tali impostazioni sono state già inserite nel file del progetto, Eventdrv.vcxproj, e quindi nessuno dei passaggi mostrati in questa sezione è realmente necessario.



4. Invocare i metodi di tracciamento

L'header creato durante la compilazione del manifesto (in questo caso evntdrvEvents.h) deve essere incluso nel progetto del provider (EventDrv) poiché conterrà alcuni metodi, sotto forma di macro, per registrare/deregistrare il provider e generare gli eventi di tracciamento. In particolare, il metodo per registrare il provider avrà un nome tipo EventRegister<provider> dove <provider> è il nome del provider fornito nell'omonimo elemento del manifesto (quindi EventRegisterSample_Driver in questo caso).  Il metodo per deregistrare il provider funziona in modo simile ed avrà un nome tipo EventUnregister<provider> (quindi EventUnregisterSample_Driver in questo caso) . I metodi per il tracciamento avranno un nome tipo EventWrite<event> dove event è il simbolo fornito nell'omonimo elemento del manifesto. Il numero, il tipo ed il nome dei parametri è preso invece dal relativo elemento template. In verità, si può notare che i metodi di tracciamento accettano un parametro in più rispetto a quelli forniti nel manifesto (Activity) ma nel contesto di questa lezione tale parametro non è importante e si può passare sempre NULL. Oltre a tutte queste macro nel file header generato ne vengono definite anche altre, come ad esempio quelle per stabilire se un evento è attivo ed il relativo metodo di tracciamento deve essere effettivamente eseguito (in modo simile a quanto succede in WPP).



5. Compilare il driver

Avendo generato il file header e le risorse ed avendo inserito le chiamate di tracciamento nel provider, tutto è ora pronto per essere compilato. Come si può notare, analizzando il file SYS, le risorse sono effettivamente incorporate nell'eseguibile.





6. Installare il manifesto

Il secondo scopo del manifesto (il primo è quello già esaminato di generare header e risorse) è quello di fornire informazioni ai consumer. In particolare, un consumer ha necessità di sapere dove si trovano le risorse: la string table con i messaggi (Message Table nell'immagine sopra) ed il file binario con le info su come codificare e formattare le altre informazioni associate agli eventi (WEVT_TEMPLATE nell'immagine sopra). A tale scopo è necessario installare il manifesto con il tool wevtutil.exe da command prompt come si è visto nel test diagnostico di esempio ad inizio lezione.



7. Testare il supporto a ETW 

Per questo punto è sufficiente ripetere il test diagnostico di esempio effettuato ad inizio lezione per vedere se effettivamente tutto funziona. Fatto ciò si può passare ad esaminare il codice vero e proprio.




Implementazione

EvntDrv.c
#include "evntdrvEvents.h"  

Innanzitutto è necessario includere il file header generato durante la compilazione del manifesto.

#define EventDrv_NT_DEVICE_NAME     L"\\Device\\EventEtw"

 
NTSTATUS
DriverEntry(
    IN PDRIVER_OBJECT DriverObject,
    IN PUNICODE_STRING RegistryPath
    )
{
    NTSTATUS Status = STATUS_SUCCESS;
    UNICODE_STRING DeviceName;
    UNICODE_STRING LinkName;
    PDEVICE_OBJECT EventDrvDeviceObject;
    WCHAR DeviceNameString[128];
    ULONG LengthToCopy = 128 * sizeof(WCHAR);
    UNREFERENCED_PARAMETER (RegistryPath);
 
    DriverObject->DriverUnload = EventDrvDriverUnload;
    DriverObject->MajorFunction[ IRP_MJ_DEVICE_CONTROL ] = EventDrvDispatchDeviceControl;    
 
    RtlInitUnicodeString( &DeviceNameEventDrv_NT_DEVICE_NAME );
 
    Status = IoCreateDevice(
                           DriverObject,
                           0,
                           &DeviceName,
                           FILE_DEVICE_UNKNOWN,
                           0,
                           FALSE,
                           &EventDrvDeviceObject);
 
    if (!NT_SUCCESS(Status)) {
        return Status;
    }
 
    // ...
 
 
    // Registra il provider con ETW
    EventRegisterSample_Driver();
 
    //
    // Copia il nome del device (che è nel buffer interno mantenuto da UNICODE_STRNIG)
    // in un array di caratteri terminato dal carattere nullo.
    // Questo perché il tipo del parametro indicato per gli eventi nel manifesto è
    // una stringa (intesa proprio come array di caratteri terminato da quello nullo)
    // mentre la stringa conservata nel buffer di UNICODE_STRING non è obbligata a
    // terminare con un carattere nullo.
    //
    if (DeviceName.Length <= 128 * sizeof(WCHAR)) {
 
        LengthToCopy = DeviceName.Length;
 
    }
 
    RtlCopyMemory(DeviceNameString, 
                  DeviceName.Buffer, 
                  LengthToCopy);
 
    DeviceNameString[LengthToCopy/sizeof(WCHAR)] = L'\0';
 
    //
    // Evento di inizio/partenza.
    // Associato ad esso c'è la stringa "Driver Loaded" (come indicato nel manifesto),
    // il nome del device, la sua lunghezza e lo status dopo la sua creazione 
    // (come indicato dagli argomenti passati come parametri).
    // Il primo param. è Activity quindi sempre NULL.
    //
    EventWriteStartEvent(NULLDeviceName.Length, DeviceNameStringStatus);
 
 
    return STATUS_SUCCESS;
}

Si noti come gli argomenti passati a EventWriteStarEvent sono esattamente quelli mostrati nella tab Dettagli del Visualizzatore Eventi nel test diagnostico effettuato all'inizio della lezione. 
Il codice di EventDrvDriverUnload è abbastanza banale: invece di registrare si deregistra il provider e si passa l'indirizzo del device object all'evento di stop/conclusione, come previsto ed indicato nel manifesto. Si può passare dunque al codice di EventDrvDispatchDeviceControl.

NTSTATUS
EventDrvDispatchDeviceControl(
    IN PDEVICE_OBJECT pDO,
    IN PIRP Irp
    )
{
    NTSTATUS Status = STATUS_SUCCESS;
    PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocationIrp );
    ULONG ControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
    
    PAGED_CODE();
 
    UNREFERENCED_PARAMETER (pDO);
 
    Irp->IoStatus.Information = 
                            irpStack->Parameters.DeviceIoControl.OutputBufferLength;
 
    switch ( ControlCode ) {
    case IOCTL_EVNTKMP_TRACE_EVENT_A:
    {
 
        //
        // Evento di gestione per questo particolare IOCTL.
        // Associato ad esso c'è la stringa "IRP A Occurred" (come indicato nel manifesto),
        // L'unico param. è Activity, quindi sempre NULL.
        //
        EventWriteSampleEventA(NULL);
        
        Irp->IoStatus.Status = STATUS_SUCCESS;
        Irp->IoStatus.Information = 0;
 
        break;
    }
 
    default:
        Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; 
        Irp->IoStatus.Information = 0; 
        break;
    }
 
    IoCompleteRequestIrpIO_NO_INCREMENT );
 
    return Status;
}

Se si guarda il codice di EvntCtrl si noterà che questo non fa altro che interrogare il driver invocando DeviceIoControl ed indicando IOCTL_EVNTKMP_TRACE_EVENT_A come codice di controllo.




Riferimenti:


Nessun commento:

Posta un commento