Пример Sleep-обфуска­ции с помощью Ekko

Sleep обфуска­ция пример Ekko

Sleep-обфускация — это одна из тех хитростей, которую хакеры используют, чтобы уклониться от обнаружения. Прос­той и самый наг­лядный пример для демонс­тра­ции Sleep-обфуска­ции — это Ekko. У него есть и более прод­винутый вари­ант, но он не столь наг­лядный, и разоб­рать­ся с ним будет слож­нее. Давайте лучше рассмотрим Ekko.

Еще по теме: Имитация атаки с помощью Caldera MITRE

Пример Sleep-обфуска­ции с помощью Ekko

Ekko поз­воля­ет, как нам и тре­бует­ся, изме­нить раз­решение памяти с помощью фун­кции VirtualProtect(), а зашиф­ровать пей­лоад через SystemFunction032. SystemFunction032 — это недоку­мен­тирован­ная фун­кция Windows, впро­чем, работа­ет она донель­зя прос­то: мы переда­ем ей блок дан­ных, а она его шиф­рует. В осно­ве фун­кции лежит XOR. Есть еще SystemFunction033() — ее механизм тот же.

Па­рамет­ров, как видишь, все­го два:

  • data — сюда пада­ет адрес струк­туры RC4_CONTEXT, которая содер­жит дан­ные для шиф­рования или рас­шифров­ки;
  • key — сюда пада­ет адрес клю­ча, который мож­но исполь­зовать для рас­шифров­ки либо шиф­рования.

При­меры кода для шиф­рования с исполь­зовани­ем этих фун­кций вы най­дете в бло­ге Осан­ды Джай­ятис­сы.

У Ekko же все­го одна‑единс­твен­ная фун­кция — EkkoObf(). Она при­нима­ет лишь один DWORD-параметр — вре­мя, на которое наш исполня­емый файл уснет. Спря­чет­ся все адресное прос­транс­тво, содер­жащее код исполня­емо­го фай­ла.

Сна­чала ини­циали­зиру­ется ключ, с помощью которо­го будет про­исхо­дить шиф­рование.

Так как у нас Sleep-обфуска­ция, нуж­но соз­дать некото­рое событие и тай­мер, который поз­волит «выс­тре­лить» в опре­делен­ный момент и спря­тать либо вер­нуть наружу нашу наг­рузку. Для это­го Ekko исполь­зует стан­дар­тные фун­кции CreateEventW() и CreateTimerQueue().

Фун­кция CreateTimerQueue() поз­воля­ет соз­дать оче­редь тай­меров, которые будут вызывать­ся друг за дру­гом. Эта цепоч­ка сыг­рает свою клю­чевую роль чуть поз­же.

Сле­дующим шагом Ekko получа­ет ука­зате­ли на адре­са ранее опи­сан­ной фун­кции SystemFunction032() и новой для нас NtContinue(). Фун­кция NtContinue() так­же не докумен­тирова­на. Ее пред­назна­чение чуть слож­нее — она при­нима­ет спе­циаль­ную струк­туру CONTEXT, которая содер­жит зна­чения регис­тров. Переда­вая эту струк­туру в фун­кцию, мы можем возоб­новить выпол­нение текуще­го потока с ука­зан­ными в струк­туре CONTEXT изме­нени­ями в регис­трах.

Нап­ример, зна­чение регис­тра EAX рав­но 10. Переда­ем в фун­кцию NtContinue() струк­туру CONTEXT со зна­чени­ем EAX 20, и EAX в нашем потоке ста­новит­ся равен 20.

Имен­но на этих фун­кци­ях и стро­ится ROP-цепоч­ка. Что­бы шиф­ровать весь наш файл в памяти, про­исхо­дит получе­ние его базово­го адре­са заг­рузки через GetModuleHandle().

На­конец, сама ROP-цепоч­ка выг­лядит так.

Здесь сна­чала соз­даем тай­мер, который сра­зу же запус­кает­ся и получа­ет кон­текст текуще­го потока. Кон­текст — это как раз та струк­тура CONTEXT со зна­чени­ями регис­тров. Пос­ле чего копиру­ем этот кон­текст во все перемен­ные, которые будут содер­жать изме­нения.

Да­лее запол­няем в каж­дой струк­туре эле­мен­ты так, что­бы они вызыва­ли нуж­ные фун­кции и ROP-цепоч­ка кор­рек­тно отра­баты­вала. В нашем слу­чае струк­туры запол­няют­ся так, что­бы шел вызов в такой пос­ледова­тель­нос­ти: VirtualProtect() → изме­нение с RWX на RW → SystemFunction032() → шиф­рование → спим столь­ко, сколь­ко ука­зано в фун­кции → SystemFunction032() → рас­шифров­ка → VirtualProtect() → изме­нение с RW на RWX.

Для вызова ROP-цепоч­ки регис­три­руют­ся тай­меры, которые по исте­чении вре­мени вызыва­ют фун­кцию NtContinue() со струк­турами CONTEXT.

Че­рез 100 мил­лисекунд выс­тре­лива­ет пер­вый гад­жет, память с нашим фай­лом ста­новит­ся RW; через 200 мил­лисекунд все в памяти шиф­рует­ся; через 300 ожи­даем событие, которое перей­дет в сиг­наль­ное сос­тояние через ука­зан­ное фун­кци­ей вре­мя для сна; через 400 про­изой­дет рас­шифров­ка, а через полови­ну секун­ды память вновь ста­нет RWX.

Ло­гич­ный воп­рос — каким обра­зом сис­тема вызыва­ет фун­кции, если мы как бы шиф­руем всю память? Здесь идет шиф­рование толь­ко непос­редс­твен­но кода прог­раммы, смап­ленной в память. При этом DLL, в которых находит­ся реали­зация фун­кций NtContinue(), SystemFunction032() и так далее, не шиф­рует­ся. Поэто­му все успешно исполня­ется, ведь мы регис­три­руем кол­бэки, ука­зывая адре­са этих фун­кций. Сис­тема эти адре­са запоми­нает, и они не под­верга­ются шиф­рованию при работе пей­лоада (так как они находят­ся за пре­дела­ми ImageBaseAddr + ImageSize). Поэто­му все отлично сра­баты­вает.

Заключение

Те­перь нам понятен основной смысл работы Sleep-обфуска­ции, но Ekko — лишь один из прос­тей­ших PoC. На GitHub их мно­го раз­новид­ностей: здесь и RustChain с исполь­зовани­ем в логике хар­двер­ных брейк‑пой­нтов, и Cronos с SleepEx(), и DeathSleep. Пос­ледний метод мож­но счи­тать прод­винутой Sleep-обфуска­цией, пос­коль­ку он бук­валь­но уби­вает текущий поток (но перед этим пред­варитель­но не забыва­ет сох­ранить все регис­тры CPU для него и стек), затем спит, пос­ле чего вос­ста­нав­лива­ет дан­ные.

В общем, теперь у вас огромный прос­тор для самос­тоятель­ного изу­чения!

ПОЛЕЗНЫЕ ССЫЛКИ:

Дима (Kozhuh)

Эксперт в кибербезопасности. Работал в ведущих компаниях занимающихся аналитикой компьютерных угроз. Анонсы новых статей в Телеграме.

Добавить комментарий