Zurück

EDR Analysis: Leveraging Fake DLLs, Guard Pages, and VEH for Enhanced Detection

In diesem Blogbeitrag möchte ich meine neuesten Erkenntnisse und Erfahrungen im Bereich des EDR (Endpoint Detection and Response) Debuggings und Reverse Engineerings dokumentieren und teilen. Kürzlich hatte ich mit einem Endpoint Detection and Response (EDR) System zu tun, das aufgrund seines Erkennungsverhaltens mein Interesse geweckt hat. Dieses System geht über die gängigen Erkennungsmechanismen wie Inline API Hooking, Event Tracing für Windows und Kernel Callbacks hinaus. In diesem Artikel möchte ich auf einen eher unkonventionellen, aber sehr interessanten Erkennungsmechanismus eingehen, der im Zusammenhang mit dem EDR verwendet wird.


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, eine spezifische Funktion bzw. einen Detektionsmechanismus eines bestimmten EDR durch Debugging und Reverse Engineering zu analysieren. Es war nicht mein primäres Ziel, den Reverse-Engineering-Prozess bis ins letzte Detail zu durchdringen. Vielmehr ging es mir darum, ein fundiertes Verständnis für die Funktionsweise des neu implementierten Erkennungsmechanismus im EDR zu entwickeln, die Funktionen dieses Mechanismus zu erforschen und die Gründe für seine mögliche Implementierung zu hinterfragen.

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.

Detection Mechanisms

EDR-Systeme (Endpoint Detection and Response) verwenden je nach Phase unterschiedliche Mechanismen zur Erkennung von Malware. In der statischen Phase werden häufig Scanner eingesetzt, die Dateien auf bekannte Hashwerte, Signaturen und bestimmte Bytefolgen untersuchen. Sollte es einem Angreifer gelingen, diese statische Erkennung zu umgehen, greifen viele EDRs auf Sandboxing zurück. Dabei wird eine potenziell verdächtige Datei in einer virtualisierten Umgebung ausgeführt, um ihr Verhalten zu analysieren. Zusätzlich implementieren EDRs Methoden wie User-Mode Hooking (z.B. Inline API Hooking oder IAT Hooking), das Antimalware Scan Interface (AMSI), Event Tracing für Windows sowie Threat Intelligence und Kernel Callbacks für die verhaltensbasierte Erkennung. 

In einigen meiner früheren Artikel habe ich das Konzept von Inline API-Hooking bereits mehrmals angesprochen, das von einigen EDRs verwendet wird, um aktiv in die Ausführung von Code und Parametern von Windows APIs einzugreifen. Für detaillierte Informationen dazu verweise ich auf meinen letzten Blog-Beitrag Syscalls via Vectored Exception Handling.

Unconventional Detection 

In diesem Artikel möchte ich jedoch auf einen eher unkonventionellen, aber sehr interessanten Erkennungsmechanismus eingehen, der im Zusammenhang mit dem EDR verwendet wird und auf einer Kombination aus Modifikation des Process Environment Blocks (PEB), der Verwendung von Fake DLLs und Guard Pages sowie der Verwendung von Vectored Exception Handling basiert.

Fake DLLs

Bei der Initialisierung eines Prozesses unter Windows werden die benötigten DLLs in den virtuellen Speicher geladen. Dies geschieht in einer bestimmten Reihenfolge, die von den Abhängigkeiten und Anforderungen des jeweiligen Prozesses abhängt. Für systemweite DLLs wie ntdll.dll und kernel32.dll legt das Betriebssystem Verweise (Pointer) im virtuellen Speicher des Prozesses ab. Diese Pointer zeigen auf die tatsächlichen physischen Speicherorte der DLLs im Shared Memory.

Die Reihenfolge, in der die DLLs geladen werden, wird durch die double linked list InLoadOrderModuleList innerhalb der Struktur PEB_LDR_DATA im Process Environment Block (PEB) festgelegt. Dabei sind die ntdll.dll und die kernel32.dll als Schlüsselkomponenten für jeden Windows-Prozess unverzichtbar und werden immer geladen. 

Eine besondere Beobachtung ergibt sich bei der Untersuchung eines aktiven Prozesses, wie z.B. cmd.exe, auf einem System, das mit einem zu untersuchenden EDR ausgestattet ist. Wir verwenden das Tool Process Hacker, um den aktiven Prozess cmd.exe genauer zu analysieren, insbesondere konzentrieren wir uns auf den Reiter Module, um die in cmd.exe geladenen Module zu überprüfen. Zunächst sieht es so aus, als ob die kernel32.dll und ntdll.dll doppelt geladen wurden. Bei näherer Betrachtung stellt sich jedoch heraus, dass die Schreibweise dieser scheinbar doppelten DLLs unterschiedlich ist, da jeweils eine Version in Leetspeak geschrieben ist. Eine genauere Untersuchung dieser imitierten DLL-Versionen mit Process Hacker zeigt, dass die Dateigrößen der Imitationen exakt mit denen der Original-DLLs übereinstimmen. Auffällig ist auch, dass den Imitationen die Modulbeschreibungen fehlen. 

Versuche, diese vermeintlichen Imitationen mit Process Hacker näher zu untersuchen, führen zu keinem Ergebnis. Sie sind nicht analysierbar und die entsprechende Images dieser Dateien sind auf der Festplatte nicht auffindbar (Fehlermeldung: Unable to load the PE file: The object name was not found). Offensichtlich handelt es sich bei diesen Imitationen um eine Art gefälschte Versionen der entsprechenden DLLs, weshalb wir sie als "Fake DLLs" bezeichnen wollen. Bei näherer Betrachtung der Fake DLL im Kontext der ntdll.dll mit Process Hacker stellt sich heraus, dass es sich tatsächlich um eine manuell gemappte Version der ntdll.dll handelt, allerdings mit verändertem Namen in Leetspeek. Ein weiteres interessantes Merkmal zeigt sich bei näherer Betrachtung der Memory Protection: Der als RX (read-execute) allokierte Speicherbereich ist zusätzlich mit einer Guard Page versehen. Wir merken uns dieses Detail und schauen uns später genauer an, was es damit auf sich hat. 

Was wir aber jetzt schon sagen können, ist, dass es sich bei der "Fake DLL" nicht um eine echte eigenständige DLL handelt, die auf der Disk zu finden ist, sondern um eine manuell gemappte Version der ntdll.dll, die allerdings manuell in einen der ntdll.dll ähnlichen Namen umbenannt wurde.

