Nim dir Zeit für Malware Detection!

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
Screenshot von der Ausführung des Programs hello.exe

Ausführung einer cross-kompilierten Anwendung.

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:

Bild der Ausführung der Anwendung messagebox-winim.exe, die ein Dialogfenster über die Windows-API erstellt

Nim ruft die Windows-API auf, um ein Dialogfenster anzuzeigen.

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:

  1. Allokation von Speicher mit Lese- und Schreibrechten via VirtualAlloc.
  2. Kopieren des Shellcodes in den allokierten Speicherbereich (Mit Hilfe von Nim’s copyMem).
  3. Ändern der Speicherberechtigungen, damit der Speicherbereich ausführbar wird (mit VirtualProtect).
  4. 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:

Screenshot der Ausführung der Anwendung T1620.exe, die einen Taschenrechner öffnet.

Ausführen von beliebigem Code mit Hilfe von Reflective Code Loading (T1620).

Process Injection (MITRE T1055)

Ähnlich einfach ist das Injizieren von Shellcode in einen anderen Prozess. Die Schritte hierbei sind wie folgt:

  1. Mit Hilfe von OpenProcess wird ein Handle für einen anderen Prozess geöffnet.
  2. Durch VirtualAllocEx wird Speicherplatz in dem anderen Prozess allokiert.
  3. Der Shellcode wird mit WriteProcessMemory in den allokierten Speicherbereich geladen.
  4. Ändern der Speicherberechtigungen damit der Speicherbereich ausführbar wird (mit VirtualProtectEx).
  5. 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:

Screenshot der Ausführung der Anwendung T1055.exe, die einen Taschenrechner öffnet.

Ausführen von beliebigem Code über Process Injection (T1055).

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:

Screenshot des VirusTotal-Scans der Datei T1620.exe. 23 von 71 Antivirensanbietern stufen diese Datei als schadhaft ein.

23 von 71 Antivirensanbietern stufen die Datei T1620.exe als schadhaft ein.

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.

Screenshot des VirusTotal-Scans der Datei T1620.exe. 1 von 69 Antivirenscannern stuft diese Datei als schadhaft ein.

Eine von 69 Sicherheitslösungen stuft die Datei T1620.exe als schadhaft ein.

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 Entwicklungs­geschwindigkeit 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 Informations­sicherheits­management­strategie. 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.

Referenzen und weiterführende Informationen