
PoC basato su Rust che utilizza i fiber di Windows per eseguire codice in memoria in modo stealth, nascondendo gli stack del payload dall'EDR passando tra fiber di controllo e fiber del payload senza callback del kernel.
Una fibra è un'unità di esecuzione che deve essere pianificata manualmente dall'applicazione invece di affidarsi al meccanismo di pianificazione basato su priorità integrato in Windows. Le fibre sono spesso chiamate thread leggeri. Per informazioni più dettagliate su cosa sono e come funzionano le fibre, consulta la documentazione ufficiale.
Le fibre permettono di avere più flussi di esecuzione in un singolo thread, ciascuno con il proprio stato dei registri e stack. D'altro canto, le fibre sono invisibili al kernel, il che le rende un metodo più furtivo (ed economico) per eseguire codice in memoria rispetto alla creazione di nuovi thread.
Un thread può creare più fibre e passare da una all'altra a piacere chiamando la funzione SwitchToFiber. Prima di ciò, il thread corrente stesso deve diventare una fibra chiamando ConvertThreadToFiber, poiché solo una fibra può creare altre fibre. Infine, per creare una fibra che, una volta pianificata, esegua codice in memoria (ad esempio, dopo aver caricato riflessivamente un PE o dello shellcode), è sufficiente effettuare una chiamata a CreateFiber.
La funzione SwitchToFiber è la parte più importante di questo processo e dove avviene tutta la magia. Questa funzione permette di pianificare una fibra o un'altra, il tutto avviene nello spazio utente. Secondo la documentazione ufficiale, "la funzione SwitchToFiber salva le informazioni sullo stato della fibra corrente e ripristina lo stato della fibra specificata". Ciò significa che quando questa funzione viene chiamata, i valori dei registri e lo stack vengono commutati dallo stato della fibra corrente allo stato della fibra di destinazione, permettendo di "nascondere" lo stack della fibra corrente una volta completato il processo. Questo permette anche di continuare l'esecuzione della fibra di destinazione dal punto in cui era stata interrotta (allo stesso modo in cui avviene quando lo scheduler passa da un thread all'altro secondo la propria logica di priorità).
Ed è esattamente ciò che fa questo semplice PoC:
run() esportata dalla dll mappata manualmente. Questa fibra sarà d'ora in poi nota come fibra del payload.Questo processo si ripete indefinitamente.
L'uso delle fibre può essere vantaggioso per alcuni tipi di payload (come un beacon C2) per alcuni di questi motivi:
JMP o CALL dal loader che puntano a regioni di memoria non mappate.Poiché utilizziamo il plugin LITCRYPT per offuscare i letterali stringa, è necessario impostare la variabile d'ambiente LITCRYPT_ENCRYPT_KEY prima di compilare il codice:
C:\Users\User\Desktop\Fiber> set LITCRYPT_ENCRYPT_KEY="tua_chiave_supersecreta"
Successivamente, compila sia il payload che il loader ed esegui quest'ultimo:
C:\Users\User\Desktop\Fiber\payload> cargo build --release
C:\Users\User\Desktop\Fiber\loader> cargo build --release
C:\Users\User\Desktop\Fiber\loader\target\release> loader.exe
Non c'è molto mistero nell'esecuzione di questo PoC. Tutto ciò che bisogna fare è eseguire il loader e utilizzare uno strumento come ProcessHacker per ispezionare lo stack del thread. Poiché il payload torna alla fibra di controllo prima di dormire, lo stack della fibra del payload rimane nascosto per la maggior parte del tempo. Nell'output si vedrà come le due fibre vengono pianificate consecutivamente seguendo la logica già commentata.
Il codice è commentato per mostrare come utilizzare, creare e pianificare le fibre. Noterai che sia il loader che il payload forniti come esempio sono "bloccati" in un ciclo infinito, permettendo di passare indefinitamente da una fibra all'altra e continuare l'esecuzione.
Se si desidera testare un payload diverso, basta modificare il percorso situato alla riga 32 del file src::main.rs del loader. In tal caso, la nuova dll deve esportare una funzione run(PVOID) che riceverà come parametro di input l'indirizzo della fibra di controllo. Questa funzione deve tornare alla fibra di controllo per chiamare la funzione Sleep, sebbene sia possibile modificare questo comportamento a piacere per adattarlo alle proprie esigenze.
Un altro modo per testare questo strumento con un payload casuale è eseguire l'IAT hooking per reindirizzare qualsiasi chiamata alla funzione Sleep (o a qualsiasi altra funzione importata) effettuata dal payload a una funzione situata nel loader, permettendo di tornare alla fibra di controllo quando si verifica questa chiamata. A te la scelta.
Negli screenshot seguenti possiamo vedere come lo stack del thread corrente si sposta da una regione di memoria privata a un'altra mentre si commutano le fibre:
