Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
Инструменты/GitHubGitHub/rwxstoned/ldrshuffle
Анализ КодаЭксплуатацияЛатеральное перемещениеШелл-кодПост-эксплуатацияТестирование на ПроникновениеRed TeamingРазработка Полезной НагрузкиЭксплуатация Бинарных Файлов
GitHubrwxstoned/ldrshuffle

LdrShuffle

Техника выполнения/внедрения кода с использованием манипуляции структурой модулей DLL в PEB

2894411 год назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться
Репозиторий

LdrShuffle

Скрытное выполнение кода путем изменения EntryPoint загруженных модулей во время выполнения.

Краткое описание

Процессы Windows имеют различные модули, загруженные во время выполнения. Каждый из этих модулей имеет определенную функцию DllMain(), которая будет вызвана при создании/уничтожении процесса или потока (четыре возможных сценария).

Чтобы правильно вызывать эти функции в течение жизненного цикла процесса, функции загрузчика Windows (ntdll!Ldrp*) обращаются к списку записей, содержащих ключевые параметры (включая поле EntryPoint) для каждого модуля.

Перезаписывая этот EntryPoint для DLL, мы гарантируем, что выполнение кода будет перенаправлено в нужное нам место.

Варианты использования

Это можно использовать как примитив для выполнения кода, так и для проксирования API, например, для выполнения определенных API с не подозрительным стеком вызовов, поскольку они будут вызваны легитимными функциями Windows.

Это также можно использовать для запуска выполнения в удаленном процессе, если атакующий имеет возможность читать и записывать память в целевом процессе. Аналогично Threadless Injection, это позволяет выполнять код в процессе без вызова классических API, связанных с выполнением (CreateRemoteThread, QueueUserAPC).

Проблемы

Загрузка/выгрузка модулей в процессе Windows — это сложная тема, которая сопряжена с множеством проблем, потенциальной нестабильностью, состояниями гонки и сбоями. Например, хорошо известное препятствие, связанное с выполнением кода в рамках функции DllMain(), заключается в том, что действует блокировка загрузчика, и мы выполняемся в потоке, который еще не полностью настроен или находится в процессе завершения.

Поэтому я постарался должным образом задокументировать, что возможно, а что нет. Например, хотя большинство обычных вызовов API можно выполнить, запуск полноценного beacon требует определенных условий в отдельном процессе, чтобы избежать взаимоблокировок, вызванных функциями, используемыми в wininet.dll или winhttp.dll.

Реализация

Повторение основ загрузки DLL в Windows

Каждый процесс поддерживает список структур _LDR_DATA_TABLE_ENTRY во время выполнения. Эти структуры содержат множество деталей, относящихся к DLL, таких как ее EntryPoint (который мы будем перезаписывать), имя, некоторые хэши, временные метки, различные флаги и т. д. Некоторые из этих структур документированы, некоторые — нет.

Их можно просмотреть с помощью этой команды WinDbg:

dt nt!_LDR_DATA_TABLE_ENTRY 0xdeadbeef

root@kitploit:~
0:006> dt nt!_LDR_DATA_TABLE_ENTRY 0x18d1c4838c0
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485de0 - 0x0000018d`1c4832b0 ]
   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485df0 - 0x0000018d`1c4832c0 ]
   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x0000018d`1c4832d0 - 0x0000018d`1c482c80 ]
   +0x030 DllBase          : 0x00007ffe`e87e0000 Void
   +0x038 EntryPoint       : 0x00007ffe`e8838d00 Void
   +0x040 SizeOfImage      : 0x2fe000
   +0x048 FullDllName      : _UNICODE_STRING "C:\WINDOWS\System32\KERNELBASE.dll"
   +0x058 BaseDllName      : _UNICODE_STRING "KERNELBASE.dll"
   +0x068 FlagGroup        : [4]  "???"
   +0x068 Flags            : 0x8a2cc
   +0x068 PackagedBinary   : 0y0
   +0x068 MarkedForRemoval : 0y0
   +0x068 ImageDll         : 0y1
(...)
   +0x0e0 MappingInfoIndexNode : _RTL_BALANCED_NODE
   +0x0f8 OriginalBase     : 0x00007ffe`e87e0000
   +0x100 LoadTime         : _LARGE_INTEGER 0x01db5f86`2fa735fc
   +0x108 BaseNameHashValue : 0x235bec4
   +0x10c LoadReason       : 0 ( LoadReasonStaticDependency )
   +0x110 ImplicitPathOptions : 0x4000
   +0x114 ReferenceCount   : 1
   +0x118 DependentLoadFlags : 0x800
   +0x11c SigningLevel     : 0 ''

Адрес структуры можно получить, обойдя дважды связанную структуру, на которую ссылается PEB процесса в структуре PEB_LDR_DATA.

dt nt!_PEB_LDR_DATA 0xb4b4c3c3

Обратите внимание на флаг DontCallForThreads. Как следует из названия, если этот флаг установлен, ОС НЕ будет вызывать DllMain() этого модуля для событий потока (т. е. DLL_THREAD_ATTACH или DLL_THREAD_DETACH).

При создании DLL необходимо следовать следующему шаблону, чтобы корректно взаимодействовать с функциями загрузчика ОС:

root@kitploit:~
BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // дескриптор модуля DLL
    DWORD fdwReason,     // причина вызова функции
    LPVOID lpvReserved ) // зарезервировано
{
    // Выполнение действий в зависимости от причины вызова.
    switch( fdwReason ) 
    { 
        case DLL_PROCESS_ATTACH:
         // Инициализация один раз для каждого нового процесса.
         // Возврат FALSE для неудачной загрузки DLL.
            break;

        case DLL_THREAD_ATTACH:
         // Инициализация, специфичная для потока.
            break;

        case DLL_THREAD_DETACH:
         // Очистка, специфичная для потока.
            break;

        case DLL_PROCESS_DETACH:
        
            if (lpvReserved != nullptr)
            {
                break; // не выполнять очистку при завершении процесса
            }
            
         // Выполнение необходимой очистки.
            break;
    }
    return TRUE;  // Успешная DLL_PROCESS_ATTACH.
}

Технические детали реализации

Настройка вызова API

Как описано выше, методика временно перезаписывает EntryPoint DLL, чтобы перенаправить выполнение. Поскольку мы не контролируем ничего, кроме перенаправления выполнения, необходимо предпринять некоторые меры для обработки того, что мы хотим запустить, с какими аргументами и как получить возвращаемое значение.

Это делается путем определения структуры DATA_T в куче, таким образом, чтобы она оставалась доступной на протяжении всех шагов.

Эта структура определена следующим образом:

root@kitploit:~
typedef struct _DATA_T {
    // Манипуляции со структурами LDR
    ULONG_PTR   runner;             // вредоносная точка входа для выполнения
    ULONG_PTR   bakOriginalBase;    // резервная копия перезаписанного OriginalBase
    ULONG_PTR   bakEntryPoint;      // резервная копия перезаписанного EntryPoint
    HANDLE      event;              // событие, сигнализирующее о выполнении Runner
    // вызов функции
    ULONG_PTR   ret;                // возвращаемое значение
    DWORD       createThread;       // запустить этот вызов API в новом потоке (требуется для wininet/winhttp)
    ULONG_PTR   function;           // вызываемая Windows API
    DWORD       dwArgs;             // количество аргументов
    ULONG_PTR   args[MAX_ARGS];     // массив аргументов
} DATA_T, * PDATA_T;

Для настройки выполнения API необходимо подготовить эти поля. Значение ret будет содержать возвращаемое значение после выполнения. event используется для синхронизации, чтобы сигнализировать о завершении выполнения. Все остальные поля являются входными и определяют, какой API вызывать (function), с какими аргументами (dwArgs и args[]), адрес функции Runner(), куда перенаправляется выполнение, и резервные копии перезаписанных исходных записей DLL (bakOriginalBase и bakEntryPoint).

Поле createThread должно быть установлено в 1 для тех сложных функций API, которые не будут работать в среде DllMain() (сюда входят многие библиотеки wininet и winhttp).

Вот пример настройки вызова MessageBoxA(), как показано в PoC:

root@kitploit:~
 pDataT->dwArgs = 4;
 pDataT->runner = (ULONG_PTR)Runner;
 pDataT->function = (ULONG_PTR)MessageBoxA;
 pDataT->args[0] = (ULONG_PTR)0;
 pDataT->args[1] = (ULONG_PTR)"Hello";
 pDataT->args[2] = (ULONG_PTR)"LDRSHUFFLE";
 pDataT->args[3] = (ULONG_PTR)MB_OKCANCEL;
 pDataT->event = CreateEventA(NULL, FALSE, FALSE, "ExecEvt");

Изменение _LDR_DATA_TABLE_ENTRY

Функция UpdateLdr() отвечает за внесение соответствующего изменения в _LDR_DATA_TABLE_ENTRY целевого модуля.

RestoreLdr() восстановит эти изменения на более позднем этапе (вызывается из Runner()).

Эти функции по сути находят PEB и обходят структуры модулей, чтобы определить нужную DLL и ее поля. В заголовочных файлах я использую определения, используемые Batsec в его DarkLoadLibrary, и рекомендую читателям ознакомиться с этим проектом и соответствующим постом в блоге MDSec, чтобы воспользоваться его отличной работой по внутреннему устройству загрузки модулей в Windows.

Примечание: этот PoC загружает жертвенную DLL (SACRIFICIAL_DLL_NAME) и выполняет эти изменения в этой DLL. Однако вполне возможно изменить уже загруженную DLL. Фактически, именно такой подход используется для межпроцессного внедрения. Из соображений стабильности я рекомендую избегать изменения важных DLL, таких как ntdll или kernel32, которые также чаще проверяются решениями безопасности.

Выполнение

