Gestion de la mémoire tampon

L’erreur la plus courante au sein d’un pilote est peut-être liée à la gestion des mémoires tampons où les mémoires tampons ne sont pas valides ou trop petites. Ces erreurs peuvent autoriser des dépassements de mémoire tampon ou provoquer des incidents système, qui constituent des compromis pour la sécurité du système.

Du point de vue d’un pilote, les mémoires tampons sont de l’une des deux variétés suivantes :

  • Mémoires tampons paginées, qui peuvent résider ou non dans la mémoire.

  • Mémoires tampons non paginées, qui doivent résider dans la mémoire.

Bien entendu, une adresse non valide n’est ni paginée ni non paginée, mais à mesure que le système d’exploitation commence à résoudre l’erreur de page qu’une mémoire tampon provoque, il isolera 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) et déclenchera le type d’erreur approprié. Les erreurs de mémoire tampon sont toujours gérées par un bogue case activée (PAGE_FAULT_IN_NONPAGED_AREA, par exemple) ou par une exception (STATUS_ACCESS_VIOLATION, par exemple). Dans le cas d’un bogue case activée, le système arrête le fonctionnement. Dans le cas d’une exception, les gestionnaires d’exceptions basés sur la pile sont appelés et, si aucun d’entre eux ne gère l’exception, un bogue case activée est appelé.

Quoi qu’il en soit, tout chemin d’accès qui peut être appelé par un programme d’application entraînant un bogue case activée est une violation de sécurité au sein du pilote. Cela permet à une application de provoquer des attaques par déni de service sur l’ensemble du système.

L’un des problèmes les plus courants dans ce domaine est que les enregistreurs de pilotes assument trop de choses sur l’environnement d’exploitation. Vous pouvez inclure ce qui suit :

  • Vérification que le bit élevé est défini dans l’adresse. Cela ne fonctionne pas sur les ordinateurs x86 où le système utilise l’optimisation de quatre gigaoctets (4GT) en définissant l’option /3 Go dans le fichier Boot.ini. Dans ce cas, les adresses en mode utilisateur définissent le bit élevé pour le troisième gigaoctet (Go) de l’espace d’adressage.

  • Utilisation de ProbeForRead et ProbeForWrite pour valider l’adresse. Bien que cela garantisse que l’adresse est valide en mode utilisateur au moment de la sonde, rien ne l’oblige à rester valide après l’opération de sonde. Ainsi, cette technique introduit une condition de race subtile qui peut conduire à des incidents irréductibles périodiques. Les appels ProbeForRead et ProbeForWrite sont nécessaires pour une raison différente : pour vérifier si 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. Si la sonde est omise, les utilisateurs peuvent passer des adresses valides en mode noyau, qui ne seront pas interceptées par un bloc __try et __except (gestion structurée des exceptions) et ouvriront un trou de sécurité important. Les appels ProbeForRead et ProbeForWrite sont donc nécessaires pour garantir l’alignement et que l’adresse en mode utilisateur, plus la longueur, se trouve dans la plage d’adresses de l’utilisateur. Toutefois, un __try et un bloc __except sont nécessaires pour vous protéger contre l’accès.

    Notez que ProbeForRead vérifie uniquement que l’adresse et la longueur sont comprises dans la plage d’adresses possible en mode utilisateur (légèrement inférieure à 2 Go pour un système sans 4GT, par exemple), et non pas 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 qu’il s’agit d’adresses mémoire valides.

  • En s’appuyant sur les fonctions du gestionnaire de mémoire (MmIsAddressValid, par exemple) pour vous assurer que l’adresse est valide. Comme pour les fonctions de sonde, cela introduit une condition de concurrence qui peut entraîner des plantages irréductibles.

  • Échec de l’utilisation de la gestion structurée des exceptions. Les fonctions __try et __except au sein 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 levées en appelant ExRaiseStatus ou l’une des fonctions associées. Un pilote qui ne parvient pas à utiliser la gestion structurée des exceptions autour d’un appel susceptible de déclencher une exception entraîne un bogue case activée (généralement KMODE_EXCEPTION_NOT_HANDLED).

    Notez qu’il est erroné d’utiliser une gestion structurée des exceptions autour du code qui n’est pas censé générer des erreurs. Cela masquera simplement les bogues réels qui seraient autrement trouvés. Placer un wrapper __try et __except au niveau de répartition supérieur de votre routine n’est pas la bonne solution à ce problème, bien qu’il s’agisse parfois de la solution réflexe essayée par les enregistreurs de pilotes.

  • En s’appuyant sur le contenu de la mémoire utilisateur qui reste stable. Par exemple, supposons qu’un pilote écrive une valeur dans un emplacement de mémoire en mode utilisateur, puis, plus tard, dans la même routine, faire référence à cet emplacement de mémoire. Une application malveillante peut modifier activement cette mémoire et, par conséquent, provoquer un blocage du pilote.

Pour les systèmes de fichiers, ces problèmes sont particulièrement graves, car ils reposent généralement sur l’accès direct aux mémoires tampons utilisateur (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. Les pilotes doivent donc se protéger contre des problèmes similaires si les E/S rapides sont prises en charge.

Le WDK contient de nombreux exemples de validation de mémoire tampon dans l’exemple de code 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 autour du code à zéro mémoire tampon utilisateur. Notez que l’exemple de code dans CdCommonRead semble utiliser les mots clés try et except. Dans l’environnement WDK, ces mots clés en C sont définis en termes d’extensions de compilateur __try et __except. Toute personne utilisant du code C++ doit utiliser les types de compilateur natifs pour gérer correctement les exceptions, car __try 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.