Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
MemFiles — Ein CobaltStrike-Toolkit, um von Beacon erzeugte Dateien in den Speicher statt auf die Festplatte zu schreiben. | Kitploit
Tools/GitHubGitHub/octoberfest7/memfiles
DatenexfiltrationPost-ExploitationCommand and ControlRed Teaming
GitHuboctoberfest7/memfiles

MemFiles

Ein CobaltStrike-Toolkit, um von Beacon erzeugte Dateien in den Speicher statt auf die Festplatte zu schreiben.

Repository anzeigen
47762vor 2 JahrenVon Kitploit geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

MemFiles

Haftungsausschluss:

Dieses Projekt ist komplex; wenn du nicht verstehst, wie es funktioniert, und es nicht ausreichend testest, kann das dazu führen, dass du Beacons zum Absturz bringst und den Zugriff verlierst!

Ich empfehle dir dringend, die gesamte Dokumentation bis zum Abschnitt „Technische Details, Designüberlegungen und Kommentare“ zu lesen!

Einführung

MemFiles ist ein Toolkit für CobaltStrike, das Operatoren ermöglicht, Dateien, die vom Beacon-Prozess erzeugt werden, in den Speicher zu schreiben, anstatt sie auf der Festplatte des Zielsystems abzulegen. Es wurde erfolgreich auf Windows 7, 10 und 11 getestet; die entsprechenden Serverversionen sollten ohne Probleme funktionieren. MemFiles ist auf x64-Beacons beschränkt.

Dies wird erreicht, indem mehrere verschiedene NtAPIs innerhalb von NTDLL.dll gehookt und Aufrufe an diese APIs auf Funktionen umgeleitet werden, die in den Speicherraum des Beacon-Prozesses injiziert wurden.

MemFiles setzt eine saubere/ungehookte Kopie von NTDLL im Beacon-Prozess voraus. Es wird keine Garantie dafür übernommen, dass MemFiles in einem Beacon-Prozess funktioniert, in dem noch EDR-Hooks vorhanden sind. Repariere/aktualisiere NTDLL, bevor du MemFiles verwendest!

Innerhalb des MemFiles-Toolkits wird ein „spezielles“, nicht vorhandenes Verzeichnis definiert; alle Dateien, die in dieses spezielle Verzeichnis geschrieben werden, werden von MemFiles erfasst und in den Speicher geschrieben, von wo sie anschließend auf den Teamserver heruntergeladen werden können.

MemFiles ist mit den meisten (nicht allen) Tools kompatibel, die innerhalb des Beacon-Prozesses ausgeführt werden und die angewiesen werden können, ihre Ausgabe in ein bestimmtes Verzeichnis zu schreiben. Es sind keine erhöhten Rechte erforderlich.

Dazu gehören:
-BOFs
-.NET-Assemblys, die inline mit etwas wie inline-executeAssembly ausgeführt werden
-PEs, die inline mit etwas wie Inline-Execute-PE ausgeführt werden

Alle diese sind kompatibel, weil sie innerhalb des Beacon-Prozesses ausgeführt werden, in dem die relevanten NtAPIs gehookt wurden.

MemFiles funktioniert NICHT mit Dingen wie:
-execute-assembly
-shell
-run

Keines davon ist kompatibel, weil sie alle andere Prozesse starten, deren NtAPIs NICHT gehookt wurden.

MemFiles wurde erfolgreich mit Tools wie Rubeus, SharpHound, Procdump und Powershell getestet, wenn sie innerhalb des Beacon-Prozesses ausgeführt werden.

Einrichtung

Klon das Repository und ändere optional die Variable hookdir, die in Zeile 56 in /PIC/Source/NtCreateFile.c und /PIC/Source/NtOpenFile.c definiert ist. Diese Variable ist das „spezielle“ Verzeichnis, das MemFiles signalisiert, dass es die erstellte Datei abfangen soll. Die Variable hookdir ist standardmäßig auf „redteam“ gesetzt. Stelle sicher, dass diese Variable kein reales Verzeichnis auf dem Zielsystem ist und dass sie in beiden Dateien identisch ist!

image

Führe 'make all' aus, um sowohl die erforderlichen BOFs als auch die PIC-Funktionen zu kompilieren.

Lade MemFiles.cna in den CobaltStrike-Client. Stelle sicher, dass das Verzeichnis, aus dem CobaltStrike ausgeführt wird, für deinen Benutzer beschreibbar ist; MemFiles erstellt dort eine Textdatei (memfiles.txt), um die Verfügbarkeit der für MemFiles erforderlichen Daten sicherzustellen.

MemFiles kann so konfiguriert werden, dass es in jedem neuen Beacon installiert wird, der sich beim Teamserver meldet; dies wird über den Menüpunkt MemFiles->Config erreicht. Standardmäßig installiert sich MemFiles NICHT automatisch in neuen Beacons. Beachte, dass dies eine globale Einstellung ist; wenn zwei Clients mit dem Teamserver verbunden sind und beide MemFiles.cna geladen haben, wirkt sich das Umschalten der Einstellung „Install on beacon initial“ durch Client A auch auf Client B aus!

image

Befehle

MemFiles besteht aus 4 targetorientierten Befehlen, die BOFs ausführen, und 1 internem Befehl, der die Projektdatenstruktur manipuliert.

Targetorientiert:

  1. meminit
  2. memlist
  3. memfetch
  4. memclean

Interne Datenstruktur:

  1. memtable

meminit

meminit ist dafür verantwortlich, MemFiles im Beacon-Prozess zu installieren.

Die Liste der von MemFiles gehookten NtAPIs lautet wie folgt:

  1. NtCreateFile
  2. NtWriteFile
  3. NtClose
  4. NtQueryVolumeInformationFile
  5. NtQueryInformationFile
  6. NtSetInformationFile
  7. NtOpenFile
  8. NtReadFile
  9. NtFlushBuffersFile

meminit führt die folgenden wichtigsten Aktionen aus:

  1. Sendet eine positionsunabhängige Ersatzfunktion für jede gehookte NtAPI an den Beacon
  2. Erstellt eine Struktur im Beacon-Speicher, um verschiedene Werte zu halten, die MemFiles während seines Lebenszyklus benötigt
  3. Patcht die Adresse dieser Struktur in jede der PIC-Ersatzfunktionen
  4. Allokiert Speicher und injiziert jede PIC-Ersatzfunktion in den Speicher des Beacon-Prozesses
  5. Erstellt für jede gehookte NtAPI einen Trampolin
  6. Hookt jede aufgeführte NtAPI, indem einige/alle Bytes überschrieben werden und die Ausführung auf die PIC-Ersatzfunktion umgeleitet wird.

memlist

memlist wird verwendet, um alle Dateien anzuzeigen, die von MemFiles für einen bestimmten Beacon derzeit im Speicher abgelegt sind.
image
Es werden mehrere Felder angezeigt; für den Benutzer am relevantesten und interessantesten sind der Name der Datei und die Länge der gespeicherten Daten.

memfetch

memfetch wird verwendet, um tatsächlich Dateien abzurufen, die von MemFiles für einen bestimmten Beacon im Speicher abgelegt wurden.

Standardmäßig ruft memfetch alle Dateien ab, die von MemFiles gespeichert wurden und deren „Handle“ geschlossen wurde. Diese Designentscheidung wurde getroffen, um Probleme zu vermeiden, die beim Herunterladen einer Datei auftreten können, die ein Programm/eine Anwendung noch nicht fertig geschrieben hat.

Das bedeutet: Wenn ein Programm/eine Anwendung das Handle, das es zur Datei geöffnet hat, nicht schließt, wird die Datei von memfetch nicht heruntergeladen.
Dies kann durch Verwendung des Arguments „force“ mit memfetch umgangen werden, also 'memfetch force', um alle Dateien unabhängig vom Status ihres Handles aus dem Speicher abzurufen.

Dateien, die memfetch aus dem Speicher abruft, werden als Download an den Teamserver gesendet und können über die Registerkarte „Downloads“ in CobaltStrike vom Teamserver auf den Client synchronisiert werden.

Sobald eine Datei vom Teamserver heruntergeladen wurde, wird sie aus dem Speicher des Beacon-Prozesses gelöscht und ihr Eintrag, der über memlist angezeigt wird, entfernt.

memclean

memclean ist dafür verantwortlich, MemFiles aus einem Beacon-Prozess zu entfernen und aufzuräumen.

Der Standard-Anwendungsfall für MemFiles besteht darin, es zu installieren und für die Lebensdauer des Beacons installiert zu lassen; sollte man MemFiles jedoch zusammen mit einem Tool verwenden wollen, um Dateiausgaben zu erfassen und abzurufen, und MemFiles anschließend deinstallieren wollen, damit seine Artefakte nicht im Speicher verbleiben, kann memclean verwendet werden, um den Beacon-Prozess in seinen ursprünglichen Zustand vor der Ausführung von meminit zurückzuversetzen.

Das beinhaltet:

  1. Das Entfernen der Hooks jeder gehookten NtAPI
  2. Das Nullsetzen und Freigeben jedes erstellten Trampolins
  3. Das Nullsetzen und Freigeben jeder injizierten PIC-Ersatzfunktion
  4. Das Nullsetzen und Freigeben der MemFiles-Struktur

Beachte, dass memclean vor diesen Aktionen alle von MemFiles im Speicher abgelegten Dateien zwangsweise herunterlädt. Wenn man MemFiles mit einem einzelnen Tool verwenden und dann entfernen möchte, kann man memfetch überspringen und einfach memclean verwenden, um sowohl die Dateien abzurufen ALS AUCH MemFiles in einem Schritt aus dem Beacon-Prozess zu entfernen.

memtable

memtable wird verwendet, um Informationen über Beacons anzuzeigen und zu verfolgen, in denen MemFiles derzeit installiert ist. Es zeigt auch globale Konfigurationsinformationen an.

Jeder CobaltStrike-Client hat seine eigene memtable; MemFiles unternimmt große Anstrengungen, um die Synchronität seiner Daten zwischen allen verbundenen CobaltStrike-Clients sicherzustellen, damit MemFiles von allen Operatoren in allen Beacons verwendet werden kann. Mehr dazu unter „Designüberlegungen und Kommentare“.

image

Verwendung

Initialisiere MemFiles in einem Beacon mit dem Befehl meminit. Dies kann so konfiguriert werden, dass es automatisch geschieht, indem die Option im Menü MemFiles->Config umgeschaltet wird.

image

Wenn MemFiles initialisiert ist, kannst du nun deine bevorzugten Tools verwenden, um Dateien in den Speicher zu schreiben! Wie du das tust, hängt vom jeweiligen Tool ab; einige erlauben es, ein Verzeichnis anzugeben, in das mehrere Dateien ausgegeben werden, während andere einen absoluten Pfad für eine einzelne Datei zulassen, die vom Tool erstellt wird. Einige Beispiele findest du unten:

SharpHound:

Hier geben wir an, dass SharpHound alle erzeugten Dateien in das Verzeichnis c:\redteam\ (unser spezielles MemFiles-Verzeichnis) ausgeben und die Dateien nicht zippen soll; MemFiles unterstützt es nicht, dass Programme Dateien aus dem Speicher lesen, sondern nur schreiben, daher funktioniert die Zip-Funktionalität in SharpHound nicht.

image

Rubeus:

Der Befehl „dump“ wird mit Rubeus verwendet, und wir weisen es an, die gesamte Konsolenausgabe in eine Datei (in unserem speziellen Verzeichnis) zu senden.

image

Powershell:

In diesem Beispiel wird Inline-Execute-PE verwendet, um powershell.exe in den Beacon-Prozess zu laden und 'Get-ADUser' auszuführen, um eine Liste der Domänenbenutzer abzurufen. Mit einer Pipe und 'out-file' können die Daten in den Speicher geschrieben und dann abgerufen werden.

image

Wenn du deine Dateien abrufen möchtest, führe memfetch aus:

image

Wenn du mit MemFiles fertig bist und/oder es nicht in einem Beacon-Prozess installiert lassen möchtest, führe memclean aus:

image

Beachte, dass es im obigen Beispiel eine Datei gab, die noch nicht heruntergeladen worden war; memclean lädt diese Datei herunter und entfernt sie aus dem Speicher, bevor MemFiles deinstalliert wird.

Frage den Status und die Konfiguration von MemFiles mit memtable ab. Entferne bei länger andauernden Operationen Einträge von toten/alten Beacons aus der memtable, um Unübersichtlichkeit zu vermeiden.

image

Fähigkeiten und Einschränkungen

Wie in der Einführung betont, benötigt MemFiles eine saubere Kopie von NTDLL im Beacon-Prozess, um zu funktionieren. Dies ist notwendig, weil es die ursprünglichen Bytes in der NtFunction liest und bestimmte davon auf den Trampolin kopiert, der später verwendet wird, um normale Aufrufe an die NtFunction abzuschließen, in die MemFiles nicht eingreifen sollte. Dieses Thema wird im Abschnitt „Technische Details, Designüberlegungen und Kommentare“ näher behandelt.

MemFiles tätigt eine anfängliche Allokation von 1048576 Bytes für jede Datei; wenn Daten in den Speicher geschrieben werden, kann und wird es diese Allokation bei Bedarf erweitern, um größere Dateien aufzunehmen.

Der in der MemFiles-Struktur gespeicherte Dateiname wird aus einem Argument extrahiert, das an die Ersatzfunktion NtCreateFile übergeben wird. MemFiles geht dabei ziemlich einfach vor: Es lokalisiert das „spezielle“ Verzeichnis im Dateipfad-Argument, springt an dessen Ende und erhöht den Zeiger um 1, um das Zeichen '\' zu berücksichtigen, das das „spezielle“ Verzeichnis vom Dateinamen trennt. Im Pfad 'C:\users\tom\redteam\myfile.txt' lokalisiert MemFiles beispielsweise 'redteam', berücksichtigt das Backslash-Zeichen und wählt 'myfile.txt' als Dateinamen aus.

MemFiles kümmert sich nicht um vorangestellte Verzeichnisse im Dateipfad; 'C:\redteam\myfile.txt' und 'c:\users\tom\appdata\local\redteam\myfile.txt' sind aus Sicht von MemFiles gleichermaßen gültige Pfade.

Angesichts der Art und Weise, wie MemFiles Dateinamen aus Dateipfaden extrahiert, sollte beachtet werden, dass MemFiles die Erstellung von Unterverzeichnissen nicht unterstützt. Das bedeutet, dass MemFiles nicht richtig mit Tools funktioniert, die beispielsweise c:\redteam\mynewdir\file1.txt, c:\redteam\mynewdir\file2.txt, c:\redteam\mysecondir\file3.txt usw. erstellen.

Die von MemFiles gehookten NtAPIs wurden als solche identifiziert, die von verschiedenen Programmen für E/A-Operationen verwendet werden. Wie bereits erwähnt, wurden die folgenden Tools/Fähigkeiten erfolgreich mit MemFiles/diesem Satz gehookter NtAPIs getestet: SharpHound, Rubeus, Powershell, Procdump, BOFs, generische C-Programme, die Dateischreiboperationen durchführen, sowie der Befehl bupload_raw von CobaltStrike (der es dem Operator ermöglicht, den entfernten Dateispeicherort anzugeben). Es gibt sicherlich andere Tools, die ohne Weiteres mit MemFiles funktionieren; andere werden inkompatibel sein.

Auf den ersten Blick wirkt der Windows-Dateierstellungsprozess unkompliziert: NtCreateFile->NtWriteFile->NtClose. Kurz nachdem ich mich in dieses Projekt eingearbeitet hatte, stellte ich fest, dass eine Reihe anderer APIs beteiligt sind, und zu allem Überfluss unterscheiden sich die beteiligten APIs je nach Programm. Einige Programme rufen im Rahmen ihres E/A-Prozesses die Win32-API SetFilePointerEx auf, die wiederum die NtAPI NtSetInformationFile aufruft. Andere, wie .NET-Programme einschließlich SharpHound, rufen letztendlich NtFlushBuffersFile auf.

Das Fehlen einer einzigen gemeinsamen Kette von API-Aufrufen zum Erstellen und Schreiben von Dateien eröffnet Raum für Inkompatibilitäten, je nachdem, mit welchem Tool MemFiles verwendet wird. Ein Programm könnte eine andere NtAPI aufrufen, die MemFiles nicht gehookt hat, dabei das von MemFiles in der Ersatzfunktion NtCreateFile erzeugte Fake-Handle übergeben, was zu einem Fehler „Invalid Handle“ führt und die Ausführung stoppt. Andere Programme führen komplexere Aktionen aus, die MemFiles nicht ausreichend vortäuschen/ersetzen kann. Eine solche bereits identifizierte Inkompatibilität ist ADExplorer.exe.

ADExplorer.exe ist eine signierte Microsoft-Binärdatei, die für die Active-Directory-Enumeration verwendet wird. Zur Laufzeit schreibt ADExplorer Daten in die angegebene Ausgabedatei und greift später wieder darauf zu, bevor es schließlich am Ende der Ausführung die endgültige Ausgabe in die Datei schreibt. Da es die Ausgabedatei gewissermaßen als Cache verwendet, muss es in der Lage sein, die Daten zu lesen, die es bereits in die Datei geschrieben hat, und die Wahrscheinlichkeit ist sehr gering, dass dies auf einfache, vorhersehbare Weise geschieht.

MemFiles unterstützt derzeit nicht, dass In-Memory-Dateien von Programmen oder Anwendungen gelesen werden, aber das sollte mit einer weiteren Ausarbeitung der benutzerdefinierten NtReadFile-Funktion und einigen zusätzlichen Variablen/Datenverfolgungsfeldern, die zur MemFiles-Struktur hinzugefügt werden, möglich sein.

ADExplorer stellt eine weitere Herausforderung in Bezug auf die Größe der erzeugten Dateien dar. In großen Unternehmensumgebungen kann die Ausgabedatei 1 GB überschreiten; obwohl MemFiles dies programmatisch bewältigen können sollte, ist es sicherlich nicht für solche Anwendungsfälle gedacht.

Inkompatible Tools

Zweifellos wird die Community Tools entdecken, mit denen MemFiles nicht ordnungsgemäß funktioniert; ich ermutige dich, ein Issue zu eröffnen, in dem das inkompatible Programm/Tool und die Umstände, unter denen du es ausgeführt hast, beschrieben werden, z. B. ob es sich um ein BOF handelt, über inline-executeAssembly, Inline-Execute-PE usw., damit ich sehen kann, ob ich MemFiles erweitern und zum Laufen bringen kann.

IOCs und AV/EDR

IOCs im Zusammenhang mit MemFiles umfassen unter anderem:

Allokieren von Speicher mit VirtualAlloc
Schreiben von Daten mit WriteProcessMemory
Ändern des Speicherschutzes auf allokiertem Speicher zwischen RW und RX
Überschreiben von Speicher in NTDLL.dll

AV/EDR

MemFiles wurde nicht gegen ein echtes EDR entwickelt oder getestet; Microsoft Defender war das, was zur Verfügung stand. Abgesehen davon würde ich sagen, dass das Tool/Programm, das Beacon zur Erzeugung einer Datei ausführt, mit höherer Wahrscheinlichkeit eine Warnung auslöst als MemFiles beim Erfassen oder Speichern dieser Datei im Speicher. Das Überschreiben von Speicher in NTDLL/das Hooken der NtAPIs kommt mir wie etwas vor, mit dem einige Produkte Probleme haben könnten, aber ich habe keine Belege, die dies untermauern. Bei Aufrufen gehookter NtFunctions, die keine Dateien betreffen, die von MemFiles erfasst werden sollen, wird der Syscall weiterhin aus dem Adressraum der NTDLL.dll heraus ausgeführt, da Sicherheitsprodukte Syscalls erkennen und darauf aufmerksam machen, die von außerhalb dieses Bereichs stammen.

Es sollte beachtet werden, dass von MemFiles im Speicher abgelegte Dateien NICHT kodiert oder verschlüsselt sind; diese Funktion könnte hinzugefügt werden, wenn ein realer Anwendungsfall identifiziert wird, in dem AV/EDR eine erzeugte Datei im Speicher meldet.

Technische Details, Designüberlegungen und Kommentare

Ich wurde vor einigen Monaten auf dem Tradecraftcon von KFiveFour durch einen Konferenzvortrag zum ersten Mal mit dem Konzept eines In-Memory-Dateisystems vertraut gemacht, bei dem ein Sprecher (@DexterGerig) einen POC demonstrierte, der ein In-Memory-Dateisystem mit einem Client-Server-Modell erstellte. Die Hälfte der von mir erdachten Funktionalität eines solchen Projekts wurde durch meine letzte große Veröffentlichung, Inline-Execute-PE, abgedeckt. Die andere Hälfte – die Idee, von Tools erzeugte Dateien erfassen und im Speicher statt auf der Platte ablegen zu können – wurde durch dieses Projekt nicht erfüllt und blieb aus offensichtlichen Gründen eine äußerst wünschenswerte Fähigkeit.

MemFiles war für mich eine unglaublich herausfordernde Aufgabe, da ich vor diesem Projekt nur sehr wenig Zeit mit einem Debugger verbracht hatte, kein Verständnis für Assembly hatte und API-Hooking nicht verstand. Während dieses Projekts stieß ich auf mehrere 10-20-stündige Hindernisse, die ich dank Ausdauer glücklicherweise überwinden konnte. Auch wenn es wahrscheinlich nicht der effizienteste Weg war, habe ich viel Vertrautheit mit Debuggern gewonnen und ein besseres Verständnis dafür entwickelt, wie Computer im Inneren funktionieren – in Bezug auf Assembly, Register, den Stack und Aufrufkonventionen.

