Principios básicos de desarrollo de drivers en Windows - Lenguaje C

Iniciado por Littlehorse, 6 Octubre 2010, 20:27 PM

0 Miembros y 1 Visitante están viendo este tema.

Littlehorse

Este articulo también se encuentra en wiki.elhacker.net,
http://wiki.elhacker.net/programacion/cc/articulos/principios-basicos-de-desarrollo-de-drivers-en-windows




Introducción

Encontrar información sobre el desarrollo de drivers no siempre es tarea fácil, sin importar el sistema operativo del cual estemos hablando.
Normalmente, lo ideal para entender este tipo de temas correctamente es leer libros especializados en el tema u ir directamente a las fuentes. Suele ser lo correcto ya que el tema es complejo, largo y puede volverse pesado para quien se apure en entender todos los conceptos implicados.

Por ahora, esto no sera mas que una introducción, por lo tanto tratare que sea breve. Es solo un intento de dar una perspectiva general sobre este tema y con esto lograr que mas gente decida a interiorizarse en el, con suerte mas adelante podremos ver técnicas y/o conceptos mas avanzados y que la mayoría los entiendan.




subsystem, ¿que es?

Es ideal conocer a fondo las herramientas que estamos utilizando, y con esto no solo me refiero al lenguaje, si no tambien al entorno de trabajo que utilicemos.

El proceso de compilado y linkeado genera un binario adecuado para que el sistema operativo en el cual estemos pueda comprenderlo. En Windows, este formato es lo que varios conocen como PE (Portable Executable Format)
Alrededor de PE, tenemos un concepto llamado subsystem. Un subsystem, entre otras opciones incluidas en la información del header PE, describe como cargar un ejecutable que también incluye el punto de entrada (Entry Point) en el binario.




Conociendo tus herramientas.

Posiblemente muchos recién se enteran lo que es un subsystem, y esto se debe a que generalmente en la etapa de aprendizaje de lenguajes como C/C++, uno puede simplemente descargar un IDE y ponerse a trabajar. Los errores no causan tantos problemas cuando todavía no salimos del modo usuario.

Por ejemplo, las personas que utilizan Visual C++, habrán hecho ya alguna aplicacion en consola u alguna aplicacion para Windows. Al crear el proyecto, el subsystem viene predefinido, tal como /SUBSYSTEM:CONSOLE o /SUBSYSTEM:WINDOWS.

La novedad en todo esto, es que un driver es linkeado con otro tipo de subsystem, llamado NATIVE.

MSDN /SUBSYSTEM
CitarNATIVE
   Device drivers for Windows NT. If /DRIVER:WDM is specified, NATIVE is the default.




Los drivers también tienen un "main".

Por supuesto, los drivers también tienen un main, un Entry Point.
Si sabemos que es un driver lo que vamos a realizar, basta con que el main reciba los parámetros adecuados y retorne lo esperado para un driver. El sistema se encargara de cargar el driver cuando lo requiramos y darse cuenta que es un driver.

Podemos utilizar cualquier nombre como Entry Point, pero por convención en Windows se utiliza DriverEntry.
Si estas utilizando el DDK, al seleccionar que vas a construir un driver se utilizan una serie de opciones predefinidas. Esta es la razón por la cual DriverEntry se convirtió en algo similar al Entry Point oficial.

Al especificar /DRIVER, tenemos tambien otras opciones, directo de la MSDN:

Use the /DRIVER linker option to build a Windows NT kernel mode driver.

CitarThe UPONLY keyword causes the linker to add the IMAGE_FILE_UP_SYSTEM_ONLY bit to the characteristics in the output header to specify that it is a uniprocessor (UP) driver. The operating system will refuse to load a UP driver on a multiprocessor (MP) system.

The WDM keyword causes the linker to set the IMAGE_DLLCHARACTERISTICS_WDM_DRIVER bit in the optional header's DllCharacteristics field. WDM video capture was designed to resolve the problems inherent in the Video for Windows architecture.

En este caso utilizaremos:

/SUBSYSTEM:NATIVE /DRIVER:WDM -entry:DriverEntry




Conceptos básicos. Lo que debes saber.

Antes de comenzar, hay que cambiar la mentalidad de "Compilar y probar" que todos solemos adquirir mientras aprendemos a programar en modo usuario.
En el mundo de los drivers la situación cambia y lo hace en forma drástica.

Como mínimo podrías ocasionar un BSOD, y si estamos ante un driver que iniciara siempre con el sistema, tenemos un problema.
Igualmente, nada que no puedas revertir entrando en modo seguro u volviendo a configuraciones previas, pero esto solo cabe en las practicas y no en casos reales.

En conclusión, no compiles y pruebes el código de un driver al menos que entiendas a ciencia cierta que es lo que realiza, y mas aun si antes de compilarlo vas a modificar secciones del código.

Cabe recordar, que esta es solo una introducción de los conceptos básicos, por lo tanto quien quiera interiorizarse a fondo no le queda mas alternativa que revisar la MSDN u libros como "Programming the Windows Driver Model".


Interrupt Request Level


Abreviado como IRQL, partamos de la definición del DDK:

CitarThe priority ranking of an interrupt. A processor has an IRQL setting that threads can raise or lower. Interrupts that occur at or below the processor's IRQL setting are masked and will not interfere with the current operation. Interrupts that occur above the processor's IRQL setting take precedence over the current operation.

The particular IRQL at which a piece of kernel-mode code executes determines its hardware priority. Kernel-mode code is always interruptible: an interrupt with a higher IRQL value can occur at any time, thereby causing another piece of kernel-mode code with the system-assigned higher IRQL to be run immediately on that processor. In other words, when a piece of code runs at a given IRQL, the Kernel masks off all interrupt vectors with a lesser or equal IRQL value on the microprocessor.

Si, puede ser difícil de comprender a la primer lectura, por eso vamos a intentar explicarlo en términos mas sencillos.
El IRQL de un procesador ayuda a especificar como un determinado thread puede ser interrumpido. Dicho thread solo puede ser interrumpido mediante código por un nivel mas alto de IRQL en el mismo procesador.
En estas épocas, es normal ver equipos con varios procesadores. En esos casos cada procesador corre en su propio IRQL

Estos 4 niveles serán con los que tendrás que trabajar normalmente.
Normalmente las APIs llevan una pequeña nota aclarando en que nivel de IRQL necesitas estar para poder utilizar determinada API. En reglas generales, a mas alto sea el nivel, menos APIs podes utilizar.

  • Passive
  • APC (Asynchronous Procedure Calls)
  • Dispatch
  • DIRQL




Citar
1) PASSIVE_LEVEL

   Interrupts Masked Off — None.

   Driver Routines Called at PASSIVE_LEVEL — DriverEntry, AddDevice, Reinitialize, Unload routines, most dispatch routines, driver-created threads, worker-thread callbacks.

El nivel mas bajo. En este nivel corre un thread que se ejecute en modo usuario.

Citar2) APC_LEVEL

   Interrupts Masked Off — APC_LEVEL interrupts are masked off.

   Driver Routines Called at APC_LEVEL — Some dispatch routines (see Dispatch Routines and IRQLs).

Este es el nivel donde ocurren las "Asynchronous Procedure Calls". Cuando ocurre una APC, el nivel del procesador se aumenta a APC_LEVEL.

Citar3) DISPATCH_LEVEL

   Interrupts Masked Off — DISPATCH_LEVEL and APC_LEVEL interrupts are masked off. Device, clock, and power failure interrupts can occur.

   Driver Routines Called at DISPATCH_LEVEL — StartIo, AdapterControl, AdapterListControl, ControllerControl, IoTimer, Cancel (while holding the cancel spin lock), DpcForIsr, CustomTimerDpc, CustomDpc routines.

La memoria paginada no es accesible en este nivel, por lo tanto toda la memoria accesible debe ser no-paginada. Esto ocasiona que disminuya en gran parte la cantidad de APIs que podes utilizar.

Citar4) DIRQL

   Interrupts Masked Off — All interrupts at IRQL<= DIRQL of driver's interrupt object. Device interrupts with a higher DIRQL value can occur, along with clock and power failure interrupts.

   Driver Routines Called at DIRQL — InterruptService, SynchCritSection routines.

En este nivel generalmente un driver no lidia con interrupciones. Es mas que nada una forma de conocer que dispositivos tienen prioridad sobre otros.

I/O Request Packet

Abreviado IRP, basicamente es una estructura que permite a diferentes drivers comunicarse entre si y requerir tareas a finalizar.
El manejo de IRPs puede ser muy simple u demasiado complejo dependiendo de cual sea la estructura de los drivers.

El IRP también contendrá "peticiones secundarias", conocido como "IRP Stack Location". Cada driver contendrá sus propias peticiones secundarias respecto de como interpretar el IRP, denominada IO_STACK_LOCATION.

Creando el DriverEntry

Comenzando con un poco de código, el prototipo del DriverEntry:

Código (cpp) [Seleccionar]

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath);


DRIVER_OBJECT es una estructura que representa el driver.  Este objeto contiene un puntero a DEVICE_OBJECT, la cual es otra estructura que representa un dispositivo en particular.
Un solo driver puede manejar múltiples dispositivos, y tal así, DRIVER_OBJETCT mantiene una lista de todos los dispositivos que para los cuales ese driver maneja peticiones.

Registrypath es una cadena que apunta a una ubicacion en el registro donde la informacion para el driver fue guardada. El driver puede utilizar luego esta ubicacion para guardar informacion especifica.

Veamos la primer parte de nuestra rutina de entrada:

Código (cpp) [Seleccionar]

NTSTATUS DriverEntry(PDRIVER_OBJECT  pDriverObject, PUNICODE_STRING  pRegistryPath)
{
   NTSTATUS NtStatus = STATUS_SUCCESS;
   UINT uiIndex = 0;
   PDEVICE_OBJECT pDeviceObject = NULL;
   UNICODE_STRING DriverName, DosDeviceName;

   DbgPrint("DriverEntry!\n");

   RtlInitUnicodeString(&DriverName, L"\\Device\\ehn");
   RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\ehn");

   NtStatus = IoCreateDevice(pDriverObject, 0,
                             &DriverName,
                             FILE_DEVICE_UNKNOWN,//No asociado a ningun dispositivo en particular
                             FILE_DEVICE_SECURE_OPEN,
                             FALSE, &pDeviceObject);


Lo primero que se nota es la llamada a DbgPrint, esta funciona como el printf de toda la vida, pero en este caso el output va a parar al kernel debugger. Estos mensajes se pueden ver perfectamente con una herramienta como DbgView de Sysinternals

RtlInitUnicodeString inicializa una estructura del tipo UNICODE_STRING.
Esta estructura esta definida de tal modo que:


Código (cpp) [Seleccionar]
typedef struct _LSA_UNICODE_STRING {
 USHORT Length;
 USHORT MaximumLength;
 PWSTR  Buffer;
} LSA_UNICODE_STRING, *PLSA_UNICODE_STRING, UNICODE_STRING, *PUNICODE_STRING;


Donde "Lenght" es la longitud actual de la cadena, "MaximunLenght" es el tamaño máximo que la cadena puede tener, y "Buffer" es el puntero a la cadena.
Como podemos ver, esta estructura contiene el tamaño de la cadena por lo tanto no necesitamos verificar si la cadena esta terminada en NULL.

Luego de crear el dispositivo, ahora queda configurar el Driver Object para que llame a nuestro driver cuando ciertas peticiones se realicen. Estas peticiones se denominan IRP Major requests.
Existen tambien las denominadas Minor requests a las cuales nos referimos con anterioridad como peticiones secundarias, se encuentran en el stack location del IRP.

Código (cpp) [Seleccionar]
       for(uiIndex = 0; uiIndex < IRP_MJ_MAXIMUM_FUNCTION; uiIndex++)
            pDriverObject->MajorFunction[uiIndex] = ehn_UnSupportedFunction;
   
       pDriverObject->MajorFunction[IRP_MJ_CLOSE]             = ehn_Close;
       pDriverObject->MajorFunction[IRP_MJ_CREATE]            = ehn_Create;
       pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]    = ehn_DeviceControl;
       pDriverObject->MajorFunction[IRP_MJ_READ]              = ehn_Read;
       pDriverObject->MajorFunction[IRP_MJ_WRITE]             = ehn_Write;


Es decir, cuando una aplicación en modo usuario llame a algunas de estas funciones:

  • CreateFile
  • CloseHandle
  • WriteFile
  • ReadFile
  • DeviceIoControl

se llamara a tu driver.




Rutina Unload

Lo siguiente es la función de descarga del driver:

Código (cpp) [Seleccionar]
pDriverObject->DriverUnload =  ehn_Unload;

En realidad la rutina de descarga solo se necesita si queres descargar tu driver en forma dinámica, en ese caso la rutina debe ser especificada. Si no se especifica una función de descarga, el sistema no dejara que el driver sea descargado.

Esta es, por ejemplo, una forma simple de rutina de descarga:

Código (cpp) [Seleccionar]
void Example_Unload(PDRIVER_OBJECT  DriverObject)
{    
   
   UNICODE_STRING DosDeviceName;
   
   DbgPrint("Unload!!\n");
   
   RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\ehn");
   IoDeleteSymbolicLink(&DosDeviceName);

   IoDeleteDevice(DriverObject->DeviceObject);
}


Definir las otras funciones es tarea para el hogar ya que creo es la parte mas divertida de todo esto y la que supongo motivara mas para los que quieran interiorizarse en el tema.
Igualmente este ejemplo es de lo mas básico y es el que se utiliza para comprender estos conceptos por lo tanto probablemente al buscar los conceptos que faltan para definir lo necesario se encuentren con ejemplos conceptualmente similares, por lo tanto la dificultad es casi nula pero si sera bastante divertido de seguro.




Cargar y descargar el driver en forma dinámica.

Código (cpp) [Seleccionar]
int _cdecl main()
{
   HANDLE HSmng,HServ;
   SERVICE_STATUS Sstatus;

   HSmng = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
   
   printf("Cargando el driver!\n");
   
if(HSmng)
   {
       HServ = CreateService(HSmng, "ehn",
                                "ehn driver",
                                 SERVICE_START | DELETE | SERVICE_STOP,
                                 SERVICE_KERNEL_DRIVER,
                                 SERVICE_DEMAND_START,
                                 SERVICE_ERROR_IGNORE,
                                 "C:\\ehn.sys",
                                 NULL, NULL, NULL, NULL, NULL);

       if(!HServ)
       HServ = OpenService(HSmng, "ehn", SERVICE_START | DELETE | SERVICE_STOP);
       

       if(HServ)
       {
           printf("Iniciando servicio\n");

           StartService(HServ, 0, NULL);
           printf("Presione una tecla para cerrar el servicio\n");
           getchar();
           ControlService(HServ, SERVICE_CONTROL_STOP, &Sstatus);

           DeleteService(HServ);

           CloseServiceHandle(HServ);
           
       }

       CloseServiceHandle(HSmng);
   }
   
   return EXIT_SUCESS;
}


Para cualquier detalle respecto de las APIs utilizadas, referirse a la documentación del DDK y de la MSDN.





return EXIT_SUCESS


Luego de definir las funciones correspondientes en el driver, al utilizar las APIs desde modo usuario podrán comprobar que secciones de código se utilizan de su driver, incluso al definir las funciones que restan les basta con poner algunos dbgprint para luego ver con dbgview que es exactamente lo que se esta ejecutando.

Lo que no hay que hacer

Toda área tiene una lista de cosas que NO hay que hacerse, el desarrollo de drivers no es la excepción y Microsoft se encargo de resaltar este tipo de errores.

Como ejemplo, las primeras 10, hay mas en el link:

CitarNever return STATUS_PENDING from a dispatch routine without marking the I/O request packet (IRP) pending (IoMarkIrpPending).
Never call KeSynchronizeExecution from an interrupt service routine (ISR). It will deadlock your system.
Never set DeviceObject->Flags to both DO_BUFFERED_IO and DO_DIRECT_IO. It can confuse the system and eventually lead to fatal error. Also, never set METHOD_BUFFERED, METHOD_NEITHER, METHOD_IN_DIRECT or METHOD_OUT_DIRECT in DeviceObject->Flags, because these values are only used in defining IOCTLs.
Never allocate dispatcher objects from a paged pool. If you do, it will cause occasional system bugchecks.
Never allocate memory from paged pool, or access memory in paged pool, while running at IRQL >= DISPATCH_LEVEL. It is a fatal error.
Never wait on a kernel dispatcher object for a nonzero interval at IRQL >= DISPATCH_LEVEL. It is a fatal error.
Never call any function that causes the calling thread to wait directly or indirectly while executing at IRQL >= DISPATCH_LEVEL. It is a fatal error.
Never lower the interrupt request level (IRQL) below the level at which your top-level routine has been invoked.
Never call KeLowerIrql() if you haven't called KeRaiseIrql().
Never stall a processor (KeStallExecutionProcessor) longer than 50 microseconds.



Referencias

Lectura recomendada para los que quieran interiorizarse y/o iniciarse en este tema.  :)

WDK
Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
Key driver concepts
Getting started: Writing Windows drivers

Libros:
"Programming the windows driver model"

Saludos!
An expert is a man who has made all the mistakes which can be made, in a very narrow field.

Jaixon Jax


d(-_-)b

#2
Muy bueno...son temas muy interesantes, me encantan.

CitarLibros:
"Programming the windows driver model"

CHM FILE:
------------------------------------------------------------------------------------------
Programming The Microsoft Windows Driver Model, 1Ed



Descargar:
http://www.mediafire.com/?vttzfvkdoyzwn5p
------------------------------------------------------------------------------------------

CHM FILE:
Programming The Microsoft Windows Driver Model, 2Ed



Descargar:
http://depositfiles.com/files/rezuxfser
http://www.megaupload.com/?d=QQRVLP0Q
------------------------------------------------------------------------------------------

saludos...
Max 400; caracteres restantes: 366

Khronos14


Oblivi0n

Habrá que empezar a mirar un poco...
Puede ayudar tambien leer el Windows Internals? He leido por ai que era lectura recomendada para los que programen drivers  o con la API de windwows  :-\

Littlehorse

Si, ambas cosas son recomendables.
"Windows Internals" es una buen libro que plantea y explica los conceptos de una forma amigable y detallada, pero es un libro para tomárselo con calma, es muy largo y abarca demasiadas cosas.

Podes leer la 5 edición ya que abarca NT 6.0 (Vista y Server 2008) así para cuando quieras leer sobre NT 6.1 (Windows 7) no te encuentres con grandes cambios. Igualmente lo que se explica en las distintas ediciones, exceptuando las cosas especificas de cada versión de Windows, aplica para cualquier versión anterior, por lo tanto por mas que se quiera aprender sobre cualquier sistema anterior a Vista igual se puede leer la 5 edición sin problemas.

Saludos
An expert is a man who has made all the mistakes which can be made, in a very narrow field.

Karman

#6
está bueno el post aunque le faltarían algunas cosas como destacar bien las diferencias entre los distintos modos (user/kernel), aporto un par de funciones que no las encontré en la red o si las encontré no funcionaban bien (por lo menos estas me funcan a mi :P), capaz a alguno le sirve...

BOOL DDKAPI ImageFullPath(PEPROCESS eprocess,PCHAR fullname){
 BOOL ret=FALSE;BYTE buffer[sizeof(UNICODE_STRING)+MAX_PATH*sizeof(WCHAR)];
 HANDLE handle;DWORD returnedLength=0;ANSI_STRING DestinationString;
 if(NT_SUCCESS(ObOpenObjectByPointer(eprocess,OBJ_KERNEL_HANDLE,NULL,GENERIC_READ,0,KernelMode,&handle))){
   if(NT_SUCCESS(ZwQueryInformationProcess(handle,ProcessImageFileName,buffer,sizeof(buffer),&returnedLength))){
     RtlUnicodeStringToAnsiString(&DestinationString,(UNICODE_STRING*)buffer,TRUE);
     strncpy(fullname,DestinationString.Buffer,DestinationString.Length);ret=TRUE;
     fullname[DestinationString.Length]=0;RtlFreeAnsiString(&DestinationString);
   }
   ZwClose(handle);
 }
 return ret;
}

BOOL DDKAPI ImageFileName(PEPROCESS eprocess,PCHAR filename){
 CHAR sImageFullPath[MAX_PATH]={0};
 if(ImageFullPath(eprocess,sImageFullPath)){
   PCHAR pIFN=sImageFullPath,pIFP=sImageFullPath;
   while(*pIFP)if(*(pIFP++)=='\\')pIFN=pIFP;
   strcpy(filename,pIFN);return TRUE;
 }
 return FALSE;
}

DWORD GetProcessIdByHandle(HANDLE Process){
 PROCESS_BASIC_INFORMATION ProcessBasicInfo;
 if(NT_SUCCESS(ZwQueryInformationProcess(Process,ProcessBasicInformation,&ProcessBasicInfo,sizeof(PROCESS_BASIC_INFORMATION),NULL)))
   return ProcessBasicInfo.UniqueProcessId;
 return 0;
}

NTSTATUS MmAllocateUserBuffer(PVOID *BaseAddress,ULONG Size){
 return ZwAllocateVirtualMemory(NtCurrentProcess(), BaseAddress, 0L, &Size, MEM_COMMIT, PAGE_READWRITE);
}

NTSTATUS MmFreeUserBuffer(PVOID *BaseAddress){
 ULONG RegionSize = 0;
 return ZwFreeVirtualMemory(NtCurrentProcess(), BaseAddress, &RegionSize, MEM_RELEASE);
}

PSERVICE_DESCRIPTOR_TABLE DDKAPI GetServiceDescriptorShadowTableAddress(){
 PBYTE check = (PBYTE)&KeAddSystemServiceTable;
PSERVICE_DESCRIPTOR_TABLE rc=0;UINT i;
for (i=0; i<1024; i++) {
 rc = *(PPSERVICE_DESCRIPTOR_TABLE)check;
if(!MmIsAddressValid(rc)||((PVOID)rc==(PVOID)&KeServiceDescriptorTable)
||(memcmp(rc,&KeServiceDescriptorTable,sizeof(SYSTEM_SERVICE_TABLE)))){
check++;
rc = 0;
}
if (rc)
break;
}
return rc;
}


S2

PD: Esto fue compilado con GCC, con Visual Studio pueden cambiar un par de cosas como por ejemplo: &KeAddSystemServiceTable se convertiría en KeAddSystemServiceTable.

fabianjsm

Muy buen aporte, como mencionas en español es escasa la información al respecto!

MRx86

creo que tengo que aprender mas sobre sistemas operativos...  :-\
"Tengo una pregunta que a veces me tortura: ¿Estoy loco
yo, o los locos son los demas?"
- Albert Einstein