Erstellen einer Proxy-DLL für die Überprüfung von Hijack-DLL-Vorgängen

Wenn ich die Software-Sicherheit untersuche, muss ich unter anderem mit dynamischen Bibliotheken arbeiten. Angriffe wie die Hijack-DLL ("DLL-Spoofing" oder "DLL-Interception") sind sehr selten. Dies ist höchstwahrscheinlich auf die Tatsache zurückzuführen, dass Windows-Entwickler Sicherheitsmechanismen hinzufügen, um Angriffe zu verhindern, und Softwareentwickler bei der Sicherheit vorsichtiger sind. Umso interessanter ist jedoch die Situation, in der die Zielsoftware anfällig ist.

Die Hijack-DLL beschreibt den Angriff kurz und erzeugt eine Situation, in der eine ausführbare Datei versucht, die DLL zu laden, der Angreifer jedoch in diesen Prozess eingreift. Anstelle der erwarteten Bibliothek wird eine speziell vorbereitete DLL mit den Nutzdaten des Angreifers geladen. Infolgedessen wird der Code aus der DLL mit den Rechten der gestarteten Anwendung ausgeführt. Daher werden normalerweise Anwendungen mit höheren Rechten als Ziel ausgewählt.

Damit die Bibliothek korrekt geladen werden kann, müssen eine Reihe von Bedingungen erfüllt sein: Die Bitgröße der ausführbaren Datei und der Bibliothek muss übereinstimmen. Wenn die Bibliothek beim Start der Anwendung geladen wird, muss die DLL alle Funktionen exportieren, die diese Anwendung voraussichtlich importieren wird. Oft reicht ein Import nicht aus - es ist sehr wünschenswert, dass die Anwendung ihre Arbeit nach dem Laden der DLL fortsetzt. Dazu ist es erforderlich, dass die Funktionen der vorbereiteten Bibliothek genauso funktionieren wie das Original. Der einfachste Weg, dies zu tun, besteht darin, die Funktionsaufrufe einfach von einer Bibliothek an eine andere zu übergeben. Dies sind die DLLs, die als Proxy-DLLs bezeichnet werden.



Unter dem Schnitt befinden sich mehrere Optionen zum Erstellen solcher Bibliotheken - sowohl in Form von Code als auch in Form von Dienstprogrammen.

Eine kleine theoretische Überprüfung


Bibliotheken werden häufiger mit der LoadLibrary-Funktion geladen, an die der Bibliotheksname übergeben wird. Wenn Sie anstelle des Namens den vollständigen Pfad übergeben, versucht die Anwendung, die angegebene Bibliothek zu laden. Wenn Sie beispielsweise LoadLibrary ("C: \ Windows \ system32 \ version.dll") aufrufen, wird die angegebene DLL geladen. Wenn die Bibliothek nicht vorhanden ist, wird sie nicht geladen.

Ein bisschen langweilig
Wenn bereits eine DLL in die Anwendung geladen ist, wird sie nicht erneut geladen. Da version.dll zu Beginn fast jeder exe-Datei geladen wird, lädt der obige Aufruf tatsächlich nichts. Aber wir betrachten immer noch den allgemeinen Fall, betrachten das Beispiel als Aufruf einer abstrakten Bibliothek.

Es ist eine ganz andere Sache, wenn Sie LoadLibrary ("version.dll") schreiben. In einer normalen Situation ist das Ergebnis genau das gleiche wie im vorherigen Fall - C: \ Windows \ system32 \ version.dll wird geladen, aber nicht so einfach.

Zunächst wird eine Bibliothek durchsucht, die in der folgenden Reihenfolge abläuft :

  1. Ausführbarer Ordner
  2. Ordner C: \ Windows \ System32
  3. Ordner C: \ Windows \ System
  4. Ordner C: \ Windows
  5. Der Ordner, der für die Anwendung als aktuell festgelegt wurde
  6. Ordner aus der Umgebungsvariablen PATH

Noch etwas Langeweile
Beim Starten von 32-Bit-Anwendungen auf einem 64-Bit-System werden alle Aufrufe von C: \ Windows \ system32 an C: \ Windows \ SysWOW64 weitergeleitet. Dies ist nur für die Richtigkeit der Beschreibung, aus Sicht des Angreifers ist der Unterschied nicht besonders wichtig.

Wenn Sie die exe-Datei ausführen, lädt das Betriebssystem alle Bibliotheken aus dem Dateiimportabschnitt. Im Allgemeinen können wir davon ausgehen, dass das Betriebssystem die Datei zum Aufrufen von LoadLibrary zwingt und alle im Importabschnitt geschriebenen Bibliotheksnamen übergibt. Da es in 99,9% der Fälle Namen und keine Pfade gibt, werden beim Start der Anwendung alle geladenen Bibliotheken im System durchsucht.

Aus der Liste der DLL-Suchorte sind zwei Punkte für uns wirklich wichtig - 1 und 6. Wenn wir version.dll in denselben Ordner legen, von dem aus die Datei gestartet wird, wird anstelle des Systems einer geladen. Diese Situation tritt fast nie auf, denn wenn die Möglichkeit besteht, eine Bibliothek einzurichten, ist es höchstwahrscheinlich möglich, die ausführbare Datei selbst zu ersetzen. Dennoch sind solche Situationen möglich. Befindet sich die ausführbare Datei beispielsweise in einem beschreibbaren Ordner und handelt es sich um einen Dienst mit automatischem Start, kann sie nicht geändert werden, während der Dienst selbst ausgeführt wird. Oder die gestartete Datei wird vor dem Start extern durch eine Prüfsumme überprüft. Das Ersetzen der Datei ist dann immer noch keine Option. Aber die Bibliothek daneben zu stellen, wird ziemlich real sein.

Möglicherweise können Sie keine Dateien neben ausführbaren Dateien erstellen, aber Sie können Ordner erstellen. In dieser Situation funktioniert möglicherweise der WinSxS-Umleitungsmechanismus (auch bekannt als „DotLocal“).

Kurz über DotLocal
Das Manifest der Datei kann eine Abhängigkeit von der Bibliothek einer bestimmten Version enthalten. In diesem Fall prüft das Betriebssystem beim Starten der ausführbaren Datei (z. B. application.exe), ob ein Ordner mit dem Namen application.exe.local im selben Ordner wie die Datei selbst vorhanden ist. Dieser Ordner sollte einen Unterordner mit einem komplexen Namen wie amd64_microsoft.windows.common-control_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b haben, in dem sich bereits eine comctl32.dll-Bibliothek befindet. Der Bibliotheksname und die Informationen für den Ordnernamen sollten im Manifest angegeben werden. Hier ist nur ein Beispiel aus dem ersten Prozess, der aufgetreten ist. Wenn keine Ordner oder Dateien vorhanden sind, wird die Bibliothek aus C: \ Windows \ WinSxS entnommen. Im Beispiel C: \ Windows \ WinSxS \ amd64_microsoft.windows.common-control_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b \ comctl32.dll.

Dies ist jedoch eher die Ausnahme als die Regel. Aber die Situationen, in denen die DLL-Suche die 6. Nummer in der Liste erreicht, sind ziemlich real. Wenn die Anwendung versucht, eine DLL zu laden, die sich nicht auf dem System oder neben der Datei befindet, werden bei allen Suchvorgängen bis zu 6 Punkte angezeigt. Dies können möglicherweise beschreibbare Ordner sein.

Beispielsweise tritt eine typische Python-Installation am häufigsten im Ordner C: \ Python (oder Schließen) auf. Das Python-Installationsprogramm selbst schlägt vor, seine Ordner zur Systemvariablen PATH hinzuzufügen. Als Ergebnis haben wir ein gutes Sprungbrett, um einen Angriff zu starten - der Ordner kann von allen Benutzern geschrieben werden, und jeder Versuch, eine nicht vorhandene Bibliothek zu laden, führt zur Pfadsuche von PATH.

Betrachten Sie nach Abschluss der Theorie die Erstellung der Nutzdaten - der Proxy-Bibliotheken selbst.

Die erste Option. Ehrliche Proxy-Bibliothek


Beginnen wir mit einer relativ einfachen - wir erstellen eine ehrliche Proxy-Bibliothek. Ehrlichkeit bedeutet in diesem Fall, dass alle Funktionen in der DLL explizit registriert werden und für jede Funktion ein Funktionsaufruf mit demselben Namen aus der ursprünglichen Bibliothek geschrieben wird. Die Arbeit mit einer solchen Bibliothek ist für den aufgerufenen Code vollständig transparent: Wenn eine Funktion aufgerufen wird, erhält sie die richtige Antwort, das Ergebnis und alles, was nebeneinander geschehen sollte.

