giovedì 25 luglio 2019

06 - Comunicazione Client / Driver: ReadFile e WriteFile

Dopo aver  discusso in maniera prettamente teorica di dispatch routine ed IRP, è arrivato il momento di mettere in pratica quanto visto finora. In questa lezione si considereranno le dispatch routine collegate agli indici IRP_MJ_READ e IRP_MJ_WRITE, che vengono invocate quando in usermode si chiamano le funzione ReadFile e WriteFile (o loro estensioni). Solitamente si usano queste dispatch routine quando il device può essere considerato come un file su cui scrivere e da cui leggere, ovvero, il driver che gestisce il device manitiene un buffer di sistema su cui scrivere o da cui leggere.




Si parte con il codice client

#include <windows.h>
#include <stdio.h>
 
int main()
{
 HANDLE hDevice = ::CreateFile(
  L"\\\\.\\RWDriver"GENERIC_READ | GENERIC_WRITE, 
  0, 
  nullptr, 
  OPEN_EXISTING, 
  0, 
  nullptr);
 if (hDevice == INVALID_HANDLE_VALUE) 
 {
  return Error("failed to open device");
 }
 
 // Read
 printf("\nTest Read\n");
 
 INT buffer;
 DWORD bytes;
 
 BOOL ok = ::ReadFile(hDevice, &buffer, sizeof(INT), &bytes, nullptr);
 if (!ok)
  return Error("failed to read");
 
 if (bytes != sizeof(INT))
  printf("Wrong number of bytes\n");
 else
  printf("System Buffer value:%d \n", buffer);
 
 // Write
 printf("Press Enter to continue\n\n");
 getchar();
 printf("Test Write\n");
 
 buffer = 123;
 
 ok = ::WriteFile(hDevice, &buffer, sizeof(INT), &bytes, nullptr);
 if (!ok)
  return Error("failed to write");
 
 if (bytes != sizeof(INT))
  printf("Wrong byte count\n");
 else
  printf("System Buffer written! User Buffer value:%d \n", buffer);
 
 // Read
 printf("Press Enter to continue\n\n");
 getchar();
 printf("Test Read\n");
 
 buffer = 0;
 
 ok = ::ReadFile(hDevice, &buffer, sizeof(INT), &bytes, nullptr);
 if (!ok)
  return Error("failed to read");
 
 if (bytes != sizeof(INT))
  printf("Wrong number of bytes\n");
 else
  printf("System Buffer value:%d \n", buffer);
 
 ::CloseHandle(hDevice);
}

int Error(const charmsg) 
{
 printf("%s: error=%d\n"msg, ::GetLastError());
 return 1;
}

CreateFile ritorna un handle al device RWDriver (creato dall'omonimo driver di cui si mostrerà il codice a breve) con permessi di lettura e scrittura. Successivamente si prova a leggere dal device (quello che si sta leggendo in realtà è il buffer di sistema mantenuto dal driver) chiamando ReadFile e passandogli l'indirizzo in user space della variabile denominata buffer e la sua dimensione. Se tutto va bene si prova a scrivere sul device (di fatto nel buffer di sistema mantenuto dal driver) passandogli ancora l'indirizzo della variabile buffer (questa volta impostata dal client con il valore 123) ed infine si verifica che la scrittura abbia avuto successo con una nuova chiamata a ReadFile. Niente di troppo complicato. Ora si passa al codice del driver RWDriver

#include <ntddk.h>
 
#define DRIVER_PREFIX "RWDriver: "
#define DRIVER_TAG 'dcba'
 
// globals
KMUTEX mutex;
INT *pBuffer;
 
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObjectPUNICODE_STRING RegistryPath) 
{
 UNREFERENCED_PARAMETER(RegistryPath);
 
 DriverObject->DriverUnload = RWDriverUnload;
 DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverObject->MajorFunction[IRP_MJ_CLOSE] = RWDriverCreateClose;
 DriverObject->MajorFunction[IRP_MJ_READ] = RWDriverRead;
 DriverObject->MajorFunction[IRP_MJ_WRITE] = RWDriverWrite;
 
 UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\RWDriver");
 UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\RWDriver");
 PDEVICE_OBJECT DeviceObject = NULL;
 NTSTATUS status = STATUS_SUCCESS;
 BOOLEAN symLinkCreated = FALSE;
 
 KeInitializeMutex(&mutex, 0);
 
 pBuffer = ExAllocatePoolWithTag(PagedPoolsizeof(INT), DRIVER_TAG);
 if (pBuffer == NULL) 
 {
  KdPrint(("Failed to allocate memory\n"));
  return STATUS_INSUFFICIENT_RESOURCES;
 }
 
 RtlZeroMemory(pBuffer, sizeof(INT));
 
 do 
 {
  status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
  if (!NT_SUCCESS(status)) 
  {
   KdPrint((DRIVER_PREFIX "failed to create device (0x%08X)\n", status));
   break;
  }
 
  status = IoCreateSymbolicLink(&symLink, &devName);
  if (!NT_SUCCESS(status)) 
  {
   KdPrint((DRIVER_PREFIX "failed to create symbolic link (0x%08X)\n", status));
   break;
  }
  symLinkCreated = TRUE;
 
 } while (FALSE);
 
 if (!NT_SUCCESS(status)) 
 {
  if (symLinkCreated)
   IoDeleteSymbolicLink(&symLink);
  if (DeviceObject)
   IoDeleteDevice(DeviceObject);
 }
 
 return status;
}

