Zurück

EDR Analysis: A Hypothesis about Call Stack Analysis and Enhanced Detection

Mit diesem Blogeintrag möchte ich meine neuesten Erkenntnisse im Bereich Endpoint Detection and Response (EDR) im Zusammenhang mit Debugging und Reverse Engineering dokumentieren und teilen. Mit dem zu untersuchenden EDR hatte ich bereits zu tun und konnte damit schon Erfahrungen sammeln. Bei meiner letzten Auseinandersetzung im Rahmen eines Red Teamings zeigte der EDR ein neuartiges Erkennungsverhalten, das mir bis dahin nicht bekannt war. Dies hat mein Interesse erneut geweckt und mich motiviert, mich etwas intensiver mit dem EDR zu beschäftigen.


Disclaimer 


Es werden keine Produkt- oder Herstellernamen genannt. Aus Gründen der Vertraulichkeit habe ich alle verwendeten Bilder entsprechend anonymisiert. Der Zweck dieses Artikels ist rein akademischer Natur; die hier geteilten Informationen sind ausschließlich für Forschungszwecke bestimmt und sollten unter keinen Umständen für unethische oder illegale Aktivitäten verwendet werden.

Ich möchte auch betonen, dass ich kein Reverse Engineer bin, aber das Gebiet fasziniert mich sehr und ich möchte mehr und mehr darüber lernen und meine Fortschritte ein wenig dokumentieren. Auch erhebe ich keinen Anspruch auf Richtigkeit oder Vollständigkeit meiner Ausführungen.

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

Zuletzt aktualisiert 06.05.24 08:12:22 06.05.24
Daniel Feichter