Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
LdrShuffle — Tecnica di esecuzione/iniezione di codice utilizzando la manipolazione della struttura del modulo DLL PEB | Kitploit
Strumenti/GitHubGitHub/rwxstoned/ldrshuffle
Analisi del CodiceExploitMovimento LateraleShellcodePost-ExploitPenetration TestingRed TeamingSviluppo PayloadBinary Exploitation
GitHubrwxstoned/ldrshuffle

LdrShuffle

Tecnica di esecuzione/iniezione di codice utilizzando la manipolazione della struttura del modulo DLL PEB

289441 anno faRevisionato da Kitploit

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi
Vedi Repository

LdrShuffle

Esecuzione furtiva del codice tramite modifica del EntryPoint dei moduli caricati in fase di runtime.

Riepilogo

I processi Windows hanno vari moduli caricati in fase di runtime. Ognuno di questi moduli ha una funzione DllMain() definita, che verrà invocata alla creazione/distruzione del processo o del thread (quattro scenari possibili).

Per chiamare correttamente queste funzioni durante la vita del processo, le funzioni del Loader di Windows (ntdll!Ldrp*) si riferiscono a un elenco di voci contenenti parametri chiave (incluso il campo EntryPoint) per ciascun modulo.

Sovrascrivendo questo EntryPoint per una DLL, ci assicuriamo che l'esecuzione del codice venga reindirizzata verso un punto da noi scelto.

Casi d'Uso

Questo può essere utilizzato sia come primitiva per l'esecuzione del codice, sia per il proxying delle API, ad esempio per eseguire determinate API con un callstack non sospetto, poiché verranno invocate da funzioni Windows legittime.

Può anche essere utilizzato per innescare l'esecuzione in un processo remoto, a patto che l'attaccante abbia la capacità di leggere e scrivere memoria su quel processo di destinazione. Analogamente alla Threadless Injection, ciò fornisce la capacità di eseguire codice in un processo senza invocare API classiche legate all'esecuzione (CreateRemoteThread, QueueUserAPC).

Sfide

Il caricamento/scaricamento dei moduli all'interno di un processo Windows è un argomento complesso che presenta molte sfide, potenziali instabilità, race conditions e crash. Un noto ostacolo relativo all'esecuzione di codice all'interno di una funzione DllMain(), ad esempio, risiede nel fatto che è attivo un Loader Lock e che stiamo eseguendo in un thread che non è stato completamente configurato, o che è in fase di terminazione.

Pertanto, ho cercato di documentare adeguatamente cosa è possibile e cosa no. Ad esempio, mentre la maggior parte delle chiamate API standard possono essere eseguite, l'esecuzione di un beacon completo richiede determinate condizioni per essere in un processo separato, al fine di evitare deadlock causati dalle funzioni utilizzate in wininet.dll o winhttp.dll.

Implementazione

Ripasso sul Caricamento delle DLL in Windows

Ogni processo mantiene una lista di strutture _LDR_DATA_TABLE_ENTRY in fase di runtime. Queste strutture contengono molti dettagli rilevanti per la DLL, come il suo EntryPoint (che sovrascriveremo), il nome, alcuni hash, timestamp, vari flag, ecc. Alcune di queste strutture sono documentate, altre no.

Possono essere visualizzate tramite questo comando WinDbg:

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 ''

L'indirizzo della struttura può essere ottenuto percorrendo una struttura doppiamente linkata referenziata nel PEB del processo in una struttura PEB_LDR_DATA.

dt nt!_PEB_LDR_DATA 0xb4b4c3c3

Notare il flag DontCallForThreads. Come suggerisce il nome, se questo flag è impostato, il Sistema Operativo NON chiamerà il DllMain() di quel modulo per eventi di thread (cioè DLL_THREAD_ATTACH o DLL_THREAD_DETACH).

Quando si crea una DLL, è necessario seguire il seguente modello per funzionare in sinergia con le funzioni del Loader del Sistema Operativo:

root@kitploit:~
BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // handle al modulo DLL
    DWORD fdwReason,     // motivo della chiamata
    LPVOID lpvReserved )  // riservato
{
    // Esegue azioni in base al motivo della chiamata.
    switch( fdwReason ) 
    { 
        case DLL_PROCESS_ATTACH:
         // Inizializza una volta per ogni nuovo processo.
         // Restituisci FALSE per far fallire il caricamento della DLL.
            break;

        case DLL_THREAD_ATTACH:
         // Esegue inizializzazione specifica del thread.
            break;

        case DLL_THREAD_DETACH:
         // Esegue pulizia specifica del thread.
            break;

        case DLL_PROCESS_DETACH:
        
            if (lpvReserved != nullptr)
            {
                break; // non eseguire la pulizia in caso di terminazione del processo
            }
            
         // Esegue eventuale pulizia necessaria.
            break;
    }
    return TRUE;  // DLL_PROCESS_ATTACH riuscita.
}

Dettagli Tecnici sull'Implementazione

Impostazione di una Chiamata API

Come descritto sopra, la tecnica sovrascrive temporaneamente il EntryPoint di una DLL per reindirizzare l'esecuzione. Poiché non abbiamo alcun controllo su altro oltre al reindirizzamento dell'esecuzione, è necessario predisporre alcuni accorgimenti per gestire ciò che vogliamo eseguire, con quali argomenti e come recuperare il valore di ritorno.

Ciò viene realizzato definendo una struttura DATA_T sull'heap, in modo tale che rimanga accessibile durante i vari passaggi.

Questa struttura è definita come segue:

root@kitploit:~
typedef struct _DATA_T {
    // Manipolazione delle strutture LDR
    ULONG_PTR   runner;             // punto di ingresso malevolo da eseguire
    ULONG_PTR   bakOriginalBase;    // backup di OriginalBase sovrascritto
    ULONG_PTR   bakEntryPoint;      // backup di EntryPoint sovrascritto
    HANDLE      event;              // evento che segnala che il Runner ha eseguito
    // chiamata di funzione
    ULONG_PTR   ret;                // valore di ritorno
    DWORD       createThread;       // esegue questa chiamata API in un nuovo thread (richiesto per wininet/winhttp)
    ULONG_PTR   function;           // API Windows da chiamare
    DWORD       dwArgs;             // numero di argomenti
    ULONG_PTR   args[MAX_ARGS];     // array di argomenti
} DATA_T, * PDATA_T;

Per impostare un'esecuzione API, è necessario preparare questi campi. Il valore ret è quello che raccoglierà il valore di ritorno dopo l'esecuzione. L'event viene utilizzato per la sincronizzazione, per segnalare che l'esecuzione è stata completata. Tutti gli altri campi sono input che definiscono quale API chiamare (function), con quali argomenti (dwArgs e args[]), l'indirizzo della funzione Runner() dove viene reindirizzata l'esecuzione, e i backup delle voci DLL originali sovrascritte (bakOriginalBase e bakEntryPoint).

Il campo createThread deve essere impostato a 1 per quelle funzioni API complesse che non funzioneranno bene in un contesto DllMain() (questo include molte librerie wininet e winhttp).

Ecco un esempio di impostazione di una chiamata a MessageBoxA() come visibile nel PoC:

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");

Modifica della struttura _LDR_DATA_TABLE_ENTRY

La funzione UpdateLdr() è responsabile di apportare la modifica corretta nel _LDR_DATA_TABLE_ENTRY del modulo di destinazione.

RestoreLdr() ripristinerà tali modifiche in una fase successiva (richiamata da Runner()).

Queste funzioni essenzialmente localizzano il PEB e percorrono le strutture del modulo per identificare la DLL corretta e i suoi campi. Nei file header sto riutilizzando le definizioni utilizzate da Batsec nel suo DarkLoadLibrary e incoraggio i lettori a dare un'occhiata a questo progetto e al relativo blogpost di MDSec per beneficiare dell'ottimo lavoro che ha svolto sugli internals del caricamento dei moduli in Windows.

Nota: questo PoC carica una DLL sacrificiale (SACRIFICIAL_DLL_NAME) e apporta queste modifiche su tale DLL. Tuttavia, è perfettamente fattibile modificare una DLL già caricata. Questo è infatti l'approccio adottato per l'iniezione cross-process. Per motivi di stabilità, consiglierei di evitare di toccare DLL importanti come ntdll o kernel32, che tendono anche ad essere più esaminate dalle soluzioni di sicurezza.

Esecuzione

Alla creazione o distruzione di un thread, l'esecuzione viene reindirizzata a Runner(), che funge da falso DllMain() per il modulo. Questa funzione quindi:

  • individua sull'heap dove si trova la struttura dati utilizzata per eseguire chiamate/ottenere il valore di ritorno (struttura PDATA_T)
  • ripristina il PEB RestoreLdr() allo stato originale
  • esegue la normale chiamata DllMain() (essenzialmente fungendo da proxy per la normale chiamata DLL).

A questo punto, l'esecuzione "normale" del Sistema Operativo è stata completata. Successivamente continua con i nostri payload:

  • esegue la nostra chiamata API malevola, secondo i valori e gli argomenti memorizzati nella struttura DATA_T. Se questa API è stata contrassegnata per essere eseguita in un nuovo thread (createThread = 1), questa chiamata verrà eseguita in un nuovo thread.
  • infine, segnala un evento (pDataT->event) in modo che il nostro codice principale sappia che la chiamata è stata eseguita.

Quando Windows finisce per invocare il nostro falso EntryPoint (che è l'indirizzo della funzione Runner()), il callstack appare come segue:

Callstack su MessageBoxA()

Esempio di API Proxying

Il PoC fornito contiene un esempio che invoca MessageBoxA().

Contiene anche una dimostrazione di download HTTP utilizzando wininet. Definisci la variabile HTTP per abilitare quel codice.

Callstack su MessageBoxA()

Esempio di Iniezione Cross-Process

I principi sopra descritti si riducono alla lettura e scrittura nello spazio di memoria del processo, per causare l'esecuzione di codice in un punto arbitrario nel futuro.

Con alcuni piccoli aggiustamenti, queste operazioni di lettura e scrittura possono essere applicate a un processo remoto per sovrascrivere il EntryPoint di una delle sue DLL.

Un prerequisito è la capacità di leggere e scrivere nello spazio di memoria del processo, cioè:

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

Un progetto aggiuntivo è presente nel PoC, chiamato LdrInject, che dimostra come eseguire questi passaggi. In sintesi, fa quanto segue:

  • in ReadPEB(), percorre la lista _LDR_DATA_TABLE_ENTRY nel processo di destinazione per identificare una DLL adatta da sovrascrivere. Nota che questa DLL deve avere DontCallForThreads == 0 perché vogliamo che Windows invochi il EntryPoint di quella DLL alla creazione del thread. Inoltre, non stiamo scegliendo le prime DLL nell'elenco poiché tendono ad essere più esaminate dai prodotti di sicurezza (ntdll.dll, kernel32.dll...).

  • i dettagli per quella DLL vengono memorizzati in una struttura dati PEBINJ_DATA.

  • shellcode (in questo caso un beacon) viene scritto nello spazio di processo remoto con InjectShellcodeToRemoteProcess()

  • due chiamate WriteProcessMemory() sovrascrivono il EntryPoint della DLL e lo salvano in OriginalBase in modo che possa essere ripristinato successivamente.

A questo punto, il prossimo evento DLL_THREAD_ATTACH o DLL_THREAD_DETACH comporterà l'esecuzione dello shellcode. Questo presenta alcune limitazioni e avvertenze nel contesto dell'esecuzione di un beacon, descritte nella sezione successiva.

Beacon per Cobalt Strike

Questa tecnica comporta l'esecuzione di uno shellcode in una situazione molto specifica. Il Loader Lock è attivo (poiché il Sistema Operativo crede di essere nel processo di caricamento/scaricamento di una DLL); un thread è in fase di creazione o distruzione; e in generale, c'è potenziale per problemi di sincronizzazione dei thread, deadlock, ecc.

Durante i test, sono state osservate due sfide:

  • l'esecuzione di un tipico beacon di Cobalt Strike causerebbe un deadlock quando si utilizzano API in wininet.dll o winhttp.dll.

  • l'esecuzione durante la distruzione del thread causa problemi di stabilità poiché stiamo eseguendo in un thread che è in fase di distruzione.

Per aumentare la stabilità, dobbiamo:

  • assicurarci che il beacon venga eseguito in un nuovo thread. Pertanto, l'UDRL creerà un nuovo thread (CreateThread) prima di invocare il solito punto di ingresso della DLL riflessiva di Cobalt Strike.

  • eseguire solo in un thread in fase di creazione e non in uno morente. Per fare ciò, ci assicuriamo che quando EntryPoint viene chiamato dal Sistema Operativo, il motivo invocato sia fdwReason == DLL_THREAD_ATTACH:

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

invece del solito

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

root@kitploit:~
    ULONG_PTR __cdecl ReflectiveLoader(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
        // esegui solo per un evento di creazione del thread
        
        if (fdwReason != DLL_THREAD_ATTACH) {
            return TRUE;
        }

        ...
    }

Questi due passaggi extra sono stati incorporati in una demo per un UDRL.

Test

Elenco delle API Testate per LdrShuffle

VirtualAlloc

VirtualProtect

CreateThread

Sleep

MessageBoxA

InternetOpenW (richiede l'esecuzione con createThread = 1)

InternetOpenUrlA (richiede l'esecuzione con createThread = 1)

TODO

  • Continuare a testare più API per LdrShuffle
Scarica lo strumento