NTSTATUS CompleteIrp(PIRP IrpNTSTATUS statusULONG_PTR info) 
{
 Irp->IoStatus.Status = status;
 Irp->IoStatus.Information = info;
 IoCompleteRequest(IrpIO_NO_INCREMENT);
 return status;
}

void RWDriverUnload(PDRIVER_OBJECT DriverObject) 
{
 UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\RWDriver");
 IoDeleteSymbolicLink(&symLink);
 IoDeleteDevice(DriverObject->DeviceObject);
 
 ExFreePool(pBuffer);
}

NTSTATUS RWDriverCreateClose(PDEVICE_OBJECT pDOPIRP pIrp) 
{
 UNREFERENCED_PARAMETER(pDO);
 
 return CompleteIrp(pIrpSTATUS_SUCCESS, 0);
}

Il codice di DriverEntry non dovrebbe, a questo punto del corso, rappresentare una novità. Si inizializzano i vari puntatori a funzione (DriverUnload e le Dispatch Routine) dell'oggetto driver e si creano device e symbolic link. L'unica vera novità è rappresentata dal buffer di sistema definito a livello globale che necessita di una qualche forma di sincronizzazione, dato che niente vieta a più client di leggerlo e scriverlo allo stesso tempo. Ci sono tanti modi di sincronizzare i thread dei client ma in questo caso si è optato per l'utilizzo di un mutex, il cui funzionamento ed utilizzo è simile a quello dei mutex in usermode. Infine, il codice relativo a IoCompleteRequest è stato incapsulato in CompleteIrp per comodità. Si passa ora alle Dispatch Routine che gestiscono le richieste di lettura e scrittura.

NTSTATUS RWDriverRead(PDEVICE_OBJECT pDOPIRP pIrp) 
{
 UNREFERENCED_PARAMETER(pDO);
 
 KeWaitForMutexObject(&mutex, ExecutiveKernelModeFALSENULL);
 NTSTATUS status = STATUS_INVALID_USER_BUFFER;
 PIO_STACK_LOCATION stack;
 ULONG len = 0;
 
 __try
 {
  stack = IoGetCurrentIrpStackLocation(pIrp);
  len = stack->Parameters.Read.Length;
  if (len == 0)
   return CompleteIrp(pIrpSTATUS_INVALID_BUFFER_SIZE, 0);
 
  PVOID buffer = pIrp->UserBuffer;
  if (!buffer)
   return CompleteIrp(pIrpSTATUS_ACCESS_VIOLATION, 0);
 
  __try
  {
   RtlCopyMemory(buffer, pBuffer, len);
   status = STATUS_SUCCESS;
  }
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
   status = STATUS_ACCESS_VIOLATION;
  }
 }
 __finally
 {
  KeReleaseMutex(&mutex, FALSE);
  return CompleteIrp(pIrp, status, len);
 }
}

RWDriverRead gestisce le richieste in lettura del client. Prima di tutto il thread corrente acquisisce il mutex. Dopodiché, per quanto visto nella lezione precedente (si veda [1]), si recupera lo IO_STACK_LOCATION corrente tramite IoGetCurrentIrpStackLocation. Si usa la struttura Parameters.Read di IO_STACK_LOCATION per recuperare alcune info sulla richiesta in quanto quest'ultima verrà gestita dalla dispatch routine relativa all'indice IRP_MJ_READ. Il campo UserBuffer di IRP contiene l'indirizzo, in user space, del buffer passato dal client ed in cui il driver può scrivere. Notare che il thread che gestirà la richiesta in kernelmode sarà, almeno in questo esempio di codice, quello del client che ha fatto richiesta in usermode quindi anche il driver avrà accesso al buffer in user space dato che condividerà lo stesso spazio virtuale del client. RtlZeroMemory e RtlCopyMemory non sono altro che wrapper di funzioni come memset e memcpy (implementate nel kernel e da non confondere con le omonime funzione implementate dalla libreria di runtime del C e che per questo non possono essere utilizzate in kernelmode). Si noti l'uso di blocchi __try\__except e __try\__finally. Il blocco __try\__exception in quella posizione serve per evitare casi in cui il client passa un indirizzo non valido per il buffer (si pensi, ad esempio, ad una operazione di lettura con un indirizzo random in kernel space per il buffer: disastro e possibile crash di sistema). Il blocco __try\__finally, invece, serve per assicurarsi che il mutex venga sempre rilasciato, anche nel caso venisse lanciata una eccezione. Infine, è altrettanto importante notare che il campo IoStatus.Information di IRP è impostato al numero di byte scritti, che è uguale alla dimensione del buffer in user space passata a RtlCopyMemory.

NTSTATUS RWDriverWrite(PDEVICE_OBJECT pDOPIRP pIrp) 
{
 UNREFERENCED_PARAMETER(pDO);
 
 KeWaitForMutexObject(&mutex, ExecutiveKernelModeFALSENULL);
 NTSTATUS status = STATUS_INVALID_USER_BUFFER;
 PIO_STACK_LOCATION stack;
 ULONG len = 0;
 
 __try
 {
  stack = IoGetCurrentIrpStackLocation(pIrp);
  len = stack->Parameters.Write.Length;
  if (len == 0)
   return CompleteIrp(pIrpSTATUS_INVALID_BUFFER_SIZE, 0);
 
  PVOID buffer = pIrp->UserBuffer;
  if (!buffer)
   return CompleteIrp(pIrpSTATUS_ACCESS_VIOLATION, 0);
 
  __try
  {
   RtlCopyMemory(pBuffer, buffer, len);
   status = STATUS_SUCCESS;
  }
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
   status = STATUS_ACCESS_VIOLATION;
  }
 }
 __finally
 {
  KeReleaseMutex(&mutex, FALSE);
  return CompleteIrp(pIrp, status, len);
 }
}

RWDriverWrite funziona praticamente allo stesso modo, ma per richieste in scrittura del client, dove il driver può leggere il buffer in user space ed usare il relativo valore per scrivere il buffer di sistema. Alla fine dei conti le uniche differenza con RWDriverRead sono l'inversione dei primi due argomenti in RtlCopyMemory e l'uso della struttura Parameters.Write di IO_STACK_LOCATION al posto di Parameters.Read.
Se durante l'esecuzione del client sul Guest dovessero apparire messaggi su questa o quella DLL mancante assicuratevi prima di tutto di installare il Visual C++ Redistributable sul Guest. Se dovessero apparire messaggi specifici riguardo VCRUNTIME140D.DLL e\o UCRTBASED.DLL cercateli sull'Host e copiateli nel Guest, nella stessa posizione.





Codice sorgente:
RWDriver.zip



Riferimenti:
[1] 05 - Comunicazione Client / driver: IRP
[2] Windows Kernel Programming - Pavel Yosifovich

Nessun commento:

Posta un commento