
PoC basado en Rust que utiliza fibras de Windows para ejecutar código en memoria de forma sigilosa, ocultando las pilas de payload del EDR al alternar entre fibras de control y de payload sin devoluciones de llamada del kernel.
Una fibra es una unidad de ejecución que debe ser programada manualmente por la aplicación en lugar de depender del mecanismo de programación basado en prioridades integrado en Windows. Las fibras a menudo se denominan hilos ligeros. Para obtener información más detallada sobre qué son y cómo funcionan las fibras, consulte la documentación oficial. Las fibras permiten tener múltiples flujos de ejecución en un solo hilo, cada uno con su propio estado de registros y pila. Por otro lado, las fibras son invisibles para el kernel, lo que las convierte en un método más sigiloso (y económico) para ejecutar código en memoria que crear nuevos hilos.
Un hilo puede crear múltiples fibras y cambiar entre ellas a voluntad llamando a la función SwitchToFiber. Antes de eso, el hilo actual debe convertirse en una fibra llamando a ConvertThreadToFiber, ya que solo una fibra puede crear otras fibras. Finalmente, para crear una fibra que, al ser programada, ejecute código en memoria (por ejemplo, después de cargar reflectivamente un PE o algún shellcode), solo se necesita llamar a CreateFiber.
La función SwitchToFiber es la parte más importante de este proceso y donde ocurre toda la magia. Esta función permite programar una fibra u otra, todo sucediendo en el espacio de usuario. Según la documentación oficial, "la función SwitchToFiber guarda la información de estado de la fibra actual y restaura el estado de la fibra especificada". Esto significa que cuando se llama a esta función, los valores de los registros y la pila se cambian del estado de la fibra actual al estado de la fibra objetivo, permitiendo "ocultar" la pila de la fibra actual una vez que se completa el proceso. Esto también permite continuar la ejecución de la fibra objetivo desde el mismo punto donde se detuvo la ejecución (de la misma manera que ocurre cuando el programador cambia entre hilos según su propia lógica de prioridad).
Y esto es exactamente lo que hace esta sencilla PoC:
run() exportada por la dll mapeada manualmente. Esta fibra se conocerá a partir de ahora como la fibra del payload.Este proceso se repite indefinidamente.
El uso de fibras puede ser ventajoso para algunos tipos de payloads (como un beacon de C2) por algunas de estas razones:
JMP o CALL del cargador apuntando a regiones de memoria no respaldadas.Dado que estamos utilizando el plugin LITCRYPT para ofuscar literales de cadena, es necesario configurar la variable de entorno LITCRYPT_ENCRYPT_KEY antes de compilar el código:
C:\Users\User\Desktop\Fiber> set LITCRYPT_ENCRYPT_KEY="yoursupersecretkey"
Después de eso, simplemente compile tanto el payload como el cargador y ejecute el último:
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
No hay mucho misterio en la ejecución de esta PoC. Todo lo que hay que hacer es ejecutar el cargador y usar cualquier herramienta como ProcessHacker para inspeccionar la pila del hilo. Dado que el payload vuelve a la fibra de control antes de dormir, la pila de la fibra del payload permanece oculta la mayor parte del tiempo. Verá en la salida cómo las dos fibras se programan consecutivamente siguiendo la lógica ya comentada.
El código está comentado para mostrar cómo usar, crear y programar fibras. Notará que tanto el cargador como el payload ofrecidos como ejemplo están "atascados" en un bucle infinito, lo que permite cambiar indefinidamente entre fibras y continuar la ejecución.
Si desea probar un payload diferente, solo modifique la ruta ubicada en la línea 32 del archivo src::main.rs del cargador. En ese caso, la nueva dll debe exportar una función run(PVOID) que recibirá como parámetro de entrada la dirección de la fibra de control. Esta función debe volver a la fibra de control para llamar a la función Sleep, aunque puede modificar este comportamiento a voluntad para adaptarlo a sus requisitos.
Otra forma de probar esta herramienta con un payload aleatorio es realizar un hooking de la IAT para redirigir cualquier llamada a la función Sleep (o cualquier otra función importada) realizada por el payload a una función ubicada en el cargador, permitiendo volver a la fibra de control cuando ocurra esta llamada. Depende de usted.
En las siguientes capturas de pantalla podemos ver cómo la pila del hilo actual se mueve de una región de memoria privada a otra a medida que cambiamos de fibras:
