Windows Rootkits 2005, Teil 1

Seite 2: Eingehängt

Inhaltsverzeichnis

Eine der Techniken, die internen Abläufe des Betriebssystems zu manipulieren, ist das "Einhängen" – auf Englisch hooking. In modernen Betriebssystemen kann man sich an vielen Stellen einklinken, weil sie von vornherein flexibel, erweiterbar und abwärtskompatibel konzipiert wurden. Indem sich das Rootkit in einen Aufruf einhängt, kann es die Informationen verändern, die die originale Betriebssystemfunktion zurückgeliefert hätte. Es gibt in Windows viele Tabellen, über die sich ein Rootkit einhängen kann. Dieser Artikel wird nur einige wenige davon vorstellen.

Windows ist sehr hardwareunabhängig und kompatibel zu anderen Beteriebssystemumgebungen wie POSIX konzipiert. Außerdem muss es so flexibel sein, dass ein Betriebssystem-Upgrade möglich ist, ohne dass Entwickler all ihre Applikationen komplett neu schreiben müssen. Windows realisiert dies über einen Satz von Subsystem-Umgebungen: das Win32-Subsystem, das POSIX- und das OS/2-Subsystem. Jede dieser Subsystem-Umgebungen wurde als Dynamic Link Library (DLL) implementiert.

Diese Subsysteme stellen eine Schnittstelle zu den Systemdiensten bereit, die im Kernel residieren. Mit diesem Application Programming Interface (API) können Entwickler Software schreiben, die die meisten Betriebssystem-Upgrades überlebt. Gewöhnlich rufen deshalb Applikationen die Windows-Systemdienste nicht selbst auf sondern nutzen stattdessen eines dieser Subsysteme.

Die DLLs exportieren die dokumentierte Schnittstelle für die Programme eines Subsystems. Meist wird das Win32-Subsystem verwendet. Es besteht aus Kernel32.dll, User32.dll, Gdi32.dll und Advapi32.dll. Ntdll.dll ist eine spezielle Hilfsbibliothek des Systems, die die Subsystem-DLLs benutzen. Sie stellt Verteiler-Stubs für die System-Funktionen der Ausführungsschicht bereit (Windows Executive). Die Verteiler-Stubs enthalten auch den architekturspezifischen Code für den Übergang vom User- in den Kernel-Mode. Die eigentliche Arbeit erledigen schließlich die in der SSDT gelisteten Kernel-Mode-System-Funktionen.

Wenn ein Windows-Programm in den Speicher geladen wird, muss der Loader die so genannte Import Address Table (IAT) der zugehörigen Datei auswerten. Sie listet die DLLs und die darin vom Programm verwendeten Funktionen. Der Loader lokalisiert jede dieser DLLs auf der Festplatte und blendet sie in den Adressraum des Prozesses ein. Dann schreibt er die Adressen der Funktionen in die IAT des Prozesses. Typische Einträge in der IAT sind Funktionen aus Kernel32.dll und Ntdll.dll Aber auch andere Bibliotheken stellen nützliche Funktionen bereit und können somit in der IAT auftauchen. Klassisches Beispiel sind die von Ws2_32.dll exportierten Socket-Funktionen. Kernel-Treiber können auch Funktionen anderer Binär-Module des Kernels importieren, beispielsweise aus Ntoskrnl.exe und Hal.dll.

Indem ein Rootkit Einträge in der IAT modifiziert, kann es den Ausführungsablauf ändern und damit beeinflussen, was eine Funktion an den Aufrufer zurückmeldet. Angenommen, eine Applikation listet alle Dateien eines Verzeichnisses, um irgendwas mit ihnen zu machen. Diese Applikation läuft im User-Mode unter einem Benutzer oder als Dienst. Des weiteren sei es eine Win32-Applikation, was bedeutet, sie benutzt Kernel32.dll, User32.dll, Gui32.dll und Advapi.dll, um schließlich auch Kernel-Funktionen aufzurufen.

Unter Win32 ruft ein Programm, das alle Dateien eines Verzeichnisses auflisten will zunächst FindFirstFile() auf, das von Kernel32.dll exportiert wird. Bei Erfolg liefert FindFirstFile() ein Handle zurück. Das dient als Parameter für die folgenden Aufrufe von FindNextFile(), um alle Dateien und Unterverzeichnisse des Directories zu erhalten. FindNextFile() gehört ebenfalls zu den Funktionen in Kernel32.dll.

Der Loader lädt folglich zur Laufzeit Kernel32.dll und kopiert die Adressen dieser Funktionen in den IAT der Applikation. Ruft die Applikation dann die Funktion FindNextFile() auf, lädt sie deren Adresse aus ihrer Import-Tabelle und springt diese dann in Kernel32.dll an. Gleiches gilt für FindFirstFile(). FindNextFile() in Kernel32.dll ruft wierderum eine Funktion in Ntdll.dll auf. Diese lädt dann in das EAX-Register die Nummer für NtQueryDirectoryFile(), das Kernel-Äquivalent zu FindNextFile(). Außerdem lädt Ntdll.dll EDX mit der Adresse der Parameter für FindNextFile im User-Space. Schließlich führt Ntdll.dll INT 2E oder den Befehl SYSENTER aus, um via Trap in den Kernel-Mode zu wechseln.

In diesem Beispiel kann ein Rootkit die IAT der Applikation so überschreiben, dass sie statt auf die Funktion in Kernel32.dll auf eine eigene zeigt. Auf die gleiche Weise könnte es sich Ntdll.dll vornehmen. Was ein Angreifer mit dieser Technik erreichen kann, hängt eigentlich nur von seiner Fantasie ab. Das Rootkit könnte beispielsweise die Originalfunktion aufrufen und dann die Ergebnisse nach belieben filtern, um Dinge wie Dateien, Verzeichnisse, Registry-Schlüssel oder Prozesse zu verstecken.

Die Sache hat nur einen Haken: Jeder Prozess bekommt seinen eigenen Adressraum. Um die IAT eines anderen Prozesses zu ändern, muss das Rootkit Prozessgrenzen überschreiten. Richter und Pietrek haben auf diesem Gebiet bereits einige Arbeiten veröffentlicht, mehr Informationen dazu finden sich unter [1, 2, 3]

Anm. d. Ü.: Die einfachste Methode Rootkit-Code im Kontext eines Prozesses auszuführen, besteht darin, die von den meisten Windows-Programmen genutzte Bibliothek User32.dll über den Registry-Key AppInit_DLLs dazu zu veranlasssen, eine Rootkit-DLL zu laden. Alternativ kann man sich mit OpenProcess() ein Handle zu einem Prozess besorgen und den eigenen Code dann via WriteProcessMemory() und CreateRemoteThread() einschleusen.

Wie bereits erwähnt, ist das Win32-Subsystem nur ein Fenster zum Kernel. Die Adressen der eigentlichen Betriebssystemfunktionen enthält eine Kerneltabelle, die als System Service Descriptor Table (SSDT) oder auch als System Call Table bezeichnet wird. Diese Adressen korrepondieren zu den NtXXX-Funktionen, die in Ntoskrnl.exe implementiert sind. Ein Kernel-Mode-Rootkit kann diese Tabelle direkt ändern und die Einträge der gewünschten NtXXX-Funktionen mit Zeigern auf Rootkit-Code ersetzen. Das ist eine sehr mächtige Technik, denn anstatt sich nur in die IAT einzelner Programme einzuhängen, verankert sie einen Hook, der alle Prozesse betrifft. Analog zum IAT-Hook-Beispiel könnte ein Kernel-Mode-Rootkit NtQueryDirectoryFile() umleiten, um Dateien und Verzeichnisse im lokalen Dateisystem zu verstecken.

Eine fortgeschrittenere Technik als IAT- oder SSDT-Hooking ist das direkte Einklinken in Funktionen -- meist als Inline Function Hooking bezeichnet. Statt Zeiger in Tabellen zu überschreiben – was, wie wir in einem späteren Teil zeigen werden, leicht zu entdecken ist – ersetzt das Rootkit dabei ein paar Bytes der Originalfunktion. Normalerweise fügt das Rootkit dabei einen unbedingten Sprung zum Rootkit-Code ein. Viele Windows-API-Funktionen beginnen mit einem Standardvorspann:

8bff     mov edi, edi
55 push ebp
8bec mov ebp, esp

Die zu überschreibenden Bytes der Funktion sichert das Rootkit, um deren Verhalten nicht zu ändern. Dann überschreibt es diesen Teil mit einem Sprung auf den eigenen Code. Die aufgeführten fünf Bytes liegen auf einer geraden Speichergrenze und genügen für die meisten Sprungtypen oder einen Call-Befehl.

e9 xx xx xx xx    jmp xxxxxxxx

Dabei steht "xx xx xx xx" für die Adresse des Rootkits. Danach kann das Rootkit an die Adresse des Original-Codes plus einem Offset springen und verändern, was die Original-Funktion des Betriebssystems zurückgeliefert hat.

Inline Function Hooking hat wie die meisten Rootkit-Techniken eine Reihe legitimer Anwendungsbereiche. Microsoft Research hat es erstmals auf einer Konferenz dokumentiert [4]. Mittlerweile hat Microsoft seine Anwendung weit über die Forschung ausgedehnt und arbeiten am so genannten "Hot Patching", durch das ein System ohne Neustart gepatcht werden kann.