MemoryModule и загрузка DLL из памяти: как меняется модель загрузки и что остаётся в телеметрии

MemoryModule — техника загрузки DLL напрямую в адресное пространство процесса без вызова LoadLibrary и без создания записи в списке загруженных модулей. Библиотека существует в памяти как набор страниц, выделенных через VirtualAlloc, но не видна стандартным средствам перечисления. Это делает её привлекательной для обхода детектов, основанных на мониторинге загрузки модулей.

Как работает стандартная загрузка DLL​


При вызове LoadLibrary или LoadLibraryEx загрузчик Windows выполняет последовательность действий:

  1. Открывает файл DLL на диске и считывает PE-заголовок.
  2. Выделяет память через NtMapViewOfSection — создаёт секцию, привязанную к файлу, и проецирует её в адресное пространство процесса.
  3. Копирует секции в адресное пространство процесса.
  4. Применяет релокации, если базовый адрес отличается от предпочтительного.
  5. Разрешает импорты: находит адреса функций в зависимых DLL.
  6. Вызывает DllMain с причиной DLL_PROCESS_ATTACH.
  7. Добавляет модуль в список InLoadOrderModuleList структуры PEB_LDR_DATA.

Ключевой момент: модуль привязан к файлу на диске через секцию. Это означает, что при запросе метаданных региона памяти (например, через NtQueryVirtualMemory с классом MemoryMappedFilenameInformation) система вернёт путь к файлу, а EnumProcessModules увидит модуль в списке.

Что делает MemoryModule иначе​


MemoryModule реализует загрузку PE-образа из буфера в памяти. Алгоритм:

  1. Парсинг PE-заголовка. Из буфера извлекаются IMAGE_DOS_HEADER, IMAGE_NT_HEADERS, таблица секций, директории импортов и релокаций.
  2. Выделение памяти. Вызывается VirtualAlloc с флагом MEM_RESERVE | MEM_COMMIT и защитой PAGE_EXECUTE_READWRITE (или поэтапно с изменением прав). Размер — SizeOfImage из optional header.
  3. Копирование секций. Каждая секция копируется из исходного буфера в выделенную область с учётом VirtualAddress и SizeOfRawData.
  4. Релокации. Если базовый адрес отличается от ImageBase, применяются записи из директории IMAGE_DIRECTORY_ENTRY_BASERELOC.
  5. Разрешение импортов. Для каждой импортируемой DLL вызывается LoadLibrary (или GetProcAddress для уже загруженных), адреса функций записываются в IAT.
  6. Вызов точки входа. DllMain вызывается с DLL_PROCESS_ATTACH.

Результат: исполняемый код DLL присутствует в памяти процесса, но модуль не зарегистрирован в PEB_LDR_DATA и не привязан к файловой секции.

Артефакты, которые остаются​


Несмотря на отсутствие записи в списке модулей, MemoryModule оставляет ряд признаков, которые защитники используют для детектирования.

Страницы памяти без привязки к файлу​


При стандартной загрузке страницы модуля имеют тип MEM_IMAGE и связаны с файловой секцией. MemoryModule использует VirtualAlloc, поэтому страницы имеют тип MEM_PRIVATE. Защитник, сканирующий память процесса, может найти регионы с правами PAGE_EXECUTE_READWRITE или PAGE_EXECUTE_READ, которые не привязаны к какому-либо файлу.

Проверка через VirtualQueryEx:

C:
MEMORY_BASIC_INFORMATION mbi;
for (LPVOID addr = NULL; VirtualQueryEx(hProcess, addr, &mbi, sizeof(mbi)); ) {
    if (mbi.State == MEM_COMMIT &&
        mbi.Type == MEM_PRIVATE &&
        (mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ |
                        PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY))) {
        // Подозрительный регион: исполняемая память без файловой привязки
    }
    addr = (LPBYTE)addr + mbi.RegionSize;
}

Отсутствие в списке модулей​


Если процесс выполняет код из региона, который не отображается в EnumProcessModules и не имеет файловой привязки при запросе метаданных памяти, это аномалия. EDR-продукты сравнивают адреса исполнения (из call stack или из перехваченных вызовов) со списком известных модулей. Адрес, попадающий в MEM_PRIVATE регион, — индикатор загруженного из памяти кода.

IAT и импорты​


MemoryModule вынужден вызывать LoadLibrary и GetProcAddress для разрешения импортов. Эти вызовы видны через хуки или ETW-провайдеры. Если процесс загружает DLL, которые не соответствуют его профилю (например, ws2_32.dll в процессе, который не работает с сетью), это может быть дополнительным сигналом.

Поведенческие признаки​


  • Вызов VirtualAlloc с PAGE_EXECUTE_READWRITE на большой регион.
  • Последующий вызов VirtualProtect для смены прав на PAGE_EXECUTE_READ (паттерн «allocate → write → change to RX»).
  • Передача управления на адрес внутри MEM_PRIVATE региона (видно через call stack analysis или hardware breakpoints).

Что видит EDR и антивирус​


Современные EDR-продукты используют несколько слоёв для обнаружения MemoryModule:

Метод детектированияЧто проверяетсяОграничения
Сканирование памяти процессаMEM_PRIVATE регионы с исполняемыми правамиТребует доступа к процессу; может быть обойдено через шифрование до момента исполнения
Call stack analysisАдреса возврата, не принадлежащие известным модулямРаботает при перехвате API-вызовов или при срабатывании алерта
ETW-мониторингСобытия VirtualAlloc, VirtualProtect, LoadImageВысокий объём данных; требует корреляции
Поведенческий анализПаттерн «выделение → запись → исполнение»Возможны ложные срабатывания на JIT-компиляторах

Подробнее о том, как антивирусы и EDR обнаруживают вредоносный код в памяти, см. Как антивирусы и EDR обнаруживают вредоносный код: сигнатуры, поведение и анализ памяти.

Ограничения техники​


MemoryModule не является универсальным решением для скрытия кода:

  • Не работает с драйверами. Загрузка ядра требует подписи и проходит через NtLoadDriver.
  • Зависимость от LoadLibrary для импортов. Если DLL импортирует функции из других DLL, они всё равно загружаются стандартным путём и видны в списке модулей.
  • Нет поддержки side-by-side сборок и manifest. Если DLL требует activation context, MemoryModule не обработает это автоматически.
  • Обнаружение по паттерну памяти. Как описано выше, MEM_PRIVATE + PAGE_EXECUTE — устойчивый индикатор.
  • Не скрывает от kernel-mode детектов. Если EDR имеет драйвер, он может сканировать память любого процесса напрямую.

Практический пример: проверка региона памяти​


Ниже — упрощённый фрагмент, который демонстрирует, как защитник может найти подозрительный регион в собственном процессе (учебный сценарий):

C:
#include <windows.h>
#include <stdio.h>

void scan_for_unbacked_executable_regions(void) {
    MEMORY_BASIC_INFORMATION mbi;
    LPVOID addr = NULL;

    while (VirtualQuery(addr, &mbi, sizeof(mbi)) != 0) {
        if (mbi.State == MEM_COMMIT &&
            mbi.Type == MEM_PRIVATE &&
            (mbi.Protect & 0xF0) != 0) {  // PAGE_EXECUTE* флаги
            printf("[!] Подозрительный регион: %p, размер %llu, protect 0x%lx\n",
                   mbi.BaseAddress,
                   (unsigned long long)mbi.RegionSize,
                   mbi.Protect);
        }
        addr = (LPBYTE)addr + mbi.RegionSize;
    }
}

Флаг 0xF0 покрывает PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE и PAGE_EXECUTE_WRITECOPY.

Что делать защитнику​


Если задача — обнаружить загрузку DLL из памяти в контролируемой среде:

  1. Мониторить VirtualAlloc с исполняемыми правами. Через ETW-провайдер Microsoft-Windows-Kernel-Memory или хуки на NtAllocateVirtualMemory.
  2. Сканировать память процессов. Искать MEM_PRIVATE регионы с PAGE_EXECUTE* и проверять, содержат ли они PE-заголовок (MZ сигнатура).
  3. Анализировать call stack. При срабатывании алерта проверять, что адреса возврата принадлежат известным модулям.
  4. Коррелировать с поведением. Сочетание «выделение памяти → запись → смена прав → передача управления» — сильный индикатор.

Дополнительно о том, почему in-memory активность оставляет признаки и как их анализируют, см. Детект в памяти Windows: почему in-memory активность оставляет признаки и как их анализируют защитники.

Итоговая таблица: стандартная загрузка vs MemoryModule​


ХарактеристикаLoadLibraryMemoryModule
Файл на дискеОбязателенНе требуется
Тип памятиMEM_IMAGEMEM_PRIVATE
Привязка к файлуДа (через секцию)Нет
Видимость в EnumProcessModulesДаНет
Видимость при запросе метаданных памятиДа (путь к файлу)Нет
Обнаружение по паттерну памятиНизкая вероятностьВысокая вероятность
Поддержка manifest/SxSДаНет

Техника MemoryModule снижает поверхность обнаружения на уровне мониторинга загрузки модулей, но не устраняет артефакты в памяти процесса. Для защитника ключевой индикатор — исполняемые страницы без файловой привязки, а для атакующего — понимание, что полное скрытие от kernel-mode сканера этой техникой не достигается.

Источники​


 
Назад
Верх Низ