
Eine speicherbasierte Umgehungstechnik, die Shellcode vom Prozessstart bis zum Ende unsichtbar macht.
Eine speicherbasierte Ausweichtechnik, die Shellcode vom Prozessstart bis zum Ende unsichtbar macht.
Ich wollte diesen POC zur Shellcode-Selbstinjektion teilen, um einige AV/EDR-Umgehungskonzepte zu demonstrieren, die für Red Teaming nützlich sein können. Vor ein paar Wochen habe ich eine benutzerdefinierte In-Memory-Umgehungstechnik entwickelt, die ich ShellGhost genannt habe. Diese Technik entstand aus der Notwendigkeit, einen Code zu haben, der einen 'unsichtbaren' Shellcode vom Prozessstart bis zum Ende ausführt.
ShellGhost nutzt Vectored Exception Handling in Kombination mit Software-Breakpoints, um die Thread-Ausführung zyklisch zu stoppen, den ausgeführten Breakpoint durch eine RC4-verschlüsselte Shellcode-Anweisung zu ersetzen, die Anweisung zu entschlüsseln und die Ausführung nach Wiederherstellung des Speicherschutzes auf RX fortzusetzen. Wenn der nachfolgende EXCEPTION_BREAKPOINT ausgelöst wird, ersetzt der Exception-Handler die vorherige Shellcode-Anweisung durch einen neuen Breakpoint, sodass die Zuordnung den vollständigen Shellcode niemals im unverschlüsselten Zustand preisgibt. Dies geschieht innerhalb einer privaten Speicherseite, die zunächst als READ/WRITE markiert ist.
Eine RW-PRV-Zuordnung wird von Speicherscannern wie PE-Sieve und Moneta nicht als 'Indicator of Compromise' betrachtet. Wenn die Zuordnung zu RX wechselt und die Seite gescannt wird, werden nur Breakpoints gefunden. Dies geschieht, während der Shellcode tatsächlich ausgeführt wird. Das folgende Bild zeigt, dass eine Reverse Shell läuft, aber von Moneta kein IOC gefunden wird (abgesehen davon, dass die Binärdatei nicht signiert ist).

Der Versuch, den Prozess mit Pe-Sieve zu scannen, hat ein noch besseres Ergebnis:

Shellcode Mapping ist die Kernfunktionalität von ShellGhost. Diese Taktik ermöglicht es dem Thread, Anweisungen zeitweise auszuführen, ohne den gesamten Shellcode jemals im Speicher preiszugeben. Dies ist möglich, weil die Position jeder einzelnen Shellcode-Anweisung, die der Thread ausführt, der Position eines bestimmten Breakpoints innerhalb der zugewiesenen Speicherseite entspricht. ShellGhost löst diese Position, indem es die Relative Virtuelle Adresse (RVA) vom Thread-RIP zur Basisadresse der zugewiesenen Speicherseite berechnet und sie zur Basisadresse des verschlüsselten Shellcodes / der verschlüsselten Anweisungen addiert. Die Anzahl der zu ersetzenden Breakpoints ist nicht immer gleich, sondern variiert je nach Anzahl der Opcodes, die jede Anweisung benötigt, um korrekt generiert und interpretiert zu werden (QUOTA). So ist zum Beispiel die Anweisung 'POP RBP' gleich '5D', was bedeutet, dass nur ein Breakpoint ersetzt wird. Im Gegensatz dazu erfordert die Anweisung 'JMP RAX' die Opcodes 'FF E0', sodass zwei Breakpoints ersetzt werden. Aus diesem Grund habe ich die folgende C-Datenstruktur erstellt.
typedef struct CRYPT_BYTES_QUOTA {
DWORD RVA; // offset to encrypted instruction
DWORD quota; // number of opcodes that generate the instruction
} CRYPT_BYTES_QUOTA, * PCRYPT_BYTES_QUOTA;
Breakpoints werden nicht sofort durch ihre Anweisungsgegenstücke ersetzt. Dies liegt daran, dass die Anweisungen vor der Ausführung eine Entschlüsselungsroutine durchlaufen müssen. Hier kommt der DWORD quota ins Spiel. ShellGhost nutzt die mittlerweile beliebte 'SystemFunction032', um RC4-Entschlüsselung durchzuführen. Im Gegensatz zu XOR ist RC4 kein Einzelbyte-Verschlüsselungsschema. Das bedeutet, dass der Shellcode nicht auf einmal verschlüsselt und entschlüsselt werden kann. Dies ist ein weiterer Grund, warum jede Anweisung separat behandelt wird. Nachdem die Breakpoints ersetzt wurden, ist die Pufferlänge, die SystemFunction032 benötigt, gleich dem 'instruction quota', das wiederum die Anzahl der Opcodes darstellt, aus denen die spezifische Anweisung besteht. Betrachten Sie zum Beispiel das folgende Snippet.
CRYPT_BYTES_QUOTA instruction[200];
instruction[5].quota = 2
USTRING buf = { 0 }; // will contain the buffer to be decrypted and its length
USTRING key = { 0 }; // will contain the RC4 key and length
buf.Length = 2 // buffer length, or length of the instruction to be decrypted
Wir wissen, dass Shellcode-Anweisung Nummer 5 aus 2 Opcodes besteht, daher wird eine Pufferlänge von 2 an SystemFunction032 übergeben. Dies ist wichtig, da der Versuch, den gesamten Shellcode mit einem einzigen Aufruf von SystemFunction032 zu entschlüsseln, diesen vollständig beschädigen würde.
Der Shellcode muss vor der Kompilierung mit ShellGhost_mapping.py gemappt werden. Das Skript extrahiert jede einzelne Anweisung und behandelt sie als kleinen, unabhängigen Shellcode. Die Anweisungen werden eine nach der anderen verschlüsselt und alle zusammen im C-Format als unsigned char ausgegeben. Das Ergebnis kann in den C-Code hartcodiert werden. Nachfolgend ein Beispiel dafür, wie verschlüsselte MSF-Shellcode-Anweisungen für calc.exe aussehen.

Dieser Shellcode hat 98 Anweisungen, daher werden 98 CRYPT_BYTES_QUOTA-Strukturen deklariert. Wenn der Code ausgeführt wird, müssen diese Strukturen mit den entsprechenden Anweisungs-RVAs und QUOTAs gefüllt werden. Der Parameter '-1' weist das Mapping-Skript an, den Code-Abschnitt auszugeben, der dies erledigt.

Metasploit-x64-Shellcodes haben typischerweise WinAPI-String-Parameter, die zwischen den Anweisungen gespeichert sind. Mit anderen Worten, ein MSF-x64-Shellcode, der Winexec aufruft, schiebt keine Reihe von Bytes mit einem Nullbyte am Ende, um den ersten Parameter-String auf dem Stack zu haben. Stattdessen ist das RCX-Register (erster Parameter) ein Zeiger innerhalb des Shellcodes selbst, wie im folgenden Bild.