Hier ist ein Link zum fertigen Beispiel ( github ) der version.dll-Bibliothek.

Code-Highlights:

  • Alle Funktionsprototypen aus der Exporttabelle der Originalbibliothek werden ehrlich beschrieben.
  • Die ursprüngliche Bibliothek wird geladen und alle Aufrufe unserer Funktionen werden in sie geworfen.

Praktischerweise funktioniert die Anwendung weiterhin ordnungsgemäß, ohne dass "Spezialeffekte" auftreten. Es ist unpraktisch, dass ich für jede der Funktionen einen Haufen einheitlichen Codes schreiben musste, um außerdem die Übereinstimmung der Prototypen sorgfältig zu überprüfen.

Die zweite Option. Vereinfachen Sie das Schreiben von Code


Wenn es sich um eine Bibliothek wie version.dll handelt, bei der die Importtabelle klein ist, es nur 17 Funktionen gibt und die Prototypen einfach sind, ist eine ehrliche Proxy-Bibliothek eine gute Wahl.



Wenn der Proxy für die Bibliothek beispielsweise bcrypt ist, ist alles komplizierter. Hier ist ihre Importtabelle:



57 Funktionen! Und hier einige Beispiele für Prototypen:




Sagen wir einfach, dass nichts unmöglich ist, aber es ist nicht sehr angenehm, einen ehrlichen Proxy für eine solche Bibliothek zu erstellen.

Sie können den Code vereinfachen, wenn Sie ein wenig mit Funktionen betrügen. Wir werden alle Funktionen in der Bibliothek als __declspec (nackt) deklarieren und im Body Assembler-Code verwenden, der einfach jmp für die Funktion aus der ursprünglichen Bibliothek erstellt. Auf diese Weise können wir keine langen Prototypen verwenden, sondern überall einfache Ankündigungen ohne Ansichtsparameter platzieren:

void foo ()

Wenn die Anwendung unsere Funktion aufruft, führt die Proxy-Bibliothek keine Manipulationen mit dem Register und dem Stapel durch, sodass die ursprüngliche Funktion die gesamte Arbeit wie gewünscht ausführen kann.

Ein Beispiel ( github ) der version.dll-Bibliothek mit diesem Ansatz.

Highlights:

  • Die ursprüngliche Bibliothek wird geladen und alle Aufrufe unserer Funktionen werden in sie geworfen. Funktionskörper und Laden sind in Makros eingeschlossen.

Bequemer und korrekter Betrieb der Anwendung und die Tatsache, dass dank Makros auch eine Vielzahl von Funktionen leicht beschrieben werden können. Es ist unpraktisch, dass eher unerwarteter Rake in x64. Visual Studio (irgendwo seit 2012, wenn ich mich richtig erinnere) verbietet die Verwendung von nackten und asm-Einfügungen in 64-Bit-Code. Wenn Sie einen Proxy von Grund auf neu schreiben, muss jede Funktion überprüfen, ob er in der Def-Datei beschrieben ist, ob das Original geladen ist und ob der Funktionskörper beschrieben ist.

Die dritte Option. Wir werfen den Körper im Allgemeinen raus


Die Verwendung von nackt schlägt eine weitere Option vor. Sie können eine Importtabelle erstellen, die sich für alle Funktionen auf eine echte Codezeile bezieht:

void nop () {}

Eine solche Bibliothek wird von der Anwendung geladen, funktioniert jedoch nicht. Wenn Sie eine der Funktionen aufrufen, wird der Stapel höchstwahrscheinlich zerrissen oder es kommt zu einem anderen Dreck. Dies ist jedoch nicht immer schlecht. Wenn das Ziel einer DLL-Injektion beispielsweise darin besteht, den Code einfach mit den erforderlichen Rechten auszuführen, reicht es aus, die Nutzdaten aus der DllMain-Proxy-Bibliothek auszuführen und die Anwendung sofort leise zu beenden. In diesem Fall kommt es nicht zu einem echten Aufruf der Funktionen, und es treten keine Fehlerabstürze auf.