Im Folgenden findest du einen technischen Deep Dive zu einigen der wichtigsten technischen Details und Designüberlegungen, die in MemFiles eingeflossen sind.

Dateien in Windows und wie MemFiles funktioniert

Die Dateierstellung unter Windows beginnt mit NtCreateFile, dem der Pfad der gewünschten Datei übergeben wird; im Gegenzug erstellt Windows eine Datei an diesem Speicherort und stellt ein Handle darauf bereit. Das zurückgegebene Handle wird in allen nachfolgenden Aufrufen verwendet, die die Datei betreffen, beispielsweise bei NtWriteFile und NtClose.

Bei der Überlegung, wie Aufrufe all dieser APIs zwischen denen, die wir abfangen und manipulieren wollen, und denen, die wir in Ruhe lassen wollen, unterschieden werden können, bin ich darauf gekommen, nach einem Schlüsselwort im NtCreateFile-Aufruf zu suchen. Dies wurde erreicht, indem ein eindeutiges, nicht vorhandenes Verzeichnis als Teil des Dateipfads im NtCreateFile-Aufruf angegeben wurde. Wenn unser Hook die Ausführung auf die Ersatzfunktion NtCreateFile umleitet, wird der als Argument an NtCreateFile übergebene Dateipfad auf das Vorhandensein dieses eindeutigen „Schlüsselworts“ untersucht; wenn es gefunden wird, weiß MemFiles, dass sich dieser NtCreateFile-Aufruf auf eine Datei bezieht, die im Speicher statt auf der Platte abgelegt werden soll. In diesem Fall initialisiert MemFiles mehrere Variablen und allokiert anfänglich 1 MB Speicher für die Verwendung durch die Datei, aber am wichtigsten ist, dass es ein Fake-Handle mit dem in der MemFiles-Struktur angegebenen Dateinamen verknüpft und dieses Fake-Handle an den Aufrufer zurückgibt.

Für alle anderen von MemFiles gehookten NtAPIs prüfen die entsprechenden Ersatz-NtFunctions das als Argument übergebene Handle und sehen nach, ob es in der MemFiles-Struktur existiert; wenn das Handle in der MemFiles-Struktur existiert (die von MemFiles erzeugten Fake-Handles sind ausreichend unecht, sodass sie niemals mit einem echten kollidieren sollten), identifiziert MemFiles diesen Aufruf als einen, der eine In-Memory-Datei betrifft, und handelt entsprechend.### Hooking-Theorie und Ersatzfunktionen Um Dateien, die für die Festplatte bestimmt waren, in den Speicher zu schreiben, muss MemFiles Aufrufe bestimmter APIs abfangen, die von Programmen ausgeführt werden, wenn diese versuchen, eine Datei zu erstellen. API-Hooking gibt es schon seit sehr langer Zeit und wird von vielen EDR-Produkten aktiv als Kernfunktion eingesetzt; Aufrufe bestimmter APIs werden in den Adressraum des EDR umgeleitet, wo der API-Aufruf und die übergebenen Variablen analysiert werden. Wenn der EDR feststellt, dass der Aufruf bösartig ist, z. B. Teil eines Angriffstools oder einer Kill-Chain, verhindert er, dass der Aufruf abgeschlossen wird, und löst einen Alarm aus. Wenn der EDR entscheidet, dass der Aufruf harmlos ist, setzt er die Ausführung an den Punkt zurück, von dem aus umgeleitet wurde, und lässt den API-Aufruf wie ursprünglich beabsichtigt abschließen. Eine einfache Analogie: Du schickst einem Freund einen Brief, aber bevor dein Freund ihn erhält, öffnet ein Dritter ihn, liest ihn und entscheidet dann, ob der Brief etwas Illegales enthält; in diesem Fall erhält dein Freund den Brief nie und die Polizei wird alarmiert.

MemFiles folgt derselben Theorie, ohne die Alarme (oder die theoretische Beteiligung der Polizei). API-Hooking wird in der Regel auf der niedrigstmöglichen Ebene im Userland implementiert, und zwar in den NtFunctions in NTDLL.dll. Werfen wir einen Blick auf NtCreateFile, bevor irgendein Hooking stattgefunden hat:

image

Alle NtFunctions sind identisch, mit Ausnahme der Syscall-Nummer, die in diesem Beispiel 55 ist. Die Syscall-Nummer ändert sich zwischen den NtFunctions, und es sollte auch beachtet werden, dass sich diese Nummer zwischen Windows-Versionen ändern kann; die Syscall-Nummer für NtCreateFile auf diesem Betriebssystem (Windows 11) ist 55, auf Windows 10 könnte sie jedoch anders sein (und auf Windows 7 ganz sicher).

Beachtenswert sind die TEST- und JNE-Anweisungen. Sie dienen dazu, festzustellen, ob die NtFunction die normale Syscall-Anweisung oder die ältere INT-2E-Anweisung verwenden soll. Ich zitiere aus klezvirus' Beitrag SysWhispers is dead, long live SysWhispers!:

Jetzt der interessante Teil: Die Funktion prüft, ob SharedUserData[0x308] (BYTE PTR DS:[7FFE0308]) auf 1 gesetzt ist. SharedUserData ist ein Symbol, das sich auf die Kernel-Mode-Struktur KUSER_SHARED_DATA bezieht.

Die KUSER_SHARED_DATA-Struktur definiert einen festen (oder vordefinierten) Speicherbereich, der zum Austausch von Informationen mit User-Mode-Software verwendet wird. Dies wurde natürlich dazu gemacht, bestimmte globale Systeminformationen für User-Land-Code verfügbar zu machen, ohne den Overhead, jedes Mal zwischen User- und Kernel-Mode-Ausführung wechseln zu müssen.

Der Wert an Index 0x308 repräsentiert die Syscall-Anweisung, die in allen Windows-Versionen ab 1511 unterstützt wird. Wie du dir vorstellen kannst, war in allen Windows-Versionen vor 1511 die Standardmethode zur Ausführung eines Syscalls der Aufruf des Interrupts int 2Eh.

...

Falls du dich fragst, warum dieses int 2Eh immer noch vorhanden ist, obwohl Windows inzwischen weit über Version 1511 hinaus ist, liegt das daran, dass diese Anweisung weiterhin verwendet wird. Wenn nämlich HVCI (Hypervisor-protected Code Integrity) aktiviert ist, wird SharedUserData[0x308] auf 0 gesetzt und int 2Eh anstelle der Syscall-Anweisung verwendet. Dies geschieht hauptsächlich aus Leistungsgründen, aufgrund der Art und Weise, wie der Ring3-zu-Ring0-Wechsel mit der einen oder anderen Anweisung durchgeführt wird.

Ich habe auf Twitter um weitere Klärung zu diesem Thema gebeten, worauf @yarden_shafir Folgendes sagte:

image

Kurz gesagt: Jede Anweisung in der NtFunction könnte irgendwann benötigt werden (vielleicht mit Ausnahme des Multi-Byte-NOP am Ende, das scheinbar nicht erreichbar ist), und wenn wir Anweisungen in der NtFunction überschreiben, müssen wir sicherstellen, dass wir sie speichern und irgendwann vor dem finalen Syscall (oder INT 2E, je nach Fall) ausführen.

