Zurück

Syscalls via Vectored Exception Handling

Im Bereich der Malware-Entwicklung bin ich immer wieder auf den Begriff Vectored Exception Handling bzw. Vectored Exception Handlers (kurz VEH) gestoßen, konnte aber bisher mit dem Begriff bzw. dem Thema nicht wirklich etwas anfangen. Während meiner Vorbereitungen für meinen kommenden Endpoint Security Insights Workshop bin ich im folgendem Artikel von cyberwarfare erneut über den Begriff Vectored Exception Handling gestolpert. Der Artikel hat meine Neugier geweckt und mich motiviert, mehr über das Thema zu erfahren. Wie immer lerne ich am besten, wenn ich selbst über ein Thema schreibe, eine Präsentation oder ähnliches vorbereite. 

Basierend auf dem Artikel von cyberwarfare möchte ich auch das Thema Vectored Exception Handling im Zusammenhang mit Shellcode Execution via syscalls aufgreifen und einen genaueren Blick auf den dafür benötigten Code werfen. 

Vorab möchte ich mich noch bei meinen beiden Kollegen Jonas Kemmner und Robert Rostek bedanken, die mich immer tatkräftig unterstützen und meine Artikel vor der Veröffentlichung Korrektur lesen.

Disclaimer


Der Inhalt dieses Artikels ist ausschließlich für Forschungszwecke bestimmt und darf nicht in einem unethischen oder illegalen Zusammenhang verwendet werden!

EDR Hooks and Evasion 

Grundsätzlich gibt es verschiedene Arten des API Hookings, eine häufig verwendete Form von EDRs wie CrowdStrike, Sentinel One, Trend Micro etc. ist das Inline API Hooking. Vereinfacht ausgedrückt wird bei dieser Variante der Execution Flow einer User-Mode-Anwendung durch eine 5 Byte lange unkonditionierte Jump Instruction jmp auf den EDR umgeleitet. Durch diese Umleitung hat ein EDR die Möglichkeit, eine dynamische Analyse der ausgeführten Anwendung im Kontext der Windows APIs durchzuführen und auf schädliches Verhalten zu prüfen. 

Vereinfacht kann man sich das Inline API Hooking als eine Art Proxy auf Prozessebene vorstellen. Erst wenn der EDR den ausgeführten Code und Parameter als nicht schädlich bewertet, erfolgt der return zur eigentlichen Funktion und die Ausführung des für den Übergang vom User- in den Kernel-Mode notwendigen syscalls.

 
Aus Sicht des Red Teams bzw. eines böswilligen Angreifers möchte man natürlich vermeiden, dass die eigene Malware auf diese Weise vom EDR analysiert, möglicherweise erkannt und an der Ausführung gehindert wird. Aus diesem Grund sind Malware-Entwickler in den letzten Jahren sehr kreativ geworden und können mittlerweile auf eine Vielzahl unterschiedlicher User-Mode Hooking Evasion Techniken zurückgreifen. Beispielsweise kann ein Angreifer versuchen, die vom User-Mode Hooking betroffene DLL, z.B. ntdll.dll oder kernel32.dll, durch verschiedene Techniken zu unhooken bzw. zu patchen.  

Alternativ oder ergänzend können auch Techniken wie direct oder indirect syscalls verwendet werden. Für die Implementierung, z.B. in einem Shellcode Loader, werden anstelle der Windows APIs die entsprechenden nativen APIs verwendet, z.B. NtAllocateVirtualMemory() ersetzt VirtualAlloc(). Durch die direkte Implementierung der nativen API bzw. des Syscall Stubs der nativen API muss der Shellcode Loader nicht mehr auf die kernel32.dll und ntdll.dll zugreifen und kann somit die Usermode Hooks umgehen. Es sein noch angemerkt, dass EDRs ihre Hooks in anderen DLLs wie z.B. user32.dll, win32u.dll, kernelbase.dll etc. platzieren. Die Gesamtanzahl der platzierten Hooks variiert stark von EDR zu EDR. So gibt es EDRs, die insgesamt 30 Hooks platzieren, während andere EDRs bis zu 80 Hooks und mehr verwenden. 

Je nachdem, ob direct- oder indirect syscalls verwendet werden, unterscheidet sich der Speicherbereich, in dem die syscall und return Anweisungen der verwendeten Native APIs ausgeführt werden. Werden direct syscalls verwendet, so erfolgt eine direkte Implementierung des kompletten syscall stubs mittels Assembly Instruktionen in der Malware. Entsprechend erfolgt die Ausführung der syscall und return Instruktion innerhalb des Speicherbereichs der Malware (.exe).

.CODE  ; direct syscalls assembly code 
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
    mov r10, rcx                 ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
    mov eax, 18h                 ; Move the syscall number into the eax register.
    syscall                      ; Execute syscall.
    ret                          ; Return from the procedure.
NtAllocateVirtualMemory ENDP     ; End of the procedure

Das Problem aus Sicht eines Malwareentwicklers hierbei: Erfolgt eine direkte Ausführung eines system calls (direct syscall) unter Windows durch eine Usermode Applikation, so führt dies aus Sicht eines EDR zu einem eindeutigen Indicator of Compromise (IOC). In diesem Fall kann beispielsweise der Thread Call Stack innerhalb einer Applikation (Malware) mittels Event Tracing for Windows (ETW) analysiert werden. Die folgende Abbildung zeigt die Anomalie der Stack Frames innerhalb des Thread Call Stacks einer Malware, die direct syscalls verwendet, bzw. die unterschiedliche Anordnung der Stack Frames im Vergleich zu einer legitimen Applikation. 


Um dieses Problem zu umgehen bzw. den Thread Call Stack innerhalb einer Malware legitimer zu gestalten, wurde eine Weiterentwicklung von direct syscalls zu indirect syscalls vorgenommen. Durch die Verwendung von indirect syscalls wird erreicht, dass die Ausführung der syscall und return Instruktion innerhalb des syscall stubs im Speicher der ntdll.dll erfolgt. Dieses Verhalten ist unter Windows legitim und im Vergleich zu direct syscalls wird durch indirect syscalls eine höhere Legitimität des Thread Call Stacks erreicht. 

Dies kann programmtechnisch unter Assembler mit Hilfe einer unkonditionierten jump Instruktion jmp erreicht werden. Nachdem die System Service Number kurz SSN anhand der mov Instruktion in das eax Register verschoben wurde, erfolgt via jmp Instruktion eine Umleitung in den Speicherbereich der ntdll.dll. Anschließend erfolgt die Ausführung der syscall und return Instruktion am Ende des syscall stubs innerhalb des Speicherbereichs der ntdll.dll

.CODE  ; indirect syscalls assembly code
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
    mov r10, rcx                  ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
    mov eax, 18h                  ; Move the syscall number into the eax register.
    jmp QWORD PTR [sysAddrNtAllocateVirtualMemory]  ; Jump to the actual syscall memory address in ntdll.dll
NtAllocateVirtualMemory ENDP      ; End of the procedure

Das Konzept von indirect syscalls, d.h. die Ausführung von syscall und return Statement im Kontext einer spezfischen Nativen API innerhalb des Speichers der ntdll.dll, kann jedoch nicht nur durch die Implementierung von Assembly Code unter C erreicht werden. Das gleiche Verhalten kann auch durch die Verwendung von Vectored Exception Handling realisiert werden. Wie dies z.B. unter C im Kontext eines Shellcode-Loaders funktioniert, soll in diesem Artikel basierend auf dem Artikel von Cyberwarfare erläutert werden.

Vectored Exception Handling

Vectored Exception Handling (VEH) wurde mit Windows XP eingeführt und ist Teil des Exception Handling Mechanismus, der Fehler (z.B. Division durch Null) und ungewöhnliche Zustände oder Ausnahmen (z.B. unzulässiger Speicherzugriff) behandelt, die während der Ausführung eines Programms auftreten können. Vectored Exception Handling ist Teil des umfassenderen Windows Structured Exception Handling (SEH) Frameworks. Im Gegensatz zu SEH, das spezifisch für eine Funktion oder einen Codeblock definiert ist, ist VEH global für die gesamte Anwendung und wird beim Auftreten eines Fehlers während der Programmausführung vor den Standard Structured Exception Handlern aufgerufen.

Die Implementierung des Handlers erfolgt über PVECTORED_EXCEPTION_HANDLER, der Aufruf bzw. das Registrieren über die Windows API AddVectoredExceptionHandler und das De-registrieren durch RemoveVectoredExceptionHandler. Mit dem Member ExceptionCode kann innerhalb der EXCEPTION_RECORD Struktur festgelegt werden, durch welche Exception der Handler ausgelöst werden soll. Mit Vectored Exception Handling können Entwickler eine individuelle und spezifische Logik für die Behandlung von Exceptions wie z.B. EXCEPTION_ACCESS_VIOLATION, EXCEPTION_BREAKPOINT, EXCEPTION_FLT_DIVIDE_BY_ZERO etc. implementieren und damit eine größere Kontrolle darüber erlangen, wie ein Programm in verschiedenen Fehlerszenarien reagiert. 

Der folgende C-Code zeigt beispielhaft, wie die Definition einer VEH-Funktion mittels VectoredExceptionHandler aussehen kann. Der Code zeigt auch, wie der Vector Exception Handler innerhalb der Main-Funktion mit AddVectoredExceptionHandler() und RemoveVectoredExceptionHandler() registriert und desregistriert werden kann.

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

// Prototype of the VEH function
LONG CALLBACK VectoredExceptionHandler(EXCEPTION_POINTERS *ExceptionInfo);

// Implementation of the VEH function
LONG CALLBACK VectoredExceptionHandler(EXCEPTION_POINTERS *ExceptionInfo) {
    // Check if it's an access violation
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
        printf("Access violation detected!\n");
        // Handle the exception here
        // ...
    }

    // Additional exceptions can be handled here
    // ...

    // EXCEPTION_CONTINUE_SEARCH indicates that the next handler function should be called
    return EXCEPTION_CONTINUE_SEARCH;
}

int main() {
    // Add the Vectored Exception Handler
    PVOID handle = AddVectoredExceptionHandler(1, VectoredExceptionHandler);

    // Normal code can be added here
    // ...

    // Remove the Vectored Exception Handler before exiting the program
    RemoveVectoredExceptionHandler(handle);

    return 0;
}

Aber auch Red Teams und Angreifer machen sich Vectored Exception Handling zunutze und können durch die Implementierung in ihre Malware z.B. den Programmablauf verschleiern oder eine gezeilte Ausführung von Shellcode durch VEH erreichen. So zeigt beispielsweise der folgende Artikel von CrowdStrike oder der Artikel von Elastic Security Labs sehr schön, wie die Malware GULOADER Vectored Exception Handling nutzt, um den Programmablauf zu verschleiern (Anti-Debugging) und damit die manuelle Analyse mittels Reverse Engineering zu erschweren.

Vectored Syscalls 

Wie bereits erwähnt, soll in diesem Artikel untersucht werden, wie eine Implementierung von Vectored Exception Handling in einem Shellcode Loader zur Ausführung von Shellcode mittels syscalls aussehen kann. Als Grundlage für meinen Shellcode Loader habe ich den Code von cyberwarefare verwendet, der auf Github zu finden ist. Da ich aus OPSEC Gründen Remote Injection bestmöglich vermeide, habe ich den Shellcoder Loader für mich so umgeschrieben, dass die Ausführung des Shellcodes innerhalb des auszuführenden Loaders erfolgt (Self Injection). Ich möchte an dieser Stelle nicht den umgeschriebenen Code veröffentlichen, sondern anhand der relevanten Codeteile das Prinzip von Vectored Exception Handling im Kontext der Shellcodeausführung erläutern. 

