Unter dem Cutter befindet sich die Übersetzung des ersten Teils des Dokuments Detecting Kernel Memory Disclosure mit x86-Emulation und Taint Tracking ( Artikelprojekt Null ) von Mateusz Jurczyk .
Im übersetzten Teil des Dokuments:
- Besonderheiten der C-Programmiersprache (als Teil des Speichererweiterungsproblems)
- die Besonderheiten des Betriebs der Windows- und Linux-Kernel (als Teil des Speichererweiterungsproblems)
- Bedeutung der Offenlegung des Kernelspeichers und Auswirkungen auf die Betriebssystemsicherheit
- vorhandene Verfahren und Techniken zum Erkennen und Gegensteuern der Offenbarung des Kernelspeichers
Obwohl sich das Dokument auf die Kommunikationsmechanismen zwischen dem privilegierten Kernel des Betriebssystems und Benutzeranwendungen konzentriert, kann das Wesentliche des Problems für jede Datenübertragung zwischen verschiedenen Sicherheitsdomänen verallgemeinert werden: Der Hypervisor ist der Gastcomputer, der privilegierte Systemdienst (Daemon) ist die GUI-Anwendung, der Netzwerkclient ist der Server usw. .

Einführung
Eine der Aufgaben moderner Betriebssysteme besteht darin, die Trennung von Berechtigungen zwischen Benutzeranwendungen und dem Kernel des Betriebssystems sicherzustellen. Dies schließt zum einen die Tatsache ein, dass der Einfluss jedes Programms auf die Laufzeit durch eine bestimmte Sicherheitsrichtlinie begrenzt werden sollte, und zum anderen, dass Programme nur auf die Informationen zugreifen können, die sie lesen dürfen. Die zweite ist angesichts der Eigenschaften der C-Sprache (der Hauptprogrammiersprache, die bei der Entwicklung des Kernels verwendet wird) schwierig bereitzustellen, was es äußerst schwierig macht, Daten sicher zwischen verschiedenen Sicherheitsdomänen zu übertragen.
Moderne Betriebssysteme, die auf x86 / x86-64-Plattformen ausgeführt werden, sind multithreaded und verwenden ein Client-Server-Modell, in dem Anwendungen (Clients) im Benutzermodus unabhängig ausgeführt werden und den Betriebssystemkern (Server) aufrufen, um mit einer vom System verwalteten Ressource zu arbeiten. Der Mechanismus, der vom Benutzermoduscode ( Ring 3 ) zum Aufrufen eines vordefinierten Satzes von Kernelfunktionen (Ring 0) verwendet wird, wird als Systemaufrufe oder (kurz) Systemaufrufe bezeichnet. Ein typischer Systemaufruf ist in Abbildung 1 dargestellt:

Abbildung 1: Lebenszyklus eines Systemaufrufs
Es ist sehr wichtig zu vermeiden, dass versehentlich Kernelspeicherinhalte verloren gehen, wenn Sie mit Programmen im Benutzermodus interagieren. Es besteht ein erhebliches Risiko, vertrauliche Kerneldaten offenzulegen. Daten können implizit in den Ausgabeparametern sicherer (aus anderer Sicht) Systemaufrufe übertragen werden.
Die Offenlegung des privilegierten Systemspeichers erfolgt, wenn der Betriebssystemkern einen Speicherbereich zurückgibt, der größer (überschüssig) ist als zum Speichern der entsprechenden Informationen (darin enthalten) erforderlich ist. Oft enthalten redundante Bytes Daten, die in einem anderen Kontext gefüllt wurden, und dann wurde der Speicher nicht vorinitialisiert, was die Verbreitung von Informationen in neuen Datenstrukturen verhindern würde.
C Besonderheiten der Programmiersprache
In diesem Abschnitt werden einige Aspekte der C-Sprache behandelt, die für das Problem der Speichererweiterung am wichtigsten sind.
Undefinierter Status nicht initialisierter Variablen
Einzelne Variablen einfacher Typen (wie char oder int) sowie Mitglieder von Datenstrukturen (Arrays, Strukturen und Gewerkschaften) bleiben bis zur ersten Initialisierung in einem undefinierten Zustand (unabhängig davon, ob sie auf dem Stapel oder auf dem Heap abgelegt werden). Relevante Zitate aus der C11-Spezifikation (ISO / IEC 9899: 201x Ausschussentwurf N1570, April 2011):
6.7.9 Initialisierung
...
10 Wenn ein Objekt mit automatischer Speicherdauer nicht explizit initialisiert wird, ist sein Wert unbestimmt .
7.22.3.4 Die Malloc-Funktion
...
2 Die Malloc-Funktion reserviert Platz für ein Objekt, dessen Größe durch die Größe angegeben wird und dessen Wert unbestimmt ist .
7.22.3.5 Die Realloc-Funktion
...
2 Die Realloc-Funktion gibt die Zuordnung des alten Objekts frei, auf das ptr zeigt, und gibt einen Zeiger auf ein neues Objekt zurück, dessen Größe durch die Größe angegeben ist. Der Inhalt des neuen Objekts muss derselbe sein wie der des alten Objekts vor der Freigabe, bis auf die geringere der neuen und alten Größen. Alle Bytes im neuen Objekt, die über die Größe des alten Objekts hinausgehen, haben unbestimmte Werte .
Der Teil, der für Systemcode gilt, ist für Objekte auf dem Stapel am relevantesten, da der Betriebssystemkern normalerweise dynamische Zuordnungsschnittstellen mit eigener Semantik hat (nicht unbedingt kompatibel mit der Standard-C-Bibliothek, wie später beschrieben wird).
Soweit wir wissen, erstellt keiner der drei beliebtesten C-Compiler für Windows und Linux (Microsoft C / C ++ - Compiler, gcc, LLVM) Code, der vom Programmierer nicht initialisierte Variablen auf dem Stack im Release-Build-Modus (oder einem gleichwertigen) vorinitialisiert. Es gibt Compileroptionen zum Markieren von Stapelrahmen mit speziellen Byte-Markern (z. B. / RTCs in Microsoft Visual Studio), die jedoch aus Leistungsgründen in Release-Builds nicht verwendet werden. Infolgedessen erben nicht initialisierte Variablen auf dem Stapel die alten Werte der entsprechenden Speicherbereiche.
Stellen Sie sich ein Beispiel für eine Standardimplementierung eines fiktiven Windows-Systemaufrufs vor, bei dem eine Eingabe-Ganzzahl mit zwei multipliziert wird und das Ergebnis der Multiplikation zurückgegeben wird (Listing 1). Im Sonderfall (InputValue == 0) bleibt die Variable OutputValue natürlich nicht initialisiert und wird zurück auf den Client kopiert. Mit diesem Fehler können Sie für jeden Aufruf vier Byte Kernel-Stack-Speicher öffnen.
NTSTATUS NTAPI NtMultiplyByTwo(DWORD InputValue, LPDWORD OutputPointer) { DWORD OutputValue; if (InputValue != 0) { OutputValue = InputValue * 2; } *OutputPointer = OutputValue; return STATUS_SUCCESS; }
Codeauflistung 1: Speichererweiterung durch eine nicht initialisierte lokale Variable.
Lecks durch eine nicht initialisierte lokale Variable sind in der Praxis nicht sehr häufig: Einerseits erkennen und warnen moderne Compiler häufig solche Probleme, andererseits sind solche Lecks Funktionsfehler, die während der Entwicklung oder des Testens erkannt werden können. Das zweite Beispiel (in Listing 2) zeigt jedoch, dass ein Leck auch durch das Strukturfeld auftreten kann.
In diesem Fall wird das reservierte Strukturfeld im Code nie explizit verwendet, sondern dennoch in den Benutzermodus zurückkopiert und stellt daher auch vier Byte Kernelspeicher für den aufrufenden Code bereit. Dieses Beispiel zeigt deutlich, dass das Initialisieren jedes Felds jeder Struktur, die für alle Zweige der Codeausführung an den Client zurückgegeben wird, keine leichte Aufgabe ist. In vielen Fällen erscheint eine erzwungene Initialisierung unlogisch, insbesondere wenn dieses Feld keine praktische Rolle spielt. Es ist jedoch die Tatsache, dass eine nicht initialisierte Variable (oder ein Strukturfeld) auf dem Stapel (oder auf dem Heap) den Inhalt von Daten akzeptiert, die zuvor in diesem Speicherbereich (im Kontext einer anderen Operation) gespeichert wurden, das Herzstück des Kernel-Speichererweiterungsproblems.
typedef struct _SYSCALL_OUTPUT { DWORD Sum; DWORD Product; DWORD Reserved; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtArithOperations( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.Product = InputValue * 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; }
Listing 2: Speichererweiterung durch ein reserviertes Strukturfeld.
Ausrichtung von Strukturen und Füllbytes
Das Initialisieren aller Felder der Ausgabestruktur ist ein guter Anfang, um eine Speichererweiterung zu vermeiden. Dies reicht jedoch nicht aus, um sicherzustellen, dass in der Darstellung auf niedriger Ebene keine nicht initialisierten Bytes vorhanden sind. Kehren wir zur C11-Spezifikation zurück:
6.5.3.4 Die Größe und Ausrichtung der Operatoren
...
4 [...] Bei Anwendung auf einen Operanden mit Struktur- oder Vereinigungstyp ergibt sich die Gesamtzahl der Bytes in einem solchen Objekt, einschließlich interner und nachfolgender Auffüllung .
6.2.8 Ausrichtung von Objekten
1 Für vollständige Objekttypen gelten Ausrichtungsanforderungen, die die Adressen einschränken, an denen Objekte dieses Typs zugewiesen werden können . Eine Ausrichtung ist ein implementierungsdefinierter integrierter ganzzahliger Wert, der die Anzahl der Bytes zwischen aufeinanderfolgenden Adressen darstellt, an denen ein bestimmtes Objekt zugewiesen werden kann. [...]
6.7.2.1 Struktur- und Gewerkschaftsspezifizierer
...
17 Am Ende einer Struktur oder Vereinigung befindet sich möglicherweise eine unbenannte Polsterung .
Das heißt, C-Sprach-Compiler für x86 (-64) -Architekturen verwenden die natürliche Ausrichtung von Feldern von Strukturen (mit einem primitiven Typ): Jedes dieser Felder wird durch N Bytes ausgerichtet, wobei N die Größe des Feldes ist. Darüber hinaus werden ganze Strukturen und Verknüpfungen auch ausgerichtet, wenn sie in einem Array deklariert werden, und die Anforderung für die Ausrichtung verschachtelter Felder ist erfüllt. Um die Ausrichtung sicherzustellen, werden bei Bedarf implizite Füllbytes in Strukturen eingefügt. Obwohl im Quellcode nicht direkt auf sie zugegriffen werden kann, erben diese Bytes auch alte Werte aus Speicherbereichen und können Informationen in den Benutzermodus übertragen.
Im Beispiel in Listing 3 wird die Struktur SYSCALL_OUTPUT an den aufrufenden Code zurückgegeben. Es enthält 4- und 8-Byte-Felder, die durch 4 Füllbytes getrennt sind, damit die Adresse des LargeSum-Feldes ein Vielfaches von 8 wird. Trotz der Tatsache, dass beide Felder korrekt initialisiert wurden, werden Füllbytes nicht explizit gesetzt, was wiederum zur Erweiterung des Kernel-Stack-Speichers führt. Die spezifische Position der Struktur im Speicher ist in Abbildung 2 dargestellt.
typedef struct _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.LargeSum = 0; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; }
Listing 3: Speichererweiterung durch Ausrichten der Struktur.

Abbildung 2: Darstellung der Struktur im Speicher unter Berücksichtigung der Ausrichtung.
Lecks durch Alignments sind relativ häufig, da viele Ausgabeparameter von Systemaufrufen durch Strukturen dargestellt werden. Das Problem ist besonders akut für 64-Bit-Plattformen, bei denen die Größe von Zeigern, size_t und ähnlichen Typen von 4 auf 8 Bytes zunimmt, was zum Auftreten von Auffüllungen führt, die zum Ausrichten der Felder solcher Strukturen erforderlich sind.
Da Auffüllbytes im Quellcode nicht adressiert werden können, muss ein Memset oder eine ähnliche Funktion verwendet werden, um den gesamten Speicherbereich der Struktur zurückzusetzen, bevor eines ihrer Felder initialisiert und in den Benutzermodus kopiert wird, zum Beispiel:
memset(&OutputStruct, 0, sizeof(OutputStruct));
Seacord RC stellt jedoch in seinem Buch "The CERT C Coding Standard, 2. Auflage: 98 Regeln für die Entwicklung sicherer, zuverlässiger und sicherer Systeme. Addison-Wesley Professional" 2014 fest, dass dies keine ideale Lösung ist, da Bytes aufgefüllt werden ) kann nach dem Aufrufen von memset beispielsweise als Nebeneffekt von Operationen mit benachbarten Feldern immer noch heruntergefahren werden. Bedenken können durch die folgende Aussage in Spezifikation C begründet werden:
6.2.6 Darstellungen von Typen
6.2.6.1 Allgemeines
...
6 Wenn ein Wert in einem Objekt vom Typ Struktur oder Vereinigung gespeichert wird , einschließlich in einem Elementobjekt , nehmen die Bytes der Objektdarstellung, die Auffüllbytes entsprechen, nicht angegebene Werte an . [...]
In der Praxis hat jedoch keiner der von uns getesteten C-Compiler außerhalb der Speicherbereiche explizit deklarierter Felder gelesen oder geschrieben. Es scheint, dass diese Meinung von Entwicklern von Betriebssystemen geteilt wird, die memset verwenden.
Gewerkschaften und Felder unterschiedlicher Größe
Joins sind ein weiteres komplexes C-Sprachkonstrukt im Zusammenhang mit der Kommunikation mit weniger privilegiertem Aufrufcode. Überlegen Sie, wie die C11-Spezifikation die Darstellung von Gewerkschaften im Speicher beschreibt:
6.2.5 Typen
...
20 Aus den Objekt- und Funktionstypen können wie folgt beliebig viele abgeleitete Typen erstellt werden: [...] Ein Vereinigungstyp beschreibt eine überlappende nicht leere Menge von Elementobjekten , von denen jedes einen optional angegebenen Namen und möglicherweise einen unterschiedlichen Typ hat.
6.7.2.1 Struktur- und Gewerkschaftsspezifizierer
...
6 Wie in 6.2.5 erläutert, ist eine Struktur ein Typ, der aus einer Folge von Elementen besteht, deren Speicher in einer geordneten Reihenfolge zugeordnet ist, und eine Vereinigung ist ein Typ, der aus einer Folge von Mitgliedern besteht, deren Speicher sich überlappen .
...
16 Die Größe einer Gewerkschaft reicht aus, um das größte ihrer Mitglieder aufzunehmen . Der Wert von höchstens einem der Mitglieder kann jederzeit in einem Gewerkschaftsobjekt gespeichert werden.
Das Problem ist, dass, wenn die Vereinigung aus mehreren Feldern unterschiedlicher Größe besteht und nur ein Feld kleinerer Größe explizit initialisiert wird, die verbleibenden Bytes, die zur Aufnahme großer Felder zugewiesen sind, nicht initialisiert bleiben. Schauen wir uns ein Beispiel für einen hypothetischen Systemaufruf-Handler an, der in Listing 4 gezeigt ist, zusammen mit der in Abbildung 3 gezeigten SYSCALL_OUTPUT-Zuordnungsspeicherzuordnung.
typedef union _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; }
Codeauflistung 4: Erweitern des Speichers durch teilweises Initialisieren einer Union.

Abbildung 3: Darstellung der Vereinigung im Speicher mit Ausrichtung.
Es stellt sich heraus, dass die Gesamtgröße der SYSCALL_OUTPUT-Vereinigung 8 Byte beträgt (aufgrund der Größe des größeren LargeSum-Felds). Die Funktion legt jedoch nur den Wert des kleineren Felds fest, sodass 4 nachfolgende Bytes nicht initialisiert werden, was anschließend zu einem Leck in der Clientanwendung führt.
Eine sichere Implementierung sollte nur das Feld Summe im Benutzeradressraum festlegen und nicht das gesamte Objekt mit möglicherweise nicht verwendeten Speicherbereichen kopieren. Eine weitere funktionierende Lösung besteht darin, die Memset-Funktion aufzurufen, um eine Kopie der Union im Kernelspeicher aufzuheben, bevor eines ihrer Felder festgelegt und in den Benutzermodus zurücküberwiesen wird.
Unsichere Größe von
Wie in den beiden vorherigen Abschnitten gezeigt, kann die Verwendung des Operators sizeof direkt oder indirekt dazu beitragen, den Kernelspeicher freizulegen, wodurch mehr Daten kopiert werden als zuvor initialisiert.
C verfügt nicht über die erforderliche Vorrichtung, um Daten sicher vom Kernel in den Benutzerbereich zu übertragen - oder allgemeiner zwischen verschiedenen Sicherheitskontexten. Die Sprache enthält keine Laufzeitmetadaten, die explizit angeben können, welche Bytes in jeder Datenstruktur festgelegt wurden, die für die Interaktion mit dem Betriebssystemkern verwendet wird. Infolgedessen liegt die Verantwortung beim Programmierer, der bestimmen muss, welche Teile jedes Objekts an den aufrufenden Code übergeben werden sollen. Bei korrekter Ausführung müssen Sie für jede in Systemaufrufen verwendete Ausgabestruktur eine separate Funktion zum sicheren Kopieren schreiben. Dies führt wiederum zu einer Aufblähung des Codes, einer Verschlechterung der Lesbarkeit und ist im Allgemeinen eine mühsame und zeitaufwändige Aufgabe.
Andererseits ist es bequem und einfach, den gesamten Speicherbereich des Kernels mit einem einzigen memcpy-Aufruf und dem Argument sizeof zu kopieren und den Client bestimmen zu lassen, welche Teile der Ausgabe verwendet werden. Es stellt sich heraus, dass dieser Ansatz heute unter Windows und Linux verwendet wird. Und wenn ein bestimmter Fall von Informationslecks erkannt wird, wird sofort ein Patch mit einem Memset-Aufruf bereitgestellt und vom Betriebssystemhersteller verteilt. Leider löst dies das Problem im allgemeinen Fall nicht.
Betriebssystemspezifikationen
Es gibt bestimmte Kernel-Design-Lösungen, Programmiermethoden und Codemuster, die sich darauf auswirken, wie anfällig das Betriebssystem für Schwachstellen bei der Speichererweiterung ist. Sie werden in den folgenden Unterabschnitten berücksichtigt.
Dynamischen Speicher wiederverwenden
Die aktuellen Allokatoren des dynamischen Speichers (sowohl im Benutzermodus als auch im Kernelmodus) sind stark optimiert, da ihre Leistung einen erheblichen Einfluss auf die Leistung des gesamten Systems hat. Eine der wichtigsten Optimierungen ist die Wiederverwendung von Speicher: Wenn der entsprechende Speicher freigegeben wird, wird er selten vollständig verworfen. Stattdessen wird er in der Liste der Regionen gespeichert, die bei der nächsten Zuweisung zurückgegeben werden können. Um CPU-Zyklen zu speichern, werden die Standardspeicherbereiche nicht zwischen Freigabe und neuer Zuweisung gelöscht. Infolgedessen stellt sich heraus, dass zwei nicht verbundene Teile des Kernels für kurze Zeit mit demselben Speicherbereich arbeiten. Dies bedeutet, dass Sie durch das Auslaufen des Inhalts des dynamischen Speichers des Kernels die Daten verschiedener Betriebssystemkomponenten anzeigen können.
In den folgenden Abschnitten geben wir einen kurzen Überblick über die im Windows- und Linux-Kernel verwendeten Allokatoren und ihre bemerkenswertesten Eigenschaften.
Windows
Die Schlüsselfunktion des Windows-Kernel-Pool-Managers ist ExAllocatePoolWithTag , der direkt oder über eine der verfügbaren Shells aufgerufen werden kann: ExAllocatePool {∅, Ex, WithQuotaTag, WithTagPriority}. Keine dieser Funktionen löscht den Inhalt des zurückgegebenen Speichers, entweder standardmäßig oder über Eingabeflags. Im Gegenteil, alle haben die folgende Warnung in ihrer jeweiligen MSDN-Dokumentation:
Hinweis Der von der Funktion zugewiesene Speicher ist nicht initialisiert. Ein Kernel-Modus-Treiber muss diesen Speicher zuerst auf Null setzen, wenn er für Benutzer-Modus-Software sichtbar gemacht werden soll (um zu vermeiden, dass potenziell privilegierte Inhalte verloren gehen).
Der aufrufende Code kann einen von sechs Haupttypen von Pools auswählen: NonPagedPool, NonPagedPoolNx, NonPagedPoolSession, NonPagedPoolSessionNx, PagedPool und PagedPoolSession. Jeder von ihnen hat einen eigenen Bereich im virtuellen Adressraum, und daher können die zugewiesenen Speicherbereiche nur innerhalb desselben Pooltyps wiederverwendet werden. Die Häufigkeit der Wiederverwendung von Speicherelementen ist sehr hoch, und Bereiche mit Nullen werden normalerweise nur zurückgegeben, wenn in den Lookaside-Listen kein geeigneter Datensatz gefunden wird oder die Anforderung so groß ist, dass neue Speicherseiten erforderlich sind. Mit anderen Worten, es gibt derzeit praktisch keine Faktoren, die die Offenlegung des Poolspeichers in Windows verhindern, und fast jeder dieser Fehler kann verwendet werden, um vertrauliche Daten aus verschiedenen Teilen des Kernels zu verlieren.
Linux
Der Linux-Kernel verfügt über drei Hauptschnittstellen für die dynamische Zuweisung von Speicher:
- kmalloc - eine allgemeine Funktion zum Zuweisen von Speicherblöcken beliebiger Größe (kontinuierlich sowohl im virtuellen als auch im physischen Adressraum), verwendet die Plattenspeicherzuweisung .
- kmem_cache_create und kmem_cache_alloc - ein spezialisierter Mechanismus zum Zuweisen von Objekten fester Größe (z. B. Strukturen) - verwendet ebenfalls die Zuweisung von Plattenspeicher .
- vmalloc ist eine selten verwendete Zuordnungsfunktion, die Regionen zurückgibt, deren Kontinuität auf der Ebene des physischen Speichers nicht garantiert ist.
Diese Funktionen (für sich allein) garantieren nicht, dass die ausgewählten Regionen keine alten (möglicherweise vertraulichen) Daten enthalten, wodurch der Speicher des Kernel-Heaps geöffnet werden kann. Es gibt jedoch verschiedene Möglichkeiten, wie der aufrufende Code den ungültigen Speicher anfordern kann:
- Die kmalloc- Funktion hat ein Analogon von kzalloc , das sicherstellt, dass der zurückgegebene Speicher gelöscht wird.
- Das optionale __GFP_ZERO-Flag kann an kmalloc , kmem_cache_alloc und einige andere Funktionen übergeben werden, um das gleiche Ergebnis zu erzielen.
- kmem_cache_create akzeptiert einen Zeiger auf eine optionale Konstruktorfunktion, die aufgerufen wird, um jedes Objekt vorab zu initialisieren, bevor es an den aufrufenden Code zurückgegeben wird. Der Konstruktor kann als Wrapper um ein Memset implementiert werden, um einen bestimmten Speicherbereich auf Null zu setzen.
Wir sehen die Verfügbarkeit dieser Optionen als günstige Bedingungen für die Kernel-Sicherheit, da sie Entwickler dazu ermutigen, fundierte Entscheidungen zu treffen und einfach mit vorhandenen Speicherzuweisungsfunktionen arbeiten zu können, anstatt nach jeder Zuweisung von dynamischem Speicher zusätzliche Memset-Aufrufe hinzuzufügen.
Arrays mit fester Größe
Der Zugriff auf eine Reihe von Betriebssystemressourcen kann über deren Testnamen erfolgen. Die Vielfalt der benannten Ressourcen in Windows ist sehr groß, zum Beispiel: Dateien und Verzeichnisse, Schlüssel und Werte von Registrierungsschlüsseln, Windows, Schriftarten und vieles mehr. Für einige von ihnen ist die Namenslänge begrenzt und wird durch eine Konstante ausgedrückt, z. B. MAX_PATH (260) oder LF_FACESIZE (32). In solchen Fällen vereinfachen Kernelentwickler den Code häufig, indem sie die Puffer mit der maximalen Größe deklarieren und als Ganzes kopieren (z. B. mit dem Schlüsselwort sizeof), anstatt nur mit dem entsprechenden Teil der Zeile zu arbeiten. Dies ist besonders nützlich, wenn Zeichenfolgen Mitglieder größerer Strukturen sind. Solche Objekte können frei im Speicher verschoben werden, ohne sich um die Verwaltung von Zeigern auf den dynamischen Speicher kümmern zu müssen.
Wie zu erwarten ist, werden große Puffer selten vollständig verwendet, und der verbleibende Speicherplatz wird häufig nicht geleert. Dies kann zu besonders starken Lecks langer zusammenhängender Bereiche des Kernelspeichers führen. In dem Beispiel in Listing 5 verwendet der Systemaufruf die Funktion RtlGetSystemPath, um den Systempfad in den lokalen Puffer zu laden. Wenn der Aufruf erfolgreich ist, werden alle 260 Bytes unabhängig von der tatsächlichen Leitungslänge an den Aufrufer übergeben.
NTSTATUS NTAPI NtGetSystemPath(PCHAR OutputPath) { CHAR SystemPath[MAX_PATH]; NTSTATUS Status; Status = RtlGetSystemPath(SystemPath, sizeof(SystemPath)); if (NT_SUCCESS(Status)) { RtlCopyMemory(OutputPath, SystemPath, sizeof(SystemPath)); } return Status; }
Listing 5: Speichererweiterung durch teilweise Initialisierung des Zeichenfolgenpuffers.
Der in diesem Beispiel in den Benutzerbereich zurückkopierte Speicherbereich ist in Abbildung 4 dargestellt.

Abbildung 4: Speicher eines teilweise initialisierten Zeilenpuffers.
Eine sichere Implementierung sollte nur den angeforderten Pfad und nicht den gesamten für die Speicherung verwendeten Puffer zurückgeben. Dieses Beispiel zeigt einmal mehr, wie die Schätzung der Datengröße mit dem Operator sizeof (der als Parameter für RtlCopyMemory verwendet wird) in Bezug auf die tatsächliche Datenmenge, die der Kernel an den Benutzerbereich übergeben muss, völlig falsch sein kann.
Beliebige Ausgabegröße für Systemaufrufe
Die meisten Systemaufrufe akzeptieren Zeiger auf die Ausgabe im Benutzermodus zusammen mit der Größe des Puffers. In den meisten Fällen sollten Größeninformationen nur verwendet werden, um festzustellen, ob der bereitgestellte Puffer ausreicht, um Systemaufrufausgaben zu empfangen. Verwenden Sie nicht die gesamte Größe des bereitgestellten Ausgabepuffers, um die zu kopierende Speichermenge anzugeben. Es treten jedoch Fälle auf, in denen der Kernel versucht, jedes Byte des Ausgabepuffers des Benutzers zu verwenden, ohne die Menge der tatsächlich zu kopierenden Daten zu berücksichtigen. Ein Beispiel für dieses Verhalten ist in Listing 6 dargestellt.
NTSTATUS NTAPI NtMagicValues(LPDWORD OutputPointer, DWORD OutputLength) { if (OutputLength < 3 * sizeof(DWORD)) { return STATUS_BUFFER_TOO_SMALL; } LPDWORD KernelBuffer = Allocate(OutputLength); KernelBuffer[0] = 0xdeadbeef; KernelBuffer[1] = 0xbadc0ffe; KernelBuffer[2] = 0xcafed00d; RtlCopyMemory(OutputPointer, KernelBuffer, OutputLength); Free(KernelBuffer); return STATUS_SUCCESS; }
Listing 6: Speichererweiterung um einen Ausgabepuffer beliebiger Größe.
Der Zweck eines Systemaufrufs besteht darin, dem aufrufenden Code drei spezielle 32-Bit-Werte bereitzustellen, die insgesamt 12 Bytes belegen. Obwohl die Überprüfung der korrekten Puffergröße am Anfang der Funktion korrekt ist, sollte die Verwendung des OutputLength-Arguments dort enden. In dem Wissen, dass der Ausgabepuffer groß genug ist, um das Ergebnis zu speichern, kann der Kernel 12 Byte Speicher zuweisen, ihn füllen und den Inhalt zurück in den bereitgestellten Benutzermoduspuffer kopieren. Stattdessen weist ein Systemaufruf einen Poolblock zu (außerdem mit einer benutzergesteuerten Länge) und kopiert den gesamten zugewiesenen Speicher in den Benutzerbereich. Es stellt sich heraus, dass alle Bytes mit Ausnahme der ersten 12 nicht initialisiert und fälschlicherweise für den Benutzer geöffnet werden, wie in Abbildung 5 dargestellt.

