Offensive

Extending The Covenant: Part 3

Der vorläufig letzte Blogpost über meine Arbeit am Open Source C2 Framework Covenant. Ein wichtiger neuer Task, zwei neue Grunt Templates und QoL Verbesserungen.


Dies ist der vierte Teil einer Reihe von Blogbeiträgen über unsere neuste Arbeit mit dem Open-Source-C2-Framework Covenant. In dieser Serie haben wir mehrere neue Tasks und andere Verbesserungen an Covenant vorgestellt. Obwohl es mir Spass gemacht hat, mit diesem Framework zu arbeiten und es zu erweitern, wird dies wahrscheinlich der letzte Beitrag in dieser Serie für eine Weile sein. Ich habe viel über neue Evasions gelesen und versucht, die verschiedenen Methoden und Proof-of-Concepts zu verstehen. Das hat mir gezeigt, dass ich meine Research ausweiten und gleichzeitig ein tieferes Verständnis für Prozesse, Threads, Stacks, deren Speicher, Debugging usw. entwickeln muss. Ein interessantes Thema, mit dem ich mich befassen werde, ist das Zusammenspiel von Memory Scanning, Verhaltenserkennung und Just-in-Time-Kompilierung (JIT), aber dazu später mehr.

Ich beginne diesen Beitrag mit einer Vorwarnung: Die vorgestellten neuen Funktionen sind nicht zu 100 % fertiggestellt. Beim Testen sind mir Fehler oder Verhaltensweisen aufgefallen, die ich nicht erklären kann, aber ich werde sie in den entsprechenden Abschnitten auflisten. Obwohl es derzeit nicht viele Aktivitäten der Community für Covenant gibt, kann vielleicht jemand mit mehr Erfahrung in der Fehlersuche diese Probleme lösen. Wie immer werden die Ergänzungen in unserem GitHub-Repositorium zu finden sein und Probleme oder Pull-Requests sind willkommen: Covenant-Additions

RunPE

Das Ausführen nativer Binärdateien im Speicher von .NET ist komplizierter als man denkt. Während Assemblys problemlos inline geladen werden können, sind native Binärdateien mit mehr Schwierigkeiten verbunden. Wollte ein Operator beispielsweise den Juicy Potato-Exploit verwenden, um seine Rechte auf einem anfälligen Rechner zu erhöhen, musste die native EXE auf den angegriffenen Rechner hochgeladen und dann mit einem Shell-Befehl (ShellCmd-Task) ausgeführt werden. Dies bedeutet, dass auf die Festplatte geschrieben werden muss, was AV-Warnungen auslösen kann, IoCs hinterlässt usw. und was weder ein Angreifer noch ein Operator tun möchte.

Glücklicherweise hat Nettitude RunPE auf GitHub veröffentlicht, einen reflektierenden C#-Loader für native Binärdateien. Dieser Loader kümmert sich unter anderem um die Ausführung der Binärdateien, das Parsen von Argumenten und API-Patching und kann leicht in C2-Frameworks eingebunden werden. Das GitHub-Readme erklärt einige Einschränkungen, die auch für unseren Grunt-Task eine Rolle spielen. Wenn es weitere Verbesserungen oder Updates für RunPE gibt, werde ich den Task aktualisieren, um diese Änderungen zu berücksichtigen.

runpe_example_1

Um den Task auszuführen, muss ein DecoyFilePath gewählt werden, die Binärdatei muss als Datei-Option angegeben werden (was bedeutet, dass sie vom Rechner des Operators hochgeladen wird) und es können optionale Argumente angegeben werden.

runpe_example_2

In diesem Beispiel wurde net.exe im Speicher ausgeführt, um die Mitglieder der Administratorengruppe auf dem kompromittierten System aufzuzählen. Um auf das ursprüngliche Beispiel von Juicy Potato zurückzukommen, ist hier die Ausgabe der Ausführung von JuicyPotato.exe ohne zusätzliche Argumente.

runpe_example_3

