
Thread Stack Spoofing - PoC per una tecnica avanzata di evasione in-memory che permette di nascondere meglio l'allocazione di memoria dello shellcode iniettato da scanner e analisti.
Una implementazione PoC per una tecnica avanzata di evasione in-memory che falsifica lo Stack dei Thread. Questa tecnica permette di bypassare le regole di esame della memoria basate sui thread e nascondere meglio gli shellcode mentre sono in memoria di processo.
Questa è un'implementazione d'esempio per la tecnica Thread Stack Spoofing che mira a eludere Analisti Malware, AV e EDR che cercano riferimenti ai frame dello shellcode nella call stack di un thread esaminato. L'idea è nascondere i riferimenti allo shellcode sulla call stack del thread, mascherando così le allocazioni che contengono il codice del malware.
Questa implementazione, insieme al mio ShellcodeFluctuation, fornisce alla comunità Offensive Security implementazioni di esempio per recuperare il terreno rispetto all'offerta dei prodotti C2 commerciali, in modo da non essere da meno nei nostri tooling Red Team. 💪
L'implementazione attuale differisce pesantemente da quella originariamente pubblicata.
Questo perché ho realizzato che esiste un approccio molto più semplice per terminare l'elaborazione della call stack del thread e nascondere i frame relativi allo shellcode, semplicemente scrivendo 0 all'indirizzo di ritorno del primo frame che controlliamo:
void WINAPI MySleep(DWORD _dwMilliseconds)
{
[...]
auto overwrite = (PULONG_PTR)_AddressOfReturnAddress();
const auto origReturnAddress = *overwrite;
*overwrite = 0;
[...]
*overwrite = origReturnAddress;
}
La precedente implementazione, che utilizzava StackWalk64, è accessibile in questo commit c250724.
Questa implementazione è molto più stabile e funziona bene sia in Debug che in Release su due architetture - x64 e x86.
Ecco come potrebbe apparire una call stack quando NON è falsificata:

Questa, invece, quando la falsificazione dello stack del thread è abilitata:

Sopra possiamo vedere che l'ultimo frame sulla nostra call stack è la nostra callback MySleep.
Ci si potrebbe chiedere se ciò porti immediatamente a nuovi IOC? Le regole di caccia possono cercare thread con call stack che non si srotolano nei seguenti punti di ingresso attesi del thread, situati nelle librerie di sistema:
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
Tuttavia la call stack del thread falsificato potrebbe sembrare strana a prima vista; un breve esame del mio sistema ha mostrato che ci sono anche altri thread che non si srotolano fino ai punti di ingresso sopra:

Lo screenshot sopra mostra un thread di Total Commander x64 non modificato. Come possiamo vedere, la sua call stack assomiglia molto alla nostra in termini di frame iniziali della call stack.
Perché dovremmo preoccuparci di falsificare attentamente la nostra call stack quando ci sono processi che mostrano tratti che possiamo semplicemente imitare?
L'algoritmo approssimativo è il seguente:
dbghelp.dll, chiamare SymInitializekernel32!Sleep puntando alla nostra callback.VirtualAlloc + memcpy + CreateThread. Il thread dovrebbe partire dalla nostra funzione runShellcode per evitare che l'Indirizzo di Inizio del Thread punti in un luogo inaspettato e anomalo (come ntdll!RtlUserThreadStart+0x21).MySleep viene invocata.0, che dovrebbe effettivamente terminare la call stack.::SleepEx per permettere al Beacon di dormire mentre attende ulteriori comunicazioni.Gli indirizzi di ritorno delle funzioni sono sparsi in tutta l'area di memoria dello stack del thread, puntati dal registro RBP/EBP.
Per trovarli sullo stack, dobbiamo prima raccogliere i puntatori ai frame, poi dereferenziarli per sovrascriverli:

(l'immagine sopra è stata presa dal post di Eli Bendersky intitolato Stack frame layout on x86-64)
*(PULONG_PTR)(frameAddr + sizeof(void*)) = Fake_Return_Address;
L'implementazione iniziale di ThreadStackSpoofer faceva questo nelle funzioni walkCallStack e spoofCallStack, tuttavia l'implementazione attuale mostra che questi sforzi non sono necessari per mantenere una call stack furtiva.
Caso d'uso:
C:\> ThreadStackSpoofer.exe <shellcode> <spoof>
Dove:
<shellcode> è il percorso del file shellcode<spoof> quando è 1 o true abiliterà la falsificazione dello stack del thread, qualsiasi altra cosa la disabilita.Esempio di esecuzione che falsifica la call stack del thread di beacon:
PS D:\dev2\ThreadStackSpoofer> .\x64\Release\ThreadStackSpoofer.exe .\tests\beacon64.bin 1
[.] Lettura dei byte dello shellcode...
[.] Aggancio di kernel32!Sleep...
[.] Iniezione dello shellcode...
[+] Shellcode ora in esecuzione.
[>] Indirizzo di ritorno originale: 0x1926747bd51. Terminazione della call stack...
===> MySleep(5000)
[<] Ripristino dell'indirizzo di ritorno originale...
[>] Indirizzo di ritorno originale: 0x1926747bd51. Terminazione della call stack...
===> MySleep(5000)
[<] Ripristino dell'indirizzo di ritorno originale...
[>] Indirizzo di ritorno originale: 0x1926747bd51. Terminazione della call stack...
Guarda il codice e la sua implementazione, comprendi il concetto e reimplementalo all'interno dei tuoi Loader di Shellcode che usi per le tue attività Red Team. Questa è un'ulteriore tecnica di evasione avanzata in-memory che aumenta le possibilità del tuo Team di non essere scoperto da Antivirus, EDR e Analisti Malware che esaminano i tuoi impianti.
Mentre sviluppi il tuo loader di shellcode avanzato, potresti anche voler implementare:
BeaconEyeRW (da RX/RWX) e crittografarne il contenuto - usando la tecnica Shellcode Fluctuation - proprio prima di dormire (che potrebbe eludere scanner come Moneta o pe-sieve)Come mi è stato fatto notare, la tecnica qui non è ancora veramente all'altezza del suo nome di falsificatore dello stack. Poiché stiamo semplicemente sovrascrivendo gli indirizzi di ritorno sullo stack del thread, non stiamo falsificando le restanti aree dello stack stesso. Inoltre stiamo lasciando la nostra call stack non srotolabile, rendendola anomala poiché il sistema non sarà in grado di percorrere correttamente l'intera catena dei frame della call stack.
Tuttavia sono consapevole di queste limitazioni; al momento l'ho lasciata così perché mi interessava principalmente eludere scanner automatizzati che potrebbero iterare sui processi, enumerare i loro thread, percorrere gli stack di quei thread e individuare eventuali indirizzi di ritorno che puntano a memoria non immagine (come SEC_PRIVATE - quella allocata dinamicamente da VirtuaAlloc e simili). Un analista malware concentrato noterebbe immediatamente l'anomalia e considererebbe il thread piuttosto insolito, dando la caccia al nostro impianto. Ne sono più che sicuro. Tuttavia, non credo che gli scanner automatizzati odierni come AV/EDR abbiano euristiche implementate che percorrano effettivamente lo stack di ogni thread per verificare se è srotolabile ¯\_(ツ)_/¯.
Sicuramente questo progetto (e l'implementazione commerciale trovata nei framework C2) fornisce argomenti ai vendor di AV & EDR per considerare l'implementazione di euristiche appropriate che coprano questa tecnica di evasione così innovativa.
Per migliorare questa tecnica, si può puntare a un vero Thread Stack Spoofer inserendo frame di stack fittizi accuratamente costruiti, stabiliti in un processo di srotolamento inverso. Leggi di più su questa idea qui sotto.
Una conversazione di ore con namazso mi ha insegnato che, per puntare a un vero falsificatore dello stack del thread, dovremmo invertire il processo di srotolamento della call stack su x64. Innanzitutto, bisogna prendere atto del processo di srotolamento dello stack spiegato in (a) qui sotto. Quando il sistema attraversa la call stack del thread su architettura x64, non si basa semplicemente sugli indirizzi di ritorno sparsi nello stack del thread, ma piuttosto:
RUNTIME_FUNCTION, UNWIND_INFO e UNWIND_CODE. Queste strutture descrivono dove sono l'indirizzo di inizio della funzione, l'indirizzo di fine e dove sono tutte le sequenze di codice che modificano RBP o RSP.UNWIND_CODE che la funzione esaminata presenta per calcolare con precisione la posizione dell'indirizzo di ritorno di quel frame e il valore del puntatore allo stack.Per interferire con questo processo avremmo bisogno di invertirlo avendo la nostra forma invertita di RtlVirtualUnwind. Dovremmo iterare sulle funzioni definite in un modulo (diciamo kernel32), scansionare i codici UNWIND_CODE di ogni funzione ed emularli attentamente all'indietro (rispetto a RtlVirtualUnwind e precisamente RtlpUnwindPrologue) per trovare le posizioni sullo stack dove mettere i nostri indirizzi di ritorno fittizi.
namazso menziona la necessità di introdurre 3 frame di stack fittizi per cucire bene la call stack:
MySleep (avendo un UWOP - codice di operazione di srotolamento diverso). Lo facciamo cercando in tutte le funzioni di un modulo, esaminando i loro UWOP, calcolando quanto grande dovrebbe essere il frame fittizio. Questo frame deve avere UWOPS diversi da quelli del chiamante della nostra MySleep.RBP dallo stack - fondamentalmente tramite codice UWOP_PUSH_NONVOL.RSP da RBP tramite il codice UWOP_SET_FPREGL'RSP ripristinato deve essere impostato con l'RSP preso da dove il flusso di controllo è entrato nella nostra MySleep in modo che tutti i nostri frame diventino nascosti, come risultato dello srotolamento del terzo gadget lì.
Per iniziare il processo, si può iterare sul .pdata dell'eseguibile dereferenziando la voce della directory dei dati IMAGE_DIRECTORY_ENTRY_EXCEPTION.
Considera l'esempio qui sotto:
ULONG_PTR imageBase = (ULONG_PTR)GetModuleHandleA("kernel32");
PIMAGE_NT_HEADERS64 pNthdrs = PIMAGE_NT_HEADERS64(imageBase + PIMAGE_DOS_HEADER(imageBase)->e_lfanew);
auto excdir = pNthdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
if (excdir.Size == 0 || excdir.VirtualAddress == 0)
return;
auto begin = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase);
auto end = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase + excdir.Size);
UNWIND_HISTORY_TABLE mshist = { 0 };
DWORD64 imageBase2 = 0;
PRUNTIME_FUNCTION currFrame = RtlLookupFunctionEntry(
(DWORD64)caller,
&imageBase2,
&mshist
);
UNWIND_INFO *mySleep = (UNWIND_INFO*)(currFrame->UnwindData + imageBase);
UNWIND_CODE myFrameUwop = (UNWIND_CODE)(mySleep->UnwindCodes[0]);
log("1. MySleep RIP UWOP: ", myFrameUwop.UnwindOpcode);
for (PRUNTIME_FUNCTION it = begin; it < end; ++it)
{
UNWIND_INFO* unwindData = (UNWIND_INFO*)(it->UnwindData + imageBase);
UNWIND_CODE frameUwop = (UNWIND_CODE)(unwindData->UnwindCodes[0]);
if (frameUwop.UnwindOpcode != myFrameUwop.UnwindOpcode)
{
// Trovata funzione candidata per un gadget frame di desincronizzazione
}
}
Il processo è un po' contorto, ma si riduce a invertire il processo di srotolamento della call stack del thread sostituendo frame arbitrari dello stack con altri accuratamente selezionati, in un approccio simile a ROP.
Questo PoC non replica questo algoritmo, perché la mia comprensione attuale mi permette di accettare la call stack che termina su un frame basato su EXE e non voglio complicare eccessivamente né i miei loader di shellcode né questo PoC. Lascio l'esercizio di implementare questo e condividerlo pubblicamente al lettore interessato. O forse mi siederò e proverò a farlo io stesso, dato un po' più di tempo libero :)
Ulteriori informazioni:
Se hai intenzione di aggiungere questa funzionalità ai tuoi loader di shellcode / tooling, assicurati di EVITARE di sganciare kernel32.dll.
Un tentativo di sganciare kernel32 ripristinerà la funzionalità originale di Sleep, impedendo che la nostra callback venga chiamata.
Se la nostra callback non viene chiamata, il thread non sarà in grado di falsificare il proprio stack da solo.
Se è questo che desideri, potresti aver bisogno di eseguire un altro thread di watchdog, assicurandoti che lo stack del thread del Beacon venga falsificato ogni volta che dorme.
Se stai usando Cobalt Strike e un BOF unhook-bof di Raphael Mudge, assicurati di dare un'occhiata alla mia Pull Request che aggiunge un parametro opzionale al BOF per specificare le librerie che non devono essere sganciate.
In questo modo puoi mantenere i tuoi agganci in kernel32:
beacon> unhook kernel32
[*] Esecuzione unhook.
Salterò questi moduli: wmp.dll, kernel32.dll
[+] host chiamato a casa, inviati: 9475 byte
[+] ricevuto output:
ntdll.dll <.text>
Unhook completato.
Questo PoC è stato progettato per funzionare con gli shellcode di Beacon di Cobalt Strike. È noto che Beacon chiama kernel32!Sleep per attendere ulteriori istruzioni dal suo C2.
Questo loader sfrutta questo fatto agganciando Sleep per eseguire le sue operazioni di manutenzione.
Questa implementazione potrebbe non funzionare con altri shellcode sul mercato (come Meterpreter) se non usano Sleep per raffreddarsi.
Poiché questo è solo un Proof of Concept che mostra la tecnica, non ho intenzione di aggiungere supporto per nessun altro framework C2.
Quando comprendi il concetto, sarai sicuramente in grado di tradurlo nei requisiti del tuo shellcode e adattare la soluzione a tuo vantaggio.
Per favore, non aprire issue su Github relative a "questo codice non funziona con lo shellcode XYZ", verranno chiuse immediatamente.
Questo e altri progetti sono il risultato di notti insonni e molto duro lavoro. Se ti piace quello che faccio e apprezzi che restituisco sempre qualcosa alla comunità, Considera l'idea di offrirmi un caffè (o meglio una birra) solo per dire grazie! 💪
Mariusz Banach / mgeeky, 21
<mb [at] binary-offensive.com>
(https://github.com/mgeeky)