Gestion des mémoires tampons
L’une des erreurs les plus courantes au sein d’un pilote concerne la gestion des mémoires tampons, où les mémoires tampons ne sont pas valides ou trop petites. Ces erreurs peuvent autoriser les dépassements de mémoire tampon ou provoquer des incidents système, ce qui peut compromettre la sécurité du système. Cet article décrit certains des problèmes courants liés à la gestion des mémoires tampons et explique comment les éviter. Il identifie également l’exemple de code WDK qui illustre les techniques de gestion des mémoires tampons appropriées.
Types de mémoires tampons et adresses non valides
Du point de vue d’un pilote, les mémoires tampons sont fournies dans l’une des deux variétés suivantes :
Mémoires tampons paginées, qui peuvent ou ne pas être résident dans la mémoire.
Mémoires tampons non paginés, qui doivent être résident dans la mémoire.
Une adresse mémoire non valide n’est pas paginée ni non paginée. Étant donné que le système d’exploitation fonctionne pour résoudre une erreur de page causée par une gestion incorrecte de la mémoire tampon, procédez comme suit :
Il isole l’adresse non valide dans l’une des plages d’adresses « standard » (adresses de noyau paginées, adresses de noyau non paginées ou adresses utilisateur).
Elle déclenche le type d’erreur approprié. Le système gère toujours les erreurs de mémoire tampon soit par une vérification de bogue, telle que PAGE_FAULT_IN_NONPAGED_AREA, soit par une exception telle que STATUS_ACCESS_VIOLATION. Si l’erreur est une vérification de bogue, le système arrête l’opération. Dans le cas d’une exception, le système appelle des gestionnaires d’exceptions basés sur la pile. Si aucun gestionnaire d’exceptions ne gère l’exception, le système appelle une vérification de bogue.
Quel que soit le chemin d’accès qu’un programme d’application peut appeler, le pilote peut entraîner une vérification de bogue est une violation de sécurité au sein du pilote. Une telle violation permet à une application de provoquer des attaques par déni de service à l’ensemble du système.
Hypothèses et erreurs courantes
L’un des problèmes les plus courants dans ce domaine est que les enregistreurs de pilotes supposent trop sur l’environnement d’exploitation. Voici quelques hypothèses et erreurs courantes :
Un pilote vérifie simplement si le bit élevé est défini dans l’adresse. S’appuyer sur un modèle de bits fixe pour déterminer le type d’adresse ne fonctionne pas sur tous les systèmes ou scénarios. Par exemple, cette vérification ne fonctionne pas sur les ordinateurs x86 lorsque le système utilise le réglage de quatre gigaoctets (4GT). Lorsque 4GT est utilisé, les adresses en mode utilisateur définissent le bit élevé pour le troisième gigaoctet de l’espace d’adressage.
Pilote utilisant uniquement ProbeForRead et ProbeForWrite pour valider l’adresse. Ces appels garantissent que l’adresse est une adresse en mode utilisateur valide au moment de la sonde. Toutefois, il n’existe aucune garantie que cette adresse reste valide après l’opération de sonde. Ainsi, cette technique introduit une condition de race subtile qui peut entraîner des plantages irréductibles périodiques.
Les appels ProbeForRead et ProbeForWrite sont toujours nécessaires. Si un pilote omet la sonde, les utilisateurs peuvent passer des adresses en mode noyau valides qu’un
__try
et__except
un bloc (gestion structurée des exceptions) ne intercepte pas et ouvre donc un trou de sécurité important.La ligne de fond est que la gestion des exceptions de détection et structurées est nécessaire :
La détection vérifie que l’adresse est une adresse en mode utilisateur et que la longueur de la mémoire tampon se trouve dans la plage d’adresses utilisateur.
Un
__try/__except
bloc protège contre l’accès.
Notez que ProbeForRead valide uniquement que l’adresse et la longueur se trouvent dans la plage d’adresses en mode utilisateur possible (légèrement moins de 2 Go pour un système sans 4GT, par exemple), et non si l’adresse mémoire est valide. En revanche, ProbeForWrite tente d’accéder au premier octet de chaque page de la longueur spécifiée pour vérifier que ces octets sont des adresses mémoire valides.
Un pilote qui s’appuie sur des fonctions de gestionnaire de mémoire telles que MmIsAddressValid pour s’assurer que l’adresse est valide. Comme décrit pour les fonctions de sonde, cette situation introduit une condition de concurrence qui peut entraîner des plantages irréductibles.
Un pilote ne parvient pas à utiliser la gestion structurée des exceptions. Les
__try/except
fonctions du compilateur utilisent la prise en charge au niveau du système d’exploitation pour la gestion des exceptions. Les exceptions au niveau du noyau sont renvoyées au système par le biais d’un appel à ExRaiseStatus ou à l’une des fonctions associées. Un pilote qui ne parvient pas à utiliser la gestion des exceptions structurées autour d’un appel susceptible de déclencher une exception entraîne une vérification des bogues (généralement KMODE_EXCEPTION_NOT_HANDLED).Il est erroné d’utiliser la gestion structurée des exceptions autour du code qui n’est pas censé déclencher des erreurs. Cette utilisation masque simplement les vrais bogues qui seraient trouvés dans le cas contraire. Le fait de placer un
__try/__except
wrapper au niveau de répartition supérieur de votre routine n’est pas la solution correcte à ce problème, bien qu’il s’agit parfois de la solution réflexive essayée par les enregistreurs de pilotes.Un pilote supposant que le contenu de la mémoire utilisateur reste stable. Par exemple, supposons qu’un pilote écrivait une valeur dans un emplacement de mémoire en mode utilisateur, puis plus tard dans la même routine référencée à cet emplacement de mémoire. Une application malveillante peut modifier activement cette mémoire après l’écriture et, par conséquent, provoquer le blocage du pilote.
Pour les systèmes de fichiers, ces problèmes sont graves, car les systèmes de fichiers s’appuient généralement sur l’accès direct aux mémoires tampons utilisateur (la méthode de transfert METHOD_NEITHER). Ces pilotes manipulent directement les mémoires tampons utilisateur et doivent donc incorporer des méthodes de précaution pour la gestion des mémoires tampons afin d’éviter les incidents au niveau du système d’exploitation. Les E/S rapides passent toujours des pointeurs de mémoire brute, de sorte que les pilotes doivent se protéger contre des problèmes similaires si les E/S rapides sont prises en charge.
Exemple de code pour la gestion des mémoires tampons
Le WDK contient de nombreux exemples de validation de mémoire tampon dans l’exemple de code du pilote de système de fichiers fastfat et CDFS, notamment :
La fonction FatLockUserBuffer dans fastfat\deviosup.c utilise MmProbeAndLockPages pour verrouiller les pages physiques derrière la mémoire tampon utilisateur et MmGetSystemAddressForMdlSafe dans FatMapUserBuffer pour créer un mappage virtuel pour les pages verrouillées.
La fonction FatGetVolumeBitmap dans fastfat\fsctl.c utilise ProbeForRead et ProbeForWrite pour valider les mémoires tampons utilisateur dans l’API de défragmentation.
La fonction CdCommonRead dans cdfs\read.c utilise
__try
et__except
entoure le code vers zéro mémoire tampon utilisateur. L’exemple de code dans CdCommonRead apparaît pour utiliser les mots clés etexcept
lestry
mots clés. Dans l’environnement WDK, ces mots clés en C sont définis en termes d’extensions__try
du compilateur et__except
. Toute personne utilisant du code C++ doit utiliser les types de compilateur natifs pour gérer correctement les exceptions, comme__try
c’est un mot clé C++, mais pas un mot clé C, et fournit une forme de gestion des exceptions C++ qui n’est pas valide pour les pilotes de noyau.