Objectif
Ce chapitre présente l’accès aux fichiers depuis le mode noyau Windows dans un driver WDM. Il couvre les contraintes d’exécution, la préparation des noms et des attributs d’objets, l’ouverture et la création de fichiers avec ZwCreateFile, la lecture et l’écriture avec ZwReadFile et ZwWriteFile, la récupération d’informations avec ZwQueryInformationFile, la portée des handles, l’utilisation de OBJ_KERNEL_HANDLE et les précautions liées au moment où les opérations fichier sont réalisées pendant l’initialisation du système.
L’accès fichier depuis le noyau ressemble conceptuellement à l’accès fichier en user mode, mais il s’effectue directement au travers des routines natives du noyau et obéit à des contraintes plus strictes.
1. Pourquoi accéder à un fichier depuis un driver ?
Un driver peut avoir besoin d’utiliser un fichier pour plusieurs raisons :
- charger une configuration ;
- charger un firmware ou un microcode ;
- lire des données nécessaires au périphérique ;
- écrire un journal technique ;
- sauvegarder des informations produites par le driver ;
- charger un fichier auxiliaire appartenant au pilote.
La règle générale reste donc :
le driver ne doit accéder directement aux fichiers que lorsqu’il existe une raison technique claire de le faire.
2. Les routines Zw*
Windows fournit plusieurs routines natives préfixées par
Code: Select all
ZwPour les fichiers, les principales routines étudiées ici sont :
- : ouvre ou crée un fichier ;
Code: Select all
ZwCreateFile - : lit des données ;
Code: Select all
ZwReadFile - : écrit des données ;
Code: Select all
ZwWriteFile - : récupère des informations sur le fichier ;
Code: Select all
ZwQueryInformationFile - : ferme un handle.
Code: Select all
ZwClose
- ;
Code: Select all
HANDLE - ;
Code: Select all
UNICODE_STRING - ;
Code: Select all
OBJECT_ATTRIBUTES - ;
Code: Select all
IO_STATUS_BLOCK - .
Code: Select all
NTSTATUS
3. Contraintes d’exécution
L’accès fichier depuis un driver n’est pas autorisé dans n’importe quel contexte.
Les opérations fichier avec les routines Zw* doivent être exécutées à PASSIVE_LEVEL.
Cela signifie qu’elles ne doivent pas être réalisées depuis un contexte où l’IRQL est élevé, par exemple depuis une routine DPC à
Code: Select all
DISPATCH_LEVELExemple :
Code: Select all
if (KeGetCurrentIrql() != PASSIVE_LEVEL)
{
return STATUS_INVALID_DEVICE_STATE;
}
Code: Select all
PASSIVE_LEVELLes opérations fichier peuvent provoquer une attente : le système peut devoir accéder au cache, au système de fichiers ou au périphérique de stockage.
Une routine exécutée à IRQL élevé ne peut pas effectuer arbitrairement ce type d’attente.
4. APC et régions critiques
Les opérations fichier nécessitent également un contexte de thread compatible avec les mécanismes d’attente du noyau.
Une opération fichier ne doit pas être lancée dans une situation où les APC nécessaires à l’I/O sont bloquées.
Par exemple,
Code: Select all
KeEnterCriticalRegionLe principe à retenir est :
les primitives de synchronisation qui élèvent l’IRQL ou bloquent les APC peuvent être incompatibles avec les opérations fichier.
Il faut donc éviter de conserver une région critique ou un verrou incompatible pendant l’appel à une routine telle que
Code: Select all
ZwReadFileCode: Select all
ZwWriteFile5. Préparer le nom du fichier
Les routines natives Windows ne prennent généralement pas directement un simple
Code: Select all
wchar_t*Le nom de l’objet est décrit par une structure :
Code: Select all
UNICODE_STRING
Code: Select all
UNICODE_STRING fileName;
RtlInitUnicodeString(
&fileName,
L"\\??\\C:\\Temp\\data.bin"
);
Code: Select all
L"\\SystemRoot\\Temp\\data.bin"
Un chemin tel que :
Code: Select all
C:\Temp\data.bin
Le préfixe :
Code: Select all
\??\
Exemple :
Code: Select all
\??\C:\Temp\data.bin
6. OBJECT_ATTRIBUTES
Après avoir construit le
Code: Select all
UNICODE_STRINGCode: Select all
OBJECT_ATTRIBUTES
- le nom de l’objet ;
- un éventuel handle de répertoire racine ;
- les attributs d’ouverture ;
- la sécurité ;
- la qualité de service de sécurité.
Code: Select all
InitializeObjectAttributes
Code: Select all
OBJECT_ATTRIBUTES attributes;
InitializeObjectAttributes(
&attributes,
&fileName,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
NULL,
NULL
);
7. OBJ_CASE_INSENSITIVE
L’attribut :
Code: Select all
OBJ_CASE_INSENSITIVE
Il s’agit d’un attribut très fréquemment utilisé pour les fichiers.
8. OBJ_KERNEL_HANDLE
L’attribut :
Code: Select all
OBJ_KERNEL_HANDLE
Un handle normal est associé à la table de handles du processus dans lequel le thread courant s’exécute.
Or un driver peut être appelé dans le contexte de processus très différents.
Si un driver crée un handle sans
Code: Select all
OBJ_KERNEL_HANDLECela peut provoquer plusieurs problèmes :
- le handle est lié à un processus utilisateur ;
- le processus peut terminer ;
- le handle pourrait être visible ou manipulable dans un contexte qui ne devrait pas y avoir accès ;
- un thread différent peut ensuite exécuter le code du driver dans un autre processus.
Code: Select all
OBJ_KERNEL_HANDLE
Dans un driver, lorsqu’un handle doit appartenir au driver et non au processus utilisateur courant, OBJ_KERNEL_HANDLE est généralement le choix approprié.
9. IO_STATUS_BLOCK
Les opérations natives d’I/O utilisent souvent :
Code: Select all
IO_STATUS_BLOCK
- le statut final de l’opération ;
- la quantité d’informations transférées.
Code: Select all
IO_STATUS_BLOCK ioStatus;
Code: Select all
ioStatus.Information
Il ne faut donc pas supposer qu’une opération a nécessairement traité exactement la taille demandée.
10. Ouvrir un fichier avec ZwCreateFile
La routine centrale est :
Code: Select all
ZwCreateFile
Elle permet également :
- d’ouvrir un fichier existant ;
- de créer un nouveau fichier ;
- d’écraser un fichier ;
- d’ouvrir ou créer selon l’existence du fichier.
Code: Select all
HANDLE fileHandle = NULL;
IO_STATUS_BLOCK ioStatus;
NTSTATUS status = ZwCreateFile(
&fileHandle,
GENERIC_READ,
&attributes,
&ioStatus,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
11. FileHandle
Le premier paramètre reçoit le handle du fichier :
Code: Select all
&fileHandle
Code: Select all
ZwReadFile
ZwWriteFile
ZwQueryInformationFile
ZwClose
12. DesiredAccess
Le deuxième paramètre décrit les droits demandés.
Exemples classiques :
Code: Select all
GENERIC_READ
GENERIC_WRITE
SYNCHRONIZE
Code: Select all
GENERIC_READ
Code: Select all
GENERIC_WRITE
Code: Select all
GENERIC_READ | GENERIC_WRITE
Code: Select all
SYNCHRONIZE13. FileAttributes
Le paramètre
Code: Select all
FileAttributesUne valeur simple est :
Code: Select all
FILE_ATTRIBUTE_NORMAL
14. ShareAccess
Le paramètre
Code: Select all
ShareAccessExemples :
Code: Select all
FILE_SHARE_READ
FILE_SHARE_WRITE
FILE_SHARE_DELETE
Code: Select all
FILE_SHARE_READ
Une valeur de :
Code: Select all
0
Le choix du partage est important : un mauvais masque peut provoquer des erreurs de type conflit de partage.
15. CreateDisposition
Le paramètre
Code: Select all
CreateDispositionLes valeurs importantes sont :
Code: Select all
FILE_OPEN
FILE_CREATE
FILE_OPEN_IF
FILE_OVERWRITE
FILE_OVERWRITE_IF
FILE_SUPERSEDE
Ouvre un fichier existant.
Si le fichier n’existe pas, l’opération échoue.
FILE_CREATE
Crée un nouveau fichier.
Si le fichier existe déjà, l’opération échoue.
FILE_OPEN_IF
Ouvre le fichier s’il existe, sinon le crée.
FILE_OVERWRITE
Écrase un fichier existant.
Si le fichier n’existe pas, l’opération échoue.
FILE_OVERWRITE_IF
Écrase le fichier s’il existe, sinon le crée.
FILE_SUPERSEDE
Remplace le fichier existant avec des sémantiques plus fortes de substitution.
16. CreateOptions
Le paramètre
Code: Select all
CreateOptionsUne option très courante dans un driver est :
Code: Select all
FILE_SYNCHRONOUS_IO_NONALERT
Avec un handle synchrone, la position courante du fichier peut être maintenue par le système.
Cela permet par exemple d’effectuer plusieurs lectures successives sans calculer manuellement un offset différent à chaque fois.
17. Ouvrir un fichier existant en lecture
Exemple complet :
Code: Select all
UNICODE_STRING fileName;
OBJECT_ATTRIBUTES attributes;
IO_STATUS_BLOCK ioStatus;
HANDLE fileHandle = NULL;
RtlInitUnicodeString(
&fileName,
L"\\??\\C:\\Temp\\input.bin"
);
InitializeObjectAttributes(
&attributes,
&fileName,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
NULL,
NULL
);
NTSTATUS status = ZwCreateFile(
&fileHandle,
GENERIC_READ,
&attributes,
&ioStatus,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
if (!NT_SUCCESS(status))
{
return status;
}
Code: Select all
GENERIC_READ
FILE_OPEN
FILE_SYNCHRONOUS_IO_NONALERT
18. Créer ou remplacer un fichier
Exemple :
Code: Select all
NTSTATUS status = ZwCreateFile(
&fileHandle,
GENERIC_WRITE,
&attributes,
&ioStatus,
NULL,
FILE_ATTRIBUTE_NORMAL,
0,
FILE_OVERWRITE_IF,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
Code: Select all
FILE_OVERWRITE_IF
- créé s’il n’existe pas ;
- remplacé ou tronqué s’il existe.
19. Vérifier le résultat de ZwCreateFile
Comme la plupart des routines natives,
Code: Select all
ZwCreateFileCode: Select all
NTSTATUS
Code: Select all
if (!NT_SUCCESS(status))
{
return status;
}
20. Lire avec ZwReadFile
Une fois le fichier ouvert, la lecture se fait avec :
Code: Select all
ZwReadFile
Code: Select all
CHAR buffer[512] = {};
status = ZwReadFile(
fileHandle,
NULL,
NULL,
NULL,
&ioStatus,
buffer,
sizeof(buffer),
NULL,
NULL
);
- le handle du fichier est fourni ;
- aucun événement n’est utilisé ;
- aucune APC de fin d’I/O n’est utilisée ;
- reçoit le résultat ;
Code: Select all
ioStatus - reçoit les données ;
Code: Select all
buffer - indique la quantité maximale demandée ;
Code: Select all
sizeof(buffer) - aucun offset explicite n’est fourni.
21. Offset de lecture
Le paramètre
Code: Select all
ByteOffsetExemple :
Code: Select all
LARGE_INTEGER offset;
offset.QuadPart = 1024;
Code: Select all
status = ZwReadFile(
fileHandle,
NULL,
NULL,
NULL,
&ioStatus,
buffer,
sizeof(buffer),
&offset,
NULL
);
Lorsque le handle est synchrone et qu’aucun offset n’est fourni, la position courante associée au handle peut être utilisée.
22. Nombre d’octets réellement lus
Après une lecture réussie :
Code: Select all
ULONG_PTR bytesRead = ioStatus.Information;
Code: Select all
bytesRead == sizeof(buffer)
23. Écrire avec ZwWriteFile
L’écriture suit un modèle très proche.
Exemple :
Code: Select all
const CHAR data[] = "Kernel log\r\n";
status = ZwWriteFile(
fileHandle,
NULL,
NULL,
NULL,
&ioStatus,
(PVOID)data,
sizeof(data) - 1,
NULL,
NULL
);
Code: Select all
ioStatus.Information
24. Écriture à un offset donné
Exemple :
Code: Select all
LARGE_INTEGER offset;
offset.QuadPart = 4096;
status = ZwWriteFile(
fileHandle,
NULL,
NULL,
NULL,
&ioStatus,
buffer,
bufferSize,
&offset,
NULL
);
25. Opérations synchrones et asynchrones
Les routines
Code: Select all
ZwReadFileCode: Select all
ZwWriteFileAvec :
Code: Select all
FILE_SYNCHRONOUS_IO_NONALERT
L’appel peut attendre la fin de l’opération avant de retourner.
Un mode asynchrone peut retourner avant l’achèvement réel de l’I/O et demande une gestion supplémentaire.
Pour de petites opérations occasionnelles dans un driver, le mode synchrone est souvent plus simple à comprendre.
Cependant :
une opération synchrone peut bloquer le thread courant.
Elle ne doit donc pas être placée arbitrairement sur un chemin critique de performance.
26. Récupérer la taille du fichier
Pour lire un fichier entier, il est utile de connaître sa taille.
La routine :
Code: Select all
ZwQueryInformationFile
Pour la taille du fichier, on peut utiliser :
Code: Select all
FILE_STANDARD_INFORMATION
Code: Select all
FILE_STANDARD_INFORMATION info;
status = ZwQueryInformationFile(
fileHandle,
&ioStatus,
&info,
sizeof(info),
FileStandardInformation
);
Code: Select all
info.EndOfFile
Code: Select all
LARGE_INTEGER
Code: Select all
LONGLONG fileSize = info.EndOfFile.QuadPart;
27. Allocation d’un buffer pour lire un fichier entier
Une fois la taille obtenue, un buffer peut être alloué.
Sur les versions modernes de Windows, on privilégie les routines modernes d’allocation de pool.
Conceptuellement :
Code: Select all
PVOID buffer = ExAllocatePool2(
POOL_FLAG_PAGED,
(SIZE_T)fileSize,
'eliF'
);
Code: Select all
PASSIVE_LEVELLe driver doit ensuite vérifier :
Code: Select all
if (buffer == NULL)
{
...
}
28. Séquence complète pour lire un fichier entier
La logique générale est :
- initialiser le nom ;
- initialiser ;
Code: Select all
OBJECT_ATTRIBUTES - ouvrir le fichier ;
- demander ;
Code: Select all
FILE_STANDARD_INFORMATION - récupérer ;
Code: Select all
EndOfFile - allouer un buffer ;
- lire le contenu ;
- traiter les données ;
- libérer le buffer ;
- fermer le handle.
Code: Select all
HANDLE fileHandle = NULL;
UNICODE_STRING fileName;
OBJECT_ATTRIBUTES attributes;
IO_STATUS_BLOCK ioStatus;
FILE_STANDARD_INFORMATION info;
PVOID buffer = NULL;
RtlInitUnicodeString(
&fileName,
L"\\??\\C:\\Temp\\input.bin"
);
InitializeObjectAttributes(
&attributes,
&fileName,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
NULL,
NULL
);
NTSTATUS status = ZwCreateFile(
&fileHandle,
GENERIC_READ,
&attributes,
&ioStatus,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
if (!NT_SUCCESS(status))
{
return status;
}
status = ZwQueryInformationFile(
fileHandle,
&ioStatus,
&info,
sizeof(info),
FileStandardInformation
);
if (!NT_SUCCESS(status))
{
ZwClose(fileHandle);
return status;
}
if (info.EndOfFile.QuadPart <= 0)
{
ZwClose(fileHandle);
return STATUS_END_OF_FILE;
}
SIZE_T size = (SIZE_T)info.EndOfFile.QuadPart;
buffer = ExAllocatePool2(
POOL_FLAG_PAGED,
size,
'eliF'
);
if (buffer == NULL)
{
ZwClose(fileHandle);
return STATUS_INSUFFICIENT_RESOURCES;
}
status = ZwReadFile(
fileHandle,
NULL,
NULL,
NULL,
&ioStatus,
buffer,
(ULONG)size,
NULL,
NULL
);
ExFreePool(buffer);
ZwClose(fileHandle);
return status;
Dans un code de production, il faut également :
- vérifier les conversions de tailles ;
- gérer les fichiers supérieurs à si nécessaire ;
Code: Select all
ULONG_MAX - éviter les allocations excessives ;
- valider ;
Code: Select all
ioStatus.Information - prévoir tous les chemins de nettoyage.
29. Pourquoi il faut vérifier les tailles
La taille d’un fichier est exprimée sur 64 bits avec :
Code: Select all
LARGE_INTEGER
Code: Select all
LengthCode: Select all
ZwReadFileCode: Select all
ULONG
Code: Select all
ULONGExemple dangereux :
Code: Select all
(ULONG)info.EndOfFile.QuadPart
Une implémentation robuste lit les gros fichiers par blocs.
30. Lire un fichier par blocs
Une stratégie plus robuste consiste à utiliser un buffer fixe.
Exemple logique :
Code: Select all
UCHAR buffer[4096];
for (;;)
{
status = ZwReadFile(
fileHandle,
NULL,
NULL,
NULL,
&ioStatus,
buffer,
sizeof(buffer),
NULL,
NULL
);
if (!NT_SUCCESS(status))
{
break;
}
if (ioStatus.Information == 0)
{
break;
}
// traiter ioStatus.Information octets
}
31. Fermer le handle avec ZwClose
Tout handle ouvert doit être fermé.
La routine est :
Code: Select all
ZwClose
Code: Select all
if (fileHandle != NULL)
{
ZwClose(fileHandle);
fileHandle = NULL;
}
Par exemple, si une requête utilisateur ouvre un fichier à chaque appel sans fermer le handle, la table de handles du système peut progressivement se remplir.
32. Portée des handles
Chaque processus possède normalement une table de handles.
Lorsqu’un handle est créé dans le contexte du processus courant sans précaution particulière, ce handle peut être associé à cette table.
Un driver ne doit donc pas considérer qu’un handle créé dans un contexte arbitraire pourra être utilisé de façon sûre depuis n’importe quel autre contexte.
C’est une des raisons pour lesquelles :
Code: Select all
OBJ_KERNEL_HANDLE
Un handle kernel est créé dans un espace réservé au noyau et n’est pas destiné à être manipulé depuis le mode utilisateur.
33. Contexte de processus dans un driver
Un driver n’est pas exécuté dans un unique processus.
Selon la source de l’appel, le thread courant peut être attaché au contexte :
- d’un processus utilisateur ;
- du processus System ;
- d’un thread worker ;
- d’un autre processus.
Principe :
un driver doit faire la différence entre le contexte d’exécution du thread courant et la durée de vie logique de ses propres ressources.
34. Accès aux fichiers pendant le démarrage
Le moment où le driver tente d’accéder à un fichier est important.
Lors des premières phases du démarrage de Windows, tous les systèmes de fichiers et tous les volumes ne sont pas nécessairement disponibles de la même manière qu’après l’initialisation complète.
Un driver initialisé très tôt peut donc ne pas pouvoir accéder immédiatement à :
Code: Select all
C:\MonDossier\fichier.bin
35. IRP_MN_START_DEVICE et disponibilité du stockage
Un driver Plug and Play peut vouloir accéder à un fichier au moment de l’initialisation de son périphérique.
Le livre souligne qu’un accès peut être réalisé en réaction à :
Code: Select all
IRP_MN_START_DEVICE
Un driver doit éviter de créer une dépendance circulaire.
Exemple conceptuel :
- un driver nécessaire au montage du disque tente de lire un fichier sur ce même disque ;
- le fichier n’est accessible qu’une fois le disque monté ;
- le montage dépend du driver.
36. \SystemRoot
Le chemin :
Code: Select all
\SystemRoot
Il correspond au répertoire Windows du système installé.
Exemple :
Code: Select all
\SystemRoot\System32\Drivers\config.bin
L’utilisation de
Code: Select all
\SystemRootCode: Select all
C:37. Chemins NT et chemins Win32
Il est important de distinguer :
Code: Select all
C:\Temp\file.txt
Code: Select all
\??\C:\Temp\file.txt
Autre exemple :
Code: Select all
\SystemRoot\Temp\file.txt
Cette différence devient importante lorsque l’on travaille avec :
- fichiers ;
- devices ;
- symbolic links ;
- events ;
- mutex ;
- sections ;
- objets du registre.
38. Exemple d’écriture d’un fichier de log
Exemple simple :
Code: Select all
NTSTATUS WriteLog()
{
UNICODE_STRING fileName;
OBJECT_ATTRIBUTES attributes;
IO_STATUS_BLOCK ioStatus;
HANDLE fileHandle = NULL;
RtlInitUnicodeString(
&fileName,
L"\\??\\C:\\Temp\\driver.log"
);
InitializeObjectAttributes(
&attributes,
&fileName,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
NULL,
NULL
);
NTSTATUS status = ZwCreateFile(
&fileHandle,
GENERIC_WRITE,
&attributes,
&ioStatus,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN_IF,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
if (!NT_SUCCESS(status))
{
return status;
}
const CHAR message[] = "Driver started\r\n";
status = ZwWriteFile(
fileHandle,
NULL,
NULL,
NULL,
&ioStatus,
(PVOID)message,
sizeof(message) - 1,
NULL,
NULL
);
ZwClose(fileHandle);
return status;
}
Un vrai mécanisme de logs doit prendre en compte :
- la concurrence ;
- la position d’écriture ;
- la taille du fichier ;
- les performances ;
- la disponibilité du stockage ;
- les erreurs d’I/O.
39. Écrire à la fin du fichier
Lorsqu’un driver veut ajouter des données à un fichier existant, il faut comprendre la position d’écriture.
Une stratégie est :
- interroger la taille du fichier ;
- utiliser cette taille comme offset ;
- écrire à cet offset.
Code: Select all
FILE_STANDARD_INFORMATION info;
status = ZwQueryInformationFile(
fileHandle,
&ioStatus,
&info,
sizeof(info),
FileStandardInformation
);
LARGE_INTEGER offset = info.EndOfFile;
status = ZwWriteFile(
fileHandle,
NULL,
NULL,
NULL,
&ioStatus,
buffer,
length,
&offset,
NULL
);
Une synchronisation ou une stratégie atomique adaptée est alors nécessaire.
40. Gestion des erreurs
Un code kernel robuste doit gérer chaque erreur.
Exemple de structure :
Code: Select all
NTSTATUS status = ZwCreateFile(...);
if (!NT_SUCCESS(status))
{
return status;
}
status = ZwQueryInformationFile(...);
if (!NT_SUCCESS(status))
{
ZwClose(fileHandle);
return status;
}
Exemple :
- le fichier est ouvert ;
- le buffer est alloué ;
- la lecture échoue.
- libérer le buffer ;
- fermer le handle ;
- retourner le statut approprié.
41. Pattern de nettoyage
Une approche simple est de centraliser le nettoyage.
Exemple :
Code: Select all
NTSTATUS status = STATUS_SUCCESS;
HANDLE fileHandle = NULL;
PVOID buffer = NULL;
// opérations...
Cleanup:
if (buffer != NULL)
{
ExFreePool(buffer);
}
if (fileHandle != NULL)
{
ZwClose(fileHandle);
}
return status;
42. Attention aux locks
Une opération disque peut être lente.
Il est donc généralement déconseillé de conserver un verrou global important pendant :
Code: Select all
ZwReadFile
Code: Select all
ZwWriteFile
Code: Select all
KeAcquireSpinLock(...);
ZwWriteFile(...);
KeReleaseSpinLock(...);
- un spinlock élève généralement l’IRQL à DISPATCH_LEVEL ;
- exige un contexte compatible avec PASSIVE_LEVEL ;
Code: Select all
ZwWriteFile - l’I/O peut attendre.
Code: Select all
PASSIVE_LEVEL43. ZwCreateFile et la philosophie de l’Object Manager
L’accès fichier dans le noyau illustre une idée centrale de Windows NT :
de nombreuses ressources système sont représentées sous forme d’objets gérés par l’Object Manager.
Le modèle général est souvent :
- préparer un nom ;
- préparer des attributs ;
- ouvrir ou créer l’objet ;
- obtenir un handle ;
- effectuer des opérations ;
- fermer le handle.
- les fichiers ;
- le registre ;
- les processus ;
- les threads ;
- les événements ;
- les sémaphores ;
- les sections ;
- les devices.
44. Lien avec les routines Zw du registre
Les routines du registre utilisent des concepts très proches.
Exemples :
Code: Select all
ZwOpenKey
ZwCreateKey
ZwQueryKey
ZwQueryValueKey
ZwEnumerateKey
ZwEnumerateValueKey
ZwClose
- création d’un ;
Code: Select all
UNICODE_STRING - construction de ;
Code: Select all
OBJECT_ATTRIBUTES - obtention d’un handle ;
- requête d’informations ;
- fermeture avec .
Code: Select all
ZwClose
45. KEY_VALUE_BASIC_INFORMATION
Pour les valeurs du registre, une structure telle que :
Code: Select all
KEY_VALUE_BASIC_INFORMATION
Code: Select all
typedef struct _KEY_VALUE_BASIC_INFORMATION
{
ULONG TitleIndex;
ULONG Type;
ULONG NameLength;
WCHAR Name[1];
} KEY_VALUE_BASIC_INFORMATION,
*PKEY_VALUE_BASIC_INFORMATION;
- : type de la valeur ;
Code: Select all
Type - : longueur du nom en octets ;
Code: Select all
NameLength - : début du nom de la valeur.
Code: Select all
Name
Code: Select all
WCHAR Name[1]
La mémoire réellement fournie doit être suffisamment grande pour contenir la structure et la chaîne retournée.
46. ZwEnumerateValueKey
La routine :
Code: Select all
ZwEnumerateValueKey
Le principe général est d’appeler la fonction avec un index :
Code: Select all
0
1
2
3
...
Une classe d’information détermine le format du buffer retourné.
Exemple de format :
Code: Select all
KeyValueBasicInformation
Code: Select all
KEY_VALUE_BASIC_INFORMATION
47. ZwEnumerateKey
La routine :
Code: Select all
ZwEnumerateKey
Comme pour les valeurs, plusieurs classes d’informations existent.
Certaines retournent uniquement des informations minimales, d’autres retournent davantage de métadonnées.
Le choix dépend de ce que le driver doit réellement récupérer.
48. Routines ZwQuery*
Les routines :
Code: Select all
ZwQueryKey
ZwQueryValueKey
ZwQueryInformationFile
- un handle désigne l’objet ;
- une classe d’information indique le format attendu ;
- un buffer reçoit les données ;
- une taille indique la capacité du buffer ;
- un résultat indique la quantité nécessaire ou effectivement retournée.
49. Pattern de buffer de taille variable
Beaucoup de routines du noyau retournent des structures de taille variable.
Le schéma classique consiste à :
- appeler une première fois pour connaître la taille ;
- allouer un buffer ;
- rappeler la fonction ;
- interpréter la structure retournée.
Code: Select all
ULONG requiredLength = 0;
status = ZwQueryValueKey(
keyHandle,
&valueName,
KeyValueFullInformation,
NULL,
0,
&requiredLength
);
Code: Select all
buffer = ExAllocatePool2(
POOL_FLAG_PAGED,
requiredLength,
'geRK'
);
50. Comparaison Win32 / kernel
En user mode, un programme peut utiliser :
Code: Select all
CreateFileW
ReadFile
WriteFile
CloseHandle
GetFileSizeEx
Code: Select all
ZwCreateFile
ZwReadFile
ZwWriteFile
ZwClose
ZwQueryInformationFile
Le contexte kernel implique :
- des IRQL ;
- des APC ;
- des handles kernel ;
- des structures NT ;
- des contraintes de synchronisation ;
- une responsabilité plus importante en cas d’erreur.
51. API et structures essentielles à retenir
Routines principales
Code: Select all
ZwCreateFileCode: Select all
ZwReadFileCode: Select all
ZwWriteFileCode: Select all
ZwQueryInformationFileCode: Select all
ZwClose
Code: Select all
UNICODE_STRINGCode: Select all
OBJECT_ATTRIBUTESCode: Select all
IO_STATUS_BLOCKCode: Select all
FILE_STANDARD_INFORMATIONCode: Select all
LARGE_INTEGER
Code: Select all
OBJ_CASE_INSENSITIVECode: Select all
OBJ_KERNEL_HANDLECode: Select all
FILE_OPENCode: Select all
FILE_CREATECode: Select all
FILE_OPEN_IFCode: Select all
FILE_OVERWRITECode: Select all
FILE_OVERWRITE_IFCode: Select all
FILE_SHARE_READCode: Select all
FILE_SHARE_WRITECode: Select all
FILE_SYNCHRONOUS_IO_NONALERTCode: Select all
FILE_ATTRIBUTE_NORMAL
52. Concepts essentiels à retenir
- Les opérations fichier doivent être réalisées à .
Code: Select all
PASSIVE_LEVEL - Les opérations fichier peuvent attendre.
- Les APC et certaines primitives de synchronisation peuvent rendre le contexte incompatible avec l’I/O fichier.
- sert à ouvrir autant qu’à créer.
Code: Select all
ZwCreateFile - définit le comportement selon l’existence du fichier.
Code: Select all
CreateDisposition - décrit le résultat de l’opération.
Code: Select all
IO_STATUS_BLOCK - permet notamment de connaître le nombre d’octets transférés.
Code: Select all
ioStatus.Information - permet de récupérer des métadonnées.
Code: Select all
ZwQueryInformationFile - donne la taille logique du fichier.
Code: Select all
FILE_STANDARD_INFORMATION.EndOfFile - protège le handle du contexte utilisateur.
Code: Select all
OBJ_KERNEL_HANDLE - Un driver doit tenir compte du contexte de processus courant.
- Tous les handles doivent être fermés avec .
Code: Select all
ZwClose - Le stockage peut ne pas être totalement disponible pendant certaines phases du boot.
- permet de référencer le répertoire système sans supposer une lettre de lecteur.
Code: Select all
\SystemRoot
53. Pièges classiques
Piège 1 : appeler ZwReadFile à DISPATCH_LEVEL
Une I/O fichier n’est pas compatible avec un contexte arbitraire à IRQL élevé.
Piège 2 : conserver un spinlock pendant une opération fichier
L’opération peut bloquer et le spinlock impose un contexte incompatible.
Piège 3 : oublier OBJ_KERNEL_HANDLE
Le handle peut être associé au mauvais processus.
Piège 4 : oublier ZwClose
Cela crée une fuite de handle.
Piège 5 : supposer que la taille demandée a entièrement été transférée
Toujours examiner
Code: Select all
IO_STATUS_BLOCK.InformationPiège 6 : confondre FILE_OPEN et FILE_OPEN_IF
Code: Select all
FILE_OPENCode: Select all
FILE_OPEN_IFPiège 7 : confondre FILE_CREATE et FILE_OVERWRITE_IF
Code: Select all
FILE_CREATECode: Select all
FILE_OVERWRITE_IFPiège 8 : utiliser directement un chemin Win32 classique
Il faut utiliser un nom compatible avec le namespace NT attendu.
Piège 9 : charger un très gros fichier en une seule allocation
La lecture par blocs est souvent plus robuste.
Piège 10 : effectuer des I/O disque dans un chemin critique
Les accès disque peuvent être beaucoup plus lents qu’une opération purement mémoire.
54. Exemple final : lecture sûre et structurée
Code: Select all
NTSTATUS ReadFileExample()
{
NTSTATUS status;
HANDLE fileHandle = NULL;
UNICODE_STRING fileName;
OBJECT_ATTRIBUTES attributes;
IO_STATUS_BLOCK ioStatus;
FILE_STANDARD_INFORMATION info;
PVOID buffer = NULL;
RtlInitUnicodeString(
&fileName,
L"\\??\\C:\\Temp\\data.bin"
);
InitializeObjectAttributes(
&attributes,
&fileName,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
NULL,
NULL
);
status = ZwCreateFile(
&fileHandle,
GENERIC_READ,
&attributes,
&ioStatus,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
if (!NT_SUCCESS(status))
{
goto Cleanup;
}
status = ZwQueryInformationFile(
fileHandle,
&ioStatus,
&info,
sizeof(info),
FileStandardInformation
);
if (!NT_SUCCESS(status))
{
goto Cleanup;
}
if (info.EndOfFile.QuadPart == 0)
{
status = STATUS_SUCCESS;
goto Cleanup;
}
if (info.EndOfFile.QuadPart > MAXULONG)
{
status = STATUS_FILE_TOO_LARGE;
goto Cleanup;
}
ULONG fileSize = (ULONG)info.EndOfFile.QuadPart;
buffer = ExAllocatePool2(
POOL_FLAG_PAGED,
fileSize,
'eliF'
);
if (buffer == NULL)
{
status = STATUS_INSUFFICIENT_RESOURCES;
goto Cleanup;
}
status = ZwReadFile(
fileHandle,
NULL,
NULL,
NULL,
&ioStatus,
buffer,
fileSize,
NULL,
NULL
);
if (!NT_SUCCESS(status))
{
goto Cleanup;
}
KdPrint((
"Bytes lus : %llu\n",
(unsigned long long)ioStatus.Information
));
Cleanup:
if (buffer != NULL)
{
ExFreePool(buffer);
}
if (fileHandle != NULL)
{
ZwClose(fileHandle);
}
return status;
}
55. Schéma mental final
L’accès fichier kernel peut être résumé par la chaîne suivante :
Code: Select all
Nom NT
|
v
UNICODE_STRING
|
v
OBJECT_ATTRIBUTES
|
v
ZwCreateFile
|
v
HANDLE
|
+--> ZwReadFile
|
+--> ZwWriteFile
|
+--> ZwQueryInformationFile
|
v
ZwClose
Code: Select all
PASSIVE_LEVEL
+
contexte de thread compatible
+
gestion correcte des APC
+
handles kernel
+
nettoyage systématique
56. À retenir en priorité
Pour retenir l’essentiel sans mémoriser tous les détails :
- ouvre ou crée.
Code: Select all
ZwCreateFile - lit.
Code: Select all
ZwReadFile - écrit.
Code: Select all
ZwWriteFile - récupère les métadonnées.
Code: Select all
ZwQueryInformationFile - ferme.
Code: Select all
ZwClose - décrit l’objet.
Code: Select all
OBJECT_ATTRIBUTES - contient son nom.
Code: Select all
UNICODE_STRING - contient le résultat de l’I/O.
Code: Select all
IO_STATUS_BLOCK - évite de lier un handle du driver au processus utilisateur courant.
Code: Select all
OBJ_KERNEL_HANDLE - permet de récupérer la taille d’un fichier.
Code: Select all
FILE_STANDARD_INFORMATION.EndOfFile - Les opérations fichier sont réalisées à .
Code: Select all
PASSIVE_LEVEL - Il ne faut pas conserver un spinlock ou un contexte incompatible pendant une I/O.
- Les chemins noyau appartiennent au namespace NT.
- Le moment du boot auquel le fichier est utilisé peut déterminer s’il est accessible ou non.