Beachtenswert ist, dass vor dem Ausführen des Syscalls die Syscall-Nummer in RAX verschoben wird (im Screenshot als EAX dargestellt). Da wir nicht sehen, dass RAX vorher auf den Stack geschoben wird, habe ich (vielleicht naiv) angenommen, dass der Wert, der vor dem Verschieben der Syscall-Nummer in RAX enthalten war, nach dem Ausführen des Syscalls nicht wichtig oder erforderlich ist. Das sind gute Nachrichten, denn es bedeutet, dass wir das RAX-Register frei verwenden können, solange wir sicherstellen, dass es vor dem Syscall die Syscall-Nummer enthält.

Um die Ausführung auf unseren eigenen Code bzw. die Ersatz-NtFunction umzuleiten, überschreiben wir einen Teil der ursprünglichen NtAPI, verschieben die Adresse unserer Ersatz-NtFunction in RAX und verwenden dann eine JMP-Anweisung, um zu diesem Code zu springen:

image

Für die MOV- und JMP-Anweisungen werden 12 Bytes benötigt; da wir andere Anweisungen durch das Überschreiben der ersten 12 Bytes der NtAPI verstümmeln, wurden diese Anweisungen durch NOPs ersetzt, um den korrekten Abstand und die Ausrichtung der NtAPI beizubehalten.

Wenn das Programm nun NtCreateFile aufruft, springt die Ausführung zu unserer Ersatz-NtFunction.

Eigener Code und Ersatz-NtFunctions

MemFiles weicht davon ab, wie EDRs das Hooking durchführen, und zwar in Bezug darauf, wo die Ersatzfunktionen, zu denen gehookte APIs umgeleitet werden, liegen. Viele EDRs laden ihre eigene DLL in einen Prozess. Gehookte APIs werden in den Adressraum dieser geladenen DLL umgeleitet, wo die Analyse stattfinden kann. Da der gesamte Sinn dieses Projekts darin bestand, das Ablegen von Dateien auf der Festplatte zu vermeiden, schien es ein schlechter Weg zu sein, eine DLL auf die Festplatte zu legen und unseren Beacon-Prozess sie laden zu lassen, um Zugriff auf unsere Ersatz-NtFunctions zu haben. Es gibt einen mehrere Jahre alten POC, der das Laden von DLLs aus dem Speicher ermöglicht, was eine praktikable Strategie für unsere Bedürfnisse ist, aber das Projekt wird nicht gepflegt und es scheint mehrere Probleme damit zu geben. Außerdem umfasst es 1200 Zeilen Code und wäre ein Monster, es in das BOF-Format zu konvertieren.

Anmerkung am Rande: Unsere Ersatz-NtFunctions können nicht in einem BOF liegen; CobaltStrike lädt, führt BOFs aus und löscht sie anschließend aus dem Prozessspeicher, wenn sie fertig sind. Da wir eine oder mehrere persistente Funktionen im Speicher benötigen, die immer dann aufgerufen werden können, wenn der Prozess eine der gehookten NtAPIs aufruft, funktionieren BOFs nicht.

Die Lösung, auf die ich kam, war positionsunabhängiger Code (Position Independent Code, PIC). Wie der Name schon sagt, kann PIC – im Gegensatz zu normalen ausführbaren Dateien, die an einer bestimmten Stelle bzw. mit Teilen in bestimmter Beziehung zueinander geladen werden müssen – überall im Speicher platziert und ausgeführt werden. Das eröffnet die Möglichkeit, unsere Ersatz-NtFunctions als PIC-Executables zu schreiben, sie in den Beacon-Prozess zu injizieren und unsere Hooks die Ausführung zu ihnen umleiten zu lassen, wenn Aufrufe an unsere gehookten NTAPIs erfolgen.

Die Vorlage für diese PIC-NtFunctions stammt aus dem Projekt ShellcodeTemplate von Cracked5pider.

Eine bemerkenswerte Abweichung vom Basisprojekt besteht darin, dass das Basisprojekt darauf ausgelegt ist, eine vollständige PIC-exe zu erstellen; das heißt, es enthält ASM, um den Stack-Pointer zu sichern, Platz auf dem Stack zu schaffen, die in der exe enthaltene designierte Ersatz-NtFunction aufzurufen und den Stack-Pointer nach der Rückkehr dieser Funktion wiederherzustellen. Die von der PIC-exe ausgeführte Call-Anweisung stellt ein Problem dar, da dabei die Rücksprungadresse des Aufrufs (im PIC-exe-ASM) auf den Stack gelegt wird, was dazu führt, dass die spätere Ret-Anweisung die Ausführung zurück in die PIC-exe führt, anstatt zum Aufrufer der ursprünglichen NtAPI.

Um dies zu entschärfen, wurde die ASM-Datei im ShellcodeTemplate-Projekt so bearbeitet, dass das ASM zum Einrichten des Stacks, zum Aufrufen der Funktion und zum anschließenden Wiederherstellen des Stack-Pointers nach Abschluss der Funktion entfernt wurde. Das Ergebnis ist, dass der in der NtAPI platzierte Hook die Ausführung nun direkt in die Ersatz-NtFunction springen lässt, wobei Stack und Register so eingerichtet sind, wie sie waren, als die ursprüngliche NtAPI vom Programm aufgerufen wurde (mit Ausnahme von RAX, das für unseren JMP verwendet wird).

Original ShellcodeTemplate ASM:
image

MemFiles ASM:
image

Jede gehookte NtAPI hat ihre eigene PIC-NtFunction, die die erforderliche Logik enthält, um entweder:

A. MemFiles-spezifische Aktionen auszuführen, wie das Erstellen eines gefälschten Handles, das Schreiben von Daten in den Speicher, das Ändern von Variablen im MemFiles-Struct usw.
oder
B. Die Ausführung zu einer Trampoline zu leiten, die den API-Aufruf wieder auf Kurs bringt und ihn zurück in NTDLL patcht, wo der Syscall ausgeführt werden kann.

Einige der Ersatz-NtFunctions sind komplexer als andere; Wenn ein Aufruf einer gehookten NtAPI MemFiles betrifft, modifizieren einige, wie NtCreateFile und NtQueryVolumeInformationFile, Variablen, die als Argumente an die NtAPI übergeben wurden, gemäß MSDN-Dokumentation, Testergebnissen und etwas Rätselraten/gesundem Menschenverstand. Andere, wie NtClose und NtReadFile, geben einfach STATUS_SUCCESS an den ursprünglichen Aufrufer zurück, um den unvermeidlichen Fehler "Invalid Handle" zu vermeiden, der andernfalls durch die Übergabe eines von MemFiles erstellten gefälschten Handles auftreten würde.

Wenn ein Aufruf einer gehookten NtAPI MemFiles NICHT betrifft, müssen wir die Ausführung zu einer Trampoline leiten, um die Dinge wieder auf Kurs zu bringen:

image

Trampoline

Das Trampolin ist dafür verantwortlich, alle Anweisungen auszuführen, die in der ursprünglichen NtAPI nicht ausgeführt wurden, weil diese API gehooked wurde; dies schließt Anweisungen ein, die durch den ursprünglichen Hook teilweise oder vollständig überschrieben wurden. Das Hooken von APIs kann schnell zu Problemen führen, bei denen wir nicht genug Platz haben, um alle benötigten Anweisungen auszuführen. Trampoline können auch dieses Problem lindern, da wir beliebig viele Aktionen ausführen können, um unsere Register und/oder den Stack einzurichten, bevor wir zurück zur ursprünglichen NtAPI springen. Die NtCreateFile-Trampoline ist unten zu sehen:

image

Am offensichtlichsten sind die drei Anweisungen, die durch unseren ersten Hook überschrieben wurden, als die ersten drei Anweisungen im Trampolin zu sehen:

MOV R10, RCX
MOV EAX, 55
TEST BYTE PTR DS:[7FFE0308], 1

Wie bereits erwähnt, ist die große Anforderung bei all dieser Manipulation, dass die Syscall-Nummer (55 im obigen Beispiel) vor dem Ausführen des Syscalls in RAX(EAX) liegt. Wir haben jedoch ein Problem, da wir immer noch eine JMP-Anweisung benötigen, um die Ausführung zurück zur ursprünglichen NtAPI zu leiten. Es mag ein anderes Register geben, das keine wichtigen Informationen enthält und dafür genutzt werden könnte, aber ich habe angesichts der Anzahl der NtAPIs, die wir hooken und die die Register jeweils unterschiedlich verwenden, keine durchgängig sichere Option gefunden. Die sichere Option ist, weiterhin RAX zu verwenden; das können wir tun, indem wir die in RAX gespeicherte Syscall-Nummer auf den Stack schieben. Dann können wir die Adresse, zu der wir in der ursprünglichen NtAPI zurückspringen möchten, in RAX verschieben und eine JMP-Anweisung verwenden, um zurück in NTDLL zu gelangen:

image

In dem Hook, der in der NtAPI installiert ist, ist eine POP-RAX-Anweisung enthalten, und dorthin springen wir mit unserem Trampolin. Die Ausführung dieser Anweisung stellt die Syscall-Nummer vom oberen Ende des Stacks in RAX wieder her und bereitet uns auf die Ausführung des Syscalls vor. Beachte, dass die JNE-Anweisung aus der ursprünglichen, nicht gehookten NtAPI noch vorhanden ist; die entsprechende TEST-Anweisung, die das ZF-Flag setzt und bestimmt, ob ein JNE ausgeführt wird (was uns über den Syscall zum INT 2E springen lassen würde), wurde im Trampolin ausgeführt. Durch diese Anordnung können wir sowohl die NtAPI erfolgreich hooken und die Ausführung zu unserer Ersatz-PIC-NtFunction umleiten als auch sicherstellen, dass wir nichts überspringen oder durch unser Hooking Funktionalität verlieren.

Ein Problem mit dem Karren vor dem Pferd

Um kurz zusammenzufassen, was bisher behandelt wurde: Wenn MemFiles initialisiert wird, wird im Speicher des Beacon-Prozesses ein Struct erstellt, das wichtige Informationen für die Funktionalität von MemFiles enthält. Informationen in diesem Struct werden während des gesamten Lebenszyklus von MemFiles ständig referenziert, einschließlich durch jede der Ersatz-PIC-NtFunctions sowie durch die BOFs, die verwendet werden, um von MemFiles im Speicher gespeicherte Dateien abzufragen und abzurufen. Zu diesem Zweck wird die Speicheradresse, an der sich das Struct befindet, nach der Erstellung an den Teamserver zurückgemeldet:

image

Nachdem diese Adresse in memtable gespeichert wurde, senden nachfolgende MemFiles-Befehle (memlist, memfetch, memclean) diese Adresse als Argument an das BOF, damit das Struct lokalisiert und referenziert werden kann. Aber wie lokalisieren die PIC-NtFunctions das Struct?

Das eklatante Problem ist, dass die PIC-NtFunctions die Speicheradresse des MemFiles-Structs benötigen, aber zum Zeitpunkt der Struct-Erstellung bereits kompiliert sind. Eine frühe Implementierung von MemFiles löste dieses Problem, indem sie die Initialisierung von MemFiles in zwei separate BOFs aufteilte. Das erste erstellte das Struct und sendete die Adresse zurück an den Teamserver, der durch etwas Aggressor-Script-Magie die Adresse herausparste, sie in die Quellcode-Datei für jede NtFunction einfügte und sie dann zur finalen PIC-NtFunction neu kompilierte. Das zweite BOF würde dann die fertigen PIC-NtFunctions übertragen und das eigentliche Injizieren und Hooken der NtAPIs durchführen.

Abgesehen davon, dass es hässlich ist und zusätzliche Zeit kostet, könnten echte Probleme auftreten, wenn mehrere Beacons gleichzeitig versuchen, MemFiles zu initialisieren. Wenn Beacon 2 mit seiner MemFiles-Struct-Adresse zurückruft, während Beacon 1 gerade dabei ist, die NtFunction-Quellcode-Dateien zu patchen und neu zu kompilieren, könnte es unschön werden.

Die elegante Lösung für dieses Problem besteht darin, einen Binärpatch auf der PIC-NtFunction durchzuführen, bei dem die MemFiles-Struct-Adresse in die kompilierte NtFunction gepatcht und zur Laufzeit zugänglich gemacht wird. Um dies zu ermöglichen, wird in jede NtFunction eine Platzhalterzeichenkette geschrieben:

image

Diese Variable kann im kompilierten Code mit einem Tool wie xxd eingesehen werden:

image

Wenn der Befehl meminit ausgeführt wird, wird jede der PIC-NtFunctions zusammen mit dem InstallHooks-BOF an Beacon gesendet. Dieses BOF ist für das Erstellen des MemFiles-Structs verantwortlich; nachdem es das getan hat, ruft es die Funktion patchAddr für jede der PIC-NtFunctions auf. patchAddr ist dafür verantwortlich, die Zeichenkette aus A's zu finden und sie durch die Stringdarstellung der Struct-Adresse zu ersetzen. Mit der Speicheradresse aus dem früheren Screenshot sieht die Variable pFileInfoStr nun wie folgt aus:

char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );

Diese Stringdarstellung kann dann in den tatsächlichen Hex-Wert umgewandelt werden, der unsere Speicheradresse ist. Die BOFs bewerkstelligen dies ganz einfach mithilfe der _strtoi64-API:

image

Natürlich konnten die Dinge für die PIC-NtFunctions unmöglich so einfach sein.

Dieses kurze Assembly-Fragment zeigt das Ende einer der PIC-NtFunctions. Beachte die JMP-RAX-Anweisung, mit der die PIC-NtFunction die Trampoline aufruft (was bedeutet, dass dies ein Aufruf ist, den MemFiles NICHT gespooft hat bzw. in den es nicht eingegriffen hat):

image

Aus unbekannten Gründen wurde diese JMP-RAX-Anweisung zu einer CALL-RAX-Anweisung, als ich versuchte, _strtoi64 (oder eine ihrer verwandten APIs wie stroull oder atoll) zu verwenden. Ich bin sicher, es gibt einen triftigen Grund dafür, der mit einer tiefen Ebene von "wie Computer funktionieren" zu tun hat, aber diese scheinbar unbedeutende Änderung bricht einige Dinge. Nachdem ich mehr als 15 Stunden damit verbracht hatte, einen Weg zu finden, die Stringdarstellung der MemFiles-Struct-Adresse in den tatsächlichen Hex-Wert umzuwandeln, um die darin gespeicherten Werte nutzen zu können, stieß ich auf diesen StackOverflow-Beitrag, in dem ein Kommentator eine benutzerdefinierte Routine bereitstellte, die für Mikrocontroller entwickelt wurde, um einen String in eine uint32 zu konvertieren; dankenswerterweise funktionierte sie ohne Änderung auch für eine uint64, und am wichtigsten: Sie bewahrte die spätere JMP-RAX-Anweisung in der PIC-NtFunction, ohne sie in ein CALL zu ändern. Das letzte Fragment:

image

Alte NtAPI vs. neue NtAPI

Der Einfachheit halber wurde es früher nicht erwähnt, aber das Format der NtAPIs hat sich im Laufe der Jahre geändert. Frühere Versionen, wie die unter Windows 7 verwendeten, sind viel kürzer als ihre modernen Pendants, nämlich nur 16 Bytes lang statt 32:

image

Dies erfordert Änderungen daran, wie die NtAPIs von MemFiles gehooked werden, sowie daran, wie das Trampolin gebaut wird. Damit MemFiles die Version der NtAPIs identifizieren kann, mit denen es arbeitet, löst das InstallHooks-BOF zunächst die Adresse der betreffenden NtAPI auf und liest dann 32 Bytes von dieser Position. Das unterschiedliche Format führt dazu, dass sich die Syscall-Anweisung in den NtAPI-Versionen an unterschiedlichen Stellen befindet; durch Prüfung des Vorhandenseins oder Fehlens des Syscalls an einem bestimmten Byte-Offset kann MemFiles feststellen, ob es mit der modernen oder der Legacy-NtAPI-Implementierung arbeitet, und entsprechend handeln:

image

Nach dem Hooking sieht die Legacy-NtCreateFile-API wie folgt aus:

image

Und das Trampolin, das verwendet wird, um Register einzurichten und dann zu NtCreateFile zurückzuspringen:

image

Insgesamt ist die Technik im Großen und Ganzen dieselbe, aber die Dinge sind viel enger und ohne viel Spielraum. Es ist erwähnenswert, dass die Syscall-Anweisung innerhalb von NtCreateFile verschoben werden musste, damit alles hineinpasst und ordnungsgemäß funktioniert; sie befindet sich weiterhin im API-Speicherbereich von NtCreateFile in NTDLL, ist aber vom 9. und 10. Byte der API zum 14. und 15. Byte der API verschoben, wobei das Multi-Byte-NOP geopfert wurde, um alles ordnungsgemäß hineinzuquetschen.

Auffinden von I/O-bezogenen NtAPIs

Einige der APIs im Zusammenhang mit Datei-I/O-Operationen waren intuitiv und daher leicht zu identifizieren und zu hooken; andere waren weitaus schwerer fassbar und erforderten viele Stunden in WinDbg und x64dbg, in denen Assembly schrittweise durchlaufen wurde, um zu identifizieren, welche APIs aufgerufen wurden. Ich muss einfach glauben, dass es einen effizienteren Weg gab, diese Aufgabe zu bewältigen, aber ich lernte im Laufe der Arbeit.Als kleines Extra wollte ich den Prozess im Detail beschreiben, der im Nachhinein betrachtet eigentlich sehr schnell hätte gehen sollen: die Identifizierung der NtAPI, die SharpHound 20 Stunden lang daran hinderte, zu funktionieren. Da SharpHound ein .NET-Programm ist, spuckte es einen sehr hässlichen Callstack aus, der sich vollständig darauf bezog, dass es festgestellt hatte: "Das Handle ist ungültig". Auch wenn .NET eine programmierfreundliche Sprache ist, wird der Großteil (alles?) der Funktionalität übersetzt und geht letztlich durch die Win32-API (und damit durch die NtAPI), wo wir sie wie alles andere beobachten und hooken können.

sharphounderror

Der Fehler "Invalid Handle" war ein eindeutiger Hinweis darauf, dass SharpHound eine NtAPI verwendete, die ich nicht hookte (im Gegensatz zu einer meiner vorhandenen PIC-NtFunctions, die nicht richtig funktionierten). Also machte ich mich auf die Suche, um herauszufinden, was es war. Die Technik, die ich bis zu diesem Zeitpunkt bei anderen Tools verwendet hatte, bestand darin, zuerst einen Haltepunkt auf NtCreateFile zu setzen und den Aufruf zu lokalisieren, der mein "spezielles" Verzeichnis betraf. Von dort aus ging ich das Programm schrittweise durch (meistens mehrmals, weil ich mich verlief) und schaute, welche Funktionen das Programm als Nächstes aufrief. Mithilfe dieser Methodik entdeckte ich, dass NtQueryVolumeInformationFile, NtQueryInformationFile und NtSetInformationFile aufgerufen wurden und gehooked werden mussten.

SharpHound wirft einem noch ein paar zusätzliche Hürden in den Weg, da es viele seiner Aufgaben asynchron ausführt. Das macht es wesentlich schwieriger, die linearen Schritte zu verfolgen, die eine einzelne Datei durch API-Aufrufe nimmt, weil mehrere Dateien gleichzeitig den Prozess durchlaufen. Außerdem stellte ich beim schrittweisen Durchgehen des Programms nach dem NtCreateFile-Aufruf fest, dass der Thread, in dem der NtCreateFile-Aufruf gemacht wurde, irgendwann beendet wird; die letztendlichen NtWriteFile-Aufrufe (und alle anderen unbekannten API-Aufrufe, die Gegenstand dieser Suche sind) finden in einem anderen Thread statt, was den Suchprozess zusätzlich erschwert.

Da ich mich nur kurz mit .NET beschäftigt hatte, war ich nicht sehr bewandert darin, von Fehlern erzeugte Callstacks zu parsen, erst recht nicht bei solchen, die durch die Verwendung von async noch zusätzlich verschandelt werden. Im Verlauf des 20-Stunden-Problems, ohne Fortschritte mit meiner früheren Strategie, kehrte ich immer wieder zu diesem Ansatz zurück und wurde dabei langsam aber sicher schlauer. Ungefähr beim dritten "Block" von oben, getrennt durch die Zeilen "--- End of stack trace...", ist eine Zeile mit "at Sharphound.Writers.JsonDataWriter..." zu sehen. Das gab mir einen relativen Ausgangspunkt im eigentlichen SharpHound-Code, der Open Source ist und auf Github zur Verfügung steht. Wie der Name vermuten ließ, befasste sich die SharpHound-Funktion damit, JSON-Ausgaben in eine Datei zu schreiben; mir war bereits bewusst, dass meine Daten nicht erfolgreich in eine Datei geschrieben wurden, also war das keine Neuigkeit. Wenn man den Callstack eine Ebene nach oben verfolgt, ist die nächste relevante Zeile "at System.IO.Streamwriter.". Das Präfix System.IO verriet mir, dass dies ein .NET-eigener Bestandteil war, im Gegensatz zu einer SharpHound-spezifischen Funktion. Im obersten Abschnitt des Callstacks fiel mir die Zeile "at System.IO.FileStream.FlushOSBuffer()" auf. Ich beschloss, nach FlushOSBuffer zu googeln und zu sehen, was ich finden konnte.

Das führte mich zur .NET-Dokumentation von Microsoft für filestream.cs. Dort fand ich die Definition für FlushOSBuffer:

image

Sie scheint eine Win32-API aufzurufen, FlushFileBuffers. Die Definition für Win32Native.FlushFileBuffers findet man in der Win32Native-Dokumentation:

image