Abbildung 5: Pufferspeicher beliebiger Größe.
Das in diesem Abschnitt beschriebene Schema ist besonders für Windows üblich. Ein ähnlicher Fehler kann einem Angreifer ein äußerst nützliches Grundelement für die Speichererweiterung liefern:
- , Windows, . , .
- . , , . , ( — ) .
, . , , .
,
, . , Windows .
, , . , : AddressSanitizer , PageHeap Special Pool . , , - . , . , , , , , . , ( ).
, , , . , .
, API
API, Windows (Win32/User32 API). API , , , . , , , , . .
, . , . , , , . , , .
, , . , KASLR (Kernel Address Space Layout Randomization ), . : Windows, Hacking Team 2015 ( Juan Vazquez. Revisiting an Info Leak ) (derandomize) win32k.sys, . , Matt Tait' Google Project Zero ( Kernel-mode ASLR leak via uninitialized memory returned to usermode by NtGdiGetTextMetrics ) MS15-080 (CVE-2015-2433).
(/) , , (control flow), : , , , , StackGuard Linux /GS Windows . , . , , .
(/)
(/) , , , : , , , . , , . . , ( , ) , , .

Microsoft Windows
2015 Windows. 2015 Matt Tait win32k!NtGdiGetTextMetrics. Windows Hacking Team. , , , 0-day Windows.
2015, WanderingGlitch (HP Zero Day Initiative) ( Acknowledgments – 2015 ). Ruxcon 2016 ( ) "Leaking Windows Kernel Pointers" .
, 2017 fanxiaocao pjf IceSword Lab (Qihoo 360) "Automatically Discovering Windows Kernel Information Leak Vulnerabilities" , , 14 2017 (8 ). Bochspwn Reloaded, , . VMware (Bochs) . , Bochspwn Reloaded, .
, , 2010-2011 , win32k: "Challenge: On 32bit Windows7, explain where the upper 16bits of eax come from after a call to NtUserRegisterClassExWOW()" "Subtle information disclosure in WIN32K.SYS syscall return values" . Windows 8, 2015 Matt Tait , : Google Project Zero Bug Tracker .
( ), , 2017 - Windows -, : Joseph Bialek — "Anyone notice my change to the Windows IO Manager to generically kill a class of info disclosure? BufferedIO output buffer is always zero'd" . , IOCTL- .
, Visual Studio 15.5 POD- , "= {0}", . , padding- () .
Linux
Windows, Linux , 2010 . , ( ) ( ) . , Windows Linux , — , .
, Linux . "Linux kernel vulnerabilities: State-of-the-art defenses and open problems" 2010 2011 28 . 2017- "Securing software systems by preventing information leaks" Lu K. 59 , 2013- 2016-. . : Rosenberg Oberheide 25 , Linux 2009-2010 , . Linux c grsecurity / PaX-hardened . Vasiliy Kulikov 25 2010-2011 , Coccinelle . , Mathias Krause 21 2013 50 .
, , Linux. — -Wuninitialized ( gcc, LLVM), . kmemcheck , Valgrind' . , . , KernelAddressSANitizer KernelMemorySANitizer . KMSAN syzkaller ( ) 19 , .
Linux. 2014 — 2016 Peir´o Coccinelle , Linux 3.12: "Detecting stack based kernel information leaks" International Joint Conference SOCO14-CISIS14-ICEUTE14, pages 321–331 (Springer, 2014) "An analysis on the impact and detection of kernel stack infoleaks" Logic Journal of the IGPL. , . 2016- Lu UniSan — , , : , . , 20% (350 1800), 19 Linux Android.
— (multi-variant program execution), , . , . , KASLR, -, . , 2006 DieHard: probabilistic memory safety for unsafe languages, 2017 — BUDDY: Securing software systems by preventing information leaks. John North "Identifying Memory Address Disclosures" 2015- . , SafeInit (Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities) , , . , , , Linux.
, . , : , . , , - , . .
CONFIG_PAGE_POISONING CONFIG_DEBUG_SLAB, -. -, . , , , Linux.
grsecurity / PaX . , PAX_MEMORY_SANITIZE , slab , ( — ). , PAX_MEMORY_STRUCTLEAK , ( ), . padding- (), 100% . , — PAX_MEMORY_STACKLEAK, . , , . (Kernel Self Protection Project) STACKLEAK .
Linux:
Secure deallocation, Chow , 2005Chow, Jim and Pfaff, Ben and Garfinkel, Tal and Rosenblum, Mendel. Shredding Your Garbage: Reducing Data Lifetime Through Secure Deallocation. In USENIX Security Symposium, pages 22–22, 2005.
, , ( ) . Linux .
Split Kernel, Kurmus Zippel, 2014Kurmus, Anil and Zippel, Robby. A tale of two kernels: Towards ending kernel hardening wars with split kernel. In Proceedings of the 2014 ACM SIGSAC Conference on Computer and Communications Security, pages 1366–1377. ACM, 2014.
, .
SafeInit, Milburn , 2017Milburn, Alyssa and Bos, Herbert and Giuffrida, Cristiano. SafeInit: Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities. In Proceedings of the 2017 Annual Network and Distributed System Security Symposium (NDSS)(San Diego, CA), 2017.
, , .
UniSan, Lu , 2016Lu, Kangjie and Song, Chengyu and Kim, Taesoo and Lee, Wenke. UniSan: Proactive kernel memory initialization to eliminate data leakages. In Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security, pages 920–932. ACM, 2016.
SafeInit , , , , .
, Linux .
( )
, , ( ). : (), , , , ( - ) . , . , , .
, :
- Bochspwn Reloaded – detection with software x86 emulation
- Windows bug reproduction techniques
- Alternative detection methods
- Other data sinks
- Future work
- Other system instrumentation schemes
, :) , .