May I Check Your P3B?

Um ein besseres Verständnis der Rolle der Fake DLLs im Kontext des zu analysierenden EDR-Systems zu erlangen, richten wir unsere Aufmerksamkeit auf den Process Environment Block (PEB) eines aktiven Prozesses - z.B. cmd.exe - auf einem System mit dem installierten EDR.

Der PEB ist eine entscheidende Speicherstruktur in jedem Windows-Prozess, verantwortlich für das Management prozessspezifischer Daten wie der Programmbasisadresse, des Heaps, der Umgebungsvariablen und der Kommandozeileninformationen. Innerhalb seiner Struktur, die zahlreiche Felder und Pointer umfasst, ist besonders die Ldr Struktur hervorzuheben, die für die Verwaltung der geladenen Module, insbesondere der DLLs, zuständig ist. Der PEB ist für jeden Prozess einzigartig und nimmt eine Schlüsselrolle in der Prozessverwaltung und im DLL-Lademechanismus ein. Es versorgt den Prozess mit den notwendigen Informationen und Ressourcen für eine effiziente Ausführung und Verwaltung.

Eine umfassende Erörterung des PEBs würde allerdings den Umfang dieses Artikels überschreiten. Für ein tiefergehendes Verständnis empfehle ich Interessierten, sich in die Windows Internals zu vertiefen. Unser primäres Ziel ist es jedoch, mehr über die Verwendung von Fake DLLs durch den EDR zu erfahren.

Um zu untersuchen, wann genau die Fake-DLLs in den Speicher gemappt werden, verwenden wir WinDbg. Nachdem wir uns an einen aktiven cmd.exe Prozess attached haben, versuchen wir innerhalb des Process Environment Blocks (PEB) auf die darin enthaltene double linked list InLoadOrderModuleList zuzugreifen. Dieser Schritt ermöglicht es uns, die Ladezeitpunkte und die Reihenfolge der Module im Speicher zu analysieren, einschließlich der Identifizierung von Fake-DLLs. 

Der erste Schritt besteht darin, den Befehl !peb in WinDbg zu verwenden, um auf den PEB von cmd.exe zuzugreifen. Wie in der folgenden Abbildung zu sehen ist, befindet sich an zweiter Stelle der InMemoryOrderModuleList die fake Version der ntdll.dll und an dritter Stelle die fake Version der kernel32.dll. Dies bestätigt, dass diese erfolgreich in den Speicher des Prozesses gemappt wurden. Es ist wichtig zu erwähnen, dass der Unterschied zwischen der InMemoryOrderModuleList und der InLoadOrderModuleList darin besteht, dass die InMemoryOrderModuleList die Module in der Reihenfolge auflistet, in der sie sich im virtuellen Speicher des Prozesses befinden. Die InLoadOrderModuleList hingegen gibt die Reihenfolge an, in der die DLLs geladen werden.

Um diese Beobachtung zu verifizieren, wollen wir innerhalb der Ldr Struktur auf die InLoadOrderModuleList zugreifen und die an zweiter und dritter Stelle aufgeführten Module überprüfen. Dazu muss zunächst die Adresse von Ldr (in diesem Fall 00007ffa61ebc4c0) ermittelt werden. Anschließend wird in WinDbg mit dem Befehl dt nt!_PEB_LDR_DATA 00007ffa61ebc4c0 auf die Struktur PEB_LDR_DATA zugegriffen. Die folgende Abbildung zeigt den Zugriff auf die Struktur PEB_LDR_DATA. Ausgehend von der Basisadresse mit einem Offset von 0x10 befindet sich der Eintrag InLoadOrderModuleList, der uns eindeutig darüber Auskunft gibt, ob die fake ntdll.dll während der Prozessinitierung an zweiter Stelle und die fake kernel32.dll an dritter Stelle wirklich geladen wurde.

Im nächsten Schritt greifen wir über die Startadresse von InLoadOrderModuleList (in diesem Fall 0x000001d5`eb302650) auf das erste Modul innerhalb dieser Liste zu. Dies geschieht durch den Befehl dt nt!_LDR_DATA_TABLE_ENTRY 0x000001d5`eb302650 in WinDbg. Die folgende Abbildung zeigt, dass das erste Modul das Image von cmd.exe selbst ist. Dieses Ergebnis ist zu erwarten, da das Image des ausführenden Prozesses in der Reihenfolge der Module immer an erster Stelle stehen muss.

Um auf das zweite Modul in der Modulreihenfolge zuzugreifen, verwenden wir wieder den vorherigen Befehl in WinDbg, aktualisieren aber die Adresse auf die Startadresse von InLoadOrderLinks, die in diesem Fall 0x000001d5`eb313d10 ist. Die folgende Abbildung nach Ausführung dieses Befehls zeigt, dass tatsächlich die imitierte Version der ntdll.dll als zweites Modul in der Reihenfolge geladen wird.

Um auch das dritte Modul in der Modul-Ladereihenfolge zu untersuchen, wiederholen wir den vorherigen Schritt in WinDbg, aktualisieren aber die Adresse entsprechend auf die Startadresse der InLoadOrderLinks für das dritte Modul, in diesem Fall 0x000001d5`eb317c20. Die folgende Abbildung bestätigt unsere vorherigen Erkenntnisse: Die fake kernel32.dll wird als drittes Modul in der Liste geladen.

Um unsere Analyse vervollständigt abzuschließen, wiederholen wir den Schritt in WinDbg noch zweimal, um die Positionen der weiteren Module in der Ladereihenfolge zu ermitteln. Die daraus resultierende Abbildung zeigt, dass die echte ntdll.dll an vierter Stelle und die echte kernel32.dll an fünfter Stelle in der Modul-Ladereihenfolge steht. Diese Information wird im weiteren Verlauf noch wichtig sein.

Ergänzend ist anzumerken, dass anhand von InLoadOrderModuleList auch festgelegt wird, an welcher Stelle die Hooking DLL des EDR geladen wird. In diesem Fall wird die Hooking DLL während der Prozessinitialisierung als siebtes Modul geladen, nachdem die kernelbase.dll als sechstes Modul geladen wurde.

Where is the catch?

Wird die gleiche Analyse mit WinDbg auf einem Endpoint ohne installiertes EDR-System oder auf einem Endpoint mit einem alternativen EDR-System durchgeführt, ergibt sich ein deutlich anderes Bild in Bezug auf die Reihenfolge, in der die Module innerhalb der InLoadOrderModuleList geladen werden. Die Analyseergebnisse zeigen, dass in diesen Szenarien keine Fake-DLLs vorhanden sind. Außerdem wird deutlich, dass die ntdll.dll wie üblich an zweiter Stelle und die kernel32.dll an dritter Stelle der Modulliste geladen werden. Dieser direkte Vergleich unterstreicht die Einzigartigkeit des ursprünglich untersuchten EDR-Systems, das sich durch die Verwendung von Fake-DLLs und eine abweichende Ladereihenfolge der Module von Standardkonfigurationen unterscheidet.

Unsere Analyseergebnisse bzw. der Vergleich mit WinDbg machen deutlich, dass der Process Environment Block (PEB) bzw. spezifischer die InLoadOrderModuleList des Prozesses - hier am Beispiel der cmd.exe - auf dem Endpoint mit dem spezifischen EDR-System gezielt modifiziert werden.

Guard Pages

Durch die Manipulation der InLoadOrderModuleList im PEB wird durch den EDR sichergestellt, dass bei der Initialisierung eines Prozesses im Usermode zunächst die Fake-Versionen der ntdll.dll und kernel32.dll an zweiter und dritter Stelle geladen werden, bevor die echten ntdll.dll und kernel32.dll an vierter und fünfter Stelle folgen. Was aber ist der Zweck dieser Manipulation durch den EDR? 

Um diese Frage zu beantworten, werfen wir noch einmal einen Blick auf die Fake-Versionen der ntdll.dll und kernel32.dll. Wie bereits festgestellt, handelt es sich um manuell in den Speicher gemappte Versionen der Original-DLLs, ergänzt um eine Guard Page im Speicherbereich, die als RX (read-execute) committed wird. Die Guard Page kann laut Microsoft-Dokumentation als Auslöser für eine STATUS_GUARD_PAGE_VIOLATION (0x80000001) Exception fungieren. 

Diese Beobachtungen legen nahe, dass die Manipulation des Process Environment Block (PEB) mittels Fake DLLs und Guard Pages vermutlich dazu dient, einen vom EDR registrierten Vectored Exception Handler (VEH) zu aktivieren. Eine genauere Untersuchung der x64 Hooking DLL des EDR unterstützt diese Theorie: Die Windows APIs AddVectoredExceptionHandler und RemoveVectoredExceptionHandler, die für die Implementierung eines VEH benötigt werden, werden von der kernel32.dll importiert. Sollte der EDR tatsächlich einen VEH registrieren, würde dies bedeuten, dass bei einer über eine Guard Page ausgelösten Exception der EDR über den VEH die Kontrolle über den Programmablauf übernimmt, anstatt dass die Exception an den Structured Exception Handler (SEH) weitergegeben wird. 

Aus einer anderen Perspektive betrachtet, muss nach dem Auslösen der Guard Page in der Fake DLL des EDR die Exception STATUS_GUARD_PAGE_VIOLATION (0x80000001) von einem Vectored Exception Handler oder dem Structured Exception Handler behandelt werden. Angesichts des Aufwands, den der EDR betreibt, erscheint eine Übergabe an den SEH eher unwahrscheinlich, während eine Übergabe an einen speziell registrierten VEH wesentlich plausibler und sinnvoller erscheint. D.h. der EDR kann nach Auslösen der Exception aktiv in den weiteren Programmablauf eingreifen und so ggf. Schadsoftware verhindern. Letztlich führt dies zu einer Umleitung des Programmablaufs einer Applikation, was letztlich als Hooking bezeichnet werden kann. Genauer als Guard Page Hooking oder Page Guard Hooking, wie in diesem Artikel beschrieben. 

Der folgende Code zeigt, wie die Implementierung für Page Guard Hooking in Verbindung mit Vectored Exception Handling in C aussehen kann. Es ist wichtig zu betonen, dass dieser Code nur ein Beispiel und ein Grundgerüst darstellt und nicht die spezifische Logik des EDR für die Reaktion auf Exceptions enthält. Es wäre jedoch denkbar, dass der EDR nach dem Auslösen der Guard Page diese in der entsprechenden Fake DLL wiederherstellt, um die Überwachung mittels Guard Page kontinuierlich fortsetzen zu können.

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

// Vectored Exception Handler function
// This function is called when an exception occurs, such as a guard page violation
LONG CALLBACK GuardPageExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) {
    // Check if the exception is a guard page violation
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) {
        printf("Guard Page Access Detected!\n");
        // Here you can add logic to log the violation, analyze the access pattern,
        // or take any other appropriate action based on your EDR's requirements.

        // Optional: Restore the guard page here if you want continuous monitoring

        // Continue execution after handling the exception
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    // If it's not a guard page violation, continue searching for other handlers
    return EXCEPTION_CONTINUE_SEARCH;
}

int main() {
    // Set up a sensitive area of memory to monitor
    // This could represent a critical section of memory you want to protect
    SYSTEM_INFO si;
    GetSystemInfo(&si); // Get system information, including page size
    LPVOID pMemory = VirtualAlloc(NULL, si.dwPageSize, MEM_COMMIT, PAGE_READWRITE);
    if (pMemory == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // Protect the sensitive memory with a guard page
    // Any access to this page will trigger the guard page violation
    DWORD oldProtect;
    if (!VirtualProtect(pMemory, si.dwPageSize, PAGE_GUARD | PAGE_READWRITE, &oldProtect)) {
        printf("Failed to set guard page\n");
        VirtualFree(pMemory, 0, MEM_RELEASE); // Clean up if setting the guard page fails
        return 1;
    }

    // Register the Vectored Exception Handler
    // This handler will be invoked for exceptions, including guard page violations
    PVOID handler = AddVectoredExceptionHandler(1, GuardPageExceptionHandler);
    if (handler == NULL) {
        printf("Failed to add Vectored Exception Handler\n");
        VirtualFree(pMemory, 0, MEM_RELEASE); // Clean up if handler registration fails
        return 1;
    }

    // Your application logic goes here
    // This is where you would implement the rest of your EDR's functionality
    // ...

    // Clean up before exiting the application
    // This includes unregistering the exception handler and freeing allocated memory
    RemoveVectoredExceptionHandler(handler);
    VirtualFree(pMemory, 0, MEM_RELEASE);

    return 0;
}

Interessant ist auch, dass innerhalb der Hooking DLL die Windows API AddVectoredExceptionHandler zusammen mit der API LdrEnumerateLoadedModules, der ntdll.dll und dem Begriff PEBTrap auftritt. Dies könnte möglicherweise ein Hinweis auf das Zusammenspiel zwischen der Modifikation des PEB, den Fake DLLs, der Guard Page und dem Vectored Exception Handler sein.

Um besser zu verstehen, unter welchen Bedingungen der Vectored Exception Handler (VEH) des EDR durch STATUS_GUARD_PAGE_VIOLATION (0x80000001) aktiviert wird, liefert eine nähere Betrachtung der Hooking DLL interessante Erkenntnisse. Es scheint, dass innerhalb dieser DLL eine spezielle Vergleichsoperation stattfindet, die prüft, ob nach dem Aufruf einer Native API die entsprechende Exception mit dem Code 0x80000001 aufgrund der Auslösung der Guard Page in der fake ntdll.dll auftritt. Konkret bedeutet dies: Wird eine Exception mit dem Code 0x80000001 ausgelöst, wird der Vectored Exception Handler des EDR aktiv (und beendet ggf. den Prozess). Folgt jedoch nach dem Aufruf der entsprechenden Native API, z.B. NtProtectVirtualMemory, keine Exception mit dem Wert 0x80000001, so ist die weitere Ausführung der Native API erlaubt. Dies ist jedoch derzeit eine Hypothese und keine eindeutige Aussage. Interessant ist jedoch, dass dieser Vergleich in der Hooking DLL für ca. 25 Native APIs durchgeführt wird, darunter NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory etc. die häufig im Zusammenhang mit der Ausführung von Malware auftauchen.

DLL Base vs. Original Base

Das Hauptproblem aus Sicht eines Malware-Entwicklers tritt dann auf, wenn die Malware darauf angewiesen ist, dynamisch Informationen aus dem PEB bzw. aus der ntdll.dll oder kernel32.dll zu beziehen. Ein konkretes Beispiel hierfür ist die Verwendung von Shellcode Loader oder Shellcode, die beispielsweise direct oder indirekt Syscalls verwenden, ohne die System Service Numbers (SSNs) fest im Code zu verankern. Stattdessen wird versucht, diese SSNs innerhalb der ntdll.dll dynamisch über eine Kombination aus PEB-Walk und Export Address Table (EAT) parsing zu erhalten. 

Um mittels PEB-Walk auf die Basisadresse der ntdll.dll zugreifen zu können, wird üblicherweise der Offset 0x30 für DLLBase verwendet. Im Kontext unseres EDR führt dies jedoch dazu, dass man im Speicher der Fake-Version der ntdll.dll landet und somit den Vectored Exception Handler des EDR via Page Guard Hook auslöst.

Um die Richtigkeit der gemachten Aussage zu untermauern, planen wir ein Experiment mit dem folgenden C-Code. Unser Ziel ist es, durch einen PEB-Walk und Zugriff auf die Basisadresse der ntdll.dll über den Offset 0x30 DllBase deren Speicheradresse zu ermitteln und auszugeben. Anschließend wollen wir diese Adresse mit den Speicheradressen der echten und der fake ntdll.dll im Speicher der cmd.exe vergleichen.

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

UINT_PTR NtdllDllBase() {
    // Read the PEB Offset from the GS Register
    UINT_PTR pebAddress = __readgsqword(0x60);
    // Access the PEB_LDR_DATA field within PEB
    UINT_PTR ldr = *(UINT_PTR*)(pebAddress + 0x18);
    // Access the first entry in the InInitializationOrderModuleList
    UINT_PTR inInitOrderModuleList = *(UINT_PTR*)(ldr + 0x10);
    // Traverse to the second module in the list (typically ntdll.dll)
    UINT_PTR secondModule = *(UINT_PTR*)(inInitOrderModuleList);
    // Uncomment the following line to advance to the third module
    // secondModule = *(UINT_PTR*)(secondModule);
   
    // Access the base address of the module by using DLL Base
    UINT_PTR baseAddress = *(UINT_PTR*)(secondModule + 0x30);

    return baseAddress;
}

int main() {
    UINT_PTR ntdllBase = NtdllDllBase();

    printf("Base address (offset 0x30) of the loaded ntdll.dll: %p\n", (void*)ntdllBase);
    printf("Press any key to exit...");

    getchar(); // wait for keypress

    return 0;
}

In der folgenden Abbildung ist zu erkennen, dass die anhand des Offsets 0x30 DllBase ermittelte Basisadresse mit der im Speicher befindlichen ntdll.dll nicht übereinstimmt. Untersucht man jedoch die vom EDR-System implementierte fake ntdll.dll, so findet man eine Übereinstimmung der Speicheradressen. Das bedeutet, dass bei einem PEB-Walk, wenn der Offset 0x30 für die DllBase verwendet wird, nicht auf den Speicherbereich der echten ntdll.dll zugegriffen wird, sondern auf den der fake ntdll.dll, die für Page Guard Hooking durch das EDR-System verwendet wird.  

Im nächsten Schritt unseres Experiments wollen wir unseren C-Code so anpassen, dass wir nicht nur über den Offset 0x30 (DllBase) auf die Basisadresse der ntdll.dll zugreifen und diese ausgeben, sondern auch über den Offset 0xF8 (OriginalBase) die Basisadresse derselben DLL ermitteln und ausgeben. Diese OriginalBase bietet eine alternative Methode, um auf die Basisadresse eines Moduls in der InLoadOrderModuleList zuzugreifen. Durch diese Erweiterung unseres Codes können wir die beiden ermittelten Adressen mit den im Speicher befindlichen Adressen der echten und der gefälschten ntdll.dll vergleichen.

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

UINT_PTR NtdllDllBase() {
    // Read the PEB Offset from the GS Register
    UINT_PTR pebAddress = __readgsqword(0x60);
    // Access the PEB_LDR_DATA field within PEB
    UINT_PTR ldr = *(UINT_PTR*)(pebAddress + 0x18);
    // Access the first entry in the InInitializationOrderModuleList
    UINT_PTR inInitOrderModuleList = *(UINT_PTR*)(ldr + 0x10);
    // Traverse to the second module in the list (typically ntdll.dll)
    UINT_PTR secondModule = *(UINT_PTR*)(inInitOrderModuleList);
    // Uncomment the following line to advance to the third module
    // secondModule = *(UINT_PTR*)(secondModule);
   
    // Access the base address of the module
    UINT_PTR baseAddress = *(UINT_PTR*)(secondModule + 0x30);

    return baseAddress;
}

UINT_PTR NtdllOriginalDLLBase() {
    // Read the PEB Offset from the GS Register
    UINT_PTR pebAddress = __readgsqword(0x60);
    // Access the PEB_LDR_DATA field within PEB
    UINT_PTR ldr = *(UINT_PTR*)(pebAddress + 0x18);
    // Access the first entry in the InInitializationOrderModuleList
    UINT_PTR inInitOrderModuleList = *(UINT_PTR*)(ldr + 0x10);
    // Traverse to the second module in the list (typically ntdll.dll)
    UINT_PTR secondModule = *(UINT_PTR*)(inInitOrderModuleList);
    // Uncomment the following line to advance to the third module
    // secondModule = *(UINT_PTR*)(secondModule);
    
    // Access the base address of the module by using Original Base
    UINT_PTR baseAddress = *(UINT_PTR*)(secondModule + 0xF8);

    return baseAddress;
}

int main() {
    UINT_PTR ntdllBase = NtdllDllBase();
    UINT_PTR ntdllOriginalBase = NtdllOriginalDLLBase();

    printf("Base address (offset 0x30) of the loaded ntdll.dll: %p\n", (void*)ntdllBase);
    printf("Original base address (offset 0xF8) of the loaded ntdll.dll: %p\n", (void*)NtdllOriginalDLLBase());
    printf("Press any key to exit...");

    getchar(); // wait for keypress

    return 0;
}

Nachdem wir unseren erweiterten Code auf dem Endpoint mit installiertem EDR ausgeführt haben, zeigt die folgende Abbildung ein aufschlussreiches Ergebnis: Die unter Verwendung des Offsets 0xF8 für die OriginalBase ermittelte Speicheradresse entspricht der Adresse der echten ntdll.dll. Dies bestätigt, dass ein PEB-Walk unter Verwendung des Offsets 0x30 (DllBase) uns in den Speicherbereich der vom EDR eingefügten fake ntdll.dll führt. Wenn wir jedoch den Offset 0xF8 für die OriginalBase wählen, gelangen wir stattdessen in den Speicherbereich der echten ntdll.dll.  

Vectored Exception Handling 

Bevor wir uns im nächsten Punkt ein wenig der internen Logik der zu untersuchenden Detection Chain widmen, möchte ich etwas näher darauf eingehen, wie die Theorie, dass der zu untersuchende EDR Vectored Exception Handling im Kontext spezifischer Prozesse verwendet, untermauert werden kann. Wir haben bereits gesehen, dass der EDR die Funktionen AddVectoredExceptionHandler und RemoveVectoredExceptionHandler aus der kernel32.dll importiert. Um aber zu beweisen, dass ein Prozess einen Vectored Exception Handler verwendet oder von einem registrierten Vectored Exception Handler überwacht wird, schauen wir uns den Process Environment Block (PEB) etwas genauer an. Mittels Debugging über Windbg wollen wir uns den Wert für CrossProcessFlags (Offset 0x50 für 64bit) innerhalb des PEB ansehen. Basierend auf dem folgenden Artikel von Olllie Whitehouse und der Dokumentation von Geoff Chapell, kann uns der CrossProcessFlags Eintrag darüber informieren, ob ein Prozess einen VEH verwendet. Mit anderen Worten, wenn der Eintrag CrossProcessFlags den dezimalen Wert 4 hat, verwendet der Prozess einen VEH. Hat der Eintrag CrossProcessFlags dagegen den dezimalen Wert 0, so verwendet der Prozess keinen VEH. 

In der folgenden Abbildung kann dies gut beobachtet werden, auf einer VM mit dem zu untersuchenden EDR wurde eine notepad.exe gestartet und mittels Windbg innerhalb des PEB der Wert für den Eintrag CrossProcessFlags überprüft. Es ist deutlich zu erkennen, dass CrossProcessFlags den dezimalen Wert 4 (in hex 0x00000004) hat und somit einen VEH verwendet bzw. vermutlich den VEH des EDR verwendet (dies wird aber später noch genauer untersucht). Im Vergleich auf der rechten Seite der Abbildung sehen wir jedoch die gleiche Prozedur auf einer VM ohne installierten EDR. Wie erwartet hat hier der Eintrag CrossProcessFlags den Wert 0, d.h. der Prozess notepad.exe verwendet keinen VEH. 

Die Untersuchung des CrossProcessFlags Eintrags im PEB gibt uns also Aufschluss darüber, ob ein Prozess Vectored Exception Handling verwendet, aber es wäre auch interessant zu wissen, welches Modul für die Registrierung des VEH verantwortlich ist. Mit anderen Worten, wir wollen nachweisen, dass der VEH vom EDR registriert wurde. Um diesen Nachweis erbringen zu können, laden wir das image notepad.exe mit einem Debugger wie z.B. x64dbg und setzen einen Breakpoint auf die native API RtlAddVectoredExceptionHandler. Wenn der Breakpoint ausgelöst wird, schauen wir uns den Call Stack an und überprüfen, von welcher Adresse bzw. von welchem Modul die Funktion RtlAddVectoredExceptionHandler aufgerufen wurde. Mit anderen Worten, wenn die Registrierung des VEH durch den EDR erfolgt, ist zu erwarten, dass die Adresse auf dem Call Stack vor dem Aufruf von RtlAddVectoredExceptionHandler eine Adresse in Bezug auf den EDR ist. 

Die folgende Abbildung unterstreicht diese Erwartung, es ist zu erkennen, dass nach dem Auslösen des Breakpoints im Kontext von RtlAddVectoredExceptionHandler zuvor der Stack Frame im Kontext der User Mode Hooking DLL des EDR aufgerufen wurde. Dies zeigt, dass der Funktionsaufruf für RtlAddVectoredExceptionHandler durch die User Mode Hooking DLL des EDR erfolgt ist. 

Durch diese Untersuchungen konnten wir zum einen anhand des Eintrags CrossProcessFlags im PEB überprüfen, ob ein Prozess einen VEH verwendet und zum anderen nachweisen, dass der Aufruf der nativen Funktion RtlAddVectoredExceptionHandler, die für die Registrierung des VEH notwendig ist, durch die User-Mode Hooking DLL des EDR erfolgt.

EDR DLL - Internal Logic

Bevor wir uns mit der Zusammenfassung und Möglichkeiten zur Evasion befassen, sollten wir versuchen, besser zu verstehen, wie der EDR überprüft, ob das GUARD_PAGE Flag im Kontext einer der betrügerischen DLLs ausgelöst wurde. Wie wir bereits wissen, sind die fake DLLs keine echten DLLs, sondern nur manuell gemappte Versionen, z.B. im Zusammenhang mit der ntdll.dll. Der EDR benötigt jedoch noch einen speicherinternen Teil, um die Logik zu handhaben oder um zu prüfen, ob die GUARD_PAGE im Kontext einer der fake DLLs ausgelöst wurde. Durch einen Blick auf die EDR-Module im Speicher können wir die DLL oder das Modul des EDR identifizieren, das für den Inline-Hooking-Teil verwendet wird. Im Zusammenhang mit diesem EDR ist dies die einzige echte DLL neben den fake DLLs, die der EDR im Speicher eines Prozesses im user mode verwendet, also wollen wir uns die Hooking-DLL des EDR genauer ansehen, um zu sehen, ob wir irgendwelche wichtigen Verbindungen im Zusammenhang mit unserer Forschung über fake DLLs usw. finden können.

Wie bereits erwähnt, wollte ich herausfinden, wo sich der logische Teil innerhalb des EDR befindet, um zu sehen, ob die Exception mit STATUS_GUARD_PAGE_VIOLATION oder genauer mit dem zugehörigen Exception Code 0x80000001 zusammenhängt. Also hatte ich eine einfache Idee mit x64dbg. Wir öffnen eine beliebige Anwendung wie z.B. notepad.exe oder cmd.exe, verbinden uns mit x64dbg und suchen im ersten Schritt in der Memory Map nach der Basisadresse der Hooking-DLL. Im zweiten Schritt erstellen wir ein Suchmuster (Pattern), das verwendet werden kann, um Vergleichsoperationen cmp mit dem Wert 0x80000001 zu identifizieren. Mit anderen Worten, wir suchen innerhalb der Hooking-DLL nach Vergleichsoperationen gegen den Exception-Code im Kontext von STATUS_GUARD_PAGE_VIOLATION. Wir wollen also nach dem Muster cmp eax, 80000001h suchen, basierend auf little endian müssen wir unser Muster in 3D 01 00 00 80 umwandeln. Dies ist unser Suchmuster, und nachdem wir es in der x64dbg Pattern-Suche verwendet haben, können wir mehrere Vergleichsoperationen gegen 80000001h im Speicher der Hook-DLL beobachten. Ich denke, die folgende Abbildung ist ein plausibler Hinweis darauf, dass die Logik des EDR, die überprüft, ob STATUS_GUARD_PAGE_VIOLATION 0x80000001 im Kontext einer der fake DLLs ausgelöst wurde, innerhalb der Hook-DLL des EDR platziert ist und dann weitere Schritte innerhalb der Hook-DLL je nach Szenario durchführt.

Die obige Abbildung zeigt, dass aufgrund der Vergleichsoperation cmp eax, 80000001h die Funktion 7FFE77F5AE51 aufgerufen wird, wenn das Register eax ungleich dem Wert 80000001h ist. Andernfalls, wenn der Wert von eax gleich 80000001h ist, wird die Funktion 7FFE77F56230 aufgerufen. Mit anderen Worten: Wenn das GUARD_PAGE Flag im Speicher einer der fake DLLs getriggert wird, wird die Funktion 7FFE77F56230 aufgerufen.

Summary

Bevor wir auf mögliche Umgehungstechniken eingehen, möchten wir unsere Analyse des EDR-Systems kurz zusammenfassen. Unsere Untersuchung ergab, dass der EDR eine gezielte Modifikation der InLoadOrderModuleList innerhalb des Process Environment Block (PEB) vornimmt, indem er fake DLLs verwendet. Diese fake DLLs sind keine eigenständigen Entitäten, sondern manuell gemappte Versionen der Original-DLLs, wie z.B. ntdll.dll. Ein Schlüsselaspekt dieser gefälschten DLLs ist, dass ihr RX-Speicherbereich (Read-Execute) mit einer Guard Page versehen ist. Wenn auf diesen Speicherbereich zugegriffen wird - entweder lesend oder ausführend - löst die Schutzseite eine STATUS_GUARD_PAGE_VIOLATION (0x80000001) Exception aus, ein Mechanismus, der auch als Page Guard Hooking bekannt ist. Diese Ausnahme aktiviert dann den Vectored Exception Handler (VEH) des EDR und ermöglicht es dem EDR, aktiv in den Programmablauf der Anwendung einzugreifen. Die genauen Maßnahmen, die nach der Aktivierung des VEH des EDR ergriffen werden, sind zu diesem Zeitpunkt jedoch noch unklar und würden eine weitere Untersuchung des EDR-Systems erfordern.

Zusammenfassend lässt sich sagen, dass diese Technik es dem EDR ermöglicht, schädliche Aktivitäten zu überwachen und möglicherweise zu entschärfen, indem der Programmablauf von (potenziell schädlichen) Anwendungen kontrolliert wird.

Possible Evasion Strategies

Abschließend betrachten wir mögliche Evasion-Strategien im Kontext eines PEB-Walks, z.B. für die dynamische Abfrage von System Service Numbers (SSNs) zur Durchführung von direct oder indirect Syscalls. 

Offset OriginalBase

Wie wir in unserer Analyse festgestellt haben, wird der Erkennungsmechanismus des EDR aktiviert, wenn der Offset 0x30 (DllBase) während des PEB-Walks verwendet wird, die Guard Page ausgelöst und schlussendlich der VEH des EDRs getriggert wird. Eine mögliche Umgehungsstrategie könnte darin bestehen, den Offset 0xF8 für die OriginalBase zu verwenden, um die Basisadresse der echten DLL, z.B. ntdll.dll, zu ermitteln, anstatt auf die fake DLL zuzugreifen. Dies kann jedoch je nach Windows-Version zu Problemen führen, da der Offset für die OriginalBase möglicherweise unterschiedlich ist. Genauere Untersuchungen dazu wurden bisher nicht durchgeführt. 

UINT_PTR NtdllOriginalDLLBase() {
    // Read the PEB Offset from the GS Register
    UINT_PTR pebAddress = __readgsqword(0x60);
    // Access the PEB_LDR_DATA field within PEB
    UINT_PTR ldr = *(UINT_PTR*)(pebAddress + 0x18);
    // Access the first entry in the InInitializationOrderModuleList
    UINT_PTR inInitOrderModuleList = *(UINT_PTR*)(ldr + 0x10);
    // Traverse to the second module in the list (typically ntdll.dll)
    UINT_PTR secondModule = *(UINT_PTR*)(inInitOrderModuleList);
    // Uncomment the following line to advance to the third module
    // secondModule = *(UINT_PTR*)(secondModule);
    
    // Access the base address of the module by using Original Base
    UINT_PTR baseAddress = *(UINT_PTR*)(secondModule + 0xF8);

    return baseAddress;
}

Access Correct Module via Flink

Eine weitere Strategie könnte darin bestehen, über den PEB-Walk gezielt auf das vierte Modul in der double linked list InLoadOrderModuleList zuzugreifen, das in diesem Fall der richtigen ntdll.dll entspricht und somit die Fake dll des EDR umgehen kann. Für einen zuverlässigen Betrieb in der Praxis (man weiß z.B. während der Vorbereitung in einem Red Teaming nicht, welcher EDR in der Zielumgebung betrieben wird) müssen jedoch ggf. zusätzliche Prüfungen implementiert werden, z.B. eine Schleife, die einen Vergleich der DLL-Namen durchführt. So kann z.B. verhindert werden, dass auf das vierte Modul innerhalb der InLoadOrderModuleList nur dann zugegriffen wird, wenn eine Modifikation des PEB durch den EDR erfolgt ist.

// Get base address from ntdll.dll on machine with default InLoadOrderModuleList in PEB 
	PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);

// Get base address from ntdll.dll on machine with modified InLoadOrderModuleList in PEB in context of the analysed EDR
	PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink->Flink->Flink - 0x10);

Windows APIs

Als eine weitere mögliche Umgehungsmethode betrachten wir den Einsatz der Windows API-Funktion GetModuleHandleA, um die Basisadresse der ntdll.dll zu ermitteln. Der folgende Code zeigt, wie GetModuleHandleA verwendet werden kann, um diese Basisadresse zu ermitteln. Nachdem der Code auf einem System mit dem zu untersuchenden EDR ausgeführt wurde, kann überprüft werden, ob auf den Speicherbereich der echten ntdll.dll oder der fake ntdll.dll zugegriffen wird. Auf diese Weise kann untersucht werden, inwieweit die spezifische Modifikation des PEB durch den EDR auch Auswirkungen auf die Verwendung von GetModuleHandleA hat.

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

UINT_PTR NtdllGetModul(){
	UINT_PTR baseAddress = GetModuleHandleA("ntdll.dll");
	return baseAddress;
    }

int main() {
    UINT_PTR ntdllGetModul = NtdllGetModul();

    printf("Base address from ntdll via GetModuleHandleA: %p\n", (void*)NtdllGetModul());
    printf("Press any key to exit...");

    getchar(); // wait for keypress

    return 0;
}

Das Ergebnis unseres Experiments, wie in der folgenden Abbildung dargestellt, zeigt, dass wir auf dem Endpoint mit dem zu untersuchenden EDR durch die Verwendung der Windows API GetModuleHandleA tatsächlich im Speicherbereich der echten ntdll.dll landen und nicht in dem der fake ntdll.dll. Dies deutet darauf hin, dass der Zugriff auf die Basisadresse eines Moduls oder einer DLL mittels GetModuleHandleA eine Möglichkeit bietet, um die vom EDR implementierte Fake-DLL und damit das Page Guard Hooking zu umgehen. 

Trotzdem ist diese Strategie nicht wirklich empfehlenswert, da z.B. die Windows APIs GetModuleHandleA, GetProcAddress, LoadLibrary etc. durch EDRs mittels Inline API-Hooking oftmals überwacht werden. Daher sollten diese APIs z.B. in einem Shellcode Loader oder im Shellcode selbst grundsätzlich vermieden werden.

PEB Iteration and String Comparison 

Anbei möchte ich noch eine letzte Evasion Möglichkeit aufzeigen. Um die Ermittlung der Basisadresse der richtigen ntdll.dll zu optimieren und sicherzustellen, dass man nicht versehentlich die Basisadresse der fake ntdll.dll erhält, kann der folgende C-Code verwendet werden. Dieser Ansatz verwendet eine Kombination aus Iteration und String-Vergleich innerhalb der InLoadOrderModuleList im PEB, um die korrekte ntdll.dll zu identifizieren. Konkret durchläuft der Code die Liste der geladenen Module, vergleicht die Modulnamen exakt mit "ntdll.dll" und extrahiert bei exakter Übereinstimmung die Basisadresse des richtigen Moduls. Diese Methode stellt eine präzise und recht zuverlässige Lösung dar, um die echte ntdll.dll von potenziell gefälschten Versionen zu unterscheiden und ihre Basisadresse korrekt zu ermitteln.

// Resources: 
// Hiding in Plain Sight: Unlinking Malicious DLLs from the PEB 

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

typedef struct _UNICODE_STRING {
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

typedef struct _PEB_LDR_DATA {
	BYTE       Reserved1[8];
	PVOID      Reserved2[3];
	LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;

typedef struct _LDR_DATA_TABLE_ENTRY {
	LIST_ENTRY InLoadOrderLinks;
	LIST_ENTRY InMemoryOrderLinks;
	LIST_ENTRY InInitializationOrderLinks;
	PVOID      DllBase;
	PVOID      EntryPoint;
	ULONG      SizeOfImage;
	UNICODE_STRING FullDllName;
	UNICODE_STRING BaseDllName;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

typedef struct _PEB {
	BYTE                          Reserved1[2];
	BYTE                          BeingDebugged;
	BYTE                          Reserved2[1];
	PVOID                         Reserved3[2];
	PPEB_LDR_DATA                 Ldr;
} PEB, * PPEB;

typedef struct _MY_LDR_DATA_TABLE_ENTRY
{
	LIST_ENTRY      InLoadOrderLinks;
	LIST_ENTRY      InMemoryOrderLinks;
	LIST_ENTRY      InInitializationOrderLinks;
	PVOID           DllBase;
	PVOID           EntryPoint;
	ULONG           SizeOfImage;
	UNICODE_STRING  FullDllName;
	UNICODE_STRING  ignored;
	ULONG           Flags;
	SHORT           LoadCount;
	SHORT           TlsIndex;
	LIST_ENTRY      HashTableEntry;
	ULONG           TimeDateStamp;
} MY_LDR_DATA_TABLE_ENTRY;


// Returns a pointer to the PEB by reading the FS or GS registry
PEB* get_peb() {
#ifdef _WIN64
    return (PEB*)__readgsqword(0x60);
#else
    return  (PEB*)__readfsdword(0x30);
#endif
}

// Get the base address of reall ntdll.dll by comparing the name of the DLL with "ntdll.dll" string and returning the base address of the DLL if the name contains "ntdll.dll"  
PVOID get_ntdll_base_via_name_comparison() {
    PEB* peb = get_peb(); // Get a pointer to the PEB
    LIST_ENTRY* current = &peb->Ldr->InMemoryOrderModuleList; // Get the first entry in the list of loaded modules
    do {
        current = current->Flink; // Move to the next entry
        MY_LDR_DATA_TABLE_ENTRY* entry = CONTAINING_RECORD(current, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
        char dllName[256]; // Buffer to store the name of the DLL
        // Assuming FullDllName is a UNICODE_STRING, conversion to char* may require more than snprintf, consider proper conversion
        snprintf(dllName, sizeof(dllName), "%wZ", entry->FullDllName);
        if (strstr(dllName, "ntdll.dll")) { // Check if dllName contains "ntdll.dll"
            return entry->DllBase; // Return the base address of ntdll.dll
        }
    } while (current != &peb->Ldr->InMemoryOrderModuleList); // Loop until we reach the first entry again

    return NULL;
}


int main() {
    // Get the base address of ntdll.dll by comparing the name of the DLL with "ntdll.dll" string
    PVOID ntdll_base = get_ntdll_base_via_name_comparison();
    if (ntdll_base == NULL) {
        printf("ntdll.dll not found\n");
    }
    else {
        printf("Base address from real ntdll.dll based on string or dll name comparison: %p\n", ntdll_base);
    }
    printf("Press any key to continue\n");
    (void)getchar();

    return 0;
}

Die folgende Abbildung zeigt, wie wir mit Hilfe des C-Codes die gefälschte ntdll.dll des EDRs umgehen und erfolgreich die Basisadresse der echten ntdll.dll ermitteln. 

Interpretation

Ich möchte meine Analyse des EDR-Systems mit einer kurzen Interpretation abschließen. Im direkten Vergleich mit anderen EDR-Lösungen zeichnet sich der beschriebene Erkennungsmechanismus, der auf Fake DLLs, Guard Pages und Vectored Exception Handling basiert, als eine eher unkonventionelle Methode aus, die eher im Bereich des Game Hackings zu finden ist. Dennoch hat sich diese Methode in der Praxis als sehr effektiv erwiesen. Aus eigener Erfahrung kann ich sagen, dass der Zeit- und Energieaufwand für eine erfolgreiche Evasion - definiert als eine Situation, in der Malware weder verhindert noch erkannt wird - im Vergleich zu anderen EDR-Systemen deutlich höher ist. Die Komplexität und der kohärente Aufbau dieses Ansatzes lassen die Methode als eine Art "Falle" erscheinen. Denn nach meinem aktuellen Verständnis wird die Guard Page in der fake ntdll.dll nur dann ausgelöst, wenn während der Prozessinitialisierung mittels PEB-Walk versucht wird, über den DLL-Base-Offset 0x30 auf die ntdll.dll zuzugreifen, man aber tatsächlich im Speicher der fake ntdll.dll landet. Verwendet eine Applikation oder Malware jedoch APIs wie GetModuleHandle oder LoadLibrary, um ein Handle auf die ntdll.dll zu erhalten, greift der Mechanismus aus fake ntdll.dll, Guard Page und VEH nicht. Aus der Sicht von Malware hat man es in solchen Fällen jedoch mit einer relativ hohen Wahrscheinlichkeit mit Inline API Hooking durch den EDR zu tun, da GetModuleHandleA und LoadLibrary häufig mit Inline Hooks versehen sind.

Während der Prozess der Modifikation des Process Environment Blocks (PEB), die Verwendung von Fake DLLs in Kombination mit Guard Pages (Page Guard Hooking) und Vectored Exception Handling relativ gut verstanden ist, bleibt die Frage offen, was genau nach der Übergabe an den VEH des EDR passiert. Eine mögliche, aber nicht bestätigte Hypothese ist, dass der betroffene Thread oder Prozess entweder direkt beendet wird oder dass eine Übergabe an die Hooking DLL des EDRs erfolgt, die entscheidet, ob der Prozess beendet wird oder nicht. Es ist jedoch wichtig zu betonen, dass dies zum jetzigen Zeitpunkt nur Spekulationen sind und nicht als endgültige Fakten betrachtet werden können.

Es wäre ebenfalls aufschlussreich, die Auswirkungen des beschriebenen EDR-Mechanismus auf die Systemleistung zu untersuchen, insbesondere im Vergleich mit anderen EDR-Systemen, die diesen speziellen Ansatz nicht verfolgen. Angesichts des komplexen Verfahrens aus PEB-Manipulation, dem Einsatz von Fake DLLs, Guard Pages und Vectored Exception Handling könnte man vermuten, dass dieser Ansatz systemintensiver ist. Ähnlich wie beim Inline API-Hooking könnten sich die EDRs auf spezifische APIs beschränken, um die Systembelastung zu minimieren. Diese Annahme wird durch die Beobachtung gestützt, dass in IDA Vergleichsoperationen für etwa 25 native APIs identifiziert wurden, was darauf hindeuten könnte, dass der EDR sich auf ausgewählte, kritische APIs konzentriert, um die Effizienz zu optimieren und die Systemleistung nicht übermäßig zu beeinträchtigen.

Ich hoffe, dass ich Ihnen mit diesem Artikel einen kleinen Einblick in einen doch recht unkonventionellen EDR Detection Mechanismus geben konnte und bedanke mich für das Lesen. Bis zum nächsten Artikel.

Happy Hacking!

Daniel Feichter @VirtualAllocEx

Zuletzt aktualisiert 06.05.24 08:10:19 06.05.24
Daniel Feichter