Heutige Angreifer und mit Ihnen zusammen aktuelle Malware werden immer besser und daher schwieriger zu entdecken. Natürlich verbessern die Verteidiger ihre Werkzeuge ebenso stetig. So kommt es zu dem Wettrüsten, welches wir seit Jahren beobachten. Auch wenn Antivirensoftware versucht bei diesem Katz-und-Maus-Spiel mitzuhalten, ist dies immer nur verzögert möglich und man sollte sich auf keinen Fall zu sehr auf sie verlassen.
Wir wollen zeigen, wie simpel es teilweise ist Antiviruslösungen zu umgehen und nebenbei eine neue, elegante Programmiersprache namens “Nim” vorstellen, die sich bei Angreifern immer größerer Beliebtheit erfreut. Also, was macht Nim so attraktiv?
Dies hat gleich mehrere Gründe: die leicht zu erlernende, Python-ähnliche Syntax und der Workflow ermöglichen ein schnelles Prototyping, was für Angreifer der heutigen Zeit unerlässlich ist. Zusätzlich ermöglicht Nim Cross-Kompilierung für Windows, Linux, macOS und sogar die Nintendo Switch und unterstützt außerdem die Kompilierung in C, C++ und JavaScript. Zu guter Letzt erstellt Nim ausführbare Dateien in kleiner Größe, was sich besonders bei der Auslieferung von Schadprogrammen als hilfreich entpuppt.
Wer mehr über Nim erfahren möchte, für den sind die offizielle Website und die Dokumentation ein guter Ausgangspunkt.
Hello World!
Werfen wir nun einen kurzen Blick auf die Kompilierung von Nim-Code. Wegen der einfachen Syntax besteht das übliche “Hello World”-Beispiel in Nim aus genau einer Zeile:
hello.nim:
echo "Hello World"
Die Datei kann mit nim c hello.nim
kompiliert und anschließend ausgeführt werden:
$ ./hello
Hello World
An dieser Stelle kommen wir zum ersten großen Pluspunkt von Nim, der einfachen Cross-Kompilierung. Um ein Programm für Windows auf einem Linux-System zu kompilieren, muss MinGW-w64 auf dem System installiert sein. Falls dies der Fall ist, können wir einfach den Nim-Compiler mit dem Argument -d:mingw
starten und unser Code wird in eine ausführbare Windows-Datei kompiliert:
nim c -d:mingw hello.nim
Windows API
Für die Entwicklung von Red-Team-Tools mit Nim müssen wir die Möglichkeiten von Nim im Hinblick auf eines der wichtigsten Instrumente in unserem Werkzeugkasten bewerten: Die Windows-API. Auf die Windows-API kann erstaunlich einfach ohne externe Bibliotheken zugegriffen werden. Wir können beispielsweise die Funktion MessageBoxA
aus der user32.dll
laden, indem wir Nims “foreign function interface” und ein weiteres Nim-Feature namens “Pragmas” verwenden. In diesem Fall wird mit Hilfe des importc
Pragmas über Nims FFI die Funktion MessageBoxA
dynamisch geladen:
messagebox.nim:
# Definieren der erforderlichen Variablentypen
type
HANDLE* = int
HWND* = HANDLE
UINT* = int32
LPCSTR* = cstring
# Dynamisches Laden der Funktion "MessageBoxA" aus der user32.dll Bibliothek
proc MessageBox*(hWnd: HWND, lpText: LPCSTR, lpCaption: LPCSTR, uType: UINT): int32
{.discardable, stdcall, dynlib: "user32", importc: "MessageBoxA".}
# Anzeige der Nachrichtenbox
MessageBox(0, "Hello, world !", "Nim is Powerful", 0)
Der Code stammt aus OffensiveNim, einer umfangreichen Quelle für den Einstieg in die Entwicklung von Red-Team-Werkzeugen, die in Nim geschrieben sind. Das obige Beispiel kann mit der winim
-Bibliothek noch einfacher gestaltet werden:
messagebox_winim.nim:
import winim
MessageBox(0, "Hello, world !", "Nim is Powerful", 0)
Bei beiden Beispielen wird bei der Ausführung ein Dialogfenster angezeigt:
Erkennung und Antivirus
Um zu zeigen wie simpel Antivirusumgehung sein kann und um die Fähigkeiten von Nim in Bezug auf Red Teaming zu demonstrieren, werden wir uns zwei Beispiele ansehen: Reflective Code Loading (MITRE T1620) und Process Injection (MITRE T1055).
Zur Veranschaulichung haben wir eine Beispiel-Payload mit Metasploit generiert, die den Taschenrechner (calc.exe
) ausführen soll. Um die Integration mit Nim zu vereinfachen, kann die Formatoption csharp
für msfvenom
verwendet:
~ $ msfvenom -p windows/x64/exec CMD='calc.exe' -f csharp
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 276 bytes
Final size of csharp file: 1430 bytes
byte[] buf = new byte[276] {
0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
...
0x47,0x13,0x72,0x6f,0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,
0x63,0x2e,0x65,0x78,0x65,0x00 };
Reflective Code Loading (MITRE T1620)
Als erstes Beispiel für eine von Angreifern verwendete Technik, wollen wir uns Reflective Code Loading ansehen. Dabei wird das Nim-Programm Shellcode in seinen eigenen Speicherbereich laden und anschließend ausführen. Dabei gehen wir folgendermaßen vor:
- Allokation von Speicher mit Lese- und Schreibrechten via
VirtualAlloc
. - Kopieren des Shellcodes in den allokierten Speicherbereich (Mit Hilfe von Nim’s
copyMem
). - Ändern der Speicherberechtigungen, damit der Speicherbereich ausführbar wird (mit
VirtualProtect
). - Ausführen des Shellcodes mit
CreateThread
.
Sobald das Program ausgeführt wird, wird der Shellcode in den Speicher des Prozesses geladen und der Taschenrechner öffnet sich:
Process Injection (MITRE T1055)
Ähnlich einfach ist das Injizieren von Shellcode in einen anderen Prozess. Die Schritte hierbei sind wie folgt:
- Mit Hilfe von
OpenProcess
wird ein Handle für einen anderen Prozess geöffnet. - Durch
VirtualAllocEx
wird Speicherplatz in dem anderen Prozess allokiert. - Der Shellcode wird mit
WriteProcessMemory
in den allokierten Speicherbereich geladen. - Ändern der Speicherberechtigungen damit der Speicherbereich ausführbar wird (mit
VirtualProtectEx
). - Ausführen des Shellcodes mit
CreateRemoteThread
.
Beim Ausführen der Binärdatei wird der Shellcode in den Speicherbereich eines anderen Prozesses injiziert und ausgeführt. Beim Ausführen des Shellcodes wird erneut der Taschenrechner geöffnet:
Detektion & Umgehung von Antivirenprogrammen
Nun ändern wir die Payload in eine Meterpreter Reverse-Shell(windows/x64/meterpreter/reverse_tcp
) und wollen uns anschauen, ob diese von Antivirenlösungen erkannt wird. Zu diesem Zweck laden wir unsere ausführbare Datei auf VirusTotal hoch:
Da wir eine Standard-Metasploit-Payload verwendet haben, sollten die meisten Antivirenlösungen kein Problem haben, diese auch zu erkennen. Zu beachten ist aber, dass nicht alle Antivirenprogramme innerhalb von VirusTotal ihre vollen Funktionalitäten nutzen. Daher sollte das Ergebnis des VirusTotal-Scans nur als allgemeiner Indikator betrachtet werden. In diesem Fall werden die Nim-Anwendungen von etwa 30 % der Anbieter als schadhaft erkannt:
T1620.exe
(Reflective Code Loading): 23/71 (32%) Antiviruslösungen stufen diese Datei als schadhaft ein.T1055.exe
(Process Injection): 22/71 (31%) Antiviruslösungen stufen diese Datei als schadhaft ein.
In den folgenden Abschnitten versuchen wir, die Erkennungsrate zu senken, wie es ein echter Angreifer tun würde. Wir werden dabei einige Umgehungstechniken mit Nim ausprobieren und die VirusTotal-Ergebnisse mit dem obigen Benchmark vergleichen. Die Umgehungstechniken, die wir verwenden werden, sind:
- Erzeugen von Hintergrundrauschen
- Shellcode Verschlüsselung
- Minimierung der Programmgröße und Metainformationen
Erzeugen von Hintergrundrauschen
Um eine ausführbare Datei harmlos erscheinen zu lassen, könnten Angreifer zum Beispiel unnötigen und irreführenden Code hinzufügen. Dabei kann es sich um Webanfragen, Primzahlberechnungen, harmlose Windows-API-Aufrufe oder irgendetwas anderes handeln, das Zeit verschafft, bevor schadhafte Aktionen ausgeführt werden. Auf diese Weise kann oftmals die Heuristik von Antivirenprogrammen umgangen werden, bei der die zu untersuchende Anwendung für eine begrenzte Zeit in einer Sandbox-Umgebung ausgeführt wird.
Dies ist umso wichtiger, da wir im nächsten Schritt die Shellcode-Verschlüsselung hinzufügen werden. Eine Anwendung, die außer ein paar Ver- oder Entschlüsselungsoperationen nichts tut, ist für die meisten Antivirenlösungen recht schnell verdächtig (Stichwort: Ransomware). Konkret werden wir ein paar willkürliche Webanfragen und sinnlose Primzahlberechnungen als Hintergrundrauschen in die Programme einfügen.
Shellcode Verschlüsselung
Um zu vermeiden, dass bekannte Malwaresignaturen in einer Binärdatei enthalten sind, verschlüsseln Threat Actors oftmals den Shellcode innerhalb der Binärdatei und entschlüsseln ihn dann zur Laufzeit wieder. In Nim kann der Shellcode über die Bibliothek nimcrypto
relativ einfach ver- und entschlüsselt werden:
proc decrypt(key: string, iv: string, plain: seq[byte]): seq[byte] =
var decrypted: seq[byte] = plain
var ectx: CTR[aes256]
ectx.init(key, iv)
ectx.decrypt(plain, decrypted)
ectx.clear()
return decrypted
var decryptedBytes: seq[byte]
var keyString = "1234567890ABCDEF1234567890ABCDEF"
var ivString = "1234567890ABCDEF"
echo "[*] Decrypting"
decryptedBytes = decrypt(keyString, ivString, encryptedBytes)
Minimierung der Programmgröße und Metainformationen
Normalerweise wird jede verwendete Windows-API-Funktion in der Import Address Table (IAT) einer ausführbaren Datei referenziert. Angreifer versuchen, diese Einträge zu vermeiden, da sie von diversen Sicherheits- und Scanlösungen (z. B. EDRs) zur Malwarebekämpfung ausgelesen werden, um potenziell schadhaftes Verhalten zu identifizieren.
Wir stellen jedoch fest, dass es keine Anzeichen für CreateRemoteThread
in der IAT der Process-Injection-Binärdatei (T1055.exe
) gibt:
$ objdump -x -D T1055.exe | head -n 200
...
The Import Tables (interpreted .idata section contents)
vma: Hint Time Forward DLL First
Table Stamp Chain Name Thunk
0004e000 0004e03c 00000000 00000000 0004e798 0004e224
DLL Name: KERNEL32.dll
vma: Hint/Ord Member-Name Bound-To
4e40c 283 DeleteCriticalSection
4e424 319 EnterCriticalSection
4e43c 630 GetLastError
4e44c 710 GetProcAddress
4e45e 743 GetStartupInfoA
4e470 892 InitializeCriticalSection
4e48c 919 IsDBCSLeadByteEx
4e4a0 984 LeaveCriticalSection
4e4b8 988 LoadLibraryA
4e4c8 1036 MultiByteToWideChar
4e4de 1394 SetUnhandledExceptionFilter
4e4fc 1410 Sleep
4e504 1445 TlsGetValue
4e512 1486 VirtualAlloc
4e522 1489 VirtualFree
4e530 1492 VirtualProtect
4e542 1494 VirtualQuery
4e552 1547 WideCharToMultiByte
Das liegt an der Art und Weise, wie die Bibliothek winim
Bibliotheken und Funktionen auflöst. Bei Verwendung dieser Bibliothek werden die Funktionen dynamisch aufgelöst, so dass die IAT immer gleich aussieht.
Die Funktionsnamen, wie z.B. CreateRemoteThread
, werden jedoch trotzdem in der Binärdatei als Strings gespeichert:
$ strings T1055.exe | grep -B 5 -A 1 CreateRemoteThread
@kernel32
OpenProcess
VirtualAllocEx
WriteProcessMemory
VirtualProtectEx
CreateRemoteThread
@invalid format string, cannot parse:
Alles, was wir tun müssen, um die Funktionsnamen zu verbergen, ist die winim
-Bibliothek über Bord zu werfen, die Funktionen selbst dynamisch aufzulösen und eine Verschlüsselungsschicht hinzuzufügen. Für die Verschlüsselung kann die strenc
-Bibliothek verwendet werden. Sie verschlüsselt im Wesentlichen alle Strings innerhalb der Binärdatei zur Kompilierzeit mit einem individuellen Schlüssel für jeden String. Alles was wir dafür tun müssen, ist die Bibliothek zu importieren:
import std/strformat
import dynlib
import std/osproc
import nimcrypto
import std/httpclient
import strenc
...
Um die letzte Spur des Funktionsaufrufs zu entfernen, kann beim Kompilieren das Flag --passL:-s
verwendet werden, wodurch alle Symbole aus der Binärdatei entfernt werden. Um zu überprüfen, ob der String CreateRemoteThread
vollständig aus der ausführbaren Datei entfernt wurde, kann grep
verwendet werden:
$ strings T1055.exe | grep CreateRemoteThread
Durch dieses Symbol-Stripping gehen wertvolle Debuginformationen verloren, die ein Antivirus nutzen könnte um die Malware als solche zu identifizieren.
Weiterhin kann die Verringerung der Dateigröße der Anwendungsdatei für Angreifer von Vorteil sein, da dadurch die Handhabung der Schadprogramme erleichtert wird. Dies kann beispielsweise durch die Flags -d:release
(Erzeugung einer Releaseversion) oder -opt:size
(Optimierung der Binärdateigröße) erreicht werden.
Das Ergebnis
Nachdem alle oben genannten Maßnahmen implementiert wurden, stuft nur einer der Antivirenscanner auf VirusTotal unsere ausführbare Datei als schadhaft ein:
T1620.exe
(Reflective Code Loading): 1/69 (1%) Antiviruslösungen stufen diese Datei als schadhaft ein.T1055.exe
(Process Injection): 1/70 (1%) Antiviruslösungen stufen diese Datei als schadhaft ein.
Wie bereits erwähnt ist hier zu beachten, dass der VirusTotal-Score kein heiliger Gral ist, sondern nur als Orientierung dafür dient, wie gut Antivirenlösungen die ausführbare Datei als schadhaft einstufen können. Das Ergebnis zeigt jedoch, wie mächtig Nim aufgrund seiner schnellen Prototyping-Fähigkeiten, hilfreichen Bibliotheken und dem foreign function interface für heutige Angreifer sein kann und wie simpel es sein kann Antiviruslösungen zu umgehen.
Fazit
Nim bietet einen leistungsstarken Werkzeugkasten, der eine schnelle und effektive Malware-Entwicklung ermöglicht. Dies hilft den Red Teams, kosteneffizienter zu arbeiten, erhöht aber auch die Entwicklungsgeschwindigkeit der echten Angreifer. Die Verteidiger müssen daher mit dem hohen Tempo mithalten. Das bedeutet, dass nicht nur die Erkennung Schritt halten muss, sondern die Unternehmenssicherheit insgesamt. Unternehmen müssen heutzutage mit einem ganzheitlichen Ansatz zur Verteidigung ihrer gesamten IT-Infrastruktur auf einen Breach vorbereitet sein, denn in keinem System sollte es einen “Single Point of Failure” geben. Daher ist “Defense in Depth” eine der wichtigsten Säulen einer jeden Informationssicherheitsmanagementstrategie. Neben den klassischen Antivirus- und EDR-Lösungen kann dies zum Beispiel noch Folgendes umfassen:
- Geschulte Mitarbeiter
- Ausgereiftes Monitoring und Logging
- Ein gehärtetes Netzwerk
- Offline Backups
Zusammenfassend ist zu betonen, dass Verteidiger die vorgestellten Techniken nicht ungeachtet lassen sollten. Wie wir gesehen haben, kann mit simplen Mitteln ein Antivirenschutz umgangen werden, was durch eine Sprache wie Nim weiter vereinfacht wird. Daher ist die (alleinige) Verwendung eines Antivirenprogramms zum Schutz eines Systems nur bedingt ausreichend. Schlimmstenfalls erzeugt nicht anschlagende Antivirensoftware wie in unseren Beispielen nur ein falsches Gefühl der Sicherheit - sie kann umfassende, regelmäßige Schulungen und Penetrationstests in keinem Fall ersetzen.