C++: Windows API-Hooking – Rootkit-Techniken (Gastartikel)

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.

MessageBoxA-Ausgabe in der Konsole

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.

Weiterführende Links

  1. Patricks Blog: DLL-Injection

Hinweis:
Dies ist ein älterer Artikel von meinem alten Blog. Die Kommentare zu diesem Artikel werden (falls vorhanden) später noch hinzugefügt.

Der Autor

Unter dem Namen »TheBlackPhantom« alias »BlackY« veröffentlichte ich auf meinem alten Blog, BlackPhantom.DE, in der Zeit von 2011 bis 2015 leidenschaftlich Beiträge über Computer, Internet, Sicherheit und Malware. Während der BlackPhantom-Zeit war ich noch grün hinter den Ohren und lernte viel dazu. Mehr Infos vielleicht in Zukunft...