
PoC basé sur Rust utilisant des fibres Windows pour exécuter du code en mémoire de manière furtive, cachant les piles de payloads des EDR en basculant entre les fibres de contrôle et de payload sans rappels du noyau.
Une fibre est une unité d'exécution qui doit être planifiée manuellement par l'application plutôt que de se reposer sur le mécanisme de planification basé sur les priorités intégré à Windows. Les fibres sont souvent appelées threads légers. Pour des informations plus détaillées sur ce que sont les fibres et comment elles fonctionnent, consultez la documentation officielle. Les fibres permettent d'avoir plusieurs flux d'exécution dans un seul thread, chacun avec son propre état des registres et sa pile. D'autre part, les fibres sont invisibles pour le noyau, ce qui en fait une méthode plus furtive (et moins coûteuse) pour exécuter du code en mémoire que de créer de nouveaux threads.
Un thread peut créer plusieurs fibres et basculer entre elles à volonté en appelant la fonction SwitchToFiber. Avant cela, le thread lui-même doit être devenu une fibre en appelant ConvertThreadToFiber car seule une fibre peut créer d'autres fibres. Enfin, pour créer une fibre qui, lorsqu'elle est programmée, exécute du code en mémoire (par exemple, après avoir chargé de manière reflective un PE ou un shellcode), il suffit d'appeler CreateFiber.
La fonction SwitchToFiber est la partie la plus importante de ce processus et là où toute la magie opère. Cette fonction permet de planifier une fibre ou une autre, tout en espace utilisateur. Selon la documentation officielle, « la fonction SwitchToFiber enregistre les informations d'état de la fibre actuelle et restaure l'état de la fibre spécifiée ». Cela signifie que lorsque cette fonction est appelée, les valeurs des registres et la pile sont commutées de l'état de la fibre actuelle à l'état de la fibre cible, permettant de « cacher » la pile de la fibre actuelle une fois le processus terminé. Cela permet également de reprendre l'exécution de la fibre cible au point où l'exécution a été arrêtée (de la même manière que lorsqu'un planificateur alterne entre les threads selon sa propre logique de priorité).
Et c'est exactement ce que fait ce PoC simple :
run() exportée par la dll mappée manuellement. Cette fibre sera appelée la fibre de charge utile à partir de maintenant.Ce processus se répète indéfiniment.
L'utilisation des fibres peut être avantageuse pour certains types de charges utiles (comme une balise C2) pour certaines de ces raisons :
JMP ou CALL du chargeur pointant vers des régions mémoire non adossées.Puisque nous utilisons le plugin LITCRYPT pour obscurcir les littéraux de chaîne, il est nécessaire de définir la variable d'environnement LITCRYPT_ENCRYPT_KEY avant de compiler le code :
C:\Users\User\Desktop\Fiber> set LITCRYPT_ENCRYPT_KEY="yoursupersecretkey"
Ensuite, compilez simplement la charge utile et le chargeur, puis exécutez ce dernier :
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
Il n'y a pas beaucoup de mystère dans l'exécution de ce PoC. Tout ce qu'il faut faire, c'est exécuter le chargeur et utiliser un outil comme ProcessHacker pour inspecter la pile du thread. Étant donné que la charge utile rebascule vers la fibre de contrôle avant de dormir, la pile de la fibre de charge utile reste cachée la plupart du temps. Vous verrez dans la sortie comment les deux fibres sont programmées consécutivement selon la logique déjà commentée.
Le code est commenté pour montrer comment utiliser, créer et planifier des fibres. Vous remarquerez que le chargeur et la charge utile fournis en exemple sont « bloqués » dans une boucle infinie, ce qui permet de basculer indéfiniment entre les fibres et de continuer l'exécution.
Si une charge utile différente doit être testée, modifiez simplement le chemin situé à la ligne 32 du fichier src::main.rs du chargeur. Dans ce cas, la nouvelle dll doit exporter une fonction run(PVOID) qui recevra comme paramètre d'entrée l'adresse de la fibre de contrôle. Cette fonction doit rebasculer vers la fibre de contrôle pour appeler la fonction Sleep, bien que vous puissiez modifier ce comportement à volonté pour répondre à vos besoins.
Une autre façon de tester cet outil avec une charge utile aléatoire est d'effectuer un hooking IAT pour rediriger tout appel à la fonction Sleep (ou à toute autre fonction importée) effectué par la charge utile vers une fonction située dans le chargeur, permettant de rebasculer vers la fibre de contrôle lorsque cet appel se produit. À vous de voir.
Dans les captures d'écran suivantes, nous pouvons voir comment la pile du thread actuel passe d'une région mémoire privée à une autre lorsque nous basculons les fibres :
