=======================[ Unsichtbarkeit auf NT Boxen ]========================== Wie man unter Windows NT unsichtbar wird ---------------------------------------- Author: Holy_Father Translation: Waldegger Thomas 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 =====[ 6. Dienste und Treiber ]================================================= Dienste und Treiber werden von vier unabhaengigen API-Funktionen aufgelistet. Deren Funktionsweise ist in jeder Windoze-Version unterschiedlich, weshalb wir alle vier Funktionen hooken muessen. BOOL EnumServicesStatusA( SC_HANDLE hSCManager, DWORD dwServiceType, DWORD dwServiceState, LPENUM_SERVICE_STATUS lpServices, DWORD cbBufSize, LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned, LPDWORD lpResumeHandle ); BOOL EnumServiceGroupW( SC_HANDLE hSCManager, DWORD dwServiceType, DWORD dwServiceState, LPBYTE lpServices, DWORD cbBufSize, LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned, LPDWORD lpResumeHandle, DWORD dwUnknown ); BOOL EnumServicesStatusExA( SC_HANDLE hSCManager, SC_ENUM_TYPE InfoLevel, DWORD dwServiceType, DWORD dwServiceState, LPBYTE lpServices, DWORD cbBufSize, LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned, LPDWORD lpResumeHandle, LPCTSTR pszGroupName ); BOOL EnumServicesStatusExW( SC_HANDLE hSCManager, SC_ENUM_TYPE InfoLevel, DWORD dwServiceType, DWORD dwServiceState, LPBYTE lpServices, DWORD cbBufSize, LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned, LPDWORD lpResumeHandle, LPCTSTR pszGroupName ); Der wichtigste Parameter ist 'lpServices', welcher auf den Puffer, in welchem die Liste der Dienste gespeichert wird, zeigt. Ebenso ist 'lpServicesReturned' fuer uns interessant, da dieser Pointer auf die Anzahl der Datensaetze zeigt. Die Struktur der Daten im Ausgabepuffer haengt von dem Typ der Funktion ab. Fuer die Funktionen 'EnumServicesStatusA' und 'EnumServicesGroupW' sieht die zurueckgegebene Struktur folgendermassen aus: typedef struct _ENUM_SERVICE_STATUS { LPTSTR lpServiceName; LPTSTR lpDisplayName; SERVICE_STATUS ServiceStatus; } ENUM_SERVICE_STATUS, *LPENUM_SERVICE_STATUS; typedef struct _SERVICE_STATUS { DWORD dwServiceType; DWORD dwCurrentState; DWORD dwControlsAccepted; DWORD dwWin32ExitCode; DWORD dwServiceSpecificExitCode; DWORD dwCheckPoint; DWORD dwWaitHint; } SERVICE_STATUS, *LPSERVICE_STATUS; fuer 'EnumServicesStatusExA' und 'EnumServicesStatusExW' sieht sie so aus: typedef struct _ENUM_SERVICE_STATUS_PROCESS { LPTSTR lpServiceName; LPTSTR lpDisplayName; SERVICE_STATUS_PROCESS ServiceStatusProcess; } ENUM_SERVICE_STATUS_PROCESS, *LPENUM_SERVICE_STATUS_PROCESS; typedef struct _SERVICE_STATUS_PROCESS { DWORD dwServiceType; DWORD dwCurrentState; DWORD dwControlsAccepted; DWORD dwWin32ExitCode; DWORD dwServiceSpecificExitCode; DWORD dwCheckPoint; DWORD dwWaitHint; DWORD dwProcessId; DWORD dwServiceFlags; } SERVICE_STATUS_PROCESS, *LPSERVICE_STATUS_PROCESS; Wir interessieren uns nur fuer 'lpServiceName', was den Namen des Dienstes beinhaltet. Die Datensaetze haben alle eine fixe Groesse, weshalb wir alle folgenden Elemente um ihre Groesse verschieben koennen, wenn wir einen Eintrag verstecken moechten. An dieser Stelle muessen wir zwischen der Groesse von 'SERVICE_STATUS' und 'SERVICE_STATUS_PROCESS' unterscheiden. =====[ 7. Hooks und deren Ausbreitung ]========================================= Um den gewuenschten Effekt zu erlangen muessen wir alle laufenden Prozesse und alle neu ausgefuehrten Prozesse hooken. Neue Prozesse sollten gehookt werden bevor die erste Instruktion ihres eigenen Codes abgearbeitet wird, ansonsten besteht die Moeglichkeit, dass eine Anwendung unsere versteckten Objekte sehen kann bevor sie gehookt wird. =====[ 7.1 Rechte ]============================================================= Zuallererst ist es gut zu wissen, dass wir Administrator-Rechte brauchen, um Zugriff auf alle laufenden Prozesse zu bekommen. Die beste Moeglichkeit ist unseren Prozess als Windoze-Dienst unter dem Kontext von 'SYSTEM/NT AUTHORITY' laufen zu lassen. Um den Dienst installieren zu koennen brauchen wir zusaetzlich spezielle Rechte. Wir muessen auch das SeDebug-Privileg bekommen, da wir dieses spaeter noch brauchen werden. Das koennen wir mit den API-Funktionen 'OpenProcessToken', 'LookupPrivilegeValue' und 'AdjustTokenPrivileges' erreichen. BOOL OpenProcessToken( HANDLE ProcessHandle, DWORD DesiredAccess, PHANDLE TokenHandle ); BOOL LookupPrivilegeValue( LPCTSTR lpSystemName, LPCTSTR lpName, PLUID lpLuid ); BOOL AdjustTokenPrivileges( HANDLE TokenHandle, BOOL DisableAllPrivileges, PTOKEN_PRIVILEGES NewState, DWORD BufferLength, PTOKEN_PRIVILEGES PreviousState, PDWORD ReturnLength ); Ohne Exceptions zu beruecksichtigen koennte ein Beispiel-Code so aussehen: #define SE_PRIVILEGE_ENABLED 0x0002 #define TOKEN_QUERY 0x0008 #define TOKEN_ADJUST_PRIVILEGES 0x0020 HANDLE hToken; LUID DebugNameValue; TOKEN_PRIVILEGES Privileges; DWORD dwRet; OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,hToken); LookupPrivilegeValue(NULL, "SeDebugPrivilege", &DebugNameValue); Privileges.PrivilegeCount = 1; Privileges.Privileges[0].Luid = DebugNameValue; Privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &Privileges, sizeof(Privileges), NULL, &dwRet); CloseHandle(hToken); =====[ 7.2 Systemweite Hooks ]================================================== Die Auflistung von Prozessen wird von der bereits erwaehnten API-Funktion 'NtQuerySystemInformation' erledigt. Es gibt ein paar wenige native Prozesse in dem System, weshalb wir die Methode des Ueberschreibens der ersten Instruktionen einer Funktion verwenden, um diese zu hooken. Bei jedem laufenden Prozess gehen wir gleich vor. Wir reservieren einen Teil des Speichers im Zielprozess, wo wir dann unseren neuen Code fuer Funktionen, welche wir hooken wollen, speichern. Dann ueberschreiben wir die ersten paar Bytes dieser Funktionen mit relativen JMP-Befehlen. Diese Spruenge leiten die Ausfuehrung auf unseren Code um, weshalb unser Code sofort ausgefuehrt wird, sobald die gehookte Funktion aufgerufen wird. Wir speichern die ersten Instruktionen jeder Funktion, welche wir geaendert haben, da wir diese noch brauchen, um den Orginal-Code der gehookten Funktion ausfuehren zu koennen. Das Speichern von Instruktionen ist im Kapitel 3.2.3 meines Dokumentes "Hooking Winodze API" beschrieben. Zuerst muessen wir den gewuenschten Prozess mittels 'NtOpenProcess' oeffnen, um das Handle auf diesen zu bekommen. Das schlaegt fehl, wenn wir nicht genuegend Rechte haben. NTSTATUS NtOpenProcess( OUT PHANDLE ProcessHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId OPTIONAL ); 'ProcessHandle' ist ein Zeiger auf das Handle, welches den Rueckgabewert speichert. 'DesiredAccess' sollte auf 'PROCESS_ALL_ACCESS' gesetzt sein. Wir setzen die PID des Zielprozesses fuer den Parameter 'ClientId' ein und setzen 'UniqueThread' auf 0. Das geoeffnete Handle kann mit 'NtClose' wieder geschlossen werden. #define PROCESS_ALL_ACCESS 0x001F0FFF Jetzt reservieren wir Speicher fuer unseren Code. Das koennen wir mit der Funktion 'NtAllocateVirtualMemory' bewerkstelligen. NTSTATUS NtAllocateVirtualMemory( IN HANDLE ProcessHandle, IN OUT PVOID BaseAddress, IN ULONG ZeroBits, IN OUT PULONG AllocationSize, IN ULONG AllocationType, IN ULONG Protect ); 'ProcessHandle' ist das Handle von 'NtOpenProcess'. 'BaseAddress' ist ein Zeiger auf einen Zeiger, welcher an den Anfang des reservierten Speichers zeigt. Hier wird die Adresse des reservierten Speichers gespeichert. Der Uebergabewert kann NULL sein. 'AllocationSize' ist ein Pointer auf die Anzahl der Bytes, welche wir reservieren wollen. Und wiederum wird es auch als Rueckgabewert fuer die wirkliche Anzahl an allozierten Bytes verwendet. Es ist eine gute Idee 'AllocationType' zusaetzlich zu 'MEM_COMMIT' auf 'MEM_TOP_DOWN' zu setzen, weil der Speicher dann an der hoechst moeglichen Adresse, nahe den DLLs, reserviert wird. #define MEM_COMMIT 0x00001000 #define MEM_TOP_DOWN 0x00100000 Dann koennen wir unseren Code mit 'NtWriteVirtualMemory' dorthin schreiben: NTSTATUS NtWriteVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN PVOID Buffer, IN ULONG BufferLength, OUT PULONG ReturnLength OPTIONAL ); Der Parameter 'BaseAddress' beinhaltet die Adresse, welche von 'NtAllocateVirtualMemory' zurueckgegeben wird. 'Buffer' zeigt auf den Speicher, den wir beschreiben wollen und 'BufferLength' gibt die Anzahl der Bytes an, die es zu schreiben gilt. Jetzt hooken wir einzelne Funktionen. Die einzige DLL, welche in alle Prozesse geladen wird, ist `ntdll.dll'. Deshalb muessen wir, falls die Funktion nicht aus `ntdll.dll' stammt, ueberpruefen, ob die Funktion, welche wir hooken wollen, importiert wurde. Da der Speicher der Funktion bereits in Verwendung ist koennte es beim Ueberschreiben der ersten Bytes der Funktion passieren, dass wir eine Exception verursachen. Das ist der Grund weshalb wir ueberpruefen muessen ob die DLL, welche die zu hookende Funktion beinhaltet, vom Zielprozess schon geladen wurde. Wir muessen den PEB (Process Environment Block) des Zielprozesses mittels 'NtQueryInformationProcess' ermitteln. NTSTATUS NtQueryInformationProcess( IN HANDLE ProcessHandle, IN PROCESSINFOCLASS ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength OPTIONAL ); Wir setzen den Wert von 'ProcessInformationClass' auf den Wert 'ProcessBasicInformation'. Dann wird die 'PROCESS_BASIC_INFORMATION' Struktur in den 'ProcessInformation' Puffer geschriebenen, dessen Groesse uns durch 'ProcessInformationLength' mitgeteilt wird. #define ProcessBasicInformation 0 typedef struct _PROCESS_BASIC_INFORMATION { NTSTATUS ExitStatus; PPEB PebBaseAddress; KAFFINITY AffinityMask; KPRIORITY BasePriority; ULONG UniqueProcessId; ULONG InheritedFromUniqueProcessId; } PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION; 'PebBaseAddress' ist fuer uns interessant. Bei 'PebBaseAddress' + 0x0C befindet sich die Adresse von 'PPEB_LDR_DATA'. Das ganze koennen wir bestimmen, indem wir 'NtReadVirtualMemory' aufrufen. NTSTATUS NtReadVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, OUT PVOID Buffer, IN ULONG BufferLength, OUT PULONG ReturnLength OPTIONAL ); Die Parameter sind aehnlich wie bei 'NtWriteVirtualMemory'. Bei 'PPEB_LDR_DATA' + 0x1C befindet sich die Adresse fuer 'InInitializationOrderModuleList'. Dies ist eine Liste von Bibliotheken (DLLs), welche von dem Prozess geladen wurden. Uns interessiert nur ein Teil dieser Struktur: typedef struct _IN_INITIALIZATION_ORDER_MODULE_LIST { PVOID Next, PVOID Prev, DWORD ImageBase, DWORD ImageEntry, DWORD ImageSize, ... ); 'Next' ist ein Zeiger auf den naechsten Datensatz und 'Prev' auf den vorigen, wobei der letzte Datensatz wieder auf den ersten zeigt. 'ImageBase' beinhaltet die Adresse des Modules im Speicher, 'ImageEntry' ist der EntryPoint des Modules und 'ImageSize' enthaelt die Groesse. Wir werden fuer alle DLLs, in welchen wir eine Funktion hooken muessen, deren ImageBase ermitteln (zum Beispiel durch die Verwendung von 'GetModuleHandle' oder 'LoadLibrary'). Die ermittelte ImageBase muessen wir mit den Werten 'ImageBase' jedes Eintrages der 'InInitializationOrderModuleList' Struktur vergleichen. Jetzt sind wir bereit fuer das Hooken. Da wir laufende Prozesse hooken besteht die Moeglichkeit, dass der Code, den wir neu schreiben wollen, zum Zeitpunkt des Schreibens ausgefuehrt wird. Da dies zu unvorhersehbaren Fehlern fuehren kann werden wir zuallerst alle Threads des zu hookenden Threads anhalten. Die Threads eines Prozesses koennen mittels 'NtQuerySystemInformation' ermittelt werden, wobei der Parameter 'SystemInformationClass' auf die Konstante 'SystemProcessesAndThreadsInformation' gesetzt wird. Der Rueckgabewert dieser Funktion ist in Kapitel 4 beschrieben worden. Wir muessen allerdings noch die Beschreibung der Struktur 'SYSTEM_THREADS' hinzufuegen, da dort die Informationen ueber die Threads gespeichert werden. typedef struct _SYSTEM_THREADS { LARGE_INTEGER KernelTime; LARGE_INTEGER UserTime; LARGE_INTEGER CreateTime; ULONG WaitTime; PVOID StartAddress; CLIENT_ID ClientId; KPRIORITY Priority; KPRIORITY BasePriority; ULONG ContextSwitchCount; THREAD_STATE State; KWAIT_REASON WaitReason; } SYSTEM_THREADS, *PSYSTEM_THREADS; Wir muessen fuer jeden Thread das Handle bekommen, indem wir 'NtOpenThread' verwenden. Dafuer verwenden wir 'ClientId'. NTSTATUS NtOpenThread( OUT PHANDLE ThreadHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId ); Das Handle, welches wir bekommen wollen, wird in 'ThreadHandle' gespeichert. Wir setzen 'DesiredAccess' auf 'THREAD_SUSPEND_RESUME'. #define THREAD_SUSPEND_RESUME 2 'ThreadHandle' wird dann verwendet, um 'NtSuspendThread' aufzurufen. NTSTATUS NtSuspendThread( IN HANDLE ThreadHandle, OUT PULONG PreviousSuspendCount OPTIONAL ); Die Funktion eines angehaltenen Prozesses kann dann schliesslich problemlos gehookt werden. Wir fahren hierbei fort wie es im Kapitel 3.2.2 des "Hooking Windows API" Papers beschrieben wird. Der einzige Unterschied ist, dass wir Funktionen fuer die anderen Prozesse verwenden werden. Nach dem Hooken starten wir wieder alle Threads mit Hilfe der Funktion 'NtResumeThread'. NTSTATUS NtResumeThread( IN HANDLE ThreadHandle, OUT PULONG PreviousSuspendCount OPTIONAL ); =====[ 7.3 Neu erzeugte Prozesse ]============================================== Das Hooken aller laufenden Prozesse beeinflusst Prozesse die spaeter gestartet werden nicht. Wir koennten uns eine Liste aller Prozesse beschaffen und dann kurze Zeit spaeter immer wieder eine neue Liste, welche wir mit der alten vergleichen, allerdings ist diese Methode alles andere als empfehlenswert. Eine viel bessere Idee ist es eine Funktion zu hooken, welche immer aufgerufen wird, sobald ein neuer Prozess erzeugt wird. Da wir alle laufenden Prozesse des Systems hooken kann es mit dieser Methode nicht passieren, dass ein Prozess ausgelassen wird. Wir koennten 'NtCreateThread' hooken, allerdings ist das nicht die einfachste Moeglichkeit. Wir werden 'NtResumeThread' hooken, da diese Funktion auch immer aufgerufen wird, sobald ein Prozess erzeugt wird, nur mit dem Unterschied, dass 'NtCreateThread' immer vorher aufgerufen wird. Das einzige Problem bei 'NtResumeThread' ist, dass es nicht nur aufgrufen wird, um neue Prozesse zu erzeugen. Allerdings laesst sich das Problem leicht loesen, da 'NtQueryInformationThread' Informationen ueber den Besitzer eines Threads zurueckgibt. Das letzte was wir dann noch tun muessen ist zu ueberpruefen, ob dieser Prozess bereits gehookt wurde oder nicht. Das kann realisiert werden, indem wir die ersten Bytes aller Funktionen, welche wir hooken wollen, lesen. NTSTATUS NtQueryInformationThread( IN HANDLE ThreadHandle, IN THREADINFOCLASS ThreadInformationClass, OUT PVOID ThreadInformation, IN ULONG ThreadInformationLength, OUT PULONG ReturnLength OPTIONAL ); Der Parameter 'ThreadInformationClass' sollte in unserem Fall auf 'ThreadBasicInformation' gesetzt werden. 'ThreadInformation' ist der Puffer fuer die zurueckgegebenen Daten und 'ThreadInformationLength' speichert dessen Groesse. #define ThreadBasicInformation 0 Bei 'ThreadBasicInformation' wird folgendene Struktur zurueckgegeben: typedef struct _THREAD_BASIC_INFORMATION { NTSTATUS ExitStatus; PNT_TIB TebBaseAddress; CLIENT_ID ClientId; KAFFINITY AffinityMask; KPRIORITY Priority; KPRIORITY BasePriority; } THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION; In 'ClientId' wird die PID des Prozesses gespeichert, zu welchem der Thread gehoert. Jetzt muessen wir noch die Funktionen des neuen Prozesses hooken. Das Problem dabei ist, dass der neue Prozess zu diesem Zeitpunkt nur `ntdll.dll' geladen hat. Alle anderen DLLs werden erst nach dem Aufruf von 'NtResumeThread' geladen. Es gibt mehrere Moeglichkeiten dieses Problem zu loesen - zum Beispiel koennen wir eine API-Funktion namens 'LdrInitializeThunk' hooken, welche waehrend der Initialisierung eines Prozesses aufgerufen wird. NTSTATUS LdrInitializeThunk( DWORD Unknown1, DWORD Unknown2, DWORD Unknown3 ); Zuerst fuehren wir den Orginal-Code aus und dann hooken wir alle gewuenschten Funktionen in dem neuen Prozess. Spaeter ist es besser 'LdrInitializeThunk' wieder zu unhooken, weil es im weiteren Verlauf noch sehr oft aufgerufen wird und wir wollen nicht immer alle Funktionen neu hooken lassen. Alles was hier gemacht wird geschieht vor der Ausfuehrung der ersten Instruktion der neuen Anwendung. Deswegen hat die Anwendung keine Moeglichkeit eine Funktion aufzurufen bevor wir sie hooken. Das Hooken selbst geschieht auf die gleiche Weise wie bei laufenden Prozessen nur mit dem Unterschied, dass wir uns hier nicht um laufende Threads kuemmern muessen. =====[ 7.4 DLLs ]=============================================================== In jedem Prozess des Systemes befindet sich einen Kopie von `ntdll.dll'. Das bedeutet, dass wir alle Funktionen von dieser DLL einfach beim Initialisieren eines Prozesses hooken koennen. Aber was machen wir mit Funktionen von anderen DLLs wie `kernel32.dll' oder `advapi32.dll'? Dazu kommt, dass es einige Prozesse gibt, die nur `ntdll.dll' geladen haben. Alle anderen DLLs koennen dynamisch waehrend der Ausfuehrung einer Anwendung geladen werden. Das ist der Grund weshalb wir auch die Funktion 'LdrLoadDll' hooken muessen, die neue Module laedt. NTSTATUS LdrLoadDll( PWSTR szcwPath, PDWORD pdwLdrErr, PUNICODE_STRING pUniModuleName, PHINSTANCE pResultInstance ); 'pUniModuleName' ist fuer uns hier das wichtigste, da hier der Name des Modules gespeichert wird. Wenn die Methode erfolgreich war, wird in 'pResultInstance' dessen Adresse gespeichert. Wir werden dann die orginale 'LdrLoadDll'-Funktion aufrufen und alle Funktionen in diesem Modul hooken. =====[ 8. Speicher ]============================================================ Wir hooken eine Funktion, indem wir die ersten Bytes von dieser veraendern. Durch einen Aufruf von 'NtReadVirtualMemory' kann man entdecken, ob eine Funktion gehookt wurde. Damit unsere Hooks nicht entdeckt werden, muessen wir deshalb wiederum 'NtReadVirtualMemory' hooken. NTSTATUS NtReadVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, OUT PVOID Buffer, IN ULONG BufferLength, OUT PULONG ReturnLength OPTIONAL ); Wir haben die ersten Bytes einer Funktion, welche wir gehookt haben, veraendert und wir haben Speicher fuer unseren neuen Code alloziert. Wir muessen ueberpruefen, ob die Methode diese ersten Bytes lesen moechte. Sofern sich unsere Bytes im Bereich von 'BaseAddress' und 'BaseAddress' + 'BufferLength' befinden muessen wir die Bytes des Puffers veraendern. Falls unser reservierter Speicher gelesen werden soll, muessen wir einen leeren Puffer und den Fehler 'STATUS_PARTIAL_COPY' zurueckgeben. Dieser Fehler bedeutet, dass nicht alle angeforderten Bytes in den Puffer kopiert wurden. Es wird auch verwendet, wenn nicht allozierter Speicher gelesen werden moechte. In diesem Fall sollte 'ReturnLength' auf 0 gesetzt werden. #define STATUS_PARTIAL_COPY 0x8000000D Wenn die ersten Bytes unserer gehookten Funktion gelesen werden sollen dann muessen wir den Orginal-Code ausfuehren und dann die originalen Bytes in den Puffer kopieren. Eine Anwendung ist dann nicht mehr in der Lage einen Hook zu entdecken, indem der Speicher einer Funktion gelesen wird. Wenn ein gehookter Prozess-Debugger debuggt wird dann haben wir ein Problem, da er zwar den Orginal-Code zeigt, allerdings unseren Code ausfuehrt. Um uns perfekt verbergen zu koennen muessen wir zusaetzlich die Funktion 'NtQueryVirtualMemory' hooken. Diese Funktion wird verwendet, um Informationen ueber den virtuellen Speicher zu bekommen. Wir koennen sie hooken, um zu verhindern, dass unser allozierter Speicher gefunden wird. NTSTATUS NtQueryVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN MEMORY_INFORMATION_CLASS MemoryInformationClass, OUT PVOID MemoryInformation, IN ULONG MemoryInformationLength, OUT PULONG ReturnLength OPTIONAL ); 'MemoryInformationClass' gibt den Typ des Rueckgabewertes an. Die ersten zwei Typen sind interessant fuer uns: #define MemoryBasicInformation 0 #define MemoryWorkingSetList 1 Bei 'MemoryBasicInformation' wird dieser Struktur zurueckgegeben: typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; PVOID AllocationBase; ULONG AllocationProtect; ULONG RegionSize; ULONG State; ULONG Protect; ULONG Type; } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; 'RegionSize' speichert die Groesse der Speicherregion und 'Type' deren Typ. Freier Speicher hat den Typ 'MEM_FREE'. #define MEM_FREE 0x10000 Wenn eine Sektion, welche vor unserer liegt, den Typ 'MEM_FREE' hat sollten wir die Groesse unserer Sektion zu ihrer 'RegionSize' dazu zaehlen. Falls die Sektion, welche hinter unserer Sektion liegt, auch den Typ 'MEM_FREE' hat, sollten wir die Groesse der Sektion wiederum zu 'RegionSize' dazu addieren. Wenn eine Sektion, welche sich vor unserer Sektion befindet, einen anderen Typ hat geben wir 'MEM_FREE' fuer unsere Sektion zurueck. Die Groesse der Sektion wird wiederum nach den Eigenschaften der darauf folgenden Sektion berechnet. Bei der Angabe der Klasse 'MemoryWorkingSetList' wird folgende Struktur zurueckgegeben: typedef struct _MEMORY_WORKING_SET_LIST { ULONG NumberOfPages; ULONG WorkingSetList[1]; } MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST; 'NumberOfPages' ist die Anzahl von Elementen in 'WorkingSetList'. Diese Anzahl sollte verkleinert werden. Wir sollten unsere Sektion in 'WorkingSetList' finden und verstecken, indem wir die darauf folgenden Sektionen ueber unsere "verschieben". 'WorkingSetList' ist ein Array von DWORDs, von welchem die hoeheren 20 Bits die Adresse der Sektion und die unteren 12 Bits die Flags repraesentieren. =====[ 9. Handle ]============================================================== Der Aufruf von 'NtQuerySystemInformation' mit dem Typ 'SystemHandleInformation' gibt uns alle offene Handles der Struktur '_SYSTEM_HANDLE_INFORMATION_EX' zurueck. #define SystemHandleInformation 0x10 typedef struct _SYSTEM_HANDLE_INFORMATION { ULONG ProcessId; UCHAR ObjectTypeNumber; UCHAR Flags; USHORT Handle; PVOID Object; ACCESS_MASK GrantedAccess; } SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION; typedef struct _SYSTEM_HANDLE_INFORMATION_EX { ULONG NumberOfHandles; SYSTEM_HANDLE_INFORMATION Information[1]; } SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX; 'ProcessId' gibt den Prozess an, welchem das Handle gehoert. 'ObjectTypeNumber' ist der Typ des Handles. 'NumberOfHandles' ist die Anzahl der Elemente in der Struktur. Ein Element zu verstecken ist trivial, da wir einfach alle folgenden Eintraege um eins nach oben schieben und den Wert in 'NumberOfHandles' um eins verkleinern muessen. Alle folgenden Handles zu verschieben ist noetig, weil die Handles nach 'ProcessId' gruppiert gespeichert werden, was bedeutet, dass alle Handles eines Prozesses beieinander liegen. Jetzt denken wir nochmal an die '_SYSTEM_PROCESSES' Struktur, welche von dieser Funktion zurueckgegeben wird, wenn als Typ 'SystemProcessesAndThreadsInformation' uebergeben wird. Hier sehen wir dann, dass fuer jeden Prozess die Anzahl der Handles in 'HandleCount' gespeichert wird. Wenn wir das Verstecken von Handles perfekt implementieren wollen dann muessen wir 'HandleCount' entsprechend der Anzahl von Handles, die wir verstecken wollen, anpassen, sobald wir diese Funktion mit dem Parameter 'SystemProcessesAndThreadsInformation' aufrufen. Aber dies waere sehr zeitaufwaendig, weil es sehr viele Handles gibt, welche in kuerzester Zeit geoeffnet und geschlossen werden. Es kann auch passieren, dass sich die Anzahl der Handles zwischen zwei Calls dieser Methode aendert und wir deswegen 'HandleCount' nicht veraendern muessen. =====[ 9.1 Bestimmung von Handles und dessen Typ ]============================== Das Verstecken von Handles ist trivial, allerdings ist es schwieriger herauszufinden, welche Handles man verbergen muss. Wenn wir einen Prozess verstecken dann sollten wir auch alle seine Handles und die Handles, welche mit diesem etwas zu tun haben, verstecken. Das Verstecken von allen Handles eines Prozesses ist wiederum einfach, da wir einfach 'ProcessId' des Handles mit der PID unserer Prozesses vergleichen und falls diese gleich sind verstecken muessen. Handles von anderen Prozessen muessen allerdings erst bestimmt werden, was nicht ganz unproblematisch ist, da die Anzahl aller Handles des Systems gewoehnlich sehr gross ist. Das beste was wir tun koennen ist es zuerst den Typ der Handles zu vergleichen und erst danach zu versuchen, die fuer uns wichtigen Handles zu bestimmen, da wir dadurch die Anzahl der zu bestimmenden Handles stark reduzieren. Die Bestimmung eines Handles und dessen Typ kann mittels 'NtQueryObject' bewerkstelligt werden: NTSTATUS ZwQueryObject( IN HANDLE ObjectHandle, IN OBJECT_INFORMATION_CLASS ObjectInformationClass, OUT PVOID ObjectInformation, IN ULONG ObjectInformationLength, OUT PULONG ReturnLength OPTIONAL ); 'ObjectHandle' ist das Handle ueber welches wir Informationen bekommen wollen. 'ObjectInformationClass' bestimmt die Art von Struktur, welche in den Ausgabepuffer 'ObjectInformation' geschrieben wird und 'ObjectInformationLength' speichert wie ueblich die Laenge von diesem. Wir werden die Typen 'ObjectNameInformation' und 'ObjectAllTypesInformation' verwenden. Die Verwendung der Konstante 'ObjectNameInformation' fuehrt dazu, dass die 'OBJECT_NAME_INFORMATION' Struktur in den Puffer geschrieben und bei 'ObjectAllTypesInformation' wird die Struktur 'OBJECT_ALL_TYPES_INFORMATION' geschrieben wird. #define ObjectNameInformation 1 #define ObjectAllTypesInformation 3 typedef struct _OBJECT_NAME_INFORMATION { UNICODE_STRING Name; } OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION; 'Name' bestimmt den Namen des Handles. typedef struct _OBJECT_TYPE_INFORMATION { UNICODE_STRING Name; ULONG ObjectCount; ULONG HandleCount; ULONG Reserved1[4]; ULONG PeakObjectCount; ULONG PeakHandleCount; ULONG Reserved2[4]; ULONG InvalidAttributes; GENERIC_MAPPING GenericMapping; ULONG ValidAccess; UCHAR Unknown; BOOLEAN MaintainHandleDatabase; POOL_TYPE PoolType; ULONG PagedPoolUsage; ULONG NonPagedPoolUsage; } OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION; typedef struct _OBJECT_ALL_TYPES_INFORMATION { ULONG NumberOfTypes; OBJECT_TYPE_INFORMATION TypeInformation; } OBJECT_ALL_TYPES_INFORMATION, *POBJECT_ALL_TYPES_INFORMATION; Zuerst kommt eine 'OBJECT_TYPE_INFORMATION' Struktur gefolgt von dem Namen des Objekttypes, welcher durch 'Name' bestimmt wird, und einem Padding, um auf die naechste DWORD-Grenze zu kommen. Daran schliesst dann die naechste Struktur an und so weiter. 'ObjectTypeNumber' der 'SYSTEM_HANDLE_INFORMATION' Struktur ist ein Index auf das 'TypeInformation' Array. Bei einem anderen Prozess ist es schwieriger den Namen des Handles zu bestimmen. Es gibt zwei Moeglichkeiten diese zu bestimmen: Einmal ist es moeglich das Handle mittels 'NtDuplicateObject' in unseren Prozess zu kopieren und ihn dann zu bestimmen, was allerdings nicht bei allen Typen von Handles moeglich ist. Da dies aber nur bei wenigen Typen der Fall ist koennen wir dies ignorieren und diese Methode nutzen. NtDuplicateObject( IN HANDLE SourceProcessHandle, IN HANDLE SourceHandle, IN HANDLE TargetProcessHandle, OUT PHANDLE TargetHandle OPTIONAL, IN ACCESS_MASK DesiredAccess, IN ULONG Attributes, IN ULONG Options ); 'SourceProcessHandle' ist das Handle auf den Prozess, welchem 'SourceHandle' gehoert. 'SourceHandle' ist das Handle, welches wir kopieren wollen. 'TargetProcessHandle' ist wie der Name schon vermuten laesst ein Handle auf einen Prozess, in welchen wir das Handle kopieren wollen. 'TargetHandle' stellt einen Zeiger auf ein Handle dar und gibt den Speicherort des Handles an. 'DesiredAccess' sollte auf 'PROCESS_QUERY_INFORMATION' gesetzt und 'Attributes' genauso wie 'Options' auf 0 gesetzt werden. Die zweite Methode ein Handle zu bestimmen, welche bei allen Handles funktioniert, ist es einen Kernel-Treiber zu verwenden. Das OpHandle Projekt, welches auf meiner Seite zu finden ist, beinhaltet die Sourcen fuer einen solchen Treiber. =====[ 10. Ports ]============================================================== Der einfachste Weg offene Ports aufzulisten ist es die Funktionen 'AllocateAndGetTcpTableFromStack' und 'AllocateAndGetUdpTableFromStack' und / oder 'AllocateAndGetTcpExTableFromStack' und 'AllocateAndGetUdpExTableFromStack' von `iphlpapi.dll' zu verwenden. Die Ex-Funktionen gibt es seit Windoze XP. typedef struct _MIB_TCPROW { DWORD dwState; DWORD dwLocalAddr; DWORD dwLocalPort; DWORD dwRemoteAddr; DWORD dwRemotePort; } MIB_TCPROW, *PMIB_TCPROW; typedef struct _MIB_TCPTABLE { DWORD dwNumEntries; MIB_TCPROW table[ANY_SIZE]; } MIB_TCPTABLE, *PMIB_TCPTABLE; typedef struct _MIB_UDPROW { DWORD dwLocalAddr; DWORD dwLocalPort; } MIB_UDPROW, *PMIB_UDPROW; typedef struct _MIB_UDPTABLE { DWORD dwNumEntries; MIB_UDPROW table[ANY_SIZE]; } MIB_UDPTABLE, *PMIB_UDPTABLE; typedef struct _MIB_TCPROW_EX { DWORD dwState; DWORD dwLocalAddr; DWORD dwLocalPort; DWORD dwRemoteAddr; DWORD dwRemotePort; DWORD dwProcessId; } MIB_TCPROW_EX, *PMIB_TCPROW_EX; typedef struct _MIB_TCPTABLE_EX { DWORD dwNumEntries; MIB_TCPROW_EX table[ANY_SIZE]; } MIB_TCPTABLE_EX, *PMIB_TCPTABLE_EX; typedef struct _MIB_UDPROW_EX { DWORD dwLocalAddr; DWORD dwLocalPort; DWORD dwProcessId; } MIB_UDPROW_EX, *PMIB_UDPROW_EX; typedef struct _MIB_UDPTABLE_EX { DWORD dwNumEntries; MIB_UDPROW_EX table[ANY_SIZE]; } MIB_UDPTABLE_EX, *PMIB_UDPTABLE_EX; DWORD WINAPI AllocateAndGetTcpTableFromStack( OUT PMIB_TCPTABLE *pTcpTable, IN BOOL bOrder, IN HANDLE hAllocHeap, IN DWORD dwAllocFlags, IN DWORD dwProtocolVersion; ); DWORD WINAPI AllocateAndGetUdpTableFromStack( OUT PMIB_UDPTABLE *pUdpTable, IN BOOL bOrder, IN HANDLE hAllocHeap, IN DWORD dwAllocFlags, IN DWORD dwProtocolVersion; ); DWORD WINAPI AllocateAndGetTcpExTableFromStack( OUT PMIB_TCPTABLE_EX *pTcpTableEx, IN BOOL bOrder, IN HANDLE hAllocHeap, IN DWORD dwAllocFlags, IN DWORD dwProtocolVersion; ); DWORD WINAPI AllocateAndGetUdpExTableFromStack( OUT PMIB_UDPTABLE_EX *pUdpTableEx, IN BOOL bOrder, IN HANDLE hAllocHeap, IN DWORD dwAllocFlags, IN DWORD dwProtocolVersion; ); Es gibt allerdings auch eine andere Moeglichkeit offene Ports aufzulisten. Wenn ein Programm einen Socket erstellt und anfaengt auf diesem zu horchen hat dieses ein offenes Handle fuer den Socket und den Port. Wir koennen alle offenen Handles des Systems auflisten und diese 'NtDeviceIoControlFile' uebergeben, um herauszufinden, ob das Handle fuer einen offenen Port steht oder nicht. Weil es eine grosse Menge an offenen Handles gibt werden wir nur die Handles testen, welche als Typ 'File' haben und deren Namen '\Device\Tcp' oder '\Device\Udp' ist. Offene Ports haben ausschliesslich diesen Typ und Namen. Wenn wir uns den Code der Funktionen in `iphlpapi.dll' ansehen dann werden wir feststellen, dass diese Funktionen auch 'NtDeviceIoControlFile' aufrufen und einen speziellen Puffer, welcher alle offene Handles beinhaltet, uebergeben, um an eine Liste der offenen Ports zu kommen. Das bedeutet, dass wir nur die Funktion 'NtDeviceIoControlFile' hooken muessen, um offene Ports verstecken zu koennen. NTSTATUS NtDeviceIoControlFile( IN HANDLE FileHandle IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, IN ULONG IoControlCode, IN PVOID InputBuffer OPTIONAL, IN ULONG InputBufferLength, OUT PVOID OutputBuffer OPTIONAL, IN ULONG OutputBufferLength ); Interessante Argumente sind fuer uns nun 'FileHandle', was ein Handle auf ein Geraet (Device) angibt mit welchem kommuniziert werden soll, 'IoStatusBlock', das auf eine Variable zeigt, welche den Status und weitere Informationen zu der durchgefuehrten Operation enthaelt, 'IoControlCode' was eine Zahl ist, welche dem Typ eines Geraetes (Device), der Methode, der Zugriffsart und der Funktion, entspricht. 'InputBuffer' beinhaltet schliesslich die uebergebenen Daten, dessen Groesse in 'InputBufferLength' gespeichert werden. Dasselbe gilt fuer 'OutputBuffer' und 'OutputbufferLength' nur, dass es sich hierbei um den zurueckgebenen Puffer und dessen Laenge handelt. =====[ 10.1 Netstat, OpPorts unter WinXP, FPort unter WinXP ]=================== OpPorts und FPort beschaffen sich unter Windoze XP neben Netstat zuerst eine Liste aller offenen Ports. Die Anwendungen rufen hierbei zweimal 'NtDeviceIoControlFile' auf, wobei 'IoControlCode' auf 0x000120003 gesetzt wird. Der Ausgabepuffer wird erst beim zweiten Aufruf gefuellt. Der Name des File-Handles ist hierbei immer '\Device\Tcp' und der Eingabepuffer unterscheidet sich bei den verschiedenen Arten von Methoden-Aufrufen. 1) Die Calls um an ein Array eines Eingabepuffers des Typs 'MIB_TCPROW' zu kommen sehen folgendermassen aus: Erster Aufruf: 0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 Zweiter Aufruf: 0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 2) Um ein Array eines 'MIB_UDPROW' Puffers zu kommen: Erster Aufruf: 0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 Zweiter Aufruf: 0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 3) Um ein Array eines 'MIB_TCPROW_EX' Puffers zu kommen: Erster Aufruf: 0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 Zweiter Aufruf: 0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 4) Um ein Array eines 'MIB_UDPROW_EX' Puffers zu kommen: Erster Aufruf: 0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 Zweiter Aufruf: 0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 Wie man sehen kann unterscheiden sich die Puffer nur bei wenigen Bytes. Wir koennen dies nun verstaendlich rekapitulieren: Funktionsaufrufe, welche uns interessieren, haben 'InputBuffer[1]' auf 0x04 und 'InputBuffer[17]' in erster Linie auf 0x01 gesetzt. Nur mit diesen Werten wird der Ausgabepuffer mit wuenschenswerten Tabellen gefuellt. Wenn wir Information zu TCP-Ports bekommen moechten dann sollten wir 'InputBuffer[0]' auf 0x00 setzen oder auf 0x01 fuer Informationen zu UDP. Wenn wir erweiterte Ausgabetabellen (MIB_TCPROW_EX oder MIB_UDPROW_EX) haben moechten dann muessen wir bei 'Inputbuffer[16]' beim zweiten Aufruf auf 0x02 setzen. Wenn wir den Methodenaufruf mit diesen Parametern finden, dann koennen wir den Ausgabepuffer veraendern. Um die Anzahl der Zeilen zu bekommen kann man einfach den Wert von 'IoStatusBlock' durch die Groesse der Zeile dividieren. Das Verstecken eines Elementes gestaltet sich dann einfach, da wir es einfach mit den folgenden Elementen ueberschreiben und die letzten Elemente loeschen muessen. Man sollte auch nicht vergessen 'OutputBufferLength' und 'IoStatusBlock' anzupassen. =====[ 10.2 OpPorts unter Win2k und NT4, FPort unter Win2k ]==================== Wir verwenden 'NtDeviceIoControlFile' mit 'IoControlCode' auf 0x00210012 gesetzt, um zu bestimmen, ob das Handle mit Typ 'File' und dem Namen '\Device\Tcp' oder '\Device\Udp' das Handle eines offenen Ports ist. Als erster vergleichen wir 'IoControlCode' und dann den Typ und den Namen des Handles. Wenn alles ueberein gestimmt hat dann ueberpruefen wir die Laenge des Eingabepuffers, welche mit der Laenge des 'TDI_CONNECTION_IN' Structs uebereinstimmen sollte. Die Laenge betraegt 0x18. Der Ausgabepuffer ist 'TDI_CONNECTION_OUT'. typedef struct _TDI_CONNECTION_IN { ULONG UserDataLength, PVOID UserData, ULONG OptionsLength, PVOID Options, ULONG RemoteAddressLength, PVOID RemoteAddress } TDI_CONNECTION_IN, *PTDI_CONNECTION_IN; typedef struct _TDI_CONNECTION_OUT { ULONG State, ULONG Event, ULONG TransmittedTsdus, ULONG ReceivedTsdus, ULONG TransmissionErrors, ULONG ReceiveErrors, LARGE_INTEGER Throughput LARGE_INTEGER Delay, ULONG SendBufferSize, ULONG ReceiveBufferSize, ULONG Unreliable, ULONG Unknown1[5], USHORT Unknown2 } TDI_CONNECTION_OUT, *PTDI_CONNECTION_OUT; Eine Implementierung wie man bestimmt, ob ein Handle ein offener Port ist, ist auf meiner Seite als Projekt 'OpPorts' verfuegbar. Uns interessiert nun das Verstecken eines bestimmten Ports. Da wir 'InputBufferLength' und 'IoControlCode' bereits verglichen haben, muessen wir nun 'RemoteAddressLength' ueberpruefen. Das Ergebnis ist immer 3 oder 4 fuer einen offenen Port. Das letzte, was wir noch tun muessen, ist 'ReceivedTsdus' der Ausgabepuffers zu vergleichen, was den Port in Netzwerk-Darstellung beinhaltet und eine Liste von Ports, dessen Eintraege es zu verstecken gilt. Die Unterscheidung zwischen TCP und UDP wird aufgrund des Namen des Handles gemacht. Durch das Loeschen des Ausgabepuffers, die Veraenderung von 'IoStatusBlock' und das Zurueckgeben des Wertes 'STATUS_INVALID_ADDRESS' verstecken wir den Port. =====[ 11. Abschlussbemerkung ]================================================= Konkrete Implementierungen fuer die beschriebenen Techniken werden im Source Code des Hacker Defender Rootkits in der Version 1.0.0 auf meiner Seite verfuegbar sein. Es ist moeglich, dass ich in Zukunft weitere Informationen ueber die Unsichtbarkeit unter NT-Systemen, hinzufuegen werde. Neue Versionen dieses Dokumentes koennen auch Verbesserungen der besprochenen Methoden beinhalten oder neue Kommentare dazu. Spezielle Gruesse gehen an Ratter, dem ich eine Menge an Wissen, welches fuer das Schreiben dieses Papers und die Realisierung des Hacker Defender Projektes noetig war, zu verdanken habe. Anmerkungen koennt ihr per Mail an mich senden oder postet sie in dem Forum auf meiner Seite. ====================================[ EOF ]=====================================