
Un progetto PoC per incorporare shellcode nella Hint/Name Table
HintInject è un embedder e loader di shellcode che ho sviluppato mentre giocavo con le strutture del file PE e della tabella della directory di import. Prende un file shellcode grezzo e inserisce lo shellcode in blocchi nelle voci della Hint/Name Table che sono raggiungibili tramite la Import Lookup Table di una finta voce DLL importata sul loader eseguibile. Il loader poi unisce questi blocchi per eseguire lo shellcode. Non so se sia una tecnica nuova o no, ma volevo condividerla per divertimento.
Prima di spiegare come funziona HintInject, voglio menzionare brevemente il processo di caricamento delle DLL facendo riferimento a un post su Stack Overflow.
Per ottenere le informazioni sulle DLL richieste e funzioni/importazioni, si dovrebbe guardare prima alla Import Directory Table. La Import Directory Table è una tabella di voci, una voce per ogni DLL importata. Queste voci contengono un puntatore al nome della DLL importata, un puntatore alla Import Lookup Table, un puntatore alla Import Address Table e altri campi per diverse informazioni.
Semplicemente, la Import Lookup Table porta alle informazioni sulle importazioni, e la Import Address Table porta agli indirizzi delle importazioni. Tuttavia, quando l'eseguibile è sul disco, o appena prima del processo di caricamento delle DLL, la Import Address Table è identica a quella della Import Lookup Table. Il contenuto della Import Address Table viene sovrascritto con l'indirizzo delle importazioni durante il processo di caricamento delle DLL.
Senza entrare nei dettagli, possiamo dare un'occhiata al diagramma qui sotto per vedere la relazione tra queste tre tabelle.
Se entriamo un po' più nel dettaglio, la Import Lookup Table non contiene direttamente i nomi delle importazioni. Per le funzioni che vengono importate per nome, contiene RVA delle voci della Hint/Name Table. Queste voci memorizzano i nomi delle funzioni come stringhe ASCII terminate da null. Pertanto, la definizione della struct di una voce della Hint/Name Table è la seguente:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Il campo Hint qui è in realtà un indice nella tabella dei puntatori ai nomi di esportazione della DLL. Viene utilizzato per accelerare la ricerca della posizione di quella importazione. Pertanto, possiamo riassumere la relazione tra la Import Lookup Table e la Hint/Name table con il diagramma qui sotto.
Per concludere, per trovare una DLL e le sue importazioni, si dovrebbero seguire questi passaggi:
Recentemente, mentre esaminavo la tecnica ImportDLLInjection condivisa da x86matthew, ho visto come aggiungere una voce falsa alla Directory di Importazione delle DLL caricate in memoria. Successivamente, ho voluto sviluppare un piccolo progetto che aggiunge una voce falsa direttamente alla tabella delle importazioni di un binario su disco, sia per divertimento che per rinfrescare le mie conoscenze. Mentre revisionavo il processo di caricamento delle DLL per sviluppare questo progetto, il campo Hint nelle voci della Hint/Name Table utilizzate in questo processo ha attirato la mia attenzione.
Secondo il documento MSDN, ho appreso che questo campo è utilizzato dal Loader di Windows per trovare l'indirizzo di quella importazione, che viene importata per nome, direttamente dalla tabella dei nomi di esportazione della DLL in cui si trova. Tuttavia, nello stesso documento, è stato affermato che se la funzione non può essere trovata utilizzando questo campo, quella funzione verrà cercata tramite un'operazione di ricerca binaria nella tabella dei nomi di esportazione della DLL. Sulla base di questa frase, ho pensato che inserire un valore errato per questo campo non avrebbe interrotto il processo di caricamento delle DLL.
Come ho menzionato sopra, c'è una voce nella Hint/Name table corrispondente a ogni funzione da importare. In altre parole, abbiamo 2 byte da utilizzare per ogni importazione. Combinando più importazioni, si possono ottenere abbastanza campi Hint per memorizzare shellcode dannosi. Di conseguenza, si può utilizzare un binario loader per incorporare lo shellcode nelle voci Hint/Nome della voce DLL di importazione fittizia. Quel binario loader può raggiungere queste voci per unire lo shellcode da eseguire durante l'esecuzione.
HintInject può essere utilizzato per creare un tale loader che contiene lo shellcode nella sua Hint/Name table. Prima crea una nuova sezione chiamata .rrdata e copia la directory di import corrente in questa sezione. Dopodiché, aggiunge una nuova voce fittizia le cui importazioni saranno utilizzate per contenere lo shellcode di input. I byte rimanenti della sezione sono utilizzati per memorizzare la Import Lookup Table, la Import Address Table, il nome della DLL e la Hint/Name table della nuova voce fittizia. Come ultimo passo, HintInject utilizza i campi Hint delle importazioni per inserire blocchi dello shellcode di input.
Esempio di importazioni di un binario loader:
HintInject.exe con il tuo shellcode in formato grezzo e un percorso di output per creare un loader di shellcode: HintInject.exe <File Shellcode> <Nome Output>Loader.exe <PID>InjectShellcode nel file HintInjectLoader/Main.cpp.Testato su Windows 10 19044 con Visual Studio 2019:
dllNames nel file DllNamesForFakeImports.h.dllNames nel file DllNamesForFakeImports.h. Per impostazione predefinita, HintInject sceglierà le funzioni dalle DLL in base all'ordine dell'array. In altre parole, lo strumento seleziona prima le funzioni esportate da user32.dll, se la dimensione dello shellcode è maggiore di 2 * il numero di esportazioni di user32.dll, inizia anche a utilizzare le esportazioni di advapi32.dll e altre.static LPCSTR dllNames[] = {"user32.dll","advapi32.dll","gdi32.dll","wininet.dll","comctl32.dll","shell32.dll","wsock32.dll","oleaut32.dll","ws2_32.dll","urlmon.dll"};
Solo per test di sicurezza autorizzati. L'uso improprio di questo strumento contro sistemi senza esplicita autorizzazione è illegale.