mercoledì 31 luglio 2019

07 - Comunicazione Client / Driver: DeviceIoControl

Se l'operazione da eseguire sul device non è la classica lettura e scrittura, intesa come nella precedente lezione (si veda [1]), si ricorre alla dispatch routine relativa all'indice IRP_MJ_DEVICE_CONTROL e alla relativa chiamata DeviceIoControl in usermode. Questa dispatch routine è particolare in quanto usa dei codici di controllo creati ad hoc ed una istruzione switch per distinguere e scegliere tra varie possibili operazioni da intraprendere. Un'altra particolarità è che sono disponibili due buffer utente, uno di input ed uno di output (entrambi dal punto di vista del driver), rispetto al singolo buffer utente disponibile per le dispatch routine relative agli indici IRP_MJ_READ e IRP_MJ_WRITE, dove il buffer era di output ed input, rispettivamente (sempre dal punto di vista del driver). Due similitudini con il caso precedente, che è importante sottolineare, sono:

- la possibilità di accedere ai buffer in user space da parte del driver poiché il thread che gestirà la richiesta in kernelmode sarà lo stesso di quello che ha effettuato la richiesta in usermode.

- il blocco __try\__exception per catturare eventuali eccezioni (ad esempio in caso il client fornisse un indirizzo non valido per il buffer).

In questa lezione vedremo un driver che permette di incrementare il valore di una variabile fornita dal client.




Il codice di controllo attraverso cui la dispatch routine distingue tra varie operazioni deve essere usato anche dal client, nella chiamata a DeviceIoControl, per passare tale informazione al driver. Per questo motivo è consigliabile inserire la definizione di tale codice di controllo in un file header separato ed utilizzabile facilmente in altri progetti. E' interessante notare che tale codice di controllo è in realtà una sequenza contigua di vari codici che conservano altrettanti tipi informazioni. Per il momento ci interessa solo l'informazione che indentifica l'operazione da intraprendere. Il formato del codice di controllo nella sua interezza è illustrato nell'immagine sotto.




Si tratta di un intero a 32-bit dove i bit

$[0, 1]$ Indicano il tipo di trasferimento dei dati, da client a driver, da usare nell'operazione identificata da tale codice di controllo. Non importante al momento, si daranno maggiori dettagli nella prossima lezione.

$[2, 13]$ Indentificano l'operazione da intraprendere. Il vero cuore dell'intero codice di controllo. Valori minori di $0x800$ sono riservati. Il bit 13 è riservato.

$[14, 15]$ Indicano il tipo di accesso che il client deve richiedere quando chiama CreateFile per ottenere l'handle al device. L'I/O Manager creerà un IRP solo se la richiesta da parte del client è valida (nel senso che il tipo di accesso al device richiesto dal client con CreateFile è compatibile con quello indicato nel codice di controllo passato a DeviceIoControl).

$[16, 31]$ Identificano il tipo di device che fornisce l'operazione identificata da tale codice di controllo. Valori minori di $0x8000$ sono riservati. Il bit 31 è riservato.


La creazione di tali codici di controllo a 32-bit è facilitata dalla macro

#define CTL_CODE(DeviceType, Function, Method, Access) (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))

che shifta i vari sottocodici alla posizione desiderata. Fatte queste premesse si può partire con l'esempio di codice

#pragma once
 
#define DICDRIVER_DEVICE 0x8000
#define IOCTL_DICDRIVER_INC_VALUE CTL_CODE(DICDRIVER_DEVICE, 0x800, METHOD_NEITHERFILE_ANY_ACCESS)

IOCTL_DICDRIVER_INC_VALUE è il codice di controllo che verrà usato da client e driver per identificare l'operazione di incremento della variabile. DICDRIVER_DEVICE ha valore $0x8000$ ma è possibile usare anche il valore riservato FILE_DEVICE_UNKNOWN (definito in ntddk.h e wdm.h). Si è usato il valore $0x800$ per identificare l'operazione di incremento della variabile fornita dal client. In verità è l'unica operazione eseguibile al momento ma è possibile aggiungerne altre definendo altri codici passando lo stesso valore per il parametro DeviceType ma valori diversi per il parametro Function della macto CTL_CODE. Per quanto riguarda il parametro Access si è passato FILE_ANY_ACCESS così da permettere a tutti i client di eseguire tale operazione. Infine, si è passato METHOD_NEITHER che indica che non si vuole nessun metodo particolare nel trasferimento dei dati da client a server. Questo significa che il driver accederà direttamente ai buffer di input ed output forniti dal client, senza supporto da parte dell'I/O Manager (il discorso verrà ripreso nella prossima lezione). Ora il codice client

#include <windows.h>
#include <stdio.h>
#include "..\DICDriver\DICDriverCommon.h"
 
int Error(const charmsg)
{
 printf("%s: error=%d\n"msg, ::GetLastError());
 return 1;
}
 
int main()
{
 ULONG myValue = 0;
 printf("myValue: %ld\n", myValue);
 
 printf("Press Enter to continue...\n");
 getchar();
 
 HANDLE hDevice = ::CreateFile(
    L"\\\\.\\DICDriver"GENERIC_READ | GENERIC_WRITE, 
    0, 
    nullptrOPEN_EXISTING, 
    0, 
    nullptr);
 if (hDevice == INVALID_HANDLE_VALUE)
 {
  return Error("failed to open device");
 }
 
 DWORD returned;
 BOOL success = DeviceIoControl(
    hDevice, 
    IOCTL_DICDRIVER_INC_VALUE, 
    &myValue, sizeof(ULONG), 
    &myValue, sizeof(ULONG), 
    &returned, 
    nullptr);
 if (success)
 {
  printf("myValue increment succeeded!\n");
  // returned uguale a IRP.IoStatus.Information
  printf("returned: %d bytes\n", returned);
  printf("myValue: %ld", myValue);
 }
 else
  Error("myValue increment failed!\n");
 
 ::CloseHandle(hDevice);
}

L'unica novità riguarda il metodo DeviceIoControl

BOOL DeviceIoControl(
 HANDLE       hDevice,
 DWORD        dwIoControlCode,
 LPVOID       lpInBuffer,
 DWORD        nInBufferSize,
 LPVOID       lpOutBuffer,
 DWORD        nOutBufferSize,
 LPDWORD      lpBytesReturned,
 LPOVERLAPPED lpOverlapped
);

hDevice è l'handle al device ottenuto da CreateFile.

dwIoControlCode è il codice di controllo che identifica l'operazione fornita dal driver e che si vuole eseguire.

lpInBuffer è l'indirizzo del buffer di input (da dove il driver leggerà i dati).

nInBufferSize è la dimesione del buffer di input.

lpOutBuffer è l'indirizzo del buffer di output (dove il driver scriverà).

nOutBufferSize è la dimesione del buffer di output.

lpBytesReturned è l'indirizzo di una variabile che conterrà il numero di byte scritti dal driver nel buffer di output.

lpOverlapped permette di eseguire l'operazione in modo asincrono, non importante al momento.

Notare che il client non è obbligato a fornire indirizzi validi per entrambi i buffer; si può passare NULL o nullptr per uno di essi (dipende dal tipo di operazione). Per quanto riguarda il codice del driver invece

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObjectPUNICODE_STRING RegistryPath)
{
 
 // ...
 
  DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DICDriverDeviceIoControl;
 
 // ...
 
}

In DriverEntry l'unica novità è l'impostazione della dispatch routine relativa all'indice IRP_MJ_DEVICE_CONTROL. Il resto del codice è sempre lo stesso fino alla definizione della dispatch routine

NTSTATUS DICDriverDeviceIoControl(PDEVICE_OBJECT pDOPIRP Irp) 
{
 UNREFERENCED_PARAMETER(pDO);
 
 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
 
 NTSTATUS status = STATUS_SUCCESS;
 ULONG len = 0;
 
 switch (stack->Parameters.DeviceIoControl.IoControlCode) 
 {
  case IOCTL_DICDRIVER_INC_VALUE:
  {
   if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(ULONG) ||
    (len = stack->Parameters.DeviceIoControl.OutputBufferLength) < sizeof(ULONG)) 
   {
    status = STATUS_BUFFER_TOO_SMALL;
    break;
   }
 
   PULONG iBuffer = (PULONG)stack->Parameters.DeviceIoControl.Type3InputBuffer;
   if (iBuffer == NULL) {
    status = STATUS_INVALID_PARAMETER;
    break;
   }
 
   PULONG oBuffer = (PULONG)Irp->UserBuffer;
   if (oBuffer == NULL) {
    status = STATUS_INVALID_PARAMETER;
    break;
   }
      
   __try 
   {
    if (*iBuffer < (ULONG)(-1))
    {
     *oBuffer = ++(*iBuffer);
     break;
    }
    else
     status = STATUS_INVALID_USER_BUFFER;
   }
   __except (EXCEPTION_EXECUTE_HANDLER) 
   {
    // something wrong with the buffer
    status = STATUS_ACCESS_VIOLATION;
   }
 
   break;
  }
  default:
  {
   status = STATUS_INVALID_DEVICE_REQUEST;
   break;
  }
 }
 
 // Irp->IoStatus.Information ritornato da I/O Manager in 
 // lpBytesReturned fornito da client in DeviceIoControl
 return CompleteIrp(Irp, status, len);
}

La principale novità è rappresentata dall'uso della struttura Parameters.DeviceIoControl di IO_STACK_LOCATION per recuperare alcune informazioni utili sulla richiesta, tra cui: indirizzo del buffer di input fornito dal client (attraverso il campo Type3InputBuffer) e dimensioni dei buffer di input ed output (attraverso i membri InputBufferLength ed OutputBufferLength). L'indirizzo del buffer di output, invece, si trova nel campo UserBuffer di IRP. Da notare che il campo IoStatus.Information di IRP deve essere impostato con il numero di byte scritti nel buffer di output (se fornito, zero altrimenti). Questo valore sarà poi passato dall'I/O Manager al client attraverso parametro lpBytesReturned di DeviceIoControl.


Codice sorgente:
DICDriver.zip


Riferimenti:
[1] 06 - Comunicazione Client \ Driver: ReadFile e WriteFile
[2] Windows Kernel Programming - Pavel Yosifovich

Nessun commento:

Posta un commento