Pufferbehandlung

Der wahrscheinlich häufigste Fehler innerhalb eines Treibers bezieht sich auf die Pufferbehandlung, bei der Puffer ungültig oder zu klein sind. Diese Fehler können Pufferüberläufe oder Systemabstürze verursachen, die Sicherheitsrisiken für das System darstellen.

Aus der Perspektive eines Treibers gibt es Puffer in einer von zwei Varianten:

  • Ausgelagerte Puffer, die sich möglicherweise im Arbeitsspeicher befinden oder nicht.

  • Nicht ausgelagerte Puffer, die sich im Arbeitsspeicher befinden müssen.

Natürlich ist eine ungültige Adresse weder ausgelagert noch nicht ausgelagert, aber wenn das Betriebssystem beginnt, den Seitenfehler zu beheben, den ein solcher Puffer verursacht, isoliert es die ungültige Adresse in einen der "Standard"-Adressbereiche (ausgelagerte Kerneladressen, nicht ausgelagerte Kerneladressen oder Benutzeradressen) und löst den entsprechenden Fehlertyp aus. Pufferfehler werden immer entweder durch eine Fehlerprüfung (z. B. PAGE_FAULT_IN_NONPAGED_AREA) oder durch eine Ausnahme (z. B. STATUS_ACCESS_VIOLATION) behandelt. Im Falle einer Fehlerüberprüfung wird der Vorgang vom System angehalten. Im Falle einer Ausnahme werden die stapelbasierten Ausnahmehandler aufgerufen, und wenn keiner von ihnen die Ausnahme behandelt, wird eine Fehlerüberprüfung aufgerufen.

Unabhängig davon ist jeder Zugriffspfad, der von einem Anwendungsprogramm aufgerufen werden kann, das dazu führt, dass der Treiber zu einer Fehlerüberprüfung führt, eine Sicherheitsverletzung innerhalb des Treibers. Dadurch kann eine Anwendung Denial-of-Service-Angriffe auf das gesamte System auslösen.

Eines der häufigsten Probleme in diesem Bereich ist, dass Treiberschreiber zu viel von der Betriebsumgebung ausgehen. Sie könnte Folgendes enthalten:

  • Überprüfen, ob das hohe Bit in der Adresse festgelegt ist. Dies funktioniert nicht auf x86-basierten Computern, auf denen das System die Vier-Gigabyte-Optimierung (4GT) verwendet, indem die Option /3GB in der Boot.ini-Datei festgelegt wird. In diesem Fall legen Benutzermodusadressen das hohe Bit für das dritte Gigabyte (GB) des Adressraums fest.

  • Verwenden von ProbeForRead und ProbeForWrite zum Überprüfen der Adresse. Dadurch wird zwar sichergestellt, dass die Adresse zum Zeitpunkt des Tests eine gültige Benutzermodusadresse ist, aber es ist nichts erforderlich, dass sie nach dem Testvorgang gültig bleibt. Daher führt diese Technik zu einer subtilen Racebedingung, die zu regelmäßigen unproduzierten Abstürze führen kann. ProbeForRead - und ProbeForWrite-Aufrufe sind aus einem anderen Grund erforderlich: um zu überprüfen, ob es sich bei der Adresse um eine Adresse im Benutzermodus handelt und ob die Länge des Puffers innerhalb des Benutzeradressbereichs liegt. Wenn der Test ausgelassen wird, können Benutzer gültige Kernelmodusadressen übergeben, die nicht von einem __try und __except-Block (strukturierte Ausnahmebehandlung) abgefangen werden und eine große Sicherheitslücke öffnen. Daher sind ProbeForRead - und ProbeForWrite-Aufrufe erforderlich, um die Ausrichtung sicherzustellen und sicherzustellen, dass sich die Benutzermodusadresse und die Länge innerhalb des Benutzeradressbereichs befinden. Es ist jedoch ein __try- und __except-Block erforderlich, um den Zugriff zu schützen.

    Beachten Sie, dass ProbeForRead nur überprüft, ob die Adresse und die Länge innerhalb des möglichen Adressbereichs des Benutzermodus liegen (z. B. etwas unter 2 GB für ein System ohne 4GT), und nicht, ob die Speicheradresse gültig ist. Im Gegensatz dazu versucht ProbeForWrite , auf das erste Byte auf jeder Seite der angegebenen Länge zuzugreifen, um zu überprüfen, ob es sich um gültige Speicheradressen handelt.

  • Verwenden von Speicher-Manager-Funktionen (z. B. MmIsAddressValid), um sicherzustellen, dass die Adresse gültig ist. Wie bei den Testfunktionen führt dies zu einer Racebedingung, die zu unproduzierten Abstürze führen kann.

  • Fehler bei der Verwendung der strukturierten Ausnahmebehandlung. Die funktionen __try und __except innerhalb des Compilers verwenden unterstützung auf Betriebssystemebene für die Ausnahmebehandlung. Ausnahmen auf Kernelebene werden durch Aufrufen von ExRaiseStatus oder einer der zugehörigen Funktionen ausgelöst. Ein Treiber, der keine strukturierte Ausnahmebehandlung für jeden Aufruf verwendet, der eine Ausnahme auslösen kann, führt zu einer Fehlerüberprüfung (in der Regel KMODE_EXCEPTION_NOT_HANDLED).

    Beachten Sie, dass es falsch ist, strukturierte Ausnahmebehandlung für Code zu verwenden, von dem nicht erwartet wird, dass Fehler ausgelöst werden. Dadurch werden nur echte Fehler maskierung, die andernfalls gefunden würden. Das Platzieren eines __try und __except Wrappers auf der obersten Dispatchebene Ihrer Routine ist nicht die richtige Lösung für dieses Problem, obwohl es manchmal die reflexartige Lösung ist, die von Treiberautoren versucht wird.

  • Der Inhalt des Benutzerarbeitsspeichers bleibt stabil. Angenommen, ein Treiber sollte einen Wert in einen Speicherspeicherort im Benutzermodus schreiben und dann später in derselben Routine auf diesen Speicherspeicherort verweisen. Eine böswillige Anwendung kann diesen Arbeitsspeicher aktiv ändern und als Folge davon führen, dass der Treiber abstürzt.

