Dieser Artikel ist ein Gastartikel von Patrick Hahn.
In diesem Artikel geht es um Rootkits und Techniken, die sie verwenden. Speziell geht es um API-Hooking. Doch vorher: Was ist ein Rootkit? Ein Rootkit ist eine Art von Malware. Sie kommt meistens mit anderen Arten, beispielsweise Botclients, Viren, Würmern, Trojanern und sorgt dafür, dass diese unentdeckt bleibt.
Ein Beispiel
Nehmen wir an, es gäbe eine Datei namens winlogon.exe
, die sich in %AppData%
eingenistet hat, einem versteckten Verzeichnis, in dem Programme ihre Daten ablegen können. Es gibt aber auch eine Datei namens winlogon.exe
im Windows-Verzeichnis, diese Datei ist eine wichtige Systemdatei.
Wenn man nun in den Taskmanager guckt, dann ist winlogon.exe
doppelt vorhanden – das sollte die meisten User stutzig machen. Nun kann man einfach herausfinden, dass eine der beiden Dateien in %AppData%
liegt, einem ungewöhnlichen Ort für Systemdateien. Man beendet diese, löscht winlogon.exe
, entfernt noch ein paar Autostart-Einträge und das System ist wieder sauber. Solche Versuche verhindern Rootkits.
Angriffspunkte für Rootkits
Rootkits klinken sich in die Ebene zwischen den Programmen und dem Betriebssystem ein und verhindern, dass die Programme bestimmte Informationen bekommen, beispielsweise, dass ein bestimmter Prozess läuft, dass eine bestimmte Datei vorhanden ist, dass eine Datei geöffnet werden kann, und so weiter.
Die Ebene zwischen dem Betriebssystem und Programmen heißt WinAPI. Zumindest bei Windows; dieser Artikel wird sich komplett auf Windows beziehen. Die WinAPI ist in aktuellen 32/64-bit Systemen in den Dateien user32.dll
, gdi32.dll
und kernel32.dll
untergebracht. Eine weitere Ebene darunter ist die NT-API, eine undokumentierte API, die die WinAPI mit Informationen versorgt.
Der Grund für diese API ist, dass es unter Windows mehrere Subsysteme gibt: Win32, WOW64, POSIX, OS/2, und diese APIs greifen auf diese NT-API zu. Auf heutigen Installationen sind POSIX und OS/2 schon längst weggeflogen, trotzdem gibt es diese zusätzliche Ebene. Hier können sich Rootkits auch einklinken, die Funktionen liegen in der Datei ntdll.dll
.
Im Gegensatz zu den gerade behandelten Userland-Rootkits haben die Kernel-Rootkits noch eine weitere Ebene; sie können sich auch in den Kernel einklinken. Dafür haben sie entweder einen Treiber oder schreiben im Kernel-Arbeitsspeicher herum.
Kernel-Rootkits werden nicht in diesem Artikel behandelt und wurden nur der Vollständigkeit halber erwähnt.
Welche Funktionen lohnen sich zum Übeschreiben?
Funktionen, die neue Prozesse erstellen
CreateProcess
CreateProcessAsUser
CreateProcessWithLogonW
Funktionen, die auf Dateien zugreifen
CreateFile
(wird auch zum Öffnen verwendet)CreateFile2
CreateDirectory
CopyFile
DeleteFile
FindFirstFile
FindNextFile
MoveFile
OpenFile
Funktionen, die Prozesse auflisten
NtQuerySystemInformation
Process32First
Process32Next
Einfach zum Spaß
MessageBoxA
MessageBoxW
Wie überschreibe ich Funktionen?
Es gibt einige Hook-Bibliotheken, die den folgenden Code stark verkürzen und vereinfachen könnten und dabei noch stabiler wären. Beispiele dafür sind MS Detours oder mHook. Da es hier aber eher darum geht, die Funktionsweise dahinter zu verstehen, werde ich auf solche Bibliotheken verzichten und alles selber schreiben.
Ich empfehle aber trotzdem, wenn man die Techniken hier in einem echten Projekt anwenden will, auf eine Bibliothek zurückzugreifen. Als Entwicklungsumgebung verwende ich Microsoft Visual Studio 2010.
Vorraussetzungen
- C++ Kenntnisse
- Assembler-Kenntnisse (optional)
Ein kleines Testprogramm
Wer Folgendes nicht versteht, für den ist das Tutorial ungeeignet. Unser erstes Ziel ist, dass der Text und der Titel in der Konsole ausgegeben werden.
#include <Windows.h>
int main() {
system("pause");
MessageBoxA(nullptr, "Beispieltext", "Beispielmeldung", MB_OK);
system("pause");
}
Die Zielfunktion
Wir erstellen erst einmal eine Funktion namens OurMessageBoxA
, die genau die gleiche Signatur besitzt, das bedeutet: gleicher Rückgabewert (int), gleiche Aufrufkonvention (WINAPI) und die gleichen Parameter (HWND, LPCSTR, LPCSTR, UINT)
:
int WINAPI OurMessageBoxA(HWND hwnd,LPCSTR text,LPCSTR title,UINT type)
{
std::cout<<"---------------------------------\n";
std::cout<<"|"<<std::setw(30)<<title<<"|\n";
std::cout<<"---------------------------------\n";
std::cout<<"|"<<std::setw(30)<<text<<"|\n";
std::cout<<"---------------------------------\n";
return IDOK;
}
Assembler-Code
Jetzt müssen wir nur noch dafür sorgen, dass unsere neue Funktion anstatt der alten aufgerufen wird. Jetzt machen wir uns die Hände schmutzig und schreiben im RAM herum. Dabei kann dank Windows nichts Schlimmes passieren. Wir überschreiben die ersten paar Bytes von MessageBoxA
mit Maschinencode. In Assemblerform sieht er so aus:
mov eax, <32-bit-Addresse>
jmp eax
Der dazugehörige Maschinencode sieht so aus:
0xb8 0xXX 0xXX 0xXX 0xXX
0xff 0xe0
Die Bytes mit X stehen für eine 32-Bit Addresse, diese wird in 4 Teile aufgeteilt und dann ein Array geschrieben. Damit es besser zum Verstehen ist, hier der komplette Code bisher:
#include <Windows.h>
#include "functions.h"
int WINAPI OurMessageBoxA(HWND hwnd,LPCSTR text,LPCSTR title,UINT type);
void install_hook() {
DWORD our_address = reinterpret_cast<DWORD>(&OurMessageBoxA);
unsigned char funcMem[7];
//mov eax,our_address
funcMem[0] = 0xb8;
funcMem[1] = our_address & 0xFF;
funcMem[2] = (our_address >> 8) & 0xFF;
funcMem[3] = (our_address >> 16) & 0xFF;
funcMem[4] = (our_address >> 24) & 0xFF;
//jmp eax
funcMem[5]=0xff;
funcMem[6]=0xe0;
}
int WINAPI OurMessageBoxA(HWND hwnd,LPCSTR text,LPCSTR title,UINT type)
{
std::cout<<"---------------------------------\n";
std::cout<<"|"<<std::setw(30)<<title<<"|\n";
std::cout<<"---------------------------------\n";
std::cout<<"|"<<std::setw(30)<<text<<"|\n";
std::cout<<"---------------------------------\n";
return IDOK;
}
Der Hooking-Vorgang
Nun kopieren wir das Array an die Addresse von MessageBoxA
. Diese finden wir mit GetProcAddress
und GetModuleHandle
. Wir speichern sie in messageBoxA_address
.
Damit Windows uns das Kopieren erlaubt, müssen wir die Berechtigungen ändern. Wir müssen den Codebereich im RAM beschreibbar machen. Das geht einfach mit VirtualProtect
. Wenn der Code beschreibbar ist, kopieren wir das Array und danach stellen wir die alten Berechtigungen wieder her.
DWORD oldProtection;
VirtualProtect((LPVOID)messageBoxA_address,1024,PAGE_EXECUTE_READWRITE,&oldProtection);
memcpy((void*)messageBoxA_address,funcMem,7);
VirtualProtect((LPVOID)messageBoxA_address,1024,oldProtection,nullptr);
Der Test
Jetzt können wir testen, ob unser Hook funktioniert. Wenn ja, dann sollten wir diese Ausgabe bekommen.
Damit wirklich nichts fehlt, hier der komplette Code für install_hook
und OurMessageBoxA
. In der Funktion main
wurde vor allem anderen ein Aufruf an install_hook
eingefügt:
#include <Windows.h>
int main() {
install_hook();
system("pause");
MessageBoxA(nullptr, "Beispieltext", "Beispielmeldung", MB_OK);
system("pause");
}
#include <Windows.h>
#include "functions.h"
int WINAPI OurMessageBoxA(HWND hwnd,LPCSTR text,LPCSTR title,UINT type);
void install_hook() {
DWORD our_address = reinterpret_cast<DWORD>(&OurMessageBoxA);
unsigned char funcMem[7];
//mov eax, our_address
funcMem[0] = 0xb8;
funcMem[1] = our_address & 0xFF;
funcMem[2] = ( our_address >> 8 ) & 0xFF;
funcMem[3] = ( our_address >> 16 ) & 0xFF;
funcMem[4] = ( our_address >> 24 ) & 0xFF;
//jmpeax
funcMem[5]=0xff;
funcMem[6]=0xe0;
DWORD messageBoxA_address = (DWORD)GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");
DWORD oldProtection;
VirtualProtect((LPVOID)messageBoxA_address,1024,PAGE_EXECUTE_READWRITE,&oldProtection);
memcpy((void*)messageBoxA_address,funcMem,7);
VirtualProtect((LPVOID)messageBoxA_address,1024,oldProtection,nullptr);
}
int WINAPI OurMessageBoxA(HWND hwnd,LPCSTR text,LPCSTR title,UINT type)
{
std::cout<<"---------------------------------\n";
std::cout<<"|"<<std::setw(30)<<title<<"|\n";
std::cout<<"---------------------------------\n";
std::cout<<"|"<<std::setw(30)<<text<<"|\n";
std::cout<<"---------------------------------\n";
return IDOK;
}
Die originale Funktion aufrufen
Wir haben nun erfolgreich einen Hook geschrieben, dabei aber auch die Funktion MessageBoxA
zerstört. Jetzt wollen wir das wieder in Ordung bringen. Wir speichern uns die ersten 7 Bytes der Funktion, patchen sie, wenn die Funktion aufgerufen wird, patchen wir sie wieder zurück, haben eine normale MessageBoxA
zur Verfügung, danach patchen wir sie wieder und alles ist beim Alten.
Globale Variablen
Eigentlich sollte man keine globale Variablen verwenden und stattdessen alles über Parameter übergeben, da wir aber die Signatur einhalten müssen, geht das leider nicht. Deshalb übergeben wir folgende Informationen als globale Variablen: Die alte Funktionsaddresse, die neue Funktionsaddresse, die 7 Byte Backup und die 7 Byte Assembler-Code.
Mit diesen Infos können wir die Funktion in beide Richtungen patchen. Aber als erstes passen wir die Funktion install_hook
an, sodass sie die neuen, globalen Variablen verwendet. Diese packen wir der Übersichtlichkeit halber in einen Namespace:
namespace globals_MessageBoxA {
typedef int (WINAPI *MessageBoxAPtr)(HWND,LPCSTR,LPCSTR,UINT);
MessageBoxAPtr origFuncPtr = reinterpret_cast<MessageBoxAPtr>(GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA"));
MessageBoxAPtr newFuncPtr=&OurMessageBoxA;
unsigned char funcMem[7];
unsigned char payloadMem[7];
}
Mit typedef
definieren wir uns eine Abkürzung für den Funktionspointer auf MessageBoxA
. So sieht das Füllen des Codes jetzt aus:
//mov eax,our_address
globals_MessageBoxA::payloadMem[0]=0xb8;
globals_MessageBoxA::payloadMem[1]=(DWORD)globals_MessageBoxA::newFuncPtr&0xFF;
globals_MessageBoxA::payloadMem[2]=((DWORD)globals_MessageBoxA::newFuncPtr>>8 )&0xFF;
globals_MessageBoxA::payloadMem[3]=((DWORD)globals_MessageBoxA::newFuncPtr>>16)&0xFF;
globals_MessageBoxA::payloadMem[4]=((DWORD)globals_MessageBoxA::newFuncPtr>>24)&0xFF;
//jmp eax
globals_MessageBoxA::payloadMem[5]=0xff;
globals_MessageBoxA::payloadMem[6]=0xe0;
Zum Patchen verwenden wir nun folgenden Code, wir sichern auch schon die ersten 7 Bytes in funcMem
.
memcpy(globals_MessageBoxA::funcMem,globals_MessageBoxA::origFuncPtr,7);
DWORD oldProtection;
VirtualProtect(globals_MessageBoxA::origFuncPtr,1024,PAGE_EXECUTE_READWRITE,&oldProtection);
memcpy(globals_MessageBoxA::origFuncPtr,globals_MessageBoxA::payloadMem,7);
VirtualProtect(globals_MessageBoxA::origFuncPtr,1024,oldProtection,nullptr);
Das Zurückpatchen
Jetzt fehlt nur noch das Zurückpatchen und wir sind fertig!
DWORD oldProtection;
VirtualProtect(globals_MessageBoxA::origFuncPtr,1024,PAGE_EXECUTE_READWRITE,&oldProtection);
memcpy(globals_MessageBoxA::origFuncPtr,globals_MessageBoxA::funcMem,7);
VirtualProtect(globals_MessageBoxA::origFuncPtr,1024,oldProtection,nullptr);
Da wir recht weit am Ende des Artikels sind, hier noch einmal den kompletten Code.
#include <Windows.h>
int main() {
install_hook();
system("pause");
MessageBoxA(nullptr, "Beispieltext", "Beispielmeldung", MB_OK);
system("pause");
}
#include <Windows.h>
#include "functions.h"
int WINAPI OurMessageBoxA(HWND hwnd,LPCSTR text,LPCSTR title,UINT type);
namespace globals_MessageBoxA {
typedef int (WINAPI *MessageBoxAPtr)(HWND,LPCSTR,LPCSTR,UINT);
MessageBoxAPtr origFuncPtr = reinterpret_cast<MessageBoxAPtr>(GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA"));
MessageBoxAPtr newFuncPtr=&OurMessageBoxA;
unsigned char funcMem[7];
unsigned char payloadMem[7];
}
void install_hook() {
// mov eax, our_address
globals_MessageBoxA::payloadMem[0] = 0xb8;
globals_MessageBoxA::payloadMem[1] = (DWORD)globals_MessageBoxA::newFuncPtr & 0xFF;
globals_MessageBoxA::payloadMem[2] = ((DWORD)globals_MessageBoxA::newFuncPtr >> 8 ) & 0xFF;
globals_MessageBoxA::payloadMem[3] = ((DWORD)globals_MessageBoxA::newFuncPtr >> 16) & 0xFF;
globals_MessageBoxA::payloadMem[4] = ((DWORD)globals_MessageBoxA::newFuncPtr >> 24) & 0xFF;
// jmp eax
globals_MessageBoxA::payloadMem[5] = 0xff;
globals_MessageBoxA::payloadMem[6] = 0xe0;
memcpy(globals_MessageBoxA::funcMem, globals_MessageBoxA::origFuncPtr, 7);
DWORD oldProtection;
VirtualProtect(globals_MessageBoxA::origFuncPtr, 1024, PAGE_EXECUTE_READWRITE, &oldProtection);
memcpy(globals_MessageBoxA::origFuncPtr, globals_MessageBoxA::payloadMem, 7);
VirtualProtect(globals_MessageBoxA::origFuncPtr, 1024, oldProtection, nullptr);
}
int WINAPI OurMessageBoxA(HWND hwnd,LPCSTR text,LPCSTR title,UINT type)
{
// DE-PATCH
DWORD oldProtection;
VirtualProtect(globals_MessageBoxA::origFuncPtr, 1024, PAGE_EXECUTE_READWRITE, &oldProtection);
memcpy(globals_MessageBoxA::origFuncPtr, globals_MessageBoxA::funcMem, 7);
VirtualProtect(globals_MessageBoxA::origFuncPtr, 1024, oldProtection, nullptr);
// DE-PATCH ENDE
std::string t(text);
t += "\nAPI was hooked";
int ret = globals_MessageBoxA::origFuncPtr(hwnd, t.c_str(), title, type);
// PATCH
oldProtection;
VirtualProtect(globals_MessageBoxA::origFuncPtr, 1024, PAGE_EXECUTE_READWRITE, &oldProtection);
memcpy(globals_MessageBoxA::origFuncPtr, globals_MessageBoxA::payloadMem, 7);
VirtualProtect(globals_MessageBoxA::origFuncPtr, 1024, oldProtection, nullptr);
// PATCH ENDE
}
Nun ist der Hook fertig, wir können mit den Parametern und den Rückgabewerten anstellen, was wir wollen. API Hooking ist eine mächtige Technik, vor allem im Zusammenhang mit DLL-Injection, über die es auf jeden Fall auch noch einen Artikel geben wird!
Variablen im Code:
Variable | Beschreibung |
---|---|
globals_MessageBoxA::origFuncPtr |
Funktionszeiger auf den Speicherbereich von MessageBoxA . |
globals_MessageBoxA::newFuncPtr |
Funktionszeiger auf OurMessageBoxA . |
globals_MessageBoxA::funcMem |
Backup der ersten 7 Bytes von MessageBoxA . |
globals_MessageBoxA::payloadMem |
Assemblercode, der die Funktion auf unsere umleitet. |