
Thread Stack Spoofing – PoC für eine fortgeschrittene In-Memory-Verschleierungstechnik, die es ermöglicht, die Speicherallokation von eingeschleustem Shellcode besser vor Scannern und Analysten zu verbergen.
Eine PoC-Implementierung einer fortschrittlichen In-Memory-Evasion-Technik, die den Thread Call Stack spoofed. Diese Technik ermöglicht es, thread-basierte Speicheruntersuchungsregeln zu umgehen und Shellcodes besser im Prozessspeicher zu verbergen.
Dies ist eine Beispielimplementierung für die Thread-Stack-Spoofing-Technik, die darauf abzielt, Malware-Analysten, AVs und EDRs zu umgehen, die nach Referenzen auf Shellcode-Frames in einem untersuchten Thread-Call-Stack suchen. Die Idee ist, Referenzen auf den Shellcode im Thread-Call-Stack zu verbergen und so Allokationen mit Malware-Code zu tarnen.
Die Implementierung zusammen mit meiner ShellcodeFluctuation bringt der Offensive-Security-Community Beispielimplementierungen, um mit dem Angebot kommerzieller C2-Produkte Schritt zu halten, damit wir in unseren Red-Team-Tools nicht schlechter dastehen. 💪
Die aktuelle Implementierung unterscheidet sich stark von der ursprünglich veröffentlichten.
Das liegt daran, dass mir klar wurde, dass es einen viel einfacheren Ansatz gibt, um den Call-Stack-Prozess eines Threads zu beenden und shellcodebezogene Frames zu verstecken, indem man einfach 0 an die Rücksprungadresse des ersten Frames schreibt, den wir kontrollieren:
void WINAPI MySleep(DWORD _dwMilliseconds)
{
[...]
auto overwrite = (PULONG_PTR)_AddressOfReturnAddress();
const auto origReturnAddress = *overwrite;
*overwrite = 0;
[...]
*overwrite = origReturnAddress;
}
Die vorherige Implementierung, die StackWalk64 nutzt, kann in diesem Commit c250724 eingesehen werden.
Diese Implementierung ist viel stabiler und funktioniert gut sowohl unter Debug als auch Release auf zwei Architekturen - x64 und x86.
So könnte ein Call-Stack aussehen, wenn er NICHT gespooft ist:

Dies hingegen, wenn Thread-Stack-Spoofing aktiviert ist:

Oben sehen wir, dass der letzte Frame in unserem Call-Stack unser MySleep-Callback ist.
Man mag sich fragen, ob dies sofort neue IOCs mit sich bringt? Jagdregeln können nach Threads suchen, deren Call-Stacks nicht in die folgenden erwarteten Thread-Eintrittspunkte in Systembibliotheken entladen:
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
Der Call-Stack des gespooften Threads mag auf den ersten Blick seltsam aussehen, eine kurze Untersuchung meines Systems zeigte jedoch, dass es andere Threads gibt, die ebenfalls nicht zu den obigen Eintrittspunkten entladen:

Der obige Screenshot zeigt einen Thread des unveränderten Total Commander x64. Wie wir sehen, ähnelt sein Call-Stack unserem in Bezug auf die initialen Call-Stack-Frames sehr.
Warum sollten wir uns darum kümmern, unseren Call-Stack sorgfältig zu fälschen, wenn es Prozesse gibt, die Eigenschaften zeigen, die wir einfach nachahmen können?
Der grobe Algorithmus ist wie folgt:
dbghelp.dll, rufe SymInitialize auf.kernel32!Sleep, um auf unseren Callback zu verweisen.VirtualAlloc + memcpy + CreateThread. Der Thread sollte von unserer runShellcode-Funktion starten, um zu vermeiden, dass die StartAddress des Threads auf etwas Unerwartetes und Anomales zeigt (wie ntdll!RtlUserThreadStart+0x21).MySleep-Callback aufgerufen.0, was effektiv den Call-Stack beenden sollte.::SleepEx getätigt, um den Beacon schlafen zu lassen, während auf weitere Kommunikation gewartet wird.Funktionsrücksprungadressen sind über den gesamten Stack-Speicherbereich des Threads verstreut, auf den das RBP/EBP-Register zeigt.
Um sie auf dem Stack zu finden, müssen wir zuerst Frame-Zeiger sammeln und sie dann dereferenzieren, um sie zu überschreiben:

(Das obige Bild wurde aus Eli Benderskys Beitrag namens Stack frame layout on x86-64 entliehen)
*(PULONG_PTR)(frameAddr + sizeof(void*)) = Fake_Return_Address;
Die anfängliche Implementierung von ThreadStackSpoofer tat dies in den Funktionen walkCallStack und spoofCallStack, jedoch zeigt die aktuelle Implementierung, dass diese Bemühungen nicht erforderlich sind, um einen verdeckten Call-Stack aufrechtzuerhalten.
Anwendungsfall:
C:\> ThreadStackSpoofer.exe <shellcode> <spoof>
Wobei:
<shellcode> ein Pfad zur Shellcode-Datei ist<spoof> bei 1 oder true das Thread-Stack-Spoofing aktiviert und alles andere deaktiviert es.Beispiellauf, der den Thread-Call-Stack des Beacons spoofed:
PS D:\dev2\ThreadStackSpoofer> .\x64\Release\ThreadStackSpoofer.exe .\tests\beacon64.bin 1
[.] Reading shellcode bytes...
[.] Hooking kernel32!Sleep...
[.] Injecting shellcode...
[+] Shellcode is now running.
[>] Original return address: 0x1926747bd51. Finishing call stack...
===> MySleep(5000)
[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...
===> MySleep(5000)
[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...
Schauen Sie sich den Code und seine Implementierung an, verstehen Sie das Konzept und implementieren Sie es in Ihren eigenen Shellcode-Ladern, die Sie für Ihre Red-Team-Einsätze verwenden. Dies ist eine weitere Technik für fortgeschrittene In-Memory-Evasion, die die Chancen Ihres Teams erhöht, nicht von Antivirenprogrammen, EDRs und Malware-Analysten erwischt zu werden, die einen Blick auf Ihre Implants werfen.
Während der Entwicklung Ihres fortschrittlichen Shellcode-Laders möchten Sie vielleicht auch Folgendes implementieren:
BeaconEye zu umgehen.RW (von RX/RWX) und verschlüsseln Sie deren Inhalt - unter Verwendung der Shellcode Fluctuation-Technik - direkt vor dem Schlafen (das könnte Scanner wie Moneta oder pe-sieve umgehen).Wie mir mitgeteilt wurde, wird die Technik hier ihrem Namen als Stack-Spoofer noch nicht wirklich gerecht. Da wir lediglich Rücksprungadressen auf dem Stack des Threads überschreiben, spoofen wir nicht die verbleibenden Bereiche des Stacks selbst. Darüber hinaus hinterlassen wir unseren Call-Stack unwindable, was ihn anomal erscheinen lässt, da das System nicht in der Lage sein wird, die gesamte Call-Stack-Frame-Kette ordnungsgemäß zu durchlaufen.
Ich bin mir dieser Mängel jedoch bewusst, habe es im Moment so belassen, da es mir hauptsächlich darum ging, automatisierte Scanner zu umgehen, die Prozesse durchlaufen, ihre Threads aufzählen, deren Stacks durchgehen und jede Rücksprungadresse erkennen könnten, die auf einen Nicht-Image-Speicher verweist (wie SEC_PRIVATE - den, der dynamisch von VirtualAlloc und Konsorten allokiert wird). Ein fokussierter Malware-Analyst würde die Seltsamkeit sofort erkennen und den Thread als eher ungewöhnlich betrachten und unser Implant aufspüren. Da bin ich mir mehr als sicher. Ich glaube jedoch nicht, dass heutige automatisierte Scanner wie AV/EDR Heuristiken implementiert haben, die tatsächlich den Stack jedes Threads durchgehen, um zu überprüfen, ob er entwindbar ist ¯\_(ツ)_/¯ .
Sicherlich gibt dieses Projekt (und die kommerzielle Implementierung in C2-Frameworks) AV- und EDR-Anbietern Argumente, darüber nachzudenken, geeignete Heuristiken zu implementieren, die eine solche neuartige Evasionstechnik abdecken.
Um diese Technik zu verbessern, kann man einen echten Thread-Stack-Spoofer anstreben, indem man sorgfältig erstellte falsche Stack-Frames einfügt, die in einem Reverse-Unwinding-Prozess eingerichtet werden. Lesen Sie mehr zu dieser Idee unten.
Ein stundenlanges Gespräch mit namazso lehrte mich, dass wir, um einen richtigen Thread-Stack-Spoofer zu erreichen, den x64-Call-Stack-Unwinding-Prozess umkehren müssten. Zunächst muss man den Stack-Unwinding-Prozess sorgfältig verstehen, der in (a) unten verlinkt erklärt wird. Das System verlässt sich beim Durchlaufen des Thread-Call-Stacks auf der x64-Architektur nicht einfach auf die über den Stack verstreuten Rücksprungadressen, sondern es:
RUNTIME_FUNCTION, UNWIND_INFO und UNWIND_CODE-Strukturen zurück. Diese Strukturen beschreiben, wo die Anfangs- und Endadresse der Funktion sind und wo sich alle Codesequenzen befinden, die RBP oder RSP modifizieren.UNWIND_CODEs, die die untersuchte Funktion aufweist, um die genaue Position des Rücksprungadresse dieses Frames und den Stack-Pointer-Wert zu berechnen.Um in diesen Prozess einzugreifen, müssten wir ihn umkehren, indem wir eine umgekehrte Form von RtlVirtualUnwind hätten. Wir müssten über die in einem Modul (sagen wir kernel32) definierten Funktionen iterieren, die UNWIND_CODE-Codes jeder Funktion scannen und sie rückwärts genau emulieren (im Vergleich zu RtlVirtualUnwind und genau RtlpUnwindPrologue), um Stellen auf dem Stack zu finden, an die wir unsere gefälschten Rücksprungadressen setzen können.
namazso erwähnt die Notwendigkeit, 3 gefälschte Stack-Frames einzuführen, um den Call-Stack sauber zusammenzunähen:
MySleep (mit einem anderen UWOP - Unwind-Operation-Code). Wir tun dies, indem wir alle Funktionen eines Moduls durchgehen, ihre UWOPs untersuchen und berechnen, wie groß das gefälschte Frame sein sollte. Dieses Frame muss UWOPs haben, die anders sind als die des Aufrufers von MySleep.RBP poppt - im Grunde durch den UWOP_PUSH_NONVOL-Code.RSP von RBP wiederherstellt, und zwar durch den Code UWOP_SET_FPREG.Das wiederhergestellte RSP muss mit dem RSP gesetzt werden, das von dem Punkt genommen wird, an dem der Kontrollfluss in unser MySleep eingetreten ist, so dass alle unsere Frames verborgen werden, als Ergebnis des Unwindings des dritten Gadgets dort.
Um den Prozess zu beginnen, kann man über das .pdata des ausführbaren Programms iterieren, indem man den Datenverzeichniseintrag IMAGE_DIRECTORY_ENTRY_EXCEPTION dereferenziert. Betrachten Sie das folgende Beispiel:
ULONG_PTR imageBase = (ULONG_PTR)GetModuleHandleA("kernel32");
PIMAGE_NT_HEADERS64 pNthdrs = PIMAGE_NT_HEADERS64(imageBase + PIMAGE_DOS_HEADER(imageBase)->e_lfanew);
auto excdir = pNthdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
if (excdir.Size == 0 || excdir.VirtualAddress == 0)
return;
auto begin = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase);
auto end = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase + excdir.Size);
UNWIND_HISTORY_TABLE mshist = { 0 };
DWORD64 imageBase2 = 0;
PRUNTIME_FUNCTION currFrame = RtlLookupFunctionEntry(
(DWORD64)caller,
&imageBase2,
&mshist
);
UNWIND_INFO *mySleep = (UNWIND_INFO*)(currFrame->UnwindData + imageBase);
UNWIND_CODE myFrameUwop = (UNWIND_CODE)(mySleep->UnwindCodes[0]);
log("1. MySleep RIP UWOP: ", myFrameUwop.UnwindOpcode);
for (PRUNTIME_FUNCTION it = begin; it < end; ++it)
{
UNWIND_INFO* unwindData = (UNWIND_INFO*)(it->UnwindData + imageBase);
UNWIND_CODE frameUwop = (UNWIND_CODE)(unwindData->UnwindCodes[0]);
if (frameUwop.UnwindOpcode != myFrameUwop.UnwindOpcode)
{
// Found candidate function for a desynch gadget frame
}
}
Der Prozess ist etwas verworren, läuft aber darauf hinaus, den Unwinding-Prozess des Thread-Call-Stacks umzukehren, indem beliebige Stack-Frames durch sorgfältig ausgewählte andere ersetzt werden, ähnlich einem ROP-Ansatz.
Dieses PoC repliziert diesen Algorithmus nicht, da mein derzeitiges Verständnis es mir erlaubt, den auf einem EXE-basierten Stack-Frame endenden Call-Stack zu akzeptieren und ich weder meine Shellcode-Lader noch dieses PoC übermäßig verkomplizieren möchte. Ich überlasse die Übung, dies zu implementieren und öffentlich zu teilen, einem interessierten Leser. Oder vielleicht setze ich mich hin und versuche es selbst, wenn ich etwas mehr Freizeit habe :)
Weitere Informationen:
Wenn Sie planen, diese Funktionalität zu Ihren eigenen Shellcode-Ladern / Tools hinzuzufügen, stellen Sie sicher, dass Sie VERMEIDEN, kernel32.dll zu enthooken.
Ein Versuch, kernel32 zu enthooken, stellt die ursprüngliche Sleep-Funktionalität wieder her und verhindert, dass unser Callback aufgerufen wird.
Wenn unser Callback nicht aufgerufen wird, kann der Thread seinen eigenen Call-Stack nicht selbst spoofen.
Wenn Sie das möchten, müssen Sie möglicherweise einen weiteren Watchdog-Thread ausführen, der sicherstellt, dass der Beacon-Thread jedes Mal gespooft wird, wenn er schläft.
Wenn Sie Cobalt Strike und ein BOF unhook-bof von Raphael Mudge verwenden, sehen Sie sich unbedingt meinen Pull Request an, der dem BOF einen optionalen Parameter hinzufügt, um Bibliotheken anzugeben, die nicht enthookt werden sollen.
Auf diese Weise können Sie Ihre Hooks in kernel32 beibehalten:
beacon> unhook kernel32
[*] Running unhook.
Will skip these modules: wmp.dll, kernel32.dll
[+] host called home, sent: 9475 bytes
[+] received output:
ntdll.dll <.text>
Unhook is done.
Dieses PoC wurde entwickelt, um mit Cobalt Strike Beacon Shellcodes zu arbeiten. Der Beacon ruft bekanntermaßen kernel32!Sleep auf, um auf weitere Anweisungen von seinem C2 zu warten. Dieser Lader nutzt diese Tatsache aus, indem er Sleep hookt, um seine Hausarbeiten zu erledigen.
Diese Implementierung funktioniert möglicherweise nicht mit anderen Shellcodes auf dem Markt (wie Meterpreter), wenn sie Sleep nicht zum Abkühlen verwenden. Da dies nur ein Proof of Concept zur Demonstration der Technik ist, habe ich nicht vor, Unterstützung für andere C2-Frameworks hinzuzufügen.
Wenn Sie das Konzept verstanden haben, werden Sie es sicherlich auf Ihre Shellcode-Anforderungen übertragen und die Lösung zu Ihrem Vorteil anpassen können.
Bitte eröffnen Sie keine Github-Issues mit "dieser Code funktioniert nicht mit XYZ-Shellcode", sie werden sofort geschlossen.
Dieses und andere Projekte sind das Ergebnis schlafloser Nächte und viel harter Arbeit. Wenn Ihnen gefällt, was ich tue, und Sie schätzen, dass ich immer etwas an die Community zurückgebe, erwägen Sie, mir einen Kaffee zu kaufen (oder besser ein Bier), nur um Danke zu sagen! 💪
Mariusz Banach / mgeeky, 21
<mb [at] binary-offensive.com>
(https://github.com/mgeeky)