LoadPE — deprecated. Встречайте HookPE!

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Приветствую! Я создатель ботнета и рата MonsterV2 и данная статья является призывом по возможности прекратить использовать LoadPE в своих крипторах и перейти на новый, более современный подход. Но обо всём по порядку.

Ещё во времена Windows XP у многих была потребность запуска исполняемого не с диска, а из памяти. К сожалению, PE загрузчик из Ntdll не экспортирует функции для загрузки PE изображения (функции с префиксом Ldr- или Ldrp-) + эти функции активно использовали незадокументированную структуру LDRP_LOAD_CONTEXT, поэтому пришлось искать костыли. Одним из старейших таких костылей является Process Hollowing или RunPE.

Process Hollowing (RunPE)

Поскольку загрузчик Windows нужных функций нам не предоставляет, пришлось изворачиваться и заставлять его самому загрузить изображение; для этого нужно было заспавнить замороженный процесс с флагом CREATE_SUSPENDED и загрузить куда-нибудь нашу полезную нагрузку, после чего для контекста главного потока заспавненного процесса патчился адрес точки входа, который хранился в регистре Rcx для x86-64 или в регистре Eax для x86-32, и поле ImageBaseAddress структуры PEB, адрес которой хранился в регистре Rdx для x86-64 или в регистре Ebx для X86-32. После всех этих махинаций мы запускали главный поток, который уже самостоятельно подгружал и запускал нашу полезную нагрузку.

Этот метод и по сей день остаётся одним из самых распространённых. Правда, на текущий момент он не работает корректно без кое-каких патчей Ntdll на Windows 11 24H2 и выше из-за некоторых сайд-эффектов внутри Ntdll и NtOsKrnl, которые проверют атрибуты памяти и ожидают установленный MEM_IMAGE тип у региона памяти, куда загружен исполняемый модуль (подробнее читайте тут). Поэтому в скором времени модификации RunPE по типу Process Doppelgänging, Process Ghosting, etc, которые работают по той же схеме: спавнят замороженный процесс, что-то в нём патчат и запускают, могут стать более популярными, так как многие из них грузят полезную нагрузку именно в исполняемую секцию.

RunPE и его модификации очень удобные для всяких лоадеров, поскольку они грузят и запускают полезную нагрузку в отдельном процессе, где она не может повлиять на работу резидентного модуля (причём 64-битный процесс мог спокойно запускать как 32-, так и 64-битный пейлоды). Но для крипторов такой вариант не подходил, поскольку эти сопутствующие действия по типу спавна замороженного процесса или использование транзакционного API (Process Doppelgänging) часто служили триггером для AV. Поэтому крипторы решили: а давайте-ка мы своими руками возьмём да и имплементируем всё то, что делал PE загрузчик из Ntdll. Так появился LoadPE.

Manual Map и LoadPE


Собственно LoadPE это ручная загрузка исполняемого файла, то есть мы копируем исполняемый образ, парсим и обрабатываем PE заголовки, после чего запускаем. Manual Map это то же самое, но шелл-кодом и по отношению к удалённому процессу, чаще всего применяется в читах и подразумевает загрузку именно DLL, в то время как LoadPE чаще всего подразумевает загрузку EXE, хотя различия между EXE и DLL минимальные.

Идея LoadPE была хорошей: иметь полный контроль над загружаемым образом. Ну не сказка ли? Но тут всплыли проблемы: LoadPE не может нормально запускать изображения с эксепшенами и статической инициализацией TLS. Ну то есть: мы можем обработать релоки, импорты и запустить TLS колбеки, но мы не сможем обработать исключения, так как Ntdll не экспортирует функции RtlInsertInvertedFunctionTable, которую PE загрузчик использует для внесения нового элемента в таблицу исключений LdrpInvertedFunctionTable, поэтому нам остаётся два варианта: искать нужную функцию в памяти Ntdll, сигнатура которой отличается от версии к версии, или вручную реализовать то, что она делает, но для этого придётся также рыться в памяти Ntdll в поисках LdrpInvertedFunctionTable. С функцией LdrpHandleTlsData та же петрушка: она не экспортируется и обращается к внутренним переменным Ntdll по типу LdrpTlsList и LdrpActiveThreadCount :)

Одна из самых полных имплементаций LoadPE, которую я видел, это китайский MemoryModulePP: https://github.com/bb107/MemoryModulePP. Он там даже частично поддерживает ручную загрузку дотнетов без CLR hosting.

Но увы, многим крипторам до такого далеко, и у них возникают проблемы даже при обработке импортов по ординалам. Поэтому я представляю вашему вниманию PE загрузчик нового поколения — HookPE!

HookPE (LWE)


Я точно не первый, кто пишет об этом методе. Как минимум тут до меня об этом писали статью с названием "LoadLibrary Reloaded": https://xss.is/threads/120096

Также мне подсказали, что первое публичное упоминание этой техники датируется аж 2011 (!) годом, а оригинальная реализация называется LWE и принадлежит пользователю Indy. За ссылки спасибо пользователю 8800: https://wasm.in/threads/pe-loader-i-tls.32411/ https://wasm.in/threads/zagruzka-dll-iz-pamjati-redux.30831/

(Осторожно: ASM) https://github.com/YHVHvx/indy_vx_sources/tree/master/indy-vx/Bin/Ldr https://github.com/YHVHvx/indy_vx_sources/tree/master/indy-vx/Bin/LdrExts https://github.com/YHVHvx/indy_vx_sources/tree/master/indy-vx/Bin/LdrUpd

Собственно, техника уже достаточно подробно описана в ссылках выше: мы вызываем LdrLoadDll и на этапе (внутри функции LdrpLoadKnownDll), когда он будет искать DLL среди KnownDlls(32), подменяем секцию, чтоб PE загрузчик сам сделал за нас всю работу, но при этом без необходимости создавать отдельный процесс. На схеме это будет примерно так:

LdrLoadDll -> LdrpLoadDll -> LdrpLoadDllInternal -> LdrpFindOrPrepareLoadingModule -> LdrpLoadKnownDll

Вариант из статьи содержит имплементацию только под x64, а также не работает на Windows 11 24H2, так как не грузит пейлод в исполняемую секцию. Прикладываю исправленный вариант с поддержкой 32-битных систем и Windows 11 24H2 специально для вас! Сама реализация почти ничем не отличается, для поддержки Windows 11 24H2 пейлод грузится в исполняемую секцию посредством Module Overloading (берётся фейковая DLL размером больше или равная нашему пейлоду, мапится с диска как SEC_IMAGE, после чего в уже спампленное изображение грузится наш пейлод со сменой защиты секций), а для поддержки 32-битных систем я поправил регистры в VEH-обработчике.

Я не просто так упомянул Manual Map в статье, потому что реализация HookPE спокойно влезет в шелл-код. Также HookPE гораздо стабильнее LoadPE: все сложные моменты PE загрузчик парсит и обрабатывает сам. Ну и HookPE не создаёт никаких процессов, как это делал RunPE и его модификации.

У HookPE есть и несколько ограничений:

  1. если по адресам хукаемых Nt-функций будет трамплин от детура, то загрузчик, вероятно, сломается;
  2. он не может грузить UWP (AppX) приложения (какие-то вообще не хотят грузится, какие-то падают при запуске);
  3. он нормально грузит .NET, но не запускает, так как я поставил специальную заглушку:
C++: Скопировать в буфер обмена
Код:
if (peconv::is_dot_net(Image, ImageSize)) {
cerr << "[-] TODO: .NET files can be loaded, but cannot be started!" << endl;
return false;
}


А поставил я её по той причине, что для того, чтоб запустить .NET приложение, нужно проинициализировать CLR, который будет пытаться себя прочитать с диска, и вам нужно будет хукать функции (можете посмотреть на пример реализация для LoadPE в MemoryModulePP). Вывод простой: не заморачивайтесь и грузите .NET CLR хостингом, я вот нигде ещё не видел ручной загрузчик для Дотнета с поддержкой всех версий.

Заключение
Спасибо всем за прочтение статьи! Надеюсь, что данная статья поможет вам отказаться от LoadPE в пользу HookPE. Я прикладываю ссылку на свою демонстрационную реализацию HookPE, вы можете изменять её как вам вздумается. Также учтите, что моя реализация использует CMake как систему сборки, чтоб вы могли потестировать на разных компиляторах.

Готовый архив с исходным кодом(Если ссылка не доступна, архив в канале MonsterV2 в telegram): https://send.exploit.in/download/2ba6832d1c62cc24/#2_yLGlIM70lwI0FsF8oONA
Пароль от архива: MonsterV2
Контакты / Contacts
Форумы:
 
Сверху Снизу