Dieser Task hätte mir in der Vergangenheit einige Kopfschmerzen erspart, und dank RunPE war es einfach in Covenant zu implementieren. Auch hier kann die YAML-Vorlage für den Task auf unserem GitHub gefunden werden. Es handelt sich im Grunde um den Quellcode von RunPE, der in einer Datei zusammengefasst ist, mit Änderungen an der Art und Weise, wie die Ausgabe zurückgegeben wird, da sie an unseren Grunt und nicht an die Konsole des kompromittierten Systems zurückgegeben werden muss. Für sein C2 SharpC2 hat Rasta Mouse zusätzlich die API-Aufrufe von kernel32.dll auf ntdll.dll gesenkt. Dies erfordert höchstwahrscheinlich eine Referenzierung von SharpSploit für diese Aufgabe in Covenant und wurde noch nicht durchgeführt.

Disposable AppDomains Grunt Template

Dieses Template lädt und führt Task-Assemblies in temporären AppDomains aus, die entladen werden, wenn der Task erledigt ist. Diese Vorlage funktioniert auch für injizierte Grunts, bei denen die Grunt-Assembly nicht von der CLR gefunden werden kann. Derzeit können lang laufende Aufgaben (z. B. ein Keylogger) nicht auf diese Weise ausgeführt werden, da ich noch keine Möglichkeit gefunden habe, die erforderliche Property aus der Assembly zu erhalten ("streamProp"). Wenn JIT, AppDomains und das Laden von Assemblies aus injizierten Prozessen bereits bekannt sind, kann zu den Bildern am Ende dieses Abschnitts gesprungen werden, um zu sehen, wie sich dieses Template verhält.

Dieses Thema beschäftigt mich schon seit geraumer Zeit. Um es etwas näher zu erläutern: Das Lesen über Speicherscanner und EDRs, ihr Fokus auf ungesicherte R(W)X-Regionen und die Techniken, die versuchen, die Erkennung durch die Scanner zu minimieren, war besorgniserregend für mich, denn als ich einen meiner Grunt-Prozesse und ihre Speicherregionen überprüfte, waren sie voll von "Private: Commit RWX"-Regionen. Die Ausführung von Moneta auf diesen Prozessen zeigte mehrere Warnungen über "Abnormal private executable memory". Wenn Moneta jedoch z. B. auf einem PowerShell-Prozess ausgeführt wird, werden die gleichen Warnmeldungen angezeigt. Dies liegt an der Just-in-Time-Kompilierung. Dieser Blogbeitrag beschreibt dies ausführlicher, aber immer noch in verständlicher Form.

Lange Rede, kurzer Sinn: Es stellte sich heraus, dass Sleep Obfuscation und das Setzen der ausführenden Speicherregion auf RW während des Sleep-Modus in .NET für mich nicht machbar ist. Ja, es gibt einen .NET Gargoyle PoC,  aber so wie ich den Code verstanden habe, lädt er mehr oder weniger nur eine Assembly über einen Timer neu, ohne Verschlüsselung. Die Verschlüsselung der .text-Region, in der die MSIL gespeichert ist, schien ebenfalls nicht machbar, da der Code für den Timer höchstwahrscheinlich ebenfalls verschlüsselt werden würde. Ich fühle mich mit der ROP-Programmierung oder anderen fortgeschrittenen Techniken noch nicht vertraut genug, um sie zu implementieren, und ich denke, es wäre schwierig, sie in einem Grunt-Template zu verwenden.

Im Vergleich zu anderen Implants ist ein Grunt jedoch im Grunde nur ein Assembly-Loader. Die Tasks sind nicht Teil des Implants, sie werden mit Roslyn kompiliert, an den Grunt gesendet und über Assembly.Load() geladen. Das bedeutet, dass der Grunt ohne Tasks eine sehr niedrige Erkennungsrate haben sollte. Die Probleme beginnen erst, wenn Tasks geladen werden. Ein grossartiger Blogbeitrag zeigt dies in Aktion und beschreibt das Problem mit dem Laden und Ausführen aller Assemblies in derselben AppDomain.

Das Problem ist: Sobald eine Assembly in eine AppDomain geladen wird, kann sie nicht mehr entladen werden und verbleibt im Speicher. Das bedeutet, dass, sobald ich zum Beispiel einen Rubeus-Task in Covenant ausführe, Rubeus nun im Speicher meines Grunt-Prozesses liegt. Ein Speicherscanner, der die ursprüngliche Ausführung möglicherweise übersehen hat, kann nun Rubeus finden und meinen Grunt beenden. Assemblies können nicht entladen werden, aber AppDomains und ihre geladenen Assemblies schon. Sie werden dann entfernt und aufgeräumt.

