Einige von Ihnen lieben es, andere hassen es, aber an diesem Punkt sollte es nicht überraschen, dass .NET Tradecraft etwas länger bleiben wird als erwartet. Das .NET Framework ist ein integraler Bestandteil des Betriebssystems von Microsoft; die aktuellste Version von .NET ist .NET Core. Core ist der plattformübergreifende Nachfolger des .NET Frameworks, das .NET auch auf Linux und macOS verfügbar macht. Dadurch ist .NET bei Angreifern und Red Teams für Post-Ausbeutung-Techniken beliebter denn je. Dieser Blogbeitrag befasst sich mit einer neuen Beacon Object File (BOF), die es Anwendern ermöglicht, .NET-Assemblys im Prozess über Cobalt Strike auszuführen, anstatt das traditionelle, integrierte Modul zum Ausführen von Assemblys zu verwenden, das die Fork-and-Run-Technik nutzt.
Cobalt Strike, eine beliebte Software zur Simulation von Angriffen, erkannte den Trend, dass Red Teams aufgrund der zunehmenden Erkennungsfähigkeit von PowerShell von PowerShell-Tools zu C# übergingen, und führte 2018 mit Cobalt Strike Version 3.11 das Modul „execute-assembly“ ein. Dadurch konnten die Betreiber die Leistungsfähigkeit von .NET-Assemblys nach der Ausbeutung nutzen, indem sie diese im Speicher ausführten, ohne das zusätzliche Risiko einzugehen, diese Tools auf die Festplatte zu übertragen. Obwohl die Fähigkeit, .NET-Assemblys über nicht verwalteten Code in den Speicher zu laden, zum Zeitpunkt der Veröffentlichung weder neu noch unbekannt war, würde ich sagen, dass Cobalt Strike diese Funktion dem breiten Publikum zugänglich gemacht und dazu beigetragen hat, die Beliebtheit von .NET für die Ausbeutung nach der Ausbeutung weiter zu steigern.
Das Execute-Assembly-Modul von Cobalt Strike verwendet die Fork-and-Run-Technik, bei der ein neuer Opferprozess gestartet wird, Ihr bösartiger Code nach der Ausbeutung in diesen neuen Prozess injiziert wird, Ihr bösartiger Code ausgeführt wird und der neue Prozess nach Abschluss beendet wird. Das hat sowohl seine Vorteile als auch seine Nachteile. Der Vorteil der Fork-and-Run-Methode besteht darin, dass die Ausführung außerhalb unseres Beacon-Implantatprozesses erfolgt. Das bedeutet, dass, wenn etwas in unserer Maßnahme nach der Ausbeutung schief geht oder entdeckt wird, die Wahrscheinlichkeit, dass unser Implantat überlebt, viel größer ist. Vereinfacht gesagt, trägt es wesentlich zur allgemeinen Stabilität des Implantats bei. Da Sicherheitsanbieter dieses Fork-and-Run-Verhalten jedoch erkannt haben, ist es nun zu einem – wie Cobalt Strike selbst zugibt – kostspieligen OPSEC-Muster geworden.
Mit der im Juni 2020 veröffentlichten Version 4.1 führte Cobalt Strike eine neue Funktion ein, um dieses Problem mit der Einführung von Beacon Object Files (BOFs) anzugehen. BOFs ermöglichen es Operatoren, die bekannten Ausführungsmuster wie oben beschrieben oder andere OPSEC-Fehler wie die Verwendung von cmd.exe/powershell.exe zu vermeiden, indem Objektdateien im Speicher innerhalb desselben Prozesses wie unser Beacon-Implantat ausgeführt werden. Ich möchte zwar nicht auf das Innenleben von BOFs eingehen, aber hier sind ein paar Blogbeiträge, die ich interessant fand:
Wenn Sie die oben genannten Blogs gelesen haben, sollten wir nun erkennen, dass BOFs nicht gerade die erhoffte Rettung waren, und wenn Sie davon geträumt haben, all diese großartigen .NET-Tools neu zu schreiben und in BOFs umzuwandeln, sind diese Träume nun zerplatzt. Entschuldigung! Die Hoffnung ist jedoch nicht verloren, denn meiner Meinung nach gibt es einige großartige Elemente, die BOFs bieten können, und ich hatte kürzlich viel Spaß (und auch etwas Frustration) dabei, die Grenzen dessen auszuloten, was man damit machen kann. Zunächst wurde CredBandit entwickelt, das einen vollständigen Speicherauszug eines Prozesses wie LSASS erstellt und diesen über den bestehenden Beacon-Kommunikationskanal zurücksendet. Heute veröffentliche ich InlineExecute-Assembly, mit dem Sie .NET-Assemblys innerhalb Ihres Beacon-Prozesses ausführen können, ohne Ihre bevorzugten .NET-Tools ändern zu müssen. Lassen Sie uns einen Blick darauf werfen, warum ich das BOF geschrieben habe, welche Hauptmerkmale es bietet, welche Einschränkungen es gibt und wie es bei der Durchführung von Adversary-Simulationen/Red Teams nützlich sein kann.
Branchen-Newsletter
Bleiben Sie mit dem Think-Newsletter über die wichtigsten – und faszinierendsten – Branchentrends in den Bereichen KI, Automatisierung, Daten und mehr auf dem Laufenden. Weitere Informationen finden Sie in der IBM Datenschutzerklärung.
Ihr Abonnement wird auf Englisch geliefert. In jedem Newsletter finden Sie einen Abmeldelink. Hier können Sie Ihre Abonnements verwalten oder sich abmelden. Weitere Informationen finden Sie in unserer IBM Datenschutzerklärung.
Der Grund für die Entwicklung von InlineExecute-Assembly ist recht einfach. Ich wollte eine Möglichkeit für unser Team zur Simulation von Gegnern schaffen, .NET-Assemblys im Prozess auszuführen, um einige der oben genannten OPSEC-Fallstricke zu vermeiden, die beim Einsatz von Cobalt Strike in ausgereiften Umgebungen auftreten. Ich benötigte das Tool auch, um unser Team nicht mit zusätzlicher Entwicklungszeit zu belasten, da wir keine Änderungen an den meisten unserer aktuellen .NET-Tools vornehmen müssten. Es musste außerdem stabil sein. So stabil ein komplexer BOF auch sein kann, denn das Letzte, was wir wollen, ist, einen unserer wenigen Beacons an die Umgebung zu verlieren. Grundsätzlich sollte es für den Bediener genauso nahtlos funktionieren wie das Execute-Assembly-Modul von Cobalt Strike.
Ich weiß, das ist ziemlich offensichtlich. Ohne sie kämen wir nicht weit, oder? Spaß beiseite, die Feinheiten der Funktionsweise der CLR und ihrer internen Abläufe könnten einen eigenen Blogbeitrag füllen. Wir werden daher nur ganz allgemein darauf eingehen, was die BOF beim Laden der CLR über nicht verwalteten Code verwendet.
Laden der CLR
Wie in dem vereinfachten Screenshot oben dargestellt, umfasst der BOF zum Laden der CLR im Wesentlichen Folgendes:
Die CLR ist nun also initialisiert, aber es muss noch ein wenig mehr passieren, bevor wir unsere bevorzugten .NET-Assemblys tatsächlich ausführen können. Wir müssen unsere AppDomain-Instanz erstellen, die Microsoft als „eine isolierte Umgebung, in der Anwendungen ausgeführt werden“ beschreibt. Mit anderen Worten: Dies wird zum Laden und Ausführen unserer .NET-Assemblys nach der Ausbeutung verwendet.
AppDomain wird erstellt und Assembly wird geladen/ausgeführt
Wie im obigen vereinfachten Screenshot dargestellt, sind die wichtigsten Schritte, die der BOF zum Laden und Aufrufen unserer .NET-Assembly ausführt, folgende:
Hoffentlich haben Sie nun ein grundlegendes Verständnis der .NET-Ausführung über nicht verwalteten Code, aber damit sind wir noch weit von einem funktionsfähigen Tool entfernt. Daher werden wir uns einige Funktionen ansehen, die im BOF implementiert wurden, um es von „meh“ zu „totes legit“ zu machen.
Sie fragen sich wahrscheinlich, warum das wichtig ist. Wenn Sie wie ich sind und Ihre Zeit schätzen, möchten Sie diese sicher nicht damit verbringen, so gut wie jede .NET-Assembly so zu ändern, dass ihr Einstiegspunkt eine Zeichenfolge mit all Ihren Daten zurückgibt, die normalerweise einfach an die Standardausgabe der Konsole weitergeleitet würden, oder? Das habe ich mir gedacht. Um dies zu vermeiden, müssen wir unsere Standardausgabe entweder an eine Named Pipe oder einen Mail Slot umleiten, die Ausgabe lesen, nachdem sie geschrieben wurde, und sie dann wieder in ihren ursprünglichen Zustand zurückversetzen. Auf diese Weise können wir unsere unveränderten Assemblys genauso ausführen wie über cmd.exe oder powershell.exe. Bevor wir uns nun mit dem Code befassen, möchte ich mich bei @N4k3dTurtl3 und ihrem Blogbeitrag über die Ausführung von Assemblys und Mail Slots bedanken. Das war ursprünglich der Grund, warum ich diese Technik in mein eigenes privates C-Implantat implementiert habe, als sie zum ersten Mal auf den Markt kam, und viele Monate später habe ich dieselbe Funktionalität auf ein BOF portiert. Nachdem wir nun die Grundlagen behandelt haben, sehen wir uns ein vereinfachtes Beispiel dafür an, wie dies durch Umleiten von stdout zu einer benannten Pipe erreicht werden kann:
Umleitung der Standardausgabe der Konsole auf benannte Leitung und anschließendes Zurücksetzen
Erinnern Sie sich, dass wir beim Laden der CLR über ICLRMetaHost ->GetRuntime angeben mussten, welche Version des .NET-Frameworks wir benötigen? Denken Sie daran, dass dies davon abhängt, mit welcher Version unsere .NET-Assembly kompiliert wurde? Es wäre doch ziemlich mühsam, jedes Mal manuell angeben zu müssen, welche Version benötigt wird, oder? Zu unserem Glück hat @b4rtik eine coole Funktion implementiert, um dies in ihrem execute-assembly-Modul für das Metasploit Framework zu handhaben, die wir ganz einfach in unser eigenes Tooling implementieren können, wie unten gezeigt:
Eine Funktion, die unsere .NET-Assembly liest und dabei hilft, die benötigte .NET-Version beim Laden der CLR zu bestimmen.
Im Wesentlichen liest diese Funktion, wenn ihr unsere Assembler-Bytes übergeben werden, diese Bytes durch und sucht nach den Hexadezimalwerten 76 34 2E 30 2E 33 30 33 31 39, die in ASCII umgewandelt v4.0.30319 ergeben. Hoffentlich kommt Ihnen das bekannt vor. Wird dieser Wert beim Lesen des Assembler-Codes gefunden, gibt die Funktion 1 oder True zurück, andernfalls 0 oder False. Damit können wir ganz einfach feststellen, welche Version geladen werden soll und ob 1/True oder 0/False zurückkommt, wie im folgenden Codebeispiel gezeigt:
If/Else-Anweisung zum Festlegen der .NET-Versionsvariablen
Wir könnten unmöglich über offensive .NET-Strategien sprechen, ohne AMSI zu erwähnen. Wir werden nicht näher darauf eingehen, was AMSI ist und wie es umgangen werden kann, da dies bereits mehrfach behandelt wurde. Wir werden jedoch kurz darauf eingehen, warum das Patchen von AMSI je nach dem, was Sie über BOF ausführen möchten, erforderlich sein kann. Wenn Sie beispielsweise beschließen, Seatbelt ohne jegliche Verschleierung auszuführen, werden Sie schnell feststellen, dass Sie keine Ausgabe erhalten haben und Ihr Beacon tot ist. Ja, KOMPLETT TOT. Dies liegt daran, dass AMSI Ihre Assembly abgefangen, sie als bösartig eingestuft und sie daraufhin abgeschaltet hat, wie eine Hausparty, die zu viel Lärm macht. Nicht ideal, oder? Nun haben wir zwei gute Optionen, wenn es um AMSI geht: Wir können entweder unsere .NET-Tools über etwas wie ConfuserX oder Invisibility Cloak verschleiern oder AMSI mithilfe verschiedener Techniken deaktivieren. In unserem Fall verwenden wir einen von RastaMouse, der die Datei „amsi.dll” im Speicher so patcht, dass sie „E_INVALIDARG” zurückgibt und das Scan-Ergebnis 0 ergibt. Wie in ihrem Blogbeitrag erwähnt, wird dies normalerweise als AMSI_RESULT_CLEAN interpretiert. Sehen wir uns unten eine vereinfachte Version des Codes für einen x64-Prozess an:
In-Memory-Patching von AmsiScanBuffer
Wie Sie dem obigen Screenshot entnehmen können, gehen wir einfach wie folgt vor:
Durch die Implementierung dieser Funktion in unser Tool sollten wir nun in der Lage sein, die Standardversion von Seatbelt.exe mit dem Parameter –amsi auszuführen, um die AMSI-Erkennung zu umgehen, wie unten gezeigt:
Beispiel für InlineExecute-Assemby-AMSI-Umgehung
Zum Glück für Verteidiger gibt es mehr als nur AMSI, um mithilfe von ETW bösartige .NET-Techniken zu erkennen. Leider lässt sich auch dies, ähnlich wie AMSI, von Angreifern relativ leicht umgehen, und @xpn hat einige wirklich beeindruckende Untersuchungen dazu durchgeführt, wie dies geschehen könnte. Im Folgenden finden Sie ein vereinfachtes Beispiel, wie Sie ETW patchen könnten, um es vollständig zu deaktivieren:
In-Memory-Patching von EtwEventWrite
Wie Sie auf dem Screenshot oben sehen können, sind die Schritte fast identisch mit denen, die wir bei AMSI angewendet haben, daher werde ich sie hier nicht noch einmal wiederholen. Unten sehen Sie einen Vorher-Nachher-Screenshot der Ausführung des Flags –etw:
Verwendung von Process Hacker zum Anzeigen der Eigenschaften von PowerShell.exe vor dem Ausführen von inlineExecute-Assembly mit dem Flag –etw
Ausführen von inline-Execute-Assembly mit dem Flag –etw
Verwendung von Process Hacker zum Anzeigen derselben PowerShell.exe-Eigenschaften nach dem Ausführen von inlineExecute-Assembly
Standardmäßig verwendet der erstellte AppDomain-, Named Pipe- oder Mail-Slot den Standardwert „TotesLegit“. Diese Werte können angepasst werden, um sich besser in die Umgebung einzufügen, die Sie testen. Dies kann entweder durch Änderung im bereitgestellten Aggressor-Skript oder über Befehlszeilenflags im laufenden Betrieb erfolgen. Ein Beispiel für die Änderung über die Kommandozeile ist unten aufgeführt:
Beispiel für InlineExecute-Assembly unter Verwendung eines eindeutigen AppDomain-Namens und eines eindeutigen Named-Pipe-Namens
Beispiel für den eindeutigen AppDomain-Namen ChangedMe
Beispiel für eine eindeutige Named Pipe LookAtMe
Beispiel für das Entfernen von AppDomain nach erfolgreicher Ausführung
Beispiel für das Entfernen einer Named Pipe nach erfolgreicher Ausführung
Dieser Abschnitt wiederholt im Wesentlichen das, was ich bereits im GitHub-Repository erwähnt habe, aber ich hielt es für wichtig, einige Punkte zu wiederholen, die Sie bei der Verwendung dieses Tools beachten sollten:
Nachfolgend einige Überlegungen zur Verteidigung: