Filesystem Windows en Kernel Mode

Ce cours dédiée au développement de drivers en kernel mode sans Framework

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 144
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Filesystem Windows en Kernel Mode

Post by Hydraxx »

Filesystem Windows en Kernel Mode

Le filesystem Windows ne se résume pas à ZwCreateFile, ZwReadFile et ZwWriteFile. Derrière une simple ouverture de fichier se trouvent l'I/O Manager, les objets FILE_OBJECT, les pilotes de système de fichiers, les volumes, les chemins NT, les structures FILE_INFORMATION_CLASS et plusieurs formats de buffers variables.

Ce cours regroupe les notions générales nécessaires pour comprendre les fichiers et répertoires côté noyau sans se limiter à un type de driver particulier.


Sommaire
  • 1. Architecture générale du chemin I/O
  • 2. FILE_OBJECT
  • 3. Volumes et Device Objects
  • 4. Chemins DOS, NT et Device Paths
  • 5. Symbolic Links et lettres de lecteurs
  • 6. Reparse Points et résolution de chemin
  • 7. Streams NTFS
  • 8. IRP_MJ_CREATE
  • 9. READ, WRITE et cache
  • 10. CLEANUP et CLOSE
  • 11. FILE_INFORMATION_CLASS
  • 12. FILE_STANDARD_INFORMATION
  • 13. FILE_BASIC_INFORMATION
  • 14. FILE_NAME_INFORMATION et structures variables
  • 15. Suppression de fichiers
  • 16. Renommage de fichiers
  • 17. IRP_MJ_DIRECTORY_CONTROL
  • 18. Structures d'énumération de répertoire
  • 19. NextEntryOffset
  • 20. Parcours sécurisé d'un buffer variable
  • 21. Lien avec SYSTEM_PROCESS_INFORMATION
  • 22. Modification d'un buffer d'énumération
  • 23. Bonnes pratiques

1. Architecture générale du chemin I/O

Lorsqu'une application ouvre ou manipule un fichier, l'opération traverse plusieurs couches.

Code: Select all

Application
    |
    v
Win32 / Native API
    |
    v
I/O Manager
    |
    v
File System Driver
    |
    v
Volume
    |
    v
Storage Stack
    |
    v
Périphérique
Le I/O Manager construit les requêtes destinées aux drivers. Le système de fichiers interprète ensuite les chemins, les attributs, les répertoires, les métadonnées et les droits.

Parmi les opérations importantes :
  • IRP_MJ_CREATE
  • IRP_MJ_READ
  • IRP_MJ_WRITE
  • IRP_MJ_QUERY_INFORMATION
  • IRP_MJ_SET_INFORMATION
  • IRP_MJ_DIRECTORY_CONTROL
  • IRP_MJ_CLEANUP
  • IRP_MJ_CLOSE

2. FILE_OBJECT

Un FILE_OBJECT représente une ouverture d'un objet géré par l'I/O Manager.

Il peut représenter :
  • un fichier
  • un répertoire
  • un périphérique
  • une ouverture particulière d'un objet de type fichier
Champs souvent rencontrés :

Code: Select all

DeviceObject
FileName
Flags
FsContext
FsContext2
Il ne faut pas considérer FileObject->FileName comme un chemin toujours complet ou normalisé. Sa signification dépend du moment où l'objet est observé.


3. Volumes et Device Objects

Un volume est généralement représenté dans le namespace NT par un nom tel que :

Code: Select all

\Device\HarddiskVolume3
La lettre :

Code: Select all

C:
n'est qu'un alias DOS.

Un volume est associé à une pile de Device Objects. Une requête peut traverser plusieurs drivers avant d'atteindre le périphérique de stockage.


4. Chemins DOS, NT et Device Paths

Il existe plusieurs représentations d'un même fichier.

Exemples :

Code: Select all

C:\Windows\System32\kernel32.dll
\??\C:\Windows\System32\kernel32.dll
\Device\HarddiskVolume3\Windows\System32\kernel32.dll
Le chemin DOS est pratique côté user mode.

Le chemin NT reflète davantage le namespace natif.

Le préfixe :

Code: Select all

\??\
permet d'accéder à l'espace des noms DOS correspondant au contexte courant.


5. Symbolic Links et lettres de lecteurs

