Windows Rootkits 2005, Teil 2
Seite 2: Virtueller Speicher
Virtueller Speicher
Die meisten modernen Architekturen unterscheiden "virtuellen" und "physikalischen" Speicher. Oftmals hat ein System sehr viel mehr virtuellen als physikalischen Speicher. Ein 32-Bit-System hat einen virtuellen Speicher von 4 GByte, auch wenn nur 256 MByte RAM installiert sind. Konkret legt das installierte RAM fest, wie groß der physikalische Speicher ist, während der virtuelle Adressraum von der Architektur des Befehlssatzes abhängt. Mit einem 32-Bit-Prozessor kann man maximal 232 Byte also 4 GByte Speicher adressieren, mit einem 64-Bit-System 264 Byte oder mehr als 16 ExaBytes!
Es gibt verschiedene Wege, virtuellen Speicher zu implementieren; dazu gehören Segmentierungs- und Paging-Verfahren. Die x86-Architektur unterstützt beides; dieser Artikel konzentriert sich jedoch auf das Paging, da dies der Teil ist, den Shadow Walker unterwandert. Die grundlegende Idee beim Paging ist, dass man virtuellen und physikalischen Speicher in Blöcke fester Größe unterteilt. Blocks im virtuellen Speicher bezeichnet man als "Pages"; sie werden auf Blöcke im physikalischen Speicher abgebildet, die man "Frames" nennt. Page Tables und Page Directories enthalten die Informationen, die den virtuellen Pages die jeweiligen Frames zuordnen. Des Weiteren enthalten sie Schutz- und Statusinformationen. Ein zentraler Punkt ist, dass der Prozessor bei eingeschaltetem Paging für jeden Speicherzugriff nachsehen muss, zu welchem physikalischen Frame er gehört und ob der im Hauptspeicher vorhanden ist. Das ergibt einen signifikanten Performance-Overhead, insbesondere wenn die Architektur wie beim Intel Pentium auf zweistufigen Page Tables beruht.
Durch die Unterscheidung zwischen virtuellem und physikalischem Speicher können Hardware und Betriebssystem die Illusion vermitteln, es gäbe mehr Speicher als tatsächlich vorhanden ist. Paging erfolgt unsichtbar für Applikationen. Jede Applikation sieht es so, als hätte sie 4 GByte virtuellen Speicher zur eigenen Verfügung. Sie muss dabei gar nicht wissen, wie groß der physikalische Speicher tatsächlich ist oder wie er mit den virtuellen Adressen zusammenhängt. Da der virtuelle Adressraum größer als der physikalische Speicher ist, kann es vorkommen, dass ein Prozess mehr Speicher anfordert, als tatsächlich vorhanden ist. Dann muss das Betriebssystem einen Teil des Speichers auf die Festplatte auslagern, um Platz zu schaffen. Dazu kopiert es einige Frames in das Pagefile und markiert die zugehörigen Einträge in der Page Table als "nicht vorhanden". Wird erneut auf diese Seiten zugegriffen, sind sie nicht im Hauptspeicher und ein Page Fault tritt auf. Dieser Page Fault ruft den Page Fault Handler des Betriebssystems auf den Plan, der den notwendigen I/O-Request zum Laden der Seite aus der Datei ausführt. Sind immer noch alle verfügbaren physikalischen Frames belegt, muss der Handler zunächst eine andere Page auslagern, bevor er die angeforderte lädt.
In einem zweistufigen Paging-Verfahren, kann ein Speicherzugriff die folgenden Schritte erfordern:
- Sieh im Page Directory nach, ob die Page Table fĂĽr die Adresse im Hauptspeicher vorhanden ist.
- Ist sie nicht da, erzeuge einen I/O-Request, der die Page Table von der Platte lädt.
- Sieh in der Page Table nach, ob die angeforderte Page im Hauptspeicher ist.
- Ist sie nicht da, erzeuge einen I/O-Request, der die Page von der Platte lädt.
- Lies das angeforderte Byte mit Offset aus der Page.
Bild 1 illustriert den Vorgang der Adressübersetzung beim x86. Aus den oben aufgeführten Schritten ergibt sich, dass ein einzelner Speicherzugriff im schlimmsten Fall drei Speicherzugriffe und zwei Festplatten-I/O-Operationen erfordert. Hardware-Entwickler haben deshalb den Translation Lookaside Buffer (TLB) erfunden. Der TLB ist ein Highspeed-Cache, der oft benutzte Zuordnungen von virtuellen zu physikalischen Adressen enthält. Bei einem Speicherzugriff wird die Zuordnung zunächst im TLB gesucht, bevor Page Directory/Table konsultiert werden. Ist sie dort vorhanden, spricht man von einem "Hit" ansonsten von einem "Miss". Da das Suchen im TLB sehr viel schneller ist als ein Zugriff auf die Page Table, kann man bei Speicherzugriffen, die der TLB auflösen kann, den größten Teil des vorher erwähnten Performancenachteils vermeiden.
Shadow Walker: So funktioniert es
Die Konzeptstudie Shadow Walker besteht derzeit aus zwei Treibern: einem modifizierten FU-Rootkit-Treiber und einem Speicher-Hook-Treiber, der das FU-Rootkit versteckt. Da Shadow Walker nur als Konzeptstudie gedacht ist, versucht er nicht, den Speicher-Hook-Treiber und den zugehörigen Interrupt-Hook zu verstecken. Außerdem ist die Implementierung begrenzt und unterstützt unter anderem keine Multiprozessorsysteme und kein PAE (Physical Address Extension). Shadow Walker soll kein voll funktionsfähiges, scharfes Rootkit sein und lässt sich in der aktuellen Implementierung auf verschiedene Arten aufspüren. Er soll vielmehr einen gruseligen Ausblick auf zukünftige Rootkit-Techniken geben. Ein fähiger Angreifer könnte auf dieser Basis durchaus ein scharfes Rootkit, einen Wurm oder Spyware entwickeln. Das ist angesichts der aktuellen Techniken zum Aufspüren von Schadsoftware schon ein ziemlich beunruhigender Gedanke. Denn mit diesen Methoden kann ein Angreifer sowohl unbekannten als auch bekannten Schadcode vor Signatur-Scannern, Heuristiken und Integritäts-Checks verbergen. In den nächsten Absätzen erläutern wir die Implementierungsdetails der Techniken, mit denen Shadow Walker die virtuelle Speicherverwaltung unterwandert.
Hintergrundinformationen
Es ist zwar allgemein bekannt, dass es drei verschiedene Arten von Speicherzugriffen gibt (Lesen, Schreiben und AusfĂĽhren). Weniger bekannt ist jedoch, dass der Paging-Mechanismus der meisten 32-Bit-x86-Prozessoren nur zwei Kombinationen von Zugriffsrechten unterstĂĽtzen: Lesen+AusfĂĽhren und Lesen+Schreiben+AusfĂĽhren. AusfĂĽhrende Zugriffe werden implizit immer erlaubt und es gibt keine direkte UnterstĂĽtzung dies anders festzulegen. Diese Macke der Architektur wurde zum Fluch von Intrusion-Detection-Systemen, die Buffer Overflows entdecken sollten, da sie nicht einfach den Stack als nicht-ausfĂĽhrbar markieren konnten [2]. Um nicht den KĂĽrzeren zu ziehen, spĂĽrten ein paar Entwickler von Sicherheitssoftware eine andere Macke auf, die es ihnen erlaubte, unter Unix "No-Execute"-Speicher mit Hilfe von Software zu implementieren [3]. Diese Implementierung wurde als PaX bekannt. Shadow Walker benutzt einen Teil der Ergebnisse des PaX-Teams, um ausgefĂĽhrten Code unter Windows zu verstecken.
Zur Wiederholung: Moderne Rootkits mĂĽssen zwei Dinge leisten, wenn sie unentdeckt bleiben wollen:
- Sie mĂĽssen ihren eigenen, ausfĂĽhrbaren Code im Speicher vor Signatur-Scannern verstecken.
- Sie müssen ihre Änderungen im Speicher von Betriebssystemkomponenten (also typischerweise Hooks) vor heuristischer Entdeckung durch Tools wie VICE und Integritäts-Checks verbergen [4].
Um dies zu erreichen, muss das Rootkit die Daten kontrollieren, die ein direkter Speicherzugriff einer Applikation wie einem Security-Scanner liefert. Wenn ein Rootkit einen lesenden Zugriff auf seinen eigenen ausführbaren Code entdeckt, ist dies ein deutlicher Hinweis, dass da möglicherweise ein Scanner nach ihm sucht!