3psilon website

slogan


Vous êtes ici : Accueil » Articles » Programmation

>>Conception d’un driver NT

27 juillet 2006
Auteur(e) : 

Présentation

Il faut tout d’abord se souvenir que Windows NT5 a été conçu pour pouvoir exécuter divers types d’applications : les « traditionnels » Win32, Win16 et DOS, certes, mais également OS/2 et POSIX6.

Pour permettre cela, une structure bien particulière a été adoptée : les modes fondamentaux.

Modes fondamentaux

  • L’architecture de NT repose sur deux modes fondamentaux :
    • Le mode user (utilisateur). Celui où s’exécutent les applications. Il utilise le niveau de privilège 3 du processeur et ne permet pas de s’adresser directement aux périphériques. L’accès aux données du système est limité et contrôlé.
    • Le mode kernel (noyau). Celui dans lequel s’exécute le système d’exploitation. Il utilise le niveau de privilège 0 du processeur, où, pour faire simple, tout est permis ! Il s’agit là de la première étape : une séparation franche entre les appli- cations et l’OS, ce qui assure, en théorie, une meilleure robustesse du système. Un réel progrès par rapport à Windows 95/98/Me !
  • Le coeur : le « cœur » de NT est constitué de trois parties
    • L’exécutif. Contient tous les services et fonctions « de base » du système d’exploitation. Par exemple, la gestion de la mémoire ou celle des threads et des processus.
    • Le kernel. Y résident les fonctions dites de « bas niveau », comme la gestion des exceptions et des interruptions.
    • Les drivers. Ce terme sert à désigner les drivers de périphériques, mais aussi ceux de réseau ou de système de fichier.

C’est la seconde étape : la « segmentation » des différents composants du système d’exploitation en différentes couches, ce qui, entre autre, permet de faciliter la portabilité vers différentes plates-formes matérielles. Service NT

Les applications ne peuvent pas appeler directement les services de NT.

Elles « passent » par un sous-système d’environnement (SSE), dont le rôle principal est de mettre à leur disposition une API9 pour accéder, de manière indirecte, aux fonctions natives du système d’exploitation. Windows NT est livré avec trois SSEs : Win32, celui dit « natif10 », OS/2 et POSIX. Un SSE actif est associé à un processus système.

Par exemple, celui de Win32 est le fameux csrss.exe qui apparaît dans la liste des processus du gestionnaire des tâches (appelé par CTRL+ALT+DEL). Cette ultime étape permet de disposer, via les SSEs, de plusieurs « OS ».

La mémoire

La mémoire gérée par Windows se divise en deux segments principaux.

-  Mémoire système (kernel)
-  Mémoire utilisateur (applications)

L’espace total est de 4 Go et est divisé en 2. L’espace d’adressage est de 32 bits.

Cliquez sur l’image pour l’agrandir

Le développement

Le développement d’un driver.

Tout d’abord vous devez disposez d’un environnement de développement (DDK — Driver Developpment Kit) répondant à votre système actuel.

Visitez le site de Microsoft.

Il existe différent type de drivers :

-  Device Object : Object mode kernel. Image binaire d’un driver chargé dans la zone mémoire du noyau windows.
-  Device Object : Object logique kernel (physique, logique, virtuelle) crée par un driver.
-  Controller Object : Object logique mode kernel avec plusieurs device sur un seul bus (ex : scsi).
-  Adapter Object : Object mode kernel pour differents adaptateurs : isa, eisa, pcî.

Tout ces objets sont contrôlés par des Entrées/Sorties : les IRP (Input Output Request Packet).

Il existe 28 types différents, mais 8 sont usuellement utilisées :

-  IRP_MJ_CREATE
-  IRP_MJ_CLOSE
-  IRP_MJ_READ
-  IRP_MJ_WRITE
-  IRP_MJ_PNP
-  IRP_MJ_POWER
-  IRP_MJ_DEVICE_CONTROL
-  IRP_MJ_SYSTEM_CONTROL

Les routines du driver

-  DriverEntry : C’est le point d’entrée du driver, qui équivaut au "main" d’un programme en C. Ici sera crée la device du driver, ainsi qu’un lien symbolique entre le nom du drivers et son nom usuel. C’est également dans cette routine que les fonctions supportées du driver seront définies (DriverObject->MajorFunction). Ou DriverObject est le premier paramètre de la routine. C’est un block mémoire alloué et partiellement initialisé par le systeme. Il contient les adresses des fonctions supportées par le driver.


NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath )
{

PDEVICE_OBJECT deviceObject = NULL;
NTSTATUS status;

UNICODE_STRING uniNtNameString;
UNICODE_STRING uniWin32NameString;

PDEVICE_EXTENSION extension;

DbgPrint( ("Entered to the driver!\n") );

//
// Crée une version caractere unicode
// du driver
//

RtlInitUnicodeString(&uniNtNameString, NT_DEVICE_NAME );

//
// Creation de l'objet Device
//
status = IoCreateDevice(
DriverObject,
// Device extension
sizeof (DEVICE_EXTENSION),
// Nom du driver
&uniNtNameString,
// Type du Driver
FILE_DEVICE_UNKNOWN,
// Device non standart
0,
// Device exclusive
FALSE,
&deviceObject
);

if ( NT_SUCCESS(status) )
{

//
// Creation des points d'entree
// des fonctions supporté par le driver
//
DriverObject->MajorFunction[IRP_MJ_CREATE]= DriverOpen;
DriverObject->MajorFunction[IRP_MJ_CLOSE]= DriverClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]= DriverDeviceControl;
DriverObject->DriverUnload = DriverUnload;

// Initialisation de l'extension
extension = (PDEVICE_EXTENSION)deviceObject->DeviceExtension;
extension->DeviceObject = deviceObject;
extension->DriverObject = DriverObject;


DbgPrint( ("Driver prêt!\n") );

//
// Definition de la methode
// employé pour utiliser les buffers
//
deviceObject->Flags |= DO_DIRECT_IO;


//
// Crée le nom de la device
// pour le systeme ou
// DOS_DEVICE_NAME est un define
//
RtlInitUnicodeString(&uniWin32NameString,DOS_DEVICE_NAME );

//
// Création d'un lien symbolique entre
// le nom réel du drivers et
// le nom utilisé par le systeme.
//
status =
IoCreateSymbolicLink(&uniWin32NameString,&uniNtNameString );

if (!NT_SUCCESS(status))
{
DbgPrint( ("Impossible de créer le lien symboliques\n") );

IoDeleteDevice(DriverObject->DeviceObject );
}
else
{
DbgPrint(
("Tout est initialisé!\n") );
}
}
else
{
DbgPrint( ("Impossible de créer la device\n") );
}
return status;
}


-  DriverUnload : Décharge le driver. Supprime la "device" et supprime le lien symbolique si il a été préalablement créé.


VOID DriverUnload(
IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING uniWin32NameString;
PDEVICE_EXTENSION extension;

extension =DriverObject->DeviceObject->DeviceExtension;
DbgPrint( ("Driver déchargé!!\n") );

RtlInitUnicodeString(&uniWin32NameString,DOS_DEVICE_NAME );

//
// Suppression du lien
//
IoDeleteSymbolicLink(&uniWin32NameString );

//
// Suppression de la device
//
IoDeleteDevice(DriverObject->DeviceObject );
}


-  IO routine : C’est cette routine qui dispatche les entrées/sorties(IRP) du driver. Elle est appellé de l’api DeviceIoControl de l’application cliente.


NTSTATUS halioDeviceControl(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp )
{
NTSTATUS ntStatus;
// Structure representant
// le stack de l'Irp (requete)
PIO_STACK_LOCATION irpStack;
PDEVICE_EXTENSION extension;

// Pointeur du Buffer de l'Irp
PULONG ioBuffer;

// Code de l'Irp
ULONG ioControlCode;

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;

// Initialisation du stack
irpStack = IoGetCurrentIrpStackLocation(Irp);

extension =DeviceObject->DeviceExtension;

// Initalisation de notre buffer
ioBuffer = Irp->AssociatedIrp.SystemBuffer;

// initialisation du code de la requete
ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;

// Suivant le Code
switch (ioControlCode) {

case xxxxxx: {

break;
}
case xxxxxx: {

break;
}

default:{
Irp->IoStatus.Status
= STATUS_INVALID_PARAMETER;
break;
}
}

// Mise a jour du statut de la requete
ntStatus = Irp->IoStatus.Status;
// Irp complete
IoCompleteRequest(Irp, IO_NO_INCREMENT);

return ntStatus;
}


Je ne vais pas toutes les décrirent ici, d’une part car cela serait trop long, et d’autre part car je ne les connais pas toutes :)

Vous pouvez définir vos propres codes IRP en utilisant la macro CTL_CODE. Attention toutefois à ne pas définir une code existant.


#define IOCTL_Device_Function CTL_CODE(
DeviceType,
FunctionCode,
TransferType,
RequiredAccess)


Les buffers

Pour dialoguer avec le driver, vous avez le choix entre différent type de buffer. Le dialoque sera réaliser via le client par l’api DeviceIoControl :


BOOL DeviceIoControl(
// handle to device of interest
HANDLE hDevice,
// control code of operation to perform
DWORD dwIoControlCode,
// pointer to buffer to supply input data
LPVOID lpInBuffer,
// size of input buffer
DWORD nInBufferSize,
// pointer to buffer to receive output data
LPVOID lpOutBuffer,
// size of output buffer
DWORD nOutBufferSize,
// pointer to variable to receive output byte
LPDWORD lpBytesReturned,
// pointer to overlapped structure
// for asynchronous operation
LPOVERLAPPED lpOverlapped
);


-  METHOD_BUFFERED : Spécifie que le l’information sera transféré à l’aide de tampons. Cette méthode est normalement utilisée pour transmettre de petites quantités de données sur demande. La majorité des codes de contrôle pour les pilotes intermédiaires utilisent cette méthode.

-  METHOD_IN_DIRECT / METHOD_OUT_DIRECT : Spécifie un mode de transfert direct qui est normalement utilisé pour écrire et lire de grande quantités d’informations en utilisant les techniques d’adressage de mémoire directe (DMA) ou tout autres méthode permettant un transfère rapide des données. L’appelant de la méthode doit spécifier METHOD_IN_DIRECT lorsqu’il veut transférer des données vers le pilote et METHOD_OUT_DIRECT lorsqu’il veut recevoir des données du pilote.

-  METHOD_NEITHER : Spécifie que le transfère de données ne sera pas fait à l’aide de tampons ou de transfert direct à la mémoire. Les IRPs vont fournir les adresses virtuelles des tampons d’entrée et de sortie du mode utilisateur qui seront spécifiés par l’appel de la fonction DeviceIoControl ou IoBuildDeviceIoControlRequest. Ces adresses virtuelles ne seront pas validées ni paginées par le pilote. Cette méthode ne peut être utilisée que si le pilote peut garantir qu’elle sera exécutée dans le même contexte que l’application ou le thread qui envoie la requête d’entrée/sortie. Seul les pilotes de haut niveau dans le mode noyau peuvent garantir qu’ils rencontreront cette condition.

Donc, cette méthode n’est pas souvent utilisée pour les codes de contrôles passés aux pilotes de bas niveau. Les pilotes doivent déterminer s’ils doivent donner un accès avec tampon ou direct aux données lorsqu’ils reçoivent cette requête. Il devra possiblement mettre sous verrou les tampons lorsqu’il transfèrera de l’information à ceux-ci. Tout ceci dans le but de prévenir que le logiciel appelant accède aux tampons lorsqu’un transfert de données est effectué.

Exemple d’utilisation d’un driver en utilsant la méthode METHOD_BUFFERED, voir les documents liés.

la Table des appels systèmes — SSDT

Lorsqu’un processus sous Windows NT fait une requête pour écrire sur un fichier, ouvrir un fichier, lire la base de registre, etc. le programme doit appeler une des fonctions fournies par l’API de Windows, comme WriteFile().

Ces fonctions API sont normalement composées de fonctions plus primitives se trouvant dans NTDLL.DLL, comme NtWriteFile().

Ces fonctions sont elles-mêmes de simples interfaces aux services que peut fournir le noyau du système d’exploitation (kernel).

Ces fonctions noyaux ont le même nom que leur homonyme dans NTDLL.DLL, sauf qu’elles commencent par Zw à la place de Nt, comme par exemple, ZwWriteFile().

WriteFile -> NtWriteFile -> ZwWriteFile

C’est la base même du fonctionnement des antivirus et rootkits. (Voir l’article sur les rootkits)

Ils placent des crochets d’interceptions(hook) directement dans le noyau windows afin d’analyser/modifier les requêtes.

Concrètement ils modifient l’adresse des fonctions dans la SDT en spécifiant leurs propres adresses. Ensuite suivant le but de la routine la véritable fonction est appellée ou pas.

Installer un driver

Pour que le driver puisse etre utilisé par une application cliente, il faut que celui ci soit enregistré auprés du système. Il faut donc crée un service qui représentera le driver.

Plusieurs solutions s’offrent à vous. Vous pouvez utiliser un logiciel qui permet d’installer un driver (en créant le service) ou le coder en utilisant les routines d’accès à la base de registre. Vous pouvez également générer le fichier .inf à l’aide de geninf (prévu dans la pack ddk) et faire un regini file.ini en mode console. Utilisation

Pour dialoguer avec le driver il faut créer un espace mémoire, il sera crée comme si le client créé un fichier via CreateFile :


HANDLE hDevice;

hDevice = CreateFile ("\\\\.\\NomDriver",
GENERIC_READ | GENERIC_WRITE,
0,NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,NULL);


Il est possible d’utiliser les fonctions ReadFile et WriteFile pour lire/écrire(dialoguer) avec le driver. Néanmoins, la plupart des accès aux drivers ce font via la routine DeviceIoControl :


BOOL DeviceIoControl(
// handle to device of interest
HANDLE hDevice,
// control code of operation to perform
DWORD dwIoControlCode,
// pointer to buffer to supply input data
LPVOID lpInBuffer,
// size of input buffer
DWORD nInBufferSize,
// pointer to buffer to receive output data
LPVOID lpOutBuffer,
// size of output buffer
DWORD nOutBufferSize,
// pointer to variable to receive output byte
LPDWORD lpBytesReturned,
// pointer to overlapped structure
// for asynchronous operation
LPOVERLAPPED lpOverlapped
);


Cet article est une première approche et fait parfois référence au très bon document “API Native” de Arnold McDonald, il est donc conseillé de lire les documents liés.

Références

ntkernel.com

wd-3.com

windowsitlibrary.com

osr.com

osronline.com

Formuler un commentaire


3psilon (c) 2003

[W3C CSS Validator] [W3C XHTML Validator] [W3C WAI AAA]