Was versteht man nun unter Syscalls über Vectored Exception Handling bzw. Vectored Syscalls? Vereinfacht gesagt wollen wir durch die Definition einer VEH-Funktion und die bewusste Erzeugung einer Exception die Ausführung von syscalls durch den Vectored Exception Handler erreichen. Wie wir später sehen werden, erreichen wir damit die Ausführung von Shellcode in Form von indirect syscalls, jedoch ohne die Implementierung von Assemblerbefehlen im Code.

Im Folgenden werden wir uns die wichtigsten Code-Elemente ansehen, die für die Implementierung von syscalls über Vectored Exception Handling notwendig sind, und ich werde versuchen, so gut wie möglich zu erklären, wie sie funktionieren.

Vectored Exception Handler Function

Im ersten Schritt betrachten wir die Vectored Exception Handler Funktion PvectoredExceptionHandler(), die später in der Main Funktion über Windows API AddVectoredExceptionHandler() aufgerufen wird. Die Definition der Funktion erfolgt über PVECTORED_EXCEPTION_HANDLER. Innerhalb der Funktion wird mit EXCEPTION_RECORD das Kriterium (Exception) definiert, das eine Übergabe an den Vectored Exception Handler auslösen soll. Genauer gesagt definieren wir innerhalb von EXCEPTION_RECORD den Wert für das Member ExceptionCode. In unserem Fall weisen wir dem Member ExceptionCode den Wert EXCEPTION_ACCESS_VIOLATION zu. Wir werden später sehen, warum wir genau diese Exception definieren und wie sie ausgelöst wird. 

