Salut l'équipe
Aujourd'hui on attaque un gros morceau du développement driver Windows : les IRP, pour I/O Request Packet.
C'est un sujet central, parce qu'un driver Windows ne sert pas juste à exister en mémoire après son
Code: Select all
DriverEntryCes requêtes sont encapsulées par Windows dans une structure semi-documentée appelée :
Code: Select all
IRP
Dans un driver WDM, presque tout finit par tourner autour de ça :
- un programme user-mode appelle ,
Code: Select all
CreateFile,Code: Select all
ReadFileouCode: Select all
WriteFile;Code: Select all
DeviceIoControl - l'I/O Manager crée un IRP ;
- l'IRP descend dans la pile de drivers ;
- un driver traite la requête, ou la passe au driver du dessous ;
- l'IRP remonte quand il est complété ;
- l'I/O Manager rend le résultat au programme appelant.
1. C'est quoi un IRP ?
Un IRP, c'est une structure allouée en kernel-mode, en général depuis du non-paged pool.
Pourquoi du non-paged pool ?
Parce qu'un IRP peut être manipulé dans des contextes où le paging n'est pas autorisé, ou par des composants noyau qui ne doivent pas dépendre d'une mémoire paginable. Un IRP doit rester accessible tant que la requête existe.
Un IRP peut être créé par plusieurs entités :
- l'I/O Manager ;
- le Plug & Play Manager ;
- le Power Manager ;
- parfois par un driver lui-même.
Exemple :
Code: Select all
CreateFile(L"\\\\.\\Zero", ...);
ReadFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
WriteFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
DeviceIoControl(hDevice, IOCTL_XXX, ...);
2. L'IRP n'est jamais vraiment seul
Un IRP n'est pas juste une structure isolée.
Il est accompagné d'une ou plusieurs structures :
Code: Select all
IO_STACK_LOCATION
- l'IRP représente la requête globale ;
- les représentent les informations spécifiques à chaque driver dans la pile.
Code: Select all
IO_STACK_LOCATION
Code: Select all
+------------------------------+
| IRP |
+------------------------------+
| IO_STACK_LOCATION pour drv 1 |
+------------------------------+
| IO_STACK_LOCATION pour drv 2 |
+------------------------------+
| IO_STACK_LOCATION pour drv 3 |
+------------------------------+
Un driver ne doit pas lire n'importe quel emplacement au hasard. Il doit récupérer l'emplacement courant avec :
Code: Select all
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
3. Pourquoi il y a plusieurs IO_STACK_LOCATION ?
Parce que les drivers Windows ne sont pas toujours seuls.
Un périphérique peut être représenté par une pile de drivers.
Exemple conceptuel :
Code: Select all
+----------------------+
| Filter Driver haut | FiDO
+----------------------+
| Function Driver | FDO
+----------------------+
| Filter Driver bas | FiDO
+----------------------+
| Bus Driver | PDO
+----------------------+
Donc quand l'IRP est alloué, le système prévoit autant de
Code: Select all
IO_STACK_LOCATION4. Device node, PDO, FDO, FiDO
Windows est plutôt orienté device object que simplement "driver".
Un driver crée généralement un objet de périphérique avec :
Code: Select all
IoCreateDevice
Code: Select all
IoCreateDeviceSecure
Dans les pages, on voit trois types de device objects :
- PDO : Physical Device Object ;
- FDO : Functional Device Object ;
- FiDO : Filter Device Object.
Le nom est un peu trompeur. Un PDO n'est pas forcément "physique" dans le sens matériel brut.
Le PDO est généralement créé par le bus driver. Il représente le fait qu'un périphérique existe sur un bus : PCI, USB, etc.
Exemple :
- le bus PCI détecte un périphérique ;
- le bus driver crée un PDO ;
- le Plug & Play Manager cherche quel driver doit gérer ce périphérique ;
- un function driver est chargé au-dessus.
Le FDO est créé par le driver principal du périphérique, celui qui comprend réellement le matériel ou la logique du périphérique.
C'est souvent le "vrai" driver fonctionnel.
4.3. FiDO - Filter Device Object
Un FiDO est un device object créé par un filter driver.
Un filter driver peut intercepter, modifier, surveiller ou enrichir les requêtes qui passent dans la stack.
Il peut y avoir :
- des filtres au-dessus du FDO ;
- des filtres en-dessous du FDO.
Code: Select all
Application
|
I/O Manager
|
Upper Filter
|
Function Driver
|
Lower Filter
|
Bus Driver / PDO
Le Plug & Play Manager est responsable de construire la stack de périphériques.
Il peut chercher dans le registre les drivers associés à un périphérique.
On retrouve des informations comme :
Code: Select all
HKLM\System\CurrentControlSet\Services\NomDuService
Pour les filtres, Windows peut regarder des valeurs comme :
Code: Select all
LowerFilters
UpperFilters
L'idée importante :
- la stack de périphérique est construite couche par couche ;
- les IRP vont ensuite pouvoir traverser cette stack ;
- un driver peut traiter l'IRP, le passer, ou faire les deux.
Un IRP peut être envoyé à un driver, puis éventuellement au driver du dessous.
Schéma :
Code: Select all
IRP
|
v
Driver A
|
v
Driver B
|
v
Driver C
|
v
Completion
^
|
Remontée de l'IRP
6.1. Possibilité 1 : traiter l'IRP lui-même
Le driver peut décider que la requête le concerne directement.
Il remplit alors :
Code: Select all
Irp->IoStatus.Status
Irp->IoStatus.Information
Code: Select all
IoCompleteRequest(Irp, IO_NO_INCREMENT);
Exemple :
Code: Select all
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
Si le driver n'est pas le dernier de la stack, il peut passer la requête plus bas avec :
Code: Select all
IoCallDriver(LowerDeviceObject, Irp);
Il peut utiliser :
Code: Select all
IoSkipCurrentIrpStackLocation(Irp);
Code: Select all
IoCopyCurrentIrpStackLocationToNext(Irp);
Code: Select all
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(LowerDeviceObject, Irp);
Elle est utile quand le driver ne veut pas modifier les paramètres et veut simplement transmettre l'IRP au driver suivant.
C'est une optimisation : elle évite de copier toute la stack location.
6.4. IoCopyCurrentIrpStackLocationToNext
Code: Select all
IoCopyCurrentIrpStackLocationToNext(Irp);
return IoCallDriver(LowerDeviceObject, Irp);
Elle est utile si le driver veut transmettre les mêmes paramètres mais aussi ajouter une routine de completion ensuite.
Exemple :
Code: Select all
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(
Irp,
MyCompletionRoutine,
Context,
TRUE,
TRUE,
TRUE
);
return IoCallDriver(LowerDeviceObject, Irp);
Un driver peut lire/modifier des données de l'IRP, puis le passer.
Exemple :
- un filtre inspecte un IOCTL ;
- il loggue l'information ;
- il laisse ensuite le driver inférieur gérer réellement la demande.
Un driver peut enregistrer une routine de completion avant de passer l'IRP.
Quand le driver inférieur complète la requête, l'IRP remonte et la completion routine est appelée.
C'est très important pour les filtres.
6.7. Possibilité 5 : traitement asynchrone
Un driver peut aussi ne pas terminer l'IRP immédiatement.
Il peut :
- marquer l'IRP comme pending ;
- retourner ;
Code: Select all
STATUS_PENDING - compléter l'IRP plus tard.
Code: Select all
IoMarkIrpPending(Irp);
QueueWorkItemOrStoreIrp(Irp);
return STATUS_PENDING;
7. Les champs importants de IRP
La structure
Code: Select all
IRP7.1. IoStatus
Le champ
Code: Select all
IoStatusIl contient notamment :
Code: Select all
Irp->IoStatus.Status
Irp->IoStatus.Information
Code: Select all
STATUS_SUCCESS
STATUS_INVALID_PARAMETER
STATUS_BUFFER_TOO_SMALL
STATUS_INSUFFICIENT_RESOURCES
STATUS_INVALID_DEVICE_REQUEST
...
Exemple :
Code: Select all
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = sizeof(MY_OUTPUT);
Code: Select all
Information7.2. AssociatedIrp.SystemBuffer
Ce champ est très important en Buffered I/O.
Code: Select all
Irp->AssociatedIrp.SystemBuffer
Le driver peut lire/écrire dedans sans accéder directement à la mémoire user-mode.
7.3. MdlAddress
En Direct I/O, l'IRP contient un MDL :
Code: Select all
Irp->MdlAddress
Le driver peut ensuite obtenir une adresse système avec :
Code: Select all
MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
Un IRP peut être annulé.
Le champ
Code: Select all
CancelRoutineDans beaucoup de drivers simples, on ne l'utilise pas au début. Mais dès qu'on garde un IRP en attente longtemps, l'annulation devient importante.
7.5. UserBuffer
Selon le type de buffering, il peut exister un pointeur vers le buffer utilisateur.
Mais attention : accéder directement à un pointeur user-mode depuis le kernel est dangereux si on ne maîtrise pas le contexte, l'IRQL et la validation.
8. Les champs importants de IO_STACK_LOCATION
Pour récupérer la stack location courante :
Code: Select all
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
Code: Select all
MajorFunction
MinorFunction
Parameters
FileObject
DeviceObject
CompletionRoutine
Context
Code: Select all
stack->MajorFunction
Exemples :
Code: Select all
IRP_MJ_CREATE
IRP_MJ_CLOSE
IRP_MJ_READ
IRP_MJ_WRITE
IRP_MJ_DEVICE_CONTROL
IRP_MJ_INTERNAL_DEVICE_CONTROL
IRP_MJ_POWER
IRP_MJ_PNP
Certaines requêtes ont aussi une fonction mineure.
C'est surtout important pour :
- Plug & Play ;
- Power Management ;
- certains types de requêtes plus spécialisés.
Code: Select all
IRP_MN_START_DEVICE
IRP_MN_QUERY_REMOVE_DEVICE
IRP_MN_SET_POWER
Code: Select all
stack->Parameters
Code: Select all
MajorFunctionPour une lecture :
Code: Select all
stack->Parameters.Read.Length
Code: Select all
stack->Parameters.Write.Length
Code: Select all
stack->Parameters.DeviceIoControl.IoControlCode
stack->Parameters.DeviceIoControl.InputBufferLength
stack->Parameters.DeviceIoControl.OutputBufferLength
8.4. FileObject
Code: Select all
stack->FileObject
Dans un driver logiciel simple, on peut ne pas trop l'utiliser au début, mais c'est important dès qu'on veut gérer des états par handle.
8.5. DeviceObject
Code: Select all
stack->DeviceObject
8.6. CompletionRoutine et Context
Un driver peut enregistrer une routine de completion pour être appelé quand l'IRP remonte.
Code: Select all
IoSetCompletionRoutine(
Irp,
MyCompletionRoutine,
Context,
TRUE,
TRUE,
TRUE
);
Code: Select all
Context9. Les dispatch routines
Dans
Code: Select all
DriverEntryLa table est dans :
Code: Select all
DriverObject->MajorFunction[]
Code: Select all
DriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = MyClose;
DriverObject->MajorFunction[IRP_MJ_READ] = MyRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = MyWrite;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDeviceControl;
Code: Select all
NTSTATUS DispatchRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp);
Code: Select all
NTSTATUS MyRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG len = stack->Parameters.Read.Length;
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = len;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
Code: Select all
IRP_MJ_CREATE
Depuis le user-mode, c'est souvent :
Code: Select all
CreateFile(L"\\\\.\\Zero", ...);
Exemple minimal :
Code: Select all
NTSTATUS ZeroCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
Code: Select all
IRP_MJ_CLOSE
Côté user-mode :
Code: Select all
CloseHandle(hDevice);
Code: Select all
CREATECode: Select all
CLOSECode: Select all
DriverObject->MajorFunction[IRP_MJ_CREATE] = ZeroCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = ZeroCreateClose;
Code: Select all
IRP_MJ_READ
Code: Select all
ReadFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
Code: Select all
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG len = stack->Parameters.Read.Length;
Dans les pages, le driver Zero utilise Direct I/O, donc il récupère l'adresse système avec :
Code: Select all
auto buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
Code: Select all
NTSTATUS ZeroRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto len = stack->Parameters.Read.Length;
if (len == 0) {
return CompleteIrp(Irp, STATUS_INVALID_BUFFER_SIZE);
}
auto buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
if (!buffer) {
return CompleteIrp(Irp, STATUS_INSUFFICIENT_RESOURCES);
}
RtlZeroMemory(buffer, len);
return CompleteIrp(Irp, STATUS_SUCCESS, len);
}
13. IRP_MJ_WRITE
Code: Select all
IRP_MJ_WRITE
Code: Select all
WriteFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
Elle indique simplement que les octets ont été écrits.
Code: Select all
NTSTATUS ZeroWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto len = stack->Parameters.Write.Length;
return CompleteIrp(Irp, STATUS_SUCCESS, len);
}
Code: Select all
MmGetSystemAddressForMdlSafeParce qu'on n'a pas besoin de lire le contenu du buffer. Si on veut juste accepter l'écriture et retourner "ok, j'ai consommé X octets", on peut se contenter de la longueur.
14. IRP_MJ_DEVICE_CONTROL
Code: Select all
IRP_MJ_DEVICE_CONTROL
Elle arrive quand le user-mode appelle :
Code: Select all
DeviceIoControl(...)
Exemple user-mode :
Code: Select all
DeviceIoControl(
hDevice,
IOCTL_ZERO_GET_STATS,
nullptr,
0,
&stats,
sizeof(stats),
&bytes,
nullptr
);
Code: Select all
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG inLen = stack->Parameters.DeviceIoControl.InputBufferLength;
ULONG outLen = stack->Parameters.DeviceIoControl.OutputBufferLength;
Code: Select all
switchCode: Select all
switch (code) {
case IOCTL_ZERO_GET_STATS:
// retourner les stats
break;
case IOCTL_ZERO_CLEAR_STATS:
// reset les stats
break;
default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
Quand un driver décide de terminer une requête, il doit :
- remplir ;
Code: Select all
Irp->IoStatus.Status - remplir ;
Code: Select all
Irp->IoStatus.Information - appeler ;
Code: Select all
IoCompleteRequest - retourner le même status que celui placé dans l'IRP.
Code: Select all
NTSTATUS CompleteIrp(PIRP Irp, NTSTATUS status = STATUS_SUCCESS, ULONG_PTR info = 0) {
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = info;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
16. Piège important : ne pas toucher l'IRP après IoCompleteRequest
Après :
Code: Select all
IoCompleteRequest(Irp, IO_NO_INCREMENT);
Pourquoi ?
Parce que l'IRP peut déjà avoir été libéré ou réutilisé par le système.
Mauvais exemple :
Code: Select all
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return Irp->IoStatus.Status; // dangereux
Code: Select all
NTSTATUS status = STATUS_SUCCESS;
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
Code: Select all
return CompleteIrp(Irp, STATUS_SUCCESS);
Prototype conceptuel :
Code: Select all
VOID IoCompleteRequest(
PIRP Irp,
CCHAR PriorityBoost
);
Pour les drivers logiciels simples, on utilise souvent :
Code: Select all
IO_NO_INCREMENT
Code: Select all
IoCompleteRequest(Irp, IO_NO_INCREMENT);
Code: Select all
IO_NO_INCREMENT18. Pourquoi l'accès aux buffers user-mode est dangereux
Un piège classique : croire qu'on peut prendre un pointeur user-mode et l'utiliser tranquillement dans le kernel.
Ce n'est pas si simple.
Un pointeur user-mode peut être dangereux parce que :
- il peut être invalide ;
- il peut pointer vers une page non présente ;
- la mémoire peut être pagée ;
- le driver peut ne plus être dans le contexte du processus appelant ;
- l'IRQL peut être trop élevé pour tolérer une page fault ;
- l'utilisateur peut modifier le buffer pendant que le driver travaille ;
- un pointeur mal contrôlé peut provoquer un crash kernel.
Code: Select all
try/exceptC'est pour ça que Windows propose des modèles de buffering :
- Buffered I/O ;
- Direct I/O ;
- Neither I/O.
Le Buffered I/O est le modèle le plus simple.
On active ce mode pour Read/Write avec un flag dans le device object :
Code: Select all
DeviceObject->Flags |= DO_BUFFERED_IO;
Le driver accède au buffer avec :
Code: Select all
Irp->AssociatedIrp.SystemBuffer
Code: Select all
User buffer
|
| copie par l'I/O Manager
v
SystemBuffer kernel
|
| le driver écrit dans SystemBuffer
v
copie retour vers User buffer
Pour une requête read/write :
- l'I/O Manager alloue un buffer kernel de même taille que le buffer utilisateur ;
- pour une écriture, il copie le buffer user vers le system buffer ;
- le driver reçoit l'IRP ;
- le driver lit/écrit dans ;
Code: Select all
Irp->AssociatedIrp.SystemBuffer - quand le driver complète l'IRP, l'I/O Manager recopie éventuellement vers le buffer user ;
- l'I/O Manager libère le system buffer.
Code: Select all
auto buffer = Irp->AssociatedIrp.SystemBuffer;
- très simple ;
- le driver n'accède pas directement au buffer user ;
- le buffer est en kernel space ;
- utile pour les petits buffers.
Il y a une copie.
Pour des gros buffers, cette copie peut coûter cher.
C'est pour ça qu'on utilise parfois Direct I/O.
20. Direct I/O
Le Direct I/O est utilisé quand on veut éviter de copier de gros buffers.
On active ce mode avec :
Code: Select all
DeviceObject->Flags |= DO_DIRECT_IO;
Code: Select all
IRP_MJ_READCode: Select all
IRP_MJ_WRITEPour
Code: Select all
IRP_MJ_DEVICE_CONTROLCode: Select all
CTL_CODE20.1. Le principe du Direct I/O
L'I/O Manager :
- valide le buffer user ;
- verrouille ses pages en RAM ;
- crée un MDL ;
- place ce MDL dans .
Code: Select all
Irp->MdlAddress
Code: Select all
PVOID buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
La mémoire virtuelle utilisateur peut être continue virtuellement, mais pas physiquement.
Un MDL décrit les pages physiques derrière le buffer.
Il permet au kernel de savoir quelles pages sont concernées, verrouillées, et comment les mapper.
20.3. MmGetSystemAddressForMdlSafe
Prototype simplifié :
Code: Select all
PVOID MmGetSystemAddressForMdlSafe(
PMDL Mdl,
ULONG Priority
);
Code: Select all
auto buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
if (!buffer) {
return CompleteIrp(Irp, STATUS_INSUFFICIENT_RESOURCES);
}
Code: Select all
NULL20.4. Avantages du Direct I/O
- moins de copies pour les gros buffers ;
- les pages utilisateur sont verrouillées ;
- le driver peut accéder à une adresse système stable ;
- utile pour des transferts importants.
- plus complexe que Buffered I/O ;
- utilise des MDL ;
- verrouiller des pages a un coût ;
- il faut mapper avec si on veut une adresse CPU côté driver.
Code: Select all
MmGetSystemAddressForMdlSafe
Le mode
Code: Select all
METHOD_NEITHERIl fournit les pointeurs user-mode, et le driver se débrouille.
C'est dangereux si mal utilisé.
Le driver doit vérifier, capturer et sécuriser les accès lui-même.
Dans un driver débutant ou pédagogique, il vaut mieux éviter ce mode sauf pour comprendre le mécanisme.
22. DeviceIoControl et les IOCTL
Un IOCTL permet d'envoyer une commande personnalisée à un driver.
Côté user-mode :
Code: Select all
BOOL DeviceIoControl(
HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped
);
- : handle obtenu avec
Code: Select all
hDevice;Code: Select all
CreateFile - : code IOCTL ;
Code: Select all
dwIoControlCode - : buffer d'entrée ;
Code: Select all
lpInBuffer - : taille entrée ;
Code: Select all
nInBufferSize - : buffer de sortie ;
Code: Select all
lpOutBuffer - : taille sortie ;
Code: Select all
nOutBufferSize - : nombre d'octets réellement retournés.
Code: Select all
lpBytesReturned
Un IOCTL se définit avec :
Code: Select all
CTL_CODE(DeviceType, Function, Method, Access)
Code: Select all
#define CTL_CODE(DeviceType, Function, Method, Access) \
(((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
- : type de device ;
Code: Select all
DeviceType - : numéro de fonction personnalisé ;
Code: Select all
Function - : méthode de buffering ;
Code: Select all
Method - : droits nécessaires.
Code: Select all
Access
Pour un device custom, on utilise souvent une valeur à partir de
Code: Select all
0x8000Exemple :
Code: Select all
#define DEVICE_ZERO 0x8002
C'est un numéro de commande.
Pour les fonctions custom, on utilise souvent des valeurs à partir de
Code: Select all
0x800Exemple :
Code: Select all
#define IOCTL_ZERO_GET_STATS \
CTL_CODE(DEVICE_ZERO, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
C'est la méthode utilisée pour accéder aux buffers.
Valeurs possibles :
Code: Select all
METHOD_BUFFERED
METHOD_IN_DIRECT
METHOD_OUT_DIRECT
METHOD_NEITHER
Contrôle les droits nécessaires.
Exemples :
Code: Select all
FILE_ANY_ACCESS
FILE_READ_ACCESS
FILE_WRITE_ACCESS
Pour
Code: Select all
DeviceIoControlCode: Select all
DO_BUFFERED_IOCode: Select all
DO_DIRECT_IOC'est le champ
Code: Select all
MethodCode: Select all
CTL_CODE24.1. METHOD_BUFFERED
Avec
Code: Select all
METHOD_BUFFEREDCode: Select all
Irp->AssociatedIrp.SystemBuffer
Exemple :
Code: Select all
auto buffer = Irp->AssociatedIrp.SystemBuffer;
Code: Select all
stack->Parameters.DeviceIoControl.InputBufferLength
stack->Parameters.DeviceIoControl.OutputBufferLength
Code: Select all
Irp->IoStatus.Information
Le nom peut être perturbant.
Avec
Code: Select all
METHOD_IN_DIRECT- le petit buffer d'entrée passe par ;
Code: Select all
SystemBuffer - le buffer principal de sortie est décrit par un MDL.
Avec
Code: Select all
METHOD_OUT_DIRECT- le buffer d'entrée passe aussi par ;
Code: Select all
SystemBuffer - le buffer de sortie principal est accessible via .
Code: Select all
Irp->MdlAddress
Code: Select all
METHOD_IN_DIRECTCode: Select all
METHOD_OUT_DIRECT24.4. METHOD_NEITHER
Avec
Code: Select all
METHOD_NEITHERLes pointeurs user-mode sont transmis dans la stack location.
C'est puissant, mais dangereux.
Il faut être très prudent.
25. Tableau mental des buffers IOCTL
Résumé pratique :
Code: Select all
METHOD_BUFFERED:
entrée -> Irp->AssociatedIrp.SystemBuffer
sortie -> Irp->AssociatedIrp.SystemBuffer
METHOD_IN_DIRECT:
entrée -> Irp->AssociatedIrp.SystemBuffer
sortie -> Irp->MdlAddress
METHOD_OUT_DIRECT:
entrée -> Irp->AssociatedIrp.SystemBuffer
sortie -> Irp->MdlAddress
METHOD_NEITHER:
entrée/sortie -> pointeurs user-mode bruts
Code: Select all
IRP_MJ_INTERNAL_DEVICE_CONTROL
Code: Select all
IRP_MJ_DEVICE_CONTROLUn programme user-mode n'est pas censé l'appeler directement avec
Code: Select all
DeviceIoControl27. WinDbg : voir les IRP
Quand on débogue le kernel, il existe des commandes utiles.
27.1. !irpfind
Code: Select all
!irpfind
On peut obtenir des informations comme :
- adresse de l'IRP ;
- thread associé ;
- device object ;
- stack count ;
- driver concerné.
Pour afficher un IRP précis :
Code: Select all
!irp AdresseIrp
Code: Select all
!irp AdresseIrp 1
- la stack trace de l'IRP ;
- les major functions ;
- les device objects ;
- les completion routines ;
- le statut ;
- si l'IRP est pending ;
- les informations de Driver Verifier si disponibles.
Quand tu regardes un IRP, fais attention à :
- affiché ;
Code: Select all
IRP_MJ_XXX - l'état pending ou non ;
- le driver actuellement concerné ;
- les arguments ;
- la completion routine ;
- le device object ;
- les flags.
28. Le Zero Driver : idée générale
Le Zero Driver est un driver logiciel pédagogique.
Son comportement :
- pour une lecture : il remplit le buffer avec des zéros ;
- pour une écriture : il consomme les octets fournis ;
- il maintient des statistiques : nombre total d'octets lus/écrits ;
- il expose des IOCTL pour lire ou remettre à zéro ces stats.
29. Création du device et du symbolic link
Dans
Code: Select all
DriverEntryCode: Select all
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\Zero");
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Zero");
PDEVICE_OBJECT DeviceObject = nullptr;
status = IoCreateDevice(
DriverObject,
0,
&devName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&DeviceObject
);
Code: Select all
status = IoCreateSymbolicLink(&symLink, &devName);
Code: Select all
\\\\.\\Zero
Dans l'exemple :
Code: Select all
DeviceObject->Flags |= DO_DIRECT_IO;
Code: Select all
ReadFileCode: Select all
WriteFilePour lire le buffer dans
Code: Select all
IRP_MJ_READCode: Select all
MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority)
Code: Select all
extern "C"
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = ZeroUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = ZeroCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = ZeroCreateClose;
DriverObject->MajorFunction[IRP_MJ_READ] = ZeroRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = ZeroWrite;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ZeroDeviceControl;
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\Zero");
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Zero");
PDEVICE_OBJECT DeviceObject = nullptr;
NTSTATUS status = IoCreateDevice(
DriverObject,
0,
&devName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&DeviceObject
);
if (!NT_SUCCESS(status)) {
return status;
}
DeviceObject->Flags |= DO_DIRECT_IO;
status = IoCreateSymbolicLink(&symLink, &devName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(DeviceObject);
return status;
}
return STATUS_SUCCESS;
}
Quand le driver se décharge, il faut supprimer le lien symbolique et le device.
Code: Select all
void ZeroUnload(PDRIVER_OBJECT DriverObject) {
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Zero");
IoDeleteSymbolicLink(&symLink);
IoDeleteDevice(DriverObject->DeviceObject);
}
Code: Select all
NTSTATUS CompleteIrp(PIRP Irp, NTSTATUS status = STATUS_SUCCESS, ULONG_PTR info = 0) {
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = info;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
34. Create/Close
Code: Select all
NTSTATUS ZeroCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
return CompleteIrp(Irp);
}
Code: Select all
NTSTATUS ZeroRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto len = stack->Parameters.Read.Length;
if (len == 0) {
return CompleteIrp(Irp, STATUS_INVALID_BUFFER_SIZE);
}
auto buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
if (!buffer) {
return CompleteIrp(Irp, STATUS_INSUFFICIENT_RESOURCES);
}
RtlZeroMemory(buffer, len);
return CompleteIrp(Irp, STATUS_SUCCESS, len);
}
Code: Select all
IoStatus.Information = len;
36. Write : consommer les octets
Code: Select all
NTSTATUS ZeroWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto len = stack->Parameters.Write.Length;
return CompleteIrp(Irp, STATUS_SUCCESS, len);
}
37. Ajouter des statistiques
Le driver peut compter les octets lus et écrits :
Code: Select all
long long g_TotalRead;
long long g_TotalWritten;
Code: Select all
struct ZeroStats {
long long TotalRead;
long long TotalWritten;
};
Code: Select all
InterlockedAdd64(&g_TotalRead, len);
Code: Select all
InterlockedAdd64(&g_TotalWritten, len);
Code: Select all
InterlockedAdd64Parce que plusieurs threads peuvent appeler le driver en même temps. Une simple addition globale peut créer une data race.
38. Définir des IOCTL pour les stats
Exemple :
Code: Select all
#define DEVICE_ZERO 0x8002
#define IOCTL_ZERO_GET_STATS \
CTL_CODE(DEVICE_ZERO, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_ZERO_CLEAR_STATS \
CTL_CODE(DEVICE_ZERO, 0x801, METHOD_NEITHER, FILE_ANY_ACCESS)
- retourne une structure ;
Code: Select all
IOCTL_ZERO_GET_STATS - remet les compteurs à zéro.
Code: Select all
IOCTL_ZERO_CLEAR_STATS
Exemple simplifié :
Code: Select all
NTSTATUS ZeroDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto& dic = stack->Parameters.DeviceIoControl;
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
ULONG_PTR len = 0;
switch (dic.IoControlCode) {
case IOCTL_ZERO_GET_STATS:
{
if (dic.OutputBufferLength < sizeof(ZeroStats)) {
status = STATUS_BUFFER_TOO_SMALL;
break;
}
auto stats = (ZeroStats*)Irp->AssociatedIrp.SystemBuffer;
if (!stats) {
status = STATUS_INVALID_PARAMETER;
break;
}
stats->TotalRead = g_TotalRead;
stats->TotalWritten = g_TotalWritten;
len = sizeof(ZeroStats);
status = STATUS_SUCCESS;
break;
}
case IOCTL_ZERO_CLEAR_STATS:
g_TotalRead = 0;
g_TotalWritten = 0;
status = STATUS_SUCCESS;
break;
}
return CompleteIrp(Irp, status, len);
}
Dans l'exemple, pour ajouter des valeurs on utilise :
Code: Select all
InterlockedAdd64
On pourrait utiliser :
- un fast mutex ;
- un push lock ;
- un spin lock selon le contexte ;
- ou accepter une approximation si c'est juste pédagogique.
Code: Select all
FAST_MUTEX g_Mutex;
ExInitializeFastMutex(&g_Mutex);
ExAcquireFastMutex(&g_Mutex);
g_TotalRead = 0;
g_TotalWritten = 0;
ExReleaseFastMutex(&g_Mutex);
41. Application user-mode de test
Le programme user-mode ouvre le driver :
Code: Select all
HANDLE hDevice = CreateFile(
L"\\\\.\\Zero",
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
0,
nullptr
);
Code: Select all
BYTE buffer[64];
DWORD bytes = 0;
BOOL ok = ReadFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
if (!ok) {
printf("ReadFile failed: %u\n", GetLastError());
}
Code: Select all
for (int i = 0; i < sizeof(buffer); i++) {
buffer[i] = i + 1;
}
ok = WriteFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
if (!ok) {
printf("WriteFile failed: %u\n", GetLastError());
}
Code: Select all
ZeroStats stats;
DWORD bytes = 0;
BOOL ok = DeviceIoControl(
hDevice,
IOCTL_ZERO_GET_STATS,
nullptr,
0,
&stats,
sizeof(stats),
&bytes,
nullptr
);
printf("Total Read: %lld, Total Written: %lld\n",
stats.TotalRead,
stats.TotalWritten);
42.1. Oublier IoCompleteRequest
Si le driver traite l'IRP mais ne le complète jamais, le client peut rester bloqué.
Mauvais :
Code: Select all
return STATUS_SUCCESS;
Correct :
Code: Select all
return CompleteIrp(Irp, STATUS_SUCCESS);
Très dangereux.
Un IRP doit être complété une seule fois par le chemin responsable.
Double completion = crash possible.
42.3. Mauvais IoStatus.Information
Si tu retournes une structure de 16 octets, mais que tu mets :
Code: Select all
Irp->IoStatus.Information = 0;
Correct :
Code: Select all
Irp->IoStatus.Information = sizeof(ZeroStats);
Pour un read :
Code: Select all
stack->Parameters.Read.Length
Code: Select all
stack->Parameters.Write.Length
Code: Select all
stack->Parameters.DeviceIoControl.OutputBufferLength
42.5. Accéder directement à un buffer user sans précaution
C'est une erreur dangereuse.
Préférer :
- pour Buffered I/O ;
Code: Select all
SystemBuffer - pour Direct I/O ;
Code: Select all
MmGetSystemAddressForMdlSafe - éviter au début.
Code: Select all
METHOD_NEITHER
Mauvais :
Code: Select all
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_INVALID_PARAMETER;
Correct :
Code: Select all
return CompleteIrp(Irp, STATUS_SUCCESS);
Quand un programme fait :
Code: Select all
ReadFile(hDevice, buffer, size, &bytes, nullptr);
Code: Select all
User-mode
|
| ReadFile
v
Kernel transition
|
v
I/O Manager
|
| crée IRP_MJ_READ
v
Driver dispatch read
|
| récupère IO_STACK_LOCATION
| récupère longueur
| accède au buffer selon le mode I/O
| écrit les données
| remplit IoStatus
| IoCompleteRequest
v
I/O Manager
|
| met à jour bytes
v
Retour user-mode
Code: Select all
User-mode
|
| DeviceIoControl(IOCTL_XXX)
v
I/O Manager
|
| crée IRP_MJ_DEVICE_CONTROL
| prépare les buffers selon METHOD_XXX
v
Driver
|
| stack->Parameters.DeviceIoControl.IoControlCode
| switch(IOCTL)
| lit/écrit les buffers
| CompleteIrp
v
Retour user-mode
Dans un driver logiciel simple comme Zero :
- le driver est souvent seul ;
- il traite les IRP directement ;
- il complète les requêtes lui-même ;
- il n'a pas besoin de .
Code: Select all
IoCallDriver
- le driver fait partie d'une stack ;
- il peut passer les IRP au driver inférieur ;
- il peut installer des completion routines ;
- il doit gérer des requêtes PnP et power ;
- il doit respecter l'ordre de propagation et de completion.
Quand on parle du driver du dessous, on parle du driver situé juste en-dessous dans la stack de périphériques.
Exemple :
Code: Select all
Upper Filter
|
Function Driver <-- si on est ici
|
Lower Filter <-- driver du dessous
|
PDO / Bus Driver
Code: Select all
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(LowerDeviceObject, Irp);
Code: Select all
IoAttachDeviceToDeviceStack
47. IRP pending
Un IRP peut être complété plus tard.
Exemple :
Code: Select all
IoMarkIrpPending(Irp);
return STATUS_PENDING;
Exemple conceptuel :
Code: Select all
IoMarkIrpPending(Irp);
InsertTailList(&g_PendingIrps, &Irp->Tail.Overlay.ListEntry);
return STATUS_PENDING;
Code: Select all
CompleteIrp(Irp, STATUS_SUCCESS, bytes);
- annulation ;
- synchronisation ;
- durée de vie de l'IRP ;
- déchargement du driver ;
- double completion.
Toutes les opérations ne sont pas autorisées à tous les IRQL.
Quelques rappels :
- accéder à une mémoire paginable peut provoquer une page fault ;
- à DISPATCH_LEVEL, les page faults ne sont pas autorisées ;
- certaines fonctions ne peuvent être appelées qu'à PASSIVE_LEVEL ;
- les buffers user-mode sont particulièrement sensibles à ces règles.
49. Ce qu'il faut retenir absolument
- Un IRP est la requête noyau utilisée par Windows pour communiquer avec les drivers.
- Chaque IRP possède une ou plusieurs .
Code: Select all
IO_STACK_LOCATION - Le driver récupère sa stack location avec .
Code: Select all
IoGetCurrentIrpStackLocation - Le type de requête est dans .
Code: Select all
MajorFunction - Les paramètres dépendent du type de requête.
- Un driver peut traiter, passer, ou traiter puis passer un IRP.
- Pour passer un IRP : .
Code: Select all
IoCallDriver - Pour compléter un IRP : .
Code: Select all
IoCompleteRequest - Après , on ne touche plus l'IRP.
Code: Select all
IoCompleteRequest - contient le résultat.
Code: Select all
IoStatus.Status - contient souvent le nombre d'octets transférés.
Code: Select all
IoStatus.Information - Buffered I/O utilise .
Code: Select all
Irp->AssociatedIrp.SystemBuffer - Direct I/O utilise et
Code: Select all
Irp->MdlAddress.Code: Select all
MmGetSystemAddressForMdlSafe - Les IOCTL sont définis avec .
Code: Select all
CTL_CODE - Pour , la méthode de buffering vient du
Code: Select all
DeviceIoControldansCode: Select all
Method.Code: Select all
CTL_CODE - est dangereux si mal utilisé.
Code: Select all
METHOD_NEITHER - WinDbg permet d'inspecter les IRP avec et
Code: Select all
!irpfind.Code: Select all
!irp
IoGetCurrentIrpStackLocation
Code: Select all
PIO_STACK_LOCATION IoGetCurrentIrpStackLocation(PIRP Irp);
IoCompleteRequest
Code: Select all
VOID IoCompleteRequest(PIRP Irp, CCHAR PriorityBoost);
IoCallDriver
Code: Select all
NTSTATUS IoCallDriver(PDEVICE_OBJECT DeviceObject, PIRP Irp);
IoSkipCurrentIrpStackLocation
Code: Select all
IoSkipCurrentIrpStackLocation(Irp);
IoCopyCurrentIrpStackLocationToNext
Code: Select all
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine
Code: Select all
IoSetCompletionRoutine(Irp, Routine, Context, TRUE, TRUE, TRUE);
MmGetSystemAddressForMdlSafe
Code: Select all
PVOID MmGetSystemAddressForMdlSafe(PMDL Mdl, ULONG Priority);
IoCreateDevice
Code: Select all
IoCreateDevice(...)
IoCreateSymbolicLink
Code: Select all
IoCreateSymbolicLink(...)
CTL_CODE
Code: Select all
CTL_CODE(DeviceType, Function, Method, Access)
Code: Select all
DeviceIoControlConclusion
Les IRP sont la base du modèle I/O de Windows.
Quand tu comprends les IRP, tu comprends beaucoup mieux ce qu'un driver fait réellement :
- il ne reçoit pas juste des appels de fonction classiques ;
- il reçoit des paquets de requête ;
- chaque requête transporte son type, ses paramètres, ses buffers et son état ;
- le driver doit respecter le protocole : lire la stack location, traiter, remplir le status, compléter ou passer l'IRP.
Le point vraiment important à retenir :
Un IRP est une responsabilité.
Si ton driver décide de le gérer, il doit le compléter proprement. S'il ne le gère pas, il doit le passer correctement. Et s'il le garde pour plus tard, il doit gérer sa durée de vie, l'annulation, la synchronisation et la completion.
C'est exactement là qu'on commence à passer du simple "code driver" au vrai raisonnement kernel.
