
Rust-based PoC, использующий Windows fibers для скрытого выполнения кода в памяти, скрывая стеки полезной нагрузки от EDR путем переключения между управляющим и полезным волокнами без вызовов ядра.
Fiber (волокно) — это единица выполнения, которая должна планироваться вручную приложением, а не полагаться на механизм планирования на основе приоритетов, встроенный в Windows. Fibers часто называют легковесными потоками. Для получения более подробной информации о том, что такое fibers и как они работают, обратитесь к официальной документации. Fibers позволяют иметь несколько потоков выполнения в одном потоке, каждый со своим собственным состоянием регистров и стеком. С другой стороны, fibers невидимы для ядра, что делает их более скрытным (и более дешёвым) методом выполнения кода в памяти, чем создание новых потоков.
Один поток может создать несколько fibers и переключаться между ними по желанию, вызывая функцию SwitchToFiber. Перед этим текущий поток сам должен стать fiber, вызвав ConvertThreadToFiber, поскольку только fiber может создавать другие fibers. Наконец, чтобы создать fiber, который при планировании выполняет код в памяти (например, после отражённой загрузки PE или шеллкода), достаточно вызвать CreateFiber.
Функция SwitchToFiber является наиболее важной частью этого процесса, и именно в ней происходит вся магия. Эта функция позволяет запланировать один fiber или другой, причём всё происходит в пользовательском пространстве. Согласно официальной документации, «функция SwitchToFiber сохраняет информацию о состоянии текущего fiber и восстанавливает состояние указанного fiber». Это означает, что при вызове этой функции значения регистров и стек переключаются с состояния текущего fiber на состояние целевого fiber, позволяя «скрыть» стек текущего fiber после завершения процесса. Это также позволяет продолжить выполнение целевого fiber с той же точки, где выполнение было остановлено (так же, как это происходит, когда планировщик переключается между потоками в соответствии с собственной логикой приоритетов).
И именно это делает данная простая PoC:
run(), экспортируемой вручную отображённой DLL. Этот fiber будет далее известен как полезный fiber.Этот процесс повторяется бесконечно.
Использование fibers может быть выгодно для некоторых типов полезных нагрузок (например, для C2 beacon) по следующим причинам:
JMP или CALL от загрузчика, указывающих на неразмеченные области памяти.Поскольку мы используем плагин LITCRYPT для обфускации строковых литералов, перед компиляцией кода необходимо задать переменную окружения LITCRYPT_ENCRYPT_KEY:
C:\Users\User\Desktop\Fiber> set LITCRYPT_ENCRYPT_KEY="yoursupersecretkey"
После этого просто скомпилируйте как полезную нагрузку, так и загрузчик, и запустите последний:
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
В выполнении этой PoC нет особой тайны. Всё, что нужно сделать, — запустить загрузчик и использовать любой инструмент, например ProcessHacker, для проверки стека потока. Поскольку полезная нагрузка переключается обратно на управляющий fiber перед сном, стек полезного fiber остаётся скрытым большую часть времени. Вы увидите в выводе, как два fibers последовательно планируются по уже описанной логике.
Код закомментирован, чтобы показать, как использовать, создавать и планировать fibers. Вы заметите, что и загрузчик, и полезная нагрузка, представленные в качестве примера, «застряли» в бесконечном цикле, что позволяет бесконечно переключаться между fibers и продолжать выполнение.
Если вы хотите протестировать другую полезную нагрузку, просто измените путь в строке 32 файла src::main.rs загрузчика. В этом случае новая DLL должна экспортировать функцию run(PVOID), которая будет получать в качестве входного параметра адрес управляющего fiber. Эта функция должна переключаться обратно на управляющий fiber для вызова функции Sleep, хотя вы можете изменить это поведение по своему усмотрению, чтобы соответствовать вашим требованиям.
Другой способ протестировать этот инструмент со случайной полезной нагрузкой — выполнить перехват IAT для перенаправления любого вызова функции Sleep (или любой другой импортированной функции), сделанного полезной нагрузкой, на функцию, расположенную в загрузчике, что позволит переключиться обратно на управляющий fiber при возникновении этого вызова. Решать вам.
На следующих скриншотах видно, как стек текущего потока перемещается из одной частной области памяти в другую при переключении fibers:
