Process Hollowing (RunPE) на Windows 11 24H2

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Перевод статьи западного ресёрчера Hasherezade: https://hshrzd.wordpress.com/2025/01/27/process-hollowing-on-windows-11-24h2/. Я также добавил несколько уточнений и немного изменил некоторые формулировки на, по моему мнению, более точные. Приятного чтения!
Несколько пояснений:
¹ Hasherezade называет "имплантом" загружаемый через Process Hollowing (или его альтернативы) PE файл. По сути имплант — полезная нагрузка.
² Тут имеется в виду — RunPE.
Нажмите, чтобы раскрыть...
Process Hollowing (aka RunPE) — это, вероятно, самая старая и популярная техника запуска исполняемого файла Windows из памяти под прикрытием безвредного процесса. Она используется в различных загрузчиках PE, PoC и наступательных инструментах. Она также использовалась для демонстрации возможностей моей библиотеки LibPeConv. 13 октября 2024 года в репозитории LibPeConv на Github была открыта issue, в которой было продемонстрировано, что RunPE больше не работает на последней версии Windows 11 24H2. Этот релиз Windows был опубликован 1 октября 2024 года, и, несмотря на различные проблемы с обновлениями, он постепенно набирает популярность. В поисках решения было установлено, что многие люди сталкивались с той же проблемой с различными реализациями RunPE, то есть это была проблема самой техники. Также в комментариях той issue были представлены решения проблемы, но они боролись лишь с симптомами, а не с корнем проблемы, поэтому мною было принято решение исследовать её глубже. В этом коротком блоге я описываю свои выводы в надежде, что это поможет другим людям, столкнувшимся с той же проблемой.

Наблюдаемая ошибка: 0xc0000141 (STATUS_INVALID_ADDRESS)


После того, как PE был имплантирован¹ в недавно созданный приостановленный процесс, мы возобновляем главный поток, который после подмены адресов сам загрузит наш PE через обычный загрузчик Windows. Однако, когда мы возобновляем 64-битный процесс в Windows 11 24H2, загрузка прерывается с ошибкой STATUS_INVALID_ADDRESS.
00.jpg


Корень проблемы


Эта проблема возникает из-за изменений в коде 64-битного PE загрузчика Windows.

Реализация Run PE включает запись полезной нагрузки в недавно выделенную память. В зависимости от варианта техники, она может быть реализована двумя способами:
  1. Анмапинг отображения исходного PE, выделение памяти по тому же адресу и запись туда импланта¹;
  2. Выделение новой области памяти, запись туда импланта¹, затем установка новой области в качестве базового адреса в структуре PEB.
В обоих случаях новый PE оказывается в MEM_PRIVATE памяти, в отличие от обычно отображаемого PE, который будет сохранён как образ (MEM_IMAGE). Это будет иметь важное значение в дальнейшем.

Windows 11 24H2 добавила нативную поддержку Hotpatching (подробности см. здесь). Это вызвало некоторые изменения при инициализации процесса: например, была добавлена новая функция RtlpInsertOrRemoveScpCfgFunctionTable (см. в разделе «extras»). Стек её вызова выглядит примерно так:
LdrInitializeThunk -> LdrpInitialize -> LdrpInitializeInternal -> _LdrpInitialize -> LdrpInitializeProcess -> LdrpProcessMappedModule -> RtlpInsertOrRemoveScpCfgFunctionTable -> NtQueryVirtualMemory

Функция NtQueryVirtualMemory предназначена для получения различных свойств региона памяти. Она вызывается с новым аргументом MemoryImageExtensionInformation, который может использоваться только для испоняемых файлов (MEM_IMAGE). Поскольку имплантированный¹ PE загружен в MEM_PRIVATE регион, то системный вызов завершается ошибкой STATUS_INVALID_ADDRESS.

Видео с демонстрацией: . После этого загрузка прерывается через вызов NtRaiseHardError

Решение


Есть два способа, с помощью которых мы можем решить эту проблему:
  1. Использовать альтернативную технику, которая сохраняет имплант¹ как MEM_IMAGE вместо MEM_PRIVATE;
  2. Пропатчить NTDLL, чтобы обойти проверку.

Альтернативные техники


Хотя RunPE по-прежнему остаётся самой известной и популярной техникой запуска исполняемого файла из памяти в новом процессе, существует некоторое множество альтернатив, с помощью которых мы можем сопоставить наш имплант¹ как MEM_IMAGE вместо MEM_PRIVATE.

Есть группа техник, которые сначала создают секцию (используя NtCreateSection), а затем вручную создают процесс и поток из неё, используя незадокументированные системные вызовы NtCreateProcessEx и NtCreateThreadEx:
  1. Process Doppelgänging (PoC)
  2. Process Ghosting (PoC)
  3. Process Herpaderping (PoC)
Однако эта группа методов не так удобна в использовании, как классический RunPE. Она подразумевает ручное заполнение и копирование в новый процесс структуры RTL_USER_PROCESS_PARAMETERS. Другая проблема заключается в том, что процесс будет отличаться от нормально созданного, поскольку он создаётся из безымянного модуля (GetProcessImageFileName возвращает пустую строку — в случае RunPE этого не происходит). Поэтому, хотя они и являются приятным дополнением к арсеналу методов, они не являются идеальной заменой классическому.

