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 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
.
Eine zentrale Frage bleibt offen: Warum erhalten wir bei Verwendung des Offsets 0xF8
für OriginalBase
die Speicheradresse der legitimen ntdll.dll
und bei Verwendung des Offsets 0x30
für DllBase
die Speicheradresse der gefälschten ntdll.dll
?
Der Hintergrund dafür ist, dass der EDR die Speicheradresse bzw. den Pointer von DllBase
mit der Adresse überschreibt, die auf den Speicherbereich der gefälschten ntdll.dll
zeigt. Dies kann in einer virtuellen Maschine mit installiertem EDR überprüft werden. Betrachtet man die Struktur der vom EDR manipulierten ntdll.dll
mit WinDbg, so erkennt man, dass der DllBase-Pointer durch die Speicheradresse ersetzt wird, die auf die gefälschte ntdll.dll
des EDR zeigt. Die Speicheradresse für OriginalBase hingegen zeigt weiterhin auf die korrekte, legitime ntdll.dll
. Die folgende Abbildung verdeutlicht diese Aussage.
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