Jeder, der in .NET mit P/Invoke gearbeitet hat, wird das Format erkennen. Ich hatte nun eine Win32-API, von der ich wusste, dass System.IO.Filestream.FlushOSBuffer(), meine problematische .NET-Funktion, sie aufruft. Ein Haltepunkt auf KERNEL32!FlushFileBuffers und das Ausführen von SharpHound bestätigten dies, und beim schrittweisen Durchgehen sah ich schnell, dass FlushFileBuffers unter der Haube NtFlushBuffersFile aufruft. Das Hooken dieser API behob die Probleme, die SharpHound hatte, und ermöglichte es, erfolgreich zu laufen und seine Ausgabedateien in den Speicher zu schreiben.

Herunterladen von Dateien aus dem Speicher

Ein kritischer Teil dieses Projekts ist die Fähigkeit, die Dateien tatsächlich auf den CobaltStrike-Teamserver herunterzuladen, sobald sie im Speicher sind. Vorhersehbar funktioniert der normale CobaltStrike-Download-Befehl nicht mit einem Dateipfad, der in Wirklichkeit nicht existiert. Mit dem, was ich heute weiß, liegt eine Lösung wahrscheinlich darin, den ersetzenden PIC-NtReadFile-Code auszuarbeiten, um zu ermöglichen, dass die In-Memory-Dateien gelesen statt nur geschrieben werden können. Da ich dieses Wissen vorher nicht hatte, war die tatsächliche Wiederbeschaffung der Dateien ein großes Hindernis.

Durch Zufall stieß ich auf ein BOF von EspressoCake, das eine Funktion enthielt, die mir sofort ins Auge sprang:

image

Beim Durchsehen des Codes scheint es eine undokumentierte Beacon-CALLBACK-Option zu verwenden:

image

Die Funktion ermöglicht es einem BOF, den Download einer Datei auf den Teamserver vom Zielsystem aus zu initiieren, anstatt vom Client. Diese Fähigkeit (von der ich später erfuhr, dass sie eine gemeinsame Anstrengung mehrerer anderer Leute war, darunter @Cr0Eax, @EthicalChaos und @anthemtotheego) beseitigte das große Hindernis, das zuvor bestand, da ich nun eine Möglichkeit hatte, eine Dateiübertragung vom Zielsystem für die In-Memory-Datei zu initiieren. Vielen Dank an alle Beteiligten für dieses Code-Snippet, das, wie ich voraussehe, auch in Zukunft nützlich sein wird.

MemFiles-Datenstruktur

Eine herausfordernde Aufgabe dieses Projekts war es, die Verfügbarkeit der MemFiles-Funktionalität für alle CobaltStrike-Clients sicherzustellen, die mit dem Team Server verbunden sind. Die Daten von MemFiles werden in Strukturen gespeichert, die von MemFiles.cna erstellt werden und in jeden Client geladen werden müssen, der das Tool verwenden möchte; folglich leben diese Datenstrukturen in jedem Client, nicht auf dem Team Server. Wenn diese Daten an einem einzigen zentralen Ort (TS) leben würden, wäre es trivial, sie von jedem Client abzurufen, und die ganze Sache wäre kein Problem; würde das CobaltStrike-Team eine Fähigkeit wie MemFiles formal in CobaltStrike integrieren, bin ich mir sicher, dass sie diesen Weg gehen würden. Da es sich jedoch um ein Community-Add-on handelt, arbeiten wir mit dem, was wir haben.

Es gibt ein paar verschiedene Szenarien, um die wir uns kümmern müssen, wenn es darum geht sicherzustellen, dass jeder CobaltStrike-Client die neuesten, genauen Daten über den MemFiles-Status in Beacons und Konfiguration hat:

Neue Clients, die sich mit dem TS verbinden und die aktuelle Memtable benötigen
Fälle, in denen nur ein einzelner Client mit dem TS verbunden ist und CobaltStrike neu startet (wodurch die im Client-Speicher gespeicherte Memtable verloren geht)
Client A nimmt eine Änderung an MemFiles-Daten vor, die Client B mitgeteilt werden muss

Es wurde ein mehrgleisiger Ansatz gewählt, um diese Szenarien zu adressieren. Für den Fall, dass nur ein einzelner CobaltStrike-Client mit dem TS verbunden ist (und somit die einzige Entität ist, die über die Memtable-Daten verfügt), schreibt der Client bei jeder Änderung der Memtable (meminit, memclean) auch den Inhalt seiner Memtable in eine lokale Textdatei im CobaltStrike-Verzeichnis. Wenn der Client beendet/neu gestartet wird oder wenn MemFiles.cna neu geladen wird, versucht er zunächst, aus der lokalen memtable.txt-Datei zu lesen, um seine In-Memory-Memtable zu befüllen.

Wenn mehrere Clients mit einem TS verbunden sind und ein neuer Client beitritt (gemäß Ereignisprotokoll), ruft jeder Client eine Liste aller mit dem TS verbundenen Benutzer ab und sortiert sie alphabetisch. Der Client, der in dieser Liste an erster Stelle steht, wird als "Broadcast"-Client ausgewählt und sendet nach einer Wartezeit von 5 Sekunden (um dem neuen Client zu ermöglichen, zu initialisieren und seine lokale memtable.txt zu lesen) Nachrichten (Aktionen) im Ereignisprotokoll für jeden Eintrag in seiner Memtable. Alle Clients (außer dem sendenden) lesen diese Nachrichten und aktualisieren ihre Memtable mit den übertragenen Informationen; dies umfasst sowohl das Aktualisieren vorhandener Einträge als auch das Hinzufügen zusätzlicher Einträge, die ihre jeweilige Memtable nicht enthält.

Normale Abläufe mit MemFiles beruhen ebenfalls auf dem Senden von Nachrichten im Ereignisprotokoll. Wenn Client A meminit ausführt, wird eine Nachricht übertragen, die alle relevanten Memtable-Informationen enthält; ALLE Clients aktualisieren ihre jeweilige Memtable, indem sie diese übertragenen Ereignisprotokoll-Nachrichten mithilfe des "on Event_Action"-Hooks parsen. Änderungen an MemFiles-Daten werden auch vorgenommen, wenn meminit die Ausführung seines BOF abschließt; diese Änderungen werden von Beacon zurückgemeldet (z. B. ruft Beacon nach dem Ausführen von meminit mit dem Speicherort der pMemAddrs-Struktur zurück) und sind daher für alle verbundenen Clients sichtbar, die ihre jeweilige Memtable mithilfe des "on Beacon_Output"-Hooks aktualisieren.

Diese separaten Bemühungen führen in Kombination dazu, dass MemFiles kritische Daten effizient und zuverlässig zwischen mehreren Clients synchronisieren kann.

Credits und Danksagungen

Dieses Projekt wäre nicht möglich ohne die Beiträge der folgenden Personen und Projekte:

  1. x64-NTAPI-inline-hook von globalpolicy
  2. x64 Function Hooking by Example von Kyle Halladay
  3. ShellcodeTemplate von Cracked5pider alias @C5pider
  4. @ilove2pwn_, durch Vermittlung von Cracked5pider
  5. DLL-Exports-Extraction-BOF von EspressoCake alias @the_bit_diddler
  6. @anthemtotheego, durch Vermittlung von EspressoCake
  7. @Cr0Eax, durch Vermittlung von @anthemtotheego
  8. @EthicalChaos, durch Vermittlung von @anthemtotheego
  9. SysWhispers is dead, long live SysWhispers! von KlezVirus
  10. @yarden_shafir
  11. @DexterGerig
Tool herunterladen