Sobald dies implementiert ist, tritt ein zusätzliches Problem auf. Das Erstellen einer AppDomain und das Laden einer Assembly darin erfordert, dass die CLR die "Basis"-Assembly findet, bei der es sich um die Grunt-Assembly handelt (alle diese Tests wurden mit einem stageless Grunt durchgeführt). Wenn die Grunt-Assembly mithilfe von Donut in Shellcode konvertiert und in einen Prozess injiziert wird, kann die CLR die Grunt-Assembly nicht finden, da sie sich nie auf der Festplatte befand.

Es wurden zwei Projekte gefunden, die dieses Problem angehen: Fileless von aconite33 und DarkMelkor von thiagomayllart (mit Verweis auf Accenture's CLRvoyance und deren Blogpost darüber). Fileless stützt sich auf MinHook.NET, was bedeuten würde, dass die DLL als eingebettete Ressource zum Task hinzugefügt werden muss (auf die gleiche Weise wie Powerkatz zum Mimikatz-Task), aber das hat mir bei früheren Versuchen schon viele Probleme bereitet, also habe ich mich für die Lösung von CLRvoyance entschieden. Die Lösung ist im Abschnitt "Assembly Inception" ihres Blogbeitrags besser und klarer beschrieben, als ich es könnte. Grob gesagt werden zwei CrossAppDomainDelegates verwendet, von denen einer eine Funktion aufruft, die immer aufgelöst werden kann (wie Console.Beep()), und einer unsere Funktion, die wir in der neuen AppDomain ausführen wollen. Mit Memory Patching wird der auflösbare Funktionsdelegat dann so geändert, dass er stattdessen auf unseren Funktionsdelegaten verweist.

Alles in allem ist das Verhalten des neuen Templates in den folgenden Bildern zu sehen. Mithilfe von "Create ShellCode Launcher" wird ein Grunt mit der neuen Vorlage mit temporären AppDomains erzeugt, und dieser Shellcode wird dann in einen Prozess (in diesem Beispiel notepad.exe) injiziert. Bevor dann ein Task ausgeführt wird, zeigt Process Hacker die folgenden .NET-Assemblies an:

assemblies_pre_execution

Die AppDomain "9TR3TH4C" stammt von Donut und kriegt immer einen neuen zufälligen 8-Zeichen-Stringnamen, wenn Covenant einen Shellcode-Launcher generiert. Die "ad2ej51c.fto" ist die Grunt-Assembly, die anfangs geladen wird. Ihr Name wird bei der Erstellung des Launcher ebenfalls zufällig gewählt.

Der folgende Screenshot zeigt, wie die Situation während der Ausführung eines Tasks aussieht:

assemblies_during_execution

Es kann gesehen werden, dass es eine neue AppDomain gibt, in die eine Assembly geladen wurde. Ich habe die Namenskonvention für diese temporären AppDomains von einem legitimen Softwareprodukt abgeleitet, aber das kann (und sollte) natürlich im Template geändert werden. Diese AppDomain ist nur während der Ausführung des Tasks sichtbar.

Sobald die Ausführung beendet ist, wird die AppDomain entladen und "garbage collected". Der Screenshot zeigt, dass die .NET-Assemblies so aussehen, wie sie vor der Ausführung eines Tasks aussahen:

assemblies_post_execution-1

Beim Ausprobieren dieser Vorlage sollte ein Blick auf die Registerkarte .NET Performance, insbesondere auf die Kategorie .NET CLR Loading, geworfen werden.

ApplicationRecoveryCallback Grunt Template

Ich habe dieses Template schon seit geraumer Zeit, ohne jemals einen "echten" Anwendungsfall dafür gefunden zu haben. Die Idee dafür kam von Wra7h's ARCInject Repo, wo eine neue Injektionsmethode mit Applicatio Recovery Callbacks vorgestellt wurde, die ausgeführt werden, wenn ein Prozess nicht mehr reagiert. Da ich mehrere Fälle erlebt habe, in denen ein Grunt-Prozess nicht mehr reagiert, dachte ich, dass dies ein praktischer Wiederherstellungsmechanismus sein könnte. Ich habe dieses Template bisher nur in einem Lab verwendet, aber es ermöglicht mir im Wesentlichen, darauf zu reagieren, dass ein Grunt nicht mehr reagiert und einen neuen Grunt vom C2-Server herunterzuladen und auszuführen.

Der Mechanismus ist interessant und ich bin sicher, dass es Anwendungsfälle gibt, in denen dies nützlich sein könnte. Weitere Informationen über die Implementierung sind im Repo von Wra7h oder im Code des Grunt-Templates zu finden.

arc_werfault

Aktualisierbarer Listener Web Log

Die Listener in Covenant führen ein Webprotokoll der empfangenen Anfragen. Jeder Listener schreibt seine Ereignisse in eine separate Protokolldatei. Der Bediener kann diese Webprotokolleinträge in den Details einsehen, allerdings werden die neuesten Ereignisse auf der letzten Seite aufgeführt und nicht automatisch aktualisiert. Wenn ich also überprüfen wollte, ob es ein neues Ereignis gab (z. B. eine gehostete Datei wurde angefordert), musste ich die Seite aktualisieren, die entsprechende Registerkarte aufrufen und dann zur letzten Seite gehen, um es zu sehen.

Um diesen Vorgang zu vereinfachen, habe ich eine Aktualisierungsschaltfläche hinzugefügt und die Reihenfolge der Protokolle umgestellt. Die neuesten Ereignisse werden nun ganz oben aufgeführt, und die Liste kann aktualisiert werden, ohne dass die Seite neu geladen werden muss. Um die geänderte .razor-Datei in einer bestehenden Covenant-Instanz zu verwenden, muss die Datei ersetzt und beim nächsten Start "dotnet clean; dotnet run" als Befehl genutzt werden. Da ich Covenant nicht mit Docker verwende, kann ich den dafür notwendigen Befehl nicht zur Verfügung stellen.

refreshable_web_log

Neues Layout für Dashboard

Mit dem neuen Diagramm-Layout (siehe unseren vorherigen Blogbeitrag), einem aktualisierbaren Webprotokoll und meiner persönlichen Vorliebe, dass ein Dashboard nicht über die Fensterhöhe hinausgehen sollte, habe ich das Dashboard von Covenant so angepasst, dass es meinen Bedürfnissen besser entspricht.

Das Diagramm ermöglicht einen schnellen Überblick, das Aufgabenprotokoll unterstreicht die Tatsache, dass Covenant als kollaborative C2-Plattform geschaffen wurde, und das Webprotokoll zeigt die Anfragen an den gerade aktiven Listener.

dashboard_layout

Ausblick

Wie ich am Anfang dieses Blogposts geschrieben habe, wird dies für eine Weile der letzte Blogpost zu Covenant sein. Ich bin weiterhin im #covenant-Kanal auf dem BloodHoundGang Slack aktiv und werde versuchen zu helfen, wenn es neue Issues auf Covenant's GitHub gibt. Ich hoffe, dass das Repo bald wieder aktualisiert wird und einige Pull Requests in den Dev-Branch einfliessen. Covenant bietet eine Menge Funktionalität und die Art und Weise, wie Tasks geladen und ausgeführt werden, ist super. Es macht Spass, neue Ergänzungen und Tasks zu entwickeln, so dass ich wirklich hoffe, dass Covenant nicht langsam mehr und mehr aufgegeben wird.

Ich werde mich bei meiner zukünftigen Research auf die Wechselwirkungen zwischen Speicherscannern, Verhaltenserkennung und JIT-kompiliertem Code konzentrieren. Ich bin sicher, dass mich das früher oder später wieder zu Covenant führen wird!

Danke fürs Lesen! Wie immer gilt: Falls es Fragen oder Rückmeldungen gibt, können diese gerne per Mail an research@avantguard.io oder im BloodHoundGang Slackchannel an mich (Username @jannlemm0913) direkt gestellt werden.

Similar posts