// Vectored Exception Handler function
LONG CALLBACK PvectoredExceptionHandler(PEXCEPTION_POINTERS exception_ptr) {
    // Check if the exception is an access violation
    if (exception_ptr->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
        // Modify the thread's context to redirect execution to the syscall address
        // Copy RCX register to R10
        exception_ptr->ContextRecord->R10 = exception_ptr->ContextRecord->Rcx;

        // Copy RIP (Instruction Pointer) to RAX (RIP keeps SSN --> RAX keeps SSN)		
        exception_ptr->ContextRecord->Rax = exception_ptr->ContextRecord->Rip;

        // Set RIP to global address (set syscalls address retrieved from NtDrawText to RIP register)		
        exception_ptr->ContextRecord->Rip = g_syscall_addr;

        // Continue execution at the new instruction pointer
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    // Continue searching for another exception handler
    return EXCEPTION_CONTINUE_SEARCH;
}

Für die Realisierung von syscalls via Vectored Exception Handling müssen innerhalb der VEH-Funktion PvectoredExceptionHandler() weitere Pointer exception_ptr definiert werden. Im Vergleich zu vorher wird jedoch die Struktur CONTEXT verwendet, um auf die gewünschten Register rcx, r10, rax, rip zugreifen zu können. Anhand von diesen Pointern bilden wir die Grundlage für die Ausführung von syscalls via VEH. Wenn ich es richtig verstanden habe, bildet die Struktur der VEH-Funktion PvectoredExceptionHandler() letztlich den Teil des Syscall Stubs einer Native API nach, der letztlich für die Vorbereitung der SSN und die Ausführung der SSN via syscall notwendig ist. Die folgende Abbildung soll diese Analogie verdeutlichen.

Am Ende der Funktion PvectoredExceptionHandler() wird mit EXCEPTION_CONTINUE_EXECUTION festgelegt, dass nach der Behandlung einer durch EXCEPTION_ACCESS_VIOLATION ausgelösten Exception die Programmausführung an der Stelle fortgesetzt werden soll, an der die Exception ausgelöst wurde. Für den Fall, dass eine Exception ausgelöst wird, die nicht durch die Exception EXCEPTION_ACCESS_VIOLATION ausgelöst wurde, erfolgt via EXCEPTION_CONTINUE_SEARCH eine Übergabe an die nächste VEH-Funktion. In unserem Fall haben wir keinen weitere VEH-Funktion definiert und somit würde durch eine Übergabe an den Structured Exception Handler (SEH) erfolgen.

// Continue execution at the new instruction pointer
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    // Continue searching for another exception handler
    return EXCEPTION_CONTINUE_SEARCH;

Exception Triggering

Nach der Definition der VEH-Funktion muss noch eine Möglichkeit gefunden werden, gezielt die Exception EXCEPTION_ACCESS_VIOLATION auszulösen. Dazu werden im Shellcode Loader alle nativen APIs (die als Pointer deklariert sind) direkt über die respektive SSN initialisiert. Da jedoch eine als Pointer definierte Variable, z.B. pNtAllocateVirtualMemory, im Normalfall auf eine Speicheradresse und nicht direkt auf einen Wert zeigen soll, führt dies zu einem unzulässigen Speicherzugriff, wodurch die VEH-Funktion über EXCEPTION_ACCESS_VIOLATION ausgelöst wird. 

// Define syscall numbers for various NT API functions
enum syscall_no {
    SysNtAllocateVirtualMem = 0x18,    // Syscall number for NtAllocateVirtualMemory
    SysNtWriteVirtualMem = 0x3A,       // Syscall number for NtWriteVirtualMemory
    SysNtProtectVirtualMem = 0x50,     // Syscall number for NtProtectVirtualMemory
    SysNtCreateThreadEx = 0xC2,        // Syscall number for NtCreateThreadEx
    SysNtWaitForSingleObject = 0x4     // Syscall number for NtWaitForSingleObject
};

// Assign system call function pointers to their respective syscall numbers
_NtAllocateVirtualMemory pNtAllocateVirtualMemory = (_NtAllocateVirtualMemory)SysNtAllocateVirtualMem;
_NtWriteVirtualMemory pNtWriteVirtualMemory = (_NtWriteVirtualMemory)SysNtWriteVirtualMem;
_NtProtectVirtualMemory pNtProtectVirtualMemory = (_NtProtectVirtualMemory)SysNtProtectVirtualMem;
_NtCreateThreadEx pNtCreateThreadEx = (_NtCreateThreadEx)SysNtCreateThreadEx;
_NtWaitForSingleObject pNtWaitForSingleObject = (_NtWaitForSingleObject)SysNtWaitForSingleObject;

Wie auch im Artikel von cyberwarefare beschrieben, hat die Initialisierung der nativen APIs über die SSN zum einen den Vorteil, dass gezielt eine EXCEPTION_ACCESS_VIOLATION ausgelöst werden kann. Zum anderen hat sie den Vorteil, dass die SSN im rip Register zwischengespeichert, an den Vectored Exception Handler übergeben und innerhalb der VEH-Funktion PvectoredExceptionHandler() an das rax Register übergeben wird. 

Dieser Vorgang lässt sich durch Debugging in IDA sehr gut veranschaulichen. In der folgenden Abbildung ist gut zu erkennen, wie der Versuch, die native API NtAllocateVirtualMemory() über SSN 0x18 zu initialisieren, zu einem unzulässigen Speicherzugriffsversuch (exc.code c0000005) führt, dadurch die Access Violation Exception getriggert wird, einer Übergabe an den Vectored Exception Handler stattfindet, die SSN 0x18 in das rip Register bzw schließlich in das rax Register verschoben wird. 

Im Prinzip erreicht man damit eine Vorbereitung der SSN im rax Register (ähnlich der Vorbereitung mittels assembly code mov eax, SSN) für die anschließende Ausführung mittels syscalls. Dieser Vorgang wird solange wiederholt, bis alle im Shellcode Loader verwendeten bzw. über SSN initiierten nativen APIs nach dem jeweiligen Auslösen einer EXCEPTION_ACCESS_VIOLATION an den Vectored Exception Handler übergeben und abgearbeitet wurden.

Anmerkung: Die SSN für NtAllocateVirtualMemory() muss nicht unbedingt 0x18 sein, da die SSNsfür die gleiche Funktion von Windows zu Windows und von Version zu Version unterschiedlich sein können.

Find Syscall and Return 

Um schließlich eine Ausführung der SSN (die sich bereits im rax Register befindet) innerhalb der VEH-Funktion PvectoredExceptionHandler() zu erreichen, muss noch eine Möglichkeit gefunden werden, die Speicheradresse einer syscall Instruktion an das rip Register zu übergeben.

Dazu verwenden wir im ersten Schritt die Windows API GetModuleHandleA(), um auf den Speicher der ntdll.dll zugreifen zu können. Im nächsten Schritt greifen wir über die API GetProcAddress() auf eine native API wie z.B. NtDrawText() zu. Auf welche API wir in diesem Fall zugreifen, spielt keine Rolle und ist unabhängig davon, welche native API wir für die Speicherreservierung, das Kopieren des Shellcodes, die Ausführung des Shellcodes etc. verwenden.

// Retrieve the module handle for ntdll.dll (Windows NT Layer DLL)
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (hNtdll == NULL) {
    printf("Failed to get module handle for ntdll.dll\n");
    exit(-1);
}

// Retrieve the address of the NtDrawText function in ntdll.dll
FARPROC drawtext = GetProcAddress(hNtdll, "NtDrawText");
if (drawtext == NULL) {
    printf("Error GetProcess Address\n");
    exit(-1);
}

Letztlich geht es nur darum, mit der Funktion FindSyscallAddr auf die Basisadresse des zuvor ausgewählten Native API NtDrawText() zugreifen zu können und im nächsten Schritt mit Hilfe eines Opcodevergleichs über eine while-Schleife die syscall und return Anweisung innerhalb des Syscall Stubs zu finden.

// Function to find the syscall instruction within a function in ntdll.dll
BYTE* FindSyscallAddr(ULONG_PTR base) {
    // Cast the base address to a BYTE pointer for byte-level manipulation
    BYTE* func_base = (BYTE*)(base);
    // Temporary pointer for searching the syscall instruction
    BYTE* temp_base = 0x00;           

    // Iterate through the function bytes to find the syscall instruction pattern (0x0F 0x05)
    // 0xc3 is the opcode for the 'ret' (return) instruction in x64 assembly
    while (*func_base != 0xc3) {      
        temp_base = func_base;
        // Check if the current byte is the first byte of the syscall instruction
        if (*temp_base == 0x0f) {     
            temp_base++;
            // Check if the next byte completes the syscall instruction
            if (*temp_base == 0x05) { 
                temp_base++;
                // Check for 'ret' following the syscall to confirm it's the end of the function
                if (*temp_base == 0xc3) { 
                    temp_base = func_base;
                    break;
                }
            }
        }
        else {
            // Move to the next byte in the function
            func_base++;              
            temp_base = 0x00;
        }
    }
    // Return the address of the syscall instruction
    return temp_base;                
}


Die folgende Abbildung zeigt mittels Debugging in IDA, wie im ersten Schritt mittels Windows APIs GetModuleHandleA() und GetProcAddress() auf die Basisadresse der nativen API NtDrawtext() innerhalb des Speichers der ntdll.dll zugegriffen und anschließend mittels cmp der Opcodevergleich für 0xf, 0x05 (syscall) und 0xc3 (return) durchgeführt wird.  

Die Zwischenspeicherung der Speicheradresse der syscall Instruktion erfolgt über die als global deklarierte Variable g_syscall_addr.

// Global variable to store the address of the syscall instruction
ULONG_PTR g_syscall_addr = 0x00;

Schließlich wird innerhalb der VEH-Funktion PvectoredExceptionHandler() mittels exception_ptr die Speicheradresse (die auf die syscall Anweisung innerhalb des syscall-Stubs von NtDrawText() zeigt) an das rip Register übergeben.

// Vectored Exception Handler function
LONG CALLBACK PvectoredExceptionHandler(PEXCEPTION_POINTERS exception_ptr) {
    // Check if the exception is an access violation
    if (exception_ptr->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
        // Modify the thread's context to redirect execution to the syscall address
        // Copy RCX register to R10
        exception_ptr->ContextRecord->R10 = exception_ptr->ContextRecord->Rcx;

        // Copy RIP (Instruction Pointer) to RAX (RIP keeps SSN --> RAX keeps SSN)		
        exception_ptr->ContextRecord->Rax = exception_ptr->ContextRecord->Rip;

        // Set RIP to global address (set syscalls address retrieved from NtDrawText to RIP register)		
        exception_ptr->ContextRecord->Rip = g_syscall_addr;

        // Continue execution at the new instruction pointer
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    // Continue searching for another exception handler
    return EXCEPTION_CONTINUE_SEARCH;
}
// Set RIP to the syscall address for execution
exception_ptr->ContextRecord->Rip = g_syscall_addr;

Zur Erinnerung: Bei dem Versuch, ein natives API z.B. NtAllocateVirtualMemory() über SSN zu initialisieren, konnten wir bereits über Access Violation Exception gezielt den Vectored Exception Handler auslösen und eine Übergabe der SSN 0x18 in das rip bzw. rax register erreichen. Da wir nun auch über eine gültige Speicheradresse für die syscall Anweisung im Kontext der nativen API NtDrawText() verfügen, können wir schließlich über Vectored Exception Handling den syscall für die native API NtAllocateVirtualMemory() ausführen. 

Wie bereits zuvor erwähnt, wird dieser Vorgang solange wiederholt, bis alle im Shellcode Loader verwendeten bzw. über SSN initiierten nativen APIs nach dem jeweiligen Auslösen einer EXCEPTION_ACCESS_VIOLATION gesondert an den Vectored Exception Handler übergeben, abgearbeitet wurden und schlussendlich der Shellcode ausgeführt wird.

Zusammenfassung 

Als Ergebnis haben wir nun die Grundlage geschaffen, die im Kontext des Shellcode Loaders verwendeten nativen APIs mittels syscalls über Vectored Exception Handling (Vectored Syscalls) auszuführen. Anbei noch einmal eine grobe Zusammenfassung der wichtigsten Abläufe im Code.

  • Anhand von PVECTORED_EXCEPTION_HANDLER erfolgt die Definition der Vectored Exception Handler Funktion PvectoredExceptionHandler()

  • Innerhalb der Funktion PvectoredExceptionHandler() definieren wir den Exception-Code, z.B. EXCEPTION_ACCESS_VIOLATION, der eine Übergabe and den Vectored Exception Handler auslösen soll.

  • Innerhalb der PvectoredExceptionHandler() Funktion definieren wir die notwendigen Pointer welche notwendig sind um auf die Register rcx, r10, rax, rip zugreifen zu können.

  • Wir provozieren absichtlich eine Auslösung der EXCEPTION_ACCESS_VIOLATION, welche als Exception Code innerhalb unserer VEH-Funktion definiert wurde. 

  • Die Auslösung der EXCEPTION_ACCESS_VIOLATION erfolgt durch den Versuch, ein Native API z.B. NtAllocateVirtualMemory() via SSN zu initiieren.

  • Die SSN, wird an das rip Register übergeben, das wiederum innerhalb der VEH-Funktion an das rax Register übergeben wird. 

  • Die Windows API GetModuleHandleA() wird für den Zugriff auf den Speicher der ntdll.dll verwendet.
     
  • Weiters greifen wir mittels GetProcAddress() auf die Basisadresse einer beliebigen Native API innerhalb der ntdll.dll zu (zum Beispiel NtDrawText()). 

  • Die Funktion FindSyscallAddr führt mittels einer While-Schleife einen Opcodevergleich durch, um die Speicheradresse der syscall Anweisung innerhalb des Sycall-Stubs der Native API (z. B. NtDrawText()) zu finden. 

  • Die Speicheradresse der Syscall Instruktion wird in der als global deklarierten Variablen g_syscall_addr gespeichert und innerhalb der VEH-Funktion an das RIP Register übergeben. 

  • Anschließend erfolgt die Ausführung des syscalls für die native API z.B. NtAllocateVirtualMemory() durch den registrierten Vectored Exception Handler. 

  • Wiederholung der Prozedur für alle anderen notwendigen nativen APIs, die zur Ausführung des Shellcodes benötigt werden, z.B. NtWriteVirtualMemory(), NtProtectVirtualMemory(), NtCreateThreadEx() und NtWaitForSingleObject().


Letztendlich erreichen wir durch diese Abfolge eine Ausführung des Shellcodes in unserem Loader in Form von (indirect) syscalls durch Vectored Exception Handling.

Erkenntnisse

Wie bereits erwähnt, können direct syscalls oder indirect syscalls über Assembly Code innerhalb eines Shellcode Loaders realisiert werden. Dieser Artikel hat jedoch gezeigt, dass die Realisierung auch über Vectored Exception Handling (VEH) erfolgen kann. 

Vergleicht man beispielsweise die Anordnung der Stack Frames innerhalb des Thread Call Stacks zwischen einem indirect syscall shellcode loader und einem vectored syscall shellcode loader, so stellt man fest, dass die Anordnung völlig identisch ist. Dieses Ergebnis war auch zu erwarten, da mittels Vectored Exception Handling die Ausführung der syscall und return Anweisung innerhalb des Speichers der ntdll.dll stattfindet. 

Trotz der Tatsache, dass in beiden Shellcode-Loadern die native API NtWaitForSingleObject() zuletzt ausgeführt wird, kann man im Thread Call Stack des Vectored Syscall Loaders (Bild rechts) erkennen, dass im Vergleich zum Indirect Syscall Loader die return Anweisung im Speicherbereich von NtDrawText() und nicht im Speicherbereich von NtWaitForSingleObject() ausgeführt wird. Dies hat den einfachen Grund, dass wir in unserem Vectored Syscall Loader über die Windows API GetProcAddress() auf die Basisadresse von NtDrawText() zugreifen, um über einen Opcodevergleich die syscall Anweisung innerhalb des Syscall Stubs zu finden, die Speicheradresse des syscalls in der global deklarierten Variablen g_syscall_addr speichern und schließlich zur Ausführung des syscalls über den Vectored Exception Handler die Speicheradresse der syscall Anweisung im Kontext von NtDrawText() innerhalb der VEH-Funktion PvectoredExceptionHandler an das rip Register übergeben.

Inwieweit die Ausführung von Syscalls über Vectored Exception Handling einen Vorteil gegenüber EDR Evasion bietet, kann zum jetzigen Zeitpunkt mangels Erfahrung noch nicht beurteilt werden. Ich hoffe, dieser Artikel hat Ihnen geholfen, mehr über das Thema Vectored Exception Handling zu erfahren und wie es bei der Entwicklung von Malware z.B. für die Ausführung von Shellcode über syscalls eingesetzt werden kann. Bis zum nächsten Artikel!

Happy Hacking!

Daniel Feichter @VirtualAllocEx

Zuletzt aktualisiert 31.03.24 17:45:36 31.03.24
Daniel Feichter