При создании или уничтожении потока выполнение перенаправляется в Runner(), который действует как поддельная DllMain() для модуля. Эта функция затем:

  • находит в куче структуру данных, используемую для выполнения вызовов/получения возвращаемого значения (структура PDATA_T)
  • восстанавливает PEB (RestoreLdr()) в исходное состояние
  • выполняет обычный вызов DllMain() (по сути, проксируя нормальный вызов DLL).

На этом этапе "нормальное" выполнение ОС завершено. Затем оно продолжается с нашими полезными нагрузками:

  • выполняет наш вредоносный вызов API, в соответствии со значениями и аргументами, хранящимися в структуре DATA_T. Если этот API помечен для запуска в новом потоке (createThread = 1), вызов будет выполнен в новом потоке.
  • наконец, сигнализирует событие (pDataT->event), чтобы наш основной код знал, что вызов выполнен.

Когда Windows в конечном итоге вызывает наш поддельный EntryPoint (который является адресом функции Runner()), стек вызовов выглядит следующим образом:

Стек вызовов для MessageBoxA()

Пример проксирования API

Предоставленный PoC содержит пример вызова MessageBoxA().

Он также содержит демонстрацию загрузки HTTP с использованием wininet. Определите переменную HTTP, чтобы активировать этот код.

Стек вызовов для MessageBoxA()

Пример межпроцессного внедрения

Описанные выше принципы сводятся к чтению и записи в адресное пространство процесса, чтобы вызвать выполнение кода в произвольный момент времени в будущем.

С некоторыми изменениями эти операции чтения и записи могут быть применены к удаленному процессу, чтобы перезаписать EntryPoint одной из его DLL.

Предварительным условием является возможность чтения и записи в адресное пространство процесса, то есть:

OpenProcess(PROCESS_VM_READ, FALSE, dwPid) и OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, PID)

В PoC представлен дополнительный проект под названием LdrInject, демонстрирующий, как выполнить эти шаги. Вкратце, он делает следующее:

  • в ReadPEB() он обходит список _LDR_DATA_TABLE_ENTRY в целевом процессе, чтобы определить подходящую DLL для перезаписи. Обратите внимание, что эта DLL должна иметь DontCallForThreads == 0, потому что мы хотим, чтобы Windows вызывала EntryPoint этой DLL при создании потока. Мы также не выбираем первые DLL в списке, так как они обычно чаще проверяются продуктами безопасности (ntdll.dll, kernel32.dll...).

  • детали этой DLL сохраняются в структуре данных PEBINJ_DATA.

  • шеллкод (в данном случае beacon) записывается в удаленное адресное пространство процесса с помощью InjectShellcodeToRemoteProcess()

  • два вызова WriteProcessMemory() перезаписывают EntryPoint DLL и сохраняют его резервную копию в OriginalBase, чтобы впоследствии его можно было восстановить.

В этот момент следующее событие DLL_THREAD_ATTACH или DLL_THREAD_DETACH приведет к вызову шеллкода. Это имеет определенные ограничения и оговорки в контексте запуска beacon, которые подробно описаны в следующем разделе.

Cobalt Strike Beacon

Эта методика приводит к выполнению шеллкода в очень специфической ситуации. Блокировка загрузчика активна (так как ОС считает, что находится в процессе загрузки/выгрузки DLL); поток либо создается, либо уничтожается; и, как правило, существует вероятность проблем с синхронизацией потоков, взаимоблокировок и т. д.

Во время тестирования были замечены две проблемы:

  • запуск типичного Cobalt Strike beacon приводил к взаимоблокировке при использовании API в wininet.dll или winhttp.dll.

  • запуск при уничтожении потока вызывает проблемы со стабильностью, поскольку мы выполняемся в потоке, который находится в процессе уничтожения.

Чтобы повысить стабильность, необходимо:

  • убедиться, что beacon будет запущен в новом потоке. Поэтому UDRL будет вызывать CreateThread перед вызовом обычной точки входа рефлексивной DLL Cobalt Strike.

  • запускать только в создаваемом потоке, а не в умирающем. Для этого мы гарантируем, что когда EntryPoint вызывается ОС, причиной является fdwReason == DLL_THREAD_ATTACH:

winApi.CreateThread(NULL, 4096, (LPTHREAD_START_ROUTINE)&runner, (LPVOID)&ct_data, 0, &dwThId);

вместо обычного

((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, 4, NULL);

root@kitploit:~
    ULONG_PTR __cdecl ReflectiveLoader(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
        // запускать только при событии создания потока
        
        if (fdwReason != DLL_THREAD_ATTACH) {
            return TRUE;
        }

        ...
    }

Эти два дополнительных шага были встроены в демонстрацию UDRL.

Тестирование

Список API, протестированных для LdrShuffle

VirtualAlloc

VirtualProtect

CreateThread

Sleep

MessageBoxA

InternetOpenW (требуется запуск с createThread = 1)

InternetOpenUrlA (требуется запуск с createThread = 1)

TODO

  • Продолжить тестирование большего количества API для LdrShuffle
Скачать инструмент