Les lettres de lecteur reposent sur des symbolic links.

Conceptuellement :

Code: Select all

C: -> \Device\HarddiskVolume3
Dans le namespace global, ces liens peuvent apparaître sous :

Code: Select all

\GLOBAL??
Un driver peut aussi exposer son propre Device Object via un symbolic link :

Code: Select all

\Device\MyDriver
\DosDevices\MyDriver
et être ouvert côté user mode via :

Code: Select all

\\.\MyDriver

6. Reparse Points et résolution de chemin

Les reparse points permettent de modifier la résolution d'un chemin.

Ils sont utilisés notamment pour :
  • junctions
  • symbolic links NTFS
  • mount points
  • certains systèmes de fichiers ou services cloud
Comparer deux chaînes de caractères ne suffit donc pas toujours à prouver qu'elles désignent deux objets différents.


7. Streams NTFS

NTFS permet plusieurs flux pour un même fichier.

Exemple :

Code: Select all

C:\Test\file.txt
C:\Test\file.txt:metadata
Le second chemin cible un Alternate Data Stream.

Le code kernel qui analyse des noms de fichiers doit tenir compte de cette possibilité.


8. IRP_MJ_CREATE

Le nom CREATE peut prêter à confusion.

IRP_MJ_CREATE sert à ouvrir un objet, pas uniquement à créer un nouveau fichier.

Il peut correspondre à :
  • ouvrir un fichier existant
  • créer un fichier
  • ouvrir un répertoire
  • ouvrir un Device Object
  • ouvrir avec des droits spécifiques
Le résultat dépend notamment de :
  • DesiredAccess
  • ShareAccess
  • CreateDisposition
  • CreateOptions

9. READ, WRITE et cache

Une lecture ou écriture n'implique pas nécessairement un accès immédiat au disque.

Windows utilise notamment :
  • Cache Manager
  • Memory Manager
  • paging I/O
  • cached I/O
  • non-cached I/O
Un WRITE applicatif peut donc être différé, regroupé ou satisfait à travers le cache.


10. CLEANUP et CLOSE

IRP_MJ_CLEANUP et IRP_MJ_CLOSE sont deux étapes différentes.

CLEANUP :
  • correspond généralement à la fermeture du dernier handle utilisateur lié à cette ouverture
  • est souvent le moment où l'on arrête certaines opérations associées au handle
CLOSE :
  • arrive plus tard
  • intervient lorsque le FILE_OBJECT n'est plus référencé et peut être détruit
Confondre les deux peut provoquer des fuites ou des use-after-free.


11. FILE_INFORMATION_CLASS

Windows utilise FILE_INFORMATION_CLASS pour déterminer la nature des informations demandées ou modifiées.

Exemples :

Code: Select all

FileBasicInformation
FileStandardInformation
FileNameInformation
FileDispositionInformation
FileDispositionInformationEx
FileRenameInformation
La classe choisie détermine la structure réelle contenue dans le buffer.


12. FILE_STANDARD_INFORMATION

Cette classe retourne notamment :
  • AllocationSize
  • EndOfFile
  • NumberOfLinks
  • DeletePending
  • Directory
Exemple conceptuel :

Code: Select all

FILE_STANDARD_INFORMATION info;

ZwQueryInformationFile(
    FileHandle,
    &IoStatus,
    &info,
    sizeof(info),
    FileStandardInformation
);
Elle permet par exemple de connaître la taille logique d'un fichier.


13. FILE_BASIC_INFORMATION

Cette classe contient les informations classiques :
  • CreationTime
  • LastAccessTime
  • LastWriteTime
  • ChangeTime
  • FileAttributes
Elle sert à lire ou modifier certaines métadonnées de base.


14. FILE_NAME_INFORMATION et structures variables

Certaines structures se terminent par un tableau de taille variable :

Code: Select all

typedef struct _FILE_NAME_INFORMATION {
    ULONG FileNameLength;
    WCHAR FileName[1];
} FILE_NAME_INFORMATION;
Le buffer réel est donc plus grand que :

Code: Select all

sizeof(FILE_NAME_INFORMATION)
Il faut toujours utiliser FileNameLength.


15. Suppression de fichiers

Une suppression n'est pas forcément représentée par une opération unique.

