
Tecnica di esecuzione/iniezione di codice utilizzando la manipolazione della struttura del modulo DLL PEB
Esecuzione furtiva del codice tramite modifica del EntryPoint dei moduli caricati in fase di runtime.
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.
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).
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.
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
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:
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.
}
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:
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: