Intro
In diesem Artikel wird versucht, einen bestimmten Erkennungsmechanismus eines bestimmten EDR durch Debugging und Reverse Engineering zu analysieren. Es war nicht mein primäres Ziel, den Prozess des Reverse Engineering bis ins letzte Detail zu durchdringen. Vielmehr ging es mir darum, ein fundiertes Verständnis der Funktionsweise des neu implementierten Erkennungsmechanismus im EDR zu entwickeln, die Funktionen dieses Mechanismus zu erforschen, die Gründe für seine mögliche Implementierung in Frage zu stellen und über mögliche Umgehungsmöglichkeiten nachzudenken.
Darüber hinaus halte ich es für wichtig zu verstehen, dass jeder kommerzielle EDR letztendlich eine Black Box ist. Man kann versuchen, durch verschiedene Methoden wie statische und dynamische Analysen mehr über die Funktionsweise des EDR zu erfahren, aber insbesondere die interne Funktionsweise und Logik bleibt weitgehend eine Black Box. Es können Hypothesen über das Innenleben und die logische Struktur von EDRs aufgestellt werden, die dann analysiert werden können, um diese Hypothesen zu validieren. Es ist jedoch oft schwierig, vollständige Klarheit und eindeutige Aussagen über ihre Funktionsweise zu erhalten.
Ausgangspunkt meiner Analyse des bisher unbekannten Erkennungsverhaltens im Kontext des zu untersuchenden EDR ist die Hypothese, dass der EDR neben der bekannten Hooking DLL eine neue, zusätzliche DLL verwendet, die im Kontext spezifischer APIs eingesetzt wird, um Malware durch die gewonnene Telemetrie mittels Thread Call Stack Analyse zu identifizieren. Im Laufe dieses Artikels werde ich versuchen, diese Hypothese zu erläutern und zu untermauern.
I thought I knew your DLLs
Wie bereits in der Einleitung erwähnt, hatte ich bereits mehrfach mit dem betroffenen EDR zu tun. So war mir bereits bekannt, dass der EDR User Mode Hooking über Inline Hooks verwendet und auch der Name der User Mode Hooking DLL war mir bereits bekannt. Mit Hilfe des Tools System Informer (oder Process Explorer oder Process Hacker) stellte sich jedoch heraus, dass der EDR neben der User Mode Hooking DLL seit kurzem noch eine weitere DLL verwendet. Im folgenden Bild habe ich einfach eine notepad.exe gestartet und mit System Informer die im Speicher des Prozesses geladenen DLLs untersucht. Da ich den Hersteller schützen möchte und den Namen der DLL nicht preisgeben werde, nennen wir diese DLL in diesem Artikel einfach die neue DLL.
Diese erste Feststellung dient als Ausgangspunkt für die weitere Analyse des EDR. Um mehr über den Umfang und die Funktionsweise zu erfahren, wollen wir versuchen, den EDR bzw. die neue DLL genauer zu analysieren. Zum einen wollen wir die neue DLL ein wenig statisch mit IDA untersuchen und zum anderen wollen wir durch Debugging mit x64dbg ein wenig mehr über das dynamische Verhalten der neuen DLL herausfinden.
Hooks, hooks and more hooks
Glücklicherweise habe ich vor einigen Monaten im Zusammenhang mit dem zu untersuchenden EDR eine Liste erstellt, die dokumentiert, welche APIs zu diesem Zeitpunkt von der User Mode Hooking DLL gehookt wurden. Beispielsweise waren typische APIs wie VirtualAlloc
oder NtAllocateVirtualMemory
zuletzt mit einem Inline Hook versehen. Generell variiert die Anzahl der Hooks im Kontext des zu untersuchenden EDR je nach Konfiguration.
Als ich die API Hooking List vor etwa 6 Monaten erstellte, wusste ich, dass der EDR in seiner umfassendsten Konfiguration etwa 60 Inline-Hooks in verschiedenen DLLs implementiert hatte, darunter ntdll.dll
, kernel32.dll
und kernelbase.dll
. Um die API Hooking List auf den neuesten Stand zu bringen, habe ich eine aktualisierte Liste erstellt. Zur automatisierten Erstellung der API Hooking List kann z.B. das Tool HookDump verwendet werden.
Ein Vergleich der neu gewonnenen Daten mit meinen früheren Aufzeichnungen zeigt, dass der EDR seit meiner letzten Untersuchung zusätzliche APIs mit Inline-Hooks ausgestattet hat, wie z.B. CreateThreadPoolWork
, CreateThreadPoolWait
, OpenProcess
und WriteProcessMemory
. Insgesamt scheint der EDR nun eine wesentlich größere Anzahl von APIs mit Inline-Hooks auszustatten, als dies bei meiner vorherigen Analyse der Fall war. Es ist wichtig zu beachten, dass die Intensität, mit der die neue DLL in der EDR-Konfiguration verwendet wird, variabel eingestellt werden kann. Dies wirkt sich direkt auf die Gesamtzahl der User Mode Inline Hooks aus.
Die Gesamtzahl der User Mode Inline Hooks setzt sich zusammen aus der Anzahl der Hooks, die im Kontext der bereits bekannten Hooking DLL implementiert wurden und der Anzahl der Hooks, die durch die bisher unbekannte, neue DLL hinzugefügt wurden. Abhängig von der Konfiguration dieser beiden DLLs innerhalb der spezifischen Konfigurationseinstellungen kann die Gesamtzahl der User Mode Inline Hooks erheblich variieren und Werte von über 150 User Mode Inline Hooks verteilt in unterschiedlichen DLLs erreichen. Im Vergleich zu anderen mir bekannten EDR-Systemen, die User Mode Hooking verwenden, erscheint diese maximale Anzahl von Inline Hooks außergewöhnlich hoch.
Um die Ergebnisse von HookDump weiter zu validieren, habe ich mit x64dbg stichprobenartig die APIs untersucht, die seit meiner letzten Analyse neu mit Inline Hooks versehen wurden. Unter anderem nahm ich die APIs CreateThreadPoolWork
und CreateThreadPoolWait
unter die Lupe. Hierfür startete ich einfach einen Notepad-Prozess und untersuchte mit dem Debugger die im Speicher geladenen DLLs auf Inline Hooks. Die folgende Abbildung bestätigt das Ergebnis von HookDump: Die beiden APIs CreateThreadPoolWork
und CreateThreadPoolWait
in der von notepad.exe
geladenen kernel32.dll
sind tatsächlich mit Inline Hooks versehen. Der Hook lässt sich an der unconditional jmp
Anweisung bzw. am zugehörigen Opcode E9
erkennen.
Bisher lässt sich also festhalten, dass der EDR neben der bereits bekannten Hooking-DLL eine zusätzliche, neue DLL einsetzt. Diese neue DLL stattet weitere APIs, wie zum Beispiel CreateThreadPoolWork
, CreateThreadPoolWait
, OpenProcess
, WriteProcessMemory
etc. mit Inline-Hooks aus, was wahrscheinlich eine Umleitung der Codeausführung in den Speicherbereich der neuen DLL bewirkt (dazu später mehr). Um jedoch ein tieferes Verständnis für das Verhalten und die Funktion der neuen DLL zu erlangen, müssen wir noch ein wenig tiefer graben.
EDR DLL - Static Analysis
Ein relativ einfacher Untersuchungsansatz, der die aufgestellte Hypothese schon ein wenig untermauern sollte. Während der statischen Analyse der neuen DLL mit IDA kann beobachtet werden, dass Funktionen importiert werden bzw. Verweise auf Funktionen vorhanden sind, die im Kontext des x64 Exception Handling und der Call Stack Analyse unter Windows verwendet werden. Zum einen werden beispielsweise die Windows APIs RtlAddVectoredExceptionHandler
und RtlRemoveVectoredExceptionHandler
aus der ntdll.dll
importiert, die für die Registrierung und Deregistrierung eines Vectored Exception Handlers (VEH) benötigt werden.
Zusätzlich enthält die neue DLL Verweise auf die Speicheradressen der Funktionen RtlCaptureContext
, RtlLookupFunctionEntry
, RtVirtualUnwind
, RtlUnhandledExceptionFilter
und NtTerminateProcess
innerhalb der ntdll.dll
(dazu später mehr).
Vectored Exception Handler Function
Im Rahmen der statischen Analyse der neuen DLL des Endpoint Detection and Response (EDR) Systems habe ich versucht, einen detaillierteren Einblick in die Struktur der Vectored Exception Handler (VEH) Funktion des EDR zu gewinnen und zu verstehen, durch welche spezifischen Exceptions der VEH aktiviert wird. Der untersuchte Pseudocode zeigt, dass der ExceptionRecord
so aufgebaut ist, dass dem ExceptionCode
der Hexadezimalwert 0x40000000
hinzugefügt wird. Anschließend erfolgt ein Vergleich mit dem Wert 0x4EFFF
mittels einer small or equal
Operation. Diese spezifische Operation und die damit verbundenen Werte sind mir zunächst nicht ganz klar, aber ein ExceptionCode
, der nach dieser Additions- und Vergleichsoperation innerhalb des definierten Bereichs liegt, führt zum Aufruf der Funktion sub_180001E10
. Da es sich letztendlich um Pseudocode handelt, kann es auch sein, dass der ExceptionCode
einfach falsch interpretiert wird. Welche Art von Exception der EDR in seiner VEH-Funktion verwendet, werden wir etwas später im Teil der dynamischen Analyse sehen.
__int64 __fastcall VectoredHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
__int64 v2; // rax
__int64 v3; // rbx
__int64 v4; // rcx
__int32 ExceptionCode; // edx
if ( ExceptionInfo->ExceptionRecord->ExceptionCode + 0x40000000 <= 0x4EFFF )
{
v2 = sub_180001E10();
v3 = v2;
if ( v2 )
{
if ( *(_DWORD *)(v2 + 20) && *(_BYTE *)(*(_QWORD *)(v2 + 24) + 49i64) )
{
**(_QWORD **)(v2 + 8) = *(_QWORD *)v2;
v4 = *(_QWORD *)(v2 + 24);
ExceptionCode = ExceptionInfo->ExceptionRecord->ExceptionCode;
_InterlockedIncrement64((volatile signed __int64 *)(v4 + 144));
if ( (**(_BYTE **)(v4 + 24) & 8) == 0 )
{
_InterlockedExchange((volatile __int32 *)(v4 + 168), ExceptionCode);
sub_180003E60(v4, v2);
}
_InterlockedExchange((volatile __int32 *)(v3 + 20), 0);
}
}
}
return 0i64;
}
EDR DLL - Dynamic or Behavioral Analysis
Um das dynamische Verhalten der neuen DLL im Zusammenhang mit Malware besser zu verstehen, wird der folgende C-Code verwendet. Dieses POC demonstriert die Ausführung von Shellcode unter Verwendung der Callback-Funktion CreateThreadPoolWork
, die, wie bereits festgestellt, vom untersuchenden EDR mit einem Inline-Hook versehen wird, um eine Umleitung auf die neue DLL zu bewirken.
Die Ausführung von Shellcode über Callbacks und Threadpools bietet aus Sicht eines Angreifers einige interessante Möglichkeiten, ich denke, das ist mitunter auch ein Grund, warum mit der EDR mit der neuen DLL auch diese APIs mit einem Inline Hook austattet. Auf die Funktionsweise von Callback-Funktionen und Thread-Pools möchte ich hier nicht weiter eingehen. Wer sich aber z.B. mit dem Thema Thread-Pooling beschäftigen möchte, dem empfehle ich den folgenden Blog Post A Deep Dive Into Exploiting Windows Thread Pools von Diago Lima oder auch den Blog Post The Pool Party You Will Never Forget von Alon Leviev.
// Based on CreateThreaPoolWait POC from Alternative Shellcode Execution via Callbacks Repo
// https://github.com/aahmad097/AlternativeShellcodeExec
#include <windows.h>
#include <stdio.h>
#include <threadpoolapiset.h>
// Define the shellcode to be executed. In practice, this would be malicious code.
unsigned char shellcode[] = "\xfc\x48\x83...";
int main() {
// Prompt the user to press any key to start the process. This is a simple synchronization point for demonstration.
printf("[+] Press Key to start debugging \n");
getchar(); // Wait for user input to proceed.
// Allocate a block of memory with read-write permissions to store the shellcode.
LPVOID addr = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_READWRITE);
// Copy the shellcode into the newly allocated memory space.
// This is necessary because executing code directly from static data sections is typically not allowed.
RtlMoveMemory(addr, shellcode, sizeof(shellcode));
// Change the memory protection to execute-read to allow the CPU to execute the shellcode.
DWORD oldProtection;
if (!VirtualProtect(addr, sizeof(shellcode), PAGE_EXECUTE_READ, &oldProtection)) {
printf("%d", GetLastError()); // If changing protection fails, print the error code.
}
// Create a thread pool work item that points to the shellcode's memory address.
// This effectively schedules the shellcode for execution in a separate thread managed by the OS.
PTP_WORK ptp_work = CreateThreadpoolWork((PTP_WORK_CALLBACK)addr, NULL, NULL);
// Submit the work item to the thread pool. This action queues the shellcode for execution.
SubmitThreadpoolWork(ptp_work);
// Wait for the thread pool work item to complete execution.
// The FALSE parameter indicates we do not cancel pending callbacks if they're not started.
WaitForThreadpoolWorkCallbacks(ptp_work, FALSE);
// This loop serves a critical purpose in the context of this program, where shellcode is executed asynchronously
// in a separate thread managed by the Windows thread pool. The execution of shellcode is scheduled via
// SubmitThreadpoolWork(), and this operation is non-blocking; meaning, it allows the main thread to continue
// running immediately after the call. Without a mechanism to keep the main thread running, the program would
// terminate, and as a result, the Windows process that hosts this program (and all its threads, including the
// thread pool ones) would be destroyed before the shellcode has a chance to execute or complete its execution.
while (TRUE) {
Sleep(3000);
}
}
Um unsere Untersuchung des dynamischen Erkennungsverhaltens des EDR in Bezug auf die Callback-Funktion CreateThreadPoolWork
zu initiieren, setzen wir als ersten Schritt einen Breakpoint auf die Basisadresse der API CreateThreadPoolWork
. Unser Ziel ist es zu validieren, ob wir während der Programmausführung tatsächlich die API erreichen.
Die folgende Abbildung bestätigt, dass wir den Breakpoint im Kontext von CreateThreadPoolWork
erfolgreich erreichen. Zudem wird ersichtlich, dass der EDR hier einen Inline-Hook mittels einer unkonditionierten jmp
Anweisung (Opcode E9
) setzt. Dies impliziert, dass vor der Ausführung der im Kontext stehenden nativen API TpAllocWork
, eine Umleitung durch den EDR stattfindet.
Bevor wir mit der Programmausführung fortfahren und der jmp
Anweisung im Programmablauf folgen, setzen wir einen Breakpoint auf die Basisadresse des .text
Abschnitts der neuen DLL des EDRs. So können wir überprüfen, ob wir nach der Ausführung der jmp
Anweisung im Kontext der gehookten API CreateThreadPoolWork
tatsächlich im Speicherbereich der neuen DLL landen.
Nach Abschluss der Vorbereitungen setzen wir die Ausführung unseres Proof-of-Concept (POC) Shellcodes im Debugger fort. Die folgende Abbildung zeigt, dass wir nach der Verwendung des Inline-Hooks nicht direkt in den Speicherbereich der neuen DLL gelangen. Stattdessen verbleiben wir zunächst in der .text
Region unseres POCs (.exe). Es zeigt sich, dass zwei weitere Sprünge mittels jmp
Anweisung folgen, bevor wir schließlich in der .text
Region der neuen DLL ankommen.
Besonders interessant ist die Phase, in der die dritte jmp
Anweisung ausgeführt wird: Hier sehen wir die Vorbereitung für den Sprung in die .text
Region der neuen DLL. Die Anweisung mov rax, EDRdll.21B0F9756E0
lädt die Speicheradresse der Funktion 21B0F9756E0
in das rax
Register. Ein anschließender Sprung (dritter Sprung) via unconditional jmp
führt uns schlussendlich in den .text
Bereich der neuen DLL des EDRs.
EDR DLL - Internal Logic
Um die eingangs aufgestellte Hypothese zu untermauern, möchte ich den Programmfluß im Kontext des POC innerhalb der neuen DLL des EDR durch eine dynamische Analyse näher betrachten.
Push Function Arguments to Call Stack
Sobald wir nach der Ausführung mehrerer aufeinanderfolgender jmp
Anweisungen im Programmablauf in der .text
Region der neuen DLL gelandet sind, zeigt die folgende Abbildung, dass in einem ersten Schritt die Inhalte wichtiger Register - insbesondere rcx
, rdx
, r8
und r9
mittels push
Anweisungen auf den Call Stack übertragen werden. Basierend auf der x64 Calling Convention bedeutet das im Kontext unseres POC und der Funktion CreateThreadPoolWork
, dass die Register rcx
, rdx
, r8
und r9
die Funktionsargumente enthalten. Kurz gesagt, an dieser Stelle werden die Funktionsargumente von CreateThreadPoolWork
auf den Call Stack gelegt.
Gemäß der x64 Calling Convention werden die ersten vier Argumente einer Funktion direkt in die Register in der Reihenfolge: rcx
, rdx
, r8
, r9
übergeben. Alle weiteren Argumente einer Funktion werden auf dem Stack abgelegt. Der folgende Codeausschnitt zeigt jedoch, dass die Callback-Funktion CreateThreadPoolWork
tatsächlich nur drei Argumente benötigt.
PTP_WORK CreateThreadpoolWork(
[in] PTP_WORK_CALLBACK pfnwk, // rcx
[in, out, optional] PVOID pv, // rdx
[in, optional] PTP_CALLBACK_ENVIRON pcbe // r8
);
Vecored Exception Handling
Wie bereits erwähnt, registriert der EDR mit der neuen DLL einen Vector Exception Handler (VEH). Die weitere Analyse der neuen DLL mit x64dbg zeigt in vereinfachter Form, dass im Falle einer Exception (z.B. durch Zugriff auf einen unzulässigen Speicherbereich) der Aufruf der Funktion 1DE044554D0
innerhalb der neuen DLL erfolgt. Bei genauer Betrachtung der Funktion werden wir feststellen, dass diese den Kern der Call Stack Analyse bildet und wiederum die Eingangs aufgestellte Hypothese stärken soll.
Die Funktion 1DE044554D0
spielt im Kontext der Hypothese eine Schlüsselrolle, da die Analyse dieser Funktion die Verwendung mehrerer kritischer APIs zeigt, die für die Verarbeitung und Analyse von Exception Handling Szenarien wesentlich sind. Hierzu zählen die APIs RtlCaptureContext
, RtlLookupFuntionEntry
, RtVirtualUnwind
, RtlUnhandledExceptionFilter
und NtTerminateProcess
. Im Kontext des zu untersuchenden EDRs kommen die APIs in Zusammenhang mit einer Call Stack Analyse zum Einatz. Die folgende Abbildung gibt einen Überblick über den Inhalt der Funktion 1DE044554D0
, im Folgenden wird jedoch auf die Funktion im Einzelnen eingegangen.
Bevor ich im nächsten Punkt darauf eingehe, welche Exception der EDR vermutlich benutzt, um seinen VEH auszulösen, und danach auf die Funktion 1DE044554D0
näher eingehe, wollen wir uns etwas näher damit beschäftigen, wie man nachweisen kann, dass der EDR im Kontext der neuen DLL tatsächlich Vectored Exception Handling benutzt bzw. einen VEH über die neue DLL registriert. Wie in meinem letzten Artikel EDR Analysis: Leveraging Fake DLLs, Guard Pages, and VEH for Enhanced Detection erläutert, können wir durch Debuggen des Process Environment Block (PEB), z.B. im Kontext eines Prozesses wie notepad.exe
auf einer VM, auf der der zu untersuchende EDR installiert ist, überprüfen, ob der Prozess VEH verwendet.
Dazu muss der Wert von CrossProcessFlags
im PEB überprüft werden; hat CrossProcessFlags
den dezimalen Wert 4, so verwendet der Prozess gemäß dieser Dokumentation von Geoff Chapell VEH. Die folgende Abbildung zeigt auf der linken Seite die Analyse von CrossProcessFlags
innerhalb des PEB auf einer VM mit dem zu untersuchenden EDR und auf der rechten Seite die Analyse von CrossProcessFlags
auf einer VM ohne installierten EDR. Es ist zu erkennen, dass auf der VM mit dem zu untersuchenden EDR für CrossProcessFlags
der Dezimalwert 4 vorliegt und somit im Kontext von notepad.exe
VEH verwendet wird und auf der VM ohne EDR für CrossProcessFlags
der Dezimalwert 0 vorliegt und somit kein VEH verwendet wird.
Wir wissen nun, dass die Verwendung von VEH wahrscheinlich auf den EDR zurückzuführen ist, aber um diese Theorie weiter zu untermauern, bzw. um nachweisen zu können, dass die Registrierung des VEH durch die neue DLL des EDR erfolgt, müssen wir noch ein wenig debuggen. Wir öffnen z.B. das Image für notepad.exe
innerhalb von x64dbg, suchen innerhalb der neuen DLL des EDR nach dem Aufruf der Funktion RtlAddVectoredExceptionHandler
und setzen einen Breakpoint auf die entsprechende Speicheradresse. Mit anderen Worten, wir wollen mit diesem Versuch nachweisen, dass die Registrierung des VEH durch die neue DLL des EDR erfolgt. Die folgende Abblidung zeigt, sobald die neue DLL des EDR in den Speicher des laufenden Prozesses notepad.exe
geladen wurde, wird der Breakpoint ausgelöst, den wir auf die Funktion RtlAddVectoredExceptionHandler
innerhalb der neuen DLL gesetzt haben. Mit anderen Worten wurde damit der Beweis erbracht, dass die Registrierung des VEH durch die neue DLL des EDR erfolgt.
Alternativ kann auch ein Breakpoint auf die native Funktion RtlAddVectoredExceptionHandler
innerhalb der ntdll.dll
gesetzt werden und bei Erreichen des Breakpoints überprüft werden, ob bzw. von welcher Speicheradresse der Aufruf der Funktion erfolgte. So konnte im Kontext von notepad.exe
mittels Untersuchung der CrossProcessFlags
nachgewiesen werden, dass notepad.exe
einen VEH verwendet. Weiterhin konnte durch Debugging mit x64dbg nachgewiesen werden, dass der Aufruf der Funktion RtlAddVectoredExceptionHandler
und damit die Registrierung des VEH über die neue DLL des EDR erfolgt.
Als Zusatzinformation sei erwähnt, dass der untersuchte EDR die Funktion RtlAddVectoredExceptionHandler
auch im Kontext seiner User Mode Hooking DLL aufruft, d.h. der EDR registriert über die Hooking DLL einen zusätzliche VEH.
Exception - Hardware Breakpoints
Dieser Nachweis war mit Abstand der schwierigste Teil der Arbeit und hat mir einige schlaflose Nächte bereitet. Grundsätzlich gibt es verschiedene Arten von Exceptions, die als ExceptionCode
innerhalb der EXCEPTION_RECORD-Struktur in einer VEH-Funktion definiert werden können. Dies kann z.B. eine Division durch Null oder ein unerlaubter Speicherzugriff sein. In meinem letzten Artikel EDR Analysis: Leveraging Fake DLLs, Guard Pages, and VEH for Enhanced Detection kann man zum Beispiel lesen, dass der VEH des betroffenen EDRs durch ein PAGE_GUARD Flag bzw. durch die dazugehörige Exception STATUS_GUARD_PAGE_VIOLATION
(0x80000001)
ausgelöst wird, was in der Game Hacking Community auch als Guard Page Hooking bekannt ist. Im Kontext der Analyse dieses EDRs scheint dies nicht der Fall zu sein, da die Speicherbereiche der neuen DLL kein PAGE_GUARD
Flag aufweisen.
Ich habe einen Hinweis erhalten, dass der EDR als EXCEPTION_RECORD
den Typ EXCEPTION_SINGLE_STEP (Hardware Breakpoint) verwendet und bin dem nachgegangen. An dieser Stelle möchte ich nicht zu tief auf das Thema Hardware Breakpoints eingehen, da dies den Rahmen dieses Artikels sprengen würde. Wer an dieser Stelle mehr über Hardware Breakpoints lesen möchte, dem empfehle ich den folgenden Artikel Blindside: A New Technique for EDR Evasion with Hardware Breakpoints.
Dennoch ein paar Grundlagen zum Thema Hardware Breakpoints, die für das Verständnis der folgenden Ausführungen hilfreich sind. Im Gegensatz zu Software Breakpoints INT3
auf Softwareebene, kommen Hardware Breakpoints Breakpoints auf Prozessorebene innerhalb der Debug-Register zum Einsatz. Debug-Register sind spezielle Register innerhalb einer CPU, die zum Debuggen von Software verwendet werden können. Aufgeteilt auf die Register DR0-DR3
können im Kontext eines Prozess insgesamt maximal 4 Hardware Breakpoints gesetzt werden. Das Register DR6
ist das Statusregister, das Informationen darüber liefert, warum ein Breakpoint ausgelöst wurde und DR7
ist das Steuerregister, das konfiguriert, wie die Breakpoints in DR0-DR3
verwendet werden. Im Kontext unserer EDRs gehen wir davon aus, dass Hardware-Breakpoints im Kontext bestimmter Prozesse an bestimmten Speicheradressen gesetzt werden und diese durch read, write oder execute dieser bestimmten Speicheradressen ausgelöst werden.
Bevor ich zur eigentlichen Erklärung komme, möchte ich noch eine Fehlinterpretation dokumentieren. Betrachtet man das folgende Bild, so konnte ich mittels statischer Analyse mit IDA die Zeile mov [rbp+460h+Context.ContextFlags], 100001h
ausfindig machen. Meine erste Vermutung war, dass ich den Codeteil identifiziert hatte, der für den Eintrag des Hardware Breakpoints im Debug Register verantwortlich ist, da ich zunächst dachte, dass der Wert 100001h
0x00100000
für CONTEXT_AMD64
und 0x00000010
CONTEXT_DEBUG_CONTROL
entspricht. Diese Annahme stellte sich jedoch als falsch heraus, da der Hex-Wert 1000001h
dem Wert 0x00100000
für CONTEXT_AMD64
und dem Wert 0x00100001
für CONTEXT_CONTROL
entspricht (Vielen Dank an 5pider von der Maldev Academy für diese Erklärung.).
Zwischen meiner ersten Fehlinterpretation und dem tatsächlichen Nachweis der Verwendung von Hardware Breakpoints als Exception verging einige Zeit. Kurz gesagt, die Überlegung war, dass das Auslösen der EDR Hardware Breakpoints relativ früh während der Initialisierung eines neuen Prozesses bzw. während des Ladens von Modulen (DLLs) stattfinden muss. Basierend auf dieser Annahme habe ich verschiedene Tests in x64dbg durchgeführt. Long story short, durch Debugging im Kontext des POC und der LoadLibrayA
API konnte ich schließlich den Zeitpunkt bzw. die Codezeile während des Ladens eines Moduls (DLL) ausfindig machen, an dem die Hardware Breakpoints des EDR in den Debug-Registern auftauchen und ausgelöst werden.
Bei der Adresse im Register DR3
könnte es sich um einen Hardware-Breakpoint des EDR handeln, der z.B. durch einen Lese- oder Schreibzugriff ausgelöst wird (von der Funktionsweise ähnlich dem GUARD_PAGE
Flag (RX+G
) im Kontext der EDR Analyse meines letzten Artikels). Aktuell bin ich mir noch nicht ganz sicher, warum die Speicheradressen rot markiert und nicht erreichbar sind, aber ich vermute, dass es sich um eine virtuelle Adresse außerhalb des virtuellen Speichers unserer poc.exe
handelt. Dies könnte z.B. eine virtuelle Adresse innerhalb des User Mode Agents des EDR sein, aber das ist zu diesem Zeitpunkt nur eine Vermutung.
Es sollte auch erwähnt werden, dass dies im Kontext unserer poc.exe
nur eine von mehreren Speicheradressen ist, die vom EDR durch Hardware Breakpoints überwacht zu werden scheinen. Die Hardware-Breakpoints in den Debug-Registern treten in derselben Code-Zeile im Kontext von LoadLibraryA
in jeder in den Speicher geladenen DLL an derselben Stelle auf. Es ist davon auszugehen, dass der EDR weitere Speicheradressen auf Lesen, Schreiben oder Ausführen mittels Hardware Breakpoints überwacht. Auch wenn es logisch erscheint, möchte ich dennoch erwähnen, dass natürlich davon ausgegangen werden kann, dass die Hardware Breakpoints des EDR auch im Kontext anderer Prozesse verwendet werden und nicht nur im Kontext der poc.exe
.
Um mit hoher Wahrscheinlichkeit sicher zu sein, dass es sich tatsächlich um Hardware Breakpoints des EDR handelt, habe ich den gleichen Test auf einer VM ohne EDR durchgeführt, um nachweisen zu können, dass sich im gleichen Kontext keine Speicheradressen innerhalb der Debug-Register befinden.
Interessant war auch zu beobachten, dass beim Versuch, die Zeile nop dword ptr ds:[rax+rax],eax
manuell mit einem Hardware Breakpoint in x64dbg zu überschreiben, der EDR die Aktion durch aktive Prävention verhinderte, den Debugger und den POC beendete und eine Detection generierte. Daraus kann abgeleitet werden, dass der EDR seine Hardware Breakpoints aktiv überwacht und im Kontext einer Manipulation entsprechend reagiert.
Ein weiterer Indikator für die Verwendung von Hardware Breakpoints durch den EDR ist die Tatsache, dass die neue DLL des EDR die Funktionen NtGetContextThread
und NtSetContextThread
dynamisch zur Laufzeit importiert bzw. zeigt die folgende Abbildung den Aufruf der beiden Funktionen innerhalb des Speichers der neuen DLL des EDR.
Aber warum scheint dieser Indikator plausibel? Schaut man sich z.B. die Struktur der nativen Funktionen NtGetContextThread und NtSetContextThread an, so erkennt man, dass innerhalb der Funktionen über das Argument pContext
auf die CONTEXT Struktur zugegriffen wird. Im Kontext unserer Analyse, ob der EDR Hardware Breakpoints als Exception verwendet, gibt uns die CONTEXT
Struktur Auskunft über die aktuelle Verwendung der Debug-Register DR0-DR7
. D.h. mittels NtGetContextThread
kann der EDR den aktuellen Zustand der Debug-Register überprüfen, möglicherweise kann der EDR damit auch die Registrierung von Hardware Breakpoints durch Malware überprüfen bzw. die eigenen Hardware Breakpoints des EDR überwachen (dies ist allerdings nur ein Verdacht, der aber durch den vorherigen Versuch, die HWBPs des EDR mittels HWBP in x64dbg zu überschreiben, plausibel erscheint). Um die Hardware Breakpoints in den Debug-Registern innerhalb der CONTEXT
Struktur zu registrieren, ruft der EDR die Funktion NtSetContextThread
innerhalb der neuen DLL des EDR auf.
Nachdem wir nun etwas mehr darüber herausgefunden haben, dass der EDR mit einer gewissen Wahrscheinlichkeit Hardware Breakpoints als Exception Typ innerhalb der VEH Funktion verwendet, können wir uns nun etwas genauer mit der bereits erwähnten Funktion 1DE044554D0
beschäftigen, die meiner Meinung nach den Kern meiner eingangs aufgestellten Hypothese bildet und den Kern der User Mode Call Stack Analyse durch den EDR darstellt.
RtlCaptureContext
Die Funktion RtlCaptureContext
innerhalb der Funktion 1DE044554D0
wird dazu verwendet, den Kontext des aktuellen Threads zu erfassen und greift wichtige Informationen wie prozessorspezifische Register, den Counter und den Stackpointer ab. Der erfasste Kontext wird in einer Context Struktur gespeichert und liefert eine Momentaufnahme des Thread-Zustands zum Zeitpunkt der Exception. Diese Momentaufnahme ist entscheidend für die Diagnose von Problemen und das Verständnis der Abfolge von Ereignissen, die zur Exception geführt haben. Erinnern wir uns, dass im Kontext unseres CreateThreaPoolWork
POC die Register rcx
, rdx
, r8
und r9
die Funktionsargumente von CreateThreaPoolWork
enthalten.
Nachdem der Thread-Kontext erfasst wurde, wird die in der CONTEXT
Struktur gespeicherte return Adresse an eine weitere wichtige Funktion, RtlLookupFunctionEntry, übergeben.
RtlLookupFunctionEntry
Die API RtlLookupFunctionEntry
ruft einen Pointer auf eine RUNTIME_FUNCTION Struktur ab, die die Unwind-Daten für den Stack enthält. Die Unwind-Daten sind essenziell, um durch den Call Stack zurückzunavigieren, ein Prozess, der als Stack Unwinding bekannt ist. Dies ermöglicht es dem EDR, die Abfolge der Funktionsaufrufe, die zur Exception führten, nachzuverfolgen.
RtlVirtualUnwind
Im Rahmen der Call Stack Analyse durch die neue DLL wird die Funktion RtlVirtualUnwind
verwendet, um das Unwinding des Stacks zu simulieren, wodurch der Kontext des Callers für jeden Stack Frame bestimmt wird. Dieser Ansatz ist entscheidend für die Rekonstruktion der Aufrufsequenz, die zu einer Exception geführt hat.
Ein kritischer Aspekt dieser Analyse ist die Identifikation von unbacked memory regions während des Unwind-Prozesses. Unbacked memory regions sind spezielle Speicherbereiche, die von ausgeführtem Code belegt werden, aber keinem physischen Modul zugeordnet werden können - beispielsweise fehlt eine direkte Zuordnung zu bekannten Modulen wie kernel32.dll
. Dies deutet darauf hin, dass der ausgeführte Code möglicherweise aus einer Quelle stammt, die nicht durch eine auf der Festplatte vorhandene Datei repräsentiert wird, und weist auf potenzielle Sicherheitsrisiken wie die Ausführung von Schadcode hin.
Durch den Vergleich der Unwind-Daten mit den Ergebnissen der API RtlVirtualUnwind
kann die Authentizität und Legitimität des Call Stacks effektiv verifiziert werden.
NtTerminateProcess
Wenn nach einer detaillierten Analyse des Call Stacks, ausgelöst durch eine spezifische Exception, bestimmte Kriterien erfüllt sind, trifft der Endpoint Detection and Response (EDR) Mechanismus innerhalb der spezifischen Funktion 1DE044554D0
eine Entscheidung, ob der betreffende Prozess - in diesem Kontext unsere poc.exe
- mittels der API NtTerminateProcess
beendet werden soll. Ein mögliches Beispiel für ein solches Kriterium könnte die Identifizierung von Stack-Frames sein, die auf unbacked memory Bereiche hinweisen oder auf direct syscalls oder indirect syscalls schließen lassen.
Im Falle einer unerwarteten Exception erfolgt keine Behandlung durch das VEH des EDR, sondern eine Übergabe bzw. ein Sprung zur Adresse des API RtlUnhandledExceptionFilter
innerhalb der ntdll.dll
. Dies ist eine Art Last-Resort-Mechanismus unter Windows, wie mit einer unbehandelten Exception weiter verfahren werden soll.
Summary
In der Einleitung dieses Blogeintrags wurde die Hypothese aufgestellt, dass der untersuchte EDR eine neue DLL verwendet, die im Kontext bestimmter Windows-APIs eingesetzt wird, um Malware durch die gewonnene Telemetrie mittels Thread Call Stack Analyse zu identifizieren.
Mit Hilfe des Tools System Informer wurde festgestellt, dass der zu untersuchende EDR neben der bereits bekannten Hooking DLL (Inline Hooking) eine weitere neue DLL verwendet. Durch Debugging mit x64dbg konnte festgestellt werden, dass diese neue DLL verwendet wird, um neben den bereits bekannten Hooking APIs wie z.B. NtAllocateVirtualMemory
weitere APIs wie z.B. CreateThreadPoolWork
, CreateThreadPoolWait
, OpenProcess
, WriteProcessMemory
etc. zu hooken. Je nachdem, wie der EDR konfiguriert ist, beträgt die Gesamtzahl der Inline-Hooks mehr als 150.
Eine einfache statische Analyse der neuen DLL mit IDA erhärtet bereits ein wenig die aufgestellte Hypothese, da innerhalb der Funktion 1DE044554D0
APIs wie RtlAddVectoredExceptionHandler
, RtlCaptureContext
, RtlLookupFunctionEntry
, RtVirtualUnwind
, RtlUnhandledExceptionFilter
und NtTerminateProcess
verwendet werden, die im Kontext von x64 Exception Handling und Callstack Analysis verwendet werden.
Durch Debugging im Kontext des verwendeten POC und der API CreateThreadPoolWork
konnten wir feststellen, dass wir mittels mehrerer jmp
Instruktionen in die .text
Region der neuen DLL des EDR gelangen. Anschließend wird der aktuelle Inhalt der Register rcx
, rdx
, r8
und r9
mittels push
Instruktion auf den Call Stack gelegt. Zu diesem Zeitpunkt und basierend auf der x64 Calling Convention enthalten die genannten Register die Funktionsargumente der Funktion CreateThreadPoolWork
, genauer gesagt nur die Register rcx
, rdx
und r8
, da CreateThreadPoolWork
nur drei Funktionsargumente besitzt.
Wird beispielsweise im Kontext unseres POC eine Exception durch die vom EDR in Debug-Registern registrierten Hardware-Breakpoints ausgelöst, so erfolgt die Auslösung des VEH des EDRs.. Die weitere dynamische Analyse der neuen DLL ergab, dass der Vectored Exception Handler die Funktion 1DE044554D0
aufruft. Diese Funktion enthält letztlich die Logik bzw. die APIs, die notwendig sind, um, vereinfacht ausgedrückt, zu analysieren und zu entscheiden, ob der Thread Call Stack legitim erscheint oder nicht, und gegebenenfalls den Thread oder Prozess zu beenden.
Interpretation
Die Analyse der neuen DLL des Endpoint Detection and Response (EDR) Systems legt nahe, dass diese DLL im Kontext der betroffenen APIs, wie z.B. CreateThreadPoolWork
, für die Call Stack Analyse verwendet wird. Dieser Ansatz ist zweifellos interessant und wird meiner Meinung nach auch von Angreifern genutzt, allerdings mit dem Ziel, Thread Call Stack Spoofing durchzuführen.
Der Ansatz erscheint plausibel, da er potenziell zur Identifizierung von unbacked memory regions, direct syscalls und indirect syscalls verwendet werden kann. Dennoch stellt sich die Frage, ob diese Form der Call Stack Analyse nicht deutlich manipulationsanfälliger ist als andere Methoden, wie z.B. die Implementierung via Event Tracing for Windows Threat Intelligence (EtwTi). So konnte z.B. die Call Stack Analyse des betroffenen EDR relativ einfach umgangen werden, indem die .text
Region, in der sich die betroffenen (gehookten) APIs befinden, z.B. CreateThreadPoolWork
in der kernel32.dll
, durch Unhooking mit einer ungehookten Version der .text
Region der kernel32.dll
überschrieben werden.
Im Zusammenhang mit den Hardware Breakpoints wäre eine weitere denkbare Möglichkeit, die Hardware Breakpoints in den Debug Registern DR0-DR3
gezielt zu überschreiben und damit zu verhindern, dass die Exception grundsätzlich ausgelöst wird, um den Prozess der Call Stack Analyse durch den EDR zu initialisieren. Es hat sich jedoch gezeigt, dass der EDR seine registrierten Hardware Breakpoints überwacht und schützt. Mit anderen Worten, es müsste ein Weg gefunden werden, die Hardware Breakpoints so zu manipulieren oder zu überschreiben, dass der EDR dies nicht bemerkt. Aber das sind nur Vermutungen und allein dieses Thema zu beleuchten, wäre wahrscheinlich einen eigenen Artikel wert.
Ich hoffe, ich konnte Ihnen mit diesem Artikel einen kleinen Einblick in die interne Funktionsweise des EDR im Zusammenhang mit der neuen DLL und der Verwendung der Call Stack Analyse über User Mode Code geben und bedanke mich für das Lesen. Bis zum nächsten Artikel.
Happy Hacking!
Daniel Feichter @VirtualAllocEx
References
- https://www.elastic.co/securit...
- https://learn.microsoft.com/en...
- https://learn.microsoft.com/en...
- https://learn.microsoft.com/en...
- https://medium.com/@fsx30/vect...
- https://mark.rxmsolutions.com/...
- https://research.nccgroup.com/...
- https://dimitrifourny.github.i...
- https://github.com/mgeeky/Thre...
- https://klezvirus.github.io/Re...
- https://labs.withsecure.com/pu...
- https://www.codereversing.com/...