Ein Beispiel auf einem Github , wieder für version.dll.

Code-Highlights:

  • Alle Funktionen aus der Def-Datei beziehen sich auf eine NOP-Funktion.

Praktischerweise wird eine solche Proxy-Bibliothek nur für ein paar Minuten geschrieben. Es ist unpraktisch, dass die aufgerufene Anwendung nicht mehr funktioniert.

Die vierte Option. Nehmen Sie vorgefertigte Dienstprogramme mit


Das Schreiben einer DLL ist gut, aber nicht immer bequem und nicht sehr schnell. Daher sollten Sie automatisierte Optionen in Betracht ziehen.

Sie können dem Pfad alter Viren folgen - nehmen Sie die Bibliothek, deren Proxys wir erstellen möchten, erstellen Sie einen ausführbaren Codeabschnitt darin, notieren Sie die Nutzdaten dort und ändern Sie den Einstiegspunkt in diesen Abschnitt. Nicht der einfachste Weg, da Sie versehentlich etwas kaputt machen können, müssen Sie in Assembler schreiben, sich an das Gerät der PE-Datei erinnern. Dies ist nicht unser Weg.

Um den DLL-Hijack zu betreiben, fügen wir einen weiteren DLL-Hijack hinzu.



Dies ist relativ einfach zu tun. Wir kopieren die Bibliothek, deren Proxy wir erstellen möchten, und fügen der Importtabelle dieser Kopie eine DLL mit einer beliebigen Funktion hinzu. Jetzt läuft der Download entlang der Kette - am Anfang der ausführbaren Datei wird die Proxy-DLL geladen, die die angegebene Bibliothek selbst lädt.

„Hey, du hast das Laden einer Bibliothek durch eine andere ersetzt. Was ist der Punkt? Trotzdem muss die DLL codiert werden! ". Alles ist richtig, aber es gibt immer noch einen Sinn. Es gibt jetzt weniger Anforderungen für eine Bibliothek mit einer Nutzlast. Sie können einen beliebigen Namen angeben. Hauptsache, Sie exportieren nur eine Funktion, die einen beliebigen Prototyp haben kann. Geben Sie den Hauptnamen der Bibliothek und Funktion in die Importtabelle ein.

Eine Bibliothek mit einer Nutzlast kann für alle Gelegenheiten eine sein.

Sie können die Importtabelle mit vielen PE-Editoren ändern, z. B. CFF Explorer oder Pe-Bear. Für mich selbst habe ich ein kleines Dienstprogramm in C # geschrieben, das eine Tabelle ohne unnötige Gesten korrigiert. Quellen zu Github , Binar im Abschnitt Release .

Fazit


In dem Artikel habe ich versucht, die grundlegenden Methoden zum Erstellen der Proxy-DLL offenzulegen, die ich selbst verwendet habe. Es bleibt nur zu sagen, wie man sich verteidigt.

Es gibt nicht viele universelle Empfehlungen:

  • Speichern Sie ausführbare Dateien, insbesondere solche mit hohen Berechtigungen, nicht in Ordnern, die für Benutzer beschreibbar sind.
  • Es ist besser, zuerst die Existenz der Bibliothek zu finden und zu überprüfen, bevor Sie LoadLibrary ausführen.
  • Sehen Sie sich die vorhandenen Schutzmethoden an, die im Betriebssystem verfügbar sind. In Windows 10 können Sie beispielsweise das Flag PreferSystem32 so setzen, dass die DLL-Suche nicht mit dem Ordner für ausführbare Dateien beginnt, sondern mit system32.

Vielen Dank für Ihre Aufmerksamkeit. Ich freue mich über Fragen, Vorschläge, Vorschläge und Kommentare.

UPD: Auf Anraten von Kommentatoren erinnere ich Sie daran, dass Sie eine Bibliothek sorgfältig und sorgfältig auswählen müssen. Wenn die Bibliothek in der KnownDlls-Liste enthalten ist oder der Name MinWin ähnelt (ApiSetSchema, api-ms-win-core-console-l1-1-0.dll - das ist alles), ist es höchstwahrscheinlich aufgrund der Verarbeitungsfunktionen nicht möglich, sie abzufangen solche DLLs im Betriebssystem.

Source: https://habr.com/ru/post/de449904/


All Articles