
Rust-basierter PoC unter Verwendung von Windows-Fasern zur verdeckten Ausführung von In-Memory-Code, der Payload-Stacks vor EDR verbirgt, indem zwischen Kontroll- und Payload-Fasern ohne Kernel-Callbacks umgeschaltet wird.
Eine Faser ist eine Ausführungseinheit, die von der Anwendung manuell geplant werden muss, anstatt sich auf den prioritätsbasierten Planungsmechanismus von Windows zu verlassen. Fasern werden oft als Lightweight-Threads bezeichnet. Ausführlichere Informationen darüber, was Fasern sind und wie sie funktionieren, finden Sie in der offiziellen Dokumentation.
Fasern ermöglichen mehrere Ausführungsflüsse in einem einzelnen Thread, jeder mit seinem eigenen Registerzustand und Stack. Andererseits sind Fasern für den Kernel unsichtbar, was sie zu einer heimlicheren (und günstigeren) Methode macht, Code im Speicher auszuführen, als neue Threads zu erstellen.
Ein Thread kann mehrere Fasern erstellen und nach Belieben zwischen ihnen umschalten, indem die Funktion SwitchToFiber aufgerufen wird. Zuvor muss der aktuelle Thread selbst durch Aufruf von ConvertThreadToFiber zu einer Faser geworden sein, da nur eine Faser andere Fasern erstellen kann. Schließlich ist es nur erforderlich, einen Aufruf von CreateFiber zu tätigen, um eine Faser zu erstellen, die bei der Planung einen In-Memory-Code ausführt (z. B. nach dem reflektiven Laden einer PE oder eines Shellcodes).
Die SwitchToFiber-Funktion ist der wichtigste Teil dieses Prozesses und der Ort, an dem die ganze Magie geschieht. Diese Funktion ermöglicht es, die eine oder die andere Faser zu planen, wobei alles im Benutzermodus abläuft. Laut der offiziellen Dokumentation "speichert die SwitchToFiber-Funktion die Statusinformationen der aktuellen Faser und stellt den Status der angegebenen Faser wieder her". Das bedeutet, dass beim Aufruf dieser Funktion die Registerwerte und der Stack vom aktuellen Faserzustand in den Ziel-Faserzustand umgeschaltet werden, was es ermöglicht, den Stack der aktuellen Faser zu "verstecken", sobald der Prozess abgeschlossen ist. Dies ermöglicht auch die Fortsetzung der Ausführung der Zielfaser an demselben Punkt, an dem die Ausführung gestoppt wurde (genauso wie es passiert, wenn der Scheduler gemäß seiner eigenen Prioritätslogik zwischen Threads wechselt).
Und genau das tut dieser einfache PoC:
run()-Funktion auszuführen. Diese Faser wird im Folgenden als Payload-Faser bezeichnet.Dieser Prozess wiederholt sich unbegrenzt.
Die Verwendung von Fasern kann für einige Arten von Payloads (wie ein C2-Beacon) aus folgenden Gründen vorteilhaft sein:
JMP oder CALL vom Loader auf nicht gesicherte Speicherbereiche verweisen müssen.Da wir das LITCRYPT-Plugin verwenden, um String-Literale zu verschleiern, muss die Umgebungsvariable LITCRYPT_ENCRYPT_KEY vor der Kompilierung des Codes gesetzt werden:
C:\Users\User\Desktop\Fiber> set LITCRYPT_ENCRYPT_KEY="yoursupersecretkey"
Danach kompilieren Sie einfach sowohl das Payload als auch den Loader und führen den letzteren aus:
C:\Users\User\Desktop\Fiber\payload> cargo build --release
C:\Users\User\Desktop\Fiber\loader> cargo build --release
C:\Users\User\Desktop\Fiber\loader\target\release> loader.exe
Es gibt nicht viel Geheimnis bei der Ausführung dieses PoC. Alles, was getan werden muss, ist, den Loader auszuführen und ein Tool wie ProcessHacker zu verwenden, um den Thread-Stack zu inspizieren. Da das Payload vor dem Schlafen zur Kontrollfaser zurückwechselt, bleibt der Stack der Payload-Faser die meiste Zeit verborgen. Sie werden in der Ausgabe sehen, wie die beiden Fasern nacheinander gemäß der bereits beschriebenen Logik geplant werden.
Der Code ist kommentiert, um zu zeigen, wie Fasern verwendet, erstellt und geplant werden. Sie werden feststellen, dass sowohl der Loader als auch das als Beispiel angebotene Payload in einer Endlosschleife "feststecken", was ein unbegrenztes Umschalten zwischen Fasern und die Fortsetzung der Ausführung ermöglicht.
Wenn ein anderes Payload getestet werden soll, ändern Sie einfach den Pfad in Zeile 32 der Datei src::main.rs des Loaders. In diesem Fall muss die neue DLL eine run(PVOID)-Funktion exportieren, die als Eingabeparameter die Adresse der Kontrollfaser erhält. Diese Funktion muss zur Kontrollfaser zurückwechseln, um die Sleep-Funktion aufzurufen, obwohl Sie dieses Verhalten nach Belieben an Ihre Anforderungen anpassen können.
Eine andere Möglichkeit, dieses Tool mit einem beliebigen Payload zu testen, besteht darin, IAT-Hooking durchzuführen, um jeden Aufruf der Sleep-Funktion (oder einer anderen importierten Funktion) durch das Payload auf eine Funktion im Loader umzuleiten, sodass beim Auftreten dieses Aufrufs zur Kontrollfaser zurückgewechselt werden kann. Es liegt an Ihnen.
In den folgenden Screenshots sehen wir, wie sich der Stack des aktuellen Threads von einem privaten Speicherbereich zu einem anderen bewegt, während wir Fasern wechseln:
