2. Introducción
2.1 Hola mundo desde el driverUna vez leído el apartado de nociones básicas, vamos a centrarnos en lo que es la programación de drivers.
Como en todo programa, tiene que haber un punto de partida, un main. El de los drivers es así:
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
//Codigo
}
Como vemos, al DriverEntry se le pasan 2 parámetros, el primero es un puntero a la estructura
DRIVER_OBJECT, más adelante veremos como usar esto. El segundo es un puntero a una cadena Unicode donde esta guardada la ruta del registrot con el que se cargó el driver.
Una vez realizadas las tareas en el DriverEntry, tenemos que retornar un valor, si no ha habido ningún error retornaremos STATUS_SUCCESS, de lo contrario, el código de error pertinente.
Cabe decir que retornando el valor no se descarga el driver, ya que para poderse descargar se tiene que crear una rutina, pecisamente esta rutina se crea a partir del puntero al primer parametro. Veamos como se hace:
void Salir(PDRIVER_OBJECT DriverObject)
{
//Codigo de salida
}
NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload=Salir;
return STATUS_SUCCESS;
}
Como vemos aquí, creamos una rutina para que al descargar el driver se llame a la rutina, dentro podemos escribir un mensaje de "Cerrando driver..." o algo asi, o en su caso, unhookear las apis, ya que si cerramos el driver si unhookear la SSDT nos va a mostrar una bonita pantalla azul, ya que se va a llamar una zona de memoria donde no hay nada.
El comando para poder escribir datos al DebugView es el comando
DbgPrint y funciona exactamente igual que el printf de C/C++. Si nos miramos la información, vemos que esta dentro de Ntddk.h, asi que la tenemos que incluir. El programa que nos dirá hola mundo al iniciarse y Adiós al cerrarse nos quedaría así:
#include <ntddk.h>
void Salir(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Adiós");
}
NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload=Salir;
DbgPrint("Hola mundo!!!");
return STATUS_SUCCESS;
}
Una vez echo esto ya podemos abrir el DebugView, le habilitamos la opción para capturar mensajes del Kernel y lo podemos ejecutar. Este ejemplo lo e probado yo mismo y pueden ejecutarlo en el PC, aunque es recomendable siempre hacer pruebas en una maquina virtual, ya que un error en el driver provocaría un reinicio de sistema.
2.2 Comunicación entre Modo kernel y modo usuarioEste tema ya es algo mas lioso. Hay varias formas de pasar información desde modo usuario a modo kernel y viceversa, la que yo voy a utilizar es el metodo que utiliza la API
DeviceIoControl.
Esto me permite enviar un mensaje desde modo usuario (MU desde ahora en adelante) hacia modo kernel (MK). Además, me retorna un puntero hacia un buffer de salida (de MK a MU) y la longitud de este, si la longitud es igual a 0 no hay datos, de lo contrario si.
La estructura donde se fundamenta la comunicación entre MU y MK es la estructura
IRP. Para poder manejarla, en el driver tendremos que crear una funcion que maneje esta estructura. Esta función se declarará igual que declaramos la función de salida en el DriverEntry. Aqui un ejemplo:
NTSTATUS Control(PDEVICE_OBJECT DeviceObject,PIRP Irp)
{
//Codigo
}
NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload=Salir;
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
DriverObject->MajorFunction[i]=Control;
return STATUS_SUCESS;
}
Aquí declaramos esa estructura como control.
El for que hay en el programa es para indicarle que todas la funciones de control las redirija a esa función. Pueden ver todas las funciones
aquí.
Para poder crear un handle desde MU hacia MK, necesitamos usar la API CreateFile, aunque antes tenemos que crear el objeto del driver. Para esto hacer esto se usa la API
IoCreateDevice.
Si leen la información de esta API, veran que se le tiene que pasar una cadena en formato unicode, esto es importante, al igual que el paso que le sigue, el de crear un vinculo para poderse abrir desde MU. Este paso se hace con la API
IoCreateSymbolicLink, al que se le pasa una cadena que sera usada en MU. Aqui un ejemplo de lo hablado hasta ahora.
//Variables globales
const WCHAR Device[]=L"\\device\\driver5";
const WCHAR sLink[]=L"\\??\\midriver5";
UNICODE_STRING Dev,lnk;
//Fin variables globales
NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
NTSTATUS s;
unsigned int i;
DriverObject->DriverUnload=Salir;
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
DriverObject->MajorFunction[i]=Control;
RtlInitUnicodeString(&Dev,Device);
RtlInitUnicodeString(&lnk,sLink);
s=IoCreateDevice(DriverObject,0,&Dev,FILE_DEVICE_UNKNOWN,0,0,&DriverObject->DeviceObject);
if (NT_SUCCESS(s))
{
s=IoCreateSymbolicLink(&lnk,&Dev);
if(!NT_SUCCESS(s))
{
IoDeleteDevice(DriverObject->DeviceObject);
DbgPrint("Error Link");
}else
DbgPrint("Cargado");
}else
DbgPrint("Error IoCreate");
return s;
}
Si no ha habido error, nos podemos centrar en la función control.
En la función de control, normalmente se analiza el tipo de mensaje que se transfiere con un IoControlCode, por ejemplo:
Hookear Datos --> 1
UnHookear --> 2
Esto se filtra mediante un switch/case. En el ejemplo usaremos un filtro para escribir que nos inventemos, yo le e llamado escribe. Para usarlo en la consola, tienen que agregar la librería winioctl.h.
NTSTATUS Control(PDEVICE_OBJECT DeviceObject,PIRP Irp)
{
NTSTATUS s=STATUS_SUCCESS;
PIO_STACK_LOCATION Stack;
unsigned int escritos;
char *iBuffer;
char *oBuffer;
char *Mensaje = "Hola desde el kernel!";
unsigned int Tam = sizeof("Hola desde el kernel!");
Stack=IoGetCurrentIrpStackLocation(Irp);
switch(Stack->Parameters.DeviceIoControl.IoControlCode)
{
case Escribe:
DbgPrint("Funcion escribir llamada");
DbgPrint("Asociando buffers...");
iBuffer = oBuffer = Irp->AssociatedIrp.SystemBuffer;
if(oBuffer && oBuffer)
{
DbgPrint("OK");
if(Stack->Parameters.DeviceIoControl.InputBufferLength !=0)
{
DbgPrint("Datos desde modo usuario: %s",iBuffer);
if(Stack->Parameters.DeviceIoControl.OutputBufferLength>= Tam)
{
DbgPrint("Enviando datos...");
RtlCopyMemory(oBuffer, Mensaje, Tam);
Irp->IoStatus.Information = Tam;
s = STATUS_SUCCESS;
}else{
DbgPrint("NO ENVIAMOS LOS DATOS");
Irp->IoStatus.Information = 0;
s = STATUS_BUFFER_TOO_SMALL;
}
}
}
else DbgPrint("ERROR");
break;
}
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return s;
}
Al inicio declaramos los buffers de entrara y salida y los mensajes que vamos a enviar a MU.
Stack=IoGetCurrentIrpStackLocation(Irp);
Esta linea sirve para poder localizar los datos que vamos a usar posteriormente, Stack esta declarada como un puntero a
IO_STACK_LOCATION.
iBuffer = oBuffer = Irp->AssociatedIrp.SystemBuffer;
Assignamos los buffers de E/S, si no hay error proseguimos.
RtlCopyMemory(oBuffer, Mensaje, Tam);
Irp->IoStatus.Information = Tam;
En la primera linea copiamos los datos al buffer de salida, en la segunda, ajustamos el tamaño del buffer de salida, esto es importante, ya que si no se configura no se transmitiran datos.
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return s;
Completamos y salimos.
Ahora vamos a ver la aplicación de consola en MU:
#include <stdio.h>
#include <windows.h>
#include <winioctl.h>
#define Escribe CTL_CODE(FILE_DEVICE_UNKNOWN, 0x00000001, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA)
int main()
{
DWORD a;
HANDLE hDevice = CreateFile("\\\\.\\midriver5",GENERIC_READ | GENERIC_WRITE ,FILE_SHARE_READ | FILE_SHARE_WRITE,0,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,0);
char iBuffer[30];
char oBuffer[1024];
if (hDevice!=INVALID_HANDLE_VALUE)
{
strcpy(iBuffer
,"Hola desde Modo Usuario!!!"); if(DeviceIoControl(hDevice,(DWORD)Escribe,iBuffer,(DWORD)sizeof(iBuffer),(LPVOID)oBuffer,(DWORD)sizeof(oBuffer),&a,NULL)==true)
{
printf("\n%d Bytes\n%s\n%s",a
,iBuffer
,oBuffer
); }else
printf("0x%08x",GetLastError
()); }
return 0;
}
La verdad es que no hay mucho que explicar de este código.
En este ejemplo se transfieren cadenas, pero se puede transferir todo lo que nosotros queramos.
Dicho esto doy por zanjado este segundo capítulo, si alguien tiene alguna duda comentenlo.
PD: Tengo que decir que parte de estos codigo son de lympex, de un codigo que publicó, lo e modificado un poco ya que usaba mas funciona, pero los nombres de las variables y eso no lo e cambiado.
Un Saludo