D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Приветствую! Я создатель ботнета и рата MonsterV2 и данная статья является призывом по возможности прекратить использовать LoadPE в своих крипторах и перейти на новый, более современный подход. Но обо всём по порядку.
Ещё во времена Windows XP у многих была потребность запуска исполняемого не с диска, а из памяти. К сожалению, PE загрузчик из Ntdll не экспортирует функции для загрузки PE изображения (функции с префиксом Ldr- или Ldrp-) + эти функции активно использовали незадокументированную структуру
Поскольку загрузчик Windows нужных функций нам не предоставляет, пришлось изворачиваться и заставлять его самому загрузить изображение; для этого нужно было заспавнить замороженный процесс с флагом
Этот метод и по сей день остаётся одним из самых распространённых. Правда, на текущий момент он не работает корректно без кое-каких патчей Ntdll на Windows 11 24H2 и выше из-за некоторых сайд-эффектов внутри Ntdll и NtOsKrnl, которые проверют атрибуты памяти и ожидают установленный
RunPE и его модификации очень удобные для всяких лоадеров, поскольку они грузят и запускают полезную нагрузку в отдельном процессе, где она не может повлиять на работу резидентного модуля (причём 64-битный процесс мог спокойно запускать как 32-, так и 64-битный пейлоды). Но для крипторов такой вариант не подходил, поскольку эти сопутствующие действия по типу спавна замороженного процесса или использование транзакционного API (Process Doppelgänging) часто служили триггером для AV. Поэтому крипторы решили: а давайте-ка мы своими руками возьмём да и имплементируем всё то, что делал PE загрузчик из Ntdll. Так появился LoadPE.
Собственно LoadPE это ручная загрузка исполняемого файла, то есть мы копируем исполняемый образ, парсим и обрабатываем PE заголовки, после чего запускаем. Manual Map это то же самое, но шелл-кодом и по отношению к удалённому процессу, чаще всего применяется в читах и подразумевает загрузку именно DLL, в то время как LoadPE чаще всего подразумевает загрузку EXE, хотя различия между EXE и DLL минимальные.
Идея LoadPE была хорошей: иметь полный контроль над загружаемым образом. Ну не сказка ли? Но тут всплыли проблемы: LoadPE не может нормально запускать изображения с эксепшенами и статической инициализацией TLS. Ну то есть: мы можем обработать релоки, импорты и запустить TLS колбеки, но мы не сможем обработать исключения, так как Ntdll не экспортирует функции
Одна из самых полных имплементаций LoadPE, которую я видел, это китайский MemoryModulePP: https://github.com/bb107/MemoryModulePP. Он там даже частично поддерживает ручную загрузку дотнетов без CLR hosting.
Но увы, многим крипторам до такого далеко, и у них возникают проблемы даже при обработке импортов по ординалам. Поэтому я представляю вашему вниманию PE загрузчик нового поколения — HookPE!
Я точно не первый, кто пишет об этом методе. Как минимум тут до меня об этом писали статью с названием "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
Собственно, техника уже достаточно подробно описана в ссылках выше: мы вызываем
Вариант из статьи содержит имплементацию только под 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 есть и несколько ограничений:
А поставил я её по той причине, что для того, чтоб запустить .NET приложение, нужно проинициализировать CLR, который будет пытаться себя прочитать с диска, и вам нужно будет хукать функции (можете посмотреть на пример реализация для LoadPE в MemoryModulePP). Вывод простой: не заморачивайтесь и грузите .NET CLR хостингом, я вот нигде ещё не видел ручной загрузчик для Дотнета с поддержкой всех версий.
Готовый архив с исходным кодом(Если ссылка не доступна, архив в канале MonsterV2 в telegram): https://send.exploit.in/download/2ba6832d1c62cc24/#2_yLGlIM70lwI0FsF8oONA
Пароль от архива: MonsterV2
Ещё во времена 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 есть и несколько ограничений:
- если по адресам хукаемых Nt-функций будет трамплин от детура, то загрузчик, вероятно, сломается;
- он не может грузить UWP (AppX) приложения (какие-то вообще не хотят грузится, какие-то падают при запуске);
- он нормально грузит .NET, но не запускает, так как я поставил специальную заглушку:
Код:
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
- Tox: 78FCE948A377D5BA27AEE0E47EC27BEB537AF607C4F2DE8BF8B5C6018E27690FB691E72B1238
- Jabber: monsterv2@exploit.im
- Session: 0521bb4bb6a0cac3007e4da53e5cb1f5f0baefa2eeb439466bd6eb2a6e45b1c661
- Telegram: https://t.me/mosterv2
- Telegram channel: https://t.me/monster_update_news
Форумы: