Bypass

Overload Mapping vs. Memory Scanner

Untersuchungen und weiterführende Gedanken zu In-Memory Execution und Detection.


Manchmal geht es als Angreifer im Cyberspace nicht darum, einen Verteidigungsmechanismus vollständig auszuhebeln, sondern nur darum, die Chance auf eine Entdeckung möglichst gering zu halten. Da die Verteidigerseite nicht schläft und fortlaufend neue Detektionen und Mechanismen entwickelt, muss auch von der Angreiferseite Research betrieben werden. Wir bei avantguard cyber security sind stets darauf bedacht, unsere Werkzeuge und Prozesse weiterzuentwickeln, um mit den Bedrohungen von heute, aber auch von morgen mithalten zu können. Eines unserer wichtigsten und liebsten Werkzeuge ist Covenant, der Open-Source-Teil unserer Command & Control (C2) Infrastruktur, welcher z.B. auch von der APT Gruppe HAFNIUM eingesetzt wird. Covenant bietet einem Operator bereits viele Möglichkeiten, Werkzeuge nur im Speicher ("In-Memory") auszuführen. Diese Methode ist in den letzten Jahren bei Angreifern beliebt worden, da dadurch nichts auf die Festplatte geschrieben werden muss und Antivirenprogramme die schädlichen Dateien gar nicht scannen können. Dies führt dazu, dass Verteidiger nun auch den Speicher von möglicherweise bösartigen Prozessen untersuchen und überwachen müssen. Immer mehr EDRs nutzen deshalb sogenannte Memory Scanner.

Dieser Blogbeitrag beschreibt unsere derzeitigen Untersuchungen zu Covenant und Memory Scannern und richtet sich insbesondere an Blue Teams und andere Red Teams. Viele Konzepte werden nicht vorgängig erläutert, da der Beitrag ansonsten einen zu grossen Umfang annehmen würde. Es wird ein mögliches Vorgehen erläutert, mit dem gewisse Funktionalitäten in Covenant von Memory Scannern weniger entdeckt werden könnten, ohne bereits einen funktionierenden PoC dazu erstellt zu haben. Fragen und Rückmeldungen sind erwünscht und können an research@avantguard.io gesendet werden.

Covenant nutzt SharpSploit, welches wiederum DInvoke nutzt. DInvoke erlaubt es Entwicklern, gewisse Funktionalitäten in C# so zu implementieren, dass sie von EDR-Agents schwieriger zu detektieren sind. Eine solche Funktionalität ist das Laden und Ausführen von Funktionen in einer DLL. Dies wird durch zwei unterschiedliche Arten ermöglicht:

  • Manual Mapping: Die benötigte DLL wird in eine freie Speicherregion geschrieben (Mapping) und die Funktionen können anschliessend aufgerufen werden. Der Vorteil dieses Vorgehens ist, dass es relativ "simpel" und verständlich ist und trotzdem auf vielen Systemen ausreichend unentdeckt bleiben kann. Der Nachteil ist, dass die DLL eigentlich keinen Grund hat, im Speicher zu sein. Die Speicherregion wird mit einer DLL gefüllt, welche entweder nicht auf dem zugrunde liegenden Filesystem vorhanden ist (sondern als Referenz in der ausführbaren Datei, welche den Prozess gestartet hat), oder die DLL ist bereits einmal im Speicher und wurde jetzt ein zweites Mal vom Filesystem geladen, um API Hooks zu umgehen. Beide Fälle sind für Memory Scanner Indizien von bösartigem Code. Daher bietet DInvoke eine zusätzliche Art des Mappings an.
  • Overload Mapping: Hier wird vor dem Laden der benötigten DLL eine mindestens gleich grosse legitime DLL gesucht, meistens in C:\Windows\System32. Legitim bedeutet hier, dass die DLL auf dem Filesystem vorhanden ist und im Prozess noch nicht genutzt wird. Weiter kann auch vorgegeben werden, dass die DLL signiert sein muss, da signierte Dateien manchmal weniger strikt überprüft werden als unsignierte. Die legitime DLL wird dann in den Speicher geladen. Das ist an sich noch nichts Ungewöhnliches, jedoch kommt jetzt das eigentliche Overload Mapping: Die DLL, dessen Funktionen eigentlich benötigt werden, wird jetzt in die Speicherregion der legitimen DLL überschrieben. Deshalb muss die legitime DLL mindestens so gross wie die benötigte DLL sein, damit genügend Platz zum Überschreiben vorhanden ist. Der Prozess ist in einer Antwort auf einen Issue auf dem Covenant GitHub mit zusätzlichen Screenshots erklärt.

Manual Mapping vs. Overload Mapping

Für Memory Scanner wird es somit bereits schwieriger zu bestimmen, ob eine DLL in einem Prozess legitim ist oder für bösartige Zwecke genutzt wird. Glücklicherweise gibt es aber Tools, die dies bestimmen können. Beispiele dafür, welche wir bereits genutzt haben, sind pe-sieve und Moneta. Beide sind Open Source und auf GitHub verfügbar. Als Beispiel soll ein laufender Prozess eines Implants (Covenant nennt diese Grunts) mit pe-sieve untersucht werden, bevor und nachdem eine DLL via Overload Mapping in den Prozessspeicher aufgenommen wurde.

pe-sieve vor Mimikatz

Der obige Ausschnitt zeigt die Ergebnisse von pe-sieve für einen Prozess, welcher via Doppelklick eines ausführbaren Grunt-Implants gestartet wurde. Der Antivirus auf dem Testsystem blockiert weder die Datei noch den entstandenen Prozess. Pe-sieve meldet drei verdächtige Befunde, welche für die derzeitige Untersuchung jedoch noch nicht relevant sind. Nun wird vom C2-Server der Befehl an das Implant gesendet, Mimikatz auszuführen. Konkret bedeutet dies, dass von SharpSploit Methoden in Mimikatz.cs und Overload.cs genutzt werden, um eine DLL vom Filesystem zu laden und die referenzierte "powerkatz_x64.dll" DLL in den Speicher überschrieben wird. Dann wird der gewünschte Befehl mithilfe der DLL ausgeführt. Nach diesem Ablauf gibt ein Scan mit pe-sieve folgende Werte zurück:

pe-sieve nach Mimikatz

Auffällig ist, dass sich der Wert von "Replaced" von null auf eins verändert hat. Dies macht durchaus Sinn, da ja eine DLL geladen und mit einer anderen DLL überschrieben wurde. Wie genau die Änderung festgestellt wird, ist uns aus ersten Nachforschungen im Quellcode noch nicht ganz klar geworden. Es sieht danach aus, als ob das Tool die exportierten Funktionen der DLL auf dem Filesystem mit denen der DLL im Speicher vergleicht (ob da noch ein anderer Weg für Verschleierungen lauert?), dies wäre jedoch mit einem ersten PoC genauer zu testen. Dies waren soweit aber mal unsere ersten Erkenntnisse.

Um jetzt zum eigentlichen Punkt dieses Beitrags zu kommen, müssen wir einen Schritt zurückgehen. Wie aus den beiden obigen Screenshots ersichtlich ist, meldet pe-sieve nebst "Replaced" auch andere verdächtige Befunde. Obwohl das Implant so wie es zurzeit ist bereits relativ unentdeckt bleiben kann, gibt es da Verbesserungspotential, welches wir ausschöpfen wollen. Ein Ansatz, um Memory Scanner zu täuschen, wurde von mgeeky als PoC auf seinem GitHub veröffentlicht als ShellcodeFluctuation. Der PoC bezieht sich zwar auf das C2 Cobalt Strike, hat uns aber auch für Covenant zum Denken angeregt. Cobalt Strike ist Closed Source, aber sehr modular und bildet einen weiteren Teil unserer C2-Infrastruktur. ShellcodeFluctuation macht grob gesagt folgendes: Während das Implant nicht genutzt wird, also keine Befehle ausgeführt werden, sondern das Implant einfach im Speicher "schläft", wird der Code des Implants verschlüsselt. Beim "Aufwachen", also wenn das Implant wieder genutzt werden soll, wird der Code entschlüsselt und kann wieder ausgeführt werden. Nach erfolgter Ausführung wird wieder verschlüsselt und geschlafen. Die Speicherregion des Codes wird zudem als nicht-ausführbar gekennzeichnet. Ein Memory Scanner, welcher während der Schlafphase den Speicher überprüft, sieht also nur unlesbaren Code in einer nicht-ausführbaren Speicherregion. Dies mag merkwürdig erscheinen, wird jedoch beispielsweise von pe-sieve nicht als verdächtig eingestuft. Da die Schlafphase zudem oftmals deutlich länger dauert als das eigentliche Ausführen von Befehlen ist deshalb das Entdeckungsrisiko deutlich reduziert, wenn auch nicht ganz behoben.

Die Lösung von ShellcodeFluctuation ist so einleuchtend und elegant, dass wir uns gefragt haben: Geht so etwas nicht auch für Overload Mapping? Und mit unserem jetzigen Wissensstand behaupten wir: Ja, auch beim Overload Mapping sollte es möglich sein, das Entdeckungsrisiko durch Veränderung des Speichers während der Laufzeit deutlich zu reduzieren. Eine mögliche Lösung könnte wie folgt aussehen:

  • Wir nehmen als Beispiel an, dass es auf dem Filesystem auf dem unser Implant läuft die DLL "win.dll" gibt. Diese ist signiert, wird von Prozessen manchmal benutzt und ist alles in allem nicht auffällig oder für bösartige Verwendungszwecke bekannt.
  • Vom C2-Server wird uns Code zugesandt, welcher ausgeführt werden soll und "powerkatz.dll" referenziert, welches nicht signiert ist und für bösartige Verwendungszwecke bekannt ist.
  • Das Implant führt den Code aus. Via Overload Mapping soll win.dll geladen und mit powerkatz.dll überschrieben werden. Jedoch soll vor dem Überschreiben ein weiterer Schritt gemacht werden: win.dll und powerkatz.dll sollen via XOR einen Schlüssel generieren. Nachfolgend wird eine vereinfachte Darstellung gezeigt werden, was im Speicher passieren soll:

DLLs XOR im Speicher

Der Schlüssel (von nun an key genannt) ist gleich gross wie powerkatz.dll. Im Moment ist immer noch win.dll in der Speicherregion, welche überschrieben werden soll. Dank den Eigenschaften von XOR kann nun win.dll XOR key ausgeführt werden, um powerkatz.dll in die gewünschte Speicherregion schreiben zu können.

XOR veranschaulicht

Dies wird nur dann gemacht, wenn wir eine exportierte Funktion der powerkatz.dll benötigen. Sobald die Funktion durchgeführt wurde, wird nun wieder powerkatz.dll XOR key ausgeführt. Was schlussendlich wieder in der Speicherregion steht, ist win.dll.

Ablauf in Speicherregion und Memory Scanner Befunde

Ein PoC wurde wie bereits erwähnt noch nicht implementiert. Die entsprechenden Stellen im Quellcode sollten zwar bekannt sein, jedoch wurde der Entwicklungsaufwand zum Zeitpunkt dieses Blogbeitrags noch nicht geleistet. Weitere Tests wären dann nötig, um die tatsächliche Effektivität dieses Vorgehens zu evaluieren. Offene Fragen sind beispielsweise:

  • Wird durch das Zurücktransformieren der ursprünglichen DLL der Memory Scanner wirklich getäuscht?
  • Wird durch das dynamische Überschreiben der DLL ein anderes verdächtiges Verhalten festgestellt?
  • Wie reagieren verschiedene Memory Scanner auf dieses Vorgehen?

Die avantguard cyber security ist froh und stolz, Research und Weiterentwicklungen eine hohe Priorität beimessen zu können. Nur so können die heutigen Bedrohungen verstanden und simuliert werden. Wir möchten uns bei allen bedanken, die trotz des eher trockenen Themas bis hierher gelesen haben und werden demnächst wieder einen Beitrag veröffentlichen, der eine grössere Zielgruppe anspricht. Hier möchten wir erneut nach Fragen und Rückmeldungen an research@avantguard.io bitten, denn Research profitiert von und macht am meisten Spass mit Wissens- und Erfahrungsaustausch!

Similar posts