On peut rencontrer :
  • FILE_DELETE_ON_CLOSE lors d'une ouverture
  • FileDispositionInformation
  • FileDispositionInformationEx
Exemple :

Code: Select all

typedef struct _FILE_DISPOSITION_INFORMATION {
    BOOLEAN DeleteFile;
} FILE_DISPOSITION_INFORMATION;
Sur les versions modernes, les variantes étendues utilisent des flags.


16. Renommage de fichiers

Le renommage utilise notamment :

Code: Select all

FileRenameInformation
La structure associée contient des données variables.

Comme toujours :
  • valider la taille du buffer
  • vérifier la classe
  • ne pas lire au-delà de la longueur annoncée

17. IRP_MJ_DIRECTORY_CONTROL

Les opérations sur les répertoires utilisent :

Code: Select all

IRP_MJ_DIRECTORY_CONTROL
avec notamment :

Code: Select all

IRP_MN_QUERY_DIRECTORY
Le filesystem remplit un buffer d'entrées décrivant les fichiers et sous-répertoires.


18. Structures d'énumération de répertoire

Le format varie selon FileInformationClass.

Exemples :
  • FileDirectoryInformation
  • FileFullDirectoryInformation
  • FileBothDirectoryInformation
  • FileNamesInformation
  • FileIdBothDirectoryInformation
  • FileIdFullDirectoryInformation
Chaque classe correspond à une structure différente.


19. NextEntryOffset

Les entrées sont souvent chaînées par un offset :

Code: Select all

ULONG NextEntryOffset;
Exemple :

Code: Select all

Entry A
   + NextEntryOffset
        |
        v
Entry B
   + NextEntryOffset
        |
        v
Entry C
   NextEntryOffset = 0
Zéro indique la dernière entrée.


20. Parcours sécurisé d'un buffer variable

Pseudo-code :

Code: Select all

PUCHAR current = buffer;
PUCHAR end = current + bufferSize;

while (current < end) {
    auto entry = reinterpret_cast<PFILE_DIRECTORY_INFORMATION>(current);

    // traiter l'entrée

    if (entry->NextEntryOffset == 0)
        break;

    if (current + entry->NextEntryOffset >= end)
        break;

    current += entry->NextEntryOffset;
}
Le code réel doit également vérifier les longueurs internes.


21. Lien avec SYSTEM_PROCESS_INFORMATION

Le pattern NextEntryOffset ne concerne pas uniquement les fichiers.

On retrouve la même idée dans :

Code: Select all

SYSTEM_PROCESS_INFORMATION
Une fois ce pattern compris, il devient facile de lire de nombreuses structures natives de Windows.


22. Modification d'un buffer d'énumération

Modifier un buffer doit préserver sa cohérence.

Pour retirer une entrée intermédiaire, l'entrée précédente peut être modifiée pour pointer vers la suivante.

Conceptuellement :

Code: Select all

A -> B -> C

devient

A -----> C
Le cas de la première entrée est différent : il faut parfois déplacer le reste du buffer.

Toute modification incorrecte de taille ou d'offset peut provoquer une corruption ou un crash.


23. Bonnes pratiques
  • Valider les tailles avant chaque cast
  • Ne jamais faire confiance à un buffer variable sans contrôle
  • Distinguer chemin DOS et chemin NT
  • Ne pas confondre CLEANUP et CLOSE
  • Tenir compte des reparse points
  • Ne pas supposer qu'un WRITE correspond à une écriture disque immédiate
  • Tester uniquement en VM lors des premières expérimentations kernel

Conclusion

Le filesystem Windows expose plusieurs couches qui se croisent : I/O Manager, FILE_OBJECT, Device Objects, volumes, namespace NT, classes d'informations et buffers variables. Le comprendre comme un ensemble cohérent permet ensuite de manipuler les fichiers proprement depuis le kernel et de lire les structures retournées par Windows sans dépendre d'un framework particulier.


Exercices
  • Afficher le DeviceObject et le FileName d'un FILE_OBJECT dans un driver de test.
  • Interroger FileStandardInformation puis FileBasicInformation sur un fichier.
  • Parser un buffer FILE_DIRECTORY_INFORMATION sans le modifier.
  • Comparer un chemin DOS et son Device Path natif dans WinObj.

Who is online

Users browsing this forum: No registered users and 1 guest