
Thread Stack Spoofing - PoC для продвинутой техники обхода в памяти, позволяющей лучше скрывать выделение памяти под внедренный шеллкод от сканеров и аналитиков.
PoC-реализация продвинутой техники внутрипроцессного обхода, которая подделывает стек вызовов потока. Эта техника позволяет обходить правила проверки памяти потока и лучше скрывать шеллкоды при их выполнении в памяти процесса.
Это пример реализации техники Thread Stack Spoofing (подделка стека потока), направленной на обход специалистов по анализу вредоносного ПО, антивирусов и EDR, которые ищут ссылки на фреймы шеллкода в стеке вызовов проверяемого потока. Идея заключается в сокрытии ссылок на шеллкод в стеке вызовов потока, маскируя таким образом выделенную память, содержащую код вредоносного ПО.
Эта реализация вместе с моим ShellcodeFluctuation предоставляет сообществу Offensive Security примеры реализаций, чтобы мы могли не отставать от предложений коммерческих C2-продуктов и делать не хуже в наших Red Team-инструментах. 💪
Текущая реализация сильно отличается от изначально опубликованной.
Это связано с тем, что я осознал: существует гораздо более простой способ завершить обработку стека вызовов потока и скрыть связанные с шеллкодом фреймы — просто записать 0 в адрес возврата первого контролируемого нами фрейма:
void WINAPI MySleep(DWORD _dwMilliseconds)
{
[...]
auto overwrite = (PULONG_PTR)_AddressOfReturnAddress();
const auto origReturnAddress = *overwrite;
*overwrite = 0;
[...]
*overwrite = origReturnAddress;
}
Предыдущая реализация, использующая StackWalk64, доступна в этом коммите: commit c250724.
Эта реализация гораздо стабильнее и отлично работает как в Debug, так и в Release под обеими архитектурами — x64 и x86.
Вот как может выглядеть стек вызовов, когда он НЕ подделан:

А вот как — когда подделка стека включена:

Выше мы видим, что последним фреймом в нашем стеке вызовов является наш колбэк MySleep.
Можно задаться вопросом: не порождает ли это сразу новые IOC? Правила охоты могут искать потоки, чьи стеки вызовов не раскручиваются до следующих ожидаемых точек входа потока, расположенных в системных библиотеках:
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
Однако стек вызовов поддельного потока поначалу может выглядеть странно. Краткое исследование моей системы показало, что существуют и другие потоки, не раскручивающиеся до указанных выше точек входа:

На скриншоте выше показан поток немодифицированного Total Commander x64. Как видим, его стек вызовов очень похож на наш с точки зрения начальных фреймов.
Зачем нам тщательно подделывать стек вызовов, если существуют процессы, демонстрирующие схожие признаки, которые мы можем просто имитировать?
Приблизительный алгоритм:
dbghelp.dll, вызов SymInitialize.kernel32!Sleep с перенаправлением на наш колбэк.VirtualAlloc + memcpy + CreateThread. Поток должен начинаться с нашей функции runShellcode, чтобы избежать указания StartAddress потока на что-то неожиданное и аномальное (например, ntdll!RtlUserThreadStart+0x21).MySleep.0, что фактически завершает стек вызовов.::SleepEx, чтобы позволить Beacon'у спать в ожидании дальнейшей связи.Адреса возврата функций разбросаны по всей области стека потока, на которую указывает регистр RBP/EBP.
Чтобы найти их в стеке, необходимо сначала собрать указатели на фреймы, а затем разыменовать их для перезаписи:

(изображение выше взято из поста Эли Бендерски (Eli Bendersky) «Stack frame layout on x86-64» (angлийская версия))
*(PULONG_PTR)(frameAddr + sizeof(void*)) = Fake_Return_Address;
Первоначальная реализация ThreadStackSpoofer делала это в функциях walkCallStack и spoofCallStack, однако текущая реализация показывает, что эти усилия не требуются для поддержания скрытного стека вызовов.
Использование:
C:\> ThreadStackSpoofer.exe <shellcode> <spoof>
Где:
<shellcode> — путь к файлу шеллкода<spoof> — если 1 или true, включает подделку стека потока; в противном случае отключает.Пример запуска, подделывающего стек вызовов потока Beacon:
PS D:\dev2\ThreadStackSpoofer> .\x64\Release\ThreadStackSpoofer.exe .\tests\beacon64.bin 1
[.] Чтение байтов шеллкода...
[.] Перехват kernel32!Sleep...
[.] Инъекция шеллкода...
[+] Шеллкод запущен.
[>] Оригинальный адрес возврата: 0x1926747bd51. Завершение стека вызовов...
===> MySleep(5000)
[<] Восстановление оригинального адреса возврата...
[>] Оригинальный адрес возврата: 0x1926747bd51. Завершение стека вызовов...
===> MySleep(5000)
[<] Восстановление оригинального адреса возврата...
[>] Оригинальный адрес возврата: 0x1926747bd51. Завершение стека вызовов...
Посмотрите на код и его реализацию, поймите концепцию и перереализуйте её в своих собственных загрузчиках шеллкода, которые вы используете для своих Red Team-задач. Это ещё одна техника продвинутого внутрипроцессного обхода, которая повышает шансы вашей команды не быть обнаруженными антивирусами, EDR и аналитиками вредоносного ПО, изучающими ваши импланты.
Разрабатывая свой продвинутый загрузчик шеллкода, вы также можете захотеть реализовать:
BeaconEyeRW (с RX/RWX) и зашифровать их содержимое — используя технику Shellcode Fluctuation — прямо перед сном (это может обойти сканеры, такие как Moneta или pe-sieve)Как мне указали, данная техника пока не совсем соответствует названию «подделка стека». Поскольку мы лишь перезаписываем адреса возврата в стеке потока, мы не подделываем остальные области самого стека. Более того, мы делаем наш стек вызовов нераскручиваемым, что выглядит аномально, так как система не сможет корректно пройти по всей цепочке фреймов стека вызовов.