Bei Dateisystemen sind diese Probleme besonders schwerwiegend, da sie in der Regel auf den direkten Zugriff auf Benutzerpuffer (die METHOD_NEITHER-Übertragungsmethode) basieren. Solche Treiber manipulieren Benutzerpuffer direkt und müssen daher vorsichtshalber Methoden für die Pufferverarbeitung integrieren, um Abstürze auf Betriebssystemebene zu vermeiden. Schnelle E/A übergibt immer unformatierte Speicherzeiger, sodass Treiber vor ähnlichen Problemen schützen müssen, wenn schnelle E/A-Vorgänge unterstützt werden.

Das WDK enthält zahlreiche Beispiele für die Pufferüberprüfung im FASTFAT- und CDFS-Dateisystembeispielcode, einschließlich:

  • Die FatLockUserBuffer-Funktion in fastfat\deviosup.c verwendet MmProbeAndLockPages , um die physischen Seiten hinter dem Benutzerpuffer und MmGetSystemAddressForMdlSafe in FatMapUserBuffer zu sperren, um eine virtuelle Zuordnung für die gesperrten Seiten zu erstellen.

  • Die FatGetVolumeBitmap-Funktion in fastfat\fsctl.c verwendet ProbeForRead und ProbeForWrite , um Benutzerpuffer in der Defragmentierungs-API zu überprüfen.

  • Die CdCommonRead-Funktion in cdfs\read.c verwendet __try und __except um Code zu null Benutzerpuffern. Beachten Sie, dass der Beispielcode in CdCommonRead die Schlüsselwörter try und außer verwendet. In der WDK-Umgebung sind diese Schlüsselwörter in C in Bezug auf die Compilererweiterungen __try und __except definiert. Jeder, der C++-Code verwendet, muss die nativen Compilertypen verwenden, um Ausnahmen ordnungsgemäß zu behandeln, da __try ein C++-Schlüsselwort (keyword), aber kein C-Schlüsselwort (keyword) ist und eine Form der C++-Ausnahmebehandlung bereitstellt, die für Kerneltreiber nicht gültig ist.