
POC zur Frustration/Besiegung von Malware-Analysten
Die folgende Arbeit ist ein POC, um Malware in die Lage zu versetzen, sich selbst an ein bestimmtes Opfer zu „binden“, um die Bemühungen von Malware-Analysten zu erschweren.
Ich übernehme keine Verantwortung für die böswillige Nutzung von Ideen oder Code, die in diesem Projekt enthalten sind. Ich stelle diese Forschung zur Verfügung, um Infosec-Experten weiterzubilden und Malware-Analysten, Reverse Engineers und Blue Teamern zusätzliche Schulungen/Denkanstöße zu bieten.
Beim ersten Ausführen der Malware auf einem Opfer verschlüsselt sie die eigentliche Nutzlast (ein RDLL) mit AES unter Verwendung von Umgebungsdaten dieses Opfers. Bei jeder weiteren Ausführung sammelt die Malware dieselben Umgebungsinformationen, entschlüsselt die als Byte-Array in der Malware gespeicherte Nutzlast mit AES und führt sie aus. Wenn die Entschlüsselung fehlschlägt / die Nutzlast nicht ausgeführt werden kann, löscht sich die Malware selbst. Schutz vor Reverse Engineers und Malware-Analysten.


Ich hatte das Gefühl, mit diesem Projekt noch nicht fertig zu sein, also bin ich zurückgegangen und habe eine ziemlich umfangreiche Neufassung vorgenommen. Die ursprüngliche Forschung und Tradecraft finden Sie hier.
Die wesentlichen Änderungen sind wie folgt:
Es gibt einige verschiedene Dinge, die aus dem Quellcode dieses Projekts für die anderweitige Verwendung übernommen werden könnten. Hoffentlich wird es für jemanden nützlich sein.
Es gab einige Mängel in der ursprünglichen Version von BeatRev, die ich zu beheben versuchte.
Stage2 war zuvor ein eigenständiges ausführbares Programm, das als alternativer Datenstrom (ADS) von Stage1 gespeichert wurde. Um die AES-Verschlüsselung durch das Opfer und die anschließende Entschlüsselung und Ausführung zu erreichen, las Stage1 bei jedem Durchlauf den ADS, entschlüsselte ihn, schrieb zurück in den ADS, rief CreateProcess auf und verschlüsselte Stage2 erneut und schrieb es zurück auf die Festplatte im ADS. Dies waren viele I/O-Operationen, und der CreateProcess-Aufruf war natürlich nicht ideal.
Ich stieß zufällig auf Steven Fewers Forschung zu Reflective DLLs und es schien eine gute Lösung zu sein. Stage2 ist nun ein RDLL; unsere Malware/Shellcode-Runner/was auch immer wir schützen wollen, kann in das RDLL-Format portiert werden und als Byte-Array in Stage1 gespeichert werden, das dann zur Laufzeit entschlüsselt und von Stage1 ausgeführt wird. Dadurch werden alle I/O-Operationen und der CreateProcess-Aufruf von Version 1 entfernt, was eine willkommene Änderung ist.
Stage1 hatte keine wirklich programmierten AV-Evasion-Maßnahmen; dies war beabsichtigt, da es zusätzliche Arbeit ist und nicht wirklich der Kern dieser Forschung war. Während der Neufassung habe ich es als zusätzliche Herausforderung gesehen und API-Hashing hinzugefügt, um Funktionen aus der Importadresstabelle von Stage1 zu entfernen. Dies hat bei der Erkennung geholfen, und Stage1 hat eine Erkennungsrate von 4/66 auf VirusTotal. Ich fühlte mich wohl dabei, Stage1 hochzuladen, da es bereits an die ursprüngliche Box gebunden ist, auf der es ausgeführt wurde, und sich die Dateisignatur aufgrund der AES-Verschlüsselung ständig ändert.
Ich habe kürzlich begonnen, auf Entropie als Mittel zur Erkennung von Malware zu achten. Um die ansonsten sehr hohe Entropie zu senken, die ein riesiger AES-verschlüsselter binärer Blob einer ausführbaren Datei verleiht, habe ich mich mit der Integration von Shellcode befasst, der als UUIDs gespeichert ist. Da die Binärdatei in String-Repräsentation gespeichert ist, ist die Gesamtentropie in der ausführbaren Datei geringer. Mit dieser Technik liegt die Entropie von Stage0 jetzt bei etwa 6,8 und die von Stage1 bei etwa 4,5 (auf einer maximalen Skala von 8).
Schließlich ist es eine riesige Aufgabe, ein vollständiges Stage0 zu integrieren und zu produzieren, aufgrund all der Teile, die manipuliert werden müssen. Um dies zu erleichtern, habe ich eine Builder-Anwendung erstellt, die eine Stage0.c-Vorlagendatei, einen Stage1-Stub, einen Stage2-Stub und eine rohe Shellcode-Datei (dies wurde für Stage2 als Shellcode-Runner mit CobaltStrike-Shellcode entwickelt) aufnimmt und eine kompilierte Stage0-Nutzlast für den Einsatz auf dem Ziel erzeugt.
Der Reflective DLL-Code von Stephen Fewer enthält einige Visual Studio-Compilerspezifische Anweisungen; ich bin sicher, dass es möglich ist, die Technik auf MinGW zu portieren, aber ich habe nicht die Fähigkeiten dazu. Das Hauptproblem hier ist, dass der CobaltStrike-Shellcode (stageless ist etwa 265 KB) in das RDLL gehen und kompiliert werden muss. Um dies zu umgehen und es gut in den Rest des Prozesses zu integrieren, habe ich mein Stage2-RDLL so geschrieben, dass es einen globalen Variablenspeicherblock in der Größe des CS-Shellcodes enthält; dieser etwa 265 KB große Speicherblock enthält einen kleinen Platzhalter, der in der kompilierten Binärdatei lokalisiert werden kann. Der Code in src/Stage2 hat dies bereits hinzugefügt.
Sobald dies kompiliert ist, wird dieser Stage2-Stub nach Kali übertragen, wo ein Binary-Patch durchgeführt werden kann, um den echten CS-Shellcode an der Stelle im Speicher zu platzieren, an die er gehört. Dies erzeugt das vollständige Stage2.
Um das zuvor beschriebene I/O- und CreateProcess-Debakel zu vermeiden, muss das vollständige Stage2 auch von Stage0 in das kompilierte Stage1 gepatcht werden; dies ist notwendig, um Stage2 nach dem Einsatz auf dem Ziel zu verschlüsseln und zu verhindern, dass Stage2 separat auf der Festplatte gespeichert wird. Das gleiche Konzept, das zuvor für Stage2 beschrieben wurde, wird von Stage0 auf dem Ziel durchgeführt, um die endgültige Stage1-Nutzlast zusammenzustellen. Es sollte beachtet werden, dass die Funktion memmem verwendet wird, um den Platzhalter in jedem Stub zu lokalisieren; diese Funktion ist unter Windows nicht verfügbar, daher wurde eine benutzerdefinierte Implementierung verwendet. Danke an Foxik384 für seinen Code.
Um einen Binary-Patch durchzuführen, müssen wir den erforderlichen Speicher im Voraus zuweisen; dies hat einen verstärkenden Effekt, da Stage1 nun groß genug sein muss, um auch Stage2 zu enthalten. Mit dem zusätzlichen Schritt der Umwandlung von Stage2 in einen UUID-String bläht Stage2 in der Größe auf, ebenso wie Stage1, um es aufzunehmen. Ein Stage2-RDLL mit einer kompilierten Größe von etwa 290 KB führt zu einer Stage0-Nutzlast von etwa 1,38 MB und einer Stage1-Nutzlast von etwa 700 KB.