Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
LdrShuffle — Codeausführungs-/Injektionstechnik durch Manipulation der DLL-PEB-Modulstruktur | Kitploit
Tools/GitHubGitHub/rwxstoned/ldrshuffle
Code-AnalyseExploitationLaterale BewegungShellcodePost-ExploitationPenetrationstestsRed TeamingPayload-EntwicklungBinary-Exploitation
GitHubrwxstoned/ldrshuffle

LdrShuffle

Codeausführungs-/Injektionstechnik durch Manipulation der DLL-PEB-Modulstruktur

28944vor 1 JahrVon Kitploit geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen
Repository anzeigen

LdrShuffle

Verdeckte Codeausführung durch Modifikation des EntryPoint geladener Module zur Laufzeit.

Zusammenfassung

Windows-Prozesse haben zur Laufzeit verschiedene Module geladen. Jedes dieser Module definiert eine DllMain()-Funktion, die bei der Erstellung bzw. Zerstörung von Prozessen oder Threads aufgerufen wird (vier mögliche Szenarien).

Um diese Funktionen während der Lebensdauer des Prozesses ordnungsgemäß aufzurufen, beziehen sich die Windows-Loader-Funktionen (ntdll!Ldrp*) auf eine Liste von Einträgen, die für jedes Modul wichtige Parameter (einschließlich des EntryPoint-Felds) enthalten.

Indem wir diesen EntryPoint einer DLL überschreiben, stellen wir sicher, dass die Codeausführung an eine von uns gewählte Stelle umgeleitet wird.

Anwendungsfälle

Dies kann sowohl als Codeausführungs-Primitive als auch für API-Proxying verwendet werden, also um bestimmte APIs mit einem unverdächtigen Callstack auszuführen, da sie von legitimen Windows-Funktionen aufgerufen werden.

Dies kann auch verwendet werden, um die Ausführung in einem entfernten Prozess auszulösen, sofern der Angreifer in der Lage ist, Speicher in diesem Zielprozess zu lesen und zu schreiben. Ähnlich wie bei der Threadless Injection ermöglicht dies die Ausführung von Code in einem Prozess, ohne klassische APIs zur Ausführung aufzurufen (CreateRemoteThread, QueueUserAPC).

Herausforderungen

Das Laden/Entladen von Modulen innerhalb eines Windows-Prozesses ist ein komplexes Thema, das viele Herausforderungen mit sich bringt: potenzielle Instabilität, Race Conditions und Abstürze. Eine bekannte Hürde bei der Ausführung von Code innerhalb einer DllMain()-Funktion liegt beispielsweise darin, dass eine Loader Lock aktiv ist und wir in einem Thread laufen, der nicht vollständig eingerichtet wurde oder sich gerade im Prozess der Beendigung befindet.

Deshalb habe ich versucht, ordentlich zu dokumentieren, was möglich ist und was nicht. Während beispielsweise die meisten üblichen API-Aufrufe ausgeführt werden können, bringt der Betrieb eines vollwertigen Beacons bestimmte Anforderungen mit sich, die einen separaten Prozess erfordern, um Deadlocks durch die in wininet.dll oder winhttp.dll verwendeten Funktionen zu vermeiden.

Implementierung

Grundlagen des DLL-Ladens in Windows

Jeder Prozess verwaltet zur Laufzeit eine Liste von _LDR_DATA_TABLE_ENTRY-Strukturen. Diese Strukturen enthalten viele Details, die für die DLL relevant sind, wie etwa ihren EntryPoint (den wir überschreiben werden), ihren Namen, bestimmte Hashes, Zeitstempel, verschiedene Flags usw. Einige dieser Strukturen sind dokumentiert, andere nicht.

Diese können mit dem folgenden WinDbg-Befehl visualisiert werden:

dt nt!_LDR_DATA_TABLE_ENTRY 0xdeadbeef

root@kitploit:~
0:006> dt nt!_LDR_DATA_TABLE_ENTRY 0x18d1c4838c0
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485de0 - 0x0000018d`1c4832b0 ]
   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485df0 - 0x0000018d`1c4832c0 ]
   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x0000018d`1c4832d0 - 0x0000018d`1c482c80 ]
   +0x030 DllBase          : 0x00007ffe`e87e0000 Void
   +0x038 EntryPoint       : 0x00007ffe`e8838d00 Void
   +0x040 SizeOfImage      : 0x2fe000
   +0x048 FullDllName      : _UNICODE_STRING "C:\WINDOWS\System32\KERNELBASE.dll"
   +0x058 BaseDllName      : _UNICODE_STRING "KERNELBASE.dll"
   +0x068 FlagGroup        : [4]  "???"
   +0x068 Flags            : 0x8a2cc
   +0x068 PackagedBinary   : 0y0
   +0x068 MarkedForRemoval : 0y0
   +0x068 ImageDll         : 0y1
(...)
   +0x0e0 MappingInfoIndexNode : _RTL_BALANCED_NODE
   +0x0f8 OriginalBase     : 0x00007ffe`e87e0000
   +0x100 LoadTime         : _LARGE_INTEGER 0x01db5f86`2fa735fc
   +0x108 BaseNameHashValue : 0x235bec4
   +0x10c LoadReason       : 0 ( LoadReasonStaticDependency )
   +0x110 ImplicitPathOptions : 0x4000
   +0x114 ReferenceCount   : 1
   +0x118 DependentLoadFlags : 0x800
   +0x11c SigningLevel     : 0 ''

Die Adresse der Struktur kann ermittelt werden, indem man eine doppelt verknüpfte Struktur durchläuft, auf die in der Prozess-PEB innerhalb einer PEB_LDR_DATA-Struktur verwiesen wird.

dt nt!_PEB_LDR_DATA 0xb4b4c3c3

Beachten Sie das DontCallForThreads-Flag. Wie der Name schon sagt, wird das Betriebssystem bei gesetztem Flag die DllMain() dieses Moduls für Thread-Ereignisse (d.h. DLL_THREAD_ATTACH oder DLL_THREAD_DETACH) NICHT aufrufen.

Beim Erstellen einer DLL muss die folgende Vorlage befolgt werden, um mit den OS-Loader-Funktionen zusammenzuarbeiten:

root@kitploit:~
BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // handle to DLL module
    DWORD fdwReason,     // reason for calling function
    LPVOID lpvReserved )  // reserved
{
    // Perform actions based on the reason for calling.
    switch( fdwReason ) 
    { 
        case DLL_PROCESS_ATTACH:
         // Initialize once for each new process.
         // Return FALSE to fail DLL load.
            break;

        case DLL_THREAD_ATTACH:
         // Do thread-specific initialization.
            break;

        case DLL_THREAD_DETACH:
         // Do thread-specific cleanup.
            break;

        case DLL_PROCESS_DETACH:
        
            if (lpvReserved != nullptr)
            {
                break; // do not do cleanup if process termination scenario
            }
            
         // Perform any necessary cleanup.
            break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}

Technische Details zur Implementierung

Einrichten eines API-Aufrufs

Wie oben beschrieben, überschreibt die Technik vorübergehend den EntryPoint einer DLL, um die Ausführung umzuleiten. Da wir keine Kontrolle über mehr als die Umleitung der Ausführung haben, müssen auf der Seite einige Vorkehrungen getroffen werden, um zu handhaben, was wir ausführen möchten, mit welchen Argumenten und wie wir den Rückgabewert zurückerhalten.

Dies geschieht durch das Definieren einer DATA_T-Struktur auf dem Heap, so dass sie während der verschiedenen Schritte zugänglich bleibt.

Diese Struktur ist wie folgt definiert:

root@kitploit:~
typedef struct _DATA_T {
    // LDR structures manipulation
    ULONG_PTR   runner;             // malicious entry point to execute
    ULONG_PTR   bakOriginalBase;    // backup of overwritten OriginalBase
    ULONG_PTR   bakEntryPoint;      // backup of overwritten EntryPoint
    HANDLE      event;              // event signalling that the Runner has executed
    // function call
    ULONG_PTR   ret;                // return value
    DWORD       createThread;       // run this API call in a new thread (required for wininet/winhttp)
    ULONG_PTR   function;           // Windows API to call
    DWORD       dwArgs;             // number of args
    ULONG_PTR   args[MAX_ARGS];     // array of args
} DATA_T, * PDATA_T;

Um eine API-Ausführung einzurichten, müssen diese Felder vorbereitet werden. Der ret-Wert ist derjenige, der nach der Ausführung den Rückgabewert aufnimmt. Das event wird zur Synchronisation verwendet, um zu signalisieren, dass die Ausführung abgeschlossen wurde. Alle anderen Felder sind Eingaben, die festlegen, welche API aufgerufen werden soll (function), mit welchen Argumenten (dwArgs und args[]), die Adresse der Runner()-Funktion, auf die die Ausführung umgeleitet wird, sowie Sicherungen der überschriebenen ursprünglichen DLL-Einträge (bakOriginalBase und bakEntryPoint).

Das Feld createThread muss für diejenigen komplexen API-Funktionen auf 1 gesetzt werden, die in einer DllMain()-Umgebung nicht gut laufen (dazu gehören viele wininet- und winhttp-Bibliotheken).

Hier ist ein Beispiel für die Einrichtung eines Aufrufs von MessageBoxA(), wie im PoC sichtbar:

root@kitploit:~
 pDataT->dwArgs = 4;
 pDataT->runner = (ULONG_PTR)Runner;
 pDataT->function = (ULONG_PTR)MessageBoxA;
 pDataT->args[0] = (ULONG_PTR)0;
 pDataT->args[1] = (ULONG_PTR)"Hello";
 pDataT->args[2] = (ULONG_PTR)"LDRSHUFFLE";
 pDataT->args[3] = (ULONG_PTR)MB_OKCANCEL;
 pDataT->event = CreateEventA(NULL, FALSE, FALSE, "ExecEvt");

Modifizieren der _LDR_DATA_TABLE_ENTRY

Die Funktion UpdateLdr() ist dafür verantwortlich, die richtige Modifikation in der _LDR_DATA_TABLE_ENTRY des Zielmoduls durchzuführen.

RestoreLdr() stellt diese Änderungen zu einem späteren Zeitpunkt wieder her (aufgerufen von Runner()).

Diese Funktionen lokalisieren im Wesentlichen die PEB und durchlaufen die Modulstrukturen, um die richtige DLL und ihre Felder zu identifizieren. In den Header-Dateien verwende ich Definitionen, die Batsec in seiner DarkLoadLibrary verwendet, und ich empfehle den Lesern, sich dieses Projekt und den zugehörigen MDSec-Blogpost anzusehen, um von der großartigen Arbeit zu profitieren, die er zu den Interna des Modulladens in Windows geleistet hat.

Hinweis: Dieser PoC lädt eine Opfer-DLL (SACRIFICIAL_DLL_NAME) und führt diese Änderungen an dieser DLL durch. Es ist jedoch ohne Weiteres möglich, eine bereits geladene DLL zu ändern. Dies ist sogar der Ansatz, der für die prozessübergreifende Injektion verwendet wird. Aus Stabilitätsgründen würde ich empfehlen, wichtige DLLs wie ntdll oder kernel32 nicht anzufassen, da diese auch eher von Sicherheitslösungen unter die Lupe genommen werden.

Ausführung

Bei der Erstellung oder Zerstörung von Threads wird die Ausführung an Runner() umgeleitet, das als gefälschtes DllMain() für das Modul fungiert. Diese Funktion wird dann:

  • die Datenstruktur auf dem Heap lokalisieren, die zum Ausführen von Aufrufen/Abrufen des Rückgabewerts verwendet wird (PDATA_T-Struktur)
  • den ursprünglichen Zustand der PEB mit RestoreLdr() wiederherstellen
  • den normalen DllMain()-Aufruf ausführen (im Wesentlichen wird der normale DLL-Aufruf durchgereicht).

An diesem Punkt wurde die „normale“ Betriebssystem-Ausführung durchgeführt. Danach fährt sie mit unseren Payloads fort:

  • unseren schädlichen API-Aufruf ausführen, gemäß den Werten und Argumenten, die in der DATA_T-Struktur gespeichert sind. Wenn diese API für die Ausführung in einem neuen Thread markiert wurde (createThread = 1), wird dieser Aufruf in einem neuen Thread ausgeführt.
  • schließlich ein Ereignis signalisieren (pDataT->event), damit unser Hauptcode weiß, dass der Aufruf ausgeführt wurde.

Wenn Windows schließlich unseren gefälschten EntryPoint aufruft (die Adresse der Runner()-Funktion), sieht der Callstack wie folgt aus:

Callstack bei MessageBoxA()

Beispiel für API-Proxying

Der bereitgestellte PoC enthält ein Beispiel, das MessageBoxA() aufruft.

Er enthält außerdem eine Demonstration eines HTTP-Downloads mit wininet. Definieren Sie die Variable HTTP, um diesen Code zu aktivieren.

Callstack bei MessageBoxA()

Beispiel für prozessübergreifende Injektion

Die oben beschriebenen Prinzipien laufen im Kern darauf hinaus, im Speicherbereich des Prozesses zu lesen und zu schreiben, um zu einem beliebigen zukünftigen Zeitpunkt Codeausführung auszulösen.

Mit ein paar Anpassungen können diese Lese- und Schreiboperationen auf einen entfernten Prozess angewendet werden, um den EntryPoint einer seiner DLLs zu überschreiben.

Eine Voraussetzung ist die Fähigkeit, im Speicherbereich des Prozesses lesen und schreiben zu können, d.h.:

OpenProcess(PROCESS_VM_READ, FALSE, dwPid) und OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, PID)

Im PoC ist ein zusätzliches Projekt namens LdrInject enthalten, das demonstriert, wie diese Schritte durchgeführt werden. Kurz gesagt führt es Folgendes aus:

  • In ReadPEB() durchläuft es die _LDR_DATA_TABLE_ENTRY-Liste im Zielprozess, um eine geeignete DLL zum Überschreiben zu identifizieren. Beachten Sie, dass diese DLL DontCallForThreads == 0 haben muss, da wir möchten, dass Windows den EntryPoint dieser DLL bei der Thread-Erstellung aufruft. Wir wählen auch nicht die ersten DLLs in der Liste, da diese eher von Sicherheitsprodukten unter die Lupe genommen werden (ntdll.dll, kernel32.dll...).
  • Die Details für diese DLL werden in einer PEBINJ_DATA-Datenstruktur gespeichert.
  • Shellcode (in diesem Fall ein Beacon) wird mit InjectShellcodeToRemoteProcess() in den Adressraum des entfernten Prozesses geschrieben.
  • Zwei WriteProcessMemory()-Aufrufe überschreiben den EntryPoint der DLL und sichern ihn in OriginalBase, sodass er später wiederhergestellt werden kann.

Ab diesem Zeitpunkt führt das nächste DLL_THREAD_ATTACH- oder DLL_THREAD_DETACH-Ereignis dazu, dass der Shellcode aufgerufen wird. Dies bringt bestimmte Einschränkungen und Vorbehalte im Zusammenhang mit dem Betrieb eines Beacons mit sich, die im nächsten Abschnitt beschrieben werden.

Cobalt Strike Beacon

Diese Technik führt zur Ausführung eines Shellcodes in einer sehr speziellen Situation. Die Loader Lock ist aktiv (da das Betriebssystem davon ausgeht, dass es sich gerade im Prozess des Ladens/Entladens einer DLL befindet); ein Thread wird entweder gerade erstellt oder zerstört; und generell besteht Potenzial für Thread-Synchronisationsprobleme, Deadlocks usw.

Während der Tests wurden zwei Herausforderungen beobachtet:

  • Das Ausführen eines typischen Cobalt Strike Beacons führte bei der Verwendung von APIs in wininet.dll oder winhttp.dll zu einem Deadlock.
  • Die Ausführung bei der Thread-Zerstörung verursacht Stabilitätsprobleme, da wir in einem Thread laufen, der sich im Prozess der Zerstörung befindet.

Um die Stabilität zu erhöhen, müssen wir:

  • sicherstellen, dass der Beacon in einem neuen Thread läuft. Daher wird das UDRL CreateThread aufrufen, bevor der übliche reflective DLL-Einstiegspunkt von Cobalt Strike aufgerufen wird.
  • nur in einem Thread laufen, der erstellt wird, und nicht in einem sterbenden. Dazu stellen wir sicher, dass der Aufrufgrund fdwReason == DLL_THREAD_ATTACH ist, wenn der EntryPoint vom Betriebssystem aufgerufen wird:

winApi.CreateThread(NULL, 4096, (LPTHREAD_START_ROUTINE)&runner, (LPVOID)&ct_data, 0, &dwThId);

anstelle des üblichen

((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, 4, NULL);

root@kitploit:~
    ULONG_PTR __cdecl ReflectiveLoader(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
        // only run for a Thread creation event
        
        if (fdwReason != DLL_THREAD_ATTACH) {
            return TRUE;
        }

        ...
    }

Diese beiden zusätzlichen Schritte wurden in einer Demo für ein UDRL eingebettet.

Tests

Liste der für LdrShuffle getesteten APIs

VirtualAlloc

VirtualProtect

CreateThread

Sleep

MessageBoxA

InternetOpenW (muss mit createThread = 1 ausgeführt werden)

InternetOpenUrlA (muss mit createThread = 1 ausgeführt werden)

TODO

  • Weitere APIs für LdrShuffle testen
Tool herunterladen