Со временем появилось больше возможностей для запуска полезной нагрузки из памяти в новом процессе. Process Doppelgänging и Process Ghosting вдохновили гибридные методы, которые по своей реализации ближе к Process Hollowing, но при этом отображают PE как MEM_IMAGE:
  1. Transacted Hollowing (PoC)
  2. Ghostly Hollowing (PoC)
  3. Herpaderply Hollowing (PoC)
В случае этих методов GetProcessImageFileName возвращает путь до целевого процесса, и сам процесс больше напоминает нормально созданный. Полезная нагрузка отображается как неименованный MEM_IMAGE.

Позже я придумала ещё один вариант загрузчика, который отображал полезную нагрузку как именованный MEM_IMAGE, делая его еще более похожим на законно загруженный PE. Подробности реализации и сравнение с другими методами можно найти в репозитории:
  1. Process Overwriting [PoC], [FAQ]
Согласно моим последним тестам, Transacted/Ghostly Hollowing, а также Process Overwriting успешно загрузили PE в Windows 11 24H2 без необходимости дополнительных изменений или исправлений.

Демонстрация (Process Overwriting в Windows 11 24H2):

Патчинг NTDLL


Если по какой-либо причине мы настаиваем на использовании оригинального RunPE и запускаем нашу полезную нагрузку из MEM_PRIVATE, то это всё ещё реализуемо! Однако для этого потребуется пропатчить функцию, вызывающую ошибку (NtQueryVirtualMemory). Конечно, мы хотим, чтобы патчинг оказал минимальное влияние на остальную часть рантайма, поэтому он должно фильтровать только один конкретный случай, когда мы делаем запрос для конкретной области памяти, содержащей нашу полезную нагрузку.

Сначала мы проверяем, будет ли запущена наша полезная нагрузка на Windows 11 24H2 или выше, поскольку в более старых версиях этой проблемы нет. Кроме того, этот патч нужен только для 64-битных процессов.

Функциональность патча можно описать следующим псевдокодом:
  1. if MEMORY_INFORMATION_CLASS != MemoryImageExtensionInformation -> вызвать оригинальный NtQueryVirtualMemory
  2. if ImageBase != implant_ptr¹ -> вызвать оригинальный NtQueryVirtualMemory
  3. в противном случае — вернуться с безобидной ошибкой: STATUS_NOT_SUPPORTED
Полную реализацию патча можно найти здесь:
  1. https://github.com/hasherezade/libpeconv/blob/master/run_pe/patch_ntdll.cpp#L91
В результате загрузка нашего импланта¹ не будет прервана, и мы сможем наслаждаться Process Hollowing в Windows 11 24H2!

Наблюдаемая ошибка: 0xC00004AC (STATUS_PATCH_CONFLICT)


Тем не менее, даже после того, как мы решили первую проблему с помощью патча NtQueryVirtualMemory, на некоторых системах может возникнуть другая ошибка с другим кодом. На этот раз это STATUS_PATCH_CONFLICT.

Мы столкнёмся с ней в Windows 11 24H2 с включеной проверкой целостности памяти:
111.png


Корень проблемы


  1. При инициплизации импланта¹ вызывается LdrpQueryCurrentPatch (адрес выделен красным):
433.png


  1. Внутри этой функции системный вызов NtManageHotPatch возвращает ошибку STATUS_CONFLICTING_ADDRESSES:
68747470733a2f2f687368727a642e776f726470726573732e636f6d2f77702d636f6e74656e742f75706c6f616473...png


Это также приводит к прекращению загрузки с наблюдаемой ошибкой.

Решение


Как и в предыдущем случае, альтернативные методы являются наилучшим вариантом — они работают из коробки, без применения каких-либо исправлений. Однако, если мы хотим придерживаться классического Process Injection², то нам необходим ещё один патч для NTDLL: на этот раз к функции NtManageHotPatch. На сей раз я решила вообще удалить функцию. Вызов немедленно завершается с безобидной ошибкой: STATUS_NOT_SUPPORTED.

Кстати, тут нам нужен патч как для 32-, так и для 64-битных приложений.

32-бита: https://github.com/hasherezade/libpeconv/blob/master/run_pe/patch_ntdll.cpp#L4

64-бита: https://github.com/hasherezade/libpeconv/blob/master/run_pe/patch_ntdll.cpp#L43

Функция NtManageHotPatch используется с ранних стадий инициализации процесса. Вот почему лучше всего патчить её сразу после создания процесса. Если мы сделаем это позже, мы должны убедиться, что кэш инструкций был очищен (FlushInstructionCache), в противном случае вместо него может быть выполнена кэшированная версия функции.

После этого Process Hollowing должен работать, даже если включена проверка целостности памяти!

Полный PoC


https://github.com/hasherezade/libpeconv/tree/master/run_pe
 
Сверху Снизу