morpheus
27.02.2006, 21:34
Ich habe den Text trennen muessen, weil er zu lange ist. Die zusammenhaengende, eventuell auch besser lesbare Version, gibt es unter http://hxdef.net.ru/knowhow/hidingge.txt bzw. http://morph3us.org/security/windoze/unsichtbar-unter-windoze-nt.txt.
Vielen Dank an nait und destructor fuers Korrektur lesen.
=======================[ Unsichtbarkeit auf NT Boxen ]==========================
Wie man unter Windows NT unsichtbar wird
----------------------------------------
Author: Holy_Father <holy_father@phreaker.net>
Translation: Waldegger Thomas <morpheus@buha.info>
http://morph3us.org/
Version: 1.2 German
Date: 27.02.2006
Web: http://www.hxdef.org, http://hxdef.net.ru,
http://hxdef.czweb.org, http://rootkit.host.sk
Link: http://www.rootkit.com
=====[ 1. Inhalt ]================================================== ============
1. Inhalt
2. Einleitung
3. Dateien
3.1 NtQueryDirectoryFile
3.2 NtVdmControl
4. Prozesse
5. Registry
5.1 NtEnumerateKey
5.2 NtEnumerateValueKey
6. Dienste und Treiber
7. Hooks und deren Ausbreitung
7.1 Rechte
7.2 Systemweite Hooks
7.3 Neu erzeugte Prozesse
7.4 DLLs
8. Speicher
9. Handle
9.1 Bestimmung von Handles und dessen Typ
10. Ports
10.1 Netstat, OpPorts unter WinXP, FPort unter WinXP
10.2 OpPorts unter Win2k und NT4, FPort unter Win2k
11. Abschlussbemerkung
=====[ 2. Einleitung ]================================================== ========
Dieses Dokument beschreibt Techniken, um Objekte, Dateien, Dienste, Prozesse
etc. unter Windoze NT unsichtbar zu machen. Diese Techniken basieren auf
Windoze API Hooking, was in meinem Paper "Hooking Windows API" naeher
beschrieben wird.
Alles, was hier besprochen wird, stammt aus meiner Forschung und Arbeit
waehrend der Entwicklung von Rootkits fuer NT-Systeme. Deshalb besteht die
Moeglichkeit, dass es effizientere und einfachere Techniken gibt. Das schliesst
auch die Implementierung selber mit ein.
Das Verstecken von beliebigen Objekten wird bewerkstelligt, indem die
Funktionen, welche das Objekt auflisten wuerden, so abgeaendert werden, dass
diese es nicht mehr anzeigen.
Die grundlegende Methode (es sei denn es wird explizit gegenteiliges gesagt)
ist es die Orginal-Funktion mit den urspruenglichen Argumenten aufzurufen und
den Rueckgabewert dieser zu veraendern.
In dieser Version des Papers werden Methoden fuer das Verstecken von Dateien,
Prozessen, Schluesseln und Werten der Registry, Diensten und Treibern,
alloziertem Speicher, Handles und Ports behandelt.
=====[ 3. Dateien ]================================================== ===========
Es gibt mehrere Moeglichkeiten Dateien zu verstecken, sodass diese vom
Betriebssystem nicht mehr angezeigt werden. Wir beschraenken uns auf das
API-Hooking und lassen Techniken, welche auf Features des Dateisystem aufbauen,
aussen vor. Es ist auch um einiges leichter, da wir nicht wissen muessen wie
das Dateisystem genau funktioniert.
=====[ 3.1 NtQueryDirectoryFile ]===============================================
Das Suchen nach Dateien wird unter NT realisiert, indem alle Dateien eines
Ordners aufgelistet werden. Wenn dieser Ordner neben Dateien auch Verzeichnisse
beinhaelt dann werden die Inhalte dieser wiederum aufgelistet. Fuer die
Auflistung von Dateien ist die Funktion 'NtQueryDirectoryFile' zustaendig.
NTSTATUS NtQueryDirectoryFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG FileInformationLength,
IN FILE_INFORMATION_CLASS FileInformationClass,
IN BOOLEAN ReturnSingleEntry,
IN PUNICODE_STRING FileName OPTIONAL,
IN BOOLEAN RestartScan
);
Wichtige Parameter fuer uns sind 'FileHandle', 'FileInformation' und
'FileInformationClass'. 'FileHandle' ist das Handle auf ein Directory-Objekt,
welcher von der Funktion 'NtOpenFile' geliefert wird. 'FileInformation' ist ein
Zeiger auf allozierten Speicher, in welchen die Funktion Daten schreibt.
'FileInformationClass' bestimmt den Typ des Datensatzes, welcher nach
'FileInformation' geschrieben wird.
'FileInformationClass' hat mehrere Aufzaehlungstypen, wobei fuer uns nur
vier Typen, welche fuer die Auflistung von Verzeichnisinhalten zustaendig sind,
von Bedeutung sind:
#define FileDirectoryInformation 1
#define FileFullDirectoryInformation 2
#define FileBothDirectoryInformation 3
#define FileNamesInformation 12
In den 'FileInformation' wird durch das setzen des Typs
'FileDirectoryInformation' folgende Struktur geschrieben:
typedef struct _FILE_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
bei 'FileFullDirectoryInformation':
typedef struct _FILE_FULL_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaInformationLength;
WCHAR FileName[1];
} FILE_FULL_DIRECTORY_INFORMATION, *PFILE_FULL_DIRECTORY_INFORMATION;
bei 'FileBothDirectoryInformation':
typedef struct _FILE_BOTH_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaInformationLength;
UCHAR AlternateNameLength;
WCHAR AlternateName[12];
WCHAR FileName[1];
} FILE_BOTH_DIRECTORY_INFORMATION, *PFILE_BOTH_DIRECTORY_INFORMATION;
und bei 'FileNamesInformation':
typedef struct _FILE_NAMES_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
Diese Funktion schreibt eine Liste von diesen Datenstrukturen in
'FileInformation'. Nur drei Variablen in allen dieses Strukturtypen sind
wichtig fuer uns.
'NextEntryOffset' ist die Laenge eines bestimmten Listenelementes. Das
erste Element befindet sich an der Adresse 'FileInformation' + 0. Das zweite
Element findet man dann unter der Adresse 'FileInformation' +
'NextEntryOffset' des ersten Elementes. Beim letzten Element ist
'NextEntryOffset' auf 0 gesetzt.
'FileName' ist der ganze Name der Datei.
'FileNameLength' ist die Laenge des Dateinamen.
Wenn wir eine Datei verstecken wollen dann muessen wir neben diesen
vier Typen bei jedem zurueckgebenen Datensatz vergleichen, ob dessen Name mit
einem Namen, welchen wir verstecken wollen, uebereinstimmt. Wenn wir den ersten
Datensatz verstecken wollen dann muessen wir alle folgenden Datensaetze um die
Groesse des ersten "nach vorne" verschieben.
Wenn wir einen anderen Datensatz verstecken wollen dann muessen wir einfach
den Wert von 'NextEntryOffset' des vorigen Elementes veraendern. Der neue Wert
fuer 'NextEntryOffset' waere 0, wenn wir das letzte Element verstecken wollen,
ansonsten den Wert des zu versteckenden Elementes plus den Wert des vorigen
Elementes. Dann solten wir noch den Wert des Parameters 'Unknown' des vorigen
Elementes veraendern, da es vermutlich ein Index fuer die naechste Suche ist.
Der Wert von 'Unknown' des vorigen Elementes sollte den Wert von 'Unknown' des
zu versteckenden Elementes haben.
Wenn kein Datensatz, der gesehen werden sollte, gefunden wird dann
muessen wir den Fehler 'STATUS_NO_SUCH_FILE' zurueckgeben.
#define STATUS_NO_SUCH_FILE 0xC000000F
=====[ 3.2 NtVdmControl ]================================================== =====
Aus unbekanntem Grund bekommt NTVDM (NT Virtual DOS Machine oder auch WOW
(Windows on Windows) genannt) auch eine Liste von Dateien mit der Funktion
'NtVdmContol'.
NTSTATUS NtVdmControl(
IN ULONG ControlCode,
IN PVOID ControlData
);
Der Parameter 'ControlCode' legt die Struktur der Daten, welche in den Puffer
'ControlData' geschrieben werden, fest. Wenn 'ControlCode' den Wert von
'VdmDirectoryFile' hat dann macht diese Funkion dasselbe wie die Funktion
'NtQueryDirectoryFile' wenn der Parameter 'FileInformationClass' den Wert
'FileBothDirectoryInformation' enthaelt.
#define VdmDirectoryFile 6
'ControlData' wird dann wie 'FileInformation' verwendet. Der einzige
Unterschied ist, dass wir die Laenge des Puffers nicht kennen, weshalb wir ihn
manuell ermitteln muessen. Wir muessen von allen Eintraegen die Werte von
'NextEntryOffset' addieren und fuer den letzten Eintrag noch einmal 0x5E und
die Laenge des Dateinamens. Die Methoden, um Dateien zu verstecken, entsprechen
denen, welche bei 'NtQueryDirectoryFile' angewendet wurden.
=====[ 4. Prozesse ]================================================== ==========
Verschiedenste Systeminformationen lassen sich mit 'NtQuerySystemInformation'
ermitteln.
NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
'SystemInformationClass' gibt den Typ der Information, welche wir ermitteln
wollen an, 'SystemInformation' ist ein Zeiger auf den Ausgabe-Puffer der
Funktion, 'SystemInformationLength' entspricht der Laenge des Puffers und
'ReturnLength' ist die Anzahl der geschriebenen Bytes. Fuer die Auflistung
aller laufenden Prozesse wird der Typ 'SystemProcessesAndThreadsInformation'
verwendet.
#define SystemInformationClass 5
Die zurueckgegebene Datenstruktur in dem Puffer 'SystemInformation' ist:
typedef struct _SYSTEM_PROCESSES {
ULONG NextEntryDelta;
ULONG ThreadCount;
ULONG Reserved1[6];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ProcessName;
KPRIORITY BasePriority;
ULONG ProcessId;
ULONG InheritedFromProcessId;
ULONG HandleCount;
ULONG Reserved2[2];
VM_COUNTERS VmCounters;
IO_COUNTERS IoCounters; // Windows 2000 only
SYSTEM_THREADS Threads[1];
} SYSTEM_PROCESSES, *PSYSTEM_PROCESSES;
Das Verstecken von Prozessen ist dem Verstecken von Dateien sehr aehnlich.
Wir muessen 'NextEntryDelta' des vorigen Datensatzes auf den Wert des
Datensatzes, welchen wir verstecken wollen, setzen. Fuer gewoehnlich werden
wir hier den ersten Prozess nicht verstecken wollen, da es sich dabei um den
Idle Prozess handelt.
=====[ 5. Registry ]================================================== ==========
Die Windoze-Registry ist eine grosse Baumstruktur, welche zwei wichtige
Datenstrukturen beinhaltet, die wir verstecken wollen. Der erste Typ sind
Registry-Schluessel und der zweite sind Registry-Werte. Aufgrund der Struktur
der Registry ist es um einiges schwieriger Daten zu verstecken als im Vergleich
zu Dateien und Prozessen.
=====[ 5.1 NtEnumerateKey ]================================================== ===
Aufgrund der Struktur der Registry ist es nicht einfach moeglich eine Liste
von Schluesseln fuer einem bestimmten Teil der Registry zu erhalten. Wir haben
nur die Moeglichkeit Information eines Keys ueber dessen Index in einem Teil
der Registry zu erhalten. Diese Funktion wird von der Methode 'NtEnumerateKey'
zur Verfuegung gestellt.
NTSTATUS NtEnumerateKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_INFORMATION_CLASS KeyInformationClass,
OUT PVOID KeyInformation,
IN ULONG KeyInformationLength,
OUT PULONG ResultLength
);
'KeyHandle' ist das Handle auf einen Schluessel, von dessen Unterschluessel
wir Informationen mittels des Index bekommen wollen. Der Typ der
zurueckgebenen Information wird von 'KeyInformationClass' bestimmt. Die Daten
werden in den Puffer von 'KeyInformation' geschrieben und die Laenge dieses
Puffers wird in 'KeyInformationLength' gespeichert. Die Anzahl
der geschriebenen Bytes wird in 'ResultLength' zurueckgegeben.
Das wichtigste an was wir denken muessen, wenn wir einen Schluessel
verstecken wollen, ist, dass die Indizes aller folgenden Schluessel geshiftet
werden. Da wir in der Lage sind Informationen ueber einen Schluessel mit einem
hoeheren Index zu bekommen, indem wir nach einen Schluessel mit niedrigerem
Index fragen, muessen wir immer zaehlen wieviele Elemente zuvor versteckt
wurden und dann die richtige Anzahl zurueckgeben.
Hier ein Beispiel dazu: Wir nehmen an, dass wir ein paar Schluessel mit den
Namen A, B, C, D, E und F in einem Teil der Registry haben. Der Index startet
bei 0, was bedeutet, dass der Schluessel E den Index 4 hat. Wenn wir nun den
Schluessel B verstecken wollen und die gehookte Anwendung ruft 'NtEnumerateKey'
mit dem Index 4 als Argument auf dann sollten wir Informationen ueber den Key F
zurueckgeben, da der Index geshiftet wird. Das Problem ist nur, dass wir nicht
wissen, dass es zu einem Shift kommt. Sollten wir die Shifts nicht
beruecksichtigen und E anstatt F zurueckgeben, wenn nach dem Key mit dem Index
4 gefragt wird dann wuerden wir nichts zurueckgeben, wenn nach dem Key
mit Index 1 gefragt wird oder wir wuerden C zurueckgeben. Beides ist falsch,
weshalb wir auf die Shifts achten muessen.
Wenn wir den Shift zaehlen, indem wir fuer jeden Index von 0 bis zum Index
die Funktion neu aufrufen dann wuerden mir sehr lange warten (auf einem 1GHz
Rechner kann es bis zu 10 Sekunden bei einer Standard-Registry dauern -
was viel zu lange ist). Deshalb muessen wir uns eine bessere Methode
ueberlegen. Wir wissen, dass die Schluessel (mit Ausnahme von Referenzen)
alphabetisch sortiert sind. Wenn wir Referenzen ignorieren, welche wir gar
nicht verstecken wollen, dann koennen wir den Shift mit folgender Methode
ermitteln: Wir sortieren unsere Liste von Schluesselnamen, welche wir
verstecken wollen, erstmal alphabetisch ('RtlCompareUnicodeString' kann dafuer
verwendet werden). Wenn die Anwendung dann 'NtEnumerateKey' aufruft dann werden
wir die Funktion nicht nochmal mit unveraenderten Parametern aufrufen, sondern
den Namen des Datensatzes ueber den angegebenen Index herausfinden.
NTSTATUS RtlCompareUnicodeString(
IN PUNICODE_STRING String1,
IN PUNICODE_STRING String2,
IN BOOLEAN CaseInSensitive
);
'String1' und 'String2' sind Strings, welche man vergleichen moechte. Wenn
'CaseInSensitive' true ist dann wird die Gross- und Kleinschreibung ignoriert.
Der Rueckgabewert der Funktion beschreibt die Relation der beiden Strings:
result > 0: String1 > String2
result = 0: String1 = String2
result < 0: String1 < String2
Jetzt bestimmen wir die "Grenze" (Anmerkung d. Ueb.: es wird gleich erklaert,
was damit gemeint ist). Wir vergleichen den Namen des Schluessels, welcher
durch den Index spezifiziert wurde, alphabetisch mit den Namen in unserer
Liste. Die "Grenze" ist nun der letzte lexikografisch kleinere Name in unserer
Liste. Der Wert des Shiftes entspricht meistens dem Index der "Grenze"
in unserer Liste. Aber nicht alle Elemente unserer Liste muessen einem
gueltigen Schluessel in dem Teil der Registry, in welchem wir uns befinden,
entsprechen. Deshalb muessen wir fuer alle Elemente in unserer Liste bis hin
zu der vorhin ermittelten "Grenze" erfragen, ob sich diese in dem Teil
der Registry befinden. Dies koennen wir mit 'NtOpenKey' bewerkstelligen.
NTSTATUS NtOpenKey(
OUT PHANDLE KeyHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
);
'KeyHandle' ist das Handle auf den uebergeordneten Schluessel. Wir verwenden
das Handle, welchen wir von 'NtEnumerateKey' bekommen. 'DesiredAccess' gibt die
gewuenschten Zugriffsrechte an. 'KEY_ENUMERATE_SUB_KEYS' ist der richtige Wert
dafuer. 'ObjectAttributes' beschreibt den Unterschluessel, welchen wir oeffnen
moechten. (den Namen mit eingeschlossen)
#define KEY_ENUMERATE_SUB_KEYS 8
Wenn der Rueckgabewert von 'NtOpenKey' 0 ist dann war das Oeffnen
erfolgreich, was bedeutet, dass der Schluessel aus unserer Liste existiert.
Ein geoeffneter Schluessel sollte wieder mit 'NtClose' geschlossen werden.
NTSTATUS NtClose(
IN HANDLE Handle
);
Bei jedem Aufruf von 'NtEnumerateKey' werden wir den Shift als die Anzahl
der Schluessel in unserer Liste in dem gegebenen Teil der Registry zaehlen.
Dann zaehlen wir diesen Shift zu dem Index-Parameter dazu und rufen
schliesslich die Orginal-'NtEnumerateKey' Funktion auf.
Um den Namen des Schluessel, der durch den Index bestimmt wird, zu bekommen
werden wir den Wert 'KeyBasicInformation' fuer 'KeyInformationClass' verwenden.
#define KeyBasicInformation 0
'NtEnumerateKey' gibt diese Struktur in 'KeyInformation' zurueck:
typedef struct _KEY_BASIC_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG NameLength;
WCHAR Name[1];
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
Die einzigen werte, die uns interessieren, sind der Name des Schluessels
'Name' und die Laenge des Namens 'NameLength'. Wenn es keinen Eintrag fuer
den geshifteten Index gibt dann geben wir den Fehler
'STATUS_EA_LIST_INCONSISTENT' zurueck.
#define STATUS_EA_LIST_INCONSISTENT 0x80000014
=====[ 5.2 NtEnumerateValueKey ]================================================
Registry-Werte sind nicht alphabetisch sortiert. Zum Glueck ist die Anzahl
der Werte in einem Schluessel verhaeltnismaessig klein, weshalb wir die Methode
neu aufrufen koennen um an den Shift zu kommen. Die API-Funktion, um
Informationen ueber einen Wert zu bekommen, lautet 'NtEnumerateValueKey'.
NTSTATUS NtEnumerateValueKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,
OUT PULONG ResultLength
);
'KeyHandle' ist hier wieder das Handle auf den uebergeordneten Schluessel.
'Index' ist ein Index auf die Liste der Werte in dem uebergebenen Schluessel.
'KeyValueInformationClass' beschreibt den Typ der Information, welche im
'KeyValueInformation'-Puffer gespeichert wird. Die Laenge dieses Puffers wird
mit 'KeyValueInformationLength' in Bytes angegeben. Die Anzahl
der geschriebenen Bytes wird in 'ResultLength' gespeichert.
Hier muessen wir wieder den Shift entsprechend zu der Anzahl der Werte in einem
Schluessel ermitteln, indem wir die Funktion fuer alle Indizes von 0 bis zum
Wert des Index aufrufen. Den Name der Schluesselwerte bekommt man, indem man
'KeyValueInformationClass' auf 'KeyValueBasicInformation' setzt.
#define KeyValueBasicInformation 0
Dann haben wir folgende Struktur im 'KeyValueInformation' Puffer:
typedef struct _KEY_VALUE_BASIC_INFORMATION {
ULONG TitleIndex;
ULONG Type;
ULONG NameLength;
WCHAR Name[1];
} KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION;
Hier sind wir wieder nur an 'Name' und 'NameLength' interessiert:
Wenn es keinen Eintrag fuer den geshifteten Index gibt dann wird
der Fehler 'STATUS_NO_MORE_ENTRIES' zurueckgegeben.
#define STATUS_NO_MORE_ENTRIES 0x8000001A
Vielen Dank an nait und destructor fuers Korrektur lesen.
=======================[ Unsichtbarkeit auf NT Boxen ]==========================
Wie man unter Windows NT unsichtbar wird
----------------------------------------
Author: Holy_Father <holy_father@phreaker.net>
Translation: Waldegger Thomas <morpheus@buha.info>
http://morph3us.org/
Version: 1.2 German
Date: 27.02.2006
Web: http://www.hxdef.org, http://hxdef.net.ru,
http://hxdef.czweb.org, http://rootkit.host.sk
Link: http://www.rootkit.com
=====[ 1. Inhalt ]================================================== ============
1. Inhalt
2. Einleitung
3. Dateien
3.1 NtQueryDirectoryFile
3.2 NtVdmControl
4. Prozesse
5. Registry
5.1 NtEnumerateKey
5.2 NtEnumerateValueKey
6. Dienste und Treiber
7. Hooks und deren Ausbreitung
7.1 Rechte
7.2 Systemweite Hooks
7.3 Neu erzeugte Prozesse
7.4 DLLs
8. Speicher
9. Handle
9.1 Bestimmung von Handles und dessen Typ
10. Ports
10.1 Netstat, OpPorts unter WinXP, FPort unter WinXP
10.2 OpPorts unter Win2k und NT4, FPort unter Win2k
11. Abschlussbemerkung
=====[ 2. Einleitung ]================================================== ========
Dieses Dokument beschreibt Techniken, um Objekte, Dateien, Dienste, Prozesse
etc. unter Windoze NT unsichtbar zu machen. Diese Techniken basieren auf
Windoze API Hooking, was in meinem Paper "Hooking Windows API" naeher
beschrieben wird.
Alles, was hier besprochen wird, stammt aus meiner Forschung und Arbeit
waehrend der Entwicklung von Rootkits fuer NT-Systeme. Deshalb besteht die
Moeglichkeit, dass es effizientere und einfachere Techniken gibt. Das schliesst
auch die Implementierung selber mit ein.
Das Verstecken von beliebigen Objekten wird bewerkstelligt, indem die
Funktionen, welche das Objekt auflisten wuerden, so abgeaendert werden, dass
diese es nicht mehr anzeigen.
Die grundlegende Methode (es sei denn es wird explizit gegenteiliges gesagt)
ist es die Orginal-Funktion mit den urspruenglichen Argumenten aufzurufen und
den Rueckgabewert dieser zu veraendern.
In dieser Version des Papers werden Methoden fuer das Verstecken von Dateien,
Prozessen, Schluesseln und Werten der Registry, Diensten und Treibern,
alloziertem Speicher, Handles und Ports behandelt.
=====[ 3. Dateien ]================================================== ===========
Es gibt mehrere Moeglichkeiten Dateien zu verstecken, sodass diese vom
Betriebssystem nicht mehr angezeigt werden. Wir beschraenken uns auf das
API-Hooking und lassen Techniken, welche auf Features des Dateisystem aufbauen,
aussen vor. Es ist auch um einiges leichter, da wir nicht wissen muessen wie
das Dateisystem genau funktioniert.
=====[ 3.1 NtQueryDirectoryFile ]===============================================
Das Suchen nach Dateien wird unter NT realisiert, indem alle Dateien eines
Ordners aufgelistet werden. Wenn dieser Ordner neben Dateien auch Verzeichnisse
beinhaelt dann werden die Inhalte dieser wiederum aufgelistet. Fuer die
Auflistung von Dateien ist die Funktion 'NtQueryDirectoryFile' zustaendig.
NTSTATUS NtQueryDirectoryFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG FileInformationLength,
IN FILE_INFORMATION_CLASS FileInformationClass,
IN BOOLEAN ReturnSingleEntry,
IN PUNICODE_STRING FileName OPTIONAL,
IN BOOLEAN RestartScan
);
Wichtige Parameter fuer uns sind 'FileHandle', 'FileInformation' und
'FileInformationClass'. 'FileHandle' ist das Handle auf ein Directory-Objekt,
welcher von der Funktion 'NtOpenFile' geliefert wird. 'FileInformation' ist ein
Zeiger auf allozierten Speicher, in welchen die Funktion Daten schreibt.
'FileInformationClass' bestimmt den Typ des Datensatzes, welcher nach
'FileInformation' geschrieben wird.
'FileInformationClass' hat mehrere Aufzaehlungstypen, wobei fuer uns nur
vier Typen, welche fuer die Auflistung von Verzeichnisinhalten zustaendig sind,
von Bedeutung sind:
#define FileDirectoryInformation 1
#define FileFullDirectoryInformation 2
#define FileBothDirectoryInformation 3
#define FileNamesInformation 12
In den 'FileInformation' wird durch das setzen des Typs
'FileDirectoryInformation' folgende Struktur geschrieben:
typedef struct _FILE_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
bei 'FileFullDirectoryInformation':
typedef struct _FILE_FULL_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaInformationLength;
WCHAR FileName[1];
} FILE_FULL_DIRECTORY_INFORMATION, *PFILE_FULL_DIRECTORY_INFORMATION;
bei 'FileBothDirectoryInformation':
typedef struct _FILE_BOTH_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaInformationLength;
UCHAR AlternateNameLength;
WCHAR AlternateName[12];
WCHAR FileName[1];
} FILE_BOTH_DIRECTORY_INFORMATION, *PFILE_BOTH_DIRECTORY_INFORMATION;
und bei 'FileNamesInformation':
typedef struct _FILE_NAMES_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
Diese Funktion schreibt eine Liste von diesen Datenstrukturen in
'FileInformation'. Nur drei Variablen in allen dieses Strukturtypen sind
wichtig fuer uns.
'NextEntryOffset' ist die Laenge eines bestimmten Listenelementes. Das
erste Element befindet sich an der Adresse 'FileInformation' + 0. Das zweite
Element findet man dann unter der Adresse 'FileInformation' +
'NextEntryOffset' des ersten Elementes. Beim letzten Element ist
'NextEntryOffset' auf 0 gesetzt.
'FileName' ist der ganze Name der Datei.
'FileNameLength' ist die Laenge des Dateinamen.
Wenn wir eine Datei verstecken wollen dann muessen wir neben diesen
vier Typen bei jedem zurueckgebenen Datensatz vergleichen, ob dessen Name mit
einem Namen, welchen wir verstecken wollen, uebereinstimmt. Wenn wir den ersten
Datensatz verstecken wollen dann muessen wir alle folgenden Datensaetze um die
Groesse des ersten "nach vorne" verschieben.
Wenn wir einen anderen Datensatz verstecken wollen dann muessen wir einfach
den Wert von 'NextEntryOffset' des vorigen Elementes veraendern. Der neue Wert
fuer 'NextEntryOffset' waere 0, wenn wir das letzte Element verstecken wollen,
ansonsten den Wert des zu versteckenden Elementes plus den Wert des vorigen
Elementes. Dann solten wir noch den Wert des Parameters 'Unknown' des vorigen
Elementes veraendern, da es vermutlich ein Index fuer die naechste Suche ist.
Der Wert von 'Unknown' des vorigen Elementes sollte den Wert von 'Unknown' des
zu versteckenden Elementes haben.
Wenn kein Datensatz, der gesehen werden sollte, gefunden wird dann
muessen wir den Fehler 'STATUS_NO_SUCH_FILE' zurueckgeben.
#define STATUS_NO_SUCH_FILE 0xC000000F
=====[ 3.2 NtVdmControl ]================================================== =====
Aus unbekanntem Grund bekommt NTVDM (NT Virtual DOS Machine oder auch WOW
(Windows on Windows) genannt) auch eine Liste von Dateien mit der Funktion
'NtVdmContol'.
NTSTATUS NtVdmControl(
IN ULONG ControlCode,
IN PVOID ControlData
);
Der Parameter 'ControlCode' legt die Struktur der Daten, welche in den Puffer
'ControlData' geschrieben werden, fest. Wenn 'ControlCode' den Wert von
'VdmDirectoryFile' hat dann macht diese Funkion dasselbe wie die Funktion
'NtQueryDirectoryFile' wenn der Parameter 'FileInformationClass' den Wert
'FileBothDirectoryInformation' enthaelt.
#define VdmDirectoryFile 6
'ControlData' wird dann wie 'FileInformation' verwendet. Der einzige
Unterschied ist, dass wir die Laenge des Puffers nicht kennen, weshalb wir ihn
manuell ermitteln muessen. Wir muessen von allen Eintraegen die Werte von
'NextEntryOffset' addieren und fuer den letzten Eintrag noch einmal 0x5E und
die Laenge des Dateinamens. Die Methoden, um Dateien zu verstecken, entsprechen
denen, welche bei 'NtQueryDirectoryFile' angewendet wurden.
=====[ 4. Prozesse ]================================================== ==========
Verschiedenste Systeminformationen lassen sich mit 'NtQuerySystemInformation'
ermitteln.
NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
'SystemInformationClass' gibt den Typ der Information, welche wir ermitteln
wollen an, 'SystemInformation' ist ein Zeiger auf den Ausgabe-Puffer der
Funktion, 'SystemInformationLength' entspricht der Laenge des Puffers und
'ReturnLength' ist die Anzahl der geschriebenen Bytes. Fuer die Auflistung
aller laufenden Prozesse wird der Typ 'SystemProcessesAndThreadsInformation'
verwendet.
#define SystemInformationClass 5
Die zurueckgegebene Datenstruktur in dem Puffer 'SystemInformation' ist:
typedef struct _SYSTEM_PROCESSES {
ULONG NextEntryDelta;
ULONG ThreadCount;
ULONG Reserved1[6];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ProcessName;
KPRIORITY BasePriority;
ULONG ProcessId;
ULONG InheritedFromProcessId;
ULONG HandleCount;
ULONG Reserved2[2];
VM_COUNTERS VmCounters;
IO_COUNTERS IoCounters; // Windows 2000 only
SYSTEM_THREADS Threads[1];
} SYSTEM_PROCESSES, *PSYSTEM_PROCESSES;
Das Verstecken von Prozessen ist dem Verstecken von Dateien sehr aehnlich.
Wir muessen 'NextEntryDelta' des vorigen Datensatzes auf den Wert des
Datensatzes, welchen wir verstecken wollen, setzen. Fuer gewoehnlich werden
wir hier den ersten Prozess nicht verstecken wollen, da es sich dabei um den
Idle Prozess handelt.
=====[ 5. Registry ]================================================== ==========
Die Windoze-Registry ist eine grosse Baumstruktur, welche zwei wichtige
Datenstrukturen beinhaltet, die wir verstecken wollen. Der erste Typ sind
Registry-Schluessel und der zweite sind Registry-Werte. Aufgrund der Struktur
der Registry ist es um einiges schwieriger Daten zu verstecken als im Vergleich
zu Dateien und Prozessen.
=====[ 5.1 NtEnumerateKey ]================================================== ===
Aufgrund der Struktur der Registry ist es nicht einfach moeglich eine Liste
von Schluesseln fuer einem bestimmten Teil der Registry zu erhalten. Wir haben
nur die Moeglichkeit Information eines Keys ueber dessen Index in einem Teil
der Registry zu erhalten. Diese Funktion wird von der Methode 'NtEnumerateKey'
zur Verfuegung gestellt.
NTSTATUS NtEnumerateKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_INFORMATION_CLASS KeyInformationClass,
OUT PVOID KeyInformation,
IN ULONG KeyInformationLength,
OUT PULONG ResultLength
);
'KeyHandle' ist das Handle auf einen Schluessel, von dessen Unterschluessel
wir Informationen mittels des Index bekommen wollen. Der Typ der
zurueckgebenen Information wird von 'KeyInformationClass' bestimmt. Die Daten
werden in den Puffer von 'KeyInformation' geschrieben und die Laenge dieses
Puffers wird in 'KeyInformationLength' gespeichert. Die Anzahl
der geschriebenen Bytes wird in 'ResultLength' zurueckgegeben.
Das wichtigste an was wir denken muessen, wenn wir einen Schluessel
verstecken wollen, ist, dass die Indizes aller folgenden Schluessel geshiftet
werden. Da wir in der Lage sind Informationen ueber einen Schluessel mit einem
hoeheren Index zu bekommen, indem wir nach einen Schluessel mit niedrigerem
Index fragen, muessen wir immer zaehlen wieviele Elemente zuvor versteckt
wurden und dann die richtige Anzahl zurueckgeben.
Hier ein Beispiel dazu: Wir nehmen an, dass wir ein paar Schluessel mit den
Namen A, B, C, D, E und F in einem Teil der Registry haben. Der Index startet
bei 0, was bedeutet, dass der Schluessel E den Index 4 hat. Wenn wir nun den
Schluessel B verstecken wollen und die gehookte Anwendung ruft 'NtEnumerateKey'
mit dem Index 4 als Argument auf dann sollten wir Informationen ueber den Key F
zurueckgeben, da der Index geshiftet wird. Das Problem ist nur, dass wir nicht
wissen, dass es zu einem Shift kommt. Sollten wir die Shifts nicht
beruecksichtigen und E anstatt F zurueckgeben, wenn nach dem Key mit dem Index
4 gefragt wird dann wuerden wir nichts zurueckgeben, wenn nach dem Key
mit Index 1 gefragt wird oder wir wuerden C zurueckgeben. Beides ist falsch,
weshalb wir auf die Shifts achten muessen.
Wenn wir den Shift zaehlen, indem wir fuer jeden Index von 0 bis zum Index
die Funktion neu aufrufen dann wuerden mir sehr lange warten (auf einem 1GHz
Rechner kann es bis zu 10 Sekunden bei einer Standard-Registry dauern -
was viel zu lange ist). Deshalb muessen wir uns eine bessere Methode
ueberlegen. Wir wissen, dass die Schluessel (mit Ausnahme von Referenzen)
alphabetisch sortiert sind. Wenn wir Referenzen ignorieren, welche wir gar
nicht verstecken wollen, dann koennen wir den Shift mit folgender Methode
ermitteln: Wir sortieren unsere Liste von Schluesselnamen, welche wir
verstecken wollen, erstmal alphabetisch ('RtlCompareUnicodeString' kann dafuer
verwendet werden). Wenn die Anwendung dann 'NtEnumerateKey' aufruft dann werden
wir die Funktion nicht nochmal mit unveraenderten Parametern aufrufen, sondern
den Namen des Datensatzes ueber den angegebenen Index herausfinden.
NTSTATUS RtlCompareUnicodeString(
IN PUNICODE_STRING String1,
IN PUNICODE_STRING String2,
IN BOOLEAN CaseInSensitive
);
'String1' und 'String2' sind Strings, welche man vergleichen moechte. Wenn
'CaseInSensitive' true ist dann wird die Gross- und Kleinschreibung ignoriert.
Der Rueckgabewert der Funktion beschreibt die Relation der beiden Strings:
result > 0: String1 > String2
result = 0: String1 = String2
result < 0: String1 < String2
Jetzt bestimmen wir die "Grenze" (Anmerkung d. Ueb.: es wird gleich erklaert,
was damit gemeint ist). Wir vergleichen den Namen des Schluessels, welcher
durch den Index spezifiziert wurde, alphabetisch mit den Namen in unserer
Liste. Die "Grenze" ist nun der letzte lexikografisch kleinere Name in unserer
Liste. Der Wert des Shiftes entspricht meistens dem Index der "Grenze"
in unserer Liste. Aber nicht alle Elemente unserer Liste muessen einem
gueltigen Schluessel in dem Teil der Registry, in welchem wir uns befinden,
entsprechen. Deshalb muessen wir fuer alle Elemente in unserer Liste bis hin
zu der vorhin ermittelten "Grenze" erfragen, ob sich diese in dem Teil
der Registry befinden. Dies koennen wir mit 'NtOpenKey' bewerkstelligen.
NTSTATUS NtOpenKey(
OUT PHANDLE KeyHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
);
'KeyHandle' ist das Handle auf den uebergeordneten Schluessel. Wir verwenden
das Handle, welchen wir von 'NtEnumerateKey' bekommen. 'DesiredAccess' gibt die
gewuenschten Zugriffsrechte an. 'KEY_ENUMERATE_SUB_KEYS' ist der richtige Wert
dafuer. 'ObjectAttributes' beschreibt den Unterschluessel, welchen wir oeffnen
moechten. (den Namen mit eingeschlossen)
#define KEY_ENUMERATE_SUB_KEYS 8
Wenn der Rueckgabewert von 'NtOpenKey' 0 ist dann war das Oeffnen
erfolgreich, was bedeutet, dass der Schluessel aus unserer Liste existiert.
Ein geoeffneter Schluessel sollte wieder mit 'NtClose' geschlossen werden.
NTSTATUS NtClose(
IN HANDLE Handle
);
Bei jedem Aufruf von 'NtEnumerateKey' werden wir den Shift als die Anzahl
der Schluessel in unserer Liste in dem gegebenen Teil der Registry zaehlen.
Dann zaehlen wir diesen Shift zu dem Index-Parameter dazu und rufen
schliesslich die Orginal-'NtEnumerateKey' Funktion auf.
Um den Namen des Schluessel, der durch den Index bestimmt wird, zu bekommen
werden wir den Wert 'KeyBasicInformation' fuer 'KeyInformationClass' verwenden.
#define KeyBasicInformation 0
'NtEnumerateKey' gibt diese Struktur in 'KeyInformation' zurueck:
typedef struct _KEY_BASIC_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG NameLength;
WCHAR Name[1];
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
Die einzigen werte, die uns interessieren, sind der Name des Schluessels
'Name' und die Laenge des Namens 'NameLength'. Wenn es keinen Eintrag fuer
den geshifteten Index gibt dann geben wir den Fehler
'STATUS_EA_LIST_INCONSISTENT' zurueck.
#define STATUS_EA_LIST_INCONSISTENT 0x80000014
=====[ 5.2 NtEnumerateValueKey ]================================================
Registry-Werte sind nicht alphabetisch sortiert. Zum Glueck ist die Anzahl
der Werte in einem Schluessel verhaeltnismaessig klein, weshalb wir die Methode
neu aufrufen koennen um an den Shift zu kommen. Die API-Funktion, um
Informationen ueber einen Wert zu bekommen, lautet 'NtEnumerateValueKey'.
NTSTATUS NtEnumerateValueKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,
OUT PULONG ResultLength
);
'KeyHandle' ist hier wieder das Handle auf den uebergeordneten Schluessel.
'Index' ist ein Index auf die Liste der Werte in dem uebergebenen Schluessel.
'KeyValueInformationClass' beschreibt den Typ der Information, welche im
'KeyValueInformation'-Puffer gespeichert wird. Die Laenge dieses Puffers wird
mit 'KeyValueInformationLength' in Bytes angegeben. Die Anzahl
der geschriebenen Bytes wird in 'ResultLength' gespeichert.
Hier muessen wir wieder den Shift entsprechend zu der Anzahl der Werte in einem
Schluessel ermitteln, indem wir die Funktion fuer alle Indizes von 0 bis zum
Wert des Index aufrufen. Den Name der Schluesselwerte bekommt man, indem man
'KeyValueInformationClass' auf 'KeyValueBasicInformation' setzt.
#define KeyValueBasicInformation 0
Dann haben wir folgende Struktur im 'KeyValueInformation' Puffer:
typedef struct _KEY_VALUE_BASIC_INFORMATION {
ULONG TitleIndex;
ULONG Type;
ULONG NameLength;
WCHAR Name[1];
} KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION;
Hier sind wir wieder nur an 'Name' und 'NameLength' interessiert:
Wenn es keinen Eintrag fuer den geshifteten Index gibt dann wird
der Fehler 'STATUS_NO_MORE_ENTRIES' zurueckgegeben.
#define STATUS_NO_MORE_ENTRIES 0x8000001A