Pufferbehandlung

Einer der häufigsten 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 zulassen oder Systemabstürzen verursachen, was die Systemsicherheit beeinträchtigen kann. In diesem Artikel werden einige der häufig auftretenden Probleme mit der Pufferbehandlung und deren Vermeidung erläutert. Außerdem werden WDK-Beispielcode identifiziert, der die richtigen Techniken für die Pufferbehandlung veranschaulicht.

Puffertypen und ungültige Adressen

Aus der Perspektive des Fahrers kommen Puffer in einer von zwei Varianten:

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

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

Eine ungültige Speicheradresse ist weder ausgelagert noch nicht ausgelagert. Da das Betriebssystem funktioniert, um einen Seitenfehler zu beheben, der durch eine falsche Pufferbehandlung verursacht wird, werden die folgenden Schritte ausgeführt:

  • Sie isoliert die ungültige Adresse in einen der Standardmäßigen Adressbereiche (seitenseitige Kerneladressen, nicht seitenseitige Kerneladressen oder Benutzeradressen).

  • Er löst den geeigneten Fehlertyp aus. Das System behandelt pufferfehler immer entweder durch eine Fehlerüberprüfung wie PAGE_FAULT_IN_NONPAGED_AREA oder durch eine Ausnahme wie STATUS_ACCESS_VIOLATION. Wenn es sich bei dem Fehler um eine Fehlerüberprüfung handelt, wird der Betrieb des Systems angehalten. Im Falle einer Ausnahme ruft das System stapelbasierte Ausnahmehandler auf. Wenn keines von Ausnahmehandlern die Ausnahme behandelt, ruft das System eine Fehlerüberprüfung auf.

Unabhängig davon ist jeder Zugriffspfad, den ein Anwendungsprogramm aufrufen kann, der dazu führt, dass der Treiber zu einer Fehlerüberprüfung führt, eine Sicherheitsverletzung innerhalb des Treibers ist. Eine solche Verletzung ermöglicht es einer Anwendung, Denial-of-Service-Angriffe auf das gesamte System zu verursachen.

Häufige Annahmen und Fehler

Eines der häufigsten Probleme in diesem Bereich ist, dass Treiberautoren zu viel von der Betriebsumgebung ausgehen. Einige häufige Annahmen und Fehler umfassen:

  • Ein Treiber überprüft einfach, ob das hohe Bit in der Adresse festgelegt ist. Die Verwendung eines festen Bitmusters zum Ermitteln des Adresstyps funktioniert nicht auf allen Systemen oder Szenarien. Diese Überprüfung funktioniert beispielsweise nicht auf x86-basierten Computern, wenn das System vier Gigabyte Tuning (4GT) verwendet. Wenn 4GT verwendet wird, legen Benutzermodusadressen das hohe Bit für das dritte Gigabyte des Adressraums fest.

  • Ein Treiber, der ausschließlich ProbeForRead und ProbeForWrite verwendet, um die Adresse zu überprüfen. Diese Aufrufe stellen sicher, dass es sich bei der Adresse um eine gültige Benutzermodusadresse zum Zeitpunkt der Probe handelt. Es gibt jedoch keine Garantien, dass diese Adresse nach dem Probevorgang gültig bleibt. Daher führt diese Technik eine subtile Racebedingung ein, die zu regelmäßigen unerklärlichen Abstürze führen kann.

    ProbeForRead - und ProbeForWrite-Aufrufe sind weiterhin erforderlich. Wenn ein Treiber den Prüfpunkt ausgelassen, können Benutzer gültige Kernelmodusadressen übergeben, die ein und __except ein __try Block (strukturierte Ausnahmebehandlung) nicht erfasst und somit ein großes Sicherheitsloch öffnet.

    Die untere Linie besteht darin, dass sowohl die Probing- als auch die strukturierte Ausnahmebehandlung erforderlich sind:

    • Die Überprüfung überprüft, ob es sich bei der Adresse um eine Benutzermodusadresse handelt und dass sich die Länge des Puffers innerhalb des Benutzeradressenbereichs befindet.

    • Ein __try/__except Block schützt vor Zugriff.

    Beachten Sie, dass ProbeForRead nur überprüft, ob die Adresse und Länge innerhalb des möglichen Adressbereichs des Benutzermodus liegen (z. B. leicht unter 2 GB für ein System ohne 4GT), 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 diese Bytes gültige Speicheradressen sind.

  • Ein Treiber, der auf Speicher-Manager-Funktionen wie MmIsAddressValid basiert, um sicherzustellen, dass die Adresse gültig ist. Wie für die Probefunktionen beschrieben, führt diese Situation eine Rennbedingung ein, die zu unerklärlichen Abstürze führen kann.

  • Ein Treiber, der keine strukturierte Ausnahmebehandlung verwendet. Die __try/except Funktionen innerhalb des Compilers verwenden unterstützung auf Betriebssystemebene für die Ausnahmebehandlung. Kernelebenen-Ausnahmen werden über einen Aufruf von ExRaiseStatus oder einer der zugehörigen Funktionen zurück in das System ausgelöst. Ein Treiber, der keine strukturierte Ausnahmebehandlung für jeden Aufruf verwendet, der eine Ausnahme auslöst, führt zu einer Fehlerüberprüfung (in der Regel KMODE_EXCEPTION_NOT_HANDLED).

    Es ist ein Fehler, die strukturierte Ausnahmebehandlung für Code zu verwenden, die nicht erwartet wird, dass Fehler ausgelöst werden. Diese Verwendung wird nur echte Fehler masken, die sonst gefunden würden. Das Platzieren eines __try/__except Wrappers auf der obersten Verteilerebene Ihrer Routine ist nicht die richtige Lösung für dieses Problem, obwohl es manchmal die Reflexlösung ist, die von Fahrerautoren versucht wurde.

  • Ein Treiber, der davon ausgeht, dass der Inhalt des Benutzerspeichers stabil bleibt. Angenommen, ein Treiber hat einen Wert in einen Speicherort für den Benutzermodus geschrieben, und später in derselben Routine, die auf diesen Speicherspeicherort verweist. Eine schädliche Anwendung könnte diesen Speicher nach dem Schreiben aktiv ändern und als Ergebnis dazu führen, dass der Treiber abstürzt.

Bei Dateisystemen sind diese Probleme schwerwiegend, da Dateisysteme in der Regel direkt auf Benutzerpuffer zugreifen (die METHOD_NEITHER Transfermethode). Solche Treiber bearbeiten Benutzerpuffer direkt und müssen daher Vorsorgemethoden für die Pufferverarbeitung integrieren, um Abstürze auf Betriebssystemebene zu vermeiden. Schnelle E/A übergibt immer unformatierte Speicherzeiger, sodass Treiber vor ähnlichen Problemen geschützt werden müssen, wenn schnelle E/A unterstützt wird.

Beispielcode für die Pufferbehandlung

Der WDK enthält zahlreiche Beispiele für die Pufferüberprüfung im Beispielcode für fastfat- und CDFS-Dateisystemtreiber, darunter:

  • 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 Seiten zu erstellen, die gesperrt sind.

  • 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 für null Benutzerpuffer. Der Beispielcode in CdCommonRead wird angezeigt, um die try Schlüsselwörter zu except verwenden. In der WDK-Umgebung werden diese Schlüsselwörter in C in Bezug auf die Compilererweiterungen __try und __except. Jeder Benutzer, der C++-Code verwendet, muss die systemeigenen Compilertypen verwenden, um Ausnahmen ordnungsgemäß zu behandeln, wie __try ein C++-Schlüsselwort, aber kein C-Schlüsselwort, und stellt eine Form der C++-Ausnahmebehandlung bereit, die für Kerneltreiber nicht gültig ist.