Eine Reihe immer komplexer werdender Programme, die das Funktions-Hooking unter 64-Bit-Windows demonstrieren.
Ich habe diese Programme geschrieben, während ich mir selbst beigebracht habe, wie Function Hooking funktioniert. Sie können für andere hilfreich sein, die das Gleiche versuchen (oder für mich in Zukunft, wenn ich wieder vergessen habe, wie das alles funktioniert). Sie sollen der Reihe nach betrachtet werden. Wenn eine Funktion zum ersten Mal in einem Programm verwendet wird, wird sie in die CPP-Datei für dieses spezifische Beispielprogramm aufgenommen, wobei dem Namen ein Unterstrich vorangestellt wird. Alle nachfolgenden Beispiele, die dieselbe Funktion verwenden, verwenden die in „hooking_common.h“ enthaltene Kopie dieser Funktion, um die Codeduplizierung zu minimieren und die späteren Beispielprogramme klein genug zu halten, damit sie immer noch leicht lesbar sind.
Ich habe ein wenig Arbeit geleistet, um sie zu bereinigen, aber die späteren Beispiele sind immer noch etwas chaotisch. Es ist wichtig zu beachten, dass das Endergebnis in diesem Repo nicht ausreicht, um eine voll funktionsfähige Hooking-Bibliothek zu schreiben, aber es reicht aus, um diesen Weg einzuschlagen.
Zur Laufzeit sind einige Projekte möglicherweise darauf angewiesen, dass andere Projekte erstellt werden (das Einfügen einer DLL erfordert, dass die DLL erstellt wurde) oder dass sie gleichzeitig ausgeführt werden (das Einbinden eines Zielprogramms erfordert, dass es bereits ausgeführt wird).
Alle Beispiele wurden mit Visual Studio 2019 (v142) mit Windows SDK 10.0.17763.0 erstellt. Ich glaube nicht, dass es hier etwas gibt, das von der VS- oder SDK-Version abhängt, aber ich liste es hier nur für den Fall auf. Es gibt mit ziemlicher Sicherheit einige Dinge, die MSVC-spezifisch sind.
Schließlich installiert das letzte Trampolin-Beispiel einen Hook in mspaint. Ich gehe davon aus, dass dieses Beispiel irgendwann in der Zukunft durch ein Update von mspaint nicht mehr funktionieren wird. Zum Zeitpunkt des Verfassens dieses Artikels war die aktuelle Version von mspaint 1909 (OS Build 18363.1016).
Die Beispiele sind in zwei Kategorien unterteilt: solche, die Trampoline benutzen, und solche, die dies nicht tun. Die Nicht-Trampolin-Beispiele dienen ausschließlich dazu, die Umleitung des Programmflusses von einer Funktion zu einer anderen in verschiedenen Situationen zu demonstrieren. Der Bau von Trampolinen ist kompliziert, und als ich herausfinden wollte, wie Function Hooking funktioniert, war es äußerst hilfreich, zunächst mit dem Bau der Nicht-Trampolin-Beispiele zu beginnen. Zusätzlich gibt es 4 „Zielprogramme“, die von Beispielen verwendet werden, die demonstrieren wollen, wie man Hooks in verschiedenen (bereits laufenden) Prozessen installiert.
Bei den meisten dieser Beispiele geht Speicher verloren, der mit den Hooks zusammenhängt. Es ist mir eigentlich egal, zum einen, weil diese Beispiele nur dazu dienen, ein Hooking-Konzept zu demonstrieren, und zum anderen, weil diese „durchgesickerten“ Allocs ohnehin bis zur Programmbeendigung bestehen bleiben müssen.
Während es anscheinend nicht viel Standardterminologie für Funktions-Hooking-Techniken zu geben scheint, werden im Code (und den Readme-Dateien) in diesem Repository die folgenden Begriffe verwendet:
Da diese Beispiele bei der Installation ihrer Haken keine Trampoline erzeugen, halte ich diese Funktionen für eine Demonstration des „destruktiven“ Einhakens, da die ursprüngliche Funktion nach dem Einhaken völlig unbrauchbar ist.
Ein kleines Beispiel für das Überschreiben der Startbytes einer Funktion mit einer Sprunganweisung, die den Programmfluss zu einer anderen Funktion innerhalb desselben Programms umleitet. Da kein Trampolin konstruiert wird, ist dieser Vorgang destruktiv und die ursprüngliche Funktion ist nicht mehr aufrufbar. Dies ist das einzige 32-Bit-Beispiel im Repository.
Die 64-Bit-Version des vorherigen Beispiels. In 64-Bit-Anwendungen können Funktionen so weit entfernt im Speicher liegen, dass sie nicht über einen relativen 32-Bit-Sprungbefehl erreichbar sind. Da es keinen relativen 64-Bit-Sprungbefehl gibt, erstellt dieses Programm zunächst eine „Relais“-Funktion, die Bytes für einen absoluten JMP-Befehl enthält, der überall im Speicher ankommen kann (und zur Nutzlastfunktion springt). Der 32-Bit-Sprung, der in der Zielfunktion installiert wird, springt zu dieser Relaisfunktion und nicht sofort zur Nutzlast.
Bietet ein Beispiel für die Verwendung der Techniken aus dem vorherigen Projekt, um eine Member-Funktion anstelle einer freien Funktion einzubinden.
Dieses Programm unterscheidet sich geringfügig von den vorherigen Beispielen und zeigt, wie ein Hook in eine virtuelle Memberfunktion installiert wird, indem die Adresse dieser Funktion über die Vtable eines Objekts abgerufen wird. Es gibt keine anderen Beispiele, die sich mit virtuellen Funktionen befassen, aber ich fand es interessant genug, um es hier aufzunehmen.
Das einfachste Beispiel für die Installation eines Hooks in einem anderen laufenden Prozess. In diesem Beispiel wird die DbgHelp-Bibliothek verwendet, um eine Funktion in einem Zielprozess (A – Ziel mit freier Funktion) anhand des Zeichenfolgennamens zu finden. Dies ist nur möglich, weil das Zielprogramm mit aktivierten Debug-Symbolen erstellt wird. Dieses Beispiel ist zwar einfach, aber aufgrund der großen Anzahl neuer Funktionen, die es einführt (zum Auffinden und Bearbeiten eines Remote-Prozesses), etwas länger als frühere Programme.
Dieses Beispiel zeigt, wie man eine Funktion einbindet, die ein anderer Prozess aus einer DLL importiert hat. Aufgrund der Funktionsweise von ASLR gibt es einige Nuancen beim Abrufen der Adresse einer DLL-Funktion in einem Remoteprozess, die hier demonstriert werden. Ansonsten ist dieses Beispiel fast identisch mit dem vorherigen.
Dieses Beispiel zeigt, wie ein Hook in einer Funktion installiert wird, die nicht von einer DLL importiert wird und die nicht in der Symboltabelle enthalten ist (wahrscheinlich, weil der Remote-Prozess keine Debug-Symbole hat). Das bedeutet, dass es keine (einfache) Möglichkeit gibt, die Zielfunktion anhand des Stringnamens zu finden. Stattdessen wird in diesem Beispiel davon ausgegangen, dass Sie einen Disassembler wie x64dbg verwendet haben, um die relative virtuelle Adresse (RVA) der Funktion abzurufen, die Sie einbinden möchten. Dieses Programm verwendet diesen RVA, um einen Hook zu installieren.
Ähnlich wie oben, außer dass dieses Beispiel die DLL-Injection verwendet, um die Payload-Funktion zu installieren, anstatt rohe Maschinencode-Bytes zu schreiben. Dies ist viel einfacher zu handhaben, da Ihre Payloads wieder in C++ geschrieben werden können. Die Nutzlast für dieses Beispiel ist im Projekt 08B-DLL-Payload enthalten.
Die folgenden Beispiele installieren Trampoline beim Hooken, was bedeutet, dass das Programm die Logik in der Zielfunktion weiterhin ausführen kann, nachdem ein Hook installiert wurde. Da durch die Installation eines Hooks mindestens die ersten 5 Bytes in der Zielfunktion überschrieben werden, werden die in diesen 5 Bytes enthaltenen Anweisungen in die Trampolinfunktion verschoben. Somit führt der Aufruf der Trampolinfunktion effektiv die ursprüngliche Logik der Zielfunktion aus.
Das Trampolin-Installationsäquivalent von Beispiel Nr. 2. Dieses Beispiel ist etwas seltsam, weil ich demonstrieren wollte, wie man ein Trampolin baut, ohne dass eine Demontagemaschine erforderlich ist. In diesem Fall wurde die Zielfunktion so erstellt, dass sie am Anfang einen bekannten 5-Byte-Befehl hat, sodass wir einfach die ersten fünf Bytes dieser Funktion in die Trampolinfunktion kopieren können. Das bedeutet, dass die Erstellung des Trampolins wirklich einfach ist, da wir die genaue Größe kennen und keine relative Adressierung verwenden, die korrigiert werden muss. Wenn Sie ein Trampolin für einen ganz bestimmten Anwendungsfall schreiben würden, könnten Sie wahrscheinlich damit durchkommen, einfach eine Variation davon zu machen.
Dieses Beispiel zeigt ein ähnliches Szenario wie das vorherige, außer dass ich dieses Mal einen Disassembler (Capstone) verwende, um die Bytes abzurufen, die wir aus der Zielfunktion stehlen müssen. Dadurch kann der Hooking-Code für jede Funktion verwendet werden, nicht nur für solche, von denen wir wissen, dass sie einfache Fälle sind. In diesem Beispiel ist tatsächlich eine ganze Menge los, denn es geht von einem gezielten Hook (wie dem vorherigen) zum Aufbau einer generischen Hooking-Funktion über. Das Trampolin muss relative Aufrufe/Sprünge in Anweisungen umwandeln, die absolute Adressen verwenden, was die Sache noch komplizierter macht. Dies ist auch kein 100 % ausgefeiltes Beispiel für generisches Hooking. Es schlägt bei Schleifenanweisungen fehl, und wenn Sie versuchen, Funktionen mit weniger als 5 Bytes an Anweisungen einzubinden.
Im Grunde dasselbe wie oben, außer dass dieses Beispiel Code enthält, um alle ausgeführten Threads anzuhalten, während ein Hook installiert wird. Dies ist nicht in allen Fällen garantiert threadsicher, aber es ist auf jeden Fall viel sicherer, als nichts zu tun.
Dies erweitert den Hooking-/Trampolin-Code, der in den beiden vorherigen Beispielen verwendet wurde, um die Umleitung mehrerer Funktionen auf dieselbe Nutzlast zu unterstützen und um Nutzlastfunktionen das Aufrufen anderer Funktionen mit darin installierten Hooks zu ermöglichen.
Dies ist das erste Trampolin-Beispiel, das einen Hook in einem anderen Prozess installiert (in diesem Fall die Ziel-App B – Ziel mit kostenlosen Funktionen aus der DLL). Die gesamte Hooking-Logik ist in einer DLL-Nutzlast 13B – Trampoline Imported Func DLL Payload enthalten. Hier gibt es nicht viel Neues, dieses Beispiel kombiniert lediglich die bereits gemachten Trampolin-Hooking-Sachen mit den zuvor gezeigten Techniken zum Hooken einer aus einer DLL importierten Funktion.
Das Kronjuwel des Repos. In diesem Beispiel wird eine DLL-Nutzlast (14B – Trampoline Hook MSPaint Payload) in eine laufende Instanz von mspaint eingefügt (Sie müssen mspaint selbst starten, bevor Sie dies ausführen). Der installierte Haken bewirkt, dass Pinsel rot gezeichnet werden, unabhängig davon, welche Farbe Sie tatsächlich in MSPaint ausgewählt haben. Ehrlich gesagt gibt es hier nichts, was nicht im vorherigen Beispiel gezeigt wurde. Es ist einfach cool zu sehen, wie dies an einem nicht erfundenen Programm funktioniert.
Einfache Zielanwendung, die eine freie Funktion in einer Schleife aufruft. Kompiliert mit Debug-Informationen.
Zielanwendung, die eine freie Funktion aufruft, die aus einer DLL (B2 - GetNum-DLL) in einer Schleife importiert wurde.
Zielanwendung, die eine nicht virtuelle Memberfunktion in einer Schleife aufruft.
Zielanwendung, die eine virtuelle Memberfunktion in einer Schleife aufruft.